Compare commits

...

61 Commits

Author SHA1 Message Date
LittleSheep
c9b71701c8 🚀 Launch 3.2.0+129 2025-08-26 01:33:57 +08:00
LittleSheep
28e98488f1 🌐 Localize new stuff 2025-08-26 01:24:58 +08:00
LittleSheep
b4d476613e 🐛 Optimzation and bug fixes 2025-08-26 01:18:54 +08:00
LittleSheep
b48a1aac44 Search messages!
♻️ Optimize messages loading, syncing
2025-08-26 01:05:30 +08:00
LittleSheep
596d212593 🐛 Fix account name localization 2025-08-26 00:17:34 +08:00
LittleSheep
54f290327e Copyable file ID 2025-08-26 00:08:50 +08:00
LittleSheep
16f248ceab 💄 Optimize file saving 2025-08-26 00:03:43 +08:00
LittleSheep
856d811187 🐛 Fix notification tap in system wide 2025-08-25 23:14:59 +08:00
LittleSheep
d07b194c04 🐛 Fixes 2025-08-25 20:44:35 +08:00
LittleSheep
2554b58be6 Show automated account in pfc 2025-08-25 20:31:43 +08:00
LittleSheep
a627b5838e 👔 Disable pin reply 2025-08-25 19:57:30 +08:00
LittleSheep
c479a9f381 Show social credits 2025-08-25 19:56:34 +08:00
LittleSheep
02057e663b Real previewing chat 2025-08-25 19:36:48 +08:00
LittleSheep
6501594100 Event dairy 2025-08-25 18:31:57 +08:00
LittleSheep
c6599edc3d 💄 Serval changes to optimize UX 2025-08-25 18:03:50 +08:00
LittleSheep
709a0620b6 Show pinned posts on realms, publishers 2025-08-25 17:09:24 +08:00
LittleSheep
f9b2a96c7c Pin post 2025-08-25 16:55:06 +08:00
LittleSheep
4dca6189cb 💄 Optimize post category subscribe loading state 2025-08-25 16:17:19 +08:00
LittleSheep
c7f5b63fe5 Subscribe to category and tags 2025-08-25 16:15:51 +08:00
LittleSheep
96c2f45c85 💄 Optimize articles page 2025-08-25 15:33:27 +08:00
LittleSheep
06f04eb3a5 🎉 Launch 3.2.0+128 2025-08-25 01:55:54 +08:00
LittleSheep
8af97e43b4 💄 Optimize articles view 2025-08-25 01:48:43 +08:00
LittleSheep
d1e8234b93 🐛 Fix bugs 2025-08-24 23:50:36 +08:00
LittleSheep
a03d6015a6 Manage secret 2025-08-24 23:46:14 +08:00
LittleSheep
246ac52d0a Custom app detail page 2025-08-24 22:33:41 +08:00
LittleSheep
abf395ff9a 🐛 Dozens of bug fixes 2025-08-24 21:49:40 +08:00
LittleSheep
4fdc8eb1d0 Feed discover and subscription 2025-08-24 13:55:06 +08:00
LittleSheep
d7dcde898c 🐛 Dozens of bug fixes 2025-08-24 02:33:02 +08:00
LittleSheep
f85484d3ed 💄 Optimize for large screen 2025-08-24 02:28:16 +08:00
LittleSheep
5060bd30c9 Rotate bot key 2025-08-24 01:49:56 +08:00
LittleSheep
3959f2260b Bot key management 2025-08-23 23:35:37 +08:00
LittleSheep
6f4f1216ad 🐛 Fix project detail 2025-08-23 17:45:08 +08:00
LittleSheep
f401ffbf81 🐛 Fix profile page 2025-08-23 17:34:01 +08:00
LittleSheep
0251697951 Show robot on profile page 2025-08-23 17:32:49 +08:00
LittleSheep
178c12b893 Bot basis 2025-08-23 17:07:42 +08:00
LittleSheep
4beda9200e Add developer projects 2025-08-23 02:56:28 +08:00
LittleSheep
7dfe411053 Add more developer pages (wip) 2025-08-23 02:19:07 +08:00
LittleSheep
1232318a5d Add feed subscription (wip) 2025-08-23 01:32:58 +08:00
LittleSheep
56f41b6c0e 🔀 Merge pull request #173 from Texas0295/v3
🐛 linux/userinfo.dart: guard Firebase calls if Firebase is uninitialized
2025-08-23 01:28:54 +08:00
Texas0295
3ea717d25a 🐛 linux/userinfo.dart: guard Firebase calls if Firebase is uninitialized 2025-08-23 00:34:56 +08:00
LittleSheep
1fe4889460 Search for stickers 2025-08-22 23:31:31 +08:00
LittleSheep
cdf2722268 Stickers order by usage 2025-08-22 22:59:41 +08:00
LittleSheep
a127b5bace Social credits 2025-08-22 19:55:26 +08:00
LittleSheep
b2097cf044 🐛 Fix chat summary failed when lastMessage is null 2025-08-22 19:00:06 +08:00
LittleSheep
701f29748d Debug set access token 2025-08-22 18:59:40 +08:00
LittleSheep
9e40ed4600 Show leveling BonusMultiplier 2025-08-22 17:58:28 +08:00
LittleSheep
c90e6fe661 Experience records & refine leveling page 2025-08-22 17:53:07 +08:00
LittleSheep
569483300d Card shuffle 2025-08-22 16:20:13 +08:00
LittleSheep
bab602d98b Shuffle post 2025-08-22 01:41:25 +08:00
LittleSheep
b4f2bb803a ⬆️ Upgrade deps 2025-08-22 00:22:06 +08:00
LittleSheep
03bfed6f46 💄 Optimize cloud file info 2025-08-22 00:17:13 +08:00
LittleSheep
f98e5a0aec Post browse by categories, tags 2025-08-21 23:21:30 +08:00
LittleSheep
3d473e2fec 🐛 Replace the push with go to view posts in creator centre in order to fix #172 2025-08-21 20:09:49 +08:00
LittleSheep
0b6efa373a 💄 Optimize realm list 2025-08-21 18:49:42 +08:00
LittleSheep
9b60e96cde 💄 Optimize post detail error page 2025-08-21 02:25:13 +08:00
LittleSheep
81cd9b2082 🐛 Fix issues when saving image to gallery without ext name 2025-08-21 02:22:21 +08:00
LittleSheep
923d5d7514 👽 Update stickers api call 2025-08-20 14:26:37 +08:00
LittleSheep
7169aff841 🚀 Launch 3.2.0+127 2025-08-18 13:28:05 +08:00
LittleSheep
fac3efb50c 💄 Add status code to userinfo alert 2025-08-18 13:19:50 +08:00
LittleSheep
e809aadaea Userinfo failed to load alert 2025-08-18 13:08:57 +08:00
LittleSheep
f33b569221 🐛 Fix realm detail 2025-08-18 11:51:44 +08:00
117 changed files with 11029 additions and 1068 deletions

View File

@@ -386,6 +386,7 @@
"postSettings": "Settings",
"postPublisherUnselected": "Publisher Unspecified",
"postType": "Post Type",
"postTypePost": "Post",
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
"postVisibility": "Post Visibility",
"postVisibilityPublic": "Public",
@@ -643,6 +644,18 @@
"enrollDeveloperHint": "Enroll one of your publishers to become a developer.",
"noPublishersToEnroll": "You don't have any publishers that can be enrolled as a developer.",
"totalCustomApps": "Total Custom Apps",
"projects": "Projects",
"noProjects": "No projects found.",
"deleteProject": "Delete Project",
"deleteProjectHint": "Are you sure you want to delete this project? This action cannot be undone.",
"createProject": "Create Project",
"editProject": "Edit Project",
"projectDetails": "Project Details",
"createBot": "Create Bot",
"bots": "Bots",
"noBots": "No bots yet.",
"deleteBotHint": "Are you sure you want to delete this bot? This action cannot be undone.",
"deleteBot": "Delete Bot",
"customApps": "Custom Apps",
"noCustomApps": "No custom apps yet.",
"createCustomApp": "Create Custom App",
@@ -850,5 +863,102 @@
"zero": "No invitation",
"one": "{} available invitation",
"other": "{} available invitations"
}
},
"failedToLoadUserInfo": "Failed to load user info",
"failedToLoadUserInfoNetwork": "It seems be network issue, you can tap the button below to try again.",
"failedToLoadUserInfoUnauthorized": "It seems your session has been logged out or not available anymore, you can still try agian to fetch the user info if you want.",
"okay": "Okay",
"postDetail": "Post Detail",
"postCount": {
"zero": "No posts",
"one": "{} post",
"other": "{} posts"
},
"mimeType": "MIME Type",
"fileSize": "File Size",
"fileHash": "File Hash",
"exifData": "EXIF Data",
"postShuffle": "Shuffle Posts",
"leveling": "Leveling",
"levelingHistory": "Leveling History",
"stellarProgram": "Stellar Program",
"socialCredits": "Social Credits",
"credits": "Credits",
"creditsStatus": "Credits Status",
"socialCreditsDescription": "Social Credit is a way for Solar Network to evaluate users. It is calculated based on their behavior and interactions. With a base score of 100, higher scores indicate a user's credibility within the community. Scores change over time to reflect a user's recent behavior. Users with higher credit ratings enjoy more benefits, while users with lower credit ratings may have some functionality restricted.",
"socialCreditsLevelPoor": "Poor",
"socialCreditsLevelNormal": "Normal",
"socialCreditsLevelGood": "Good",
"socialCreditsLevelExcellent": "Excellent",
"orderByPopularity": "Sort by popularity",
"orderByReleaseDate": "Sort by release date",
"editBot": "Edit Bot",
"botAutomatedBy": "Automated by {}",
"botDetails": "Bot Details",
"overview": "Overview",
"keys": "Keys",
"botNotFound": "Bot not found.",
"newBotKey": "New Bot Key",
"newBotKeyHint": "Enter a name for your new key. The key will be shown only once.",
"revokeBotKey": "Revoke Bot Key",
"revokeBotKeyHint": "Are you sure you want to revoke this key? This action cannot be undone and any application using this key will stop working.",
"noBotKeys": "No bot keys yet.",
"revoke": "Revoke",
"keyName": "Key Name",
"newKeyGenerated": "New Key Generated",
"copyKeyHint": "Please copy this key and store it somewhere safe. You will not be able to see it again.",
"rotateKey": "Rotate Key",
"rotateBotKey": "Rotate Bot Key",
"rotateBotKeyHint": "Are you sure you want to rotate this key? The old key will become invalid immediately. This action cannot be undone.",
"webFeedArticleCount": {
"zero": "No articles",
"one": "{} article",
"other": "{} articles"
},
"webFeedSubscribed": "The feed has been subscribed",
"webFeedUnsubscribed": "The feed has been unsubscribed",
"appDetails": "App Details",
"secrets": "Secrets",
"appNotFound": "App not found.",
"secretCopied": "Secret copied to clipboard.",
"deleteSecret": "Delete Secret",
"deleteSecretHint": "Are you sure you want to delete this secret? This action cannot be undone.",
"generateSecret": "Generate New Secret",
"createdAt": "Created at {}",
"newSecretGenerated": "New Secret Generated",
"copySecretHint": "Please copy this secret and store it somewhere safe. You will not be able to see it again.",
"expiresIn": "Expires In (seconds)",
"isOidc": "OIDC Compliant",
"pinPost": "Pin Post",
"unpinPost": "Unpin Post",
"pinnedPost": "Pinned",
"publisherPage": "Publisher Page",
"realmPage": "Realm Page",
"replyPage": "Reply Page",
"pinPostPublisherHint": "Pin this post to your publisher page",
"pinPostRealmHint": "Pin this post to the realm page",
"pinPostRealmDisabledHint": "This post doesn't belong to any realm",
"pinPostReplyHint": "Pin this post to the reply page",
"pinPostReplyDisabledHint": "This post is not a reply",
"pin": "Pin",
"unpinPostHint": "Are you sure you want to unpin this post?",
"all": "All",
"statusPresent": "Present",
"accountAutomated": "Automated",
"chatBreakClearButton": "Clear",
"chatBreak5m": "5m",
"chatBreak10m": "10m",
"chatBreak15m": "15m",
"chatBreak30m": "30m",
"chatBreakCustomMinutes": "Custom (minutes)",
"chatBreakEnterMinutes": "Enter minutes",
"errorGeneric": "Error: {}",
"searchMessages": "Search Messages",
"messagesCount": "{} messages",
"dotSeparator": "·",
"roleValidationHint": "Role must be between 0 and 100",
"searchMessagesHint": "Search messages...",
"searchLinks": "Links",
"searchAttachments": "Attachments",
"noMessagesFound": "No messages found"
}

View File

@@ -345,7 +345,7 @@
"accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。",
"unauthorized": "未授权",
"unauthorizedHint": "您未登录或会话已过期,请重新登录。",
"publisherBelongsTo": "属于",
"publisherBelongsTo": "属于 {}",
"postContent": "内容",
"postSettings": "设置",
"postPublisherUnselected": "未指定发布者",
@@ -824,5 +824,38 @@
"zero": "无邀请",
"one": "{} 个可用邀请",
"other": "{} 个可用邀请"
}
},
"failedToLoadUserInfo": "加载用户信息失败",
"failedToLoadUserInfoNetwork": "这看起来是个网络问题,你可以按下面的按钮来重试",
"failedToLoadUserInfoUnauthorized": "看来您的会话已被注销或不再可用,如果您愿意,您仍然可以再次尝试获取用户信息。",
"okay": "了解",
"postDetail": "帖子详情",
"mimeType": "类型",
"fileSize": "大小",
"fileHash": "哈希",
"exifData": "EXIF 数据",
"leveling": "等级",
"levelingHistory": "经验记录",
"stellarProgram": "恒星计划",
"socialCredits": "社会信用点",
"credits": "信用",
"socialCreditsDescription": "社会信用是 Solar Network 评价用户的一种方式。它基于用户的行为和互动来计算。以 100 分为基准,分数越高表示用户在社区中的信誉越好。分数会随着时间的推移而变化,反映用户的最新行为。信用等级高的用户可以享受到更多的福利,反之的用户部份功能可能受到限制。",
"socialCreditsLevelPoor": "糟糕",
"socialCreditsLevelNormal": "正常",
"socialCreditsLevelGood": "良好",
"socialCreditsLevelExcellent": "优秀",
"appDetails": "应用详情",
"secrets": "密钥",
"appNotFound": "应用未找到。",
"secretCopied": "密钥已复制到剪贴板。",
"deleteSecret": "删除密钥",
"deleteSecretHint": "您确定要删除此密钥吗?此操作无法撤销。",
"generateSecret": "生成新密钥",
"createdAt": "创建于 {}",
"newSecretGenerated": "已生成新密钥",
"copySecretHint": "请复制此密钥并将其存放在安全的地方。您将无法再次看到它。",
"expiresIn": "过期时间(秒)",
"isOidc": "OIDC 兼容",
"statusPresent": "至今",
"accountAutomated": "机器人"
}

View File

@@ -811,5 +811,17 @@
"filesListAdditional": {
"one": "+{} 個文件被摺疊",
"other": "+{} 個文件被摺疊"
}
},
"appDetails": "應用程式詳情",
"secrets": "密鑰",
"appNotFound": "找不到應用程式。",
"secretCopied": "密鑰已複製到剪貼簿。",
"deleteSecret": "刪除密鑰",
"deleteSecretHint": "您確定要刪除此密鑰嗎?此操作無法復原。",
"generateSecret": "產生新密鑰",
"createdAt": "建立於 {}",
"newSecretGenerated": "已產生新密鑰",
"copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。",
"expiresIn": "過期時間(秒)",
"isOidc": "OIDC 相容"
}

View File

@@ -40,6 +40,8 @@ PODS:
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- file_saver (0.0.1):
- Flutter
- Firebase/CoreOnly (12.0.0):
- FirebaseCore (~> 12.0.0)
- Firebase/Crashlytics (12.0.0):
@@ -303,6 +305,7 @@ DEPENDENCIES:
- croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
@@ -381,6 +384,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
file_saver:
:path: ".symlinks/plugins/file_saver/ios"
firebase_analytics:
:path: ".symlinks/plugins/firebase_analytics/ios"
firebase_core:
@@ -464,6 +469,7 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Firebase: 800d487043c0557d9faed71477a38d9aafb08a41
firebase_analytics: cd56fc56f75c1df30a6ff5290cd56e230996a76d
firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4

View File

@@ -68,6 +68,34 @@ class AppDatabase extends _$AppDatabase {
return (delete(chatMessages)..where((m) => m.id.equals(id))).go();
}
Future<int> getTotalMessagesForRoom(String roomId) {
return (select(chatMessages)..where((m) => m.roomId.equals(roomId))).get().then((list) => list.length);
}
Future<List<LocalChatMessage>> searchMessages(
String roomId,
String query,
) async {
var selectStatement = select(chatMessages)
..where((m) => m.roomId.equals(roomId));
if (query.isNotEmpty) {
selectStatement =
selectStatement
..where((m) => m.content.like('%${query.toLowerCase()}%'));
}
final messages =
await (selectStatement
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
.get();
return messages.map((msg) => companionToMessage(msg)).toList();
}
// Convert between Drift and model objects
ChatMessagesCompanion messageToCompanion(LocalChatMessage message) {
return ChatMessagesCompanion(

View File

@@ -8,7 +8,7 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -169,12 +169,12 @@ class IslandApp extends HookConsumerWidget {
final theme = ref.watch(themeProvider);
void handleMessage(RemoteMessage notification) {
if (notification.data['action_uri'] != null) {
var uri = notification.data['action_uri'] as String;
if (notification.data['meta']?['action_uri'] != null) {
var uri = notification.data['meta']['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
final router = ref.read(routerProvider);
router.go(notification.data['action_uri']);
router.push(notification.data['meta']['action_uri']);
} else {
// External links
launchUrlString(uri);
@@ -186,27 +186,6 @@ class IslandApp extends HookConsumerWidget {
if (!kIsWeb && Platform.isLinux) {
return null;
}
const channel = MethodChannel('dev.solsynth.solian/notifications');
Future<void> handleInitialLink() async {
final String? link = await channel.invokeMethod('initialLink');
if (link != null) {
final router = ref.read(routerProvider);
router.go(link);
}
}
if (!kIsWeb && Platform.isAndroid) {
handleInitialLink();
}
channel.setMethodCallHandler((call) async {
if (call.method == 'newLink') {
final String link = call.arguments;
final router = ref.read(routerProvider);
router.go(link);
}
});
// When the app is opened from a terminated state.
FirebaseMessaging.instance.getInitialMessage().then((message) {

View File

@@ -14,6 +14,7 @@ sealed class SnAccount with _$SnAccount {
required String nick,
required String language,
required bool isSuperuser,
required String? automatedId,
required SnAccountProfile profile,
required SnWalletSubscriptionRef? perkSubscription,
@Default([]) List<SnAccountBadge> badges,
@@ -70,6 +71,8 @@ sealed class SnAccountProfile with _$SnAccountProfile {
SnAccountBadge? activeBadge,
required int experience,
required int level,
@Default(100) double socialCredits,
@Default(0) int socialCreditsLevel,
required double levelingProgress,
required SnCloudFile? picture,
required SnCloudFile? background,
@@ -208,3 +211,37 @@ sealed class SnAuthDeviceWithChallenge with _$SnAuthDeviceWithChallenge {
factory SnAuthDeviceWithChallenge.fromJson(Map<String, dynamic> json) =>
_$SnAuthDeviceWithChallengeFromJson(json);
}
@freezed
sealed class SnExperienceRecord with _$SnExperienceRecord {
const factory SnExperienceRecord({
required String id,
required int delta,
required String reasonType,
required String reason,
@Default(1.0) double? bonusMultiplier,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnExperienceRecord;
factory SnExperienceRecord.fromJson(Map<String, dynamic> json) =>
_$SnExperienceRecordFromJson(json);
}
@freezed
sealed class SnSocialCreditRecord with _$SnSocialCreditRecord {
const factory SnSocialCreditRecord({
required String id,
required double delta,
required String reasonType,
required String reason,
required DateTime? expiredAt,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnSocialCreditRecord;
factory SnSocialCreditRecord.fromJson(Map<String, dynamic> json) =>
_$SnSocialCreditRecordFromJson(json);
}

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnAccount {
String get id; String get name; String get nick; String get language; bool get isSuperuser; SnAccountProfile get profile; SnWalletSubscriptionRef? get perkSubscription; List<SnAccountBadge> get badges; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String get name; String get nick; String get language; bool get isSuperuser; String? get automatedId; SnAccountProfile get profile; SnWalletSubscriptionRef? get perkSubscription; List<SnAccountBadge> get badges; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccount
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $SnAccountCopyWith<SnAccount> get copyWith => _$SnAccountCopyWithImpl<SnAccount>
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other.badges, badges)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.automatedId, automatedId) || other.automatedId == automatedId)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other.badges, badges)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,profile,perkSubscription,const DeepCollectionEquality().hash(badges),createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,automatedId,profile,perkSubscription,const DeepCollectionEquality().hash(badges),createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, automatedId: $automatedId, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -48,7 +48,7 @@ abstract mixin class $SnAccountCopyWith<$Res> {
factory $SnAccountCopyWith(SnAccount value, $Res Function(SnAccount) _then) = _$SnAccountCopyWithImpl;
@useResult
$Res call({
String id, String name, String nick, String language, bool isSuperuser, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String name, String nick, String language, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -65,14 +65,15 @@ class _$SnAccountCopyWithImpl<$Res>
/// Create a copy of SnAccount
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? automatedId = freezed,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable
as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable
as bool,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable
as bool,automatedId: freezed == automatedId ? _self.automatedId : automatedId // ignore: cast_nullable_to_non_nullable
as String?,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable
as SnAccountProfile,perkSubscription: freezed == perkSubscription ? _self.perkSubscription : perkSubscription // ignore: cast_nullable_to_non_nullable
as SnWalletSubscriptionRef?,badges: null == badges ? _self.badges : badges // ignore: cast_nullable_to_non_nullable
as List<SnAccountBadge>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
@@ -181,10 +182,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String nick, String language, bool isSuperuser, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String nick, String language, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnAccount() when $default != null:
return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
@@ -202,10 +203,10 @@ return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String nick, String language, bool isSuperuser, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String nick, String language, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnAccount():
return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);}
return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -219,10 +220,10 @@ return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String nick, String language, bool isSuperuser, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String nick, String language, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnAccount() when $default != null:
return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
@@ -234,7 +235,7 @@ return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,
@JsonSerializable()
class _SnAccount implements SnAccount {
const _SnAccount({required this.id, required this.name, required this.nick, required this.language, required this.isSuperuser, required this.profile, required this.perkSubscription, final List<SnAccountBadge> badges = const [], required this.createdAt, required this.updatedAt, required this.deletedAt}): _badges = badges;
const _SnAccount({required this.id, required this.name, required this.nick, required this.language, required this.isSuperuser, required this.automatedId, required this.profile, required this.perkSubscription, final List<SnAccountBadge> badges = const [], required this.createdAt, required this.updatedAt, required this.deletedAt}): _badges = badges;
factory _SnAccount.fromJson(Map<String, dynamic> json) => _$SnAccountFromJson(json);
@override final String id;
@@ -242,6 +243,7 @@ class _SnAccount implements SnAccount {
@override final String nick;
@override final String language;
@override final bool isSuperuser;
@override final String? automatedId;
@override final SnAccountProfile profile;
@override final SnWalletSubscriptionRef? perkSubscription;
final List<SnAccountBadge> _badges;
@@ -268,16 +270,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other._badges, _badges)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.automatedId, automatedId) || other.automatedId == automatedId)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other._badges, _badges)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,profile,perkSubscription,const DeepCollectionEquality().hash(_badges),createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,automatedId,profile,perkSubscription,const DeepCollectionEquality().hash(_badges),createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, automatedId: $automatedId, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -288,7 +290,7 @@ abstract mixin class _$SnAccountCopyWith<$Res> implements $SnAccountCopyWith<$Re
factory _$SnAccountCopyWith(_SnAccount value, $Res Function(_SnAccount) _then) = __$SnAccountCopyWithImpl;
@override @useResult
$Res call({
String id, String name, String nick, String language, bool isSuperuser, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String name, String nick, String language, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -305,14 +307,15 @@ class __$SnAccountCopyWithImpl<$Res>
/// Create a copy of SnAccount
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? automatedId = freezed,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAccount(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable
as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable
as bool,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable
as bool,automatedId: freezed == automatedId ? _self.automatedId : automatedId // ignore: cast_nullable_to_non_nullable
as String?,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable
as SnAccountProfile,perkSubscription: freezed == perkSubscription ? _self.perkSubscription : perkSubscription // ignore: cast_nullable_to_non_nullable
as SnWalletSubscriptionRef?,badges: null == badges ? _self._badges : badges // ignore: cast_nullable_to_non_nullable
as List<SnAccountBadge>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
@@ -610,7 +613,7 @@ as String,
/// @nodoc
mixin _$SnAccountProfile {
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get socialCredits; int get socialCreditsLevel; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -623,16 +626,16 @@ $SnAccountProfileCopyWith<SnAccountProfile> get copyWith => _$SnAccountProfileCo
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -643,7 +646,7 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> {
factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
@useResult
$Res call({
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -660,7 +663,7 @@ class _$SnAccountProfileCopyWithImpl<$Res>
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
@@ -677,6 +680,8 @@ as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : last
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
as int,socialCredits: null == socialCredits ? _self.socialCredits : socialCredits // ignore: cast_nullable_to_non_nullable
as double,socialCreditsLevel: null == socialCreditsLevel ? _self.socialCreditsLevel : socialCreditsLevel // ignore: cast_nullable_to_non_nullable
as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : levelingProgress // ignore: cast_nullable_to_non_nullable
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
@@ -814,10 +819,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
@@ -835,10 +840,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnAccountProfile():
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -852,10 +857,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
@@ -867,7 +872,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
@JsonSerializable()
class _SnAccountProfile implements SnAccountProfile {
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, this.socialCredits = 100, this.socialCreditsLevel = 0, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
@override final String id;
@@ -891,6 +896,8 @@ class _SnAccountProfile implements SnAccountProfile {
@override final SnAccountBadge? activeBadge;
@override final int experience;
@override final int level;
@override@JsonKey() final double socialCredits;
@override@JsonKey() final int socialCreditsLevel;
@override final double levelingProgress;
@override final SnCloudFile? picture;
@override final SnCloudFile? background;
@@ -912,16 +919,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -932,7 +939,7 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi
factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
@override @useResult
$Res call({
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -949,7 +956,7 @@ class __$SnAccountProfileCopyWithImpl<$Res>
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAccountProfile(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
@@ -966,6 +973,8 @@ as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : last
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
as int,socialCredits: null == socialCredits ? _self.socialCredits : socialCredits // ignore: cast_nullable_to_non_nullable
as double,socialCreditsLevel: null == socialCreditsLevel ? _self.socialCreditsLevel : socialCreditsLevel // ignore: cast_nullable_to_non_nullable
as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : levelingProgress // ignore: cast_nullable_to_non_nullable
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
@@ -3018,6 +3027,562 @@ as bool,
}
}
/// @nodoc
mixin _$SnExperienceRecord {
String get id; int get delta; String get reasonType; String get reason; double? get bonusMultiplier; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnExperienceRecord
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnExperienceRecordCopyWith<SnExperienceRecord> get copyWith => _$SnExperienceRecordCopyWithImpl<SnExperienceRecord>(this as SnExperienceRecord, _$identity);
/// Serializes this SnExperienceRecord to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnExperienceRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.delta, delta) || other.delta == delta)&&(identical(other.reasonType, reasonType) || other.reasonType == reasonType)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.bonusMultiplier, bonusMultiplier) || other.bonusMultiplier == bonusMultiplier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,delta,reasonType,reason,bonusMultiplier,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnExperienceRecord(id: $id, delta: $delta, reasonType: $reasonType, reason: $reason, bonusMultiplier: $bonusMultiplier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnExperienceRecordCopyWith<$Res> {
factory $SnExperienceRecordCopyWith(SnExperienceRecord value, $Res Function(SnExperienceRecord) _then) = _$SnExperienceRecordCopyWithImpl;
@useResult
$Res call({
String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnExperienceRecordCopyWithImpl<$Res>
implements $SnExperienceRecordCopyWith<$Res> {
_$SnExperienceRecordCopyWithImpl(this._self, this._then);
final SnExperienceRecord _self;
final $Res Function(SnExperienceRecord) _then;
/// Create a copy of SnExperienceRecord
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? delta = null,Object? reasonType = null,Object? reason = null,Object? bonusMultiplier = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,delta: null == delta ? _self.delta : delta // ignore: cast_nullable_to_non_nullable
as int,reasonType: null == reasonType ? _self.reasonType : reasonType // ignore: cast_nullable_to_non_nullable
as String,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,bonusMultiplier: freezed == bonusMultiplier ? _self.bonusMultiplier : bonusMultiplier // ignore: cast_nullable_to_non_nullable
as double?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// Adds pattern-matching-related methods to [SnExperienceRecord].
extension SnExperienceRecordPatterns on SnExperienceRecord {
/// 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( _SnExperienceRecord value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnExperienceRecord() 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( _SnExperienceRecord value) $default,){
final _that = this;
switch (_that) {
case _SnExperienceRecord():
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( _SnExperienceRecord value)? $default,){
final _that = this;
switch (_that) {
case _SnExperienceRecord() 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, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnExperienceRecord() when $default != null:
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.bonusMultiplier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnExperienceRecord():
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.bonusMultiplier,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnExperienceRecord() when $default != null:
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.bonusMultiplier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnExperienceRecord implements SnExperienceRecord {
const _SnExperienceRecord({required this.id, required this.delta, required this.reasonType, required this.reason, this.bonusMultiplier = 1.0, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnExperienceRecord.fromJson(Map<String, dynamic> json) => _$SnExperienceRecordFromJson(json);
@override final String id;
@override final int delta;
@override final String reasonType;
@override final String reason;
@override@JsonKey() final double? bonusMultiplier;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnExperienceRecord
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnExperienceRecordCopyWith<_SnExperienceRecord> get copyWith => __$SnExperienceRecordCopyWithImpl<_SnExperienceRecord>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnExperienceRecordToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnExperienceRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.delta, delta) || other.delta == delta)&&(identical(other.reasonType, reasonType) || other.reasonType == reasonType)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.bonusMultiplier, bonusMultiplier) || other.bonusMultiplier == bonusMultiplier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,delta,reasonType,reason,bonusMultiplier,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnExperienceRecord(id: $id, delta: $delta, reasonType: $reasonType, reason: $reason, bonusMultiplier: $bonusMultiplier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnExperienceRecordCopyWith<$Res> implements $SnExperienceRecordCopyWith<$Res> {
factory _$SnExperienceRecordCopyWith(_SnExperienceRecord value, $Res Function(_SnExperienceRecord) _then) = __$SnExperienceRecordCopyWithImpl;
@override @useResult
$Res call({
String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnExperienceRecordCopyWithImpl<$Res>
implements _$SnExperienceRecordCopyWith<$Res> {
__$SnExperienceRecordCopyWithImpl(this._self, this._then);
final _SnExperienceRecord _self;
final $Res Function(_SnExperienceRecord) _then;
/// Create a copy of SnExperienceRecord
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? delta = null,Object? reasonType = null,Object? reason = null,Object? bonusMultiplier = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnExperienceRecord(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,delta: null == delta ? _self.delta : delta // ignore: cast_nullable_to_non_nullable
as int,reasonType: null == reasonType ? _self.reasonType : reasonType // ignore: cast_nullable_to_non_nullable
as String,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,bonusMultiplier: freezed == bonusMultiplier ? _self.bonusMultiplier : bonusMultiplier // ignore: cast_nullable_to_non_nullable
as double?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
mixin _$SnSocialCreditRecord {
String get id; double get delta; String get reasonType; String get reason; DateTime? get expiredAt; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnSocialCreditRecord
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnSocialCreditRecordCopyWith<SnSocialCreditRecord> get copyWith => _$SnSocialCreditRecordCopyWithImpl<SnSocialCreditRecord>(this as SnSocialCreditRecord, _$identity);
/// Serializes this SnSocialCreditRecord to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSocialCreditRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.delta, delta) || other.delta == delta)&&(identical(other.reasonType, reasonType) || other.reasonType == reasonType)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,delta,reasonType,reason,expiredAt,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnSocialCreditRecord(id: $id, delta: $delta, reasonType: $reasonType, reason: $reason, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnSocialCreditRecordCopyWith<$Res> {
factory $SnSocialCreditRecordCopyWith(SnSocialCreditRecord value, $Res Function(SnSocialCreditRecord) _then) = _$SnSocialCreditRecordCopyWithImpl;
@useResult
$Res call({
String id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnSocialCreditRecordCopyWithImpl<$Res>
implements $SnSocialCreditRecordCopyWith<$Res> {
_$SnSocialCreditRecordCopyWithImpl(this._self, this._then);
final SnSocialCreditRecord _self;
final $Res Function(SnSocialCreditRecord) _then;
/// Create a copy of SnSocialCreditRecord
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? delta = null,Object? reasonType = null,Object? reason = null,Object? expiredAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,delta: null == delta ? _self.delta : delta // ignore: cast_nullable_to_non_nullable
as double,reasonType: null == reasonType ? _self.reasonType : reasonType // ignore: cast_nullable_to_non_nullable
as String,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// Adds pattern-matching-related methods to [SnSocialCreditRecord].
extension SnSocialCreditRecordPatterns on SnSocialCreditRecord {
/// 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( _SnSocialCreditRecord value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnSocialCreditRecord() 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( _SnSocialCreditRecord value) $default,){
final _that = this;
switch (_that) {
case _SnSocialCreditRecord():
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( _SnSocialCreditRecord value)? $default,){
final _that = this;
switch (_that) {
case _SnSocialCreditRecord() 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, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnSocialCreditRecord() when $default != null:
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnSocialCreditRecord():
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnSocialCreditRecord() when $default != null:
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnSocialCreditRecord implements SnSocialCreditRecord {
const _SnSocialCreditRecord({required this.id, required this.delta, required this.reasonType, required this.reason, required this.expiredAt, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnSocialCreditRecord.fromJson(Map<String, dynamic> json) => _$SnSocialCreditRecordFromJson(json);
@override final String id;
@override final double delta;
@override final String reasonType;
@override final String reason;
@override final DateTime? expiredAt;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnSocialCreditRecord
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnSocialCreditRecordCopyWith<_SnSocialCreditRecord> get copyWith => __$SnSocialCreditRecordCopyWithImpl<_SnSocialCreditRecord>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnSocialCreditRecordToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSocialCreditRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.delta, delta) || other.delta == delta)&&(identical(other.reasonType, reasonType) || other.reasonType == reasonType)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,delta,reasonType,reason,expiredAt,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnSocialCreditRecord(id: $id, delta: $delta, reasonType: $reasonType, reason: $reason, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnSocialCreditRecordCopyWith<$Res> implements $SnSocialCreditRecordCopyWith<$Res> {
factory _$SnSocialCreditRecordCopyWith(_SnSocialCreditRecord value, $Res Function(_SnSocialCreditRecord) _then) = __$SnSocialCreditRecordCopyWithImpl;
@override @useResult
$Res call({
String id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnSocialCreditRecordCopyWithImpl<$Res>
implements _$SnSocialCreditRecordCopyWith<$Res> {
__$SnSocialCreditRecordCopyWithImpl(this._self, this._then);
final _SnSocialCreditRecord _self;
final $Res Function(_SnSocialCreditRecord) _then;
/// Create a copy of SnSocialCreditRecord
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? delta = null,Object? reasonType = null,Object? reason = null,Object? expiredAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnSocialCreditRecord(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,delta: null == delta ? _self.delta : delta // ignore: cast_nullable_to_non_nullable
as double,reasonType: null == reasonType ? _self.reasonType : reasonType // ignore: cast_nullable_to_non_nullable
as String,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
// dart format on

View File

@@ -12,6 +12,7 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
nick: json['nick'] as String,
language: json['language'] as String,
isSuperuser: json['is_superuser'] as bool,
automatedId: json['automated_id'] as String?,
profile: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
perkSubscription:
json['perk_subscription'] == null
@@ -39,6 +40,7 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
'nick': instance.nick,
'language': instance.language,
'is_superuser': instance.isSuperuser,
'automated_id': instance.automatedId,
'profile': instance.profile.toJson(),
'perk_subscription': instance.perkSubscription?.toJson(),
'badges': instance.badges.map((e) => e.toJson()).toList(),
@@ -84,6 +86,8 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
),
experience: (json['experience'] as num).toInt(),
level: (json['level'] as num).toInt(),
socialCredits: (json['social_credits'] as num?)?.toDouble() ?? 100,
socialCreditsLevel: (json['social_credits_level'] as num?)?.toInt() ?? 0,
levelingProgress: (json['leveling_progress'] as num).toDouble(),
picture:
json['picture'] == null
@@ -126,6 +130,8 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
'active_badge': instance.activeBadge?.toJson(),
'experience': instance.experience,
'level': instance.level,
'social_credits': instance.socialCredits,
'social_credits_level': instance.socialCreditsLevel,
'leveling_progress': instance.levelingProgress,
'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(),
@@ -348,3 +354,62 @@ Map<String, dynamic> _$SnAuthDeviceWithChallengeeToJson(
'challenges': instance.challenges.map((e) => e.toJson()).toList(),
'is_current': instance.isCurrent,
};
_SnExperienceRecord _$SnExperienceRecordFromJson(Map<String, dynamic> json) =>
_SnExperienceRecord(
id: json['id'] as String,
delta: (json['delta'] as num).toInt(),
reasonType: json['reason_type'] as String,
reason: json['reason'] as String,
bonusMultiplier: (json['bonus_multiplier'] as num?)?.toDouble() ?? 1.0,
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> _$SnExperienceRecordToJson(_SnExperienceRecord instance) =>
<String, dynamic>{
'id': instance.id,
'delta': instance.delta,
'reason_type': instance.reasonType,
'reason': instance.reason,
'bonus_multiplier': instance.bonusMultiplier,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_SnSocialCreditRecord _$SnSocialCreditRecordFromJson(
Map<String, dynamic> json,
) => _SnSocialCreditRecord(
id: json['id'] as String,
delta: (json['delta'] as num).toDouble(),
reasonType: json['reason_type'] as String,
reason: json['reason'] as String,
expiredAt:
json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
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> _$SnSocialCreditRecordToJson(
_SnSocialCreditRecord instance,
) => <String, dynamic>{
'id': instance.id,
'delta': instance.delta,
'reason_type': instance.reasonType,
'reason': instance.reason,
'expired_at': instance.expiredAt?.toIso8601String(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@@ -54,7 +54,7 @@ sealed class SnEventCalendarEntry with _$SnEventCalendarEntry {
const factory SnEventCalendarEntry({
required DateTime date,
required SnCheckInResult? checkInResult,
required List<dynamic> statuses,
required List<SnAccountStatus> statuses,
}) = _SnEventCalendarEntry;
factory SnEventCalendarEntry.fromJson(Map<String, dynamic> json) =>

View File

@@ -861,7 +861,7 @@ as String,
/// @nodoc
mixin _$SnEventCalendarEntry {
DateTime get date; SnCheckInResult? get checkInResult; List<dynamic> get statuses;
DateTime get date; SnCheckInResult? get checkInResult; List<SnAccountStatus> get statuses;
/// Create a copy of SnEventCalendarEntry
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -894,7 +894,7 @@ abstract mixin class $SnEventCalendarEntryCopyWith<$Res> {
factory $SnEventCalendarEntryCopyWith(SnEventCalendarEntry value, $Res Function(SnEventCalendarEntry) _then) = _$SnEventCalendarEntryCopyWithImpl;
@useResult
$Res call({
DateTime date, SnCheckInResult? checkInResult, List<dynamic> statuses
DateTime date, SnCheckInResult? checkInResult, List<SnAccountStatus> statuses
});
@@ -916,7 +916,7 @@ class _$SnEventCalendarEntryCopyWithImpl<$Res>
date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
as DateTime,checkInResult: freezed == checkInResult ? _self.checkInResult : checkInResult // ignore: cast_nullable_to_non_nullable
as SnCheckInResult?,statuses: null == statuses ? _self.statuses : statuses // ignore: cast_nullable_to_non_nullable
as List<dynamic>,
as List<SnAccountStatus>,
));
}
/// Create a copy of SnEventCalendarEntry
@@ -1010,7 +1010,7 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime date, SnCheckInResult? checkInResult, List<dynamic> statuses)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime date, SnCheckInResult? checkInResult, List<SnAccountStatus> statuses)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnEventCalendarEntry() when $default != null:
return $default(_that.date,_that.checkInResult,_that.statuses);case _:
@@ -1031,7 +1031,7 @@ return $default(_that.date,_that.checkInResult,_that.statuses);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime date, SnCheckInResult? checkInResult, List<dynamic> statuses) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime date, SnCheckInResult? checkInResult, List<SnAccountStatus> statuses) $default,) {final _that = this;
switch (_that) {
case _SnEventCalendarEntry():
return $default(_that.date,_that.checkInResult,_that.statuses);}
@@ -1048,7 +1048,7 @@ return $default(_that.date,_that.checkInResult,_that.statuses);}
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime date, SnCheckInResult? checkInResult, List<dynamic> statuses)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime date, SnCheckInResult? checkInResult, List<SnAccountStatus> statuses)? $default,) {final _that = this;
switch (_that) {
case _SnEventCalendarEntry() when $default != null:
return $default(_that.date,_that.checkInResult,_that.statuses);case _:
@@ -1063,13 +1063,13 @@ return $default(_that.date,_that.checkInResult,_that.statuses);case _:
@JsonSerializable()
class _SnEventCalendarEntry implements SnEventCalendarEntry {
const _SnEventCalendarEntry({required this.date, required this.checkInResult, required final List<dynamic> statuses}): _statuses = statuses;
const _SnEventCalendarEntry({required this.date, required this.checkInResult, required final List<SnAccountStatus> statuses}): _statuses = statuses;
factory _SnEventCalendarEntry.fromJson(Map<String, dynamic> json) => _$SnEventCalendarEntryFromJson(json);
@override final DateTime date;
@override final SnCheckInResult? checkInResult;
final List<dynamic> _statuses;
@override List<dynamic> get statuses {
final List<SnAccountStatus> _statuses;
@override List<SnAccountStatus> get statuses {
if (_statuses is EqualUnmodifiableListView) return _statuses;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_statuses);
@@ -1109,7 +1109,7 @@ abstract mixin class _$SnEventCalendarEntryCopyWith<$Res> implements $SnEventCal
factory _$SnEventCalendarEntryCopyWith(_SnEventCalendarEntry value, $Res Function(_SnEventCalendarEntry) _then) = __$SnEventCalendarEntryCopyWithImpl;
@override @useResult
$Res call({
DateTime date, SnCheckInResult? checkInResult, List<dynamic> statuses
DateTime date, SnCheckInResult? checkInResult, List<SnAccountStatus> statuses
});
@@ -1131,7 +1131,7 @@ class __$SnEventCalendarEntryCopyWithImpl<$Res>
date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
as DateTime,checkInResult: freezed == checkInResult ? _self.checkInResult : checkInResult // ignore: cast_nullable_to_non_nullable
as SnCheckInResult?,statuses: null == statuses ? _self._statuses : statuses // ignore: cast_nullable_to_non_nullable
as List<dynamic>,
as List<SnAccountStatus>,
));
}

View File

@@ -87,7 +87,10 @@ _SnEventCalendarEntry _$SnEventCalendarEntryFromJson(
: SnCheckInResult.fromJson(
json['check_in_result'] as Map<String, dynamic>,
),
statuses: json['statuses'] as List<dynamic>,
statuses:
(json['statuses'] as List<dynamic>)
.map((e) => SnAccountStatus.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$SnEventCalendarEntryToJson(
@@ -95,5 +98,5 @@ Map<String, dynamic> _$SnEventCalendarEntryToJson(
) => <String, dynamic>{
'date': instance.date.toIso8601String(),
'check_in_result': instance.checkInResult?.toJson(),
'statuses': instance.statuses,
'statuses': instance.statuses.map((e) => e.toJson()).toList(),
};

63
lib/models/bot.dart Normal file
View File

@@ -0,0 +1,63 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/account.dart';
import 'package:island/models/developer.dart';
part 'bot.freezed.dart';
part 'bot.g.dart';
@freezed
sealed class Bot with _$Bot {
const factory Bot({
required String id,
required String slug,
required bool isActive,
required String projectId,
required DateTime createdAt,
required DateTime updatedAt,
required SnAccount account,
SnDeveloper? developer,
}) = _Bot;
factory Bot.fromJson(Map<String, dynamic> json) => _$BotFromJson(json);
}
@freezed
sealed class BotConfig with _$BotConfig {
const factory BotConfig({
@Default(false) bool isPublic,
@Default(false) bool isInteractive,
@Default([]) List<String> allowedRealms,
@Default([]) List<String> allowedChatTypes,
@Default({}) Map<String, dynamic> metadata,
}) = _BotConfig;
factory BotConfig.fromJson(Map<String, dynamic> json) =>
_$BotConfigFromJson(json);
}
@freezed
sealed class BotLinks with _$BotLinks {
const factory BotLinks({
String? website,
String? documentation,
String? privacyPolicy,
String? termsOfService,
}) = _BotLinks;
factory BotLinks.fromJson(Map<String, dynamic> json) =>
_$BotLinksFromJson(json);
}
@freezed
sealed class BotSecret with _$BotSecret {
const factory BotSecret({
@Default('') String id,
@Default('') String secret,
String? description,
DateTime? expiredAt,
@Default('') String botId,
}) = _BotSecret;
factory BotSecret.fromJson(Map<String, dynamic> json) =>
_$BotSecretFromJson(json);
}

1156
lib/models/bot.freezed.dart Normal file

File diff suppressed because it is too large Load Diff

91
lib/models/bot.g.dart Normal file
View File

@@ -0,0 +1,91 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bot.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_Bot _$BotFromJson(Map<String, dynamic> json) => _Bot(
id: json['id'] as String,
slug: json['slug'] as String,
isActive: json['is_active'] as bool,
projectId: json['project_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
developer:
json['developer'] == null
? null
: SnDeveloper.fromJson(json['developer'] as Map<String, dynamic>),
);
Map<String, dynamic> _$BotToJson(_Bot instance) => <String, dynamic>{
'id': instance.id,
'slug': instance.slug,
'is_active': instance.isActive,
'project_id': instance.projectId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'account': instance.account.toJson(),
'developer': instance.developer?.toJson(),
};
_BotConfig _$BotConfigFromJson(Map<String, dynamic> json) => _BotConfig(
isPublic: json['is_public'] as bool? ?? false,
isInteractive: json['is_interactive'] as bool? ?? false,
allowedRealms:
(json['allowed_realms'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
allowedChatTypes:
(json['allowed_chat_types'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
);
Map<String, dynamic> _$BotConfigToJson(_BotConfig instance) =>
<String, dynamic>{
'is_public': instance.isPublic,
'is_interactive': instance.isInteractive,
'allowed_realms': instance.allowedRealms,
'allowed_chat_types': instance.allowedChatTypes,
'metadata': instance.metadata,
};
_BotLinks _$BotLinksFromJson(Map<String, dynamic> json) => _BotLinks(
website: json['website'] as String?,
documentation: json['documentation'] as String?,
privacyPolicy: json['privacy_policy'] as String?,
termsOfService: json['terms_of_service'] as String?,
);
Map<String, dynamic> _$BotLinksToJson(_BotLinks instance) => <String, dynamic>{
'website': instance.website,
'documentation': instance.documentation,
'privacy_policy': instance.privacyPolicy,
'terms_of_service': instance.termsOfService,
};
_BotSecret _$BotSecretFromJson(Map<String, dynamic> json) => _BotSecret(
id: json['id'] as String? ?? '',
secret: json['secret'] as String? ?? '',
description: json['description'] as String?,
expiredAt:
json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
botId: json['bot_id'] as String? ?? '',
);
Map<String, dynamic> _$BotSecretToJson(_BotSecret instance) =>
<String, dynamic>{
'id': instance.id,
'secret': instance.secret,
'description': instance.description,
'expired_at': instance.expiredAt?.toIso8601String(),
'bot_id': instance.botId,
};

20
lib/models/bot_key.dart Normal file
View File

@@ -0,0 +1,20 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'bot_key.freezed.dart';
part 'bot_key.g.dart';
@freezed
sealed class SnAccountApiKey with _$SnAccountApiKey {
const factory SnAccountApiKey({
required String id,
required String label,
required String accountId,
required String sessionId,
required DateTime createdAt,
required DateTime updatedAt,
String? key,
}) = _SnAccountApiKey;
factory SnAccountApiKey.fromJson(Map<String, dynamic> json) =>
_$SnAccountApiKeyFromJson(json);
}

View File

@@ -0,0 +1,289 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'bot_key.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnAccountApiKey {
String get id; String get label; String get accountId; String get sessionId; DateTime get createdAt; DateTime get updatedAt; String? get key;
/// Create a copy of SnAccountApiKey
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAccountApiKeyCopyWith<SnAccountApiKey> get copyWith => _$SnAccountApiKeyCopyWithImpl<SnAccountApiKey>(this as SnAccountApiKey, _$identity);
/// Serializes this SnAccountApiKey to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountApiKey&&(identical(other.id, id) || other.id == id)&&(identical(other.label, label) || other.label == label)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.sessionId, sessionId) || other.sessionId == sessionId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.key, key) || other.key == key));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,label,accountId,sessionId,createdAt,updatedAt,key);
@override
String toString() {
return 'SnAccountApiKey(id: $id, label: $label, accountId: $accountId, sessionId: $sessionId, createdAt: $createdAt, updatedAt: $updatedAt, key: $key)';
}
}
/// @nodoc
abstract mixin class $SnAccountApiKeyCopyWith<$Res> {
factory $SnAccountApiKeyCopyWith(SnAccountApiKey value, $Res Function(SnAccountApiKey) _then) = _$SnAccountApiKeyCopyWithImpl;
@useResult
$Res call({
String id, String label, String accountId, String sessionId, DateTime createdAt, DateTime updatedAt, String? key
});
}
/// @nodoc
class _$SnAccountApiKeyCopyWithImpl<$Res>
implements $SnAccountApiKeyCopyWith<$Res> {
_$SnAccountApiKeyCopyWithImpl(this._self, this._then);
final SnAccountApiKey _self;
final $Res Function(SnAccountApiKey) _then;
/// Create a copy of SnAccountApiKey
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = null,Object? accountId = null,Object? sessionId = null,Object? createdAt = null,Object? updatedAt = null,Object? key = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,sessionId: null == sessionId ? _self.sessionId : sessionId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,key: freezed == key ? _self.key : key // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [SnAccountApiKey].
extension SnAccountApiKeyPatterns on SnAccountApiKey {
/// 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( _SnAccountApiKey value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnAccountApiKey() 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( _SnAccountApiKey value) $default,){
final _that = this;
switch (_that) {
case _SnAccountApiKey():
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( _SnAccountApiKey value)? $default,){
final _that = this;
switch (_that) {
case _SnAccountApiKey() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String label, String accountId, String sessionId, DateTime createdAt, DateTime updatedAt, String? key)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnAccountApiKey() when $default != null:
return $default(_that.id,_that.label,_that.accountId,_that.sessionId,_that.createdAt,_that.updatedAt,_that.key);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String label, String accountId, String sessionId, DateTime createdAt, DateTime updatedAt, String? key) $default,) {final _that = this;
switch (_that) {
case _SnAccountApiKey():
return $default(_that.id,_that.label,_that.accountId,_that.sessionId,_that.createdAt,_that.updatedAt,_that.key);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String label, String accountId, String sessionId, DateTime createdAt, DateTime updatedAt, String? key)? $default,) {final _that = this;
switch (_that) {
case _SnAccountApiKey() when $default != null:
return $default(_that.id,_that.label,_that.accountId,_that.sessionId,_that.createdAt,_that.updatedAt,_that.key);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnAccountApiKey implements SnAccountApiKey {
const _SnAccountApiKey({required this.id, required this.label, required this.accountId, required this.sessionId, required this.createdAt, required this.updatedAt, this.key});
factory _SnAccountApiKey.fromJson(Map<String, dynamic> json) => _$SnAccountApiKeyFromJson(json);
@override final String id;
@override final String label;
@override final String accountId;
@override final String sessionId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final String? key;
/// Create a copy of SnAccountApiKey
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAccountApiKeyCopyWith<_SnAccountApiKey> get copyWith => __$SnAccountApiKeyCopyWithImpl<_SnAccountApiKey>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAccountApiKeyToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountApiKey&&(identical(other.id, id) || other.id == id)&&(identical(other.label, label) || other.label == label)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.sessionId, sessionId) || other.sessionId == sessionId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.key, key) || other.key == key));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,label,accountId,sessionId,createdAt,updatedAt,key);
@override
String toString() {
return 'SnAccountApiKey(id: $id, label: $label, accountId: $accountId, sessionId: $sessionId, createdAt: $createdAt, updatedAt: $updatedAt, key: $key)';
}
}
/// @nodoc
abstract mixin class _$SnAccountApiKeyCopyWith<$Res> implements $SnAccountApiKeyCopyWith<$Res> {
factory _$SnAccountApiKeyCopyWith(_SnAccountApiKey value, $Res Function(_SnAccountApiKey) _then) = __$SnAccountApiKeyCopyWithImpl;
@override @useResult
$Res call({
String id, String label, String accountId, String sessionId, DateTime createdAt, DateTime updatedAt, String? key
});
}
/// @nodoc
class __$SnAccountApiKeyCopyWithImpl<$Res>
implements _$SnAccountApiKeyCopyWith<$Res> {
__$SnAccountApiKeyCopyWithImpl(this._self, this._then);
final _SnAccountApiKey _self;
final $Res Function(_SnAccountApiKey) _then;
/// Create a copy of SnAccountApiKey
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = null,Object? accountId = null,Object? sessionId = null,Object? createdAt = null,Object? updatedAt = null,Object? key = freezed,}) {
return _then(_SnAccountApiKey(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,sessionId: null == sessionId ? _self.sessionId : sessionId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,key: freezed == key ? _self.key : key // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
// dart format on

29
lib/models/bot_key.g.dart Normal file
View File

@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bot_key.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnAccountApiKey _$SnAccountApiKeyFromJson(Map<String, dynamic> json) =>
_SnAccountApiKey(
id: json['id'] as String,
label: json['label'] as String,
accountId: json['account_id'] as String,
sessionId: json['session_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
key: json['key'] as String?,
);
Map<String, dynamic> _$SnAccountApiKeyToJson(_SnAccountApiKey instance) =>
<String, dynamic>{
'id': instance.id,
'label': instance.label,
'account_id': instance.accountId,
'session_id': instance.sessionId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'key': instance.key,
};

View File

@@ -104,7 +104,7 @@ sealed class SnChatMember with _$SnChatMember {
sealed class SnChatSummary with _$SnChatSummary {
const factory SnChatSummary({
required int unreadCount,
required SnChatMessage lastMessage,
required SnChatMessage? lastMessage,
}) = _SnChatSummary;
factory SnChatSummary.fromJson(Map<String, dynamic> json) =>

View File

@@ -1410,7 +1410,7 @@ $SnAccountStatusCopyWith<$Res>? get status {
/// @nodoc
mixin _$SnChatSummary {
int get unreadCount; SnChatMessage get lastMessage;
int get unreadCount; SnChatMessage? get lastMessage;
/// Create a copy of SnChatSummary
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -1443,11 +1443,11 @@ abstract mixin class $SnChatSummaryCopyWith<$Res> {
factory $SnChatSummaryCopyWith(SnChatSummary value, $Res Function(SnChatSummary) _then) = _$SnChatSummaryCopyWithImpl;
@useResult
$Res call({
int unreadCount, SnChatMessage lastMessage
int unreadCount, SnChatMessage? lastMessage
});
$SnChatMessageCopyWith<$Res> get lastMessage;
$SnChatMessageCopyWith<$Res>? get lastMessage;
}
/// @nodoc
@@ -1460,20 +1460,23 @@ class _$SnChatSummaryCopyWithImpl<$Res>
/// Create a copy of SnChatSummary
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? unreadCount = null,Object? lastMessage = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? unreadCount = null,Object? lastMessage = freezed,}) {
return _then(_self.copyWith(
unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable
as int,lastMessage: null == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable
as SnChatMessage,
as int,lastMessage: freezed == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable
as SnChatMessage?,
));
}
/// Create a copy of SnChatSummary
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnChatMessageCopyWith<$Res> get lastMessage {
$SnChatMessageCopyWith<$Res>? get lastMessage {
if (_self.lastMessage == null) {
return null;
}
return $SnChatMessageCopyWith<$Res>(_self.lastMessage, (value) {
return $SnChatMessageCopyWith<$Res>(_self.lastMessage!, (value) {
return _then(_self.copyWith(lastMessage: value));
});
}
@@ -1555,7 +1558,7 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int unreadCount, SnChatMessage lastMessage)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int unreadCount, SnChatMessage? lastMessage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnChatSummary() when $default != null:
return $default(_that.unreadCount,_that.lastMessage);case _:
@@ -1576,7 +1579,7 @@ return $default(_that.unreadCount,_that.lastMessage);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int unreadCount, SnChatMessage lastMessage) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int unreadCount, SnChatMessage? lastMessage) $default,) {final _that = this;
switch (_that) {
case _SnChatSummary():
return $default(_that.unreadCount,_that.lastMessage);}
@@ -1593,7 +1596,7 @@ return $default(_that.unreadCount,_that.lastMessage);}
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int unreadCount, SnChatMessage lastMessage)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int unreadCount, SnChatMessage? lastMessage)? $default,) {final _that = this;
switch (_that) {
case _SnChatSummary() when $default != null:
return $default(_that.unreadCount,_that.lastMessage);case _:
@@ -1612,7 +1615,7 @@ class _SnChatSummary implements SnChatSummary {
factory _SnChatSummary.fromJson(Map<String, dynamic> json) => _$SnChatSummaryFromJson(json);
@override final int unreadCount;
@override final SnChatMessage lastMessage;
@override final SnChatMessage? lastMessage;
/// Create a copy of SnChatSummary
/// with the given fields replaced by the non-null parameter values.
@@ -1647,11 +1650,11 @@ abstract mixin class _$SnChatSummaryCopyWith<$Res> implements $SnChatSummaryCopy
factory _$SnChatSummaryCopyWith(_SnChatSummary value, $Res Function(_SnChatSummary) _then) = __$SnChatSummaryCopyWithImpl;
@override @useResult
$Res call({
int unreadCount, SnChatMessage lastMessage
int unreadCount, SnChatMessage? lastMessage
});
@override $SnChatMessageCopyWith<$Res> get lastMessage;
@override $SnChatMessageCopyWith<$Res>? get lastMessage;
}
/// @nodoc
@@ -1664,11 +1667,11 @@ class __$SnChatSummaryCopyWithImpl<$Res>
/// Create a copy of SnChatSummary
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? unreadCount = null,Object? lastMessage = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? unreadCount = null,Object? lastMessage = freezed,}) {
return _then(_SnChatSummary(
unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable
as int,lastMessage: null == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable
as SnChatMessage,
as int,lastMessage: freezed == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable
as SnChatMessage?,
));
}
@@ -1676,9 +1679,12 @@ as SnChatMessage,
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnChatMessageCopyWith<$Res> get lastMessage {
$SnChatMessageCopyWith<$Res>? get lastMessage {
if (_self.lastMessage == null) {
return null;
}
return $SnChatMessageCopyWith<$Res>(_self.lastMessage, (value) {
return $SnChatMessageCopyWith<$Res>(_self.lastMessage!, (value) {
return _then(_self.copyWith(lastMessage: value));
});
}

View File

@@ -213,15 +213,18 @@ Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) =>
_SnChatSummary _$SnChatSummaryFromJson(Map<String, dynamic> json) =>
_SnChatSummary(
unreadCount: (json['unread_count'] as num).toInt(),
lastMessage: SnChatMessage.fromJson(
json['last_message'] as Map<String, dynamic>,
),
lastMessage:
json['last_message'] == null
? null
: SnChatMessage.fromJson(
json['last_message'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$SnChatSummaryToJson(_SnChatSummary instance) =>
<String, dynamic>{
'unread_count': instance.unreadCount,
'last_message': instance.lastMessage.toJson(),
'last_message': instance.lastMessage?.toJson(),
};
_MessageChange _$MessageChangeFromJson(Map<String, dynamic> json) =>

View File

@@ -0,0 +1,19 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'custom_app_secret.freezed.dart';
part 'custom_app_secret.g.dart';
@freezed
sealed class CustomAppSecret with _$CustomAppSecret {
const factory CustomAppSecret({
required String id,
required String? secret,
required DateTime createdAt,
String? description,
int? expiresIn,
bool? isOidc,
}) = _CustomAppSecret;
factory CustomAppSecret.fromJson(Map<String, dynamic> json) =>
_$CustomAppSecretFromJson(json);
}

View File

@@ -0,0 +1,286 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'custom_app_secret.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$CustomAppSecret {
String get id; String? get secret; DateTime get createdAt; String? get description; int? get expiresIn; bool? get isOidc;
/// Create a copy of CustomAppSecret
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$CustomAppSecretCopyWith<CustomAppSecret> get copyWith => _$CustomAppSecretCopyWithImpl<CustomAppSecret>(this as CustomAppSecret, _$identity);
/// Serializes this CustomAppSecret to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiresIn, expiresIn) || other.expiresIn == expiresIn)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description,expiresIn,isOidc);
@override
String toString() {
return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description, expiresIn: $expiresIn, isOidc: $isOidc)';
}
}
/// @nodoc
abstract mixin class $CustomAppSecretCopyWith<$Res> {
factory $CustomAppSecretCopyWith(CustomAppSecret value, $Res Function(CustomAppSecret) _then) = _$CustomAppSecretCopyWithImpl;
@useResult
$Res call({
String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc
});
}
/// @nodoc
class _$CustomAppSecretCopyWithImpl<$Res>
implements $CustomAppSecretCopyWith<$Res> {
_$CustomAppSecretCopyWithImpl(this._self, this._then);
final CustomAppSecret _self;
final $Res Function(CustomAppSecret) _then;
/// Create a copy of CustomAppSecret
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? secret = freezed,Object? createdAt = null,Object? description = freezed,Object? expiresIn = freezed,Object? isOidc = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,secret: freezed == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,expiresIn: freezed == expiresIn ? _self.expiresIn : expiresIn // ignore: cast_nullable_to_non_nullable
as int?,isOidc: freezed == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable
as bool?,
));
}
}
/// Adds pattern-matching-related methods to [CustomAppSecret].
extension CustomAppSecretPatterns on CustomAppSecret {
/// 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( _CustomAppSecret value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _CustomAppSecret() 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( _CustomAppSecret value) $default,){
final _that = this;
switch (_that) {
case _CustomAppSecret():
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( _CustomAppSecret value)? $default,){
final _that = this;
switch (_that) {
case _CustomAppSecret() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _CustomAppSecret() when $default != null:
return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc) $default,) {final _that = this;
switch (_that) {
case _CustomAppSecret():
return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc)? $default,) {final _that = this;
switch (_that) {
case _CustomAppSecret() when $default != null:
return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _CustomAppSecret implements CustomAppSecret {
const _CustomAppSecret({required this.id, required this.secret, required this.createdAt, this.description, this.expiresIn, this.isOidc});
factory _CustomAppSecret.fromJson(Map<String, dynamic> json) => _$CustomAppSecretFromJson(json);
@override final String id;
@override final String? secret;
@override final DateTime createdAt;
@override final String? description;
@override final int? expiresIn;
@override final bool? isOidc;
/// Create a copy of CustomAppSecret
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$CustomAppSecretCopyWith<_CustomAppSecret> get copyWith => __$CustomAppSecretCopyWithImpl<_CustomAppSecret>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$CustomAppSecretToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiresIn, expiresIn) || other.expiresIn == expiresIn)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description,expiresIn,isOidc);
@override
String toString() {
return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description, expiresIn: $expiresIn, isOidc: $isOidc)';
}
}
/// @nodoc
abstract mixin class _$CustomAppSecretCopyWith<$Res> implements $CustomAppSecretCopyWith<$Res> {
factory _$CustomAppSecretCopyWith(_CustomAppSecret value, $Res Function(_CustomAppSecret) _then) = __$CustomAppSecretCopyWithImpl;
@override @useResult
$Res call({
String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc
});
}
/// @nodoc
class __$CustomAppSecretCopyWithImpl<$Res>
implements _$CustomAppSecretCopyWith<$Res> {
__$CustomAppSecretCopyWithImpl(this._self, this._then);
final _CustomAppSecret _self;
final $Res Function(_CustomAppSecret) _then;
/// Create a copy of CustomAppSecret
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? secret = freezed,Object? createdAt = null,Object? description = freezed,Object? expiresIn = freezed,Object? isOidc = freezed,}) {
return _then(_CustomAppSecret(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,secret: freezed == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,expiresIn: freezed == expiresIn ? _self.expiresIn : expiresIn // ignore: cast_nullable_to_non_nullable
as int?,isOidc: freezed == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable
as bool?,
));
}
}
// dart format on

View File

@@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'custom_app_secret.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_CustomAppSecret _$CustomAppSecretFromJson(Map<String, dynamic> json) =>
_CustomAppSecret(
id: json['id'] as String,
secret: json['secret'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
description: json['description'] as String?,
expiresIn: (json['expires_in'] as num?)?.toInt(),
isOidc: json['is_oidc'] as bool?,
);
Map<String, dynamic> _$CustomAppSecretToJson(_CustomAppSecret instance) =>
<String, dynamic>{
'id': instance.id,
'secret': instance.secret,
'created_at': instance.createdAt.toIso8601String(),
'description': instance.description,
'expires_in': instance.expiresIn,
'is_oidc': instance.isOidc,
};

View File

@@ -0,0 +1,23 @@
class DevProject {
final String id;
final String slug;
final String name;
final String? description;
DevProject({
required this.id,
required this.slug,
required this.name,
this.description,
});
factory DevProject.fromJson(Map<String, dynamic> json) {
return DevProject(
id: json['id'],
slug: json['slug'],
name: json['name'],
description: json['description'],
);
}
}

View File

@@ -27,6 +27,7 @@ sealed class SnPost with _$SnPost {
@Default(0) int upvotes,
@Default(0) int downvotes,
@Default(0) int repliesCount,
int? pinMode,
String? threadedPostId,
SnPost? threadedPost,
String? repliedPostId,

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnPost {
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; String? get slug; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; String? get realmId; SnRealm? get realm; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<SnPostTag> get tags; List<SnPostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated;
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; String? get slug; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; int? get pinMode; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; String? get realmId; SnRealm? get realm; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<SnPostTag> get tags; List<SnPostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated;
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $SnPostCopyWith<SnPost> get copyWith => _$SnPostCopyWithImpl<SnPost>(this as SnP
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactionsMade, reactionsMade)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactionsMade, reactionsMade)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactionsMade),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]);
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactionsMade),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]);
@override
String toString() {
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
}
@@ -48,7 +48,7 @@ abstract mixin class $SnPostCopyWith<$Res> {
factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl;
@useResult
$Res call({
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
});
@@ -65,7 +65,7 @@ class _$SnPostCopyWithImpl<$Res>
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
@@ -83,7 +83,8 @@ as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore:
as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable
as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable
as int,repliesCount: null == repliesCount ? _self.repliesCount : repliesCount // ignore: cast_nullable_to_non_nullable
as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable
as int,pinMode: freezed == pinMode ? _self.pinMode : pinMode // ignore: cast_nullable_to_non_nullable
as int?,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable
as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable
as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable
as String?,repliedPost: freezed == repliedPost ? _self.repliedPost : repliedPost // ignore: cast_nullable_to_non_nullable
@@ -242,10 +243,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPost() when $default != null:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
return orElse();
}
@@ -263,10 +264,10 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated) $default,) {final _that = this;
switch (_that) {
case _SnPost():
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);}
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -280,10 +281,10 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,) {final _that = this;
switch (_that) {
case _SnPost() when $default != null:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
return null;
}
@@ -295,7 +296,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
@JsonSerializable()
class _SnPost implements SnPost {
const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.slug, this.type = 0, final Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, this.realmId, this.realm, final List<SnCloudFile> attachments = const [], required this.publisher, final Map<String, int> reactionsCount = const {}, final Map<String, bool> reactionsMade = const {}, final List<dynamic> reactions = const [], final List<SnPostTag> tags = const [], final List<SnPostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.slug, this.type = 0, final Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.pinMode, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, this.realmId, this.realm, final List<SnCloudFile> attachments = const [], required this.publisher, final Map<String, int> reactionsCount = const {}, final Map<String, bool> reactionsMade = const {}, final List<dynamic> reactions = const [], final List<SnPostTag> tags = const [], final List<SnPostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json);
@override final String id;
@@ -322,6 +323,7 @@ class _SnPost implements SnPost {
@override@JsonKey() final int upvotes;
@override@JsonKey() final int downvotes;
@override@JsonKey() final int repliesCount;
@override final int? pinMode;
@override final String? threadedPostId;
@override final SnPost? threadedPost;
@override final String? repliedPostId;
@@ -398,16 +400,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactionsMade, _reactionsMade)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactionsMade, _reactionsMade)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactionsMade),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]);
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactionsMade),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]);
@override
String toString() {
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
}
@@ -418,7 +420,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl;
@override @useResult
$Res call({
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
});
@@ -435,7 +437,7 @@ class __$SnPostCopyWithImpl<$Res>
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
return _then(_SnPost(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
@@ -453,7 +455,8 @@ as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore:
as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable
as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable
as int,repliesCount: null == repliesCount ? _self.repliesCount : repliesCount // ignore: cast_nullable_to_non_nullable
as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable
as int,pinMode: freezed == pinMode ? _self.pinMode : pinMode // ignore: cast_nullable_to_non_nullable
as int?,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable
as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable
as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable
as String?,repliedPost: freezed == repliedPost ? _self.repliedPost : repliedPost // ignore: cast_nullable_to_non_nullable

View File

@@ -29,6 +29,7 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
upvotes: (json['upvotes'] as num?)?.toInt() ?? 0,
downvotes: (json['downvotes'] as num?)?.toInt() ?? 0,
repliesCount: (json['replies_count'] as num?)?.toInt() ?? 0,
pinMode: (json['pin_mode'] as num?)?.toInt(),
threadedPostId: json['threaded_post_id'] as String?,
threadedPost:
json['threaded_post'] == null
@@ -109,6 +110,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'upvotes': instance.upvotes,
'downvotes': instance.downvotes,
'replies_count': instance.repliesCount,
'pin_mode': instance.pinMode,
'threaded_post_id': instance.threadedPostId,
'threaded_post': instance.threadedPost?.toJson(),
'replied_post_id': instance.repliedPostId,

View File

@@ -15,6 +15,7 @@ sealed class SnPostCategory with _$SnPostCategory {
required String slug,
String? name,
@Default([]) List<SnPost> posts,
@Default(0) int usage,
}) = _SnPostCategory;
factory SnPostCategory.fromJson(Map<String, dynamic> json) =>

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnPostCategory {
String get id; String get slug; String? get name; List<SnPost> get posts;
String get id; String get slug; String? get name; List<SnPost> get posts; int get usage;
/// Create a copy of SnPostCategory
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $SnPostCategoryCopyWith<SnPostCategory> get copyWith => _$SnPostCategoryCopyWith
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts)&&(identical(other.usage, usage) || other.usage == usage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(posts));
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(posts),usage);
@override
String toString() {
return 'SnPostCategory(id: $id, slug: $slug, name: $name, posts: $posts)';
return 'SnPostCategory(id: $id, slug: $slug, name: $name, posts: $posts, usage: $usage)';
}
@@ -48,7 +48,7 @@ abstract mixin class $SnPostCategoryCopyWith<$Res> {
factory $SnPostCategoryCopyWith(SnPostCategory value, $Res Function(SnPostCategory) _then) = _$SnPostCategoryCopyWithImpl;
@useResult
$Res call({
String id, String slug, String? name, List<SnPost> posts
String id, String slug, String? name, List<SnPost> posts, int usage
});
@@ -65,13 +65,14 @@ class _$SnPostCategoryCopyWithImpl<$Res>
/// Create a copy of SnPostCategory
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,Object? usage = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,posts: null == posts ? _self.posts : posts // ignore: cast_nullable_to_non_nullable
as List<SnPost>,
as List<SnPost>,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
as int,
));
}
@@ -153,10 +154,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts, int usage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPostCategory() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
return $default(_that.id,_that.slug,_that.name,_that.posts,_that.usage);case _:
return orElse();
}
@@ -174,10 +175,10 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts, int usage) $default,) {final _that = this;
switch (_that) {
case _SnPostCategory():
return $default(_that.id,_that.slug,_that.name,_that.posts);}
return $default(_that.id,_that.slug,_that.name,_that.posts,_that.usage);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -191,10 +192,10 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);}
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String? name, List<SnPost> posts)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String? name, List<SnPost> posts, int usage)? $default,) {final _that = this;
switch (_that) {
case _SnPostCategory() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
return $default(_that.id,_that.slug,_that.name,_that.posts,_that.usage);case _:
return null;
}
@@ -206,7 +207,7 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
@JsonSerializable()
class _SnPostCategory extends SnPostCategory {
const _SnPostCategory({required this.id, required this.slug, this.name, final List<SnPost> posts = const []}): _posts = posts,super._();
const _SnPostCategory({required this.id, required this.slug, this.name, final List<SnPost> posts = const [], this.usage = 0}): _posts = posts,super._();
factory _SnPostCategory.fromJson(Map<String, dynamic> json) => _$SnPostCategoryFromJson(json);
@override final String id;
@@ -219,6 +220,7 @@ class _SnPostCategory extends SnPostCategory {
return EqualUnmodifiableListView(_posts);
}
@override@JsonKey() final int usage;
/// Create a copy of SnPostCategory
/// with the given fields replaced by the non-null parameter values.
@@ -233,16 +235,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts)&&(identical(other.usage, usage) || other.usage == usage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(_posts));
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(_posts),usage);
@override
String toString() {
return 'SnPostCategory(id: $id, slug: $slug, name: $name, posts: $posts)';
return 'SnPostCategory(id: $id, slug: $slug, name: $name, posts: $posts, usage: $usage)';
}
@@ -253,7 +255,7 @@ abstract mixin class _$SnPostCategoryCopyWith<$Res> implements $SnPostCategoryCo
factory _$SnPostCategoryCopyWith(_SnPostCategory value, $Res Function(_SnPostCategory) _then) = __$SnPostCategoryCopyWithImpl;
@override @useResult
$Res call({
String id, String slug, String? name, List<SnPost> posts
String id, String slug, String? name, List<SnPost> posts, int usage
});
@@ -270,13 +272,14 @@ class __$SnPostCategoryCopyWithImpl<$Res>
/// Create a copy of SnPostCategory
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,Object? usage = null,}) {
return _then(_SnPostCategory(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,posts: null == posts ? _self._posts : posts // ignore: cast_nullable_to_non_nullable
as List<SnPost>,
as List<SnPost>,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
as int,
));
}

View File

@@ -16,6 +16,7 @@ _SnPostCategory _$SnPostCategoryFromJson(Map<String, dynamic> json) =>
?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
usage: (json['usage'] as num?)?.toInt() ?? 0,
);
Map<String, dynamic> _$SnPostCategoryToJson(_SnPostCategory instance) =>
@@ -24,4 +25,5 @@ Map<String, dynamic> _$SnPostCategoryToJson(_SnPostCategory instance) =>
'slug': instance.slug,
'name': instance.name,
'posts': instance.posts.map((e) => e.toJson()).toList(),
'usage': instance.usage,
};

View File

@@ -11,6 +11,7 @@ sealed class SnPostTag with _$SnPostTag {
required String slug,
String? name,
@Default([]) List<SnPost> posts,
@Default(0) int usage,
}) = _SnPostTag;
factory SnPostTag.fromJson(Map<String, dynamic> json) =>

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnPostTag {
String get id; String get slug; String? get name; List<SnPost> get posts;
String get id; String get slug; String? get name; List<SnPost> get posts; int get usage;
/// Create a copy of SnPostTag
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $SnPostTagCopyWith<SnPostTag> get copyWith => _$SnPostTagCopyWithImpl<SnPostTag>
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts)&&(identical(other.usage, usage) || other.usage == usage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(posts));
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(posts),usage);
@override
String toString() {
return 'SnPostTag(id: $id, slug: $slug, name: $name, posts: $posts)';
return 'SnPostTag(id: $id, slug: $slug, name: $name, posts: $posts, usage: $usage)';
}
@@ -48,7 +48,7 @@ abstract mixin class $SnPostTagCopyWith<$Res> {
factory $SnPostTagCopyWith(SnPostTag value, $Res Function(SnPostTag) _then) = _$SnPostTagCopyWithImpl;
@useResult
$Res call({
String id, String slug, String? name, List<SnPost> posts
String id, String slug, String? name, List<SnPost> posts, int usage
});
@@ -65,13 +65,14 @@ class _$SnPostTagCopyWithImpl<$Res>
/// Create a copy of SnPostTag
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,Object? usage = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,posts: null == posts ? _self.posts : posts // ignore: cast_nullable_to_non_nullable
as List<SnPost>,
as List<SnPost>,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
as int,
));
}
@@ -153,10 +154,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts, int usage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPostTag() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
return $default(_that.id,_that.slug,_that.name,_that.posts,_that.usage);case _:
return orElse();
}
@@ -174,10 +175,10 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts, int usage) $default,) {final _that = this;
switch (_that) {
case _SnPostTag():
return $default(_that.id,_that.slug,_that.name,_that.posts);}
return $default(_that.id,_that.slug,_that.name,_that.posts,_that.usage);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -191,10 +192,10 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);}
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String? name, List<SnPost> posts)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String? name, List<SnPost> posts, int usage)? $default,) {final _that = this;
switch (_that) {
case _SnPostTag() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
return $default(_that.id,_that.slug,_that.name,_that.posts,_that.usage);case _:
return null;
}
@@ -206,7 +207,7 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
@JsonSerializable()
class _SnPostTag implements SnPostTag {
const _SnPostTag({required this.id, required this.slug, this.name, final List<SnPost> posts = const []}): _posts = posts;
const _SnPostTag({required this.id, required this.slug, this.name, final List<SnPost> posts = const [], this.usage = 0}): _posts = posts;
factory _SnPostTag.fromJson(Map<String, dynamic> json) => _$SnPostTagFromJson(json);
@override final String id;
@@ -219,6 +220,7 @@ class _SnPostTag implements SnPostTag {
return EqualUnmodifiableListView(_posts);
}
@override@JsonKey() final int usage;
/// Create a copy of SnPostTag
/// with the given fields replaced by the non-null parameter values.
@@ -233,16 +235,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts)&&(identical(other.usage, usage) || other.usage == usage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(_posts));
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(_posts),usage);
@override
String toString() {
return 'SnPostTag(id: $id, slug: $slug, name: $name, posts: $posts)';
return 'SnPostTag(id: $id, slug: $slug, name: $name, posts: $posts, usage: $usage)';
}
@@ -253,7 +255,7 @@ abstract mixin class _$SnPostTagCopyWith<$Res> implements $SnPostTagCopyWith<$Re
factory _$SnPostTagCopyWith(_SnPostTag value, $Res Function(_SnPostTag) _then) = __$SnPostTagCopyWithImpl;
@override @useResult
$Res call({
String id, String slug, String? name, List<SnPost> posts
String id, String slug, String? name, List<SnPost> posts, int usage
});
@@ -270,13 +272,14 @@ class __$SnPostTagCopyWithImpl<$Res>
/// Create a copy of SnPostTag
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,Object? usage = null,}) {
return _then(_SnPostTag(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,posts: null == posts ? _self._posts : posts // ignore: cast_nullable_to_non_nullable
as List<SnPost>,
as List<SnPost>,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
as int,
));
}

View File

@@ -15,6 +15,7 @@ _SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) => _SnPostTag(
?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
usage: (json['usage'] as num?)?.toInt() ?? 0,
);
Map<String, dynamic> _$SnPostTagToJson(_SnPostTag instance) =>
@@ -23,4 +24,5 @@ Map<String, dynamic> _$SnPostTagToJson(_SnPostTag instance) =>
'slug': instance.slug,
'name': instance.name,
'posts': instance.posts.map((e) => e.toJson()).toList(),
'usage': instance.usage,
};

View File

@@ -1,6 +1,12 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io' show Platform;
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_platform_alert/flutter_platform_alert.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart';
@@ -13,13 +19,59 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
UserInfoNotifier(this._ref) : super(const AsyncValue.data(null));
Future<void> fetchUser() async {
final token = _ref.watch(tokenProvider);
if (token == null) {
log('[UserInfo] No token found, not going to fetch...');
return;
}
try {
final client = _ref.read(apiClientProvider);
final response = await client.get('/id/accounts/me');
final user = SnAccount.fromJson(response.data);
state = AsyncValue.data(user);
FirebaseAnalytics.instance.setUserId(id: user.id);
if (kIsWeb || !Platform.isLinux) {
FirebaseAnalytics.instance.setUserId(id: user.id);
}
} catch (error, stackTrace) {
if (!kIsWeb) {
if (error is DioException) {
FlutterPlatformAlert.showCustomAlert(
windowTitle: 'failedToLoadUserInfo'.tr(),
text: [
(error.response?.statusCode == 401
? 'failedToLoadUserInfoUnauthorized'
: 'failedToLoadUserInfoNetwork')
.tr()
.trim(),
'${error.response!.statusCode}\n${error.response?.headers}',
jsonEncode(error.response?.data),
].join('\n\n'),
iconStyle: IconStyle.error,
neutralButtonTitle: 'retry'.tr(),
negativeButtonTitle: 'okay'.tr(),
).then((value) {
if (value == CustomButton.neutralButton) {
fetchUser();
}
});
}
FlutterPlatformAlert.showCustomAlert(
windowTitle: 'failedToLoadUserInfo'.tr(),
text:
[
'failedToLoadUserInfoNetwork'.tr(),
error.toString(),
].join('\n\n').trim(),
iconStyle: IconStyle.error,
neutralButtonTitle: 'retry'.tr(),
negativeButtonTitle: 'okay'.tr(),
).then((value) {
if (value == CustomButton.neutralButton) {
fetchUser();
}
});
}
log(
"[UserInfo] Failed to fetch user info...",
name: 'UserInfoNotifier',
@@ -35,7 +87,9 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
final prefs = _ref.read(sharedPreferencesProvider);
await prefs.remove(kTokenPairStoreKey);
_ref.invalidate(tokenProvider);
FirebaseAnalytics.instance.setUserId(id: null);
if (kIsWeb || !Platform.isLinux) {
FirebaseAnalytics.instance.setUserId(id: null);
}
}
}

View File

@@ -6,11 +6,20 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/screens/about.dart';
import 'package:island/screens/developers/apps.dart';
import 'package:island/screens/account/credits.dart';
import 'package:island/screens/developers/app_detail.dart';
import 'package:island/screens/developers/bot_detail.dart';
import 'package:island/screens/developers/edit_app.dart';
import 'package:island/screens/developers/edit_bot.dart';
import 'package:island/screens/developers/new_app.dart';
import 'package:island/screens/developers/hub.dart';
import 'package:island/screens/developers/new_bot.dart';
import 'package:island/screens/developers/projects.dart';
import 'package:island/screens/developers/edit_project.dart';
import 'package:island/screens/developers/new_project.dart';
import 'package:island/screens/developers/project_detail.dart';
import 'package:island/screens/discovery/articles.dart';
import 'package:island/screens/posts/post_categories_list.dart';
import 'package:island/screens/posts/post_category_detail.dart';
import 'package:island/screens/posts/post_search.dart';
import 'package:island/widgets/app_wrapper.dart';
@@ -29,12 +38,15 @@ import 'package:island/screens/chat/chat.dart';
import 'package:island/screens/chat/room.dart';
import 'package:island/screens/chat/room_detail.dart';
import 'package:island/screens/chat/call.dart';
import 'package:island/screens/chat/search_messages_screen.dart';
import 'package:island/screens/creators/hub.dart';
import 'package:island/screens/creators/posts/post_manage_list.dart';
import 'package:island/screens/creators/stickers/stickers.dart';
import 'package:island/screens/creators/stickers/pack_detail.dart';
import 'package:island/screens/stickers/marketplace.dart';
import 'package:island/screens/stickers/sticker_marketplace.dart';
import 'package:island/screens/stickers/pack_detail.dart';
import 'package:island/screens/discovery/feeds/feed_marketplace.dart';
import 'package:island/screens/discovery/feeds/feed_detail.dart';
import 'package:island/screens/creators/poll/poll_list.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
@@ -52,6 +64,7 @@ import 'package:island/screens/account/event_calendar.dart';
import 'package:island/screens/discovery/realms.dart';
import 'package:island/screens/reports/report_detail.dart';
import 'package:island/screens/reports/report_list.dart';
import 'package:island/widgets/post/post_shuffle.dart';
// Shell route keys for nested navigation
final rootNavigatorKey = GlobalKey<NavigatorState>();
@@ -286,30 +299,99 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const DeveloperHubScreen(),
),
GoRoute(
name: 'developerApps',
path: '/developers/:name/apps',
name: 'developerProjects',
path: '/developers/:name/projects',
builder:
(context, state) => CustomAppsScreen(
(context, state) => DevProjectsScreen(
publisherName: state.pathParameters['name']!,
),
),
GoRoute(
name: 'developerAppNew',
path: '/developers/:name/apps/new',
name: 'developerProjectNew',
path: '/developers/:name/projects/new',
builder:
(context, state) => NewCustomAppScreen(
(context, state) => NewProjectScreen(
publisherName: state.pathParameters['name']!,
),
),
GoRoute(
name: 'developerAppEdit',
path: '/developers/:name/apps/:id',
name: 'developerProjectEdit',
path: '/developers/:name/projects/:id/edit',
builder:
(context, state) => EditAppScreen(
(context, state) => EditProjectScreen(
publisherName: state.pathParameters['name']!,
id: state.pathParameters['id']!,
),
),
GoRoute(
name: 'developerProjectDetail',
path: '/developers/:name/projects/:projectId',
builder:
(context, state) => ProjectDetailScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
),
routes: [
GoRoute(
name: 'developerAppNew',
path: 'apps/new',
builder:
(context, state) => NewCustomAppScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
),
),
GoRoute(
name: 'developerAppEdit',
path: 'apps/:id/edit',
builder:
(context, state) => EditAppScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
id: state.pathParameters['id']!,
),
),
GoRoute(
name: 'developerAppDetail',
path: 'apps/:appId',
builder:
(context, state) => AppDetailScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
appId: state.pathParameters['appId']!,
),
),
GoRoute(
name: 'developerBotDetail',
path: 'bots/:botId',
builder:
(context, state) => BotDetailScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
botId: state.pathParameters['botId']!,
),
),
GoRoute(
name: 'developerBotNew',
path: 'bots/new',
builder:
(context, state) => NewBotScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
),
),
GoRoute(
name: 'developerBotEdit',
path: 'bots/:id/edit',
builder:
(context, state) => EditBotScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
id: state.pathParameters['id']!,
),
),
],
),
],
),
@@ -376,12 +458,14 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const PostSearchScreen(),
),
GoRoute(
name: 'postDetail',
path: '/posts/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return PostDetailScreen(id: id);
},
name: 'postShuffle',
path: '/posts/shuffle',
builder: (context, state) => const PostShuffleScreen(),
),
GoRoute(
name: 'postCategories',
path: '/posts/categories',
builder: (context, state) => const PostCategoriesListScreen(),
),
GoRoute(
name: 'postCategoryDetail',
@@ -391,6 +475,11 @@ final routerProvider = Provider<GoRouter>((ref) {
return PostCategoryDetailScreen(slug: slug, isCategory: true);
},
),
GoRoute(
name: 'postTags',
path: '/posts/tags',
builder: (context, state) => const PostTagsListScreen(),
),
GoRoute(
name: 'postTagDetail',
path: '/posts/tags/:slug',
@@ -402,6 +491,14 @@ final routerProvider = Provider<GoRouter>((ref) {
);
},
),
GoRoute(
name: 'postDetail',
path: '/posts/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return PostDetailScreen(id: id);
},
),
GoRoute(
name: 'publisherProfile',
path: '/publishers/:name',
@@ -459,6 +556,14 @@ final routerProvider = Provider<GoRouter>((ref) {
return ChatDetailScreen(id: id);
},
),
GoRoute(
name: 'searchMessages',
path: '/chat/:id/search',
builder: (context, state) {
final id = state.pathParameters['id']!;
return SearchMessagesScreen(roomId: id);
},
),
],
),
@@ -528,6 +633,22 @@ final routerProvider = Provider<GoRouter>((ref) {
),
],
),
GoRoute(
name: 'webFeedMarketplace',
path: '/feeds',
builder:
(context, state) => const MarketplaceWebFeedsScreen(),
routes: [
GoRoute(
name: 'webFeedDetail',
path: ':feedId',
builder: (context, state) {
final feedId = state.pathParameters['feedId']!;
return MarketplaceWebFeedDetailScreen(id: feedId);
},
),
],
),
GoRoute(
name: 'notifications',
path: '/account/notifications',
@@ -538,6 +659,11 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/account/wallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
name: 'socialCredits',
path: '/account/credits',
builder: (context, state) => const SocialCreditsScreen(),
),
GoRoute(
name: 'relationships',
path: '/account/relationships',

View File

@@ -236,6 +236,26 @@ class AccountScreen extends HookConsumerWidget {
context.pushNamed('stickerMarketplace');
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.rss_feed),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('webFeeds').tr(),
onTap: () {
context.pushNamed('webFeedMarketplace');
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.star),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('credits').tr(),
onTap: () {
context.pushNamed('socialCredits');
},
),
ListTile(
minTileHeight: 48,
title: Text('abuseReport').tr(),
@@ -389,6 +409,15 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
},
child: Text('about').tr(),
),
TextButton(
child: Text('debugOptions').tr(),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (context) => DebugSheet(),
);
},
),
TextButton(
onPressed: () {
context.pushNamed('settings');

View File

@@ -0,0 +1,152 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
part 'credits.g.dart';
@riverpod
Future<double> socialCredits(Ref ref) async {
final client = ref.watch(apiClientProvider);
final response = await client.get('/id/accounts/me/credits');
if (response.statusCode != 200) {
throw Exception('Failed to load social credits');
}
return response.data?.toDouble() ?? 0.0;
}
@riverpod
class SocialCreditHistoryNotifier extends _$SocialCreditHistoryNotifier
with CursorPagingNotifierMixin<SnSocialCreditRecord> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnSocialCreditRecord>> build() => fetch(cursor: null);
@override
Future<CursorPagingData<SnSocialCreditRecord>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final queryParams = {'offset': offset, 'take': _pageSize};
final response = await client.get(
'/id/accounts/me/credits/history',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final records =
data.map((json) => SnSocialCreditRecord.fromJson(json)).toList();
final hasMore = offset + records.length < total;
final nextCursor = hasMore ? (offset + records.length).toString() : null;
return CursorPagingData(
items: records,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
class SocialCreditsScreen extends HookConsumerWidget {
const SocialCreditsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final socialCredits = ref.watch(socialCreditsProvider);
return AppScaffold(
appBar: AppBar(title: Text('socialCredits').tr()),
body: Column(
children: [
Card(
margin: EdgeInsets.only(left: 16, right: 16, top: 8),
child: socialCredits
.when(
data:
(credits) => Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
credits < 100
? 'socialCreditsLevelPoor'.tr()
: credits < 150
? 'socialCreditsLevelNormal'.tr()
: credits < 200
? 'socialCreditsLevelGood'.tr()
: 'socialCreditsLevelExcellent'.tr(),
).tr().bold().fontSize(20),
Text(
'${credits.toStringAsFixed(2)} pts',
).fontSize(14),
const Gap(8),
LinearProgressIndicator(value: credits / 200),
],
),
Positioned(
right: 0,
top: 0,
child: IconButton(
onPressed: () {},
icon: const Icon(Symbols.info),
tooltip: 'socialCreditsDescription'.tr(),
),
),
],
),
error: (_, _) => Text('Error loading credits'),
loading: () => const LinearProgressIndicator(),
)
.padding(horizontal: 20, vertical: 16),
),
Expanded(
child: PagingHelperView(
provider: socialCreditHistoryNotifierProvider,
futureRefreshable: socialCreditHistoryNotifierProvider.future,
notifierRefreshable: socialCreditHistoryNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final record = data.items[index];
return ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text(record.reason),
subtitle: Text(
DateFormat.yMMMd().format(record.createdAt),
),
trailing: Text(
record.delta > 0
? '+${record.delta}'
: '${record.delta}',
style: TextStyle(
color: record.delta > 0 ? Colors.green : Colors.red,
),
),
);
},
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,49 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'credits.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$socialCreditsHash() => r'2599844e892127ee4d315caced5c10e4dbaea142';
/// See also [socialCredits].
@ProviderFor(socialCredits)
final socialCreditsProvider = AutoDisposeFutureProvider<double>.internal(
socialCredits,
name: r'socialCreditsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$socialCreditsHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef SocialCreditsRef = AutoDisposeFutureProviderRef<double>;
String _$socialCreditHistoryNotifierHash() =>
r'950db020754160f835c64cedf3fa2175e61e4d64';
/// See also [SocialCreditHistoryNotifier].
@ProviderFor(SocialCreditHistoryNotifier)
final socialCreditHistoryNotifierProvider = AutoDisposeAsyncNotifierProvider<
SocialCreditHistoryNotifier,
CursorPagingData<SnSocialCreditRecord>
>.internal(
SocialCreditHistoryNotifier.new,
name: r'socialCreditHistoryNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$socialCreditHistoryNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SocialCreditHistoryNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnSocialCreditRecord>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart';
import 'package:island/models/wallet.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
@@ -19,6 +20,7 @@ import 'package:island/widgets/payment/payment_overlay.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
part 'leveling.g.dart';
@@ -35,13 +37,49 @@ Future<SnWalletSubscription?> accountStellarSubscription(Ref ref) async {
}
}
@riverpod
class LevelingHistoryNotifier extends _$LevelingHistoryNotifier
with CursorPagingNotifierMixin<SnExperienceRecord> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnExperienceRecord>> build() => fetch(cursor: null);
@override
Future<CursorPagingData<SnExperienceRecord>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final queryParams = {'offset': offset, 'take': _pageSize};
final response = await client.get(
'/id/accounts/me/leveling',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final records =
data.map((json) => SnExperienceRecord.fromJson(json)).toList();
final hasMore = offset + records.length < total;
final nextCursor = hasMore ? (offset + records.length).toString() : null;
return CursorPagingData(
items: records,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
class LevelingScreen extends HookConsumerWidget {
const LevelingScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userInfoProvider);
final stellarSubscription = ref.watch(accountStellarSubscriptionProvider);
if (user.value == null) {
return AppScaffold(
@@ -50,47 +88,166 @@ class LevelingScreen extends HookConsumerWidget {
);
}
final currentLevel = user.value!.profile.level;
final currentExp = user.value!.profile.experience;
final progress = user.value!.profile.levelingProgress;
return AppScaffold(
appBar: AppBar(title: Text('levelingProgress'.tr())),
body: SingleChildScrollView(
padding: getTabbedPadding(context, horizontal: 20, vertical: 20),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Current Progress Card
LevelingProgressCard(
level: currentLevel,
experience: currentExp,
progress: progress,
),
const Gap(24),
// Level Stairs Graph
Text(
'levelProgress'.tr(),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
return DefaultTabController(
length: 2,
child: AppScaffold(
appBar: AppBar(
title: Text('levelingProgress'.tr()),
bottom: TabBar(
tabs: [
Tab(
child: Text(
'leveling'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
const Gap(16),
),
Tab(
child: Text(
'stellarProgram'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),
body: TabBarView(
children: [
_buildLevelingTab(context, ref, user.value!),
_buildStellarProgramTab(context, ref),
],
),
),
);
}
// Stairs visualization with fixed height and horizontal scroll
_buildLevelStairs(context, currentLevel),
Widget _buildLevelingTab(
BuildContext context,
WidgetRef ref,
SnAccount user,
) {
final currentLevel = user.profile.level;
final currentExp = user.profile.experience;
final progress = user.profile.levelingProgress;
const Gap(24),
return Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20),
constraints: const BoxConstraints(maxWidth: 480),
child: CustomScrollView(
slivers: [
const SliverGap(20),
// Membership section
_buildMembershipSection(context, ref, stellarSubscription),
const Gap(16),
],
// Current Progress Card
SliverToBoxAdapter(
child: LevelingProgressCard(
level: currentLevel,
experience: currentExp,
progress: progress,
),
),
const SliverGap(24),
// Level Stairs Graph
SliverToBoxAdapter(
child: Text(
'levelProgress'.tr(),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
const SliverGap(16),
// Stairs visualization with fixed height and horizontal scroll
SliverToBoxAdapter(child: _buildLevelStairs(context, currentLevel)),
const SliverGap(24),
// Leveling History
SliverToBoxAdapter(
child: Text(
'levelingHistory'.tr(),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
const SliverGap(8),
PagingHelperSliverView(
provider: levelingHistoryNotifierProvider,
futureRefreshable: levelingHistoryNotifierProvider.future,
notifierRefreshable: levelingHistoryNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final record = data.items[index];
return ListTile(
title: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(record.reason),
Row(
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: [
Text(
'${record.delta > 0 ? '+' : ''}${record.delta} EXP',
),
if (record.bonusMultiplier != 1.0)
Text('x${record.bonusMultiplier}'),
],
),
minTileHeight: 56,
contentPadding: EdgeInsets.symmetric(horizontal: 4),
);
},
),
),
SliverGap(getTabbedPadding(context, vertical: 20).vertical),
],
),
),
);
}
Widget _buildStellarProgramTab(BuildContext context, WidgetRef ref) {
final stellarSubscription = ref.watch(accountStellarSubscriptionProvider);
return SingleChildScrollView(
padding: getTabbedPadding(context, horizontal: 20, vertical: 20),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildMembershipSection(context, ref, stellarSubscription),
const Gap(16),
],
),
),
),

View File

@@ -27,5 +27,26 @@ final accountStellarSubscriptionProvider =
// ignore: unused_element
typedef AccountStellarSubscriptionRef =
AutoDisposeFutureProviderRef<SnWalletSubscription?>;
String _$levelingHistoryNotifierHash() =>
r'e795f9b7911c9e50f15c095ea237cb0e87bf1e89';
/// See also [LevelingHistoryNotifier].
@ProviderFor(LevelingHistoryNotifier)
final levelingHistoryNotifierProvider = AutoDisposeAsyncNotifierProvider<
LevelingHistoryNotifier,
CursorPagingData<SnExperienceRecord>
>.internal(
LevelingHistoryNotifier.new,
name: r'levelingHistoryNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$levelingHistoryNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$LevelingHistoryNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnExperienceRecord>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/developer.dart';
import 'package:island/models/relationship.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/config.dart';
@@ -112,6 +113,24 @@ Future<SnRelationship?> accountRelationship(Ref ref, String uname) async {
}
}
@riverpod
Future<SnDeveloper?> accountBotDeveloper(Ref ref, String uname) async {
final account = await ref.watch(accountProvider(uname).future);
if (account.automatedId == null) return null;
final apiClient = ref.watch(apiClientProvider);
try {
final resp = await apiClient.get(
"/develop/bots/${account.automatedId}/developer",
);
return SnDeveloper.fromJson(resp.data);
} catch (err) {
if (err is DioException && err.response?.statusCode == 404) {
return null;
}
rethrow;
}
}
class AccountProfileScreen extends HookConsumerWidget {
final String name;
const AccountProfileScreen({super.key, required this.name});
@@ -128,6 +147,7 @@ class AccountProfileScreen extends HookConsumerWidget {
);
final accountChat = ref.watch(accountDirectChatProvider(name));
final accountRelationship = ref.watch(accountRelationshipProvider(name));
final accountDeveloper = ref.watch(accountBotDeveloperProvider(name));
final appbarColor = ref.watch(accountAppbarForcegroundColorProvider(name));
@@ -259,6 +279,24 @@ class AccountProfileScreen extends HookConsumerWidget {
if (data.profile.lastName.isNotEmpty) Text(data.profile.lastName),
],
),
Tooltip(
message: 'creditsStatus'.tr(),
child: Row(
spacing: 6,
children: [
Icon(Symbols.star, size: 17, fill: 1).padding(right: 2),
Text('${data.profile.socialCredits.toStringAsFixed(2)} pts'),
Text('·').bold(),
switch (data.profile.socialCreditsLevel) {
-1 => Text('socialCreditsLevelPoor').tr(),
0 => Text('socialCreditsLevelNormal').tr(),
1 => Text('socialCreditsLevelGood').tr(),
2 => Text('socialCreditsLevelExcellent').tr(),
_ => Text('unknown').tr(),
},
],
),
),
];
}
@@ -292,6 +330,19 @@ class AccountProfileScreen extends HookConsumerWidget {
),
],
),
if (accountDeveloper.value != null)
Row(
spacing: 7,
children: [
const Icon(Symbols.smart_toy, size: 18),
Text(
'botAutomatedBy'.tr(
args: [accountDeveloper.value!.publisher!.nick],
),
).fontSize(13),
],
).opacity(0.75),
const Gap(4),
AccountStatusWidget(uname: name, padding: EdgeInsets.zero),
],
),

View File

@@ -639,5 +639,128 @@ class _AccountRelationshipProviderElement
String get uname => (origin as AccountRelationshipProvider).uname;
}
String _$accountBotDeveloperHash() =>
r'673534770640a8cf1484ea0af0f4d0ef283ef157';
/// See also [accountBotDeveloper].
@ProviderFor(accountBotDeveloper)
const accountBotDeveloperProvider = AccountBotDeveloperFamily();
/// See also [accountBotDeveloper].
class AccountBotDeveloperFamily extends Family<AsyncValue<SnDeveloper?>> {
/// See also [accountBotDeveloper].
const AccountBotDeveloperFamily();
/// See also [accountBotDeveloper].
AccountBotDeveloperProvider call(String uname) {
return AccountBotDeveloperProvider(uname);
}
@override
AccountBotDeveloperProvider getProviderOverride(
covariant AccountBotDeveloperProvider provider,
) {
return call(provider.uname);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'accountBotDeveloperProvider';
}
/// See also [accountBotDeveloper].
class AccountBotDeveloperProvider
extends AutoDisposeFutureProvider<SnDeveloper?> {
/// See also [accountBotDeveloper].
AccountBotDeveloperProvider(String uname)
: this._internal(
(ref) => accountBotDeveloper(ref as AccountBotDeveloperRef, uname),
from: accountBotDeveloperProvider,
name: r'accountBotDeveloperProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountBotDeveloperHash,
dependencies: AccountBotDeveloperFamily._dependencies,
allTransitiveDependencies:
AccountBotDeveloperFamily._allTransitiveDependencies,
uname: uname,
);
AccountBotDeveloperProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.uname,
}) : super.internal();
final String uname;
@override
Override overrideWith(
FutureOr<SnDeveloper?> Function(AccountBotDeveloperRef provider) create,
) {
return ProviderOverride(
origin: this,
override: AccountBotDeveloperProvider._internal(
(ref) => create(ref as AccountBotDeveloperRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeFutureProviderElement<SnDeveloper?> createElement() {
return _AccountBotDeveloperProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AccountBotDeveloperProvider && other.uname == uname;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, uname.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AccountBotDeveloperRef on AutoDisposeFutureProviderRef<SnDeveloper?> {
/// The parameter `uname` of this provider.
String get uname;
}
class _AccountBotDeveloperProviderElement
extends AutoDisposeFutureProviderElement<SnDeveloper?>
with AccountBotDeveloperRef {
_AccountBotDeveloperProviderElement(super.provider);
@override
String get uname => (origin as AccountBotDeveloperProvider).uname;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -79,33 +79,38 @@ class ChatRoomListTile extends HookConsumerWidget {
color: Theme.of(context).colorScheme.primary,
),
),
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,
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,
),
),
Align(
alignment: Alignment.centerRight,
child: Text(
RelativeTime(context).format(data.lastMessage.createdAt),
style: Theme.of(context).textTheme.bodySmall,
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,
),
),
],
),
],
);
},

View File

@@ -72,6 +72,207 @@ class _AppLifecycleObserver extends WidgetsBindingObserver {
}
}
class _PublicRoomPreview extends HookConsumerWidget {
final String id;
final SnChatRoom room;
const _PublicRoomPreview({required this.id, required this.room});
@override
Widget build(BuildContext context, WidgetRef ref) {
final messages = ref.watch(messagesNotifierProvider(id));
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
final scrollController = useScrollController();
final listController = useMemoized(() => ListController(), []);
var isLoading = false;
// Add scroll listener for pagination
useEffect(() {
void onScroll() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (isLoading) return;
isLoading = true;
messagesNotifier.loadMore().then((_) => isLoading = false);
}
}
scrollController.addListener(onScroll);
return () => scrollController.removeListener(onScroll);
}, [scrollController]);
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
SuperListView.builder(
listController: listController,
padding: EdgeInsets.symmetric(vertical: 16),
controller: scrollController,
reverse: true, // Show newest messages at the bottom
itemCount: messageList.length,
findChildIndexCallback: (key) {
final valueKey = key as ValueKey;
final messageId = valueKey.value as String;
return messageList.indexWhere((m) => m.id == messageId);
},
extentEstimation: (_, _) => 40,
itemBuilder: (context, index) {
final message = messageList[index];
final nextMessage =
index < messageList.length - 1 ? messageList[index + 1] : null;
final isLastInGroup =
nextMessage == null ||
nextMessage.senderId != message.senderId ||
nextMessage.createdAt
.difference(message.createdAt)
.inMinutes
.abs() >
3;
return MessageItem(
message: message,
isCurrentUser: false, // User is not a member, so not current user
onAction: null, // No actions allowed in preview mode
onJump: (_) {}, // No jump functionality in preview
progress: null,
showAvatar: isLastInGroup,
);
},
);
final compactHeader = isWideScreen(context);
Widget comfortHeaderWidget() => Column(
spacing: 4,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 26,
width: 26,
child:
(room.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
room.members!
.map((e) => e.account.profile.picture?.id)
.toList(),
)
: room.picture?.id != null
? ProfilePictureWidget(
fileId: room.picture?.id,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
child: Text(
room.name![0].toUpperCase(),
style: const TextStyle(fontSize: 12),
),
),
),
Text(
(room.type == 1 && room.name == null)
? room.members!.map((e) => e.account.nick).join(', ')
: room.name!,
).fontSize(15),
],
);
Widget compactHeaderWidget() => Row(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 26,
width: 26,
child:
(room.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
room.members!
.map((e) => e.account.profile.picture?.id)
.toList(),
)
: room.picture?.id != null
? ProfilePictureWidget(
fileId: room.picture?.id,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
child: Text(
room.name![0].toUpperCase(),
style: const TextStyle(fontSize: 12),
),
),
),
Text(
(room.type == 1 && room.name == null)
? room.members!.map((e) => e.account.nick).join(', ')
: room.name!,
).fontSize(19),
],
);
return AppScaffold(
appBar: AppBar(
leading: !compactHeader ? const Center(child: PageBackButton()) : null,
automaticallyImplyLeading: false,
toolbarHeight: compactHeader ? null : 64,
title: compactHeader ? compactHeaderWidget() : comfortHeaderWidget(),
actions: [
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
context.pushNamed('chatDetail', pathParameters: {'id': id});
},
),
const Gap(8),
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: chatMessageListWidget(messageList),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
),
),
// Join button at the bottom for public rooms
Container(
padding: const EdgeInsets.all(16),
child: FilledButton.tonalIcon(
onPressed: () async {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
await apiClient.post('/sphere/chat/${room.id}/members/me');
ref.invalidate(chatroomIdentityProvider(id));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
},
label: Text('chatJoin').tr(),
icon: const Icon(Icons.add),
),
),
],
),
);
}
}
@riverpod
class MessagesNotifier extends _$MessagesNotifier {
late final Dio _apiClient;
@@ -82,6 +283,9 @@ class MessagesNotifier extends _$MessagesNotifier {
final Map<String, LocalChatMessage> _pendingMessages = {};
final Map<String, Map<int, double>> _fileUploadProgress = {};
int? _totalCount;
String? _searchQuery;
bool? _withLinks;
bool? _withAttachments;
late final String _roomId;
int _currentPage = 0;
@@ -96,28 +300,42 @@ class MessagesNotifier extends _$MessagesNotifier {
_database = ref.watch(databaseProvider);
final room = await ref.watch(chatroomProvider(roomId).future);
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
if (room == null || identity == null) {
throw Exception('Room or identity not found');
if (room == null) {
throw Exception('Room not found');
}
_room = room;
_identity = identity;
// Allow building even if identity is null for public rooms
if (identity != null) {
_identity = identity;
}
developer.log(
'MessagesNotifier built for room $roomId',
name: 'MessagesNotifier',
);
ref.listen(appLifecycleStateProvider, (_, next) {
if (next.hasValue && next.value == AppLifecycleState.resumed) {
developer.log(
'App resumed, syncing messages',
name: 'MessagesNotifier',
);
syncMessages();
}
});
// Only setup sync and lifecycle listeners if user is a member
if (identity != null) {
ref.listen(appLifecycleStateProvider, (_, next) {
if (next.hasValue && next.value == AppLifecycleState.resumed) {
developer.log(
'App resumed, syncing messages',
name: 'MessagesNotifier',
);
syncMessages();
}
});
}
return await loadInitial();
loadInitial();
return [];
}
List<LocalChatMessage> _sortMessages(List<LocalChatMessage> messages) {
messages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
return messages;
}
Future<List<LocalChatMessage>> _getCachedMessages({
@@ -128,13 +346,32 @@ class MessagesNotifier extends _$MessagesNotifier {
'Getting cached messages from offset $offset, take $take',
name: 'MessagesNotifier',
);
final dbMessages = await _database.getMessagesForRoom(
_roomId,
offset: offset,
limit: take,
);
final dbLocalMessages =
dbMessages.map(_database.companionToMessage).toList();
final List<LocalChatMessage> dbMessages;
if (_searchQuery != null && _searchQuery!.isNotEmpty) {
dbMessages = await _database.searchMessages(_roomId, _searchQuery ?? '');
} else {
final chatMessagesFromDb = await _database.getMessagesForRoom(
_roomId,
offset: offset,
limit: take,
);
dbMessages =
chatMessagesFromDb.map(_database.companionToMessage).toList();
}
List<LocalChatMessage> filteredMessages = dbMessages;
if (_withLinks == true) {
filteredMessages =
filteredMessages.where((msg) => _hasLink(msg)).toList();
}
if (_withAttachments == true) {
filteredMessages =
filteredMessages.where((msg) => _hasAttachment(msg)).toList();
}
final dbLocalMessages = filteredMessages;
if (offset == 0) {
final pendingForRoom =
@@ -143,7 +380,7 @@ class MessagesNotifier extends _$MessagesNotifier {
.toList();
final allMessages = [...pendingForRoom, ...dbLocalMessages];
allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
_sortMessages(allMessages); // Use the helper function
final uniqueMessages = <LocalChatMessage>[];
final seenIds = <String>{};
@@ -218,7 +455,7 @@ class MessagesNotifier extends _$MessagesNotifier {
_isSyncing = true;
developer.log('Starting message sync', name: 'MessagesNotifier');
ref.read(isSyncingProvider.notifier).state = true;
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
try {
final dbMessages = await _database.getMessagesForRoom(
_room.id,
@@ -279,7 +516,9 @@ class MessagesNotifier extends _$MessagesNotifier {
showErrorAlert(err);
} finally {
developer.log('Finished message sync', name: 'MessagesNotifier');
ref.read(isSyncingProvider.notifier).state = false;
Future.microtask(
() => ref.read(isSyncingProvider.notifier).state = false,
);
_isSyncing = false;
}
}
@@ -290,7 +529,9 @@ class MessagesNotifier extends _$MessagesNotifier {
bool synced = false,
}) async {
try {
if (offset == 0 && !synced) {
if (offset == 0 &&
!synced &&
(_searchQuery == null || _searchQuery!.isEmpty)) {
_fetchAndCacheMessages(offset: 0, take: take).catchError((_) {
return <LocalChatMessage>[];
});
@@ -305,7 +546,11 @@ class MessagesNotifier extends _$MessagesNotifier {
return localMessages;
}
return await _fetchAndCacheMessages(offset: offset, take: take);
if (_searchQuery == null || _searchQuery!.isEmpty) {
return await _fetchAndCacheMessages(offset: offset, take: take);
} else {
return []; // If searching, and no local messages, don't fetch from network
}
} catch (e) {
final localMessages = await _getCachedMessages(
offset: offset,
@@ -319,13 +564,15 @@ class MessagesNotifier extends _$MessagesNotifier {
}
}
Future<List<LocalChatMessage>> loadInitial() async {
Future<void> loadInitial() async {
developer.log('Loading initial messages', name: 'MessagesNotifier');
syncMessages();
if (_searchQuery == null || _searchQuery!.isEmpty) {
syncMessages();
}
final messages = await _getCachedMessages(offset: 0, take: 100);
_currentPage = 0;
_hasMore = messages.length == _pageSize;
return messages;
state = AsyncValue.data(messages);
}
Future<void> loadMore() async {
@@ -344,7 +591,9 @@ class MessagesNotifier extends _$MessagesNotifier {
_hasMore = false;
}
state = AsyncValue.data([...currentMessages, ...newMessages]);
state = AsyncValue.data(
_sortMessages([...currentMessages, ...newMessages]),
);
} catch (err, stackTrace) {
developer.log(
'Error loading more messages',
@@ -455,10 +704,13 @@ class MessagesNotifier extends _$MessagesNotifier {
final currentMessages = state.value ?? [];
if (editingTo != null) {
final newMessages = currentMessages
.where((m) => m.id != localMessage.id) // remove pending message
.map((m) => m.id == editingTo.id ? updatedMessage : m) // update original message
.toList();
final newMessages =
currentMessages
.where((m) => m.id != localMessage.id) // remove pending message
.map(
(m) => m.id == editingTo.id ? updatedMessage : m,
) // update original message
.toList();
state = AsyncValue.data(newMessages);
} else {
final newMessages =
@@ -566,7 +818,7 @@ class MessagesNotifier extends _$MessagesNotifier {
}
return m;
}).toList();
state = AsyncValue.data(newMessages);
state = AsyncValue.data(_sortMessages(newMessages));
showErrorAlert(e);
}
}
@@ -626,7 +878,7 @@ class MessagesNotifier extends _$MessagesNotifier {
if (index >= 0) {
final newList = [...currentMessages];
newList[index] = updatedMessage;
state = AsyncValue.data(newList);
state = AsyncValue.data(_sortMessages(newList));
}
}
@@ -686,6 +938,20 @@ class MessagesNotifier extends _$MessagesNotifier {
}
}
void searchMessages(String query, {bool? withLinks, bool? withAttachments}) {
_searchQuery = query.trim();
_withLinks = withLinks;
_withAttachments = withAttachments;
loadInitial();
}
void clearSearch() {
_searchQuery = null;
_withLinks = null;
_withAttachments = null;
loadInitial();
}
Future<LocalChatMessage?> fetchMessageById(String messageId) async {
developer.log(
'Fetching message by id $messageId',
@@ -715,6 +981,18 @@ class MessagesNotifier extends _$MessagesNotifier {
rethrow;
}
}
bool _hasLink(LocalChatMessage message) {
final content = message.toRemoteMessage().content;
if (content == null) return false;
final urlRegex = RegExp(r'https?://[^\s/$.?#].[^\s]*');
return urlRegex.hasMatch(content);
}
bool _hasAttachment(LocalChatMessage message) {
final remoteMessage = message.toRemoteMessage();
return remoteMessage.attachments.isNotEmpty;
}
}
class ChatRoomScreen extends HookConsumerWidget {
@@ -734,57 +1012,77 @@ class ChatRoomScreen extends HookConsumerWidget {
);
} else if (chatIdentity.value == null) {
// Identity was not found, user was not joined
return AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: Center(
child:
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 280),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
chatRoom.value?.isCommunity == true
? Symbols.person_add
: Symbols.person_remove,
size: 36,
fill: 1,
).padding(bottom: 4),
Text('chatNotJoined').tr(),
if (chatRoom.value?.isCommunity != true)
Text(
'chatUnableJoin',
textAlign: TextAlign.center,
).tr().bold()
else
FilledButton.tonalIcon(
onPressed: () async {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
if (chatRoom.value == null) {
hideLoadingModal(context);
return;
}
await apiClient.post(
'/sphere/chat/${chatRoom.value!.id}/members/me',
);
ref.invalidate(chatroomIdentityProvider(id));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
},
label: Text('chatJoin').tr(),
icon: const Icon(Icons.add),
).padding(top: 8),
],
),
).center(),
),
return chatRoom.when(
data: (room) {
if (room!.isPublic) {
// Show public room preview with messages but no input
return _PublicRoomPreview(id: id, room: room);
} else {
// Show regular "not joined" screen for private rooms
return AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: Center(
child:
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 280),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
room.isCommunity == true
? Symbols.person_add
: Symbols.person_remove,
size: 36,
fill: 1,
).padding(bottom: 4),
Text('chatNotJoined').tr(),
if (room.isCommunity != true)
Text(
'chatUnableJoin',
textAlign: TextAlign.center,
).tr().bold()
else
FilledButton.tonalIcon(
onPressed: () async {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
await apiClient.post(
'/sphere/chat/${room.id}/members/me',
);
ref.invalidate(chatroomIdentityProvider(id));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) {
hideLoadingModal(context);
}
}
},
label: Text('chatJoin').tr(),
icon: const Icon(Icons.add),
).padding(top: 8),
],
),
).center(),
),
);
}
},
loading:
() => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: CircularProgressIndicator().center(),
),
error:
(error, _) => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: ResponseErrorWidget(
error: error,
onRetry: () => ref.refresh(chatroomProvider(id)),
),
),
);
}
@@ -1549,7 +1847,7 @@ class _ChatInput extends HookConsumerWidget {
children: [
IconButton(
tooltip: 'stickers'.tr(),
icon: const Icon(Symbols.emoji_symbols),
icon: const Icon(Symbols.add_reaction),
onPressed: () {
final size = MediaQuery.of(context).size;
showStickerPickerPopover(
@@ -1659,8 +1957,13 @@ class _ChatInput extends HookConsumerWidget {
horizontal: 12,
vertical: 4,
),
counterText:
messageController.text.length > 1024
? '${messageController.text.length}/4096'
: null,
),
maxLines: null,
maxLines: 3,
minLines: 1,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),

View File

@@ -6,7 +6,7 @@ part of 'room.dart';
// RiverpodGenerator
// **************************************************************************
String _$messagesNotifierHash() => r'32afe6ea24086d869cc47bd3389c8fd734409ca0';
String _$messagesNotifierHash() => r'fc3b66dfb8dd3fc55d142dae5c5e7bdc67eca5d4';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/chat.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/chat/chat.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/alert.dart';
@@ -19,10 +20,17 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/pods/database.dart';
part 'room_detail.freezed.dart';
part 'room_detail.g.dart';
@riverpod
Future<int> totalMessagesCount(Ref ref, String roomId) async {
final database = ref.watch(databaseProvider);
return database.getTotalMessagesForRoom(roomId);
}
class ChatDetailScreen extends HookConsumerWidget {
final String id;
const ChatDetailScreen({super.key, required this.id});
@@ -31,6 +39,7 @@ class ChatDetailScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final roomState = ref.watch(chatroomProvider(id));
final roomIdentity = ref.watch(chatroomIdentityProvider(id));
final totalMessages = ref.watch(totalMessagesCountProvider(id));
const kNotifyLevelText = [
'chatNotifyLevelAll',
@@ -131,7 +140,7 @@ class ChatDetailScreen extends HookConsumerWidget {
const Text('chatBreakDescription').tr(),
const Gap(16),
ListTile(
title: const Text('Clear').tr(),
title: const Text('chatBreakClearButton').tr(),
subtitle: const Text('chatBreakClear').tr(),
leading: const Icon(Icons.notifications_active),
onTap: () {
@@ -143,8 +152,8 @@ class ChatDetailScreen extends HookConsumerWidget {
},
),
ListTile(
title: const Text('5m'),
subtitle: const Text('chatBreakHour').tr(args: ['5m']),
title: const Text('chatBreak5m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak5m'.tr()]),
leading: const Icon(Symbols.circle),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 5)));
@@ -155,8 +164,8 @@ class ChatDetailScreen extends HookConsumerWidget {
},
),
ListTile(
title: const Text('10m'),
subtitle: const Text('chatBreakHour').tr(args: ['10m']),
title: const Text('chatBreak10m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak10m'.tr()]),
leading: const Icon(Symbols.circle),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 10)));
@@ -167,8 +176,8 @@ class ChatDetailScreen extends HookConsumerWidget {
},
),
ListTile(
title: const Text('15m'),
subtitle: const Text('chatBreakHour').tr(args: ['15m']),
title: const Text('chatBreak15m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak15m'.tr()]),
leading: const Icon(Symbols.timer_3),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 15)));
@@ -179,8 +188,8 @@ class ChatDetailScreen extends HookConsumerWidget {
},
),
ListTile(
title: const Text('30m'),
subtitle: const Text('chatBreakHour').tr(args: ['30m']),
title: const Text('chatBreak30m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak30m'.tr()]),
leading: const Icon(Symbols.timer),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 30)));
@@ -194,8 +203,8 @@ class ChatDetailScreen extends HookConsumerWidget {
TextField(
controller: durationController,
decoration: InputDecoration(
labelText: 'Custom (minutes)'.tr(),
hintText: 'Enter minutes'.tr(),
labelText: 'chatBreakCustomMinutes'.tr(),
hintText: 'chatBreakEnterMinutes'.tr(),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: const Icon(Icons.check),
@@ -238,7 +247,7 @@ class ChatDetailScreen extends HookConsumerWidget {
return AppScaffold(
body: roomState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),
error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))),
data:
(currentRoom) => CustomScrollView(
slivers: [
@@ -358,6 +367,22 @@ class ChatDetailScreen extends HookConsumerWidget {
: const Text('chatBreakNone').tr(),
onTap: () => showChatBreakDialog(),
),
ListTile(
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Icons.search),
trailing: const Icon(Symbols.chevron_right),
title: const Text('searchMessages').tr(),
subtitle: totalMessages.when(
data: (count) => Text('messagesCount'.tr(args: [count.toString()])),
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('errorGeneric'.tr(args: [err.toString()])),
),
onTap: () {
context.pushNamed('searchMessages', pathParameters: {'id': id});
},
),
],
),
error: (_, _) => const SizedBox.shrink(),
@@ -666,8 +691,11 @@ class _ChatMemberListSheet extends HookConsumerWidget {
final member = data.items[index];
return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget(
fileId: member.account.profile.picture?.id,
leading: AccountPfcGestureDetector(
uname: member.account.name,
child: ProfilePictureWidget(
fileId: member.account.profile.picture?.id,
),
),
title: Row(
spacing: 6,
@@ -688,7 +716,7 @@ class _ChatMemberListSheet extends HookConsumerWidget {
? 'permissionModerator'
: 'permissionMember',
).tr(),
Text('·').bold().padding(horizontal: 6),
Text('dotSeparator').bold().padding(horizontal: 6),
Expanded(child: Text("@${member.account.name}")),
],
),
@@ -848,7 +876,7 @@ class _ChatMemberRoleSheet extends HookConsumerWidget {
try {
final newRole = int.parse(roleController.text);
if (newRole < 0 || newRole > 100) {
throw 'Role must be between 0 and 100';
throw 'roleValidationHint'.tr();
}
final apiClient = ref.read(apiClientProvider);

View File

@@ -6,8 +6,8 @@ part of 'room_detail.dart';
// RiverpodGenerator
// **************************************************************************
String _$chatMemberListNotifierHash() =>
r'c8fbf4b95df6dae24b1ba21063e9a43351832974';
String _$totalMessagesCountHash() =>
r'a15c03461f25c2d4d39c0926509bf626ae2550a6';
/// Copied from Dart SDK
class _SystemHash {
@@ -30,6 +30,128 @@ class _SystemHash {
}
}
/// See also [totalMessagesCount].
@ProviderFor(totalMessagesCount)
const totalMessagesCountProvider = TotalMessagesCountFamily();
/// See also [totalMessagesCount].
class TotalMessagesCountFamily extends Family<AsyncValue<int>> {
/// See also [totalMessagesCount].
const TotalMessagesCountFamily();
/// See also [totalMessagesCount].
TotalMessagesCountProvider call(String roomId) {
return TotalMessagesCountProvider(roomId);
}
@override
TotalMessagesCountProvider getProviderOverride(
covariant TotalMessagesCountProvider provider,
) {
return call(provider.roomId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'totalMessagesCountProvider';
}
/// See also [totalMessagesCount].
class TotalMessagesCountProvider extends AutoDisposeFutureProvider<int> {
/// See also [totalMessagesCount].
TotalMessagesCountProvider(String roomId)
: this._internal(
(ref) => totalMessagesCount(ref as TotalMessagesCountRef, roomId),
from: totalMessagesCountProvider,
name: r'totalMessagesCountProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$totalMessagesCountHash,
dependencies: TotalMessagesCountFamily._dependencies,
allTransitiveDependencies:
TotalMessagesCountFamily._allTransitiveDependencies,
roomId: roomId,
);
TotalMessagesCountProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.roomId,
}) : super.internal();
final String roomId;
@override
Override overrideWith(
FutureOr<int> Function(TotalMessagesCountRef provider) create,
) {
return ProviderOverride(
origin: this,
override: TotalMessagesCountProvider._internal(
(ref) => create(ref as TotalMessagesCountRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
roomId: roomId,
),
);
}
@override
AutoDisposeFutureProviderElement<int> createElement() {
return _TotalMessagesCountProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is TotalMessagesCountProvider && other.roomId == roomId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, roomId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin TotalMessagesCountRef on AutoDisposeFutureProviderRef<int> {
/// The parameter `roomId` of this provider.
String get roomId;
}
class _TotalMessagesCountProviderElement
extends AutoDisposeFutureProviderElement<int>
with TotalMessagesCountRef {
_TotalMessagesCountProviderElement(super.provider);
@override
String get roomId => (origin as TotalMessagesCountProvider).roomId;
}
String _$chatMemberListNotifierHash() =>
r'3ea30150278523e9f6b23f9200ea9a9fbae9c973';
abstract class _$ChatMemberListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnChatMember>> {
late final String roomId;

View File

@@ -0,0 +1,139 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/screens/chat/room.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/message_item.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
class SearchMessagesScreen extends HookConsumerWidget {
final String roomId;
const SearchMessagesScreen({super.key, required this.roomId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final searchController = useTextEditingController();
final withLinks = useState(false);
final withAttachments = useState(false);
final messagesNotifier = ref.read(
messagesNotifierProvider(roomId).notifier,
);
final messages = ref.watch(messagesNotifierProvider(roomId));
useEffect(() {
// Clear search when screen is disposed
return () {
messagesNotifier.clearSearch();
};
}, []);
return AppScaffold(
appBar: AppBar(title: const Text('searchMessages').tr()),
body: Column(
children: [
Column(
children: [
TextField(
controller: searchController,
decoration: InputDecoration(
hintText: 'searchMessagesHint'.tr(),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.only(
left: 16,
right: 16,
top: 12,
bottom: 16,
),
suffix: IconButton(
iconSize: 18,
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.clear),
onPressed: () {
searchController.clear();
messagesNotifier.clearSearch();
},
),
),
onChanged: (query) {
messagesNotifier.searchMessages(
query,
withLinks: withLinks.value,
withAttachments: withAttachments.value,
);
},
),
Row(
children: [
Expanded(
child: CheckboxListTile(
secondary: const Icon(Symbols.link),
title: const Text('searchLinks').tr(),
value: withLinks.value,
onChanged: (bool? value) {
withLinks.value = value!;
messagesNotifier.searchMessages(
searchController.text,
withLinks: withLinks.value,
withAttachments: withAttachments.value,
);
},
),
),
Expanded(
child: CheckboxListTile(
secondary: const Icon(Symbols.file_copy),
title: const Text('searchAttachments').tr(),
value: withAttachments.value,
onChanged: (bool? value) {
withAttachments.value = value!;
messagesNotifier.searchMessages(
searchController.text,
withLinks: withLinks.value,
withAttachments: withAttachments.value,
);
},
),
),
],
),
],
),
const Divider(height: 1),
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('noMessagesFound'.tr()))
: SuperListView.builder(
padding: const EdgeInsets.symmetric(vertical: 16),
reverse: true, // Show newest messages at the bottom
itemCount: messageList.length,
itemBuilder: (context, index) {
final message = messageList[index];
// Simplified MessageItem for search results, no grouping logic
return MessageItem(
message: message,
isCurrentUser:
false, // Or determine based on actual user
onAction: null,
onJump: (_) {},
progress: null,
showAvatar: true,
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))),
),
),
],
),
);
}
}

View File

@@ -106,11 +106,7 @@ class StickerPacksNotifier extends _$StickerPacksNotifier
try {
final response = await client.get(
'/sphere/stickers',
queryParameters: {
'offset': offset,
'take': _pageSize,
'pubName': pubName,
},
queryParameters: {'offset': offset, 'take': _pageSize, 'pub': pubName},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');

View File

@@ -148,7 +148,7 @@ class _StickerPackProviderElement
}
String _$stickerPacksNotifierHash() =>
r'0a8edcf9c35396c411f1214f5e77b1e8fac6a3e6';
r'30024b35235f3085a5b1ec2204d0a974ee907e22';
abstract class _$StickerPacksNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnStickerPack>> {

View File

@@ -0,0 +1,156 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/custom_app.dart';
import 'package:island/screens/developers/app_secrets.dart';
import 'package:island/screens/developers/apps.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class AppDetailScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
final String appId;
const AppDetailScreen({
super.key,
required this.publisherName,
required this.projectId,
required this.appId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabController = useTabController(initialLength: 2);
final appData = ref.watch(
customAppProvider(publisherName, projectId, appId),
);
return AppScaffold(
appBar: AppBar(
title: Text(appData.value?.name ?? 'appDetails'.tr()),
actions: [
IconButton(
icon: const Icon(Symbols.edit),
onPressed:
appData.value == null
? null
: () {
context.pushNamed(
'developerAppEdit',
pathParameters: {
'name': publisherName,
'projectId': projectId,
'id': appId,
},
);
},
),
],
bottom: TabBar(
controller: tabController,
tabs: [
Tab(
child: Text(
'overview'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'secrets'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),
body: appData.when(
data: (app) {
return TabBarView(
controller: tabController,
physics: const NeverScrollableScrollPhysics(),
children: [
_AppOverview(app: app),
AppSecretsScreen(
publisherName: publisherName,
projectId: projectId,
appId: appId,
),
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry:
() => ref.invalidate(
customAppProvider(publisherName, projectId, appId),
),
),
),
);
}
}
class _AppOverview extends StatelessWidget {
final CustomApp app;
const _AppOverview({required this.app});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child:
app.background != null
? CloudFileWidget(
item: app.background!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
Positioned(
left: 20,
bottom: -32,
child: ProfilePictureWidget(
fileId: app.picture?.id,
radius: 40,
fallbackIcon: Symbols.apps,
),
),
],
),
).padding(bottom: 32),
ListTile(title: Text('name'.tr()), subtitle: Text(app.name)),
ListTile(title: Text('slug'.tr()), subtitle: Text(app.slug)),
if (app.description?.isNotEmpty ?? false)
ListTile(
title: Text('description'.tr()),
subtitle: Text(app.description!),
),
],
).padding(bottom: 24),
);
}
}

View File

@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/custom_app_secret.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app_secrets.g.dart';
@riverpod
Future<List<CustomAppSecret>> customAppSecrets(
Ref ref,
String publisherName,
String projectId,
String appId,
) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets',
);
return (resp.data as List)
.map((e) => CustomAppSecret.fromJson(e))
.cast<CustomAppSecret>()
.toList();
}
class AppSecretsScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
final String appId;
const AppSecretsScreen({
super.key,
required this.publisherName,
required this.projectId,
required this.appId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final secrets = ref.watch(
customAppSecretsProvider(publisherName, projectId, appId),
);
void showNewSecretSheet(String newSecret) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'newSecretGenerated'.tr(),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Text('copySecretHint'.tr()),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(8),
),
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()),
),
],
),
),
),
).whenComplete(() {
ref.invalidate(
customAppSecretsProvider(publisherName, projectId, appId),
);
});
}
void createSecret() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return HookBuilder(
builder: (context) {
final descriptionController = useTextEditingController();
final expiresInController = useTextEditingController();
final isOidc = useState(false);
return SheetScaffold(
titleText: 'generateSecret'.tr(),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
),
autofocus: true,
),
const SizedBox(height: 20),
TextFormField(
controller: expiresInController,
decoration: InputDecoration(
labelText: 'expiresIn'.tr(),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 20),
SwitchListTile(
title: Text('isOidc'.tr()),
value: isOidc.value,
onChanged: (value) => isOidc.value = value,
),
const SizedBox(height: 20),
FilledButton.icon(
onPressed: () async {
final description = descriptionController.text;
final expiresIn = int.tryParse(
expiresInController.text,
);
Navigator.pop(context); // Close the sheet
try {
final client = ref.read(apiClientProvider);
final resp = await client.post(
'/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets',
data: {
'description': description,
'expires_in': expiresIn,
'is_oidc': isOidc.value,
},
);
final newSecret = CustomAppSecret.fromJson(
resp.data,
);
if (newSecret.secret != null) {
showNewSecretSheet(newSecret.secret!);
}
} catch (e) {
showErrorAlert(e.toString());
}
},
icon: const Icon(Symbols.add),
label: Text('create'.tr()),
),
],
),
),
);
},
);
},
);
}
return secrets.when(
data: (data) {
return RefreshIndicator(
onRefresh:
() => ref.refresh(
customAppSecretsProvider(
publisherName,
projectId,
appId,
).future,
),
child: Column(
children: [
ListTile(
leading: const Icon(Symbols.add),
title: Text('generateSecret'.tr()),
onTap: createSecret,
),
const Divider(height: 1),
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.length,
itemBuilder: (context, index) {
final secret = data[index];
return ListTile(
title: Text(secret.description ?? secret.id),
subtitle: Text(
'createdAt'.tr(args: [secret.createdAt.formatSystem()]),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Symbols.delete, color: Colors.red),
onPressed: () {
showConfirmAlert(
'deleteSecretHint'.tr(),
'deleteSecret'.tr(),
).then((confirm) {
if (confirm) {
final client = ref.read(apiClientProvider);
client.delete(
'/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets/${secret.id}',
);
ref.invalidate(
customAppSecretsProvider(
publisherName,
projectId,
appId,
),
);
}
});
},
),
],
),
);
},
),
),
],
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry:
() => ref.invalidate(
customAppSecretsProvider(publisherName, projectId, appId),
),
),
);
}
}

View File

@@ -0,0 +1,188 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_secrets.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$customAppSecretsHash() => r'1bc62ad812487883ce739793b22a76168d656752';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [customAppSecrets].
@ProviderFor(customAppSecrets)
const customAppSecretsProvider = CustomAppSecretsFamily();
/// See also [customAppSecrets].
class CustomAppSecretsFamily extends Family<AsyncValue<List<CustomAppSecret>>> {
/// See also [customAppSecrets].
const CustomAppSecretsFamily();
/// See also [customAppSecrets].
CustomAppSecretsProvider call(
String publisherName,
String projectId,
String appId,
) {
return CustomAppSecretsProvider(publisherName, projectId, appId);
}
@override
CustomAppSecretsProvider getProviderOverride(
covariant CustomAppSecretsProvider provider,
) {
return call(provider.publisherName, provider.projectId, provider.appId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'customAppSecretsProvider';
}
/// See also [customAppSecrets].
class CustomAppSecretsProvider
extends AutoDisposeFutureProvider<List<CustomAppSecret>> {
/// See also [customAppSecrets].
CustomAppSecretsProvider(String publisherName, String projectId, String appId)
: this._internal(
(ref) => customAppSecrets(
ref as CustomAppSecretsRef,
publisherName,
projectId,
appId,
),
from: customAppSecretsProvider,
name: r'customAppSecretsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$customAppSecretsHash,
dependencies: CustomAppSecretsFamily._dependencies,
allTransitiveDependencies:
CustomAppSecretsFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
appId: appId,
);
CustomAppSecretsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.projectId,
required this.appId,
}) : super.internal();
final String publisherName;
final String projectId;
final String appId;
@override
Override overrideWith(
FutureOr<List<CustomAppSecret>> Function(CustomAppSecretsRef provider)
create,
) {
return ProviderOverride(
origin: this,
override: CustomAppSecretsProvider._internal(
(ref) => create(ref as CustomAppSecretsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
appId: appId,
),
);
}
@override
AutoDisposeFutureProviderElement<List<CustomAppSecret>> createElement() {
return _CustomAppSecretsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is CustomAppSecretsProvider &&
other.publisherName == publisherName &&
other.projectId == projectId &&
other.appId == appId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, projectId.hashCode);
hash = _SystemHash.combine(hash, appId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin CustomAppSecretsRef
on AutoDisposeFutureProviderRef<List<CustomAppSecret>> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
/// The parameter `appId` of this provider.
String get appId;
}
class _CustomAppSecretsProviderElement
extends AutoDisposeFutureProviderElement<List<CustomAppSecret>>
with CustomAppSecretsRef {
_CustomAppSecretsProviderElement(super.provider);
@override
String get publisherName =>
(origin as CustomAppSecretsProvider).publisherName;
@override
String get projectId => (origin as CustomAppSecretsProvider).projectId;
@override
String get appId => (origin as CustomAppSecretsProvider).appId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/custom_app.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
@@ -16,50 +15,98 @@ import 'package:styled_widget/styled_widget.dart';
part 'apps.g.dart';
@riverpod
Future<List<CustomApp>> customApps(Ref ref, String publisherName) async {
Future<CustomApp> customApp(
Ref ref,
String publisherName,
String projectId,
String appId,
) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/develop/developers/$publisherName/apps');
return resp.data.map((e) => CustomApp.fromJson(e)).cast<CustomApp>().toList();
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/apps/$appId',
);
return CustomApp.fromJson(resp.data);
}
@riverpod
Future<List<CustomApp>> customApps(
Ref ref,
String publisherName,
String projectId,
) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/apps',
);
return (resp.data as List)
.map((e) => CustomApp.fromJson(e))
.cast<CustomApp>()
.toList();
}
class CustomAppsScreen extends HookConsumerWidget {
final String publisherName;
const CustomAppsScreen({super.key, required this.publisherName});
final String projectId;
const CustomAppsScreen({
super.key,
required this.publisherName,
required this.projectId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final apps = ref.watch(customAppsProvider(publisherName));
final apps = ref.watch(customAppsProvider(publisherName, projectId));
return AppScaffold(
appBar: AppBar(
title: Text('customApps').tr(),
actions: [
IconButton(
icon: const Icon(Symbols.add),
onPressed: () {
context.pushNamed(
'developerAppNew',
pathParameters: {'name': publisherName},
);
},
),
],
),
body: apps.when(
data: (data) {
if (data.isEmpty) {
return Center(child: Text('noCustomApps').tr());
}
return RefreshIndicator(
onRefresh:
() => ref.refresh(customAppsProvider(publisherName).future),
child: ListView.builder(
padding: EdgeInsets.only(top: 4),
itemCount: data.length,
itemBuilder: (context, index) {
final app = data[index];
return Card(
margin: const EdgeInsets.all(8.0),
return apps.when(
data: (data) {
if (data.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('noCustomApps').tr(),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
context.pushNamed(
'developerAppNew',
pathParameters: {
'name': publisherName,
'projectId': projectId,
},
);
},
icon: const Icon(Symbols.add),
label: Text('createCustomApp').tr(),
),
],
),
);
}
return RefreshIndicator(
onRefresh:
() => ref.refresh(
customAppsProvider(publisherName, projectId).future,
),
child: ListView.builder(
padding: EdgeInsets.only(top: 4),
itemCount: data.length,
itemBuilder: (context, index) {
final app = data[index];
return Card(
margin: const EdgeInsets.all(8.0),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
context.pushNamed(
'developerAppDetail',
pathParameters: {
'name': publisherName,
'projectId': projectId,
'appId': app.id,
},
);
},
child: Column(
children: [
SizedBox(
@@ -128,6 +175,7 @@ class CustomAppsScreen extends HookConsumerWidget {
'developerAppEdit',
pathParameters: {
'name': publisherName,
'projectId': projectId,
'id': app.id,
},
);
@@ -139,10 +187,13 @@ class CustomAppsScreen extends HookConsumerWidget {
if (confirm) {
final client = ref.read(apiClientProvider);
client.delete(
'/develop/developers/$publisherName/apps/${app.id}',
'/develop/developers/$publisherName/projects/$projectId/apps/${app.id}',
);
ref.invalidate(
customAppsProvider(publisherName),
customAppsProvider(
publisherName,
projectId,
),
);
}
});
@@ -152,18 +203,21 @@ class CustomAppsScreen extends HookConsumerWidget {
),
],
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(customAppsProvider(publisherName)),
),
),
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry:
() => ref.invalidate(
customAppsProvider(publisherName, projectId),
),
),
);
}
}

View File

@@ -6,7 +6,7 @@ part of 'apps.dart';
// RiverpodGenerator
// **************************************************************************
String _$customAppsHash() => r'c6ac78060eb51a2b208a749a81ecbe0a9c608ce1';
String _$customAppHash() => r'be05431ba8bf06fd20ee988a61c3663a68e15fc9';
/// Copied from Dart SDK
class _SystemHash {
@@ -29,6 +29,148 @@ class _SystemHash {
}
}
/// See also [customApp].
@ProviderFor(customApp)
const customAppProvider = CustomAppFamily();
/// See also [customApp].
class CustomAppFamily extends Family<AsyncValue<CustomApp>> {
/// See also [customApp].
const CustomAppFamily();
/// See also [customApp].
CustomAppProvider call(String publisherName, String projectId, String appId) {
return CustomAppProvider(publisherName, projectId, appId);
}
@override
CustomAppProvider getProviderOverride(covariant CustomAppProvider provider) {
return call(provider.publisherName, provider.projectId, provider.appId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'customAppProvider';
}
/// See also [customApp].
class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp> {
/// See also [customApp].
CustomAppProvider(String publisherName, String projectId, String appId)
: this._internal(
(ref) =>
customApp(ref as CustomAppRef, publisherName, projectId, appId),
from: customAppProvider,
name: r'customAppProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$customAppHash,
dependencies: CustomAppFamily._dependencies,
allTransitiveDependencies: CustomAppFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
appId: appId,
);
CustomAppProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.projectId,
required this.appId,
}) : super.internal();
final String publisherName;
final String projectId;
final String appId;
@override
Override overrideWith(
FutureOr<CustomApp> Function(CustomAppRef provider) create,
) {
return ProviderOverride(
origin: this,
override: CustomAppProvider._internal(
(ref) => create(ref as CustomAppRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
appId: appId,
),
);
}
@override
AutoDisposeFutureProviderElement<CustomApp> createElement() {
return _CustomAppProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is CustomAppProvider &&
other.publisherName == publisherName &&
other.projectId == projectId &&
other.appId == appId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, projectId.hashCode);
hash = _SystemHash.combine(hash, appId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin CustomAppRef on AutoDisposeFutureProviderRef<CustomApp> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
/// The parameter `appId` of this provider.
String get appId;
}
class _CustomAppProviderElement
extends AutoDisposeFutureProviderElement<CustomApp>
with CustomAppRef {
_CustomAppProviderElement(super.provider);
@override
String get publisherName => (origin as CustomAppProvider).publisherName;
@override
String get projectId => (origin as CustomAppProvider).projectId;
@override
String get appId => (origin as CustomAppProvider).appId;
}
String _$customAppsHash() => r'450bedaf4220b8963cb44afeb14d4c0e80f01b11';
/// See also [customApps].
@ProviderFor(customApps)
const customAppsProvider = CustomAppsFamily();
@@ -39,15 +181,15 @@ class CustomAppsFamily extends Family<AsyncValue<List<CustomApp>>> {
const CustomAppsFamily();
/// See also [customApps].
CustomAppsProvider call(String publisherName) {
return CustomAppsProvider(publisherName);
CustomAppsProvider call(String publisherName, String projectId) {
return CustomAppsProvider(publisherName, projectId);
}
@override
CustomAppsProvider getProviderOverride(
covariant CustomAppsProvider provider,
) {
return call(provider.publisherName);
return call(provider.publisherName, provider.projectId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@@ -68,9 +210,9 @@ class CustomAppsFamily extends Family<AsyncValue<List<CustomApp>>> {
/// See also [customApps].
class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
/// See also [customApps].
CustomAppsProvider(String publisherName)
CustomAppsProvider(String publisherName, String projectId)
: this._internal(
(ref) => customApps(ref as CustomAppsRef, publisherName),
(ref) => customApps(ref as CustomAppsRef, publisherName, projectId),
from: customAppsProvider,
name: r'customAppsProvider',
debugGetCreateSourceHash:
@@ -80,6 +222,7 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
dependencies: CustomAppsFamily._dependencies,
allTransitiveDependencies: CustomAppsFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
);
CustomAppsProvider._internal(
@@ -90,9 +233,11 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.projectId,
}) : super.internal();
final String publisherName;
final String projectId;
@override
Override overrideWith(
@@ -108,6 +253,7 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
),
);
}
@@ -119,13 +265,16 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
@override
bool operator ==(Object other) {
return other is CustomAppsProvider && other.publisherName == publisherName;
return other is CustomAppsProvider &&
other.publisherName == publisherName &&
other.projectId == projectId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, projectId.hashCode);
return _SystemHash.finish(hash);
}
@@ -136,6 +285,9 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
mixin CustomAppsRef on AutoDisposeFutureProviderRef<List<CustomApp>> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
}
class _CustomAppsProviderElement
@@ -145,6 +297,8 @@ class _CustomAppsProviderElement
@override
String get publisherName => (origin as CustomAppsProvider).publisherName;
@override
String get projectId => (origin as CustomAppsProvider).projectId;
}
// ignore_for_file: type=lint

View File

@@ -0,0 +1,161 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/bot.dart';
import 'package:island/screens/developers/bot_keys.dart';
import 'package:island/screens/developers/edit_bot.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:go_router/go_router.dart';
import 'package:styled_widget/styled_widget.dart';
class BotDetailScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
final String botId;
const BotDetailScreen({
super.key,
required this.publisherName,
required this.projectId,
required this.botId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabController = useTabController(initialLength: 2);
final botData = ref.watch(botProvider(publisherName, projectId, botId));
return AppScaffold(
appBar: AppBar(
title: Text(botData.value?.account.nick ?? 'botDetails'.tr()),
actions: [
IconButton(
icon: const Icon(Symbols.edit),
onPressed:
botData.value == null
? null
: () {
context.pushNamed(
'developerBotEdit',
pathParameters: {
'name': publisherName,
'projectId': projectId,
'id': botId,
},
);
},
),
],
bottom: TabBar(
controller: tabController,
tabs: [
Tab(
child: Text(
'overview'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'keys'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),
body: botData.when(
data: (bot) {
if (bot == null) {
return Center(child: Text('botNotFound'.tr()));
}
return TabBarView(
controller: tabController,
physics: const NeverScrollableScrollPhysics(),
children: [
_BotOverview(bot: bot),
BotKeysScreen(
publisherName: publisherName,
projectId: projectId,
botId: botId,
),
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry:
() => ref.invalidate(
botProvider(publisherName, projectId, botId),
),
),
),
);
}
}
class _BotOverview extends StatelessWidget {
final Bot bot;
const _BotOverview({required this.bot});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child:
bot.account.profile.background != null
? CloudFileWidget(
item: bot.account.profile.background!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
Positioned(
left: 20,
bottom: -32,
child: ProfilePictureWidget(
fileId: bot.account.profile.picture?.id,
radius: 40,
fallbackIcon: Symbols.smart_toy,
),
),
],
),
).padding(bottom: 32),
ListTile(title: Text('name'.tr()), subtitle: Text(bot.account.name)),
ListTile(
title: Text('nickname'.tr()),
subtitle: Text(bot.account.nick),
),
ListTile(title: Text('slug'.tr()), subtitle: Text(bot.slug)),
if (bot.account.profile.bio.isNotEmpty)
ListTile(
title: Text('bio'.tr()),
subtitle: Text(bot.account.profile.bio),
),
],
).padding(bottom: 24),
);
}
}

View File

@@ -0,0 +1,278 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/bot_key.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'bot_keys.g.dart';
@riverpod
Future<List<SnAccountApiKey>> botKeys(
Ref ref,
String publisherName,
String projectId,
String botId,
) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys',
);
return (resp.data as List).map((e) => SnAccountApiKey.fromJson(e)).toList();
}
class BotKeysScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
final String botId;
const BotKeysScreen({
super.key,
required this.publisherName,
required this.projectId,
required this.botId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final keys = ref.watch(botKeysProvider(publisherName, projectId, botId));
final keyNameController = useTextEditingController();
void showNewKeySheet(SnAccountApiKey newApiKey) {
final token = newApiKey.key;
if (token == null) return;
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'newKeyGenerated'.tr(),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Text('copyKeyHint'.tr()),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(8),
),
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()),
),
],
),
),
),
).whenComplete(() {
ref.invalidate(botKeysProvider(publisherName, projectId, botId));
});
}
void createKey() {
keyNameController.clear();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'newBotKey'.tr(),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: keyNameController,
decoration: InputDecoration(labelText: 'keyName'.tr()),
autofocus: true,
),
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()),
),
],
),
),
),
);
}
void rotateKey(String keyId) {
showConfirmAlert('rotateBotKeyHint'.tr(), 'rotateBotKey'.tr()).then((
confirm,
) async {
if (confirm) {
try {
if (context.mounted) showLoadingModal(context);
final client = ref.read(apiClientProvider);
final resp = await client.post(
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys/$keyId/rotate',
);
final rotatedApiKey = SnAccountApiKey.fromJson(resp.data);
showNewKeySheet(rotatedApiKey);
} catch (err) {
showErrorAlert(err.toString());
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
});
}
void revokeKey(String keyId) {
showConfirmAlert('revokeBotKeyHint'.tr(), 'revokeBotKey'.tr()).then((
confirm,
) {
if (confirm) {
final client = ref.read(apiClientProvider);
client
.delete(
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys/$keyId',
)
.then((_) {
ref.invalidate(
botKeysProvider(publisherName, projectId, botId),
);
})
.catchError((err) {
showErrorAlert(err.toString());
});
}
});
}
return keys.when(
data: (data) {
return Column(
children: [
ListTile(
leading: const Icon(Symbols.add),
title: Text('newBotKey'.tr()),
trailing: const Icon(Symbols.chevron_right),
onTap: createKey,
),
const Divider(height: 1),
Expanded(
child:
data.isEmpty
? Center(child: Text('noBotKeys'.tr()))
: RefreshIndicator(
onRefresh:
() => ref.refresh(
botKeysProvider(
publisherName,
projectId,
botId,
).future,
),
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.length,
itemBuilder: (context, index) {
final apiKey = data[index];
return ListTile(
title: Text(apiKey.label),
subtitle: Text(apiKey.createdAt.formatSystem()),
contentPadding: EdgeInsets.only(
left: 16,
right: 12,
),
trailing: PopupMenuButton(
itemBuilder:
(context) => [
PopupMenuItem(
value: 'rotate',
child: Row(
children: [
const Icon(Symbols.refresh),
const Gap(12),
Text('rotateKey'.tr()),
],
),
),
PopupMenuItem(
value: 'revoke',
child: Row(
children: [
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') {
revokeKey(apiKey.id);
}
},
),
);
},
),
),
),
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry:
() => ref.invalidate(
botKeysProvider(publisherName, projectId, botId),
),
),
);
}
}

View File

@@ -0,0 +1,172 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bot_keys.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$botKeysHash() => r'f7d1121833dc3da0cbd84b6171c2b2539edeb785';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [botKeys].
@ProviderFor(botKeys)
const botKeysProvider = BotKeysFamily();
/// See also [botKeys].
class BotKeysFamily extends Family<AsyncValue<List<SnAccountApiKey>>> {
/// See also [botKeys].
const BotKeysFamily();
/// See also [botKeys].
BotKeysProvider call(String publisherName, String projectId, String botId) {
return BotKeysProvider(publisherName, projectId, botId);
}
@override
BotKeysProvider getProviderOverride(covariant BotKeysProvider provider) {
return call(provider.publisherName, provider.projectId, provider.botId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'botKeysProvider';
}
/// See also [botKeys].
class BotKeysProvider extends AutoDisposeFutureProvider<List<SnAccountApiKey>> {
/// See also [botKeys].
BotKeysProvider(String publisherName, String projectId, String botId)
: this._internal(
(ref) => botKeys(ref as BotKeysRef, publisherName, projectId, botId),
from: botKeysProvider,
name: r'botKeysProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$botKeysHash,
dependencies: BotKeysFamily._dependencies,
allTransitiveDependencies: BotKeysFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
botId: botId,
);
BotKeysProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.projectId,
required this.botId,
}) : super.internal();
final String publisherName;
final String projectId;
final String botId;
@override
Override overrideWith(
FutureOr<List<SnAccountApiKey>> Function(BotKeysRef provider) create,
) {
return ProviderOverride(
origin: this,
override: BotKeysProvider._internal(
(ref) => create(ref as BotKeysRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
botId: botId,
),
);
}
@override
AutoDisposeFutureProviderElement<List<SnAccountApiKey>> createElement() {
return _BotKeysProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is BotKeysProvider &&
other.publisherName == publisherName &&
other.projectId == projectId &&
other.botId == botId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, projectId.hashCode);
hash = _SystemHash.combine(hash, botId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin BotKeysRef on AutoDisposeFutureProviderRef<List<SnAccountApiKey>> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
/// The parameter `botId` of this provider.
String get botId;
}
class _BotKeysProviderElement
extends AutoDisposeFutureProviderElement<List<SnAccountApiKey>>
with BotKeysRef {
_BotKeysProviderElement(super.provider);
@override
String get publisherName => (origin as BotKeysProvider).publisherName;
@override
String get projectId => (origin as BotKeysProvider).projectId;
@override
String get botId => (origin as BotKeysProvider).botId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -0,0 +1,167 @@
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/bot.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'bots.g.dart';
@riverpod
Future<List<Bot>> bots(Ref ref, String publisherName, String projectId) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/bots',
);
return (resp.data as List).map((e) => Bot.fromJson(e)).cast<Bot>().toList();
}
class BotsScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
const BotsScreen({
super.key,
required this.publisherName,
required this.projectId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final botsList = ref.watch(botsProvider(publisherName, projectId));
return botsList.when(
data: (data) {
if (data.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('noBots').tr(),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
context.pushNamed(
'developerBotNew',
pathParameters: {
'name': publisherName,
'projectId': projectId,
},
);
},
icon: const Icon(Symbols.add),
label: Text('createBot').tr(),
),
],
),
);
}
return RefreshIndicator(
onRefresh:
() => ref.refresh(botsProvider(publisherName, projectId).future),
child: ListView.builder(
padding: const EdgeInsets.only(top: 4),
itemCount: data.length,
itemBuilder: (context, index) {
final bot = data[index];
return Card(
margin: const EdgeInsets.all(8.0),
child: ListTile(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8.0)),
),
leading: CircleAvatar(
child:
bot.account.profile.picture != null
? ProfilePictureWidget(
file: bot.account.profile.picture!,
)
: const Icon(Symbols.smart_toy),
),
title: Text(bot.account.nick),
subtitle: Text(bot.account.name),
trailing: PopupMenuButton(
itemBuilder:
(context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Symbols.edit),
const SizedBox(width: 12),
Text('edit').tr(),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
const Icon(Symbols.delete, color: Colors.red),
const SizedBox(width: 12),
Text(
'delete',
style: TextStyle(color: Colors.red),
).tr(),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
context.pushNamed(
'developerBotEdit',
pathParameters: {
'name': publisherName,
'projectId': projectId,
'id': bot.id,
},
);
} else if (value == 'delete') {
showConfirmAlert(
'deleteBotHint'.tr(),
'deleteBot'.tr(),
).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(
'developerBotDetail',
pathParameters: {
'name': publisherName,
'projectId': projectId,
'botId': bot.id,
},
);
},
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry:
() => ref.invalidate(botsProvider(publisherName, projectId)),
),
);
}
}

View File

@@ -0,0 +1,156 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bots.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$botsHash() => r'15cefd5781350eb68208a342e85fcb0b9e0e3269';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [bots].
@ProviderFor(bots)
const botsProvider = BotsFamily();
/// See also [bots].
class BotsFamily extends Family<AsyncValue<List<Bot>>> {
/// See also [bots].
const BotsFamily();
/// See also [bots].
BotsProvider call(String publisherName, String projectId) {
return BotsProvider(publisherName, projectId);
}
@override
BotsProvider getProviderOverride(covariant BotsProvider provider) {
return call(provider.publisherName, provider.projectId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'botsProvider';
}
/// See also [bots].
class BotsProvider extends AutoDisposeFutureProvider<List<Bot>> {
/// See also [bots].
BotsProvider(String publisherName, String projectId)
: this._internal(
(ref) => bots(ref as BotsRef, publisherName, projectId),
from: botsProvider,
name: r'botsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$botsHash,
dependencies: BotsFamily._dependencies,
allTransitiveDependencies: BotsFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
);
BotsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.projectId,
}) : super.internal();
final String publisherName;
final String projectId;
@override
Override overrideWith(FutureOr<List<Bot>> Function(BotsRef provider) create) {
return ProviderOverride(
origin: this,
override: BotsProvider._internal(
(ref) => create(ref as BotsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
),
);
}
@override
AutoDisposeFutureProviderElement<List<Bot>> createElement() {
return _BotsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is BotsProvider &&
other.publisherName == publisherName &&
other.projectId == projectId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, projectId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin BotsRef on AutoDisposeFutureProviderRef<List<Bot>> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
}
class _BotsProviderElement extends AutoDisposeFutureProviderElement<List<Bot>>
with BotsRef {
_BotsProviderElement(super.provider);
@override
String get publisherName => (origin as BotsProvider).publisherName;
@override
String get projectId => (origin as BotsProvider).projectId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -22,21 +22,37 @@ import 'package:island/widgets/content/sheet.dart';
part 'edit_app.g.dart';
@riverpod
Future<CustomApp?> customApp(Ref ref, String publisherName, String id) async {
Future<CustomApp?> customApp(
Ref ref,
String publisherName,
String projectId,
String id,
) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/develop/developers/$publisherName/apps/$id');
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/apps/$id',
);
return CustomApp.fromJson(resp.data);
}
class EditAppScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
final String? id;
const EditAppScreen({super.key, required this.publisherName, this.id});
const EditAppScreen({
super.key,
required this.publisherName,
required this.projectId,
this.id,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null;
final app = isNew ? null : ref.watch(customAppProvider(publisherName, id!));
final app =
isNew
? null
: ref.watch(customAppProvider(publisherName, projectId, id!));
final formKey = useMemoized(() => GlobalKey<FormState>());
@@ -281,18 +297,26 @@ class EditAppScreen extends HookConsumerWidget {
}
: null,
};
if (isNew) {
await client.post(
'/develop/developers/$publisherName/apps',
data: data,
);
} else {
await client.patch(
'/develop/developers/$publisherName/apps/$id',
data: data,
);
try {
showLoadingModal(context);
if (isNew) {
await client.post(
'/develop/developers/$publisherName/projects/$projectId/apps',
data: data,
);
} else {
await client.patch(
'/develop/developers/$publisherName/projects/$projectId/apps/$id',
data: data,
);
}
} catch (err) {
showErrorAlert(err);
return;
} finally {
if (context.mounted) hideLoadingModal(context);
}
ref.invalidate(customAppsProvider(publisherName));
ref.invalidate(customAppsProvider(publisherName, projectId));
if (context.mounted) {
Navigator.pop(context);
}
@@ -309,7 +333,9 @@ class EditAppScreen extends HookConsumerWidget {
? ResponseErrorWidget(
error: app!.error,
onRetry:
() => ref.invalidate(customAppProvider(publisherName, id!)),
() => ref.invalidate(
customAppProvider(publisherName, projectId, id!),
),
)
: SingleChildScrollView(
child: Column(

View File

@@ -6,7 +6,7 @@ part of 'edit_app.dart';
// RiverpodGenerator
// **************************************************************************
String _$customAppHash() => r'42ad937b8439c793e3c5c35568bb5fa4da017df3';
String _$customAppHash() => r'8e1b38f3dc9b04fad362ee1141fcbfc53f008c09';
/// Copied from Dart SDK
class _SystemHash {
@@ -39,13 +39,13 @@ class CustomAppFamily extends Family<AsyncValue<CustomApp?>> {
const CustomAppFamily();
/// See also [customApp].
CustomAppProvider call(String publisherName, String id) {
return CustomAppProvider(publisherName, id);
CustomAppProvider call(String publisherName, String projectId, String id) {
return CustomAppProvider(publisherName, projectId, id);
}
@override
CustomAppProvider getProviderOverride(covariant CustomAppProvider provider) {
return call(provider.publisherName, provider.id);
return call(provider.publisherName, provider.projectId, provider.id);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@@ -66,9 +66,9 @@ class CustomAppFamily extends Family<AsyncValue<CustomApp?>> {
/// See also [customApp].
class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
/// See also [customApp].
CustomAppProvider(String publisherName, String id)
CustomAppProvider(String publisherName, String projectId, String id)
: this._internal(
(ref) => customApp(ref as CustomAppRef, publisherName, id),
(ref) => customApp(ref as CustomAppRef, publisherName, projectId, id),
from: customAppProvider,
name: r'customAppProvider',
debugGetCreateSourceHash:
@@ -78,6 +78,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
dependencies: CustomAppFamily._dependencies,
allTransitiveDependencies: CustomAppFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
id: id,
);
@@ -89,10 +90,12 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.projectId,
required this.id,
}) : super.internal();
final String publisherName;
final String projectId;
final String id;
@override
@@ -109,6 +112,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
id: id,
),
);
@@ -123,6 +127,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
bool operator ==(Object other) {
return other is CustomAppProvider &&
other.publisherName == publisherName &&
other.projectId == projectId &&
other.id == id;
}
@@ -130,6 +135,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, projectId.hashCode);
hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash);
@@ -142,6 +148,9 @@ mixin CustomAppRef on AutoDisposeFutureProviderRef<CustomApp?> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
/// The parameter `id` of this provider.
String get id;
}
@@ -154,6 +163,8 @@ class _CustomAppProviderElement
@override
String get publisherName => (origin as CustomAppProvider).publisherName;
@override
String get projectId => (origin as CustomAppProvider).projectId;
@override
String get id => (origin as CustomAppProvider).id;
}

View File

@@ -0,0 +1,425 @@
import 'package:croppy/croppy.dart' hide cropImage;
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/bot.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/file.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'edit_bot.g.dart';
@riverpod
Future<Bot?> bot(
Ref ref,
String publisherName,
String projectId,
String id,
) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/bots/$id',
);
return Bot.fromJson(resp.data);
}
class EditBotScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
final String? id;
const EditBotScreen({
super.key,
required this.publisherName,
required this.projectId,
this.id,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null;
final botData =
isNew ? null : ref.watch(botProvider(publisherName, projectId, id!));
final formKey = useMemoized(() => GlobalKey<FormState>());
final submitting = useState(false);
final nameController = useTextEditingController();
final nickController = useTextEditingController();
final slugController = useTextEditingController();
final picture = useState<SnCloudFile?>(null);
final firstNameController = useTextEditingController();
final middleNameController = useTextEditingController();
final lastNameController = useTextEditingController();
final genderController = useTextEditingController();
final pronounsController = useTextEditingController();
final locationController = useTextEditingController();
final timeZoneController = useTextEditingController();
final bioController = useTextEditingController();
final birthday = useState<DateTime?>(null);
final background = useState<SnCloudFile?>(null);
useEffect(() {
if (botData?.value != null) {
nameController.text = botData!.value!.account.name;
nickController.text = botData.value!.account.nick;
slugController.text = botData.value!.slug;
picture.value = botData.value!.account.profile.picture;
background.value = botData.value!.account.profile.background;
// Populate from botData.value.account.profile
firstNameController.text = botData.value!.account.profile.firstName;
middleNameController.text = botData.value!.account.profile.middleName;
lastNameController.text = botData.value!.account.profile.lastName;
genderController.text = botData.value!.account.profile.gender;
pronounsController.text = botData.value!.account.profile.pronouns;
locationController.text = botData.value!.account.profile.location;
timeZoneController.text = botData.value!.account.profile.timeZone;
bioController.text = botData.value!.account.profile.bio;
birthday.value = botData.value!.account.profile.birthday?.toLocal();
}
return null;
}, [botData]);
void setPicture(String position) async {
showLoadingModal(context);
var result = await ref
.read(imagePickerProvider)
.pickImage(source: ImageSource.gallery);
if (result == null) {
if (context.mounted) hideLoadingModal(context);
return;
}
if (!context.mounted) return;
hideLoadingModal(context);
result = await cropImage(
context,
image: result,
allowedAspectRatios: [
if (position == 'background')
const CropAspectRatio(height: 7, width: 16)
else
const CropAspectRatio(height: 1, width: 1),
],
);
if (result == null) {
if (context.mounted) hideLoadingModal(context);
return;
}
if (!context.mounted) return;
showLoadingModal(context);
submitting.value = true;
try {
final baseUrl = ref.watch(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile =
await putMediaToCloud(
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
atk: token,
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
switch (position) {
case 'picture':
picture.value = cloudFile;
case 'background':
background.value = cloudFile;
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
submitting.value = false;
}
}
void performAction() async {
final client = ref.read(apiClientProvider);
final data = {
'name': nameController.text,
'nick': nickController.text,
'slug': slugController.text,
'picture_id': picture.value?.id,
'background_id': background.value?.id,
'first_name': firstNameController.text,
'middle_name': middleNameController.text,
'last_name': lastNameController.text,
'gender': genderController.text,
'pronouns': pronounsController.text,
'location': locationController.text,
'time_zone': timeZoneController.text,
'bio': bioController.text,
'birthday': birthday.value?.toUtc().toIso8601String(),
};
try {
showLoadingModal(context);
if (isNew) {
await client.post(
'/develop/developers/$publisherName/projects/$projectId/bots',
data: data,
);
} else {
await client.patch(
'/develop/developers/$publisherName/projects/$projectId/bots/$id',
data: data,
);
}
if (context.mounted) {
context.pop();
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
return AppScaffold(
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
body:
botData == null && !isNew
? const Center(child: CircularProgressIndicator())
: botData?.hasError == true && !isNew
? ResponseErrorWidget(
error: botData!.error,
onRetry:
() => ref.invalidate(
botProvider(publisherName, projectId, id!),
),
)
: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
GestureDetector(
child: Container(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: picture.value?.id,
radius: 40,
fallbackIcon: Symbols.smart_toy,
),
onTap: () {
setPicture('picture');
},
),
),
],
),
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(labelText: 'name'.tr()),
),
const SizedBox(height: 16),
TextFormField(
controller: nickController,
decoration: InputDecoration(
labelText: 'nickname'.tr(),
alignLabelWithHint: true,
),
),
const SizedBox(height: 16),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
),
),
const SizedBox(height: 16),
TextFormField(
controller: bioController,
decoration: InputDecoration(
labelText: 'bio'.tr(),
alignLabelWithHint: true,
),
maxLines: 3,
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: firstNameController,
decoration: InputDecoration(
labelText: 'firstName'.tr(),
),
),
),
Expanded(
child: TextFormField(
controller: middleNameController,
decoration: InputDecoration(
labelText: 'middleName'.tr(),
),
),
),
Expanded(
child: TextFormField(
controller: lastNameController,
decoration: InputDecoration(
labelText: 'lastName'.tr(),
),
),
),
],
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: genderController,
decoration: InputDecoration(
labelText: 'gender'.tr(),
),
),
),
Expanded(
child: TextFormField(
controller: pronounsController,
decoration: InputDecoration(
labelText: 'pronouns'.tr(),
),
),
),
],
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: locationController,
decoration: InputDecoration(
labelText: 'location'.tr(),
),
),
),
Expanded(
child: TextFormField(
controller: timeZoneController,
decoration: InputDecoration(
labelText: 'timeZone'.tr(),
),
),
),
],
),
const SizedBox(height: 16),
GestureDetector(
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: birthday.value ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
);
if (date != null) {
birthday.value = date;
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'birthday'.tr(),
style: TextStyle(
color: Theme.of(context).hintColor,
),
),
Text(
birthday.value != null
? DateFormat.yMMMd().format(
birthday.value!,
)
: 'Select a date'.tr(),
),
],
),
),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed:
submitting.value ? null : performAction,
label: Text('saveChanges').tr(),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,167 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'edit_bot.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$botHash() => r'7bec47bb2a4061a5babc6d6d19c3d4c320c91188';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [bot].
@ProviderFor(bot)
const botProvider = BotFamily();
/// See also [bot].
class BotFamily extends Family<AsyncValue<Bot?>> {
/// See also [bot].
const BotFamily();
/// See also [bot].
BotProvider call(String publisherName, String projectId, String id) {
return BotProvider(publisherName, projectId, id);
}
@override
BotProvider getProviderOverride(covariant BotProvider provider) {
return call(provider.publisherName, provider.projectId, provider.id);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'botProvider';
}
/// See also [bot].
class BotProvider extends AutoDisposeFutureProvider<Bot?> {
/// See also [bot].
BotProvider(String publisherName, String projectId, String id)
: this._internal(
(ref) => bot(ref as BotRef, publisherName, projectId, id),
from: botProvider,
name: r'botProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$botHash,
dependencies: BotFamily._dependencies,
allTransitiveDependencies: BotFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
id: id,
);
BotProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.projectId,
required this.id,
}) : super.internal();
final String publisherName;
final String projectId;
final String id;
@override
Override overrideWith(FutureOr<Bot?> Function(BotRef provider) create) {
return ProviderOverride(
origin: this,
override: BotProvider._internal(
(ref) => create(ref as BotRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
id: id,
),
);
}
@override
AutoDisposeFutureProviderElement<Bot?> createElement() {
return _BotProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is BotProvider &&
other.publisherName == publisherName &&
other.projectId == projectId &&
other.id == id;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, projectId.hashCode);
hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin BotRef on AutoDisposeFutureProviderRef<Bot?> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
/// The parameter `id` of this provider.
String get id;
}
class _BotProviderElement extends AutoDisposeFutureProviderElement<Bot?>
with BotRef {
_BotProviderElement(super.provider);
@override
String get publisherName => (origin as BotProvider).publisherName;
@override
String get projectId => (origin as BotProvider).projectId;
@override
String get id => (origin as BotProvider).id;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/dev_project.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/developers/projects.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'edit_project.g.dart';
@riverpod
Future<DevProject?> devProject(Ref ref, String pubName, String id) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/develop/developers/$pubName/projects/$id');
return DevProject.fromJson(resp.data);
}
class EditProjectScreen extends HookConsumerWidget {
final String publisherName;
final String? id;
const EditProjectScreen({super.key, required this.publisherName, this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null;
final projectData =
isNew ? null : ref.watch(devProjectProvider(publisherName, id!));
final formKey = useMemoized(() => GlobalKey<FormState>());
final submitting = useState(false);
final nameController = useTextEditingController();
final slugController = useTextEditingController();
final descriptionController = useTextEditingController();
useEffect(() {
if (projectData?.value != null) {
nameController.text = projectData!.value!.name;
slugController.text = projectData.value!.slug;
descriptionController.text = projectData.value!.description ?? '';
}
return null;
}, [projectData]);
void performAction() async {
final client = ref.read(apiClientProvider);
final data = {
'name': nameController.text,
'slug': slugController.text,
'description': descriptionController.text,
};
if (isNew) {
await client.post(
'/develop/developers/$publisherName/projects',
data: data,
);
} else {
await client.put(
'/develop/developers/$publisherName/projects/$id',
data: data,
);
}
ref.invalidate(devProjectsProvider(publisherName));
if (context.mounted) {
context.pop();
}
}
return AppScaffold(
appBar: AppBar(
title: Text(isNew ? 'createProject'.tr() : 'editProject'.tr()),
),
body:
projectData == null && !isNew
? const Center(child: CircularProgressIndicator())
: projectData?.hasError == true && !isNew
? ResponseErrorWidget(
error: projectData!.error,
onRetry:
() =>
ref.invalidate(devProjectProvider(publisherName, id!)),
)
: SingleChildScrollView(
child: Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(labelText: 'name'.tr()),
),
const SizedBox(height: 16),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
alignLabelWithHint: true,
),
maxLines: 3,
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : performAction,
label: Text('saveChanges'.tr()),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
),
);
}
}

View File

@@ -0,0 +1,163 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'edit_project.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$devProjectHash() => r'd92be3f5cdc510c2a377615ed5c70622a6842bf2';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [devProject].
@ProviderFor(devProject)
const devProjectProvider = DevProjectFamily();
/// See also [devProject].
class DevProjectFamily extends Family<AsyncValue<DevProject?>> {
/// See also [devProject].
const DevProjectFamily();
/// See also [devProject].
DevProjectProvider call(String pubName, String id) {
return DevProjectProvider(pubName, id);
}
@override
DevProjectProvider getProviderOverride(
covariant DevProjectProvider provider,
) {
return call(provider.pubName, provider.id);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'devProjectProvider';
}
/// See also [devProject].
class DevProjectProvider extends AutoDisposeFutureProvider<DevProject?> {
/// See also [devProject].
DevProjectProvider(String pubName, String id)
: this._internal(
(ref) => devProject(ref as DevProjectRef, pubName, id),
from: devProjectProvider,
name: r'devProjectProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$devProjectHash,
dependencies: DevProjectFamily._dependencies,
allTransitiveDependencies: DevProjectFamily._allTransitiveDependencies,
pubName: pubName,
id: id,
);
DevProjectProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.pubName,
required this.id,
}) : super.internal();
final String pubName;
final String id;
@override
Override overrideWith(
FutureOr<DevProject?> Function(DevProjectRef provider) create,
) {
return ProviderOverride(
origin: this,
override: DevProjectProvider._internal(
(ref) => create(ref as DevProjectRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
pubName: pubName,
id: id,
),
);
}
@override
AutoDisposeFutureProviderElement<DevProject?> createElement() {
return _DevProjectProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is DevProjectProvider &&
other.pubName == pubName &&
other.id == id;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, pubName.hashCode);
hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin DevProjectRef on AutoDisposeFutureProviderRef<DevProject?> {
/// The parameter `pubName` of this provider.
String get pubName;
/// The parameter `id` of this provider.
String get id;
}
class _DevProjectProviderElement
extends AutoDisposeFutureProviderElement<DevProject?>
with DevProjectRef {
_DevProjectProviderElement(super.provider);
@override
String get pubName => (origin as DevProjectProvider).pubName;
@override
String get id => (origin as DevProjectProvider).id;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -235,15 +235,15 @@ class DeveloperHubScreen extends HookConsumerWidget {
).padding(vertical: 12, horizontal: 12),
ListTile(
minTileHeight: 48,
title: Text('customApps').tr(),
trailing: Icon(Symbols.chevron_right),
leading: const Icon(Symbols.apps),
contentPadding: EdgeInsets.symmetric(
title: Text('projects').tr(),
trailing: const Icon(Symbols.chevron_right),
leading: const Icon(Symbols.folder_managed),
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
),
onTap: () {
context.pushNamed(
'developerApps',
'developerProjects',
pathParameters: {
'name':
currentDeveloper.value!.publisher!.name,

View File

@@ -3,10 +3,11 @@ import 'package:island/screens/developers/edit_app.dart';
class NewCustomAppScreen extends StatelessWidget {
final String publisherName;
const NewCustomAppScreen({super.key, required this.publisherName});
final String projectId;
const NewCustomAppScreen({super.key, required this.publisherName, required this.projectId});
@override
Widget build(BuildContext context) {
return EditAppScreen(publisherName: publisherName);
return EditAppScreen(publisherName: publisherName, projectId: projectId);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:island/screens/developers/edit_bot.dart';
class NewBotScreen extends StatelessWidget {
final String publisherName;
final String projectId;
const NewBotScreen({super.key, required this.publisherName, required this.projectId});
@override
Widget build(BuildContext context) {
return EditBotScreen(publisherName: publisherName, projectId: projectId);
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:island/screens/developers/edit_project.dart';
class NewProjectScreen extends StatelessWidget {
final String publisherName;
const NewProjectScreen({super.key, required this.publisherName});
@override
Widget build(BuildContext context) {
return EditProjectScreen(publisherName: publisherName);
}
}

View File

@@ -0,0 +1,92 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/screens/developers/apps.dart';
import 'package:island/screens/developers/bots.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
class ProjectDetailScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
const ProjectDetailScreen({
super.key,
required this.publisherName,
required this.projectId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabController = useTabController(initialLength: 2);
return AppScaffold(
appBar: AppBar(
title: Text('projectDetails').tr(),
actions: [
IconButton(
icon: const Icon(Symbols.add),
onPressed: () {
// Get current tab index
final index = tabController.index;
switch (index) {
case 0:
context.pushNamed(
'developerAppNew',
pathParameters: {
'name': publisherName,
'projectId': projectId,
},
);
break;
case 1:
context.pushNamed(
'developerBotNew',
pathParameters: {
'name': publisherName,
'projectId': projectId,
},
);
break;
}
},
),
const Gap(8),
],
bottom: TabBar(
controller: tabController,
tabs: [
Tab(
child: Text(
'customApps'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'bots'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),
body: TabBarView(
controller: tabController,
children: [
CustomAppsScreen(publisherName: publisherName, projectId: projectId),
BotsScreen(publisherName: publisherName, projectId: projectId),
],
),
);
}
}

View File

@@ -0,0 +1,150 @@
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/dev_project.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'projects.g.dart';
@riverpod
Future<List<DevProject>> devProjects(Ref ref, String pubName) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/develop/developers/$pubName/projects');
return (resp.data as List)
.map((e) => DevProject.fromJson(e))
.cast<DevProject>()
.toList();
}
class DevProjectsScreen extends HookConsumerWidget {
final String publisherName;
const DevProjectsScreen({super.key, required this.publisherName});
@override
Widget build(BuildContext context, WidgetRef ref) {
final projects = ref.watch(devProjectsProvider(publisherName));
return AppScaffold(
appBar: AppBar(
title: Text('projects').tr(),
actions: [
IconButton(
icon: const Icon(Symbols.add),
onPressed: () {
context.pushNamed(
'developerProjectNew',
pathParameters: {'name': publisherName},
);
},
),
const Gap(8),
],
),
body: projects.when(
data: (data) {
if (data.isEmpty) {
return Center(child: Text('noProjects').tr());
}
return RefreshIndicator(
onRefresh:
() => ref.refresh(devProjectsProvider(publisherName).future),
child: ListView.builder(
padding: EdgeInsets.only(top: 4),
itemCount: data.length,
itemBuilder: (context, index) {
final project = data[index];
return Card(
margin: const EdgeInsets.all(8.0),
child: ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
contentPadding: EdgeInsets.only(left: 20, right: 12),
title: Text(project.name),
subtitle: Text(project.description ?? ''),
trailing: PopupMenuButton(
itemBuilder:
(context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Symbols.edit),
const SizedBox(width: 12),
Text('edit').tr(),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
const Icon(Symbols.delete, color: Colors.red),
const SizedBox(width: 12),
Text(
'delete',
style: TextStyle(color: Colors.red),
).tr(),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
context.pushNamed(
'developerProjectEdit',
pathParameters: {
'name': publisherName,
'id': project.id,
},
);
} else if (value == 'delete') {
showConfirmAlert(
'deleteProjectHint'.tr(),
'deleteProject'.tr(),
).then((confirm) {
if (confirm) {
final client = ref.read(apiClientProvider);
client.delete(
'/develop/developers/$publisherName/projects/${project.id}',
);
ref.invalidate(
devProjectsProvider(publisherName),
);
}
});
}
},
),
onTap: () {
context.pushNamed(
'developerProjectDetail',
pathParameters: {
'name': publisherName,
'projectId': project.id,
},
);
},
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(devProjectsProvider(publisherName)),
),
),
);
}
}

View File

@@ -0,0 +1,151 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'projects.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$devProjectsHash() => r'87fdcab47cd7d79ab019a5625617abeb1ffa1f39';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [devProjects].
@ProviderFor(devProjects)
const devProjectsProvider = DevProjectsFamily();
/// See also [devProjects].
class DevProjectsFamily extends Family<AsyncValue<List<DevProject>>> {
/// See also [devProjects].
const DevProjectsFamily();
/// See also [devProjects].
DevProjectsProvider call(String pubName) {
return DevProjectsProvider(pubName);
}
@override
DevProjectsProvider getProviderOverride(
covariant DevProjectsProvider provider,
) {
return call(provider.pubName);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'devProjectsProvider';
}
/// See also [devProjects].
class DevProjectsProvider extends AutoDisposeFutureProvider<List<DevProject>> {
/// See also [devProjects].
DevProjectsProvider(String pubName)
: this._internal(
(ref) => devProjects(ref as DevProjectsRef, pubName),
from: devProjectsProvider,
name: r'devProjectsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$devProjectsHash,
dependencies: DevProjectsFamily._dependencies,
allTransitiveDependencies: DevProjectsFamily._allTransitiveDependencies,
pubName: pubName,
);
DevProjectsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.pubName,
}) : super.internal();
final String pubName;
@override
Override overrideWith(
FutureOr<List<DevProject>> Function(DevProjectsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: DevProjectsProvider._internal(
(ref) => create(ref as DevProjectsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
pubName: pubName,
),
);
}
@override
AutoDisposeFutureProviderElement<List<DevProject>> createElement() {
return _DevProjectsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is DevProjectsProvider && other.pubName == pubName;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, pubName.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin DevProjectsRef on AutoDisposeFutureProviderRef<List<DevProject>> {
/// The parameter `pubName` of this provider.
String get pubName;
}
class _DevProjectsProviderElement
extends AutoDisposeFutureProviderElement<List<DevProject>>
with DevProjectsRef {
_DevProjectsProviderElement(super.provider);
@override
String get pubName => (origin as DevProjectsProvider).pubName;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -101,7 +101,7 @@ class SliverArticlesList extends ConsumerWidget {
publisherId: publisherId,
).notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder(
(data, widgetCount, endItemView) => SliverList.separated(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
@@ -111,38 +111,116 @@ class SliverArticlesList extends ConsumerWidget {
final article = data.items[index];
return WebArticleCard(article: article, showDetails: true);
},
separatorBuilder: (context, index) => const SizedBox(height: 12),
),
);
}
}
@riverpod
Future<List<SnWebFeed>> subscribedFeeds(Ref ref) async {
final client = ref.watch(apiClientProvider);
final response = await client.get('/sphere/feeds/subscribed');
final data = response.data as List<dynamic>;
return data.map((json) => SnWebFeed.fromJson(json)).toList();
}
class ArticlesScreen extends ConsumerWidget {
final String? feedId;
final String? publisherId;
final String? title;
const ArticlesScreen({super.key, this.feedId, this.publisherId, this.title});
const ArticlesScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return AppScaffold(
appBar: AppBar(title: Text(title ?? 'Articles')),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
sliver: SliverArticlesList(
feedId: feedId,
publisherId: publisherId,
),
final subscribedFeedsAsync = ref.watch(subscribedFeedsProvider);
return subscribedFeedsAsync.when(
data: (feeds) {
return DefaultTabController(
length: feeds.length + 1,
child: AppScaffold(
isNoBackground: false,
appBar: AppBar(
title: const Text('Articles'),
bottom: TabBar(
isScrollable: true,
tabs: [
Tab(
child: Text(
'All',
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
...feeds.map(
(feed) => Tab(
child: Text(
feed.title,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
),
],
),
],
),
body: TabBarView(
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.only(
top: 12,
left: 8,
right: 8,
),
sliver: SliverArticlesList(),
),
],
),
),
),
...feeds.map((feed) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.only(
top: 8,
left: 8,
right: 8,
),
sliver: SliverArticlesList(feedId: feed.id),
),
],
),
),
);
}),
],
),
),
);
},
loading:
() => AppScaffold(
isNoBackground: false,
appBar: AppBar(title: const Text('Articles')),
body: const Center(child: CircularProgressIndicator()),
),
error:
(err, stack) => AppScaffold(
isNoBackground: false,
appBar: AppBar(title: const Text('Articles')),
body: Center(child: Text('Error: $err')),
),
),
),
);
}
}

View File

@@ -6,6 +6,25 @@ part of 'articles.dart';
// RiverpodGenerator
// **************************************************************************
String _$subscribedFeedsHash() => r'5c0c8c30c5f543f6ea1d39786a6778f77ba5b3df';
/// See also [subscribedFeeds].
@ProviderFor(subscribedFeeds)
final subscribedFeedsProvider =
AutoDisposeFutureProvider<List<SnWebFeed>>.internal(
subscribedFeeds,
name: r'subscribedFeedsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$subscribedFeedsHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef SubscribedFeedsRef = AutoDisposeFutureProviderRef<List<SnWebFeed>>;
String _$articlesListNotifierHash() =>
r'579741af4d90c7c81f2e2697e57c4895b7a9dabc';

View File

@@ -0,0 +1,240 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/web_article_card.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
part 'feed_detail.g.dart';
@riverpod
Future<SnWebFeed> marketplaceWebFeed(Ref ref, String feedId) async {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/sphere/feeds/$feedId');
return SnWebFeed.fromJson(resp.data);
}
/// Provider for web feed articles content
@riverpod
class MarketplaceWebFeedContentNotifier
extends _$MarketplaceWebFeedContentNotifier
with CursorPagingNotifierMixin<SnWebArticle> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnWebArticle>> build(String feedId) async {
_feedId = feedId;
return fetch(cursor: null);
}
late final String _feedId;
ValueNotifier<int> totalCount = ValueNotifier(0);
@override
Future<CursorPagingData<SnWebArticle>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final queryParams = {'offset': offset, 'take': _pageSize};
final response = await client.get(
'/sphere/feeds/$_feedId/articles',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
totalCount.value = total;
final List<dynamic> data = response.data;
final articles = data.map((json) => SnWebArticle.fromJson(json)).toList();
final hasMore = offset + articles.length < total;
final nextCursor = hasMore ? (offset + articles.length).toString() : null;
return CursorPagingData(
items: articles,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
void dispose() {
totalCount.dispose();
}
}
/// Provider for web feed subscription status
@riverpod
Future<bool> marketplaceWebFeedSubscription(
Ref ref, {
required String feedId,
}) async {
final api = ref.watch(apiClientProvider);
try {
await api.get('/sphere/feeds/$feedId/subscription');
// If not 404, consider subscribed
return true;
} on Object catch (e) {
// Dio error handling agnostic: treat 404 as not-subscribed, rethrow others
final msg = e.toString();
if (msg.contains('404')) return false;
rethrow;
}
}
class MarketplaceWebFeedDetailScreen extends HookConsumerWidget {
final String id;
const MarketplaceWebFeedDetailScreen({super.key, required this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final feed = ref.watch(marketplaceWebFeedProvider(id));
final subscribed = ref.watch(
marketplaceWebFeedSubscriptionProvider(feedId: id),
);
// Subscribe to web feed
Future<void> subscribeToFeed() async {
final apiClient = ref.watch(apiClientProvider);
await apiClient.post('/sphere/feeds/$id/subscribe');
HapticFeedback.selectionClick();
ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id));
if (!context.mounted) return;
showSnackBar('webFeedSubscribed'.tr());
}
// Unsubscribe from web feed
Future<void> unsubscribeFromFeed() async {
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete('/sphere/feeds/$id/subscribe');
HapticFeedback.selectionClick();
ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id));
if (!context.mounted) return;
showSnackBar('webFeedUnsubscribed'.tr());
}
final feedNotifier = ref.watch(
marketplaceWebFeedContentNotifierProvider(id).notifier,
);
useEffect(() {
return feedNotifier.dispose;
}, []);
return AppScaffold(
appBar: AppBar(title: Text(feed.value?.title ?? 'loading'.tr())),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Feed meta
feed
.when(
data:
(data) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(data.description ?? 'descriptionNone'.tr()),
Row(
spacing: 4,
children: [
const Icon(Symbols.rss_feed, size: 16),
ListenableBuilder(
listenable: feedNotifier.totalCount,
builder:
(context, _) => Text(
'webFeedArticleCount'.plural(
feedNotifier.totalCount.value,
),
),
),
],
).opacity(0.85),
Row(
spacing: 4,
children: [
const Icon(Symbols.link, size: 16),
SelectableText(data.url),
],
).opacity(0.85),
],
),
error: (err, _) => Text(err.toString()),
loading: () => CircularProgressIndicator().center(),
)
.padding(horizontal: 24, vertical: 24),
const Divider(height: 1),
// Articles list
Expanded(
child: PagingHelperView(
provider: marketplaceWebFeedContentNotifierProvider(id),
futureRefreshable:
marketplaceWebFeedContentNotifierProvider(id).future,
notifierRefreshable:
marketplaceWebFeedContentNotifierProvider(id).notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.separated(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 20,
),
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final article = data.items[index];
return WebArticleCard(article: article);
},
separatorBuilder: (context, index) => const Gap(12),
),
),
),
Container(
padding: EdgeInsets.only(
bottom: 16 + MediaQuery.of(context).padding.bottom,
left: 24,
right: 24,
top: 16,
),
color: Theme.of(context).colorScheme.surfaceContainer,
child: subscribed.when(
data:
(isSubscribed) => FilledButton.icon(
onPressed:
isSubscribed ? unsubscribeFromFeed : subscribeToFeed,
icon: Icon(
isSubscribed ? Symbols.remove_circle : Symbols.add_circle,
),
label: Text(
isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(),
),
),
loading:
() => const SizedBox(
height: 32,
width: 32,
child: CircularProgressIndicator(strokeWidth: 2),
),
error:
(_, _) => OutlinedButton.icon(
onPressed: subscribeToFeed,
icon: const Icon(Symbols.add_circle),
label: Text('subscribe').tr(),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,458 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'feed_detail.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$marketplaceWebFeedHash() =>
r'8383f94f1bc272b903c341b8d95000313b69d14c';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [marketplaceWebFeed].
@ProviderFor(marketplaceWebFeed)
const marketplaceWebFeedProvider = MarketplaceWebFeedFamily();
/// See also [marketplaceWebFeed].
class MarketplaceWebFeedFamily extends Family<AsyncValue<SnWebFeed>> {
/// See also [marketplaceWebFeed].
const MarketplaceWebFeedFamily();
/// See also [marketplaceWebFeed].
MarketplaceWebFeedProvider call(String feedId) {
return MarketplaceWebFeedProvider(feedId);
}
@override
MarketplaceWebFeedProvider getProviderOverride(
covariant MarketplaceWebFeedProvider provider,
) {
return call(provider.feedId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'marketplaceWebFeedProvider';
}
/// See also [marketplaceWebFeed].
class MarketplaceWebFeedProvider extends AutoDisposeFutureProvider<SnWebFeed> {
/// See also [marketplaceWebFeed].
MarketplaceWebFeedProvider(String feedId)
: this._internal(
(ref) => marketplaceWebFeed(ref as MarketplaceWebFeedRef, feedId),
from: marketplaceWebFeedProvider,
name: r'marketplaceWebFeedProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$marketplaceWebFeedHash,
dependencies: MarketplaceWebFeedFamily._dependencies,
allTransitiveDependencies:
MarketplaceWebFeedFamily._allTransitiveDependencies,
feedId: feedId,
);
MarketplaceWebFeedProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.feedId,
}) : super.internal();
final String feedId;
@override
Override overrideWith(
FutureOr<SnWebFeed> Function(MarketplaceWebFeedRef provider) create,
) {
return ProviderOverride(
origin: this,
override: MarketplaceWebFeedProvider._internal(
(ref) => create(ref as MarketplaceWebFeedRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
feedId: feedId,
),
);
}
@override
AutoDisposeFutureProviderElement<SnWebFeed> createElement() {
return _MarketplaceWebFeedProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is MarketplaceWebFeedProvider && other.feedId == feedId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, feedId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin MarketplaceWebFeedRef on AutoDisposeFutureProviderRef<SnWebFeed> {
/// The parameter `feedId` of this provider.
String get feedId;
}
class _MarketplaceWebFeedProviderElement
extends AutoDisposeFutureProviderElement<SnWebFeed>
with MarketplaceWebFeedRef {
_MarketplaceWebFeedProviderElement(super.provider);
@override
String get feedId => (origin as MarketplaceWebFeedProvider).feedId;
}
String _$marketplaceWebFeedSubscriptionHash() =>
r'2ff06a48ed7d4236b57412ecca55e94c0a0b6330';
/// Provider for web feed subscription status
///
/// Copied from [marketplaceWebFeedSubscription].
@ProviderFor(marketplaceWebFeedSubscription)
const marketplaceWebFeedSubscriptionProvider =
MarketplaceWebFeedSubscriptionFamily();
/// Provider for web feed subscription status
///
/// Copied from [marketplaceWebFeedSubscription].
class MarketplaceWebFeedSubscriptionFamily extends Family<AsyncValue<bool>> {
/// Provider for web feed subscription status
///
/// Copied from [marketplaceWebFeedSubscription].
const MarketplaceWebFeedSubscriptionFamily();
/// Provider for web feed subscription status
///
/// Copied from [marketplaceWebFeedSubscription].
MarketplaceWebFeedSubscriptionProvider call({required String feedId}) {
return MarketplaceWebFeedSubscriptionProvider(feedId: feedId);
}
@override
MarketplaceWebFeedSubscriptionProvider getProviderOverride(
covariant MarketplaceWebFeedSubscriptionProvider provider,
) {
return call(feedId: provider.feedId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'marketplaceWebFeedSubscriptionProvider';
}
/// Provider for web feed subscription status
///
/// Copied from [marketplaceWebFeedSubscription].
class MarketplaceWebFeedSubscriptionProvider
extends AutoDisposeFutureProvider<bool> {
/// Provider for web feed subscription status
///
/// Copied from [marketplaceWebFeedSubscription].
MarketplaceWebFeedSubscriptionProvider({required String feedId})
: this._internal(
(ref) => marketplaceWebFeedSubscription(
ref as MarketplaceWebFeedSubscriptionRef,
feedId: feedId,
),
from: marketplaceWebFeedSubscriptionProvider,
name: r'marketplaceWebFeedSubscriptionProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$marketplaceWebFeedSubscriptionHash,
dependencies: MarketplaceWebFeedSubscriptionFamily._dependencies,
allTransitiveDependencies:
MarketplaceWebFeedSubscriptionFamily._allTransitiveDependencies,
feedId: feedId,
);
MarketplaceWebFeedSubscriptionProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.feedId,
}) : super.internal();
final String feedId;
@override
Override overrideWith(
FutureOr<bool> Function(MarketplaceWebFeedSubscriptionRef provider) create,
) {
return ProviderOverride(
origin: this,
override: MarketplaceWebFeedSubscriptionProvider._internal(
(ref) => create(ref as MarketplaceWebFeedSubscriptionRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
feedId: feedId,
),
);
}
@override
AutoDisposeFutureProviderElement<bool> createElement() {
return _MarketplaceWebFeedSubscriptionProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is MarketplaceWebFeedSubscriptionProvider &&
other.feedId == feedId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, feedId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin MarketplaceWebFeedSubscriptionRef on AutoDisposeFutureProviderRef<bool> {
/// The parameter `feedId` of this provider.
String get feedId;
}
class _MarketplaceWebFeedSubscriptionProviderElement
extends AutoDisposeFutureProviderElement<bool>
with MarketplaceWebFeedSubscriptionRef {
_MarketplaceWebFeedSubscriptionProviderElement(super.provider);
@override
String get feedId =>
(origin as MarketplaceWebFeedSubscriptionProvider).feedId;
}
String _$marketplaceWebFeedContentNotifierHash() =>
r'25688082884cb824eeff300888ba38c9748295dc';
abstract class _$MarketplaceWebFeedContentNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnWebArticle>> {
late final String feedId;
FutureOr<CursorPagingData<SnWebArticle>> build(String feedId);
}
/// Provider for web feed articles content
///
/// Copied from [MarketplaceWebFeedContentNotifier].
@ProviderFor(MarketplaceWebFeedContentNotifier)
const marketplaceWebFeedContentNotifierProvider =
MarketplaceWebFeedContentNotifierFamily();
/// Provider for web feed articles content
///
/// Copied from [MarketplaceWebFeedContentNotifier].
class MarketplaceWebFeedContentNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnWebArticle>>> {
/// Provider for web feed articles content
///
/// Copied from [MarketplaceWebFeedContentNotifier].
const MarketplaceWebFeedContentNotifierFamily();
/// Provider for web feed articles content
///
/// Copied from [MarketplaceWebFeedContentNotifier].
MarketplaceWebFeedContentNotifierProvider call(String feedId) {
return MarketplaceWebFeedContentNotifierProvider(feedId);
}
@override
MarketplaceWebFeedContentNotifierProvider getProviderOverride(
covariant MarketplaceWebFeedContentNotifierProvider provider,
) {
return call(provider.feedId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'marketplaceWebFeedContentNotifierProvider';
}
/// Provider for web feed articles content
///
/// Copied from [MarketplaceWebFeedContentNotifier].
class MarketplaceWebFeedContentNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
MarketplaceWebFeedContentNotifier,
CursorPagingData<SnWebArticle>
> {
/// Provider for web feed articles content
///
/// Copied from [MarketplaceWebFeedContentNotifier].
MarketplaceWebFeedContentNotifierProvider(String feedId)
: this._internal(
() => MarketplaceWebFeedContentNotifier()..feedId = feedId,
from: marketplaceWebFeedContentNotifierProvider,
name: r'marketplaceWebFeedContentNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$marketplaceWebFeedContentNotifierHash,
dependencies: MarketplaceWebFeedContentNotifierFamily._dependencies,
allTransitiveDependencies:
MarketplaceWebFeedContentNotifierFamily._allTransitiveDependencies,
feedId: feedId,
);
MarketplaceWebFeedContentNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.feedId,
}) : super.internal();
final String feedId;
@override
FutureOr<CursorPagingData<SnWebArticle>> runNotifierBuild(
covariant MarketplaceWebFeedContentNotifier notifier,
) {
return notifier.build(feedId);
}
@override
Override overrideWith(MarketplaceWebFeedContentNotifier Function() create) {
return ProviderOverride(
origin: this,
override: MarketplaceWebFeedContentNotifierProvider._internal(
() => create()..feedId = feedId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
feedId: feedId,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
MarketplaceWebFeedContentNotifier,
CursorPagingData<SnWebArticle>
>
createElement() {
return _MarketplaceWebFeedContentNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is MarketplaceWebFeedContentNotifierProvider &&
other.feedId == feedId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, feedId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin MarketplaceWebFeedContentNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnWebArticle>> {
/// The parameter `feedId` of this provider.
String get feedId;
}
class _MarketplaceWebFeedContentNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
MarketplaceWebFeedContentNotifier,
CursorPagingData<SnWebArticle>
>
with MarketplaceWebFeedContentNotifierRef {
_MarketplaceWebFeedContentNotifierProviderElement(super.provider);
@override
String get feedId =>
(origin as MarketplaceWebFeedContentNotifierProvider).feedId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -0,0 +1,169 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'dart:async';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
part 'feed_marketplace.g.dart';
@riverpod
class MarketplaceWebFeedsNotifier extends _$MarketplaceWebFeedsNotifier
with CursorPagingNotifierMixin<SnWebFeed> {
String? _query;
@override
Future<CursorPagingData<SnWebFeed>> build({required String? query}) {
_query = query;
return fetch(cursor: null);
}
@override
Future<CursorPagingData<SnWebFeed>> fetch({required String? cursor}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/sphere/feeds/explore',
queryParameters: {
'offset': offset,
'take': 20,
if (_query != null && _query!.isNotEmpty) 'query': _query,
},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final feeds = data.map((e) => SnWebFeed.fromJson(e)).toList();
final hasMore = offset + feeds.length < total;
final nextCursor = hasMore ? (offset + feeds.length).toString() : null;
return CursorPagingData(
items: feeds,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
/// Marketplace screen for browsing web feeds.
class MarketplaceWebFeedsScreen extends HookConsumerWidget {
const MarketplaceWebFeedsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final query = useState<String?>(null);
final searchController = useTextEditingController();
final focusNode = useFocusNode();
final debounceTimer = useState<Timer?>(null);
// Clear search when query is cleared
useEffect(() {
if (query.value == null || query.value!.isEmpty) {
searchController.clear();
}
return null;
}, [query.value]);
// Clean up timer on dispose
useEffect(() {
return () {
debounceTimer.value?.cancel();
};
}, []);
return AppScaffold(
appBar: AppBar(
title: const Text('webFeeds').tr(),
actions: const [Gap(8)],
),
body: PagingHelperView(
provider: marketplaceWebFeedsNotifierProvider(query: query.value),
futureRefreshable:
marketplaceWebFeedsNotifierProvider(query: query.value).future,
notifierRefreshable:
marketplaceWebFeedsNotifierProvider(query: query.value).notifier,
contentBuilder:
(data, widgetCount, endItemView) => Column(
children: [
// Search bar above the list
Padding(
padding: const EdgeInsets.all(16),
child: SearchBar(
elevation: WidgetStateProperty.all(4),
controller: searchController,
focusNode: focusNode,
hintText: 'search'.tr(),
leading: const Icon(Symbols.search),
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(horizontal: 24),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
trailing: [
if (query.value != null && query.value!.isNotEmpty)
IconButton(
icon: const Icon(Symbols.close),
onPressed: () {
query.value = null;
searchController.clear();
focusNode.unfocus();
},
),
],
onChanged: (value) {
// Debounce search to avoid excessive API calls
debounceTimer.value?.cancel();
debounceTimer.value = Timer(
const Duration(milliseconds: 500),
() {
query.value = value.isEmpty ? null : value;
},
);
},
onSubmitted: (value) {
query.value = value.isEmpty ? null : value;
focusNode.unfocus();
},
),
),
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final feed = data.items[index];
return ListTile(
title: Text(feed.title),
subtitle: Text(feed.description ?? ''),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
// Navigate to web feed detail page
context.pushNamed(
'webFeedDetail',
pathParameters: {'feedId': feed.id},
);
},
);
},
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,180 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'feed_marketplace.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$marketplaceWebFeedsNotifierHash() =>
r'774b2985f2f7d61fe958f534f84e39f814327c4e';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$MarketplaceWebFeedsNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnWebFeed>> {
late final String? query;
FutureOr<CursorPagingData<SnWebFeed>> build({required String? query});
}
/// See also [MarketplaceWebFeedsNotifier].
@ProviderFor(MarketplaceWebFeedsNotifier)
const marketplaceWebFeedsNotifierProvider = MarketplaceWebFeedsNotifierFamily();
/// See also [MarketplaceWebFeedsNotifier].
class MarketplaceWebFeedsNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnWebFeed>>> {
/// See also [MarketplaceWebFeedsNotifier].
const MarketplaceWebFeedsNotifierFamily();
/// See also [MarketplaceWebFeedsNotifier].
MarketplaceWebFeedsNotifierProvider call({required String? query}) {
return MarketplaceWebFeedsNotifierProvider(query: query);
}
@override
MarketplaceWebFeedsNotifierProvider getProviderOverride(
covariant MarketplaceWebFeedsNotifierProvider provider,
) {
return call(query: provider.query);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'marketplaceWebFeedsNotifierProvider';
}
/// See also [MarketplaceWebFeedsNotifier].
class MarketplaceWebFeedsNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
MarketplaceWebFeedsNotifier,
CursorPagingData<SnWebFeed>
> {
/// See also [MarketplaceWebFeedsNotifier].
MarketplaceWebFeedsNotifierProvider({required String? query})
: this._internal(
() => MarketplaceWebFeedsNotifier()..query = query,
from: marketplaceWebFeedsNotifierProvider,
name: r'marketplaceWebFeedsNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$marketplaceWebFeedsNotifierHash,
dependencies: MarketplaceWebFeedsNotifierFamily._dependencies,
allTransitiveDependencies:
MarketplaceWebFeedsNotifierFamily._allTransitiveDependencies,
query: query,
);
MarketplaceWebFeedsNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.query,
}) : super.internal();
final String? query;
@override
FutureOr<CursorPagingData<SnWebFeed>> runNotifierBuild(
covariant MarketplaceWebFeedsNotifier notifier,
) {
return notifier.build(query: query);
}
@override
Override overrideWith(MarketplaceWebFeedsNotifier Function() create) {
return ProviderOverride(
origin: this,
override: MarketplaceWebFeedsNotifierProvider._internal(
() => create()..query = query,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
query: query,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
MarketplaceWebFeedsNotifier,
CursorPagingData<SnWebFeed>
>
createElement() {
return _MarketplaceWebFeedsNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is MarketplaceWebFeedsNotifierProvider && other.query == query;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, query.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin MarketplaceWebFeedsNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnWebFeed>> {
/// The parameter `query` of this provider.
String? get query;
}
class _MarketplaceWebFeedsNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
MarketplaceWebFeedsNotifier,
CursorPagingData<SnWebFeed>
>
with MarketplaceWebFeedsNotifierRef {
_MarketplaceWebFeedsNotifierProviderElement(super.provider);
@override
String? get query => (origin as MarketplaceWebFeedsNotifierProvider).query;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -173,12 +173,60 @@ class ExploreScreen extends HookConsumerWidget {
),
tooltip: 'webArticlesStand'.tr(),
),
IconButton(
onPressed: () {
context.pushNamed('postSearch');
},
PopupMenuButton(
itemBuilder:
(context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.category),
const Gap(12),
Text('categories').tr(),
],
),
onTap: () {
context.pushNamed('postCategories');
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.label),
const Gap(12),
Text('tags').tr(),
],
),
onTap: () {
context.pushNamed('postTags');
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.shuffle),
const Gap(12),
Text('postShuffle').tr(),
],
),
onTap: () {
context.pushNamed('postShuffle');
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.search),
const Gap(12),
Text('search').tr(),
],
),
onTap: () {
context.pushNamed('postSearch');
},
),
],
icon: Icon(
Symbols.search,
Symbols.action_key,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
tooltip: 'search'.tr(),

View File

@@ -0,0 +1,242 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
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/widgets/app_scaffold.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
// Post Categories Notifier
final postCategoriesNotifierProvider = StateNotifierProvider.autoDispose<
PostCategoriesNotifier,
AsyncValue<CursorPagingData<SnPostCategory>>
>((ref) {
return PostCategoriesNotifier(ref);
});
class PostCategoriesNotifier
extends StateNotifier<AsyncValue<CursorPagingData<SnPostCategory>>> {
final AutoDisposeRef ref;
static const int _pageSize = 20;
bool _isLoading = false;
PostCategoriesNotifier(this.ref) : super(const AsyncValue.loading()) {
state = const AsyncValue.data(
CursorPagingData(items: [], hasMore: false, nextCursor: null),
);
fetch(cursor: null);
}
Future<void> fetch({String? cursor}) async {
if (_isLoading) return;
_isLoading = true;
if (cursor == null) {
state = const AsyncValue.loading();
}
try {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/sphere/posts/categories',
queryParameters: {
'offset': offset,
'take': _pageSize,
'order': 'usage',
},
);
final data = response.data as List;
final categories =
data.map((json) => SnPostCategory.fromJson(json)).toList();
final hasMore = categories.length == _pageSize;
final nextCursor =
hasMore ? (offset + categories.length).toString() : null;
state = AsyncValue.data(
CursorPagingData(
items: [...(state.value?.items ?? []), ...categories],
hasMore: hasMore,
nextCursor: nextCursor,
),
);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
} finally {
_isLoading = false;
}
}
}
// Post Tags Notifier
final postTagsNotifierProvider = StateNotifierProvider.autoDispose<
PostTagsNotifier,
AsyncValue<CursorPagingData<SnPostTag>>
>((ref) {
return PostTagsNotifier(ref);
});
class PostTagsNotifier
extends StateNotifier<AsyncValue<CursorPagingData<SnPostTag>>> {
final AutoDisposeRef ref;
static const int _pageSize = 20;
bool _isLoading = false;
PostTagsNotifier(this.ref) : super(const AsyncValue.loading()) {
state = const AsyncValue.data(
CursorPagingData(items: [], hasMore: false, nextCursor: null),
);
fetch(cursor: null);
}
Future<void> fetch({String? cursor}) async {
if (_isLoading) return;
_isLoading = true;
if (cursor == null) {
state = const AsyncValue.loading();
}
try {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/sphere/posts/tags',
queryParameters: {
'offset': offset,
'take': _pageSize,
'order': 'usage',
},
);
final data = response.data as List;
final tags = data.map((json) => SnPostTag.fromJson(json)).toList();
final hasMore = tags.length == _pageSize;
final nextCursor = hasMore ? (offset + tags.length).toString() : null;
state = AsyncValue.data(
CursorPagingData(
items: [...(state.value?.items ?? []), ...tags],
hasMore: hasMore,
nextCursor: nextCursor,
),
);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
} finally {
_isLoading = false;
}
}
}
class PostCategoriesListScreen extends ConsumerWidget {
const PostCategoriesListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final categoriesState = ref.watch(postCategoriesNotifierProvider);
return AppScaffold(
appBar: AppBar(title: const Text('categories').tr()),
body: categoriesState.when(
data: (data) {
if (data.items.isEmpty) {
return const Center(child: Text('No categories found'));
}
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.items.length + (data.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= data.items.length) {
ref
.read(postCategoriesNotifierProvider.notifier)
.fetch(cursor: data.nextCursor);
return const Center(child: CircularProgressIndicator());
}
final category = data.items[index];
return ListTile(
leading: const Icon(Symbols.category),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Symbols.chevron_right),
title: Text(category.categoryDisplayTitle),
subtitle: Text('postCount'.plural(category.usage)),
onTap: () {
context.pushNamed(
'postCategoryDetail',
pathParameters: {'slug': category.slug},
);
},
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(postCategoriesNotifierProvider),
),
),
);
}
}
class PostTagsListScreen extends ConsumerWidget {
const PostTagsListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tagsState = ref.watch(postTagsNotifierProvider);
return AppScaffold(
appBar: AppBar(title: const Text('tags').tr()),
body: tagsState.when(
data: (data) {
if (data.items.isEmpty) {
return const Center(child: Text('No tags found'));
}
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.items.length + (data.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= data.items.length) {
ref
.read(postTagsNotifierProvider.notifier)
.fetch(cursor: data.nextCursor);
return const Center(child: CircularProgressIndicator());
}
final tag = data.items[index];
return ListTile(
title: Text(tag.name ?? '#${tag.slug}'),
contentPadding: 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},
);
},
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(postTagsNotifierProvider),
),
),
);
}
}

View File

@@ -8,6 +8,7 @@ import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_list.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -27,6 +28,49 @@ Future<SnPostTag> postTag(Ref ref, String slug) async {
return SnPostTag.fromJson(resp.data);
}
@riverpod
Future<bool> postCategorySubscriptionStatus(
Ref ref,
String slug,
bool isCategory,
) async {
final apiClient = ref.watch(apiClientProvider);
try {
final resp = await apiClient.get(
'/sphere/posts/${isCategory ? 'categories' : 'tags'}/$slug/subscription',
);
return resp.statusCode == 200;
} catch (_) {
return false;
}
}
Future<void> _subscribeToCategoryOrTag(
WidgetRef ref, {
required String slug,
required bool isCategory,
}) async {
final apiClient = ref.read(apiClientProvider);
await apiClient.post(
'/sphere/posts/${isCategory ? 'categories' : 'tags'}/$slug/subscribe',
);
// Invalidate the subscription status to refresh it
ref.invalidate(postCategorySubscriptionStatusProvider(slug, isCategory));
}
Future<void> _unsubscribeFromCategoryOrTag(
WidgetRef ref, {
required String slug,
required bool isCategory,
}) async {
final apiClient = ref.read(apiClientProvider);
await apiClient.post(
'/sphere/posts/${isCategory ? 'categories' : 'tags'}/$slug/unsubscribe',
);
// Invalidate the subscription status to refresh it
ref.invalidate(postCategorySubscriptionStatusProvider(slug, isCategory));
}
class PostCategoryDetailScreen extends HookConsumerWidget {
final String slug;
final bool isCategory;
@@ -41,6 +85,9 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
final postCategory =
isCategory ? ref.watch(postCategoryProvider(slug)) : null;
final postTag = isCategory ? null : ref.watch(postTagProvider(slug));
final subscriptionStatus = ref.watch(
postCategorySubscriptionStatusProvider(slug, isCategory),
);
final postFilterTitle =
isCategory
@@ -50,57 +97,158 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
return AppScaffold(
isNoBackground: false,
appBar: AppBar(title: Text(postFilterTitle).tr()),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isCategory)
postCategory!.when(
data:
(category) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(category.categoryDisplayTitle).bold().fontSize(15),
Text('A category'),
],
).padding(horizontal: 24, vertical: 16),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(postCategoryProvider(slug)),
body: Expanded(
child: CustomScrollView(
slivers: [
if (isCategory)
SliverToBoxAdapter(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 540),
child: Card(
margin: EdgeInsets.only(top: 8),
child: postCategory!.when(
data:
(category) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
category.categoryDisplayTitle,
).bold().fontSize(15),
Text('A category'),
const Gap(8),
subscriptionStatus.when(
data:
(isSubscribed) =>
isSubscribed
? FilledButton.icon(
onPressed: () async {
await _unsubscribeFromCategoryOrTag(
ref,
slug: slug,
isCategory: isCategory,
);
},
icon: const Icon(
Symbols.remove_circle,
),
label: Text('unsubscribe'.tr()),
)
: FilledButton.icon(
onPressed: () async {
await _subscribeToCategoryOrTag(
ref,
slug: slug,
isCategory: isCategory,
);
},
icon: const Icon(
Symbols.add_circle,
),
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),
),
),
loading: () => ResponseLoadingWidget(),
),
),
),
loading: () => ResponseLoadingWidget(),
)
else
postTag!.when(
data:
(tag) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(tag.name ?? '#${tag.slug}').bold().fontSize(15),
Text('A tag'),
],
).padding(horizontal: 24, vertical: 16),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(postTagProvider(slug)),
),
loading: () => ResponseLoadingWidget(),
),
const Divider(height: 1),
Expanded(
child: CustomScrollView(
slivers: [
const SliverGap(4),
SliverPostList(
categories: isCategory ? [slug] : null,
tags: isCategory ? null : [slug],
),
SliverGap(MediaQuery.of(context).padding.bottom + 8),
],
)
else
SliverToBoxAdapter(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 540),
child: Card(
margin: EdgeInsets.only(top: 8),
child: postTag!.when(
data:
(tag) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
tag.name ?? '#${tag.slug}',
).bold().fontSize(15),
Text('A tag'),
const Gap(8),
subscriptionStatus.when(
data:
(isSubscribed) =>
isSubscribed
? FilledButton.icon(
onPressed: () async {
await _unsubscribeFromCategoryOrTag(
ref,
slug: slug,
isCategory: isCategory,
);
},
icon: const Icon(
Symbols.add_circle,
),
label: Text('unsubscribe'.tr()),
)
: FilledButton.icon(
onPressed: () async {
await _subscribeToCategoryOrTag(
ref,
slug: slug,
isCategory: isCategory,
);
},
icon: const Icon(
Symbols.remove_circle,
),
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)),
),
loading: () => ResponseLoadingWidget(),
),
),
),
),
),
const SliverGap(4),
SliverPostList(
categories: isCategory ? [slug] : null,
tags: isCategory ? null : [slug],
maxWidth: 540 + 16,
),
),
],
SliverGap(MediaQuery.of(context).padding.bottom + 8),
],
),
),
);
}

View File

@@ -266,5 +266,146 @@ class _PostTagProviderElement
String get slug => (origin as PostTagProvider).slug;
}
String _$postCategorySubscriptionStatusHash() =>
r'407dc7fcaeffc461b591b4ee2418811aa4f0a63f';
/// See also [postCategorySubscriptionStatus].
@ProviderFor(postCategorySubscriptionStatus)
const postCategorySubscriptionStatusProvider =
PostCategorySubscriptionStatusFamily();
/// See also [postCategorySubscriptionStatus].
class PostCategorySubscriptionStatusFamily extends Family<AsyncValue<bool>> {
/// See also [postCategorySubscriptionStatus].
const PostCategorySubscriptionStatusFamily();
/// See also [postCategorySubscriptionStatus].
PostCategorySubscriptionStatusProvider call(String slug, bool isCategory) {
return PostCategorySubscriptionStatusProvider(slug, isCategory);
}
@override
PostCategorySubscriptionStatusProvider getProviderOverride(
covariant PostCategorySubscriptionStatusProvider provider,
) {
return call(provider.slug, provider.isCategory);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'postCategorySubscriptionStatusProvider';
}
/// See also [postCategorySubscriptionStatus].
class PostCategorySubscriptionStatusProvider
extends AutoDisposeFutureProvider<bool> {
/// See also [postCategorySubscriptionStatus].
PostCategorySubscriptionStatusProvider(String slug, bool isCategory)
: this._internal(
(ref) => postCategorySubscriptionStatus(
ref as PostCategorySubscriptionStatusRef,
slug,
isCategory,
),
from: postCategorySubscriptionStatusProvider,
name: r'postCategorySubscriptionStatusProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$postCategorySubscriptionStatusHash,
dependencies: PostCategorySubscriptionStatusFamily._dependencies,
allTransitiveDependencies:
PostCategorySubscriptionStatusFamily._allTransitiveDependencies,
slug: slug,
isCategory: isCategory,
);
PostCategorySubscriptionStatusProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.slug,
required this.isCategory,
}) : super.internal();
final String slug;
final bool isCategory;
@override
Override overrideWith(
FutureOr<bool> Function(PostCategorySubscriptionStatusRef provider) create,
) {
return ProviderOverride(
origin: this,
override: PostCategorySubscriptionStatusProvider._internal(
(ref) => create(ref as PostCategorySubscriptionStatusRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
slug: slug,
isCategory: isCategory,
),
);
}
@override
AutoDisposeFutureProviderElement<bool> createElement() {
return _PostCategorySubscriptionStatusProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PostCategorySubscriptionStatusProvider &&
other.slug == slug &&
other.isCategory == isCategory;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, slug.hashCode);
hash = _SystemHash.combine(hash, isCategory.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PostCategorySubscriptionStatusRef on AutoDisposeFutureProviderRef<bool> {
/// The parameter `slug` of this provider.
String get slug;
/// The parameter `isCategory` of this provider.
bool get isCategory;
}
class _PostCategorySubscriptionStatusProviderElement
extends AutoDisposeFutureProviderElement<bool>
with PostCategorySubscriptionStatusRef {
_PostCategorySubscriptionStatusProviderElement(super.provider);
@override
String get slug => (origin as PostCategorySubscriptionStatusProvider).slug;
@override
bool get isCategory =>
(origin as PostCategorySubscriptionStatusProvider).isCategory;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -8,6 +9,7 @@ import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_quick_reply.dart';
import 'package:island/widgets/post/post_replies.dart';
import 'package:island/widgets/response.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -55,7 +57,10 @@ class PostDetailScreen extends HookConsumerWidget {
return AppScaffold(
isNoBackground: false,
appBar: AppBar(title: const Text('Post')),
appBar: AppBar(
leading: const PageBackButton(),
title: Text('postDetail').tr(),
),
body: postState.when(
data: (post) {
return Stack(
@@ -117,8 +122,12 @@ class PostDetailScreen extends HookConsumerWidget {
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Text('Error: $e'),
loading: () => ResponseLoadingWidget(),
error:
(e, _) => ResponseErrorWidget(
error: e,
onRetry: () => ref.invalidate(postStateProvider(id)),
),
),
);
}

View File

@@ -196,7 +196,8 @@ class PublisherProfileScreen extends HookConsumerWidget {
'publisherBelongsTo'.tr(args: ['@${data.account!.name}']),
).fontSize(14),
],
).opacity(0.85).padding(bottom: 6),
).opacity(0.85),
const Gap(4),
if (data.type == 0 && data.account != null)
AccountStatusWidget(
uname: data.account!.name,
@@ -287,7 +288,11 @@ class PublisherProfileScreen extends HookConsumerWidget {
controller: categoryTabController,
dividerColor: Colors.transparent,
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
tabs: [Tab(text: 'All'), Tab(text: 'Posts'), Tab(text: 'Articles')],
tabs: [
Tab(text: 'all'.tr()),
Tab(text: 'postTypePost'.tr()),
Tab(text: 'postArticle'.tr()),
],
),
);
@@ -344,12 +349,14 @@ class PublisherProfileScreen extends HookConsumerWidget {
child: CustomScrollView(
slivers: [
SliverGap(16),
SliverPostList(pubName: name, pinned: true),
SliverToBoxAdapter(
child: publisherCategoryTabWidget(),
),
SliverPostList(
key: ValueKey(categoryTab.value),
pubName: name,
pinned: false,
type: switch (categoryTab.value) {
1 => 0,
2 => 1,
@@ -432,10 +439,12 @@ class PublisherProfileScreen extends HookConsumerWidget {
child: publisherVerificationWidget(data),
),
SliverToBoxAdapter(child: publisherBioWidget(data)),
SliverPostList(pubName: name, pinned: true),
SliverToBoxAdapter(child: publisherCategoryTabWidget()),
SliverPostList(
key: ValueKey(categoryTab.value),
pubName: name,
pinned: false,
type: switch (categoryTab.value) {
1 => 0,
2 => 1,

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:island/models/chat.dart';
import 'package:island/services/color.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/post/post_list.dart';
import 'package:palette_generator/palette_generator.dart';
@@ -142,7 +143,9 @@ class RealmDetailScreen extends HookConsumerWidget {
error: (error, _) => Center(child: Text('Error: $error')),
data: (rooms) {
if (rooms.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
return Text(
'dataEmpty',
).tr().padding(horizontal: 24, bottom: 12);
}
return Column(
children: [
@@ -242,7 +245,10 @@ class RealmDetailScreen extends HookConsumerWidget {
Flexible(
flex: 3,
child: CustomScrollView(
slivers: [SliverPostList(realm: slug)],
slivers: [
SliverPostList(realm: slug, pinned: true),
SliverPostList(realm: slug, pinned: false),
],
),
),
Flexible(
@@ -357,7 +363,8 @@ class RealmDetailScreen extends HookConsumerWidget {
SliverToBoxAdapter(
child: realmChatRoomListWidget(realm),
),
SliverPostList(realm: slug),
SliverPostList(realm: slug, pinned: true),
SliverPostList(realm: slug, pinned: false),
],
),
),
@@ -518,9 +525,11 @@ class _RealmActionMenu extends HookConsumerWidget {
class RealmMemberListNotifier extends _$RealmMemberListNotifier
with CursorPagingNotifierMixin<SnRealmMember> {
static const int _pageSize = 20;
ValueNotifier<int> totalCount = ValueNotifier(0);
@override
Future<CursorPagingData<SnRealmMember>> build(String realmSlug) async {
totalCount.value = 0;
return fetch();
}
@@ -539,6 +548,7 @@ class RealmMemberListNotifier extends _$RealmMemberListNotifier
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
totalCount.value = total;
final List<dynamic> data = response.data;
final members = data.map((e) => SnRealmMember.fromJson(e)).toList();
@@ -551,52 +561,9 @@ class RealmMemberListNotifier extends _$RealmMemberListNotifier
nextCursor: nextCursor,
);
}
}
// Keep the old provider for backward compatibility
final realmMemberStateProvider =
StateNotifierProvider.family<RealmMemberNotifier, RealmMemberState, String>(
(ref, realmSlug) {
final apiClient = ref.watch(apiClientProvider);
return RealmMemberNotifier(apiClient, realmSlug);
},
);
class RealmMemberNotifier extends StateNotifier<RealmMemberState> {
final String realmSlug;
final Dio _apiClient;
RealmMemberNotifier(this._apiClient, this.realmSlug)
: super(const RealmMemberState(members: [], isLoading: false, total: 0));
Future<void> loadMore({int offset = 0, int take = 20}) async {
if (state.isLoading) return;
if (state.total > 0 && state.members.length >= state.total) return;
state = state.copyWith(isLoading: true, error: null);
try {
final response = await _apiClient.get(
'/sphere/realms/$realmSlug/members',
queryParameters: {'offset': offset, 'take': take, 'withStatus': true},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final members = data.map((e) => SnRealmMember.fromJson(e)).toList();
state = state.copyWith(
members: [...state.members, ...members],
total: total,
isLoading: false,
);
} catch (e) {
state = state.copyWith(error: e.toString(), isLoading: false);
}
}
void reset() {
state = const RealmMemberState(members: [], isLoading: false, total: 0);
void dispose() {
totalCount.dispose();
}
}
@@ -608,18 +575,10 @@ class _RealmMemberListSheet extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final realmIdentity = ref.watch(realmIdentityProvider(realmSlug));
final memberListProvider = realmMemberListNotifierProvider(realmSlug);
// For backward compatibility and to show total count in the header
final memberState = ref.watch(realmMemberStateProvider(realmSlug));
final memberNotifier = ref.read(
realmMemberStateProvider(realmSlug).notifier,
);
final memberListNotifier = ref.watch(memberListProvider.notifier);
useEffect(() {
Future(() {
memberNotifier.loadMore();
});
return null;
return memberListNotifier.dispose;
}, []);
Future<void> invitePerson() async {
@@ -636,9 +595,7 @@ class _RealmMemberListSheet extends HookConsumerWidget {
'/sphere/realms/invites/$realmSlug',
data: {'related_user_id': result.id, 'role': 0},
);
// Refresh both providers
memberNotifier.reset();
await memberNotifier.loadMore();
// Refresh the provider
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
@@ -650,12 +607,17 @@ class _RealmMemberListSheet extends HookConsumerWidget {
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
child: Row(
children: [
Text(
'members'.plural(memberState.total),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
ListenableBuilder(
listenable: memberListNotifier.totalCount,
builder:
(context, _) => Text(
'members'.plural(memberListNotifier.totalCount.value),
key: ValueKey(memberListNotifier),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
),
),
const Spacer(),
IconButton(
@@ -666,9 +628,7 @@ class _RealmMemberListSheet extends HookConsumerWidget {
IconButton(
icon: const Icon(Symbols.refresh),
onPressed: () {
// Refresh both providers
memberNotifier.reset();
memberNotifier.loadMore();
// Refresh the provider
ref.invalidate(memberListProvider);
},
),
@@ -699,8 +659,11 @@ class _RealmMemberListSheet extends HookConsumerWidget {
final member = data.items[index];
return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id,
leading: AccountPfcGestureDetector(
uname: member.account!.name,
child: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id,
),
),
title: Row(
spacing: 6,
@@ -742,9 +705,7 @@ class _RealmMemberListSheet extends HookConsumerWidget {
),
).then((value) {
if (value != null) {
// Refresh both providers
memberNotifier.reset();
memberNotifier.loadMore();
// Refresh the provider
ref.invalidate(memberListProvider);
}
});
@@ -764,9 +725,7 @@ class _RealmMemberListSheet extends HookConsumerWidget {
await apiClient.delete(
'/sphere/realms/$realmSlug/members/${member.accountId}',
);
// Refresh both providers
memberNotifier.reset();
memberNotifier.loadMore();
// Refresh the provider
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
@@ -799,34 +758,6 @@ class _RealmMemberListSheet extends HookConsumerWidget {
}
}
class RealmMemberState {
final List<SnRealmMember> members;
final bool isLoading;
final int total;
final String? error;
const RealmMemberState({
required this.members,
required this.isLoading,
required this.total,
this.error,
});
RealmMemberState copyWith({
List<SnRealmMember>? members,
bool? isLoading,
int? total,
String? error,
}) {
return RealmMemberState(
members: members ?? this.members,
isLoading: isLoading ?? this.isLoading,
total: total ?? this.total,
error: error ?? this.error,
);
}
}
class _RealmMemberRoleSheet extends HookConsumerWidget {
final String realmSlug;
final SnRealmMember member;

View File

@@ -399,7 +399,7 @@ class _RealmChatRoomsProviderElement
}
String _$realmMemberListNotifierHash() =>
r'022bcef5a90cbae05ff23b937851afc3ef913d42';
r'db1fd8a6741dfb3d5bb921d5d965f0cfdc0e7bcc';
abstract class _$RealmMemberListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnRealmMember>> {

View File

@@ -106,7 +106,7 @@ class RealmListScreen extends HookConsumerWidget {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 540),
child: RealmListTile(realm: value[item]),
).center();
).padding(horizontal: 8).center();
},
separatorBuilder: (_, _) => const Gap(8),
),

View File

@@ -1,103 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/sticker.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
part 'marketplace.g.dart';
@riverpod
class MarketplaceStickerPacksNotifier extends _$MarketplaceStickerPacksNotifier
with CursorPagingNotifierMixin<SnStickerPack> {
@override
Future<CursorPagingData<SnStickerPack>> build() {
return fetch(cursor: null);
}
@override
Future<CursorPagingData<SnStickerPack>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/sphere/stickers',
queryParameters: {'offset': offset, 'take': 20},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final stickers = data.map((e) => SnStickerPack.fromJson(e)).toList();
final hasMore = offset + stickers.length < total;
final nextCursor = hasMore ? (offset + stickers.length).toString() : null;
return CursorPagingData(
items: stickers,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
/// User-facing marketplace screen for browsing sticker packs.
/// This version does NOT rely on publisher name (no pubName).
class MarketplaceStickersScreen extends HookConsumerWidget {
const MarketplaceStickersScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return AppScaffold(
appBar: AppBar(
title: const Text('stickers').tr(),
actions: const [Gap(8)],
),
body: const SliverMarketplaceStickerPacksList(),
);
}
}
class SliverMarketplaceStickerPacksList extends HookConsumerWidget {
const SliverMarketplaceStickerPacksList({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return PagingHelperView(
provider: marketplaceStickerPacksNotifierProvider,
futureRefreshable: marketplaceStickerPacksNotifierProvider.future,
notifierRefreshable: marketplaceStickerPacksNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final pack = data.items[index];
return ListTile(
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},
);
},
);
},
),
);
}
}

View File

@@ -1,32 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'marketplace.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$marketplaceStickerPacksNotifierHash() =>
r'b62ae8b7f5c4f8bb3be8c17fc005ea26da355187';
/// See also [MarketplaceStickerPacksNotifier].
@ProviderFor(MarketplaceStickerPacksNotifier)
final marketplaceStickerPacksNotifierProvider =
AutoDisposeAsyncNotifierProvider<
MarketplaceStickerPacksNotifier,
CursorPagingData<SnStickerPack>
>.internal(
MarketplaceStickerPacksNotifier.new,
name: r'marketplaceStickerPacksNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$marketplaceStickerPacksNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$MarketplaceStickerPacksNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnStickerPack>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -216,7 +216,7 @@ class MarketplaceStickerPackDetailScreen extends HookConsumerWidget {
),
),
),
Gap(MediaQuery.of(context).padding.bottom),
Gap(MediaQuery.of(context).padding.bottom + 16),
],
);
},

View File

@@ -0,0 +1,199 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/sticker.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'dart:async';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
part 'sticker_marketplace.g.dart';
@riverpod
class MarketplaceStickerPacksNotifier extends _$MarketplaceStickerPacksNotifier
with CursorPagingNotifierMixin<SnStickerPack> {
@override
Future<CursorPagingData<SnStickerPack>> build({
required String? query,
required bool byUsage,
}) {
return fetch(cursor: null);
}
@override
Future<CursorPagingData<SnStickerPack>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/sphere/stickers',
queryParameters: {
'offset': offset,
'take': 20,
'order': byUsage ? 'usage' : 'date',
if (query != null && query!.isNotEmpty) 'query': query,
},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final stickers = data.map((e) => SnStickerPack.fromJson(e)).toList();
final hasMore = offset + stickers.length < total;
final nextCursor = hasMore ? (offset + stickers.length).toString() : null;
return CursorPagingData(
items: stickers,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
/// User-facing marketplace screen for browsing sticker packs.
/// This version does NOT rely on publisher name (no pubName).
class MarketplaceStickersScreen extends HookConsumerWidget {
const MarketplaceStickersScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final byUsage = useState(true);
final query = useState<String?>(null);
final searchController = useTextEditingController();
final focusNode = useFocusNode();
final debounceTimer = useState<Timer?>(null);
// Clear search when query is cleared
useEffect(() {
if (query.value == null || query.value!.isEmpty) {
searchController.clear();
}
return null;
}, [query.value]);
// Clean up timer on dispose
useEffect(() {
return () {
debounceTimer.value?.cancel();
};
}, []);
return AppScaffold(
appBar: AppBar(
title: const Text('stickers').tr(),
actions: [
IconButton(
onPressed: () {
byUsage.value = !byUsage.value;
},
icon:
byUsage.value
? const Icon(Symbols.local_fire_department)
: const Icon(Symbols.access_time),
tooltip:
byUsage.value
? 'orderByPopularity'.tr()
: 'orderByReleaseDate'.tr(),
),
const Gap(8),
],
),
body: PagingHelperView(
provider: marketplaceStickerPacksNotifierProvider(
byUsage: byUsage.value,
query: query.value,
),
futureRefreshable:
marketplaceStickerPacksNotifierProvider(
byUsage: byUsage.value,
query: query.value,
).future,
notifierRefreshable:
marketplaceStickerPacksNotifierProvider(
byUsage: byUsage.value,
query: query.value,
).notifier,
contentBuilder:
(data, widgetCount, endItemView) => Column(
children: [
// Search bar above the list
Padding(
padding: const EdgeInsets.all(16),
child: SearchBar(
elevation: WidgetStateProperty.all(4),
controller: searchController,
focusNode: focusNode,
hintText: 'search'.tr(),
leading: const Icon(Symbols.search),
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(horizontal: 24),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
trailing: [
if (query.value != null && query.value!.isNotEmpty)
IconButton(
icon: const Icon(Symbols.close),
onPressed: () {
query.value = null;
searchController.clear();
focusNode.unfocus();
},
),
],
onChanged: (value) {
// Debounce search to avoid excessive API calls
debounceTimer.value?.cancel();
debounceTimer.value = Timer(
const Duration(milliseconds: 500),
() {
query.value = value.isEmpty ? null : value;
},
);
},
onSubmitted: (value) {
query.value = value.isEmpty ? null : value;
focusNode.unfocus();
},
),
),
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final pack = data.items[index];
return ListTile(
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},
);
},
);
},
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,213 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sticker_marketplace.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$marketplaceStickerPacksNotifierHash() =>
r'3bde76e18bb024f45ff6261fe735cdba97b02808';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$MarketplaceStickerPacksNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnStickerPack>> {
late final String? query;
late final bool byUsage;
FutureOr<CursorPagingData<SnStickerPack>> build({
required String? query,
required bool byUsage,
});
}
/// See also [MarketplaceStickerPacksNotifier].
@ProviderFor(MarketplaceStickerPacksNotifier)
const marketplaceStickerPacksNotifierProvider =
MarketplaceStickerPacksNotifierFamily();
/// See also [MarketplaceStickerPacksNotifier].
class MarketplaceStickerPacksNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnStickerPack>>> {
/// See also [MarketplaceStickerPacksNotifier].
const MarketplaceStickerPacksNotifierFamily();
/// See also [MarketplaceStickerPacksNotifier].
MarketplaceStickerPacksNotifierProvider call({
required String? query,
required bool byUsage,
}) {
return MarketplaceStickerPacksNotifierProvider(
query: query,
byUsage: byUsage,
);
}
@override
MarketplaceStickerPacksNotifierProvider getProviderOverride(
covariant MarketplaceStickerPacksNotifierProvider provider,
) {
return call(query: provider.query, byUsage: provider.byUsage);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'marketplaceStickerPacksNotifierProvider';
}
/// See also [MarketplaceStickerPacksNotifier].
class MarketplaceStickerPacksNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
MarketplaceStickerPacksNotifier,
CursorPagingData<SnStickerPack>
> {
/// See also [MarketplaceStickerPacksNotifier].
MarketplaceStickerPacksNotifierProvider({
required String? query,
required bool byUsage,
}) : this._internal(
() =>
MarketplaceStickerPacksNotifier()
..query = query
..byUsage = byUsage,
from: marketplaceStickerPacksNotifierProvider,
name: r'marketplaceStickerPacksNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$marketplaceStickerPacksNotifierHash,
dependencies: MarketplaceStickerPacksNotifierFamily._dependencies,
allTransitiveDependencies:
MarketplaceStickerPacksNotifierFamily._allTransitiveDependencies,
query: query,
byUsage: byUsage,
);
MarketplaceStickerPacksNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.query,
required this.byUsage,
}) : super.internal();
final String? query;
final bool byUsage;
@override
FutureOr<CursorPagingData<SnStickerPack>> runNotifierBuild(
covariant MarketplaceStickerPacksNotifier notifier,
) {
return notifier.build(query: query, byUsage: byUsage);
}
@override
Override overrideWith(MarketplaceStickerPacksNotifier Function() create) {
return ProviderOverride(
origin: this,
override: MarketplaceStickerPacksNotifierProvider._internal(
() =>
create()
..query = query
..byUsage = byUsage,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
query: query,
byUsage: byUsage,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
MarketplaceStickerPacksNotifier,
CursorPagingData<SnStickerPack>
>
createElement() {
return _MarketplaceStickerPacksNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is MarketplaceStickerPacksNotifierProvider &&
other.query == query &&
other.byUsage == byUsage;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, query.hashCode);
hash = _SystemHash.combine(hash, byUsage.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin MarketplaceStickerPacksNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnStickerPack>> {
/// The parameter `query` of this provider.
String? get query;
/// The parameter `byUsage` of this provider.
bool get byUsage;
}
class _MarketplaceStickerPacksNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
MarketplaceStickerPacksNotifier,
CursorPagingData<SnStickerPack>
>
with MarketplaceStickerPacksNotifierRef {
_MarketplaceStickerPacksNotifierProviderElement(super.provider);
@override
String? get query =>
(origin as MarketplaceStickerPacksNotifierProvider).query;
@override
bool get byUsage =>
(origin as MarketplaceStickerPacksNotifierProvider).byUsage;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -42,6 +42,16 @@ class AccountName extends StatelessWidget {
StellarMembershipMark(membership: account.perkSubscription!),
if (account.profile.verification != null)
VerificationMark(mark: account.profile.verification!),
if (account.automatedId != null)
Tooltip(
message: 'accountAutomated'.tr(),
child: Icon(
Symbols.smart_toy,
size: 16,
color: nameStyle.color,
fill: 1,
),
),
],
);
}
@@ -141,7 +151,7 @@ class VerificationStatusCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
mark.type == 4

View File

@@ -1,6 +1,7 @@
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_popup_card/flutter_popup_card.dart';
@@ -74,7 +75,42 @@ class AccountProfileCard extends HookConsumerWidget {
uname: data.name,
padding: EdgeInsets.zero,
),
if (data.profile.timeZone.isNotEmpty)
Tooltip(
message: 'creditsStatus'.tr(),
child: Row(
spacing: 6,
children: [
Icon(
Symbols.star,
size: 17,
fill: 1,
).padding(right: 2),
Text(
'${data.profile.socialCredits.toStringAsFixed(2)} pts',
).fontSize(12),
switch (data.profile.socialCreditsLevel) {
-1 => Text('socialCreditsLevelPoor').tr(),
0 => Text('socialCreditsLevelNormal').tr(),
1 => Text('socialCreditsLevelGood').tr(),
2 => Text('socialCreditsLevelExcellent').tr(),
_ => Text('unknown').tr(),
}.fontSize(12),
],
),
),
if (data.automatedId != null)
Row(
spacing: 6,
children: [
Icon(
Symbols.smart_toy,
size: 17,
fill: 1,
).padding(right: 2),
Text('accountAutomated').tr().fontSize(12),
],
),
if (data.profile.timeZone.isNotEmpty && !kIsWeb)
Row(
spacing: 6,
children: [

View File

@@ -2,7 +2,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart';
import 'package:island/models/activity.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/widgets/account/event_details_widget.dart';
import 'package:table_calendar/table_calendar.dart';
@@ -87,24 +89,56 @@ class EventCalendarWidget extends HookConsumerWidget {
return Center(child: Text(text));
},
markerBuilder: (context, day, events) {
var checkInResult =
final checkInResult =
events.whereType<SnCheckInResult>().firstOrNull;
final statuses = events.whereType<SnAccountStatus>().toList();
final textColor =
isSameDay(selectedDay.value, day)
? Colors.white
: isSameDay(DateTime.now(), day)
? Colors.white
: Theme.of(context).colorScheme.onSurface;
final shadow =
isSameDay(selectedDay.value, day) ||
isSameDay(DateTime.now(), day)
? [
Shadow(
color: Colors.black.withOpacity(0.5),
offset: const Offset(0, 1),
blurRadius: 4,
),
]
: null;
if (checkInResult != null) {
return Positioned(
top: 32,
child: Text(
'checkInResultT${checkInResult.level}'.tr(),
style: TextStyle(
fontSize: 9,
color:
isSameDay(selectedDay.value, day)
? Theme.of(context).colorScheme.onPrimaryContainer
: isSameDay(DateTime.now(), day)
? Theme.of(
context,
).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onSurface,
),
child: Row(
spacing: 2,
children: [
Text(
'checkInResultT${checkInResult.level}'.tr(),
style: TextStyle(
fontSize: 9,
color: textColor,
shadows: shadow,
),
),
if (statuses.isNotEmpty) ...[
Icon(
switch (statuses.first.attitude) {
0 => Symbols.sentiment_satisfied,
2 => Symbols.sentiment_dissatisfied,
_ => Symbols.sentiment_neutral,
},
size: 12,
color: textColor,
shadows: shadow,
),
],
],
),
);
}

View File

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:island/models/activity.dart';
import 'package:island/services/time.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -53,6 +54,33 @@ class EventDetailsWidget extends StatelessWidget {
),
],
).padding(top: 8),
if (event!.statuses.isNotEmpty) ...[
const Gap(16),
Text('statusLabel').tr().fontSize(16).bold(),
],
for (final status in event!.statuses) ...[
Row(
spacing: 8,
children: [
Icon(switch (status.attitude) {
0 => Symbols.sentiment_satisfied,
2 => Symbols.sentiment_dissatisfied,
_ => Symbols.sentiment_neutral,
}),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(status.label),
Text(
'${status.createdAt.formatSystem()} - ${status.clearedAt?.formatSystem() ?? 'present'.tr()}',
).fontSize(11).opacity(0.8),
],
),
),
],
).padding(vertical: 8),
],
],
),
if (event?.checkInResult == null && (event?.statuses.isEmpty ?? true))

View File

@@ -235,7 +235,11 @@ class PageBackButton extends StatelessWidget {
return IconButton(
onPressed: () {
onWillPop?.call();
context.pop();
if (context.canPop()) {
context.pop();
} else {
context.go('/');
}
},
icon: Icon(
color: color,

View File

@@ -50,6 +50,6 @@ class AppWrapper extends HookConsumerWidget {
}
}
return TourTriggerWidget(child: child);
return TourTriggerWidget(key: UniqueKey(), child: child);
}
}

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