Compare commits

..

57 Commits

Author SHA1 Message Date
446c33d8b0 🐛 Fix wrong route path 2025-07-06 12:28:42 +08:00
996462f1fd 🚀 Launch 3.0.0+112 2025-07-03 22:25:02 +08:00
778f6bb79f Android deeplinking 2025-07-03 22:18:02 +08:00
8747f948b9 Apple deeplinking 2025-07-03 22:14:34 +08:00
9546d6e4b8 🗑️ Disable the technical review tour 2025-07-03 22:11:12 +08:00
f8d1940af6 Report user 2025-07-03 22:07:51 +08:00
b2b0891d24 Block user 2025-07-03 22:03:12 +08:00
274168d4bc 💄 Better abuse reports 2025-07-03 21:31:37 +08:00
2c98b348d5 Abuse reports 2025-07-03 20:53:30 +08:00
afc7887ddd 🐛 Fix delete post on explore didn't refresh 2025-07-03 19:08:56 +08:00
99ff78a3d5 🐛 Fix post item padding 2025-07-03 13:12:39 +08:00
2ad85addf6 🐛 Fix wrong notification permission is requested 2025-07-03 01:29:43 +08:00
552b4b2572 🚀 Launch 3.0.0+111 2025-07-03 01:20:54 +08:00
594ac39e3d Article attachments 2025-07-03 01:18:07 +08:00
23321171f3 💄 Optimized attachment insert in article compose 2025-07-03 01:11:56 +08:00
ee72d79c93 Hide attachments list on article 2025-07-03 01:06:01 +08:00
a20c2598fc 🐛 Fixes render errors of unauthorized 2025-07-03 00:49:11 +08:00
2eba871a6d 💄 Web article has max width 2025-07-03 00:39:45 +08:00
46919dec31 📝 Updated about screen 2025-07-03 00:34:11 +08:00
9dd6cffe0c 🐛 Trying to fix push notification 2025-07-03 00:27:30 +08:00
2ea9f5e907 💄 Optimized rendering for article post 2025-07-03 00:16:10 +08:00
050750a808 🐛 Fixes articles bugs 2025-07-02 23:57:07 +08:00
f479b9fc8b 🐛 Bug fixes on posts writing and etc 2025-07-02 23:34:27 +08:00
13ea182707 💄 Localized about page 2025-07-02 22:59:28 +08:00
14183a7316 💄 Colorful name for subscribed users 2025-07-02 22:24:56 +08:00
9fc9b87608 💄 Optimized leveling page 2025-07-02 22:17:25 +08:00
53c2445ba9 🐛 Remove extra items inside settings 2025-07-02 21:47:26 +08:00
d414695eb3 🍱 Update English translation 2025-07-02 21:44:54 +08:00
27bc17079e 🔀 Merge pull request #52 from Solsynth/l10n_v3
New Crowdin updates
2025-07-02 21:43:11 +08:00
295188459b New translations assets/i18n/en-US.json (bundle: 6) 2025-07-02 21:40:33 +08:00
66115258a7 New translations assets/i18n/zh-TW.json (bundle: 6) 2025-07-02 21:40:32 +08:00
2cf2c515b4 New translations assets/i18n/zh-CN.json (bundle: 6) 2025-07-02 21:40:32 +08:00
925cb2b423 🔀 Merge pull request #51 from Solsynth/l10n_v3
New Crowdin updates
2025-07-02 21:36:36 +08:00
0a2804a404 New translations assets/i18n/en-US.json (bundle: 6) 2025-07-02 21:34:38 +08:00
12bbcbf69c New translations assets/i18n/zh-TW.json (bundle: 6) 2025-07-02 21:34:37 +08:00
52ce490725 New translations assets/i18n/zh-CN.json (bundle: 6) 2025-07-02 21:34:37 +08:00
82067fb3aa 🐛 Fix explore page styling 2025-07-02 13:17:45 +08:00
007acedf29 🚀 Launch 3.0.0+110 2025-07-02 02:23:46 +08:00
8e903ec6c1 About page 2025-07-02 02:20:31 +08:00
b55e56c3c4 Web articles list 2025-07-02 01:11:25 +08:00
6f9de431b1 Search post 2025-07-02 00:40:35 +08:00
a8efd26262 ⬆️ Upgrade dependencies 2025-07-01 22:54:58 +08:00
e367fc3f5c Web articles detail page & explore feed 2025-07-01 13:19:14 +08:00
8a1af120ea Discover article 2025-07-01 01:34:34 +08:00
f03f0181f8 EXIF viewer 2025-07-01 00:14:12 +08:00
6c7d42c31a 🐛 Bug fixes 2025-06-30 23:55:44 +08:00
d6c829c26a Web feed 2025-06-30 23:33:14 +08:00
666a2dfbf5 Feature flags 2025-06-30 01:11:31 +08:00
fd979c3a35 Custom apps 2025-06-30 00:22:59 +08:00
847fc6e864 💄 Optimized custom app display 2025-06-29 23:34:56 +08:00
356b7bf01a Developer app basis 2025-06-29 23:00:51 +08:00
450d5ebc81 Developer portal basis 2025-06-29 19:36:37 +08:00
f04285848f 🐛 Fix upload file in share sheet 2025-06-29 18:03:18 +08:00
c4becb0a05 Publisher members 2025-06-28 18:57:32 +08:00
d22619396b 🐛 Fix linux builds 2025-06-28 11:45:50 +08:00
fe8640a6db 🚀 Launch 3.0.0+109 2025-06-28 02:48:15 +08:00
ff475d43dd 🐛 Dozen of hot fixes 2025-06-28 02:40:50 +08:00
112 changed files with 11089 additions and 1678 deletions

View File

@ -3,7 +3,7 @@ name: Build Release
on:
push:
tags:
- '*'
- "*"
workflow_dispatch:
jobs:
@ -59,6 +59,7 @@ jobs:
sudo apt-get install -y libnotify-dev
sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
sudo apt-get install -y gstreamer-1.0
sudo apt-get install -y libsecret-1-0
- run: flutter pub get
- run: flutter build linux
- name: Archive production artifacts
@ -80,4 +81,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: build-output-linux-appimage
path: './*.AppImage*'
path: "./*.AppImage*"

View File

@ -41,7 +41,16 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deeplinking -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="solian.app" />
<data android:scheme="https" />
</intent-filter>
<!-- Share Intent Filters -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
@ -98,7 +107,7 @@
<receiver
android:name=".receiver.ReplyReceiver"
android:enabled="true"
android:exported="false" />
android:exported="true" />
<service
android:name=".service.MessagingService"

View File

@ -1,14 +1,39 @@
package dev.solsynth.solian
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin
class MainActivity : FlutterActivity()
{
private val CHANNEL = "dev.solsynth.solian/notifications"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// https://github.com/flutter/flutter/issues/153075#issuecomment-2693189362
flutterEngine.plugins.add(LegacySharedPreferencesPlugin())
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "initialLink") {
val roomId = intent.getStringExtra("room_id")
if (roomId != null) {
result.success("/rooms/$roomId")
} else {
result.success(null)
}
} else {
result.notImplemented()
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val roomId = intent.getStringExtra("room_id")
if (roomId != null) {
MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, CHANNEL).invokeMethod("newLink", "/rooms/$roomId")
}
}
}

View File

@ -1,48 +1,47 @@
package dev.solsynth.solian.network
import android.content.Context
import okhttp3.*
import android.content.SharedPreferences
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONObject
import java.io.IOException
class ApiClient(private val context: Context) {
private val client = OkHttpClient()
private val sharedPreferences: SharedPreferences = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
fun sendMessage(roomId: String, content: String, repliedMessageId: String, callback: () -> Unit) {
val prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
val token = prefs.getString("flutter.token", null)
val serverUrl = prefs.getString("flutter.serverUrl", null)
if (token == null || serverUrl == null) {
fun sendMessage(roomId: String, message: String, replyTo: String, callback: (Boolean) -> Unit) {
val token = sharedPreferences.getString("flutter.token", null)
if (token == null) {
callback(false)
return
}
val url = "$serverUrl/chat/$roomId/messages"
val json = JSONObject()
json.put("content", content)
json.put("replied_message_id", repliedMessageId)
val requestBody = json.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
val json = JSONObject().apply {
put("content", message)
put("reply_to", replyTo)
}
val body = json.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
val request = Request.Builder()
.url(url)
.post(requestBody)
.addHeader("Authorization", "AtField $token")
.url("https://solian.dev/api/rooms/$roomId/messages")
.header("Authorization", "Bearer $token")
.post(body)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
// Handle failure
callback()
callback(false)
}
override fun onResponse(call: Call, response: Response) {
// Handle success
callback()
callback(response.isSuccessful)
}
})
}
}
}

View File

@ -72,6 +72,7 @@ class MessagingService: FirebaseMessagingService() {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.putExtra("room_id", roomId)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, pendingIntentFlags)
val notificationBuilder = NotificationCompat.Builder(this, "messages")

View File

@ -46,7 +46,7 @@
"delete": "Delete",
"deletePublisher": "Delete Publisher",
"deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.",
"somethingWentWrong": "Something went wrong...",
"somethingWentWrong": "Something went wrong",
"deletePost": "Delete Post",
"safetyReport": "Report",
"safetyReportTitle": "Safety Report",
@ -89,14 +89,32 @@
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
"authFactorPin": "Pin Code",
"authFactorPinDescription": "It consists of 6 digits. It cannot be used to log in. When performing some dangerous operations, the system will ask you to enter this PIN for confirmation.",
"realms": "Realms",
"createRealm": "Create a Realm",
"createRealmHint": "Meet friends with same interests, build communities, and more.",
"editRealm": "Edit Realm",
"deleteRealm": "Delete Realm",
"deleteRealmHint": "Are you sure to delete this realm? This will also deleted all the channels, publishers, and posts under this realm.",
"explore": "Explore",
"exploreFilterSubscriptions": "Subscriptions",
"exploreFilterFriends": "Friends",
"discover": "Discover",
"joinRealm": "Join Realm",
"account": "Account",
"name": "Name",
"slug": "Slug",
"slugHint": "The slug will be used in the URL to access this resource, it should be unique and URL safe.",
"createChatRoom": "Create a Room",
"editChatRoom": "Edit Room",
"deleteChatRoom": "Delete Room",
"deleteChatRoomHint": "Are you sure to delete this room? This action cannot be undone.",
"chat": "Chat",
"chatTabAll": "All",
"chatTabDirect": "Direct Messages",
"chatTabGroup": "Group Chats",
"chatMessageHint": "Message in {}",
"chatDirectMessageHint": "Message to {}",
"directMessage": "Direct Message",
"loading": "Loading...",
"descriptionNone": "No description yet.",
"invites": "Invites",
@ -231,6 +249,7 @@
"uploadingProgress": "Uploading {} of {}",
"uploadAll": "Upload All",
"stickerCopyPlaceholder": "Copy Placeholder",
"realmSelection": "Select a Realm",
"individual": "Individual",
"firstPostBadgeName": "First Post",
"firstPostBadgeDescription": "Created your first post on Solar Network",
@ -286,6 +305,12 @@
"levelingProgressExperience": "{} EXP",
"levelingProgressLevel": "Level {}",
"fileUploadingProgress": "Uploading file #{}: {}%",
"removeChatMember": "Remove Chat Room Member",
"removeChatMemberHint": "Are you sure to remove this member from the room?",
"removeRealmMember": "Remove Realm Member",
"removeRealmMemberHint": "Are you sure to remove this member from the realm?",
"removePublisherMember": "Remove Publisher Member",
"removePublisherMemberHint": "Are you sure to remove this member from the publisher?",
"memberRole": "Member Role",
"memberRoleHint": "Greater number has higher permission.",
"memberRoleEdit": "Edit role for @{}",
@ -293,6 +318,10 @@
"openLinkConfirmDescription": "You're going to leave the Solar Network and open the link ({}) in your browser. It is not related to Solar Network. Beware of phishing and scams.",
"brokenLink": "Unable open link {}... It might be broken or missing uri parts...",
"copyToClipboard": "Copy to clipboard",
"leaveChatRoom": "Leave Chat Room",
"leaveChatRoomHint": "Are you sure to leave this chat room?",
"leaveRealm": "Leave Realm",
"leaveRealmHint": "Are you sure to leave this realm?",
"walletNotFound": "Wallet not found",
"walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.",
"walletCreate": "Create a Wallet",
@ -304,6 +333,12 @@
"settingsBackgroundImageClear": "Clear Background Image",
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
"messageNone": "No content to display",
"unreadMessages": {
"one": "{} unread message",
"other": "{} unread messages"
},
"chatBreakNone": "None",
"settingsRealmCompactView": "Compact Realm View",
"settingsMixedFeed": "Mixed Feed",
"settingsAutoTranslate": "Auto Translate",
"settingsHideBottomNav": "Hide Bottom Navigation",
@ -340,12 +375,15 @@
"postContent": "Content",
"postSettings": "Settings",
"postPublisherUnselected": "Publisher Unspecified",
"postVisibility": "Visibility",
"postType": "Post Type",
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
"postVisibility": "Post Visibility",
"postVisibilityPublic": "Public",
"postVisibilityFriends": "Friends Only",
"postVisibilityUnlisted": "Unlisted",
"postVisibilityPrivate": "Private",
"postTruncated": "Content truncated, tap to view full post",
"copyMessage": "Copy Message",
"authFactor": "Authentication Factor",
"authFactorDelete": "Delete the Factor",
"authFactorDeleteHint": "Are you sure you want to delete this authentication factor? This action cannot be undone.",
@ -373,6 +411,10 @@
"lastActiveAt": "Last active at {}",
"authDeviceLogout": "Logout",
"authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.",
"typingHint": {
"one": "{} is typing...",
"other": "{} are typing..."
},
"authDeviceEditLabel": "Edit Label",
"authDeviceLabelTitle": "Edit Device Label",
"authDeviceLabelHint": "Enter a name for this device",
@ -439,6 +481,21 @@
"contactMethodSetPrimary": "Set as Primary",
"contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications",
"contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.",
"chatNotifyLevel": "Notify Level",
"chatNotifyLevelDescription": "Decide how many notifications you will receive.",
"chatNotifyLevelAll": "All",
"chatNotifyLevelMention": "Mentions",
"chatNotifyLevelNone": "None",
"chatNotifyLevelUpdated": "The notify level has been updated to {}.",
"chatBreak": "Take a Break",
"chatBreakDescription": "Set a time, before that time, your notification level will be metions only, to take a break of the current topic they're talking about.",
"chatBreakClear": "Clear the break time",
"chatBreakHour": "{} break",
"chatBreakDay": "{} day break",
"chatBreakSet": "Break set for {}",
"chatBreakCleared": "Chat break has been cleared.",
"chatBreakCustom": "Custom duration",
"chatBreakEnterMinutes": "Enter minutes",
"firstName": "First Name",
"middleName": "Middle Name",
"lastName": "Last Name",
@ -483,29 +540,19 @@
"paymentError": "Payment failed: {error}",
"usePinInstead": "Use PIN Code",
"levelProgress": "Level Progress",
"unlockedFeatures": "Unlocked Features",
"unlockedFeaturesDescription": "Features unlocked at your current level will be displayed here.",
"stellarMembership": "Stellar Membership",
"upgradeYourPlan": "Upgrade Your Plan",
"chooseYourPlan": "Choose Your Plan",
"currentMembership": "Current: {}",
"currentMembershipMember": "A member of Stellar Program · {}",
"membershipExpires": "Expires: {}",
"membershipTierStellar": "Stellar",
"membershipTierNova": "Nova",
"membershipTierSupernova": "Supernova",
"membershipTierUnknown": "Unknown",
"membershipPriceStellar": "10 NS$ per month",
"membershipPriceNova": "20 NS$ per month",
"membershipPriceSupernova": "30 NS$ per month",
"membershipFeatureBasic": "Basic features",
"membershipFeaturePrioritySupport": "Priority support",
"membershipFeatureAdFree": "Ad-free experience",
"membershipFeatureAllPrimary": "All Primary features",
"membershipFeatureAdvancedCustomization": "Advanced customization",
"membershipFeatureEarlyAccess": "Early access",
"membershipFeatureAllNova": "All Nova features",
"membershipFeatureExclusiveContent": "Exclusive content",
"membershipFeatureVipSupport": "VIP support",
"membershipPriceStellar": "1200 NSP per month, level 3+ required",
"membershipPriceNova": "2400 NSP per month, level 6+ required",
"membershipPriceSupernova": "3600 NSP per month, level 9+ required",
"membershipCurrentBadge": "CURRENT",
"restorePurchase": "Restore Purchase",
"restorePurchaseDescription": "Enter your payment provider and order ID to restore your purchase.",
@ -520,17 +567,30 @@
"quickActions": "Quick Actions",
"post": "Post",
"copy": "Copy",
"sendToChat": "Send to Chat",
"failedToShareToPost": "Failed to share to post: {}",
"shareToChatComingSoon": "Share to chat functionality coming soon",
"failedToShareToChat": "Failed to share to chat: {}",
"shareToSpecificChatComingSoon": "Share to {} coming soon",
"directChat": "Direct Chat",
"systemShareComingSoon": "System share functionality coming soon",
"failedToShareToSystem": "Failed to share to system: {}",
"failedToCopy": "Failed to copy: {}",
"noChatRoomsAvailable": "No chat rooms available",
"failedToLoadChats": "Failed to load chats",
"contentToShare": "Content to share:",
"unknownChat": "Unknown Chat",
"addAdditionalMessage": "Add additional message...",
"uploadingFiles": "Uploading files...",
"sharedSuccessfully": "Shared successfully!",
"shareSuccess": "Shared successfully!",
"shareToSpecificChatSuccess": "Shared to {} successfully!",
"wouldYouLikeToGoToChat": "Would you like to go to the chat?",
"no": "No",
"yes": "Yes",
"navigateToChat": "Navigate to Chat",
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
"abuseReports": "Abuse Reports",
"abuseReport": "Report",
"abuseReportTitle": "Report Content",
"abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.",
@ -564,5 +624,79 @@
"realmJoinSuccess": "Successfully joined the realm.",
"discoverRealms": "Discover Realms",
"discoverPublishers": "Discover Publishers",
"search": "Search"
}
"search": "Search",
"publisherMembers": "Collaborators",
"developerHub": "Developer Hub",
"developerHubUnselectedHint": "Select a developer to see stats or enroll a new one.",
"enrollDeveloper": "Enroll as a Developer",
"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",
"customApps": "Custom Apps",
"noCustomApps": "No custom apps yet.",
"createCustomApp": "Create Custom App",
"editCustomApp": "Edit Custom App",
"deleteCustomApp": "Delete Custom App",
"deleteCustomAppHint": "Are you sure you want to delete this custom app? This action cannot be undone.",
"publicRealm": "Public Realm",
"publicRealmDescription": "Anyone can preview the content of this realm.",
"communityRealm": "Community Realm",
"communityRealmDescription": "Anyone can join this realm and participate in discussions. And will show in the discover page & feed.",
"publicChat": "Public Chat",
"publicChatDescription": "Anyone can preview the content of this chat. Including unjoined bots.",
"communityChat": "Community Chat",
"communityChatDescription": "Anyone can join this chat and participate in discussions.",
"appLinks": "App Links",
"homePageUrl": "Home Page URL",
"privacyPolicyUrl": "Privacy Policy URL",
"termsOfServiceUrl": "Terms of Service URL",
"oauthConfig": "OAuth Configuration",
"clientUri": "Client URI",
"redirectUris": "Redirect URIs",
"addRedirectUri": "Add Redirect URI",
"allowedScopes": "Allowed Scopes",
"requirePkce": "Require PKCE",
"allowOfflineAccess": "Allow Offline Access",
"redirectUri": "Redirect URI",
"redirectUriHint": "The redirect URI is used for OAuth authentication. When the app goes to production, we will validate the redirect URI is match your configuration to reject invalid requests.",
"uriRequired": "The URI is required.",
"uriInvalid": "The URI is invalid.",
"add": "Add",
"addScope": "Add Scope",
"scope": "Scope",
"publisherFeatures": "Features",
"publisherFeatureDevelop": "Developer Program",
"publisherFeatureDevelopDescription": "Unlock development abilities for your publisher, including custom apps, API keys, and more.",
"publisherFeatureDevelopHint": "Currently, this feature is under active development, you need send a request to unlock this feature.",
"learnMore": "Learn More",
"discoverWebArticles": "Articles from external sites",
"webArticlesStand": "Article Stand",
"about": "About",
"membershipCancel": "Cancel Membership",
"membershipCancelConfirm": "Are you sure to cancel your membership?",
"membershipCancelHint": "Are you sure to cancel your membership? You will not be charged again. Your membership will remain active until the end of the current billing period. And you will not able to resubscribe until the end of the current subscription ends.",
"membershipCancelSuccess": "Your membership has been successfully canceled.",
"aboutScreenTitle": "About",
"aboutScreenVersionInfo": "Version {} ({})",
"aboutScreenAppInfoSectionTitle": "App Information",
"aboutScreenPackageNameLabel": "Package Name",
"aboutScreenVersionLabel": "Version",
"aboutScreenBuildNumberLabel": "Build Number",
"aboutScreenLinksSectionTitle": "Links",
"aboutScreenPrivacyPolicyTitle": "Privacy Policy",
"aboutScreenTermsOfServiceTitle": "Terms of Service",
"aboutScreenOpenSourceLicensesTitle": "Open Source Licenses",
"aboutScreenDeveloperSectionTitle": "Developer",
"aboutScreenContactUsTitle": "Contact Us",
"aboutScreenLicenseTitle": "License",
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
"aboutScreenCopyright": "All rights reserved © Solsynth {}",
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
"copiedToClipboard": "Copied to clipboard",
"copyToClipboardTooltip": "Copy to clipboard",
"postForwardingTo": "Forwarding to",
"postReplyingTo": "Replying to",
"postEditing": "You are editing an existing post",
"postArticle": "Article"
}

View File

@ -10,6 +10,8 @@
"loginEnterPassword": "输入验证码",
"loginSuccess": "已登录为 {}",
"loginGreeting": "欢迎回来!",
"loginOr": "或使用\n第三方登录",
"loginInProgress": "登录中……",
"username": "用户名",
"usernameCannotChangeHint": "用户名创建后无法更改。",
"usernameLookupHint": "您也可以输入电子邮件地址。",
@ -44,7 +46,7 @@
"delete": "删除",
"deletePublisher": "删除发布者",
"deletePublisherHint": "确定要删除此发布者吗?这也会删除此发布者下的所有帖子和收藏。",
"somethingWentWrong": "发生了一些错误...",
"somethingWentWrong": "发生了一些错误",
"deletePost": "删除帖子",
"deletePostHint": "确定要删除这篇帖子吗?",
"copyLink": "复制链接",
@ -59,10 +61,12 @@
"authFactorPasswordDescription": "您注册时设置的密码。",
"authFactorEmail": "电子邮件验证码",
"authFactorEmailDescription": "发送到您注册时设置的电子邮件地址的一次性验证码。",
"authFactorTOTP": "基于时间的一次性密码 (TOTP)",
"authFactorTOTPDescription": "由 TOTP 验证器(例如 Google Authenticator 或 Authy生成的一次性验证码。",
"authFactorTOTP": "时序验证码",
"authFactorTOTPDescription": "由 TOTP 验证器生成的一次性验证码。",
"authFactorInAppNotify": "应用内通知",
"authFactorInAppNotifyDescription": "通过应用内通知发送的一次性验证码。",
"authFactorPin": "Pin 码",
"authFactorPinDescription": "它由6位数字组成。它不能用于登录。 当执行一些危险的操作时,系统将要求您输入此 PIN 进行确认。",
"realms": "领域",
"createRealm": "创建领域",
"createRealmHint": "结识志同道合的朋友、建立社区等等。",
@ -70,9 +74,10 @@
"deleteRealm": "删除领域",
"deleteRealmHint": "确定要删除此领域吗?这也会删除此领域下的所有频道、发布者和帖子。",
"explore": "探索",
"exploreFilterSubscriptions": "已关注",
"exploreFilterFriends": "好友圈",
"account": "账号",
"name": "名称",
"description": "描述",
"slug": "别名",
"slugHint": "此别名将用于 URL 以访问此资源,它应该独一无二且 URL 安全。",
"createChatRoom": "创建聊天室",
@ -86,10 +91,10 @@
"chatMessageHint": "在 {} 消息",
"chatDirectMessageHint": "消息给 {}",
"directMessage": "私人消息",
"loading": "载入中...",
"loading": "载入中……",
"descriptionNone": "暂无描述。",
"invites": "邀请",
"invitesEmpty": "暂无邀请,真是个孤独的人...",
"invitesEmpty": "暂无邀请,真是个孤独的人……",
"members": {
"one": "{} 位成员",
"other": "{} 位成员"
@ -98,13 +103,20 @@
"permissionModerator": "版主",
"permissionMember": "成员",
"reply": "回复",
"repliesCount": {
"zero": "暂无回复",
"one": "{} 回复",
"other": "{} 个回复"
},
"forward": "转发",
"repliedTo": "回复了",
"forwarded": "转发了",
"hasAttachments": {
"one": "{} 个附件",
"other": "{}个附件"
},
"postHasAttachments": {
"one": "{} 个附件",
"other": "{}个附件"
},
"edited": "已编辑",
@ -112,6 +124,7 @@
"addPhoto": "添加照片",
"addFile": "添加文件",
"createDirectMessage": "创建新私人消息",
"gotoDirectMessage": "前往私信",
"react": "反应",
"reactions": {
"zero": "反应",
@ -124,6 +137,25 @@
"connectionConnected": "已连接",
"connectionDisconnected": "已断开连接",
"connectionReconnecting": "重新连接中",
"accountConnections": "帐户连接",
"accountConnectionsDescription": "管理您的外部帐户连接",
"accountConnectionAdd": "添加连接",
"accountConnectionDelete": "删除连接",
"accountConnectionDeleteHint": "您确定要删除此连接吗?此操作无法撤消。",
"accountConnectionsEmpty": "未找到连接。请添加连接以便开始。",
"accountConnectionProvider": "平台",
"accountConnectionProviderHint": "输入平台名称",
"accountConnectionIdentifier": "标识",
"accountConnectionIdentifierHint": "输入此平台的标识",
"accountConnectionDescription": "添加连接以将您的帐户与外部服务链接起来。",
"accountConnectionAddSuccess": "添加连接成功。",
"accountConnectionAddError": "无法建立连接。",
"accountConnectionProviderApple": "Apple",
"accountConnectionProviderMicrosoft": "Microsoft",
"accountConnectionProviderGoogle": "Google",
"accountConnectionProviderGithub": "GitHub",
"accountConnectionProviderDiscord": "Discord",
"accountConnectionProviderAfdian": "爱发电",
"checkIn": "签到",
"checkInNone": "尚未签到",
"checkInNoneHint": "通过签到获取您的财富提示和每日奖励。",
@ -132,14 +164,11 @@
"checkInResultLevel2": "一个普通的日常",
"checkInResultLevel3": "好运",
"checkInResultLevel4": "最佳运气",
"checkInResultLevelShort0": "最差",
"checkInResultLevelShort1": "坏",
"checkInResultLevelShort2": "普通",
"checkInResultLevelShort3": "好",
"checkInResultLevelShort4": "最佳",
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
"eventCalander": "活动日历",
"eventCalanderEmpty": "该日无活动。",
"fortuneGraph": "时运趋势",
"noFortuneData": "本月沒有时运數據。",
"creatorHub": "创作者中心",
"creatorHubDescription": "管理帖子、分析等。",
"developerPortal": "开发者入口",
@ -195,7 +224,7 @@
"uploading": "上传中",
"uploadingProgress": "正在上传 {} / {}",
"uploadAll": "全部上传",
"stickerCopyPlaceholder": "复制占位符",
"stickerCopyPlaceholder": "复制表情占位符",
"realmSelection": "选择一个领域",
"individual": "个人",
"firstPostBadgeName": "首篇帖子",
@ -213,9 +242,9 @@
"expertBadgeName": "专家",
"expertBadgeDescription": "因您的专业知识和宝贵贡献而受到认可",
"founderBadgeName": "创始人",
"founderBadgeDescription": " Solar Network 最早的成员之一",
"founderBadgeDescription": "Solar Network 最早的成员之一",
"betaTesterBadgeName": "Beta 测试员",
"betaTesterBadgeDescription": "在 Beta 测试期间协助测试和改进 Solar Network ",
"betaTesterBadgeDescription": "在 Beta 测试期间协助测试和改进 Solar Network",
"moderatorBadgeName": "版主",
"moderatorBadgeDescription": "协助维护和管理社区",
"developerBadgeName": "开发者",
@ -231,6 +260,7 @@
"creatorHubUnselectedHint": "选择/创建一个发布者以开始使用。",
"relationships": "关系",
"addFriend": "发送好友请求",
"addFriendShort": "添加好友",
"addFriendHint": "将朋友添加到您的关系列表。",
"pendingRequest": "待处理",
"waitingRequest": "等待中",
@ -258,9 +288,9 @@
"memberRole": "成员角色",
"memberRoleHint": "数字越大权限越高。",
"memberRoleEdit": "编辑 @{} 的角色",
"openLinkConfirm": "离开 Solar Network ",
"openLinkConfirm": "你正在离开 Solar Network",
"openLinkConfirmDescription": "您将离开 Solar Network 并在浏览器中打开链接 ({})。它与 Solar Network 无关。请注意网络钓鱼和诈骗。",
"brokenLink": "无法打开链接 {}... 它可能已损坏或缺少 URI 部分...",
"brokenLink": "无法打开链接 {}…… 它可能已损坏或缺少 URI 部分……",
"copyToClipboard": "复制到剪贴板",
"leaveChatRoom": "离开聊天室",
"leaveChatRoomHint": "确定要离开此聊天室吗?",
@ -275,11 +305,13 @@
"posts": "帖子",
"settingsBackgroundImage": "背景图片",
"settingsBackgroundImageClear": "清除背景图片",
"settingsBackgroundGenerateColor": "从背景图像生成主题色",
"messageNone": "没有内容可显示",
"unreadMessages": {
"one": "{} 条未读消息",
"other": "{} 条未读消息"
},
"chatBreakNone": "无",
"settingsRealmCompactView": "紧凑领域视图",
"settingsMixedFeed": "混合动态",
"settingsAutoTranslate": "自动翻译",
@ -287,12 +319,118 @@
"settingsSoundEffects": "音效",
"settingsAprilFoolFeatures": "愚人节功能",
"settingsEnterToSend": "按下 Enter 发送",
"settingsTransparentAppBar": "使用完全透明的状态栏",
"settingsCustomFonts": "自定义字体",
"settingsCustomFontsHint": "应用中的所有文本都将使用自定义字体。请确保您的设备上已安装该字体。",
"settingsColorScheme": "色彩主题",
"postTitle": "标题",
"postDescription": "描述",
"call": "通话",
"done": "完成",
"loginResetPasswordSent": "密码重置邮件已发送,请检查您的收件箱。",
"accountDeletion": "删除帐户",
"accountDeletionHint": "您确定要删除您的帐户吗? 如果您确认,我们将向您的电子邮件地址发送一封确认邮件。 您可以按照电子邮件中的安装继续删除过程。",
"accountDeletionSent": "帐号删除确认邮件已发送,请检查您的邮箱。",
"accountSecurityTitle": "安全选项",
"accountDangerZoneTitle": "危险操作",
"accountPassword": "密码",
"accountPasswordDescription": "更改您的账户密码",
"accountPasswordChange": "更改密码",
"accountPasswordChangeSent": "密码重置邮件已发送,请检查您的收件箱。",
"accountPasswordChangeDescription": "我们将向您的电子邮件地址发送一封电子邮件以重置您的密码。",
"accountAuthFactor": "认证因子",
"accountAuthFactorDescription": "确保安全和多因子身份验证矶",
"accountDeletionDescription": "永久删除您的帐户和所有数据",
"accountSettingsHelp": "账户设置帮助",
"accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。",
"unauthorized": "未授权",
"unauthorizedHint": "您未登录或会话已过期,请重新登录。",
"publisherBelongsTo": "属于 {}",
"postContent": "内容",
"postSettings": "设置",
"postPublisherUnselected": "未指定发布者",
"postVisibility": "可见性",
"postVisibilityPublic": "公开",
"postVisibilityFriends": "仅好友可见",
"postVisibilityUnlisted": "不公开",
"postVisibilityPrivate": "私密",
"postTruncated": "内容已截断,点击查看完整帖子",
"copyMessage": "复制消息",
"authFactor": "身份验证因子",
"authFactorDelete": "删除验证因子",
"authFactorDeleteHint": "您确定要删除此连接吗?此操作无法撤消。",
"authFactorDisable": "禁用因子认证",
"authFactorDisableHint": "您确定要禁用此身份验证因素吗?您可以稍后再启用它。",
"authFactorEnable": "启用双因子认证",
"authFactorEnableHint": "授权因子生成的代码来启用它。",
"authFactorNew": "创建认证的因子",
"authFactorSecret": "密钥",
"authFactorSecretHint": "为此因子创建一个秘密。",
"authFactorQrCodeScan": "用您的身份验证程序扫描这个二维码来设置 TOTP 身份验证",
"authFactorNoQrCode": "此身份验证因子没有可用的 QR 代码",
"cancel": "取消",
"confirm": "确认",
"authFactorAdditional": "最后一步",
"authFactorHint": "联系方式",
"authFactorHintHelper": "您需要提供您的联系方式,若与我们的记录相符,我们将会向该联系方式发送验证码",
"authSessions": "活跃会话",
"authSessionsDescription": "查看您当前登录的设备。",
"authSessionsCount": {
"one": "{} 会话",
"other": "{} 会话"
},
"authDeviceCurrent": "当前设备",
"lastActiveAt": "最后一次活动于 {}",
"authDeviceLogout": "登出",
"authDeviceLogoutHint": "您确定要注销此设备吗?这也会禁用掉此设备的推送通知。",
"authDeviceEditLabel": "编辑标签",
"authDeviceLabelTitle": "编辑设备标签",
"authDeviceLabelHint": "给设备命名",
"authDeviceSwipeEditHint": "左滑编辑标签",
"authDeviceSwipeLogoutHint": "右滑登出设备",
"typingHint": {
"one": "{} 正在输入……",
"other": "{} 正在输入……"
},
"settingsAppearance": "外观",
"settingsServer": "服务器",
"settingsBehavior": "行为",
"settingsDesktop": "桌面",
"settingsKeyboardShortcuts": "快捷键",
"settingsEnterToSendDesktopHint": "按 Enter 键发送消息,使用 Shift+Enter 添加换行。",
"settingsHelp": "设置帮助",
"settingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果需要其他帮助,请联系管理员。",
"settingsKeyboardShortcutSearch": "搜索",
"settingsKeyboardShortcutSettings": "设置",
"settingsKeyboardShortcutNewMessage": "新消息",
"settingsKeyboardShortcutCloseDialog": "关闭对话框",
"close": "关闭",
"contactMethod": "联系方式",
"contactMethodType": "联系方式类型",
"contactMethodTypeEmail": "电子邮件",
"contactMethodTypePhone": "电话",
"contactMethodTypeAddress": "地址",
"contactMethodEmailHint": "请输入您的电子邮件地址",
"contactMethodPhoneHint": "请输入您的电话号码",
"contactMethodAddressHint": "输入您的现实地址",
"contactMethodEmailDescription": "您的电子邮件将用于帐户恢复和通知",
"contactMethodPhoneDescription": "您的电话号码将用于帐户恢复和通知",
"contactMethodAddressDescription": "您的实际地址将用于运输和计费目的。",
"contactMethodVerified": "已验证",
"contactMethodUnverified": "未认证",
"contactMethodVerify": "验证联系方式",
"contactMethodDelete": "删除联系方式",
"contactMethodNew": "新建联系方式",
"contactMethodContentEmpty": "联系方式内容不能为空",
"contactMethodVerificationSent": "验证码已发送到对应的联系方式",
"contactMethodVerificationNeeded": "联系方式已添加,但尚未验证。您可以通过点击它来验证。",
"accountContactMethod": "联系方法",
"accountContactMethodDescription": "管理您的账户恢复和通知的联系方式",
"authFactorVerificationNeeded": "认证因子已添加,但尚未启用。您可以通过点击它并输入验证码来启用。",
"contactMethodPrimary": "主要的",
"contactMethodSetPrimary": "设为主要",
"contactMethodSetPrimaryHint": "设置此联系方式作为您的账户恢复和通知的主要联系方式",
"contactMethodDeleteHint": "确定要删除此贴图吗?此操作无法撤销。",
"chatNotifyLevel": "通知级别",
"chatNotifyLevelDescription": "决定您将收到多少通知。",
"chatNotifyLevelAll": "全部",
@ -308,49 +446,99 @@
"chatBreakCleared": "聊天暂停已清除。",
"chatBreakCustom": "自定义时长",
"chatBreakEnterMinutes": "输入分钟数",
"chatBreakNone": "",
"firstName": "姓名",
"middleName": "中间名",
"lastName": "姓氏",
"gender": "性別",
"pronouns": "代词",
"location": "位置",
"timeZone": "时区",
"birthday": "生日",
"selectADate": "选择日期",
"checkInResultT0": "大凶",
"checkInResultT1": "凶",
"checkInResultT2": "中平",
"checkInResultT3": "吉",
"checkInResultT4": "大吉",
"authenticating": "认证中...",
"processing": "处理中...",
"processingPayment": "处理付款中...",
"accountProfileView": "查看个人资料",
"unspecified": "未指定",
"added": "已添加",
"preview": "预览",
"togglePreview": "切换预览",
"subscribe": "订阅",
"unsubscribe": "取消订阅",
"paymentVerification": "支付验证",
"paymentSummary": "付款摘要",
"amount": "数量",
"description": "描述",
"pinCode": "PIN 码",
"biometric": "生物识别",
"enterPinToConfirm": "请输入您的 6 位数字 PIN 以确认付款",
"clearPin": "清除 PIN 码",
"useBiometricToConfirm": "使用生物特征认证来确认付款",
"touchSensorToAuthenticate": "触摸传感器进行身份验证",
"authenticating": "认证中……",
"authenticateNow": "立即认证",
"processing": "处理中……",
"processingPayment": "处理付款中……",
"pleaseWait": "请稍候",
"paymentFailed": "付款失败,请重试。",
"invalidPin": "错误的 PIN。请再试一次。",
"biometricAuthFailed": "生物识别身份验证失败。请重试。",
"paymentSuccess": "付款成功完成!",
"drafts": "草稿",
"noDrafts": "暂无草稿",
"articleDrafts": "文章草稿",
"postDrafts": "帖子草稿",
"saveDraft": "保存草稿",
"draftSaved": "草稿已保存",
"draftSaveFailed": "保存草稿失败",
"clearAllDrafts": "清空所有草稿",
"clearAllDraftsConfirm": "确定要删除所有草稿吗?此操作无法撤销。",
"clearAll": "清空全部",
"untitled": "无标题",
"noContent": "无内容",
"justNow": "刚刚",
"minutesAgo": "{} 分钟前",
"hoursAgo": "{} 小时前",
"postContentEmpty": "帖子内容不能为空",
"share": "分享",
"quickActions": "快捷操作",
"post": "帖子",
"copy": "复制",
"sendToChat": "发送到聊天",
"failedToShareToPost": "分享到帖子失败:{}",
"shareToChatComingSoon": "聊天分享功能即将推出",
"failedToShareToChat": "分享到聊天失败:{}",
"shareToSpecificChatComingSoon": "分享到 {} 即将推出",
"directChat": "私聊",
"systemShareComingSoon": "系统分享功能即将推出",
"failedToShareToSystem": "系统分享失败:{}",
"membershipPurchaseSuccess": "好耶,会员购买成功!",
"paymentError": "付款失败: {error}",
"usePinInstead": "使用 PIN 码",
"levelProgress": "等级进度",
"stellarMembership": "恒星计划",
"upgradeYourPlan": "升级您的计划",
"chooseYourPlan": "选择你的方案",
"currentMembership": "当前:{}",
"currentMembershipMember": "恒星计划「{}」级会员",
"membershipExpires": "过期于:{}",
"membershipTierStellar": "恒星",
"membershipTierNova": "新星",
"membershipTierSupernova": "超新星",
"membershipTierUnknown": "未知",
"membershipPriceStellar": "每月 1200 源点,至少需要 3 级",
"membershipPriceNova": "每月 2400 源点,至少需要 6 级",
"membershipPriceSupernova": "每月 3600 源点,至少需要 9 级",
"membershipFeatureBasic": "基础功能",
"membershipCurrentBadge": "当前",
"restorePurchase": "恢复购买",
"restorePurchaseDescription": "输入您付款的提供商和订单 ID 以恢复您的购买。",
"provider": "平台",
"selectProvider": "选择一个平台",
"orderId": "订单 ID",
"enterOrderId": "输入您的订单 ID",
"restore": "恢复",
"keyboardShortcuts": "键盘快捷键",
"about": "关于",
"membershipCancel": "取消会员订阅",
"membershipCancelConfirm": "您确定要取消您的会员订阅?",
"membershipCancelHint": "您确定要取消您的会员订阅吗?您将不会再被收费。您的会员资格将在当前计费周期结束前保持有效。并且您在当前订阅结束之前无法重新订阅。",
"membershipCancelSuccess": "您的会员订阅已成功取消。",
"aboutScreenTitle": "关于",
"aboutScreenVersionInfo": "版本 {} ({})",
"aboutScreenAppInfoSectionTitle": "应用信息",
"aboutScreenPackageNameLabel": "包名",
"aboutScreenVersionLabel": "版本",
"aboutScreenBuildNumberLabel": "构建编号",
"aboutScreenLinksSectionTitle": "链接",
"aboutScreenPrivacyPolicyTitle": "隐私政策",
"aboutScreenTermsOfServiceTitle": "服务条款",
"aboutScreenOpenSourceLicensesTitle": "开源许可证",
"aboutScreenDeveloperSectionTitle": "开发者",
"aboutScreenContactUsTitle": "联系我们",
"aboutScreenLicenseTitle": "许可证",
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
"aboutScreenCopyright": "版权所有 © 索尔辛茨 {}",
"aboutScreenMadeWith": "由 Solar Network Team 用 ❤︎️ 制作",
"aboutScreenFailedToLoadPackageInfo": "加载包信息失败:{error}",
"copiedToClipboard": "已复制到剪贴板",
"failedToCopy": "复制失败:{}",
"noChatRoomsAvailable": "没有可用的聊天室",
"failedToLoadChats": "加载聊天失败",
"unknownChat": "未知聊天"
}
"copyToClipboardTooltip": "复制到剪贴板",
"postForwardingTo": "转发给",
"postReplyingTo": "回复给",
"postEditing": "您正在编辑现有帖子",
"postArticle": "文章"
}

View File

@ -1,371 +1,525 @@
{
"login": "登入",
"loginDescription": "既有用戶?歡迎您回來!",
"forgotPassword": "忘記密碼",
"loginPickFactor": "選擇驗證方式",
"loginMultiFactor": {
"one": "還剩 {} 步驟",
"other": "還剩 {} 步驟"
},
"loginEnterPassword": "輸入驗證碼",
"loginSuccess": "已登入為 {}",
"loginGreeting": "歡迎回來!",
"username": "使用者名稱",
"usernameCannotChangeHint": "使用者名稱建立後無法更改。",
"usernameLookupHint": "您也可以輸入電子郵件地址。",
"unknown": "未知",
"termAcceptNextWithAgree": "繼續即表示您同意我們的服務條款及其他條款和條件。",
"termAcceptLink": "查看詳情",
"loginResetPasswordHint": "請提供您的使用者名稱以接收密碼重設連結。",
"password": "密碼",
"next": "下一步",
"createAccount": "建立帳號",
"createAccountDescription": "新來乍到?我們隨時為您服務!",
"nickname": "暱稱",
"email": "電子郵件",
"bio": "個人簡介",
"fieldCannotBeEmpty": "此欄位不能為空。",
"fieldEmailAddressMustBeValid": "電子郵件地址必須有效。",
"logout": "登出",
"updateYourProfile": "編輯個人資料",
"accountBasicInfo": "基本資料",
"accountProfile": "您的個人資料",
"saveChanges": "儲存變更",
"publishers": "發佈者",
"managedPublisher": "已管理發佈者",
"createPublisher": "建立發佈者",
"createPublisherHint": "用於建立貼文、收藏等。",
"editPublisher": "編輯發佈者",
"syncPublisher": "使用帳號資料",
"syncPublisherRealm": "使用領域資料",
"create": "建立",
"update": "更新",
"edit": "編輯",
"delete": "刪除",
"deletePublisher": "刪除發佈者",
"deletePublisherHint": "確定要刪除此發佈者嗎?這也將刪除此發佈者下的所有貼文和收藏。",
"somethingWentWrong": "發生了一些錯誤...",
"deletePost": "刪除貼文",
"deletePostHint": "確定要刪除這篇貼文嗎?",
"copyLink": "複製連結",
"postCreateAccountTitle": "感謝您的加入!",
"postCreateAccountNext": "下一步?",
"postCreateAccountNext1": "前往您的電子郵件收件匣並接收帳號啟用電子郵件。",
"postCreateAccountNext2": "登入您的帳號並開始探索 Solar Network ",
"postPlaceholder": "在想些什麼?",
"publishersEmpty": "尚未有發佈者",
"publishersEmptyDescription": "您需要建立一個發佈者才能開始發佈您的貼文。",
"authFactorPassword": "密碼",
"authFactorPasswordDescription": "您註冊時設定的密碼。",
"authFactorEmail": "電子郵件驗證碼",
"authFactorEmailDescription": "發送到您註冊時設定的電子郵件地址的一次性驗證碼。",
"authFactorTOTP": "時間基於一次性密碼 (TOTP)",
"authFactorTOTPDescription": "由 TOTP 驗證器(例如 Google Authenticator 或 Authy生成的一次性驗證碼。",
"authFactorInAppNotify": "應用程式內通知",
"authFactorInAppNotifyDescription": "透過應用程式內通知發送的一次性驗證碼。",
"realms": "領域",
"createRealm": "建立領域",
"createRealmHint": "結識志同道合的朋友、建立社群等等。",
"editRealm": "編輯領域",
"deleteRealm": "刪除領域",
"deleteRealmHint": "確定要刪除此領域嗎?這也將刪除該領域下的所有頻道、發佈者和貼文。",
"explore": "探索",
"account": "帳號",
"name": "名稱",
"description": "描述",
"slug": "代稱",
"slugHint": "此代稱將用於 URL 以存取此資源,它應該是獨一無二且 URL 安全的。",
"createChatRoom": "建立聊天室",
"editChatRoom": "編輯聊天室",
"deleteChatRoom": "刪除聊天室",
"deleteChatRoomHint": "確定要刪除此聊天室嗎?此操作無法撤銷。",
"chat": "聊天",
"chatTabAll": "全部",
"chatTabDirect": "私人訊息",
"chatTabGroup": "群組聊天",
"chatMessageHint": "在 {} 訊息",
"chatDirectMessageHint": "訊息給 {}",
"directMessage": "私人訊息",
"loading": "載入中...",
"descriptionNone": "尚未有描述。",
"invites": "邀請",
"invitesEmpty": "尚未有邀請,真是個寂寞的人...",
"members": {
"one": "{} 位成員",
"other": "{} 位成員"
},
"permissionOwner": "擁有者",
"permissionModerator": "版主",
"permissionMember": "成員",
"reply": "回覆",
"forward": "轉發",
"repliedTo": "回覆了",
"forwarded": "轉發了",
"hasAttachments": {
"other": "{}個附件"
},
"postHasAttachments": {
"other": "{}個附件"
},
"edited": "已編輯",
"addVideo": "新增影片",
"addPhoto": "新增照片",
"addFile": "新增檔案",
"createDirectMessage": "建立新私人訊息",
"react": "反應",
"reactions": {
"zero": "反應",
"one": "{} 個反應",
"other": "{} 個反應"
},
"reactionPositive": "正面",
"reactionNegative": "負面",
"reactionNeutral": "中立",
"connectionConnected": "已連線",
"connectionDisconnected": "已中斷連線",
"connectionReconnecting": "重新連線中",
"checkIn": "簽到",
"checkInNone": "尚未簽到",
"checkInNoneHint": "透過簽到獲取您的財富提示和每日獎勵。",
"checkInResultLevel0": "最差運氣",
"checkInResultLevel1": "壞運氣",
"checkInResultLevel2": "一個普通的日子",
"checkInResultLevel3": "好運",
"checkInResultLevel4": "最佳運氣",
"checkInResultLevelShort0": "最差",
"checkInResultLevelShort1": "",
"checkInResultLevelShort2": "普通",
"checkInResultLevelShort3": "",
"checkInResultLevelShort4": "最佳",
"checkInActivityTitle": "{} 在 {} 簽到並獲得了 {}",
"eventCalander": "活動日曆",
"eventCalanderEmpty": "該日無活動。",
"creatorHub": "創作者中心",
"creatorHubDescription": "管理貼文、分析等。",
"developerPortal": "開發者入口",
"developerPortalDescription": "使用 Solar Network ™ 進行建構。",
"statusCreateHint": "您在想些什麼?新增狀態。",
"statusCreate": "新增狀態",
"statusUpdate": "更新狀態",
"statusLabel": "狀態",
"statusAttitude": "態度",
"attitudePositive": "正面",
"attitudeNeutral": "中立",
"attitudeNegative": "負面",
"statusInvisible": "隱身",
"statusInvisibleDescription": "您將顯示為離線狀態。",
"statusNotDisturb": "勿擾",
"statusNotDisturbDescription": "推播通知將被禁用。",
"statusClearTime": "清除於",
"statusNoAutoClear": "不自動清除",
"online": "線上",
"offline": "離線",
"status": "狀態",
"statusActivityTitle": "{} 正在 {} {}",
"statusActivityEndedTitle": "{} 正在 {} {} 直到 {}",
"appSettings": "應用程式設定",
"accountSettings": "帳號設定",
"settings": "設定",
"language": "語言",
"accountLanguageHint": "此語言將用於電子郵件和推播通知。",
"settingsDisplayLanguage": "顯示語言",
"languageFollowSystem": "跟隨系統",
"postsCreatedCount": "貼文",
"stickerPacksCreatedCount": "貼圖包",
"stickersCreatedCount": "貼圖",
"upvoteReceived": "收到的讚",
"downvoteReceived": "收到的倒讚",
"stickerPacks": "貼圖包",
"createStickerPack": "建立貼圖包",
"editStickerPack": "編輯貼圖包",
"deleteStickerPack": "刪除貼圖包",
"deleteStickerPackHint": "確定要刪除此貼圖包嗎?此操作無法撤銷。",
"stickerPackPrefix": "前綴",
"stickerPackPrefixHint": "此前綴將添加在此包中每個貼圖的代稱之前。",
"stickers": "貼圖",
"createSticker": "建立貼圖",
"editSticker": "編輯貼圖",
"deleteSticker": "刪除貼圖",
"deleteStickerHint": "確定要刪除此貼圖嗎?此操作無法撤銷。",
"stickerImage": "圖片",
"stickerSlug": "代稱",
"stickerSlugHint": "此代稱將與前綴結合形成貼圖的唯一識別碼。",
"dataEmpty": "這裡什麼都沒有。",
"pickFile": "選擇檔案",
"uploading": "上傳中",
"uploadingProgress": "正在上傳 {} / {}",
"uploadAll": "全部上傳",
"stickerCopyPlaceholder": "複製佔位符",
"realmSelection": "選擇一個領域",
"individual": "個人",
"firstPostBadgeName": "首篇貼文",
"firstPostBadgeDescription": "在 Solar Network 建立您的首篇貼文",
"popularPostBadgeName": "熱門貼文",
"popularPostBadgeDescription": "您的貼文獲得了社群的顯著參與",
"viralPostBadgeName": "病毒式貼文",
"viralPostBadgeDescription": "您的貼文廣為流傳並觸及了廣泛的受眾",
"helpfulCommentBadgeName": "有幫助的評論",
"helpfulCommentBadgeDescription": "您的評論被其他人標記為有幫助",
"newcomerBadgeName": "新成員",
"newcomerBadgeDescription": "歡迎來到 Solar Network !開始探索和聯繫",
"contributorBadgeName": "貢獻者",
"contributorBadgeDescription": "積極為 Solar Network 社群做出貢獻",
"expertBadgeName": "專家",
"expertBadgeDescription": "因您的專業知識和寶貴貢獻而受到認可",
"founderBadgeName": "創始人",
"founderBadgeDescription": " Solar Network 最早的成員之一",
"betaTesterBadgeName": "Beta 測試員",
"betaTesterBadgeDescription": "在 Beta 測試期間協助測試和改進 Solar Network ",
"moderatorBadgeName": "版主",
"moderatorBadgeDescription": "協助維護和管理社群",
"developerBadgeName": "開發者",
"developerBadgeDescription": "為 Solar Network 的開發做出貢獻",
"translatorBadgeName": "翻譯者",
"translatorBadgeDescription": "協助將 Solar Network 翻譯成不同語言",
"wallet": "錢包",
"walletCurrencyPoints": "新太陽點",
"walletCurrencyShortPoints": "NSP",
"walletCurrencyGolds": "太陽幣",
"walletCurrencyShortGolds": "TSD",
"retry": "重試",
"creatorHubUnselectedHint": "選擇/建立一個發佈者以開始使用。",
"relationships": "關係",
"addFriend": "傳送好友邀請",
"addFriendHint": "將朋友新增到您的關係清單。",
"pendingRequest": "待處理",
"waitingRequest": "等待中",
"relationshipStatusFriend": "朋友",
"relationshipStatusBlocked": "已封鎖",
"blockUser": "封鎖用戶",
"unblockUser": "解除封鎖用戶",
"friendRequestAccepted": "已接受 {} 的好友邀請",
"friendRequestDeclined": "已拒絕 {} 的好友邀請",
"requestExpiredIn": "{} 後過期",
"friendSentRequest": "已傳送的好友邀請",
"friendSentRequestEmpty": "尚未傳送好友邀請",
"friendSentRequestHint": {
"one": "已傳送 {} 個好友邀請",
"other": "已傳送 {} 個好友邀請"
},
"levelingProgress": "等級進度",
"levelingProgressExperience": "{} 經驗值",
"levelingProgressLevel": "等級 {}",
"fileUploadingProgress": "正在上傳檔案 #{}: {}%",
"removeChatMember": "移除聊天室成員",
"removeChatMemberHint": "確定要將此成員從聊天室中移除嗎?",
"removeRealmMember": "移除領域成員",
"removeRealmMemberHint": "確定要將此成員從領域中移除嗎?",
"memberRole": "成員角色",
"memberRoleHint": "數字越大權限越高。",
"memberRoleEdit": "編輯 @{} 的角色",
"openLinkConfirm": "離開 Solar Network ",
"openLinkConfirmDescription": "您將離開 Solar Network 並在瀏覽器中開啟連結 ({})。它與 Solar Network 無關。請注意網路釣魚和詐騙。",
"brokenLink": "無法開啟連結 {}... 它可能已損壞或缺少 URI 部分...",
"copyToClipboard": "複製到剪貼簿",
"leaveChatRoom": "離開聊天室",
"leaveChatRoomHint": "確定要離開此聊天室嗎?",
"leaveRealm": "離開領域",
"leaveRealmHint": "確定要離開此領域嗎?",
"walletNotFound": "找不到錢包",
"walletCreateHint": "您尚未有錢包。建立一個以開始使用 Solar Network 電子錢包。",
"walletCreate": "建立錢包",
"settingsServerUrl": "伺服器 URL",
"settingsApplied": "設定已應用。",
"notifications": "通知",
"posts": "貼文",
"settingsBackgroundImage": "背景圖片",
"settingsBackgroundImageClear": "清除背景圖片",
"messageNone": "沒有內容可顯示",
"unreadMessages": {
"one": "{} 條未讀訊息",
"other": "{} 條未讀訊息"
},
"settingsRealmCompactView": "精簡領域視圖",
"settingsMixedFeed": "混合動態",
"settingsAutoTranslate": "自動翻譯",
"settingsHideBottomNav": "隱藏底部導航",
"settingsSoundEffects": "音效",
"settingsAprilFoolFeatures": "愚人節功能",
"settingsEnterToSend": "按下 Enter 傳送",
"postVisibility": "可見性",
"postVisibilityPublic": "公開",
"postVisibilityFriends": "僅好友可見",
"postVisibilityUnlisted": "不公開",
"postVisibilityPrivate": "私密",
"chatNotifyLevel": "通知等級",
"chatNotifyLevelDescription": "決定您將收到多少通知。",
"chatNotifyLevelAll": "全部",
"chatNotifyLevelMention": "提及",
"chatNotifyLevelNone": "",
"chatNotifyLevelUpdated": "通知等級已更新為 {}。",
"chatBreak": "暫停聊天",
"chatBreakDescription": "設定一個時間,在該時間之前,您的通知等級將僅為提及,以暫時休息當前討論的話題。",
"chatBreakClear": "清除暫停時間",
"chatBreakHour": "暫停 {} 分鐘",
"chatBreakDay": "暫停 {} 天",
"chatBreakSet": "已設定暫停 {}",
"chatBreakCleared": "聊天暫停已清除。",
"chatBreakCustom": "自訂時長",
"chatBreakEnterMinutes": "輸入分鐘數",
"chatBreakNone": "無",
"paymentError": "付款失敗:{error}",
"usePinInstead": "使用密碼",
"levelProgress": "等級進度",
"unlockedFeatures": "已解鎖功能",
"unlockedFeaturesDescription": "您目前等級解鎖的功能將會顯示在此。",
"stellarMembership": "星際會員",
"upgradeYourPlan": "升級您的方案",
"chooseYourPlan": "選擇您的方案",
"currentMembership": "目前:{}",
"membershipExpires": "到期:{}",
"membershipTierStellar": "星際",
"membershipTierNova": "新星",
"membershipTierSupernova": "超新星",
"membershipTierUnknown": "未知",
"membershipPriceStellar": "每月 10 星幣",
"membershipPriceNova": "每月 20 星幣",
"membershipPriceSupernova": "每月 30 星幣",
"membershipFeatureBasic": "基本功能",
"membershipFeaturePrioritySupport": "優先支援",
"membershipFeatureAdFree": "無廣告體驗",
"membershipFeatureAllPrimary": "所有主要功能",
"membershipFeatureAdvancedCustomization": "進階自訂",
"membershipFeatureEarlyAccess": "搶先體驗",
"membershipFeatureAllNova": "所有新星功能",
"membershipFeatureExclusiveContent": "獨家內容",
"membershipFeatureVipSupport": "VIP 支援",
"membershipCurrentBadge": "目前",
"drafts": "草稿",
"noDrafts": "暫無草稿",
"articleDrafts": "文章草稿",
"postDrafts": "貼文草稿",
"saveDraft": "儲存草稿",
"draftSaved": "草稿已儲存",
"draftSaveFailed": "儲存草稿失敗",
"clearAllDrafts": "清空所有草稿",
"clearAllDraftsConfirm": "確定要刪除所有草稿嗎?此操作無法復原。",
"clearAll": "清空全部",
"untitled": "無標題",
"noContent": "無內容",
"justNow": "剛剛",
"minutesAgo": "{} 分鐘前",
"hoursAgo": "{} 小時前",
"postContentEmpty": "貼文內容不能為空",
"share": "分享",
"quickActions": "快速操作",
"post": "貼文",
"copy": "複製",
"sendToChat": "傳送到聊天",
"failedToShareToPost": "分享到貼文失敗:{}",
"shareToChatComingSoon": "聊天分享功能即將推出",
"failedToShareToChat": "分享到聊天失敗:{}",
"shareToSpecificChatComingSoon": "分享到 {} 即將推出",
"directChat": "私人聊天",
"systemShareComingSoon": "系統分享功能即將推出",
"failedToShareToSystem": "系統分享失敗:{}",
"copiedToClipboard": "已複製到剪貼簿",
"failedToCopy": "複製失敗:{}",
"noChatRoomsAvailable": "沒有可用的聊天室",
"failedToLoadChats": "載入聊天失敗",
"unknownChat": "未知聊天"
"login": "登入",
"loginDescription": "既有用戶?歡迎您回來!",
"forgotPassword": "忘記密碼",
"loginPickFactor": "選擇驗證方式",
"loginMultiFactor": {
"one": "還剩 {} 步驟",
"other": "還剩 {} 步驟"
},
"loginEnterPassword": "輸入驗證碼",
"loginSuccess": "已登入為 {}",
"loginGreeting": "歡迎回來!",
"loginOr": "Or login with\nthird parties",
"loginInProgress": "Logging you in...",
"username": "使用者名稱",
"usernameCannotChangeHint": "使用者名稱建立後無法更改。",
"usernameLookupHint": "您也可以輸入電子郵件地址。",
"unknown": "未知",
"termAcceptNextWithAgree": "繼續即表示您同意我們的服務條款及其他條款和條件。",
"termAcceptLink": "查看詳情",
"loginResetPasswordHint": "請提供您的使用者名稱以接收密碼重設連結。",
"password": "密碼",
"next": "下一步",
"createAccount": "建立帳號",
"createAccountDescription": "新來乍到?我們隨時為您服務!",
"nickname": "暱稱",
"email": "電子郵件",
"bio": "個人簡介",
"fieldCannotBeEmpty": "此欄位不能為空。",
"fieldEmailAddressMustBeValid": "電子郵件地址必須有效。",
"logout": "登出",
"updateYourProfile": "編輯個人資料",
"accountBasicInfo": "基本資料",
"accountProfile": "您的個人資料",
"saveChanges": "儲存變更",
"publishers": "發佈者",
"managedPublisher": "已管理發佈者",
"createPublisher": "建立發佈者",
"createPublisherHint": "用於建立貼文、收藏等。",
"editPublisher": "編輯發佈者",
"syncPublisher": "使用帳號資料",
"syncPublisherRealm": "使用領域資料",
"create": "建立",
"update": "更新",
"edit": "編輯",
"delete": "刪除",
"deletePublisher": "刪除發佈者",
"deletePublisherHint": "確定要刪除此發佈者嗎?這也將刪除此發佈者下的所有貼文和收藏。",
"somethingWentWrong": "發生了一些錯誤...",
"deletePost": "刪除貼文",
"deletePostHint": "確定要刪除這篇貼文嗎?",
"copyLink": "複製連結",
"postCreateAccountTitle": "感謝您的加入!",
"postCreateAccountNext": "下一步?",
"postCreateAccountNext1": "前往您的電子郵件收件匣並接收帳號啟用電子郵件。",
"postCreateAccountNext2": "登入您的帳號並開始探索 Solar Network ",
"postPlaceholder": "在想些什麼?",
"publishersEmpty": "尚未有發佈者",
"publishersEmptyDescription": "您需要建立一個發佈者才能開始發佈您的貼文。",
"authFactorPassword": "碼",
"authFactorPasswordDescription": "您註冊時設定的碼。",
"authFactorEmail": "電子郵件驗證碼",
"authFactorEmailDescription": "發送到您註冊時設定的電子郵件地址的一次性驗證碼。",
"authFactorTOTP": "時間基於一次性密碼 (TOTP)",
"authFactorTOTPDescription": "由 TOTP 驗證器(例如 Google Authenticator 或 Authy生成的一次性驗證碼。",
"authFactorInAppNotify": "應用程式內通知",
"authFactorInAppNotifyDescription": "透過應用程式內通知發送的一次性驗證碼。",
"authFactorPin": "Pin Code",
"authFactorPinDescription": "It consists of 6 digits. It cannot be used to log in. When performing some dangerous operations, the system will ask you to enter this PIN for confirmation.",
"realms": "領域",
"createRealm": "建立領域",
"createRealmHint": "結識志同道合的朋友、建立社群等等。",
"editRealm": "編輯領域",
"deleteRealm": "刪除領域",
"deleteRealmHint": "確定要刪除此領域嗎?這也將刪除該領域下的所有頻道、發佈者和貼文。",
"explore": "探索",
"exploreFilterSubscriptions": "Subscriptions",
"exploreFilterFriends": "Friends",
"account": "帳號",
"name": "名稱",
"slug": "代稱",
"slugHint": "此代稱將用於 URL 以存取此資源,它應該是獨一無二且 URL 安全的。",
"createChatRoom": "建立聊天室",
"editChatRoom": "編輯聊天室",
"deleteChatRoom": "刪除聊天",
"deleteChatRoomHint": "確定要刪除此聊天室嗎?此操作無法撤銷。",
"chat": "聊天",
"chatTabAll": "全部",
"chatTabDirect": "私人訊息",
"chatTabGroup": "群組聊天",
"chatMessageHint": "在 {} 訊息",
"chatDirectMessageHint": "訊息給 {}",
"directMessage": "私人訊息",
"loading": "載入中...",
"descriptionNone": "尚未有描述。",
"invites": "邀請",
"invitesEmpty": "尚未有邀請,真是個寂寞的人...",
"members": {
"one": "{} 位成員",
"other": "{} 位成員"
},
"permissionOwner": "擁有者",
"permissionModerator": "版主",
"permissionMember": "成員",
"reply": "回覆",
"repliesCount": {
"zero": "No reply",
"one": "{} reply",
"other": "{} replies"
},
"forward": "轉發",
"repliedTo": "回覆了",
"forwarded": "轉發了",
"hasAttachments": {
"one": "{} attachment",
"other": "{}個附件"
},
"postHasAttachments": {
"one": "{} attachment",
"other": "{}個附件"
},
"edited": "已編輯",
"addVideo": "新增影片",
"addPhoto": "新增照片",
"addFile": "新增檔案",
"createDirectMessage": "建立新私人訊息",
"gotoDirectMessage": "Go to DM",
"react": "反應",
"reactions": {
"zero": "反應",
"one": "{} 個反應",
"other": "{} 個反應"
},
"reactionPositive": "正面",
"reactionNegative": "負面",
"reactionNeutral": "中立",
"connectionConnected": "已連線",
"connectionDisconnected": "已中斷連線",
"connectionReconnecting": "重新連線中",
"accountConnections": "Account Connections",
"accountConnectionsDescription": "Manage your external account connections",
"accountConnectionAdd": "Add Connection",
"accountConnectionDelete": "Delete Connection",
"accountConnectionDeleteHint": "Are you sure you want to delete this connection? This action cannot be undone.",
"accountConnectionsEmpty": "No connections found. Add a connection to get started.",
"accountConnectionProvider": "Provider",
"accountConnectionProviderHint": "Enter provider name",
"accountConnectionIdentifier": "Identifier",
"accountConnectionIdentifierHint": "Enter your identifier for this provider",
"accountConnectionDescription": "Add a connection to link your account with external services.",
"accountConnectionAddSuccess": "Connection added successfully.",
"accountConnectionAddError": "Unable to setup connection.",
"accountConnectionProviderApple": "Apple",
"accountConnectionProviderMicrosoft": "Microsoft",
"accountConnectionProviderGoogle": "Google",
"accountConnectionProviderGithub": "GitHub",
"accountConnectionProviderDiscord": "Discord",
"accountConnectionProviderAfdian": "Afdian",
"checkIn": "簽到",
"checkInNone": "尚未簽到",
"checkInNoneHint": "透過簽到獲取您的財富提示和每日獎勵。",
"checkInResultLevel0": "最差運氣",
"checkInResultLevel1": "壞運氣",
"checkInResultLevel2": "一個普通的日子",
"checkInResultLevel3": "好運",
"checkInResultLevel4": "最佳運氣",
"checkInActivityTitle": "{} 在 {} 簽到並獲得了 {}",
"eventCalander": "活動日曆",
"eventCalanderEmpty": "該日無活動。",
"fortuneGraph": "Fortune Trend",
"noFortuneData": "No fortune data available for this month.",
"creatorHub": "創作者中心",
"creatorHubDescription": "管理貼文、分析等。",
"developerPortal": "開發者入口",
"developerPortalDescription": "使用 Solar Network ™ 進行建構。",
"statusCreateHint": "您在想些什麼?新增狀態。",
"statusCreate": "新增狀態",
"statusUpdate": "更新狀態",
"statusLabel": "狀態",
"statusAttitude": "態度",
"attitudePositive": "正面",
"attitudeNeutral": "中立",
"attitudeNegative": "負面",
"statusInvisible": "隱身",
"statusInvisibleDescription": "您將顯示為離線狀態。",
"statusNotDisturb": "勿擾",
"statusNotDisturbDescription": "推播通知將被禁用。",
"statusClearTime": "清除於",
"statusNoAutoClear": "不自動清除",
"online": "線上",
"offline": "離線",
"status": "狀態",
"statusActivityTitle": "{} 正在 {} {}",
"statusActivityEndedTitle": "{} 正在 {} {} 直到 {}",
"appSettings": "應用程式設定",
"accountSettings": "帳號設定",
"settings": "設定",
"language": "語言",
"accountLanguageHint": "此語言將用於電子郵件和推播通知。",
"settingsDisplayLanguage": "顯示語言",
"languageFollowSystem": "跟隨系統",
"postsCreatedCount": "貼文",
"stickerPacksCreatedCount": "貼圖包",
"stickersCreatedCount": "貼圖",
"upvoteReceived": "收到的讚",
"downvoteReceived": "收到的倒讚",
"stickerPacks": "貼圖包",
"createStickerPack": "建立貼圖包",
"editStickerPack": "編輯貼圖包",
"deleteStickerPack": "刪除貼圖包",
"deleteStickerPackHint": "確定要刪除此貼圖包嗎?此操作無法撤銷。",
"stickerPackPrefix": "前綴",
"stickerPackPrefixHint": "此前綴將添加在此包中每個貼圖的代稱之前。",
"stickers": "貼圖",
"createSticker": "建立貼圖",
"editSticker": "編輯貼圖",
"deleteSticker": "刪除貼圖",
"deleteStickerHint": "確定要刪除此貼圖嗎?此操作無法撤銷。",
"stickerImage": "圖片",
"stickerSlug": "代稱",
"stickerSlugHint": "此代稱將與前綴結合形成貼圖的唯一識別碼。",
"dataEmpty": "這裡什麼都沒有。",
"pickFile": "選擇檔案",
"uploading": "上傳中",
"uploadingProgress": "正在上傳 {} / {}",
"uploadAll": "全部上傳",
"stickerCopyPlaceholder": "複製佔位符",
"realmSelection": "選擇一個領域",
"individual": "個人",
"firstPostBadgeName": "首篇貼文",
"firstPostBadgeDescription": "在 Solar Network 建立您的首篇貼文",
"popularPostBadgeName": "熱門貼文",
"popularPostBadgeDescription": "您的貼文獲得了社群的顯著參與",
"viralPostBadgeName": "病毒式貼文",
"viralPostBadgeDescription": "您的貼文廣為流傳並觸及了廣泛的受眾",
"helpfulCommentBadgeName": "有幫助的評論",
"helpfulCommentBadgeDescription": "您的評論被其他人標記為有幫助",
"newcomerBadgeName": "新成員",
"newcomerBadgeDescription": "歡迎來到 Solar Network !開始探索和聯繫",
"contributorBadgeName": "貢獻者",
"contributorBadgeDescription": "積極為 Solar Network 社群做出貢獻",
"expertBadgeName": "專家",
"expertBadgeDescription": "因您的專業知識和寶貴貢獻而受到認可",
"founderBadgeName": "創始人",
"founderBadgeDescription": " Solar Network 最早的成員之一",
"betaTesterBadgeName": "Beta 測試員",
"betaTesterBadgeDescription": "在 Beta 測試期間協助測試和改進 Solar Network ",
"moderatorBadgeName": "版主",
"moderatorBadgeDescription": "協助維護和管理社群",
"developerBadgeName": "開發者",
"developerBadgeDescription": "為 Solar Network 的開發做出貢獻",
"translatorBadgeName": "翻譯者",
"translatorBadgeDescription": "協助將 Solar Network 翻譯成不同語言",
"wallet": "錢包",
"walletCurrencyPoints": "新太陽點",
"walletCurrencyShortPoints": "NSP",
"walletCurrencyGolds": "太陽幣",
"walletCurrencyShortGolds": "TSD",
"retry": "重試",
"creatorHubUnselectedHint": "選擇/建立一個發佈者以開始使用。",
"relationships": "關係",
"addFriend": "傳送好友邀請",
"addFriendShort": "Add as Friend",
"addFriendHint": "將朋友新增到您的關係清單。",
"pendingRequest": "待處理",
"waitingRequest": "等待中",
"relationshipStatusFriend": "朋友",
"relationshipStatusBlocked": "已封鎖",
"blockUser": "封鎖用戶",
"unblockUser": "解除封鎖用戶",
"friendRequestAccepted": "已接受 {} 的好友邀請",
"friendRequestDeclined": "已拒絕 {} 的好友邀請",
"requestExpiredIn": "{} 後過期",
"friendSentRequest": "已傳送的好友邀請",
"friendSentRequestEmpty": "尚未傳送好友邀請",
"friendSentRequestHint": {
"one": "已傳送 {} 個好友邀請",
"other": "已傳送 {} 個好友邀請"
},
"levelingProgress": "等級進度",
"levelingProgressExperience": "{} 經驗值",
"levelingProgressLevel": "等級 {}",
"fileUploadingProgress": "正在上傳檔案 #{}: {}%",
"removeChatMember": "移除聊天室成員",
"removeChatMemberHint": "確定要將此成員從聊天室中移除嗎?",
"removeRealmMember": "移除領域成員",
"removeRealmMemberHint": "確定要將此成員從領域中移除嗎?",
"memberRole": "成員角色",
"memberRoleHint": "數字越大權限越高。",
"memberRoleEdit": "編輯 @{} 的角色",
"openLinkConfirm": "離開 Solar Network ",
"openLinkConfirmDescription": "您將離開 Solar Network 並在瀏覽器中開啟連結 ({})。它與 Solar Network 無關。請注意網路釣魚和詐騙。",
"brokenLink": "無法開啟連結 {}... 它可能已損壞或缺少 URI 部分...",
"copyToClipboard": "複製到剪貼簿",
"leaveChatRoom": "離開聊天室",
"leaveChatRoomHint": "確定要離開此聊天室嗎?",
"leaveRealm": "離開領域",
"leaveRealmHint": "確定要離開此領域嗎?",
"walletNotFound": "找不到錢包",
"walletCreateHint": "您尚未有錢包。建立一個以開始使用 Solar Network 電子錢包。",
"walletCreate": "建立錢包",
"settingsServerUrl": "伺服器 URL",
"settingsApplied": "設定已應用。",
"notifications": "通知",
"posts": "貼文",
"settingsBackgroundImage": "背景圖片",
"settingsBackgroundImageClear": "清除背景圖片",
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
"messageNone": "沒有內容可顯示",
"unreadMessages": {
"one": "{} 條未讀訊息",
"other": "{} 條未讀訊息"
},
"chatBreakNone": "",
"settingsRealmCompactView": "精簡領域視圖",
"settingsMixedFeed": "混合動態",
"settingsAutoTranslate": "自動翻譯",
"settingsHideBottomNav": "隱藏底部導航",
"settingsSoundEffects": "音效",
"settingsAprilFoolFeatures": "愚人節功能",
"settingsEnterToSend": "按下 Enter 傳送",
"settingsTransparentAppBar": "Transparent App Bar",
"settingsCustomFonts": "Custom Fonts",
"settingsCustomFontsHint": "Custom fonts will be used for all text in the app. Make sure it is installed on your device.",
"settingsColorScheme": "Color Scheme",
"postTitle": "Title",
"postDescription": "Description",
"call": "Call",
"done": "Done",
"loginResetPasswordSent": "Password reset link sent, please check your email inbox.",
"accountDeletion": "Delete Account",
"accountDeletionHint": "Are you sure to delete your account? If you confirmed, we will send an confirmation email to your primary email address, you can continue the deletion process by follow the insturctions in the email.",
"accountDeletionSent": "Account deletion confirmation email sent, please check your email inbox.",
"accountSecurityTitle": "Security",
"accountDangerZoneTitle": "Danger Zone",
"accountPassword": "Password",
"accountPasswordDescription": "Change your account password",
"accountPasswordChange": "Change Password",
"accountPasswordChangeSent": "Password reset link sent, please check your email inbox.",
"accountPasswordChangeDescription": "We will send an email to your primary email address to reset your password.",
"accountAuthFactor": "Auth factors",
"accountAuthFactorDescription": "Multi-factor authentication to ensure safety and convience",
"accountDeletionDescription": "Permanently delete your account and all your data",
"accountSettingsHelp": "Account Settings Help",
"accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support.",
"unauthorized": "Unauthorized",
"unauthorizedHint": "You're not signed in or session expired, please sign in again.",
"publisherBelongsTo": "Belongs to {}",
"postContent": "Content",
"postSettings": "Settings",
"postPublisherUnselected": "Publisher Unspecified",
"postVisibility": "可見性",
"postVisibilityPublic": "公開",
"postVisibilityFriends": "僅好友可見",
"postVisibilityUnlisted": "不公開",
"postVisibilityPrivate": "私密",
"postTruncated": "Content truncated, tap to view full post",
"copyMessage": "Copy Message",
"authFactor": "Authentication Factor",
"authFactorDelete": "Delete the Factor",
"authFactorDeleteHint": "Are you sure you want to delete this authentication factor? This action cannot be undone.",
"authFactorDisable": "Disable the Factor",
"authFactorDisableHint": "Are you sure you want to disable this authentication factor? You can enable it again later.",
"authFactorEnable": "Enable the Factor",
"authFactorEnableHint": "Please enter the code that generated by the factor to enable it.",
"authFactorNew": "Create Auth Factor",
"authFactorSecret": "Secret",
"authFactorSecretHint": "Create an secret for this factor.",
"authFactorQrCodeScan": "Scan this QR code with your authenticator app to set up TOTP authentication",
"authFactorNoQrCode": "No QR code available for this authentication factor",
"cancel": "Cancel",
"confirm": "Confirm",
"authFactorAdditional": "One more step",
"authFactorHint": "Contact method hint",
"authFactorHintHelper": "You need provide a part of your contact method and we will send the verification code to that contact method if it matched our records",
"authSessions": "Active Sessions",
"authSessionsDescription": "See devices you currently logged in.",
"authSessionsCount": {
"one": "{} session",
"other": "{} sessions"
},
"authDeviceCurrent": "Current device",
"lastActiveAt": "Last active at {}",
"authDeviceLogout": "Logout",
"authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.",
"authDeviceEditLabel": "Edit Label",
"authDeviceLabelTitle": "Edit Device Label",
"authDeviceLabelHint": "Enter a name for this device",
"authDeviceSwipeEditHint": "Swipe left to edit label",
"authDeviceSwipeLogoutHint": "Swipe right to logout device",
"typingHint": {
"one": "{} is typing...",
"other": "{} are typing..."
},
"settingsAppearance": "Appearance",
"settingsServer": "Server",
"settingsBehavior": "Behavior",
"settingsDesktop": "Desktop",
"settingsKeyboardShortcuts": "Keyboard Shortcuts",
"settingsEnterToSendDesktopHint": "Press Enter to send messages, use Shift+Enter for new line.",
"settingsHelp": "Settings Help",
"settingsHelpContent": "This page allows you to manage your app settings, appearance, and behavior. If you need assistance, please contact support.",
"settingsKeyboardShortcutSearch": "Search",
"settingsKeyboardShortcutSettings": "Settings",
"settingsKeyboardShortcutNewMessage": "New Message",
"settingsKeyboardShortcutCloseDialog": "Close Dialog",
"close": "Close",
"contactMethod": "Contact Method",
"contactMethodType": "Contact Type",
"contactMethodTypeEmail": "Email",
"contactMethodTypePhone": "Phone",
"contactMethodTypeAddress": "Address",
"contactMethodEmailHint": "Enter your email address",
"contactMethodPhoneHint": "Enter your phone number",
"contactMethodAddressHint": "Enter your physical address",
"contactMethodEmailDescription": "Your email will be used for account recovery and notifications",
"contactMethodPhoneDescription": "Your phone number will be used for account recovery and notifications",
"contactMethodAddressDescription": "Your physical address will be used for shipping and billing purposes.",
"contactMethodVerified": "Verified",
"contactMethodUnverified": "Unverified",
"contactMethodVerify": "Verify Contact",
"contactMethodDelete": "Delete Contact",
"contactMethodNew": "New Contact Method",
"contactMethodContentEmpty": "Contact content cannot be empty",
"contactMethodVerificationSent": "Verification code sent to your contact method",
"contactMethodVerificationNeeded": "The contact method is added, but not verified yet. You can verify it by tapping it and select verify.",
"accountContactMethod": "Contact Methods",
"accountContactMethodDescription": "Manage your contact methods for account recovery and notifications",
"authFactorVerificationNeeded": "The auth factor is added, but it is not enabled yet. You can enable it by tapping it and enter the verification code.",
"contactMethodPrimary": "Primary",
"contactMethodSetPrimary": "Set as Primary",
"contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications",
"contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.",
"chatNotifyLevel": "通知等級",
"chatNotifyLevelDescription": "決定您將收到多少通知。",
"chatNotifyLevelAll": "全部",
"chatNotifyLevelMention": "提及",
"chatNotifyLevelNone": "無",
"chatNotifyLevelUpdated": "通知等級已更新為 {}。",
"chatBreak": "暫停聊天",
"chatBreakDescription": "設定一個時間,在該時間之前,您的通知等級將僅為提及,以暫時休息當前討論的話題。",
"chatBreakClear": "清除暫停時間",
"chatBreakHour": "暫停 {} 分鐘",
"chatBreakDay": "暫停 {} 天",
"chatBreakSet": "已設定暫停 {}",
"chatBreakCleared": "聊天暫停已清除。",
"chatBreakCustom": "自訂時長",
"chatBreakEnterMinutes": "輸入分鐘數",
"firstName": "First Name",
"middleName": "Middle Name",
"lastName": "Last Name",
"gender": "Gender",
"pronouns": "Pronouns",
"location": "Location",
"timeZone": "Time Zone",
"birthday": "Birthday",
"selectADate": "Select a date",
"checkInResultT0": "Worst",
"checkInResultT1": "Poor",
"checkInResultT2": "Mid",
"checkInResultT3": "Good",
"checkInResultT4": "Best",
"accountProfileView": "View Profile",
"unspecified": "Unspecified",
"added": "Added",
"preview": "Preview",
"togglePreview": "Toggle Preview",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"paymentVerification": "Payment Verification",
"paymentSummary": "Payment Summary",
"amount": "Amount",
"description": "描述",
"pinCode": "PIN Code",
"biometric": "Biometric",
"enterPinToConfirm": "Enter your 6-digit PIN to confirm payment",
"clearPin": "Clear PIN",
"useBiometricToConfirm": "Use biometric authentication to confirm payment",
"touchSensorToAuthenticate": "Touch the sensor to authenticate",
"authenticating": "Authenticating...",
"authenticateNow": "Authenticate Now",
"processing": "Processing...",
"processingPayment": "Processing Payment...",
"pleaseWait": "Please wait",
"paymentFailed": "Payment failed. Please try again.",
"invalidPin": "Invalid PIN. Please try again.",
"biometricAuthFailed": "Biometric authentication failed. Please try again.",
"paymentSuccess": "Payment completed successfully!",
"membershipPurchaseSuccess": "Membership purchased successfully!",
"paymentError": "付款失敗:{error}",
"usePinInstead": "使用密碼",
"levelProgress": "等級進度",
"unlockedFeatures": "已解鎖功能",
"unlockedFeaturesDescription": "您目前等級解鎖的功能將會顯示在此。",
"stellarMembership": "星際會員",
"upgradeYourPlan": "升級您的方案",
"chooseYourPlan": "選擇您的方案",
"currentMembership": "目前:{}",
"membershipExpires": "到期:{}",
"membershipTierStellar": "星際",
"membershipTierNova": "新星",
"membershipTierSupernova": "超新星",
"membershipTierUnknown": "未知",
"membershipPriceStellar": "每月 10 星幣",
"membershipPriceNova": "每月 20 星幣",
"membershipPriceSupernova": "每月 30 星幣",
"membershipFeatureBasic": "基本功能",
"membershipFeaturePrioritySupport": "優先支援",
"membershipFeatureAdFree": "無廣告體驗",
"membershipFeatureAllPrimary": "所有主要功能",
"membershipFeatureAdvancedCustomization": "進階自訂",
"membershipFeatureEarlyAccess": "搶先體驗",
"membershipFeatureAllNova": "所有新星功能",
"membershipFeatureExclusiveContent": "獨家內容",
"membershipFeatureVipSupport": "VIP 支援",
"membershipCurrentBadge": "目前",
"restorePurchase": "Restore Purchase",
"restorePurchaseDescription": "Enter your payment provider and order ID to restore your purchase.",
"provider": "Provider",
"selectProvider": "Select a provider",
"orderId": "Order ID",
"enterOrderId": "Enter your order ID",
"restore": "Restore",
"keyboardShortcuts": "Keyboard Shortcuts"
}

View File

@ -40,31 +40,31 @@ PODS:
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/CoreOnly (11.13.0):
- FirebaseCore (~> 11.13.0)
- Firebase/Messaging (11.13.0):
- Firebase/CoreOnly (11.15.0):
- FirebaseCore (~> 11.15.0)
- Firebase/Messaging (11.15.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.13.0)
- firebase_core (3.14.0):
- Firebase/CoreOnly (= 11.13.0)
- FirebaseMessaging (~> 11.15.0)
- firebase_core (3.15.0):
- Firebase/CoreOnly (= 11.15.0)
- Flutter
- firebase_messaging (15.2.7):
- Firebase/Messaging (= 11.13.0)
- firebase_messaging (15.2.8):
- Firebase/Messaging (= 11.15.0)
- firebase_core
- Flutter
- FirebaseCore (11.13.0):
- FirebaseCoreInternal (~> 11.13.0)
- FirebaseCore (11.15.0):
- FirebaseCoreInternal (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (11.13.0):
- FirebaseCoreInternal (11.15.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (11.13.0):
- FirebaseCore (~> 11.13.0)
- FirebaseInstallations (11.15.0):
- FirebaseCore (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.13.0):
- FirebaseCore (~> 11.13.0)
- FirebaseMessaging (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
@ -80,6 +80,8 @@ PODS:
- flutter_inappwebview_ios/Core (0.0.1):
- Flutter
- OrderedSet (~> 6.0.3)
- flutter_keyboard_visibility (0.0.1):
- Flutter
- flutter_native_splash (2.4.3):
- Flutter
- flutter_platform_alert (0.0.1):
@ -128,8 +130,8 @@ PODS:
- Flutter
- irondash_engine_context (0.0.1):
- Flutter
- Kingfisher (8.3.2)
- livekit_client (2.4.8):
- Kingfisher (8.3.3)
- livekit_client (2.4.9):
- Flutter
- flutter_webrtc
- WebRTC-SDK (= 125.6422.07)
@ -155,6 +157,8 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- pointer_interceptor_ios (0.0.1):
- Flutter
- PromisesObjC (2.4.0)
- receive_sharing_intent (1.8.1):
- Flutter
@ -217,6 +221,7 @@ DEPENDENCIES:
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
@ -235,6 +240,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- record_ios (from `.symlinks/plugins/record_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
@ -286,6 +292,8 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_keyboard_visibility:
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_platform_alert:
@ -320,6 +328,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/pasteboard/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
pointer_interceptor_ios:
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios"
record_ios:
@ -351,15 +361,16 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: 3435bc66b4d494c2f22c79fd3aae4c1db6662327
firebase_core: 700bac7ed92bb754fd70fbf01d72b36ecdd6d450
firebase_messaging: 860c017fcfbb5e27c163062d1d3135388f3ef954
FirebaseCore: c692c7f1c75305ab6aff2b367f25e11d73aa8bd0
FirebaseCoreInternal: 29d7b3af4aaf0b8f3ed20b568c13df399b06f68c
FirebaseInstallations: 0ee9074f2c1e86561ace168ee1470dc67aabaf02
FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_core: c727a02c560a53f1f1e56e18f16515eb5753c492
firebase_messaging: 4158969b04b667f5435731ec9d6e453bb58b0c4c
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
@ -371,8 +382,8 @@ SPEC CHECKSUMS:
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
Kingfisher: 0621d0ac0c78fecb19f6dc5303bde2b52abaf2f5
livekit_client: 9e901890552514206e5ff828903ed271531da264
Kingfisher: ff82cb91d9266ddb56cbb2f72d32c26f00d3e5be
livekit_client: 3f79d79233a5bd13d5b541732624ef959d7c538e
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
@ -382,6 +393,7 @@ SPEC CHECKSUMS:
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b

View File

@ -772,6 +772,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -1202,6 +1203,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -1229,6 +1231,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@ -2,16 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>BUNDLE_ID</key>
<string>dev.solsynth.solian</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
@ -32,31 +26,46 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>CLIENT_ID</key>
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCalendarsUsageDescription</key>
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
<key>NSCameraUsageDescription</key>
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
<key>NSFaceIDUsageDescription</key>
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
<key>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
<string>INSendMessageIntent</string>
</array>
<key>PLIST_VERSION</key>
<string>1</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
@ -75,24 +84,13 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSFaceIDUsageDescription</key>
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
<string>INSendMessageIntent</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
</dict>
</plist>

View File

@ -7,7 +7,9 @@ import 'package:firebase_core/firebase_core.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';
import 'package:image_picker_android/image_picker_android.dart';
import 'package:island/firebase_options.dart';
@ -44,6 +46,10 @@ void main() async {
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
}
if (kIsWeb) {
GoRouter.optionURLReflectsImperativeAPIs = true;
}
try {
await EasyLocalization.ensureInitialized();
await Firebase.initializeApp(
@ -158,6 +164,28 @@ class IslandApp extends HookConsumerWidget {
}
useEffect(() {
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) {
if (message != null) {
@ -193,7 +221,7 @@ class IslandApp extends HookConsumerWidget {
Future(() {
userNotifier.fetchUser().then((_) {
final user = ref.watch(userInfoProvider);
if (user.hasValue) {
if (user.value != null) {
final apiClient = ref.read(apiClientProvider);
subscribePushNotification(apiClient);
final wsNotifier = ref.read(websocketStateProvider.notifier);

View File

@ -0,0 +1,23 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'abuse_report.freezed.dart';
part 'abuse_report.g.dart';
@freezed
sealed class SnAbuseReport with _$SnAbuseReport {
const factory SnAbuseReport({
required String id,
required String resourceIdentifier,
required int type,
required String reason,
required DateTime? resolvedAt,
required String? resolution,
required String accountId,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnAbuseReport;
factory SnAbuseReport.fromJson(Map<String, dynamic> json) =>
_$SnAbuseReportFromJson(json);
}

View File

@ -0,0 +1,175 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'abuse_report.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnAbuseReport {
String get id; String get resourceIdentifier; int get type; String get reason; DateTime? get resolvedAt; String? get resolution; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAbuseReportCopyWith<SnAbuseReport> get copyWith => _$SnAbuseReportCopyWithImpl<SnAbuseReport>(this as SnAbuseReport, _$identity);
/// Serializes this SnAbuseReport to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAbuseReport&&(identical(other.id, id) || other.id == id)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.type, type) || other.type == type)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.resolvedAt, resolvedAt) || other.resolvedAt == resolvedAt)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,resourceIdentifier,type,reason,resolvedAt,resolution,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAbuseReport(id: $id, resourceIdentifier: $resourceIdentifier, type: $type, reason: $reason, resolvedAt: $resolvedAt, resolution: $resolution, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnAbuseReportCopyWith<$Res> {
factory $SnAbuseReportCopyWith(SnAbuseReport value, $Res Function(SnAbuseReport) _then) = _$SnAbuseReportCopyWithImpl;
@useResult
$Res call({
String id, String resourceIdentifier, int type, String reason, DateTime? resolvedAt, String? resolution, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnAbuseReportCopyWithImpl<$Res>
implements $SnAbuseReportCopyWith<$Res> {
_$SnAbuseReportCopyWithImpl(this._self, this._then);
final SnAbuseReport _self;
final $Res Function(SnAbuseReport) _then;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? resourceIdentifier = null,Object? type = null,Object? reason = null,Object? resolvedAt = freezed,Object? resolution = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,resolvedAt: freezed == resolvedAt ? _self.resolvedAt : resolvedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,resolution: freezed == resolution ? _self.resolution : resolution // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnAbuseReport implements SnAbuseReport {
const _SnAbuseReport({required this.id, required this.resourceIdentifier, required this.type, required this.reason, required this.resolvedAt, required this.resolution, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnAbuseReport.fromJson(Map<String, dynamic> json) => _$SnAbuseReportFromJson(json);
@override final String id;
@override final String resourceIdentifier;
@override final int type;
@override final String reason;
@override final DateTime? resolvedAt;
@override final String? resolution;
@override final String accountId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAbuseReportCopyWith<_SnAbuseReport> get copyWith => __$SnAbuseReportCopyWithImpl<_SnAbuseReport>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAbuseReportToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAbuseReport&&(identical(other.id, id) || other.id == id)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.type, type) || other.type == type)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.resolvedAt, resolvedAt) || other.resolvedAt == resolvedAt)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,resourceIdentifier,type,reason,resolvedAt,resolution,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAbuseReport(id: $id, resourceIdentifier: $resourceIdentifier, type: $type, reason: $reason, resolvedAt: $resolvedAt, resolution: $resolution, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnAbuseReportCopyWith<$Res> implements $SnAbuseReportCopyWith<$Res> {
factory _$SnAbuseReportCopyWith(_SnAbuseReport value, $Res Function(_SnAbuseReport) _then) = __$SnAbuseReportCopyWithImpl;
@override @useResult
$Res call({
String id, String resourceIdentifier, int type, String reason, DateTime? resolvedAt, String? resolution, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnAbuseReportCopyWithImpl<$Res>
implements _$SnAbuseReportCopyWith<$Res> {
__$SnAbuseReportCopyWithImpl(this._self, this._then);
final _SnAbuseReport _self;
final $Res Function(_SnAbuseReport) _then;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? resourceIdentifier = null,Object? type = null,Object? reason = null,Object? resolvedAt = freezed,Object? resolution = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAbuseReport(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,resolvedAt: freezed == resolvedAt ? _self.resolvedAt : resolvedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,resolution: freezed == resolution ? _self.resolution : resolution // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
// dart format on

View File

@ -0,0 +1,41 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'abuse_report.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnAbuseReport _$SnAbuseReportFromJson(Map<String, dynamic> json) =>
_SnAbuseReport(
id: json['id'] as String,
resourceIdentifier: json['resource_identifier'] as String,
type: (json['type'] as num).toInt(),
reason: json['reason'] as String,
resolvedAt:
json['resolved_at'] == null
? null
: DateTime.parse(json['resolved_at'] as String),
resolution: json['resolution'] as String?,
accountId: json['account_id'] 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> _$SnAbuseReportToJson(_SnAbuseReport instance) =>
<String, dynamic>{
'id': instance.id,
'resource_identifier': instance.resourceIdentifier,
'type': instance.type,
'reason': instance.reason,
'resolved_at': instance.resolvedAt?.toIso8601String(),
'resolution': instance.resolution,
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@ -0,0 +1,38 @@
enum AbuseReportType {
copyright(0),
harassment(1),
impersonation(2),
offensiveContent(3),
spam(4),
privacyViolation(5),
illegalContent(6),
other(7);
const AbuseReportType(this.value);
final int value;
static AbuseReportType fromValue(int value) {
return values.firstWhere((e) => e.value == value);
}
String get displayName {
switch (this) {
case AbuseReportType.copyright:
return 'Copyright';
case AbuseReportType.harassment:
return 'Harassment';
case AbuseReportType.impersonation:
return 'Impersonation';
case AbuseReportType.offensiveContent:
return 'Offensive Content';
case AbuseReportType.spam:
return 'Spam';
case AbuseReportType.privacyViolation:
return 'Privacy Violation';
case AbuseReportType.illegalContent:
return 'Illegal Content';
case AbuseReportType.other:
return 'Other';
}
}
}

View File

@ -0,0 +1,34 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'auto_completion.freezed.dart';
part 'auto_completion.g.dart';
@freezed
sealed class AutoCompletionResponse with _$AutoCompletionResponse {
const factory AutoCompletionResponse.account({
required String type,
required List<AutoCompletionItem> items,
}) = AutoCompletionAccountResponse;
const factory AutoCompletionResponse.sticker({
required String type,
required List<AutoCompletionItem> items,
}) = AutoCompletionStickerResponse;
factory AutoCompletionResponse.fromJson(Map<String, dynamic> json) =>
_$AutoCompletionResponseFromJson(json);
}
@freezed
sealed class AutoCompletionItem with _$AutoCompletionItem {
const factory AutoCompletionItem({
required String id,
required String displayName,
required String? secondaryText,
required String type,
required dynamic data,
}) = _AutoCompletionItem;
factory AutoCompletionItem.fromJson(Map<String, dynamic> json) =>
_$AutoCompletionItemFromJson(json);
}

View File

@ -0,0 +1,410 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'auto_completion.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
AutoCompletionResponse _$AutoCompletionResponseFromJson(
Map<String, dynamic> json
) {
switch (json['runtimeType']) {
case 'account':
return AutoCompletionAccountResponse.fromJson(
json
);
case 'sticker':
return AutoCompletionStickerResponse.fromJson(
json
);
default:
throw CheckedFromJsonException(
json,
'runtimeType',
'AutoCompletionResponse',
'Invalid union type "${json['runtimeType']}"!'
);
}
}
/// @nodoc
mixin _$AutoCompletionResponse {
String get type; List<AutoCompletionItem> get items;
/// Create a copy of AutoCompletionResponse
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AutoCompletionResponseCopyWith<AutoCompletionResponse> get copyWith => _$AutoCompletionResponseCopyWithImpl<AutoCompletionResponse>(this as AutoCompletionResponse, _$identity);
/// Serializes this AutoCompletionResponse to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionResponse&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.items, items));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(items));
@override
String toString() {
return 'AutoCompletionResponse(type: $type, items: $items)';
}
}
/// @nodoc
abstract mixin class $AutoCompletionResponseCopyWith<$Res> {
factory $AutoCompletionResponseCopyWith(AutoCompletionResponse value, $Res Function(AutoCompletionResponse) _then) = _$AutoCompletionResponseCopyWithImpl;
@useResult
$Res call({
String type, List<AutoCompletionItem> items
});
}
/// @nodoc
class _$AutoCompletionResponseCopyWithImpl<$Res>
implements $AutoCompletionResponseCopyWith<$Res> {
_$AutoCompletionResponseCopyWithImpl(this._self, this._then);
final AutoCompletionResponse _self;
final $Res Function(AutoCompletionResponse) _then;
/// Create a copy of AutoCompletionResponse
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? items = null,}) {
return _then(_self.copyWith(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,items: null == items ? _self.items : items // ignore: cast_nullable_to_non_nullable
as List<AutoCompletionItem>,
));
}
}
/// @nodoc
@JsonSerializable()
class AutoCompletionAccountResponse implements AutoCompletionResponse {
const AutoCompletionAccountResponse({required this.type, required final List<AutoCompletionItem> items, final String? $type}): _items = items,$type = $type ?? 'account';
factory AutoCompletionAccountResponse.fromJson(Map<String, dynamic> json) => _$AutoCompletionAccountResponseFromJson(json);
@override final String type;
final List<AutoCompletionItem> _items;
@override List<AutoCompletionItem> get items {
if (_items is EqualUnmodifiableListView) return _items;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_items);
}
@JsonKey(name: 'runtimeType')
final String $type;
/// Create a copy of AutoCompletionResponse
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AutoCompletionAccountResponseCopyWith<AutoCompletionAccountResponse> get copyWith => _$AutoCompletionAccountResponseCopyWithImpl<AutoCompletionAccountResponse>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$AutoCompletionAccountResponseToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionAccountResponse&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._items, _items));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(_items));
@override
String toString() {
return 'AutoCompletionResponse.account(type: $type, items: $items)';
}
}
/// @nodoc
abstract mixin class $AutoCompletionAccountResponseCopyWith<$Res> implements $AutoCompletionResponseCopyWith<$Res> {
factory $AutoCompletionAccountResponseCopyWith(AutoCompletionAccountResponse value, $Res Function(AutoCompletionAccountResponse) _then) = _$AutoCompletionAccountResponseCopyWithImpl;
@override @useResult
$Res call({
String type, List<AutoCompletionItem> items
});
}
/// @nodoc
class _$AutoCompletionAccountResponseCopyWithImpl<$Res>
implements $AutoCompletionAccountResponseCopyWith<$Res> {
_$AutoCompletionAccountResponseCopyWithImpl(this._self, this._then);
final AutoCompletionAccountResponse _self;
final $Res Function(AutoCompletionAccountResponse) _then;
/// Create a copy of AutoCompletionResponse
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? items = null,}) {
return _then(AutoCompletionAccountResponse(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable
as List<AutoCompletionItem>,
));
}
}
/// @nodoc
@JsonSerializable()
class AutoCompletionStickerResponse implements AutoCompletionResponse {
const AutoCompletionStickerResponse({required this.type, required final List<AutoCompletionItem> items, final String? $type}): _items = items,$type = $type ?? 'sticker';
factory AutoCompletionStickerResponse.fromJson(Map<String, dynamic> json) => _$AutoCompletionStickerResponseFromJson(json);
@override final String type;
final List<AutoCompletionItem> _items;
@override List<AutoCompletionItem> get items {
if (_items is EqualUnmodifiableListView) return _items;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_items);
}
@JsonKey(name: 'runtimeType')
final String $type;
/// Create a copy of AutoCompletionResponse
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AutoCompletionStickerResponseCopyWith<AutoCompletionStickerResponse> get copyWith => _$AutoCompletionStickerResponseCopyWithImpl<AutoCompletionStickerResponse>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$AutoCompletionStickerResponseToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionStickerResponse&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._items, _items));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(_items));
@override
String toString() {
return 'AutoCompletionResponse.sticker(type: $type, items: $items)';
}
}
/// @nodoc
abstract mixin class $AutoCompletionStickerResponseCopyWith<$Res> implements $AutoCompletionResponseCopyWith<$Res> {
factory $AutoCompletionStickerResponseCopyWith(AutoCompletionStickerResponse value, $Res Function(AutoCompletionStickerResponse) _then) = _$AutoCompletionStickerResponseCopyWithImpl;
@override @useResult
$Res call({
String type, List<AutoCompletionItem> items
});
}
/// @nodoc
class _$AutoCompletionStickerResponseCopyWithImpl<$Res>
implements $AutoCompletionStickerResponseCopyWith<$Res> {
_$AutoCompletionStickerResponseCopyWithImpl(this._self, this._then);
final AutoCompletionStickerResponse _self;
final $Res Function(AutoCompletionStickerResponse) _then;
/// Create a copy of AutoCompletionResponse
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? items = null,}) {
return _then(AutoCompletionStickerResponse(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable
as List<AutoCompletionItem>,
));
}
}
/// @nodoc
mixin _$AutoCompletionItem {
String get id; String get displayName; String? get secondaryText; String get type; dynamic get data;
/// Create a copy of AutoCompletionItem
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AutoCompletionItemCopyWith<AutoCompletionItem> get copyWith => _$AutoCompletionItemCopyWithImpl<AutoCompletionItem>(this as AutoCompletionItem, _$identity);
/// Serializes this AutoCompletionItem to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionItem&&(identical(other.id, id) || other.id == id)&&(identical(other.displayName, displayName) || other.displayName == displayName)&&(identical(other.secondaryText, secondaryText) || other.secondaryText == secondaryText)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.data, data));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,displayName,secondaryText,type,const DeepCollectionEquality().hash(data));
@override
String toString() {
return 'AutoCompletionItem(id: $id, displayName: $displayName, secondaryText: $secondaryText, type: $type, data: $data)';
}
}
/// @nodoc
abstract mixin class $AutoCompletionItemCopyWith<$Res> {
factory $AutoCompletionItemCopyWith(AutoCompletionItem value, $Res Function(AutoCompletionItem) _then) = _$AutoCompletionItemCopyWithImpl;
@useResult
$Res call({
String id, String displayName, String? secondaryText, String type, dynamic data
});
}
/// @nodoc
class _$AutoCompletionItemCopyWithImpl<$Res>
implements $AutoCompletionItemCopyWith<$Res> {
_$AutoCompletionItemCopyWithImpl(this._self, this._then);
final AutoCompletionItem _self;
final $Res Function(AutoCompletionItem) _then;
/// Create a copy of AutoCompletionItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? displayName = null,Object? secondaryText = freezed,Object? type = null,Object? data = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable
as String,secondaryText: freezed == secondaryText ? _self.secondaryText : secondaryText // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as dynamic,
));
}
}
/// @nodoc
@JsonSerializable()
class _AutoCompletionItem implements AutoCompletionItem {
const _AutoCompletionItem({required this.id, required this.displayName, required this.secondaryText, required this.type, required this.data});
factory _AutoCompletionItem.fromJson(Map<String, dynamic> json) => _$AutoCompletionItemFromJson(json);
@override final String id;
@override final String displayName;
@override final String? secondaryText;
@override final String type;
@override final dynamic data;
/// Create a copy of AutoCompletionItem
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$AutoCompletionItemCopyWith<_AutoCompletionItem> get copyWith => __$AutoCompletionItemCopyWithImpl<_AutoCompletionItem>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$AutoCompletionItemToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AutoCompletionItem&&(identical(other.id, id) || other.id == id)&&(identical(other.displayName, displayName) || other.displayName == displayName)&&(identical(other.secondaryText, secondaryText) || other.secondaryText == secondaryText)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.data, data));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,displayName,secondaryText,type,const DeepCollectionEquality().hash(data));
@override
String toString() {
return 'AutoCompletionItem(id: $id, displayName: $displayName, secondaryText: $secondaryText, type: $type, data: $data)';
}
}
/// @nodoc
abstract mixin class _$AutoCompletionItemCopyWith<$Res> implements $AutoCompletionItemCopyWith<$Res> {
factory _$AutoCompletionItemCopyWith(_AutoCompletionItem value, $Res Function(_AutoCompletionItem) _then) = __$AutoCompletionItemCopyWithImpl;
@override @useResult
$Res call({
String id, String displayName, String? secondaryText, String type, dynamic data
});
}
/// @nodoc
class __$AutoCompletionItemCopyWithImpl<$Res>
implements _$AutoCompletionItemCopyWith<$Res> {
__$AutoCompletionItemCopyWithImpl(this._self, this._then);
final _AutoCompletionItem _self;
final $Res Function(_AutoCompletionItem) _then;
/// Create a copy of AutoCompletionItem
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? displayName = null,Object? secondaryText = freezed,Object? type = null,Object? data = freezed,}) {
return _then(_AutoCompletionItem(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable
as String,secondaryText: freezed == secondaryText ? _self.secondaryText : secondaryText // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as dynamic,
));
}
}
// dart format on

View File

@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'auto_completion.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AutoCompletionAccountResponse _$AutoCompletionAccountResponseFromJson(
Map<String, dynamic> json,
) => AutoCompletionAccountResponse(
type: json['type'] as String,
items:
(json['items'] as List<dynamic>)
.map((e) => AutoCompletionItem.fromJson(e as Map<String, dynamic>))
.toList(),
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$AutoCompletionAccountResponseToJson(
AutoCompletionAccountResponse instance,
) => <String, dynamic>{
'type': instance.type,
'items': instance.items.map((e) => e.toJson()).toList(),
'runtimeType': instance.$type,
};
AutoCompletionStickerResponse _$AutoCompletionStickerResponseFromJson(
Map<String, dynamic> json,
) => AutoCompletionStickerResponse(
type: json['type'] as String,
items:
(json['items'] as List<dynamic>)
.map((e) => AutoCompletionItem.fromJson(e as Map<String, dynamic>))
.toList(),
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$AutoCompletionStickerResponseToJson(
AutoCompletionStickerResponse instance,
) => <String, dynamic>{
'type': instance.type,
'items': instance.items.map((e) => e.toJson()).toList(),
'runtimeType': instance.$type,
};
_AutoCompletionItem _$AutoCompletionItemFromJson(Map<String, dynamic> json) =>
_AutoCompletionItem(
id: json['id'] as String,
displayName: json['display_name'] as String,
secondaryText: json['secondary_text'] as String?,
type: json['type'] as String,
data: json['data'],
);
Map<String, dynamic> _$AutoCompletionItemToJson(_AutoCompletionItem instance) =>
<String, dynamic>{
'id': instance.id,
'display_name': instance.displayName,
'secondary_text': instance.secondaryText,
'type': instance.type,
'data': instance.data,
};

View File

@ -0,0 +1,71 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
import 'package:island/models/user.dart';
part 'custom_app.freezed.dart';
part 'custom_app.g.dart';
@freezed
sealed class CustomApp with _$CustomApp {
const factory CustomApp({
@Default('') String id,
@Default('') String slug,
@Default('') String name,
String? description,
@Default(0) int status,
SnCloudFile? picture,
SnCloudFile? background,
SnVerificationMark? verification,
CustomAppOauthConfig? oauthConfig,
CustomAppLinks? links,
@Default([]) List<CustomAppSecret> secrets,
@Default('') String publisherId,
}) = _CustomApp;
factory CustomApp.fromJson(Map<String, dynamic> json) =>
_$CustomAppFromJson(json);
}
@freezed
sealed class CustomAppLinks with _$CustomAppLinks {
const factory CustomAppLinks({
String? homePage,
String? privacyPolicy,
String? termsOfService,
}) = _CustomAppLinks;
factory CustomAppLinks.fromJson(Map<String, dynamic> json) =>
_$CustomAppLinksFromJson(json);
}
@freezed
sealed class CustomAppOauthConfig with _$CustomAppOauthConfig {
const factory CustomAppOauthConfig({
String? clientUri,
@Default([]) List<String> redirectUris,
List<String>? postLogoutRedirectUris,
@Default(['openid', 'profile', 'email']) List<String> allowedScopes,
@Default(['authorization_code', 'refresh_token'])
List<String> allowedGrantTypes,
@Default(true) bool requirePkce,
@Default(false) bool allowOfflineAccess,
}) = _CustomAppOauthConfig;
factory CustomAppOauthConfig.fromJson(Map<String, dynamic> json) =>
_$CustomAppOauthConfigFromJson(json);
}
@freezed
sealed class CustomAppSecret with _$CustomAppSecret {
const factory CustomAppSecret({
@Default('') String id,
@Default('') String secret,
String? description,
DateTime? expiredAt,
@Default(false) bool isOidc,
@Default('') String appId,
}) = _CustomAppSecret;
factory CustomAppSecret.fromJson(Map<String, dynamic> json) =>
_$CustomAppSecretFromJson(json);
}

View File

@ -0,0 +1,771 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'custom_app.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$CustomApp {
String get id; String get slug; String get name; String? get description; int get status; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; CustomAppOauthConfig? get oauthConfig; CustomAppLinks? get links; List<CustomAppSecret> get secrets; String get publisherId;
/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$CustomAppCopyWith<CustomApp> get copyWith => _$CustomAppCopyWithImpl<CustomApp>(this as CustomApp, _$identity);
/// Serializes this CustomApp to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomApp&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.status, status) || other.status == status)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.oauthConfig, oauthConfig) || other.oauthConfig == oauthConfig)&&(identical(other.links, links) || other.links == links)&&const DeepCollectionEquality().equals(other.secrets, secrets)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,description,status,picture,background,verification,oauthConfig,links,const DeepCollectionEquality().hash(secrets),publisherId);
@override
String toString() {
return 'CustomApp(id: $id, slug: $slug, name: $name, description: $description, status: $status, picture: $picture, background: $background, verification: $verification, oauthConfig: $oauthConfig, links: $links, secrets: $secrets, publisherId: $publisherId)';
}
}
/// @nodoc
abstract mixin class $CustomAppCopyWith<$Res> {
factory $CustomAppCopyWith(CustomApp value, $Res Function(CustomApp) _then) = _$CustomAppCopyWithImpl;
@useResult
$Res call({
String id, String slug, String name, String? description, int status, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, CustomAppOauthConfig? oauthConfig, CustomAppLinks? links, List<CustomAppSecret> secrets, String publisherId
});
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification;$CustomAppOauthConfigCopyWith<$Res>? get oauthConfig;$CustomAppLinksCopyWith<$Res>? get links;
}
/// @nodoc
class _$CustomAppCopyWithImpl<$Res>
implements $CustomAppCopyWith<$Res> {
_$CustomAppCopyWithImpl(this._self, this._then);
final CustomApp _self;
final $Res Function(CustomApp) _then;
/// Create a copy of CustomApp
/// 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 = null,Object? description = freezed,Object? status = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? oauthConfig = freezed,Object? links = freezed,Object? secrets = null,Object? publisherId = 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: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as int,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
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,oauthConfig: freezed == oauthConfig ? _self.oauthConfig : oauthConfig // ignore: cast_nullable_to_non_nullable
as CustomAppOauthConfig?,links: freezed == links ? _self.links : links // ignore: cast_nullable_to_non_nullable
as CustomAppLinks?,secrets: null == secrets ? _self.secrets : secrets // ignore: cast_nullable_to_non_nullable
as List<CustomAppSecret>,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,
));
}
/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$CustomAppOauthConfigCopyWith<$Res>? get oauthConfig {
if (_self.oauthConfig == null) {
return null;
}
return $CustomAppOauthConfigCopyWith<$Res>(_self.oauthConfig!, (value) {
return _then(_self.copyWith(oauthConfig: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$CustomAppLinksCopyWith<$Res>? get links {
if (_self.links == null) {
return null;
}
return $CustomAppLinksCopyWith<$Res>(_self.links!, (value) {
return _then(_self.copyWith(links: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _CustomApp implements CustomApp {
const _CustomApp({this.id = '', this.slug = '', this.name = '', this.description, this.status = 0, this.picture, this.background, this.verification, this.oauthConfig, this.links, final List<CustomAppSecret> secrets = const [], this.publisherId = ''}): _secrets = secrets;
factory _CustomApp.fromJson(Map<String, dynamic> json) => _$CustomAppFromJson(json);
@override@JsonKey() final String id;
@override@JsonKey() final String slug;
@override@JsonKey() final String name;
@override final String? description;
@override@JsonKey() final int status;
@override final SnCloudFile? picture;
@override final SnCloudFile? background;
@override final SnVerificationMark? verification;
@override final CustomAppOauthConfig? oauthConfig;
@override final CustomAppLinks? links;
final List<CustomAppSecret> _secrets;
@override@JsonKey() List<CustomAppSecret> get secrets {
if (_secrets is EqualUnmodifiableListView) return _secrets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_secrets);
}
@override@JsonKey() final String publisherId;
/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$CustomAppCopyWith<_CustomApp> get copyWith => __$CustomAppCopyWithImpl<_CustomApp>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$CustomAppToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomApp&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.status, status) || other.status == status)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.oauthConfig, oauthConfig) || other.oauthConfig == oauthConfig)&&(identical(other.links, links) || other.links == links)&&const DeepCollectionEquality().equals(other._secrets, _secrets)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,description,status,picture,background,verification,oauthConfig,links,const DeepCollectionEquality().hash(_secrets),publisherId);
@override
String toString() {
return 'CustomApp(id: $id, slug: $slug, name: $name, description: $description, status: $status, picture: $picture, background: $background, verification: $verification, oauthConfig: $oauthConfig, links: $links, secrets: $secrets, publisherId: $publisherId)';
}
}
/// @nodoc
abstract mixin class _$CustomAppCopyWith<$Res> implements $CustomAppCopyWith<$Res> {
factory _$CustomAppCopyWith(_CustomApp value, $Res Function(_CustomApp) _then) = __$CustomAppCopyWithImpl;
@override @useResult
$Res call({
String id, String slug, String name, String? description, int status, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, CustomAppOauthConfig? oauthConfig, CustomAppLinks? links, List<CustomAppSecret> secrets, String publisherId
});
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification;@override $CustomAppOauthConfigCopyWith<$Res>? get oauthConfig;@override $CustomAppLinksCopyWith<$Res>? get links;
}
/// @nodoc
class __$CustomAppCopyWithImpl<$Res>
implements _$CustomAppCopyWith<$Res> {
__$CustomAppCopyWithImpl(this._self, this._then);
final _CustomApp _self;
final $Res Function(_CustomApp) _then;
/// Create a copy of CustomApp
/// 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 = null,Object? description = freezed,Object? status = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? oauthConfig = freezed,Object? links = freezed,Object? secrets = null,Object? publisherId = null,}) {
return _then(_CustomApp(
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: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as int,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
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,oauthConfig: freezed == oauthConfig ? _self.oauthConfig : oauthConfig // ignore: cast_nullable_to_non_nullable
as CustomAppOauthConfig?,links: freezed == links ? _self.links : links // ignore: cast_nullable_to_non_nullable
as CustomAppLinks?,secrets: null == secrets ? _self._secrets : secrets // ignore: cast_nullable_to_non_nullable
as List<CustomAppSecret>,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,
));
}
/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$CustomAppOauthConfigCopyWith<$Res>? get oauthConfig {
if (_self.oauthConfig == null) {
return null;
}
return $CustomAppOauthConfigCopyWith<$Res>(_self.oauthConfig!, (value) {
return _then(_self.copyWith(oauthConfig: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$CustomAppLinksCopyWith<$Res>? get links {
if (_self.links == null) {
return null;
}
return $CustomAppLinksCopyWith<$Res>(_self.links!, (value) {
return _then(_self.copyWith(links: value));
});
}
}
/// @nodoc
mixin _$CustomAppLinks {
String? get homePage; String? get privacyPolicy; String? get termsOfService;
/// Create a copy of CustomAppLinks
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$CustomAppLinksCopyWith<CustomAppLinks> get copyWith => _$CustomAppLinksCopyWithImpl<CustomAppLinks>(this as CustomAppLinks, _$identity);
/// Serializes this CustomAppLinks to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppLinks&&(identical(other.homePage, homePage) || other.homePage == homePage)&&(identical(other.privacyPolicy, privacyPolicy) || other.privacyPolicy == privacyPolicy)&&(identical(other.termsOfService, termsOfService) || other.termsOfService == termsOfService));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,homePage,privacyPolicy,termsOfService);
@override
String toString() {
return 'CustomAppLinks(homePage: $homePage, privacyPolicy: $privacyPolicy, termsOfService: $termsOfService)';
}
}
/// @nodoc
abstract mixin class $CustomAppLinksCopyWith<$Res> {
factory $CustomAppLinksCopyWith(CustomAppLinks value, $Res Function(CustomAppLinks) _then) = _$CustomAppLinksCopyWithImpl;
@useResult
$Res call({
String? homePage, String? privacyPolicy, String? termsOfService
});
}
/// @nodoc
class _$CustomAppLinksCopyWithImpl<$Res>
implements $CustomAppLinksCopyWith<$Res> {
_$CustomAppLinksCopyWithImpl(this._self, this._then);
final CustomAppLinks _self;
final $Res Function(CustomAppLinks) _then;
/// Create a copy of CustomAppLinks
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? homePage = freezed,Object? privacyPolicy = freezed,Object? termsOfService = freezed,}) {
return _then(_self.copyWith(
homePage: freezed == homePage ? _self.homePage : homePage // ignore: cast_nullable_to_non_nullable
as String?,privacyPolicy: freezed == privacyPolicy ? _self.privacyPolicy : privacyPolicy // ignore: cast_nullable_to_non_nullable
as String?,termsOfService: freezed == termsOfService ? _self.termsOfService : termsOfService // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _CustomAppLinks implements CustomAppLinks {
const _CustomAppLinks({this.homePage, this.privacyPolicy, this.termsOfService});
factory _CustomAppLinks.fromJson(Map<String, dynamic> json) => _$CustomAppLinksFromJson(json);
@override final String? homePage;
@override final String? privacyPolicy;
@override final String? termsOfService;
/// Create a copy of CustomAppLinks
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$CustomAppLinksCopyWith<_CustomAppLinks> get copyWith => __$CustomAppLinksCopyWithImpl<_CustomAppLinks>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$CustomAppLinksToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppLinks&&(identical(other.homePage, homePage) || other.homePage == homePage)&&(identical(other.privacyPolicy, privacyPolicy) || other.privacyPolicy == privacyPolicy)&&(identical(other.termsOfService, termsOfService) || other.termsOfService == termsOfService));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,homePage,privacyPolicy,termsOfService);
@override
String toString() {
return 'CustomAppLinks(homePage: $homePage, privacyPolicy: $privacyPolicy, termsOfService: $termsOfService)';
}
}
/// @nodoc
abstract mixin class _$CustomAppLinksCopyWith<$Res> implements $CustomAppLinksCopyWith<$Res> {
factory _$CustomAppLinksCopyWith(_CustomAppLinks value, $Res Function(_CustomAppLinks) _then) = __$CustomAppLinksCopyWithImpl;
@override @useResult
$Res call({
String? homePage, String? privacyPolicy, String? termsOfService
});
}
/// @nodoc
class __$CustomAppLinksCopyWithImpl<$Res>
implements _$CustomAppLinksCopyWith<$Res> {
__$CustomAppLinksCopyWithImpl(this._self, this._then);
final _CustomAppLinks _self;
final $Res Function(_CustomAppLinks) _then;
/// Create a copy of CustomAppLinks
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? homePage = freezed,Object? privacyPolicy = freezed,Object? termsOfService = freezed,}) {
return _then(_CustomAppLinks(
homePage: freezed == homePage ? _self.homePage : homePage // ignore: cast_nullable_to_non_nullable
as String?,privacyPolicy: freezed == privacyPolicy ? _self.privacyPolicy : privacyPolicy // ignore: cast_nullable_to_non_nullable
as String?,termsOfService: freezed == termsOfService ? _self.termsOfService : termsOfService // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
mixin _$CustomAppOauthConfig {
String? get clientUri; List<String> get redirectUris; List<String>? get postLogoutRedirectUris; List<String> get allowedScopes; List<String> get allowedGrantTypes; bool get requirePkce; bool get allowOfflineAccess;
/// Create a copy of CustomAppOauthConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$CustomAppOauthConfigCopyWith<CustomAppOauthConfig> get copyWith => _$CustomAppOauthConfigCopyWithImpl<CustomAppOauthConfig>(this as CustomAppOauthConfig, _$identity);
/// Serializes this CustomAppOauthConfig to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppOauthConfig&&(identical(other.clientUri, clientUri) || other.clientUri == clientUri)&&const DeepCollectionEquality().equals(other.redirectUris, redirectUris)&&const DeepCollectionEquality().equals(other.postLogoutRedirectUris, postLogoutRedirectUris)&&const DeepCollectionEquality().equals(other.allowedScopes, allowedScopes)&&const DeepCollectionEquality().equals(other.allowedGrantTypes, allowedGrantTypes)&&(identical(other.requirePkce, requirePkce) || other.requirePkce == requirePkce)&&(identical(other.allowOfflineAccess, allowOfflineAccess) || other.allowOfflineAccess == allowOfflineAccess));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,clientUri,const DeepCollectionEquality().hash(redirectUris),const DeepCollectionEquality().hash(postLogoutRedirectUris),const DeepCollectionEquality().hash(allowedScopes),const DeepCollectionEquality().hash(allowedGrantTypes),requirePkce,allowOfflineAccess);
@override
String toString() {
return 'CustomAppOauthConfig(clientUri: $clientUri, redirectUris: $redirectUris, postLogoutRedirectUris: $postLogoutRedirectUris, allowedScopes: $allowedScopes, allowedGrantTypes: $allowedGrantTypes, requirePkce: $requirePkce, allowOfflineAccess: $allowOfflineAccess)';
}
}
/// @nodoc
abstract mixin class $CustomAppOauthConfigCopyWith<$Res> {
factory $CustomAppOauthConfigCopyWith(CustomAppOauthConfig value, $Res Function(CustomAppOauthConfig) _then) = _$CustomAppOauthConfigCopyWithImpl;
@useResult
$Res call({
String? clientUri, List<String> redirectUris, List<String>? postLogoutRedirectUris, List<String> allowedScopes, List<String> allowedGrantTypes, bool requirePkce, bool allowOfflineAccess
});
}
/// @nodoc
class _$CustomAppOauthConfigCopyWithImpl<$Res>
implements $CustomAppOauthConfigCopyWith<$Res> {
_$CustomAppOauthConfigCopyWithImpl(this._self, this._then);
final CustomAppOauthConfig _self;
final $Res Function(CustomAppOauthConfig) _then;
/// Create a copy of CustomAppOauthConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? clientUri = freezed,Object? redirectUris = null,Object? postLogoutRedirectUris = freezed,Object? allowedScopes = null,Object? allowedGrantTypes = null,Object? requirePkce = null,Object? allowOfflineAccess = null,}) {
return _then(_self.copyWith(
clientUri: freezed == clientUri ? _self.clientUri : clientUri // ignore: cast_nullable_to_non_nullable
as String?,redirectUris: null == redirectUris ? _self.redirectUris : redirectUris // ignore: cast_nullable_to_non_nullable
as List<String>,postLogoutRedirectUris: freezed == postLogoutRedirectUris ? _self.postLogoutRedirectUris : postLogoutRedirectUris // ignore: cast_nullable_to_non_nullable
as List<String>?,allowedScopes: null == allowedScopes ? _self.allowedScopes : allowedScopes // ignore: cast_nullable_to_non_nullable
as List<String>,allowedGrantTypes: null == allowedGrantTypes ? _self.allowedGrantTypes : allowedGrantTypes // ignore: cast_nullable_to_non_nullable
as List<String>,requirePkce: null == requirePkce ? _self.requirePkce : requirePkce // ignore: cast_nullable_to_non_nullable
as bool,allowOfflineAccess: null == allowOfflineAccess ? _self.allowOfflineAccess : allowOfflineAccess // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _CustomAppOauthConfig implements CustomAppOauthConfig {
const _CustomAppOauthConfig({this.clientUri, final List<String> redirectUris = const [], final List<String>? postLogoutRedirectUris, final List<String> allowedScopes = const ['openid', 'profile', 'email'], final List<String> allowedGrantTypes = const ['authorization_code', 'refresh_token'], this.requirePkce = true, this.allowOfflineAccess = false}): _redirectUris = redirectUris,_postLogoutRedirectUris = postLogoutRedirectUris,_allowedScopes = allowedScopes,_allowedGrantTypes = allowedGrantTypes;
factory _CustomAppOauthConfig.fromJson(Map<String, dynamic> json) => _$CustomAppOauthConfigFromJson(json);
@override final String? clientUri;
final List<String> _redirectUris;
@override@JsonKey() List<String> get redirectUris {
if (_redirectUris is EqualUnmodifiableListView) return _redirectUris;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_redirectUris);
}
final List<String>? _postLogoutRedirectUris;
@override List<String>? get postLogoutRedirectUris {
final value = _postLogoutRedirectUris;
if (value == null) return null;
if (_postLogoutRedirectUris is EqualUnmodifiableListView) return _postLogoutRedirectUris;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
final List<String> _allowedScopes;
@override@JsonKey() List<String> get allowedScopes {
if (_allowedScopes is EqualUnmodifiableListView) return _allowedScopes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_allowedScopes);
}
final List<String> _allowedGrantTypes;
@override@JsonKey() List<String> get allowedGrantTypes {
if (_allowedGrantTypes is EqualUnmodifiableListView) return _allowedGrantTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_allowedGrantTypes);
}
@override@JsonKey() final bool requirePkce;
@override@JsonKey() final bool allowOfflineAccess;
/// Create a copy of CustomAppOauthConfig
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$CustomAppOauthConfigCopyWith<_CustomAppOauthConfig> get copyWith => __$CustomAppOauthConfigCopyWithImpl<_CustomAppOauthConfig>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$CustomAppOauthConfigToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppOauthConfig&&(identical(other.clientUri, clientUri) || other.clientUri == clientUri)&&const DeepCollectionEquality().equals(other._redirectUris, _redirectUris)&&const DeepCollectionEquality().equals(other._postLogoutRedirectUris, _postLogoutRedirectUris)&&const DeepCollectionEquality().equals(other._allowedScopes, _allowedScopes)&&const DeepCollectionEquality().equals(other._allowedGrantTypes, _allowedGrantTypes)&&(identical(other.requirePkce, requirePkce) || other.requirePkce == requirePkce)&&(identical(other.allowOfflineAccess, allowOfflineAccess) || other.allowOfflineAccess == allowOfflineAccess));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,clientUri,const DeepCollectionEquality().hash(_redirectUris),const DeepCollectionEquality().hash(_postLogoutRedirectUris),const DeepCollectionEquality().hash(_allowedScopes),const DeepCollectionEquality().hash(_allowedGrantTypes),requirePkce,allowOfflineAccess);
@override
String toString() {
return 'CustomAppOauthConfig(clientUri: $clientUri, redirectUris: $redirectUris, postLogoutRedirectUris: $postLogoutRedirectUris, allowedScopes: $allowedScopes, allowedGrantTypes: $allowedGrantTypes, requirePkce: $requirePkce, allowOfflineAccess: $allowOfflineAccess)';
}
}
/// @nodoc
abstract mixin class _$CustomAppOauthConfigCopyWith<$Res> implements $CustomAppOauthConfigCopyWith<$Res> {
factory _$CustomAppOauthConfigCopyWith(_CustomAppOauthConfig value, $Res Function(_CustomAppOauthConfig) _then) = __$CustomAppOauthConfigCopyWithImpl;
@override @useResult
$Res call({
String? clientUri, List<String> redirectUris, List<String>? postLogoutRedirectUris, List<String> allowedScopes, List<String> allowedGrantTypes, bool requirePkce, bool allowOfflineAccess
});
}
/// @nodoc
class __$CustomAppOauthConfigCopyWithImpl<$Res>
implements _$CustomAppOauthConfigCopyWith<$Res> {
__$CustomAppOauthConfigCopyWithImpl(this._self, this._then);
final _CustomAppOauthConfig _self;
final $Res Function(_CustomAppOauthConfig) _then;
/// Create a copy of CustomAppOauthConfig
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? clientUri = freezed,Object? redirectUris = null,Object? postLogoutRedirectUris = freezed,Object? allowedScopes = null,Object? allowedGrantTypes = null,Object? requirePkce = null,Object? allowOfflineAccess = null,}) {
return _then(_CustomAppOauthConfig(
clientUri: freezed == clientUri ? _self.clientUri : clientUri // ignore: cast_nullable_to_non_nullable
as String?,redirectUris: null == redirectUris ? _self._redirectUris : redirectUris // ignore: cast_nullable_to_non_nullable
as List<String>,postLogoutRedirectUris: freezed == postLogoutRedirectUris ? _self._postLogoutRedirectUris : postLogoutRedirectUris // ignore: cast_nullable_to_non_nullable
as List<String>?,allowedScopes: null == allowedScopes ? _self._allowedScopes : allowedScopes // ignore: cast_nullable_to_non_nullable
as List<String>,allowedGrantTypes: null == allowedGrantTypes ? _self._allowedGrantTypes : allowedGrantTypes // ignore: cast_nullable_to_non_nullable
as List<String>,requirePkce: null == requirePkce ? _self.requirePkce : requirePkce // ignore: cast_nullable_to_non_nullable
as bool,allowOfflineAccess: null == allowOfflineAccess ? _self.allowOfflineAccess : allowOfflineAccess // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
mixin _$CustomAppSecret {
String get id; String get secret; String? get description; DateTime? get expiredAt; bool get isOidc; String get appId;
/// 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.description, description) || other.description == description)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc)&&(identical(other.appId, appId) || other.appId == appId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,secret,description,expiredAt,isOidc,appId);
@override
String toString() {
return 'CustomAppSecret(id: $id, secret: $secret, description: $description, expiredAt: $expiredAt, isOidc: $isOidc, appId: $appId)';
}
}
/// @nodoc
abstract mixin class $CustomAppSecretCopyWith<$Res> {
factory $CustomAppSecretCopyWith(CustomAppSecret value, $Res Function(CustomAppSecret) _then) = _$CustomAppSecretCopyWithImpl;
@useResult
$Res call({
String id, String secret, String? description, DateTime? expiredAt, bool isOidc, String appId
});
}
/// @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 = null,Object? description = freezed,Object? expiredAt = freezed,Object? isOidc = null,Object? appId = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isOidc: null == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable
as bool,appId: null == appId ? _self.appId : appId // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _CustomAppSecret implements CustomAppSecret {
const _CustomAppSecret({this.id = '', this.secret = '', this.description, this.expiredAt, this.isOidc = false, this.appId = ''});
factory _CustomAppSecret.fromJson(Map<String, dynamic> json) => _$CustomAppSecretFromJson(json);
@override@JsonKey() final String id;
@override@JsonKey() final String secret;
@override final String? description;
@override final DateTime? expiredAt;
@override@JsonKey() final bool isOidc;
@override@JsonKey() final String appId;
/// 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.description, description) || other.description == description)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc)&&(identical(other.appId, appId) || other.appId == appId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,secret,description,expiredAt,isOidc,appId);
@override
String toString() {
return 'CustomAppSecret(id: $id, secret: $secret, description: $description, expiredAt: $expiredAt, isOidc: $isOidc, appId: $appId)';
}
}
/// @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, String? description, DateTime? expiredAt, bool isOidc, String appId
});
}
/// @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 = null,Object? description = freezed,Object? expiredAt = freezed,Object? isOidc = null,Object? appId = null,}) {
return _then(_CustomAppSecret(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isOidc: null == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable
as bool,appId: null == appId ? _self.appId : appId // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on

View File

@ -0,0 +1,137 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'custom_app.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_CustomApp _$CustomAppFromJson(Map<String, dynamic> json) => _CustomApp(
id: json['id'] as String? ?? '',
slug: json['slug'] as String? ?? '',
name: json['name'] as String? ?? '',
description: json['description'] as String?,
status: (json['status'] as num?)?.toInt() ?? 0,
picture:
json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background:
json['background'] == null
? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
verification:
json['verification'] == null
? null
: SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>,
),
oauthConfig:
json['oauth_config'] == null
? null
: CustomAppOauthConfig.fromJson(
json['oauth_config'] as Map<String, dynamic>,
),
links:
json['links'] == null
? null
: CustomAppLinks.fromJson(json['links'] as Map<String, dynamic>),
secrets:
(json['secrets'] as List<dynamic>?)
?.map((e) => CustomAppSecret.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
publisherId: json['publisher_id'] as String? ?? '',
);
Map<String, dynamic> _$CustomAppToJson(_CustomApp instance) =>
<String, dynamic>{
'id': instance.id,
'slug': instance.slug,
'name': instance.name,
'description': instance.description,
'status': instance.status,
'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(),
'verification': instance.verification?.toJson(),
'oauth_config': instance.oauthConfig?.toJson(),
'links': instance.links?.toJson(),
'secrets': instance.secrets.map((e) => e.toJson()).toList(),
'publisher_id': instance.publisherId,
};
_CustomAppLinks _$CustomAppLinksFromJson(Map<String, dynamic> json) =>
_CustomAppLinks(
homePage: json['home_page'] as String?,
privacyPolicy: json['privacy_policy'] as String?,
termsOfService: json['terms_of_service'] as String?,
);
Map<String, dynamic> _$CustomAppLinksToJson(_CustomAppLinks instance) =>
<String, dynamic>{
'home_page': instance.homePage,
'privacy_policy': instance.privacyPolicy,
'terms_of_service': instance.termsOfService,
};
_CustomAppOauthConfig _$CustomAppOauthConfigFromJson(
Map<String, dynamic> json,
) => _CustomAppOauthConfig(
clientUri: json['client_uri'] as String?,
redirectUris:
(json['redirect_uris'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
postLogoutRedirectUris:
(json['post_logout_redirect_uris'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
allowedScopes:
(json['allowed_scopes'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ['openid', 'profile', 'email'],
allowedGrantTypes:
(json['allowed_grant_types'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ['authorization_code', 'refresh_token'],
requirePkce: json['require_pkce'] as bool? ?? true,
allowOfflineAccess: json['allow_offline_access'] as bool? ?? false,
);
Map<String, dynamic> _$CustomAppOauthConfigToJson(
_CustomAppOauthConfig instance,
) => <String, dynamic>{
'client_uri': instance.clientUri,
'redirect_uris': instance.redirectUris,
'post_logout_redirect_uris': instance.postLogoutRedirectUris,
'allowed_scopes': instance.allowedScopes,
'allowed_grant_types': instance.allowedGrantTypes,
'require_pkce': instance.requirePkce,
'allow_offline_access': instance.allowOfflineAccess,
};
_CustomAppSecret _$CustomAppSecretFromJson(Map<String, dynamic> json) =>
_CustomAppSecret(
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),
isOidc: json['is_oidc'] as bool? ?? false,
appId: json['app_id'] as String? ?? '',
);
Map<String, dynamic> _$CustomAppSecretToJson(_CustomAppSecret instance) =>
<String, dynamic>{
'id': instance.id,
'secret': instance.secret,
'description': instance.description,
'expired_at': instance.expiredAt?.toIso8601String(),
'is_oidc': instance.isOidc,
'app_id': instance.appId,
};

14
lib/models/developer.dart Normal file
View File

@ -0,0 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'developer.freezed.dart';
part 'developer.g.dart';
@freezed
sealed class DeveloperStats with _$DeveloperStats {
const factory DeveloperStats({
@Default(0) int totalCustomApps,
}) = _DeveloperStats;
factory DeveloperStats.fromJson(Map<String, dynamic> json) =>
_$DeveloperStatsFromJson(json);
}

View File

@ -0,0 +1,148 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'developer.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$DeveloperStats {
int get totalCustomApps;
/// Create a copy of DeveloperStats
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$DeveloperStatsCopyWith<DeveloperStats> get copyWith => _$DeveloperStatsCopyWithImpl<DeveloperStats>(this as DeveloperStats, _$identity);
/// Serializes this DeveloperStats to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is DeveloperStats&&(identical(other.totalCustomApps, totalCustomApps) || other.totalCustomApps == totalCustomApps));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,totalCustomApps);
@override
String toString() {
return 'DeveloperStats(totalCustomApps: $totalCustomApps)';
}
}
/// @nodoc
abstract mixin class $DeveloperStatsCopyWith<$Res> {
factory $DeveloperStatsCopyWith(DeveloperStats value, $Res Function(DeveloperStats) _then) = _$DeveloperStatsCopyWithImpl;
@useResult
$Res call({
int totalCustomApps
});
}
/// @nodoc
class _$DeveloperStatsCopyWithImpl<$Res>
implements $DeveloperStatsCopyWith<$Res> {
_$DeveloperStatsCopyWithImpl(this._self, this._then);
final DeveloperStats _self;
final $Res Function(DeveloperStats) _then;
/// Create a copy of DeveloperStats
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? totalCustomApps = null,}) {
return _then(_self.copyWith(
totalCustomApps: null == totalCustomApps ? _self.totalCustomApps : totalCustomApps // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _DeveloperStats implements DeveloperStats {
const _DeveloperStats({this.totalCustomApps = 0});
factory _DeveloperStats.fromJson(Map<String, dynamic> json) => _$DeveloperStatsFromJson(json);
@override@JsonKey() final int totalCustomApps;
/// Create a copy of DeveloperStats
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$DeveloperStatsCopyWith<_DeveloperStats> get copyWith => __$DeveloperStatsCopyWithImpl<_DeveloperStats>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$DeveloperStatsToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DeveloperStats&&(identical(other.totalCustomApps, totalCustomApps) || other.totalCustomApps == totalCustomApps));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,totalCustomApps);
@override
String toString() {
return 'DeveloperStats(totalCustomApps: $totalCustomApps)';
}
}
/// @nodoc
abstract mixin class _$DeveloperStatsCopyWith<$Res> implements $DeveloperStatsCopyWith<$Res> {
factory _$DeveloperStatsCopyWith(_DeveloperStats value, $Res Function(_DeveloperStats) _then) = __$DeveloperStatsCopyWithImpl;
@override @useResult
$Res call({
int totalCustomApps
});
}
/// @nodoc
class __$DeveloperStatsCopyWithImpl<$Res>
implements _$DeveloperStatsCopyWith<$Res> {
__$DeveloperStatsCopyWithImpl(this._self, this._then);
final _DeveloperStats _self;
final $Res Function(_DeveloperStats) _then;
/// Create a copy of DeveloperStats
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? totalCustomApps = null,}) {
return _then(_DeveloperStats(
totalCustomApps: null == totalCustomApps ? _self.totalCustomApps : totalCustomApps // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
// dart format on

View File

@ -0,0 +1,15 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'developer.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_DeveloperStats _$DeveloperStatsFromJson(Map<String, dynamic> json) =>
_DeveloperStats(
totalCustomApps: (json['total_custom_apps'] as num?)?.toInt() ?? 0,
);
Map<String, dynamic> _$DeveloperStatsToJson(_DeveloperStats instance) =>
<String, dynamic>{'total_custom_apps': instance.totalCustomApps};

View File

@ -2,7 +2,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart';
import 'package:island/models/user.dart';
import 'package:island/models/publisher.dart';
part 'post.freezed.dart';
part 'post.g.dart';
@ -32,7 +32,7 @@ sealed class SnPost with _$SnPost {
String? forwardedPostId,
SnPost? forwardedPost,
@Default([]) List<SnCloudFile> attachments,
@Default(SnPublisher()) SnPublisher publisher,
required SnPublisher publisher,
@Default({}) Map<String, int> reactionsCount,
@Default([]) List<dynamic> reactions,
@Default([]) List<PostTag> tags,
@ -47,29 +47,6 @@ sealed class SnPost with _$SnPost {
factory SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json);
}
@freezed
sealed class SnPublisher with _$SnPublisher {
const factory SnPublisher({
@Default('') String id,
@Default(0) int type,
@Default('') String name,
@Default('') String nick,
@Default('') String bio,
SnCloudFile? picture,
SnCloudFile? background,
SnAccount? account,
String? accountId,
@Default(null) DateTime? createdAt,
@Default(null) DateTime? updatedAt,
DateTime? deletedAt,
String? realmId,
SnVerificationMark? verification,
}) = _SnPublisher;
factory SnPublisher.fromJson(Map<String, dynamic> json) =>
_$SnPublisherFromJson(json);
}
@freezed
sealed class SnPublisherStats with _$SnPublisherStats {
const factory SnPublisherStats({

View File

@ -156,7 +156,7 @@ $SnPublisherCopyWith<$Res> get publisher {
@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.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, final List<SnCloudFile> attachments = const [], this.publisher = const SnPublisher(), final Map<String, int> reactionsCount = const {}, final List<dynamic> reactions = const [], final List<PostTag> tags = const [], final List<PostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_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.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, final List<SnCloudFile> attachments = const [], required this.publisher, final Map<String, int> reactionsCount = const {}, final List<dynamic> reactions = const [], final List<PostTag> tags = const [], final List<PostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json);
@override final String id;
@ -195,7 +195,7 @@ class _SnPost implements SnPost {
return EqualUnmodifiableListView(_attachments);
}
@override@JsonKey() final SnPublisher publisher;
@override final SnPublisher publisher;
final Map<String, int> _reactionsCount;
@override@JsonKey() Map<String, int> get reactionsCount {
if (_reactionsCount is EqualUnmodifiableMapView) return _reactionsCount;
@ -373,274 +373,6 @@ $SnPublisherCopyWith<$Res> get publisher {
}
/// @nodoc
mixin _$SnPublisher {
String get id; int get type; String get name; String get nick; String get bio; SnCloudFile? get picture; SnCloudFile? get background; SnAccount? get account; String? get accountId; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; String? get realmId; SnVerificationMark? get verification;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<SnPublisher> get copyWith => _$SnPublisherCopyWithImpl<SnPublisher>(this as SnPublisher, _$identity);
/// Serializes this SnPublisher to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.verification, verification) || other.verification == verification));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId,verification);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId, verification: $verification)';
}
}
/// @nodoc
abstract mixin class $SnPublisherCopyWith<$Res> {
factory $SnPublisherCopyWith(SnPublisher value, $Res Function(SnPublisher) _then) = _$SnPublisherCopyWithImpl;
@useResult
$Res call({
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String? realmId, SnVerificationMark? verification
});
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnAccountCopyWith<$Res>? get account;$SnVerificationMarkCopyWith<$Res>? get verification;
}
/// @nodoc
class _$SnPublisherCopyWithImpl<$Res>
implements $SnPublisherCopyWith<$Res> {
_$SnPublisherCopyWithImpl(this._self, this._then);
final SnPublisher _self;
final $Res Function(SnPublisher) _then;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? realmId = freezed,Object? verification = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,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,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,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
as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
as String?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,
));
}
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnPublisher implements SnPublisher {
const _SnPublisher({this.id = '', this.type = 0, this.name = '', this.nick = '', this.bio = '', this.picture, this.background, this.account, this.accountId, this.createdAt = null, this.updatedAt = null, this.deletedAt, this.realmId, this.verification});
factory _SnPublisher.fromJson(Map<String, dynamic> json) => _$SnPublisherFromJson(json);
@override@JsonKey() final String id;
@override@JsonKey() final int type;
@override@JsonKey() final String name;
@override@JsonKey() final String nick;
@override@JsonKey() final String bio;
@override final SnCloudFile? picture;
@override final SnCloudFile? background;
@override final SnAccount? account;
@override final String? accountId;
@override@JsonKey() final DateTime? createdAt;
@override@JsonKey() final DateTime? updatedAt;
@override final DateTime? deletedAt;
@override final String? realmId;
@override final SnVerificationMark? verification;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnPublisherCopyWith<_SnPublisher> get copyWith => __$SnPublisherCopyWithImpl<_SnPublisher>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnPublisherToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.verification, verification) || other.verification == verification));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId,verification);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId, verification: $verification)';
}
}
/// @nodoc
abstract mixin class _$SnPublisherCopyWith<$Res> implements $SnPublisherCopyWith<$Res> {
factory _$SnPublisherCopyWith(_SnPublisher value, $Res Function(_SnPublisher) _then) = __$SnPublisherCopyWithImpl;
@override @useResult
$Res call({
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String? realmId, SnVerificationMark? verification
});
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnAccountCopyWith<$Res>? get account;@override $SnVerificationMarkCopyWith<$Res>? get verification;
}
/// @nodoc
class __$SnPublisherCopyWithImpl<$Res>
implements _$SnPublisherCopyWith<$Res> {
__$SnPublisherCopyWithImpl(this._self, this._then);
final _SnPublisher _self;
final $Res Function(_SnPublisher) _then;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? realmId = freezed,Object? verification = freezed,}) {
return _then(_SnPublisher(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,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,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,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
as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
as String?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,
));
}
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}
}
/// @nodoc
mixin _$SnPublisherStats {

View File

@ -48,10 +48,7 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
?.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
publisher:
json['publisher'] == null
? const SnPublisher()
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
publisher: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
reactionsCount:
(json['reactions_count'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, (e as num).toInt()),
@ -119,64 +116,6 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'is_truncated': instance.isTruncated,
};
_SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
id: json['id'] as String? ?? '',
type: (json['type'] as num?)?.toInt() ?? 0,
name: json['name'] as String? ?? '',
nick: json['nick'] as String? ?? '',
bio: json['bio'] as String? ?? '',
picture:
json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background:
json['background'] == null
? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
account:
json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
accountId: json['account_id'] as String?,
createdAt:
json['created_at'] == null
? null
: DateTime.parse(json['created_at'] as String),
updatedAt:
json['updated_at'] == null
? null
: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
realmId: json['realm_id'] as String?,
verification:
json['verification'] == null
? null
: SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
<String, dynamic>{
'id': instance.id,
'type': instance.type,
'name': instance.name,
'nick': instance.nick,
'bio': instance.bio,
'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(),
'account': instance.account?.toJson(),
'account_id': instance.accountId,
'created_at': instance.createdAt?.toIso8601String(),
'updated_at': instance.updatedAt?.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'realm_id': instance.realmId,
'verification': instance.verification?.toJson(),
};
_SnPublisherStats _$SnPublisherStatsFromJson(Map<String, dynamic> json) =>
_SnPublisherStats(
postsCreated: (json['posts_created'] as num).toInt(),

47
lib/models/publisher.dart Normal file
View File

@ -0,0 +1,47 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
import 'package:island/models/user.dart';
part 'publisher.freezed.dart';
part 'publisher.g.dart';
@freezed
sealed class SnPublisher with _$SnPublisher {
const factory SnPublisher({
@Default('') String id,
@Default(0) int type,
@Default('') String name,
@Default('') String nick,
@Default('') String bio,
SnCloudFile? picture,
SnCloudFile? background,
SnAccount? account,
String? accountId,
@Default(null) DateTime? createdAt,
@Default(null) DateTime? updatedAt,
DateTime? deletedAt,
String? realmId,
SnVerificationMark? verification,
}) = _SnPublisher;
factory SnPublisher.fromJson(Map<String, dynamic> json) =>
_$SnPublisherFromJson(json);
}
@freezed
sealed class SnPublisherMember with _$SnPublisherMember {
const factory SnPublisherMember({
required String publisherId,
required SnPublisher? publisher,
required String accountId,
required SnAccount? account,
required int role,
required DateTime? joinedAt,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnPublisherMember;
factory SnPublisherMember.fromJson(Map<String, dynamic> json) =>
_$SnPublisherMemberFromJson(json);
}

View File

@ -0,0 +1,488 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'publisher.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnPublisher {
String get id; int get type; String get name; String get nick; String get bio; SnCloudFile? get picture; SnCloudFile? get background; SnAccount? get account; String? get accountId; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; String? get realmId; SnVerificationMark? get verification;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<SnPublisher> get copyWith => _$SnPublisherCopyWithImpl<SnPublisher>(this as SnPublisher, _$identity);
/// Serializes this SnPublisher to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.verification, verification) || other.verification == verification));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId,verification);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId, verification: $verification)';
}
}
/// @nodoc
abstract mixin class $SnPublisherCopyWith<$Res> {
factory $SnPublisherCopyWith(SnPublisher value, $Res Function(SnPublisher) _then) = _$SnPublisherCopyWithImpl;
@useResult
$Res call({
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String? realmId, SnVerificationMark? verification
});
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnAccountCopyWith<$Res>? get account;$SnVerificationMarkCopyWith<$Res>? get verification;
}
/// @nodoc
class _$SnPublisherCopyWithImpl<$Res>
implements $SnPublisherCopyWith<$Res> {
_$SnPublisherCopyWithImpl(this._self, this._then);
final SnPublisher _self;
final $Res Function(SnPublisher) _then;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? realmId = freezed,Object? verification = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,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,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,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
as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
as String?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,
));
}
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnPublisher implements SnPublisher {
const _SnPublisher({this.id = '', this.type = 0, this.name = '', this.nick = '', this.bio = '', this.picture, this.background, this.account, this.accountId, this.createdAt = null, this.updatedAt = null, this.deletedAt, this.realmId, this.verification});
factory _SnPublisher.fromJson(Map<String, dynamic> json) => _$SnPublisherFromJson(json);
@override@JsonKey() final String id;
@override@JsonKey() final int type;
@override@JsonKey() final String name;
@override@JsonKey() final String nick;
@override@JsonKey() final String bio;
@override final SnCloudFile? picture;
@override final SnCloudFile? background;
@override final SnAccount? account;
@override final String? accountId;
@override@JsonKey() final DateTime? createdAt;
@override@JsonKey() final DateTime? updatedAt;
@override final DateTime? deletedAt;
@override final String? realmId;
@override final SnVerificationMark? verification;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnPublisherCopyWith<_SnPublisher> get copyWith => __$SnPublisherCopyWithImpl<_SnPublisher>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnPublisherToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.verification, verification) || other.verification == verification));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId,verification);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId, verification: $verification)';
}
}
/// @nodoc
abstract mixin class _$SnPublisherCopyWith<$Res> implements $SnPublisherCopyWith<$Res> {
factory _$SnPublisherCopyWith(_SnPublisher value, $Res Function(_SnPublisher) _then) = __$SnPublisherCopyWithImpl;
@override @useResult
$Res call({
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String? realmId, SnVerificationMark? verification
});
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnAccountCopyWith<$Res>? get account;@override $SnVerificationMarkCopyWith<$Res>? get verification;
}
/// @nodoc
class __$SnPublisherCopyWithImpl<$Res>
implements _$SnPublisherCopyWith<$Res> {
__$SnPublisherCopyWithImpl(this._self, this._then);
final _SnPublisher _self;
final $Res Function(_SnPublisher) _then;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? realmId = freezed,Object? verification = freezed,}) {
return _then(_SnPublisher(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,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,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,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
as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
as String?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,
));
}
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}
}
/// @nodoc
mixin _$SnPublisherMember {
String get publisherId; SnPublisher? get publisher; String get accountId; SnAccount? get account; int get role; DateTime? get joinedAt; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnPublisherMemberCopyWith<SnPublisherMember> get copyWith => _$SnPublisherMemberCopyWithImpl<SnPublisherMember>(this as SnPublisherMember, _$identity);
/// Serializes this SnPublisherMember to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisherMember&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.role, role) || other.role == role)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(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,publisherId,publisher,accountId,account,role,joinedAt,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnPublisherMember(publisherId: $publisherId, publisher: $publisher, accountId: $accountId, account: $account, role: $role, joinedAt: $joinedAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnPublisherMemberCopyWith<$Res> {
factory $SnPublisherMemberCopyWith(SnPublisherMember value, $Res Function(SnPublisherMember) _then) = _$SnPublisherMemberCopyWithImpl;
@useResult
$Res call({
String publisherId, SnPublisher? publisher, String accountId, SnAccount? account, int role, DateTime? joinedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnPublisherCopyWith<$Res>? get publisher;$SnAccountCopyWith<$Res>? get account;
}
/// @nodoc
class _$SnPublisherMemberCopyWithImpl<$Res>
implements $SnPublisherMemberCopyWith<$Res> {
_$SnPublisherMemberCopyWithImpl(this._self, this._then);
final SnPublisherMember _self;
final $Res Function(SnPublisherMember) _then;
/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? publisherId = null,Object? publisher = freezed,Object? accountId = null,Object? account = freezed,Object? role = null,Object? joinedAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as SnPublisher?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
as int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // 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?,
));
}
/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<$Res>? get publisher {
if (_self.publisher == null) {
return null;
}
return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) {
return _then(_self.copyWith(publisher: value));
});
}/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnPublisherMember implements SnPublisherMember {
const _SnPublisherMember({required this.publisherId, required this.publisher, required this.accountId, required this.account, required this.role, required this.joinedAt, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnPublisherMember.fromJson(Map<String, dynamic> json) => _$SnPublisherMemberFromJson(json);
@override final String publisherId;
@override final SnPublisher? publisher;
@override final String accountId;
@override final SnAccount? account;
@override final int role;
@override final DateTime? joinedAt;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnPublisherMemberCopyWith<_SnPublisherMember> get copyWith => __$SnPublisherMemberCopyWithImpl<_SnPublisherMember>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnPublisherMemberToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisherMember&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.role, role) || other.role == role)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(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,publisherId,publisher,accountId,account,role,joinedAt,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnPublisherMember(publisherId: $publisherId, publisher: $publisher, accountId: $accountId, account: $account, role: $role, joinedAt: $joinedAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnPublisherMemberCopyWith<$Res> implements $SnPublisherMemberCopyWith<$Res> {
factory _$SnPublisherMemberCopyWith(_SnPublisherMember value, $Res Function(_SnPublisherMember) _then) = __$SnPublisherMemberCopyWithImpl;
@override @useResult
$Res call({
String publisherId, SnPublisher? publisher, String accountId, SnAccount? account, int role, DateTime? joinedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnPublisherCopyWith<$Res>? get publisher;@override $SnAccountCopyWith<$Res>? get account;
}
/// @nodoc
class __$SnPublisherMemberCopyWithImpl<$Res>
implements _$SnPublisherMemberCopyWith<$Res> {
__$SnPublisherMemberCopyWithImpl(this._self, this._then);
final _SnPublisherMember _self;
final $Res Function(_SnPublisherMember) _then;
/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? publisherId = null,Object? publisher = freezed,Object? accountId = null,Object? account = freezed,Object? role = null,Object? joinedAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnPublisherMember(
publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as SnPublisher?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
as int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // 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?,
));
}
/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<$Res>? get publisher {
if (_self.publisher == null) {
return null;
}
return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) {
return _then(_self.copyWith(publisher: value));
});
}/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
}
// dart format on

103
lib/models/publisher.g.dart Normal file
View File

@ -0,0 +1,103 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'publisher.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
id: json['id'] as String? ?? '',
type: (json['type'] as num?)?.toInt() ?? 0,
name: json['name'] as String? ?? '',
nick: json['nick'] as String? ?? '',
bio: json['bio'] as String? ?? '',
picture:
json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background:
json['background'] == null
? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
account:
json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
accountId: json['account_id'] as String?,
createdAt:
json['created_at'] == null
? null
: DateTime.parse(json['created_at'] as String),
updatedAt:
json['updated_at'] == null
? null
: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
realmId: json['realm_id'] as String?,
verification:
json['verification'] == null
? null
: SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
<String, dynamic>{
'id': instance.id,
'type': instance.type,
'name': instance.name,
'nick': instance.nick,
'bio': instance.bio,
'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(),
'account': instance.account?.toJson(),
'account_id': instance.accountId,
'created_at': instance.createdAt?.toIso8601String(),
'updated_at': instance.updatedAt?.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'realm_id': instance.realmId,
'verification': instance.verification?.toJson(),
};
_SnPublisherMember _$SnPublisherMemberFromJson(Map<String, dynamic> json) =>
_SnPublisherMember(
publisherId: json['publisher_id'] as String,
publisher:
json['publisher'] == null
? null
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
accountId: json['account_id'] as String,
account:
json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
role: (json['role'] as num).toInt(),
joinedAt:
json['joined_at'] == null
? null
: DateTime.parse(json['joined_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> _$SnPublisherMemberToJson(_SnPublisherMember instance) =>
<String, dynamic>{
'publisher_id': instance.publisherId,
'publisher': instance.publisher?.toJson(),
'account_id': instance.accountId,
'account': instance.account?.toJson(),
'role': instance.role,
'joined_at': instance.joinedAt?.toIso8601String(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@ -1,6 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
part 'sticker.freezed.dart';
part 'sticker.g.dart';

64
lib/models/webfeed.dart Normal file
View File

@ -0,0 +1,64 @@
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/embed.dart';
part 'webfeed.freezed.dart';
part 'webfeed.g.dart';
@freezed
sealed class SnWebFeedConfig with _$SnWebFeedConfig {
const factory SnWebFeedConfig({@Default(false) bool scrapPage}) =
_SnWebFeedConfig;
factory SnWebFeedConfig.fromJson(Map<String, dynamic> json) =>
_$SnWebFeedConfigFromJson(json);
}
@freezed
sealed class SnWebFeed with _$SnWebFeed {
const factory SnWebFeed({
required String id,
required String url,
required String title,
String? description,
SnScrappedLink? preview,
@Default(SnWebFeedConfig()) SnWebFeedConfig config,
required String publisherId,
@Default([]) List<SnWebArticle> articles,
required DateTime createdAt,
required DateTime updatedAt,
DateTime? deletedAt,
}) = _SnWebFeed;
factory SnWebFeed.fromJson(Map<String, dynamic> json) =>
_$SnWebFeedFromJson(json);
factory SnWebFeed.fromJsonString(String jsonString) =>
SnWebFeed.fromJson(jsonDecode(jsonString) as Map<String, dynamic>);
}
@freezed
sealed class SnWebArticle with _$SnWebArticle {
const factory SnWebArticle({
required String id,
required String title,
required String url,
String? author,
Map<String, dynamic>? meta,
SnScrappedLink? preview,
SnWebFeed? feed,
String? content,
DateTime? publishedAt,
required String feedId,
required DateTime createdAt,
required DateTime updatedAt,
DateTime? deletedAt,
}) = _SnWebArticle;
factory SnWebArticle.fromJson(Map<String, dynamic> json) =>
_$SnWebArticleFromJson(json);
factory SnWebArticle.fromJsonString(String jsonString) =>
SnWebArticle.fromJson(jsonDecode(jsonString) as Map<String, dynamic>);
}

View File

@ -0,0 +1,584 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'webfeed.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnWebFeedConfig {
bool get scrapPage;
/// Create a copy of SnWebFeedConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnWebFeedConfigCopyWith<SnWebFeedConfig> get copyWith => _$SnWebFeedConfigCopyWithImpl<SnWebFeedConfig>(this as SnWebFeedConfig, _$identity);
/// Serializes this SnWebFeedConfig to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWebFeedConfig&&(identical(other.scrapPage, scrapPage) || other.scrapPage == scrapPage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,scrapPage);
@override
String toString() {
return 'SnWebFeedConfig(scrapPage: $scrapPage)';
}
}
/// @nodoc
abstract mixin class $SnWebFeedConfigCopyWith<$Res> {
factory $SnWebFeedConfigCopyWith(SnWebFeedConfig value, $Res Function(SnWebFeedConfig) _then) = _$SnWebFeedConfigCopyWithImpl;
@useResult
$Res call({
bool scrapPage
});
}
/// @nodoc
class _$SnWebFeedConfigCopyWithImpl<$Res>
implements $SnWebFeedConfigCopyWith<$Res> {
_$SnWebFeedConfigCopyWithImpl(this._self, this._then);
final SnWebFeedConfig _self;
final $Res Function(SnWebFeedConfig) _then;
/// Create a copy of SnWebFeedConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? scrapPage = null,}) {
return _then(_self.copyWith(
scrapPage: null == scrapPage ? _self.scrapPage : scrapPage // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnWebFeedConfig implements SnWebFeedConfig {
const _SnWebFeedConfig({this.scrapPage = false});
factory _SnWebFeedConfig.fromJson(Map<String, dynamic> json) => _$SnWebFeedConfigFromJson(json);
@override@JsonKey() final bool scrapPage;
/// Create a copy of SnWebFeedConfig
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnWebFeedConfigCopyWith<_SnWebFeedConfig> get copyWith => __$SnWebFeedConfigCopyWithImpl<_SnWebFeedConfig>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnWebFeedConfigToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWebFeedConfig&&(identical(other.scrapPage, scrapPage) || other.scrapPage == scrapPage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,scrapPage);
@override
String toString() {
return 'SnWebFeedConfig(scrapPage: $scrapPage)';
}
}
/// @nodoc
abstract mixin class _$SnWebFeedConfigCopyWith<$Res> implements $SnWebFeedConfigCopyWith<$Res> {
factory _$SnWebFeedConfigCopyWith(_SnWebFeedConfig value, $Res Function(_SnWebFeedConfig) _then) = __$SnWebFeedConfigCopyWithImpl;
@override @useResult
$Res call({
bool scrapPage
});
}
/// @nodoc
class __$SnWebFeedConfigCopyWithImpl<$Res>
implements _$SnWebFeedConfigCopyWith<$Res> {
__$SnWebFeedConfigCopyWithImpl(this._self, this._then);
final _SnWebFeedConfig _self;
final $Res Function(_SnWebFeedConfig) _then;
/// Create a copy of SnWebFeedConfig
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? scrapPage = null,}) {
return _then(_SnWebFeedConfig(
scrapPage: null == scrapPage ? _self.scrapPage : scrapPage // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
mixin _$SnWebFeed {
String get id; String get url; String get title; String? get description; SnScrappedLink? get preview; SnWebFeedConfig get config; String get publisherId; List<SnWebArticle> get articles; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnWebFeedCopyWith<SnWebFeed> get copyWith => _$SnWebFeedCopyWithImpl<SnWebFeed>(this as SnWebFeed, _$identity);
/// Serializes this SnWebFeed to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWebFeed&&(identical(other.id, id) || other.id == id)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.config, config) || other.config == config)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&const DeepCollectionEquality().equals(other.articles, articles)&&(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,url,title,description,preview,config,publisherId,const DeepCollectionEquality().hash(articles),createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnWebFeed(id: $id, url: $url, title: $title, description: $description, preview: $preview, config: $config, publisherId: $publisherId, articles: $articles, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnWebFeedCopyWith<$Res> {
factory $SnWebFeedCopyWith(SnWebFeed value, $Res Function(SnWebFeed) _then) = _$SnWebFeedCopyWithImpl;
@useResult
$Res call({
String id, String url, String title, String? description, SnScrappedLink? preview, SnWebFeedConfig config, String publisherId, List<SnWebArticle> articles, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnScrappedLinkCopyWith<$Res>? get preview;$SnWebFeedConfigCopyWith<$Res> get config;
}
/// @nodoc
class _$SnWebFeedCopyWithImpl<$Res>
implements $SnWebFeedCopyWith<$Res> {
_$SnWebFeedCopyWithImpl(this._self, this._then);
final SnWebFeed _self;
final $Res Function(SnWebFeed) _then;
/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? url = null,Object? title = null,Object? description = freezed,Object? preview = freezed,Object? config = null,Object? publisherId = null,Object? articles = 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,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable
as SnScrappedLink?,config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable
as SnWebFeedConfig,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,articles: null == articles ? _self.articles : articles // ignore: cast_nullable_to_non_nullable
as List<SnWebArticle>,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?,
));
}
/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnScrappedLinkCopyWith<$Res>? get preview {
if (_self.preview == null) {
return null;
}
return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) {
return _then(_self.copyWith(preview: value));
});
}/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWebFeedConfigCopyWith<$Res> get config {
return $SnWebFeedConfigCopyWith<$Res>(_self.config, (value) {
return _then(_self.copyWith(config: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnWebFeed implements SnWebFeed {
const _SnWebFeed({required this.id, required this.url, required this.title, this.description, this.preview, this.config = const SnWebFeedConfig(), required this.publisherId, final List<SnWebArticle> articles = const [], required this.createdAt, required this.updatedAt, this.deletedAt}): _articles = articles;
factory _SnWebFeed.fromJson(Map<String, dynamic> json) => _$SnWebFeedFromJson(json);
@override final String id;
@override final String url;
@override final String title;
@override final String? description;
@override final SnScrappedLink? preview;
@override@JsonKey() final SnWebFeedConfig config;
@override final String publisherId;
final List<SnWebArticle> _articles;
@override@JsonKey() List<SnWebArticle> get articles {
if (_articles is EqualUnmodifiableListView) return _articles;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_articles);
}
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnWebFeedCopyWith<_SnWebFeed> get copyWith => __$SnWebFeedCopyWithImpl<_SnWebFeed>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnWebFeedToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWebFeed&&(identical(other.id, id) || other.id == id)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.config, config) || other.config == config)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&const DeepCollectionEquality().equals(other._articles, _articles)&&(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,url,title,description,preview,config,publisherId,const DeepCollectionEquality().hash(_articles),createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnWebFeed(id: $id, url: $url, title: $title, description: $description, preview: $preview, config: $config, publisherId: $publisherId, articles: $articles, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnWebFeedCopyWith<$Res> implements $SnWebFeedCopyWith<$Res> {
factory _$SnWebFeedCopyWith(_SnWebFeed value, $Res Function(_SnWebFeed) _then) = __$SnWebFeedCopyWithImpl;
@override @useResult
$Res call({
String id, String url, String title, String? description, SnScrappedLink? preview, SnWebFeedConfig config, String publisherId, List<SnWebArticle> articles, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnScrappedLinkCopyWith<$Res>? get preview;@override $SnWebFeedConfigCopyWith<$Res> get config;
}
/// @nodoc
class __$SnWebFeedCopyWithImpl<$Res>
implements _$SnWebFeedCopyWith<$Res> {
__$SnWebFeedCopyWithImpl(this._self, this._then);
final _SnWebFeed _self;
final $Res Function(_SnWebFeed) _then;
/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? url = null,Object? title = null,Object? description = freezed,Object? preview = freezed,Object? config = null,Object? publisherId = null,Object? articles = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnWebFeed(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable
as SnScrappedLink?,config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable
as SnWebFeedConfig,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,articles: null == articles ? _self._articles : articles // ignore: cast_nullable_to_non_nullable
as List<SnWebArticle>,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?,
));
}
/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnScrappedLinkCopyWith<$Res>? get preview {
if (_self.preview == null) {
return null;
}
return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) {
return _then(_self.copyWith(preview: value));
});
}/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWebFeedConfigCopyWith<$Res> get config {
return $SnWebFeedConfigCopyWith<$Res>(_self.config, (value) {
return _then(_self.copyWith(config: value));
});
}
}
/// @nodoc
mixin _$SnWebArticle {
String get id; String get title; String get url; String? get author; Map<String, dynamic>? get meta; SnScrappedLink? get preview; SnWebFeed? get feed; String? get content; DateTime? get publishedAt; String get feedId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnWebArticleCopyWith<SnWebArticle> get copyWith => _$SnWebArticleCopyWithImpl<SnWebArticle>(this as SnWebArticle, _$identity);
/// Serializes this SnWebArticle to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWebArticle&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.url, url) || other.url == url)&&(identical(other.author, author) || other.author == author)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.feed, feed) || other.feed == feed)&&(identical(other.content, content) || other.content == content)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.feedId, feedId) || other.feedId == feedId)&&(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,title,url,author,const DeepCollectionEquality().hash(meta),preview,feed,content,publishedAt,feedId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnWebArticle(id: $id, title: $title, url: $url, author: $author, meta: $meta, preview: $preview, feed: $feed, content: $content, publishedAt: $publishedAt, feedId: $feedId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnWebArticleCopyWith<$Res> {
factory $SnWebArticleCopyWith(SnWebArticle value, $Res Function(SnWebArticle) _then) = _$SnWebArticleCopyWithImpl;
@useResult
$Res call({
String id, String title, String url, String? author, Map<String, dynamic>? meta, SnScrappedLink? preview, SnWebFeed? feed, String? content, DateTime? publishedAt, String feedId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnScrappedLinkCopyWith<$Res>? get preview;$SnWebFeedCopyWith<$Res>? get feed;
}
/// @nodoc
class _$SnWebArticleCopyWithImpl<$Res>
implements $SnWebArticleCopyWith<$Res> {
_$SnWebArticleCopyWithImpl(this._self, this._then);
final SnWebArticle _self;
final $Res Function(SnWebArticle) _then;
/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = null,Object? url = null,Object? author = freezed,Object? meta = freezed,Object? preview = freezed,Object? feed = freezed,Object? content = freezed,Object? publishedAt = freezed,Object? feedId = 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,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
as String?,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable
as SnScrappedLink?,feed: freezed == feed ? _self.feed : feed // ignore: cast_nullable_to_non_nullable
as SnWebFeed?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,feedId: null == feedId ? _self.feedId : feedId // 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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnScrappedLinkCopyWith<$Res>? get preview {
if (_self.preview == null) {
return null;
}
return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) {
return _then(_self.copyWith(preview: value));
});
}/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWebFeedCopyWith<$Res>? get feed {
if (_self.feed == null) {
return null;
}
return $SnWebFeedCopyWith<$Res>(_self.feed!, (value) {
return _then(_self.copyWith(feed: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnWebArticle implements SnWebArticle {
const _SnWebArticle({required this.id, required this.title, required this.url, this.author, final Map<String, dynamic>? meta, this.preview, this.feed, this.content, this.publishedAt, required this.feedId, required this.createdAt, required this.updatedAt, this.deletedAt}): _meta = meta;
factory _SnWebArticle.fromJson(Map<String, dynamic> json) => _$SnWebArticleFromJson(json);
@override final String id;
@override final String title;
@override final String url;
@override final String? author;
final Map<String, dynamic>? _meta;
@override Map<String, dynamic>? get meta {
final value = _meta;
if (value == null) return null;
if (_meta is EqualUnmodifiableMapView) return _meta;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override final SnScrappedLink? preview;
@override final SnWebFeed? feed;
@override final String? content;
@override final DateTime? publishedAt;
@override final String feedId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnWebArticleCopyWith<_SnWebArticle> get copyWith => __$SnWebArticleCopyWithImpl<_SnWebArticle>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnWebArticleToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWebArticle&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.url, url) || other.url == url)&&(identical(other.author, author) || other.author == author)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.feed, feed) || other.feed == feed)&&(identical(other.content, content) || other.content == content)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.feedId, feedId) || other.feedId == feedId)&&(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,title,url,author,const DeepCollectionEquality().hash(_meta),preview,feed,content,publishedAt,feedId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnWebArticle(id: $id, title: $title, url: $url, author: $author, meta: $meta, preview: $preview, feed: $feed, content: $content, publishedAt: $publishedAt, feedId: $feedId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnWebArticleCopyWith<$Res> implements $SnWebArticleCopyWith<$Res> {
factory _$SnWebArticleCopyWith(_SnWebArticle value, $Res Function(_SnWebArticle) _then) = __$SnWebArticleCopyWithImpl;
@override @useResult
$Res call({
String id, String title, String url, String? author, Map<String, dynamic>? meta, SnScrappedLink? preview, SnWebFeed? feed, String? content, DateTime? publishedAt, String feedId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnScrappedLinkCopyWith<$Res>? get preview;@override $SnWebFeedCopyWith<$Res>? get feed;
}
/// @nodoc
class __$SnWebArticleCopyWithImpl<$Res>
implements _$SnWebArticleCopyWith<$Res> {
__$SnWebArticleCopyWithImpl(this._self, this._then);
final _SnWebArticle _self;
final $Res Function(_SnWebArticle) _then;
/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = null,Object? url = null,Object? author = freezed,Object? meta = freezed,Object? preview = freezed,Object? feed = freezed,Object? content = freezed,Object? publishedAt = freezed,Object? feedId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnWebArticle(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
as String?,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable
as SnScrappedLink?,feed: freezed == feed ? _self.feed : feed // ignore: cast_nullable_to_non_nullable
as SnWebFeed?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,feedId: null == feedId ? _self.feedId : feedId // 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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnScrappedLinkCopyWith<$Res>? get preview {
if (_self.preview == null) {
return null;
}
return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) {
return _then(_self.copyWith(preview: value));
});
}/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWebFeedCopyWith<$Res>? get feed {
if (_self.feed == null) {
return null;
}
return $SnWebFeedCopyWith<$Res>(_self.feed!, (value) {
return _then(_self.copyWith(feed: value));
});
}
}
// dart format on

103
lib/models/webfeed.g.dart Normal file
View File

@ -0,0 +1,103 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'webfeed.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnWebFeedConfig _$SnWebFeedConfigFromJson(Map<String, dynamic> json) =>
_SnWebFeedConfig(scrapPage: json['scrap_page'] as bool? ?? false);
Map<String, dynamic> _$SnWebFeedConfigToJson(_SnWebFeedConfig instance) =>
<String, dynamic>{'scrap_page': instance.scrapPage};
_SnWebFeed _$SnWebFeedFromJson(Map<String, dynamic> json) => _SnWebFeed(
id: json['id'] as String,
url: json['url'] as String,
title: json['title'] as String,
description: json['description'] as String?,
preview:
json['preview'] == null
? null
: SnScrappedLink.fromJson(json['preview'] as Map<String, dynamic>),
config:
json['config'] == null
? const SnWebFeedConfig()
: SnWebFeedConfig.fromJson(json['config'] as Map<String, dynamic>),
publisherId: json['publisher_id'] as String,
articles:
(json['articles'] as List<dynamic>?)
?.map((e) => SnWebArticle.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
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> _$SnWebFeedToJson(_SnWebFeed instance) =>
<String, dynamic>{
'id': instance.id,
'url': instance.url,
'title': instance.title,
'description': instance.description,
'preview': instance.preview?.toJson(),
'config': instance.config.toJson(),
'publisher_id': instance.publisherId,
'articles': instance.articles.map((e) => e.toJson()).toList(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_SnWebArticle _$SnWebArticleFromJson(Map<String, dynamic> json) =>
_SnWebArticle(
id: json['id'] as String,
title: json['title'] as String,
url: json['url'] as String,
author: json['author'] as String?,
meta: json['meta'] as Map<String, dynamic>?,
preview:
json['preview'] == null
? null
: SnScrappedLink.fromJson(
json['preview'] as Map<String, dynamic>,
),
feed:
json['feed'] == null
? null
: SnWebFeed.fromJson(json['feed'] as Map<String, dynamic>),
content: json['content'] as String?,
publishedAt:
json['published_at'] == null
? null
: DateTime.parse(json['published_at'] as String),
feedId: json['feed_id'] 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> _$SnWebArticleToJson(_SnWebArticle instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'url': instance.url,
'author': instance.author,
'meta': instance.meta,
'preview': instance.preview?.toJson(),
'feed': instance.feed?.toJson(),
'content': instance.content,
'published_at': instance.publishedAt?.toIso8601String(),
'feed_id': instance.feedId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@ -0,0 +1,31 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/network.dart';
/// Provider that fetches a single article by its ID
final articleDetailProvider = FutureProvider.autoDispose.family<SnWebArticle, String>(
(ref, articleId) async {
final dio = ref.watch(apiClientProvider);
try {
final response = await dio.get<Map<String, dynamic>>(
'/feeds/articles/$articleId',
);
if (response.statusCode == 200 && response.data != null) {
return SnWebArticle.fromJson(response.data!);
} else {
throw Exception('Failed to load article');
}
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw Exception('Article not found');
} else {
throw Exception('Failed to load article: ${e.message}');
}
} catch (e) {
throw Exception('Failed to load article: $e');
}
},
);

View File

@ -0,0 +1 @@

View File

@ -11,11 +11,6 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
UserInfoNotifier(this._ref) : super(const AsyncValue.data(null));
Future<String?> getAccessToken() async {
final prefs = _ref.read(sharedPreferencesProvider);
return prefs.getString(kTokenPairStoreKey);
}
Future<void> fetchUser() async {
try {
final client = _ref.read(apiClientProvider);
@ -23,8 +18,13 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
final user = SnAccount.fromJson(response.data);
state = AsyncValue.data(user);
} catch (error, stackTrace) {
log("[UserInfo] Failed to fetch user info: $error");
state = AsyncValue.error(error, stackTrace);
log(
"[UserInfo] Failed to fetch user info...",
name: 'UserInfoNotifier',
error: error,
stackTrace: stackTrace,
);
state = AsyncValue.data(null);
}
}

123
lib/pods/webfeed.dart Normal file
View File

@ -0,0 +1,123 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/network.dart';
final webFeedListProvider = FutureProvider.family<List<SnWebFeed>, String>((
ref,
pubName,
) async {
final client = ref.watch(apiClientProvider);
final response = await client.get('/publishers/$pubName/feeds');
return (response.data as List)
.map((json) => SnWebFeed.fromJson(json))
.toList();
});
class WebFeedNotifier
extends
AutoDisposeFamilyAsyncNotifier<
SnWebFeed,
({String pubName, String? feedId})
> {
@override
FutureOr<SnWebFeed> build(({String pubName, String? feedId}) arg) async {
if (arg.feedId == null || arg.feedId!.isEmpty) {
return SnWebFeed(
id: '',
url: '',
title: '',
publisherId: arg.pubName,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
);
}
try {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/publishers/${arg.pubName}/feeds/${arg.feedId}',
);
return SnWebFeed.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<void> saveFeed(SnWebFeed feed) async {
state = const AsyncValue.loading();
try {
final client = ref.read(apiClientProvider);
final url = '/publishers/${feed.publisherId}/feeds';
final response =
feed.id.isEmpty
? await client.post(url, data: feed.toJson())
: await client.patch('$url/${feed.id}', data: feed.toJson());
state = AsyncValue.data(SnWebFeed.fromJson(response.data));
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
rethrow;
}
}
Future<void> deleteFeed() async {
final feedId = arg.feedId;
if (feedId == null || feedId.isEmpty) return;
state = const AsyncValue.loading();
try {
final client = ref.read(apiClientProvider);
await client.delete('/publishers/${arg.pubName}/feeds/$feedId');
state = AsyncValue.data(
SnWebFeed(
id: '',
url: '',
title: '',
publisherId: arg.pubName,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
),
);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
rethrow;
}
}
Future<void> scrapFeed() async {
final feedId = arg.feedId;
if (feedId == null || feedId.isEmpty) return;
state = const AsyncValue.loading();
try {
final client = ref.read(apiClientProvider);
await client.post(
'/publishers/${arg.pubName}/feeds/$feedId/scrap',
options: Options(
sendTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 180),
),
);
// Reload the feed
final response = await client.get(
'/publishers/${arg.pubName}/feeds/$feedId',
);
state = AsyncValue.data(SnWebFeed.fromJson(response.data));
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
rethrow;
}
}
}
final webFeedNotifierProvider = AsyncNotifierProvider.autoDispose
.family<WebFeedNotifier, SnWebFeed, ({String pubName, String? feedId})>(
WebFeedNotifier.new,
);

View File

@ -1,10 +1,17 @@
import 'package:flutter/material.dart';
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/developers/edit_app.dart';
import 'package:island/screens/developers/new_app.dart';
import 'package:island/screens/developers/hub.dart';
import 'package:island/screens/discovery/articles.dart';
import 'package:island/screens/posts/post_search.dart';
import 'package:island/widgets/app_wrapper.dart';
import 'package:island/screens/tabs.dart';
import 'package:island/screens/explore.dart';
import 'package:island/screens/discovery/article_detail.dart';
import 'package:island/screens/account.dart';
import 'package:island/screens/notification.dart';
import 'package:island/screens/wallet.dart';
@ -18,20 +25,24 @@ 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/creators/hub.dart';
import 'package:island/screens/creators/posts/list.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/creators/publishers.dart';
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
import 'package:island/screens/creators/webfeed/webfeed_edit.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/screens/posts/detail.dart';
import 'package:island/screens/posts/post_detail.dart';
import 'package:island/screens/posts/pub_profile.dart';
import 'package:island/screens/auth/login.dart';
import 'package:island/screens/auth/create_account.dart';
import 'package:island/screens/settings.dart';
import 'package:island/screens/realm/realms.dart';
import 'package:island/screens/realm/detail.dart';
import 'package:island/screens/realm/realm_detail.dart';
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';
// Shell route keys for nested navigation
final rootNavigatorKey = GlobalKey<NavigatorState>();
@ -56,6 +67,9 @@ final routerProvider = Provider<GoRouter>((ref) {
builder:
(context, state) => PostComposeScreen(
initialState: state.extra as PostComposeInitialState?,
type:
int.tryParse(state.uri.queryParameters['type'] ?? '0') ??
0,
),
),
GoRoute(
@ -79,33 +93,64 @@ final routerProvider = Provider<GoRouter>((ref) {
return EventCalanderScreen(name: name);
},
),
GoRoute(
path: '/creators',
builder: (context, state) => const CreatorHubScreen(),
ShellRoute(
builder:
(context, state, child) => CreatorHubShellScreen(child: child),
routes: [
GoRoute(
path: ':name/posts',
path: '/creators',
builder: (context, state) => const CreatorHubScreen(),
),
// Web Feed Routes
GoRoute(
path: '/creators/:name/feeds',
builder: (context, state) {
final name = state.pathParameters['name']!;
return WebFeedListScreen(pubName: name);
},
routes: [
GoRoute(
path: 'new',
builder: (context, state) {
return WebFeedNewScreen(
pubName: state.pathParameters['name']!,
);
},
),
GoRoute(
path: ':feedId',
builder: (context, state) {
return WebFeedEditScreen(
pubName: state.pathParameters['name']!,
feedId: state.pathParameters['feedId'],
);
},
),
],
),
GoRoute(
path: '/creators/:name/posts',
builder: (context, state) {
final name = state.pathParameters['name']!;
return CreatorPostListScreen(pubName: name);
},
),
GoRoute(
path: ':name/stickers',
path: '/creators/:name/stickers',
builder: (context, state) {
final name = state.pathParameters['name']!;
return StickersScreen(pubName: name);
},
),
GoRoute(
path: ':name/stickers/new',
path: '/creators/:name/stickers/new',
builder: (context, state) {
final name = state.pathParameters['name']!;
return NewStickerPacksScreen(pubName: name);
},
),
GoRoute(
path: ':name/stickers/:packId/edit',
path: '/creators/:name/stickers/:packId/edit',
builder: (context, state) {
final name = state.pathParameters['name']!;
final packId = state.pathParameters['packId']!;
@ -113,7 +158,7 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
GoRoute(
path: ':name/stickers/:packId',
path: '/creators/:name/stickers/:packId',
builder: (context, state) {
final name = state.pathParameters['name']!;
final packId = state.pathParameters['packId']!;
@ -121,14 +166,14 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
GoRoute(
path: ':name/stickers/:packId/new',
path: '/creators/:name/stickers/:packId/new',
builder: (context, state) {
final packId = state.pathParameters['packId']!;
return NewStickersScreen(packId: packId);
},
),
GoRoute(
path: ':name/stickers/:packId/:id/edit',
path: '/creators/:name/stickers/:packId/:id/edit',
builder: (context, state) {
final packId = state.pathParameters['packId']!;
final id = state.pathParameters['id']!;
@ -136,11 +181,11 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
GoRoute(
path: 'new',
path: '/creators/new',
builder: (context, state) => const NewPublisherScreen(),
),
GoRoute(
path: ':name/edit',
path: '/creators/:name/edit',
builder: (context, state) {
final name = state.pathParameters['name']!;
return EditPublisherScreen(name: name);
@ -148,6 +193,52 @@ final routerProvider = Provider<GoRouter>((ref) {
),
],
),
ShellRoute(
builder:
(context, state, child) =>
DeveloperHubShellScreen(child: child),
routes: [
GoRoute(
path: '/developers',
builder: (context, state) => const DeveloperHubScreen(),
),
GoRoute(
path: '/developers/:name/apps',
builder:
(context, state) => CustomAppsScreen(
publisherName: state.pathParameters['name']!,
),
),
GoRoute(
path: '/developers/:name/apps/new',
builder:
(context, state) => NewCustomAppScreen(
publisherName: state.pathParameters['name']!,
),
),
GoRoute(
path: '/developers/:name/apps/:id',
builder:
(context, state) => EditAppScreen(
publisherName: state.pathParameters['name']!,
id: state.pathParameters['id']!,
),
),
],
),
// Web articles
GoRoute(
path: '/feeds/articles',
builder: (context, state) => const ArticlesScreen(),
),
GoRoute(
path: '/feeds/articles/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ArticleDetailScreen(articleId: id);
},
),
// Auth routes
GoRoute(
@ -164,6 +255,23 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/settings',
builder: (context, state) => const SettingsScreen(),
),
GoRoute(
path: '/about',
builder: (context, state) => const AboutScreen(),
),
GoRoute(
path: '/safety/reports/me',
builder: (context, state) => const AbuseReportListScreen(),
),
GoRoute(
path: '/safety/reports/me/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return AbuseReportDetailScreen(reportId: id);
},
),
// Main tabs with TabsScreen shell
ShellRoute(
@ -173,56 +281,68 @@ final routerProvider = Provider<GoRouter>((ref) {
},
routes: [
// Explore tab
GoRoute(
path: '/',
builder: (context, state) => const ExploreScreen(),
ShellRoute(
builder:
(context, state, child) => ExploreShellScreen(child: child),
routes: [
GoRoute(
path: 'posts/:id',
path: '/',
builder: (context, state) => const ExploreScreen(),
),
GoRoute(
path: '/posts/search',
builder: (context, state) => const PostSearchScreen(),
),
GoRoute(
path: '/posts/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return PostDetailScreen(id: id);
},
),
GoRoute(
path: 'publishers/:name',
path: '/publishers/:name',
builder: (context, state) {
final name = state.pathParameters['name']!;
return PublisherProfileScreen(name: name);
},
),
GoRoute(
path: 'discovery/realms',
path: '/discovery/realms',
builder: (context, state) => const DiscoveryRealmsScreen(),
),
],
),
// Chat tab
GoRoute(
path: '/chat',
builder: (context, state) => const ChatListScreen(),
ShellRoute(
builder:
(context, state, child) => ChatShellScreen(child: child),
routes: [
GoRoute(
path: 'new',
path: '/chat',
builder: (context, state) => const ChatListScreen(),
),
GoRoute(
path: '/chat/new',
builder: (context, state) => const NewChatScreen(),
),
GoRoute(
path: ':id',
path: '/chat/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ChatRoomScreen(id: id);
},
),
GoRoute(
path: ':id/edit',
path: '/chat/:id/edit',
builder: (context, state) {
final id = state.pathParameters['id']!;
return EditChatScreen(id: id);
},
),
GoRoute(
path: ':id/detail',
path: '/chat/:id/detail',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ChatDetailScreen(id: id);
@ -258,39 +378,43 @@ final routerProvider = Provider<GoRouter>((ref) {
),
// Account tab
GoRoute(
path: '/account',
builder: (context, state) => const AccountScreen(),
ShellRoute(
builder:
(context, state, child) => AccountShellScreen(child: child),
routes: [
GoRoute(
path: 'notifications',
path: '/account',
builder: (context, state) => const AccountScreen(),
),
GoRoute(
path: '/account/notifications',
builder: (context, state) => const NotificationScreen(),
),
GoRoute(
path: 'wallet',
path: '/account/wallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: 'relationships',
path: '/account/relationships',
builder: (context, state) => const RelationshipScreen(),
),
GoRoute(
path: ':name',
path: '/account/:name',
builder: (context, state) {
final name = state.pathParameters['name']!;
return AccountProfileScreen(name: name);
},
),
GoRoute(
path: 'me/update',
path: '/account/me/update',
builder: (context, state) => const UpdateProfileScreen(),
),
GoRoute(
path: 'me/leveling',
path: '/account/me/leveling',
builder: (context, state) => const LevelingScreen(),
),
GoRoute(
path: 'settings',
path: '/account/me/settings',
builder: (context, state) => const AccountSettingsScreen(),
),
],

399
lib/screens/about.dart Normal file
View File

@ -0,0 +1,399 @@
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/notify.dart';
import 'package:island/services/udid.native.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:easy_localization/easy_localization.dart';
class AboutScreen extends ConsumerStatefulWidget {
const AboutScreen({super.key});
@override
ConsumerState<AboutScreen> createState() => _AboutScreenState();
}
class _AboutScreenState extends ConsumerState<AboutScreen> {
PackageInfo _packageInfo = PackageInfo(
appName: 'Solian',
packageName: 'dev.solsynth.solian',
version: '1.0.0',
buildNumber: '1',
);
BaseDeviceInfo? _deviceInfo;
String? _deviceUdid;
bool _isLoading = true;
String? _errorMessage;
@override
void initState() {
super.initState();
_initPackageInfo();
_initDeviceInfo();
}
Future<void> _initPackageInfo() async {
try {
final info = await PackageInfo.fromPlatform();
if (mounted) {
setState(() {
_packageInfo = info;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = 'aboutScreenFailedToLoadPackageInfo'.tr(
args: [e.toString()],
);
_isLoading = false;
});
}
}
}
Future<void> _initDeviceInfo() async {
try {
final deviceInfoPlugin = DeviceInfoPlugin();
_deviceInfo = await deviceInfoPlugin.deviceInfo;
_deviceUdid = await getUdid();
if (mounted) {
setState(() {});
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = 'aboutScreenFailedToLoadDeviceInfo'.tr(
args: [e.toString()],
);
});
}
}
}
Future<void> _launchURL(String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AppScaffold(
appBar: AppBar(title: Text('about'.tr()), elevation: 0),
body:
_isLoading
? const Center(child: CircularProgressIndicator())
: _errorMessage != null
? Center(child: Text(_errorMessage!))
: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 24),
// App Icon and Name
CircleAvatar(
radius: 50,
backgroundColor: theme.colorScheme.primary.withOpacity(
0.1,
),
child: Image.asset(
'assets/icons/icon.png',
width: 56,
height: 56,
),
),
const SizedBox(height: 16),
Text(
_packageInfo.appName,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'aboutScreenVersionInfo'.tr(
args: [_packageInfo.version, _packageInfo.buildNumber],
),
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.textTheme.bodySmall?.color,
),
),
const SizedBox(height: 32),
// App Info Card
_buildSection(
context,
title: 'aboutScreenAppInfoSectionTitle'.tr(),
children: [
_buildInfoItem(
context,
icon: Symbols.info,
label: 'aboutScreenPackageNameLabel'.tr(),
value: _packageInfo.packageName,
),
_buildInfoItem(
context,
icon: Symbols.update,
label: 'aboutScreenVersionLabel'.tr(),
value: _packageInfo.version,
),
_buildInfoItem(
context,
icon: Symbols.build,
label: 'aboutScreenBuildNumberLabel'.tr(),
value: _packageInfo.buildNumber,
),
],
),
if (_deviceInfo != null) const SizedBox(height: 16),
if (_deviceInfo != null)
_buildSection(
context,
title: 'Device Information',
children: [
_buildInfoItem(
context,
icon: Symbols.label,
label: 'Device Name',
value: _deviceInfo?.data['name'],
),
_buildInfoItem(
context,
icon: Symbols.fingerprint,
label: 'Device Identifier',
value: _deviceUdid ?? 'N/A',
copyable: true,
),
const Divider(height: 1),
_buildListTile(
context,
icon: Symbols.notifications_active,
title: 'Reactivate Push Notifications',
onTap: () async {
showLoadingModal(context);
try {
await subscribePushNotification(
ref.watch(apiClientProvider),
);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
},
),
],
),
const SizedBox(height: 16),
// Links Card
_buildSection(
context,
title: 'aboutScreenLinksSectionTitle'.tr(),
children: [
_buildListTile(
context,
icon: Symbols.privacy_tip,
title: 'aboutScreenPrivacyPolicyTitle'.tr(),
onTap:
() => _launchURL(
'https://solsynth.dev/terms/privacy-policy',
),
),
_buildListTile(
context,
icon: Symbols.description,
title: 'aboutScreenTermsOfServiceTitle'.tr(),
onTap:
() => _launchURL(
'https://solsynth.dev/terms/basic-law',
),
),
_buildListTile(
context,
icon: Symbols.code,
title: 'aboutScreenOpenSourceLicensesTitle'.tr(),
onTap: () {
showLicensePage(
context: context,
applicationName: _packageInfo.appName,
applicationVersion:
'Version ${_packageInfo.version}',
);
},
),
],
),
const SizedBox(height: 16),
// Developer Info
_buildSection(
context,
title: 'aboutScreenDeveloperSectionTitle'.tr(),
children: [
_buildListTile(
context,
icon: Symbols.email,
title: 'aboutScreenContactUsTitle'.tr(),
subtitle: 'lily@solsynth.dev',
onTap: () => _launchURL('mailto:lily@solsynth.dev'),
),
_buildListTile(
context,
icon: Symbols.copyright,
title: 'aboutScreenLicenseTitle'.tr(),
subtitle: 'aboutScreenLicenseContent'.tr(
args: [DateTime.now().year.toString()],
),
onTap:
() => _launchURL(
'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt',
),
),
],
),
const SizedBox(height: 32),
// Copyright
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text(
'aboutScreenCopyright'.tr(
args: [DateTime.now().year.toString()],
),
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
const Gap(1),
Text(
'aboutScreenMadeWith'.tr(),
textAlign: TextAlign.center,
).fontSize(10).opacity(0.8),
],
),
),
Gap(MediaQuery.of(context).padding.bottom + 16),
],
),
),
);
}
Widget _buildSection(
BuildContext context, {
required String title,
required List<Widget> children,
}) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
title,
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
const Divider(height: 1),
...children,
],
),
);
}
Widget _buildInfoItem(
BuildContext context, {
required IconData icon,
required String label,
required String value,
bool copyable = false,
}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(icon, size: 20, color: Theme.of(context).hintColor),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: Theme.of(context).textTheme.bodySmall),
const SizedBox(height: 2),
SelectableText(
value,
style: Theme.of(context).textTheme.bodyMedium,
maxLines: copyable ? 1 : null,
),
],
),
),
if (value.startsWith('http') || value.contains('@') || copyable)
IconButton(
icon: const Icon(Symbols.content_copy, size: 16),
onPressed: () {
Clipboard.setData(ClipboardData(text: value));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('copiedToClipboard'.tr())),
);
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
tooltip: 'copyToClipboardTooltip'.tr(),
),
],
),
);
}
Widget _buildListTile(
BuildContext context, {
required IconData icon,
required String title,
String? subtitle,
required VoidCallback onTap,
}) {
final multipleLines = subtitle?.contains('\n') ?? false;
return Column(
children: [
ListTile(
leading: Icon(icon).padding(top: multipleLines ? 8 : 0),
title: Text(title),
subtitle: subtitle != null ? Text(subtitle) : null,
isThreeLine: multipleLines,
trailing: const Icon(
Symbols.chevron_right,
).padding(top: multipleLines ? 8 : 0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
onTap: onTap,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
minLeadingWidth: 24,
),
],
);
}
}

View File

@ -59,7 +59,7 @@ class AccountScreen extends HookConsumerWidget {
notificationUnreadCountNotifierProvider,
);
if (!user.hasValue || user.value == null) {
if (user.value == null || user.value == null) {
return _UnauthorizedAccountScreen();
}
@ -143,7 +143,7 @@ class AccountScreen extends HookConsumerWidget {
progress: user.value!.profile.levelingProgress,
),
onTap: () {
context.push('/account/leveling');
context.push('/account/me/leveling');
},
).padding(horizontal: 12),
Row(
@ -178,7 +178,9 @@ class AccountScreen extends HookConsumerWidget {
Text('developerPortalDescription').tr(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {},
onTap: () {
context.push('/developers');
},
),
).height(140),
),
@ -210,7 +212,7 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('wallet').tr(),
onTap: () {
context.push('/wallet');
context.push('/account/wallet');
},
),
ListTile(
@ -220,9 +222,17 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('relationships').tr(),
onTap: () {
context.push('/account/relationship');
context.push('/account/relationships');
},
),
ListTile(
minTileHeight: 48,
title: Text('abuseReports').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.gavel),
trailing: const Icon(Symbols.chevron_right),
onTap: () => context.push('/safety/reports/me'),
),
const Divider(height: 1).padding(vertical: 8),
ListTile(
minTileHeight: 48,
@ -279,6 +289,16 @@ class AccountScreen extends HookConsumerWidget {
},
),
const Divider(height: 1).padding(vertical: 8),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.info),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('about').tr(),
onTap: () {
context.push('/about');
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.logout),
@ -316,7 +336,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
child: Card(
child: InkWell(
onTap: () {
context.push('/auth/create');
context.push('/auth/create-account');
},
child: Padding(
padding: const EdgeInsets.all(16),
@ -355,12 +375,23 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
),
),
const Gap(8),
TextButton(
onPressed: () {
context.push('/settings');
},
child: Text('appSettings').tr(),
).center(),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () {
context.push('/about');
},
child: Text('about').tr(),
),
TextButton(
onPressed: () {
context.push('/settings');
},
child: Text('appSettings').tr(),
),
],
),
],
),
).center(),

View File

@ -82,7 +82,7 @@ class EventCalanderScreen extends HookConsumerWidget {
),
// Show user profile if viewing someone else's calendar
if (name != 'me' && user.hasValue)
if (name != 'me' && user.value != null)
AccountNameplate(name: name),
],
),
@ -106,7 +106,7 @@ class EventCalanderScreen extends HookConsumerWidget {
).padding(horizontal: 8, vertical: 4),
// Show user profile if viewing someone else's calendar
if (name != 'me' && user.hasValue)
if (name != 'me' && user.value != null)
AccountNameplate(name: name),
Gap(MediaQuery.of(context).padding.bottom + 16),
],

View File

@ -1,4 +1,7 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
@ -14,7 +17,9 @@ import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
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:styled_widget/styled_widget.dart';
part 'leveling.g.dart';
@ -84,35 +89,6 @@ class LevelingScreen extends HookConsumerWidget {
// Membership section
_buildMembershipSection(context, ref, stellarSubscription),
const Gap(16),
// Unlocked features section
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'unlockedFeatures'.tr(),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const Gap(8),
Text(
'unlockedFeaturesDescription'.tr(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
),
@ -292,6 +268,31 @@ class LevelingScreen extends HookConsumerWidget {
) {
final isActive = membership?.isActive ?? false;
Future<void> membershipCancel() async {
if (!isActive || membership == null) return;
final confirm = await showConfirmAlert(
'membershipCancelHint'.tr(),
'membershipCancelConfirm'.tr(),
);
if (!confirm || !context.mounted) return;
try {
showLoadingModal(context);
final client = ref.watch(apiClientProvider);
await client.post('/subscriptions/${membership.identifier}/cancel');
ref.invalidate(accountStellarSubscriptionProvider);
ref.read(userInfoProvider.notifier).fetchUser();
if (context.mounted) {
hideLoadingModal(context);
showSnackBar('membershipCancelSuccess'.tr());
}
} catch (err) {
if (context.mounted) hideLoadingModal(context);
showErrorAlert(err);
}
}
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
@ -307,7 +308,7 @@ class LevelingScreen extends HookConsumerWidget {
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
@ -327,27 +328,42 @@ class LevelingScreen extends HookConsumerWidget {
if (isActive) ...[
_buildCurrentMembershipCard(context, membership!),
const Gap(16),
const Gap(12),
FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Theme.of(context).colorScheme.error,
),
foregroundColor: WidgetStateProperty.all(
Theme.of(context).colorScheme.onError,
),
),
onPressed: membershipCancel,
icon: const Icon(Symbols.cancel),
label: Text('membershipCancel'.tr()),
),
],
Text(
isActive ? 'upgradeYourPlan'.tr() : 'chooseYourPlan'.tr(),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const Gap(12),
_buildMembershipTiers(context, ref, membership),
const Gap(12),
if (!isActive) ...[
Text(
'chooseYourPlan'.tr(),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const Gap(12),
_buildMembershipTiers(context, ref, membership),
],
// Restore Purchase Button
OutlinedButton.icon(
onPressed: () => _showRestorePurchaseSheet(context, ref),
icon: const Icon(Icons.restore),
label: Text('restorePurchase'.tr()),
style: OutlinedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
),
),
// As you know Apple platform need IAP
if (kIsWeb || !(Platform.isIOS || Platform.isMacOS))
OutlinedButton.icon(
onPressed: () => _showRestorePurchaseSheet(context, ref),
icon: const Icon(Icons.restore),
label: Text('restorePurchase'.tr()),
style: OutlinedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
),
).padding(top: 12),
],
),
);
@ -410,33 +426,18 @@ class LevelingScreen extends HookConsumerWidget {
'id': 'solian.stellar.primary',
'name': 'membershipTierStellar'.tr(),
'price': 'membershipPriceStellar'.tr(),
'features': [
'membershipFeatureBasic'.tr(),
'membershipFeaturePrioritySupport'.tr(),
'membershipFeatureAdFree'.tr(),
],
'color': Colors.blue,
},
{
'id': 'solian.stellar.nova',
'name': 'membershipTierNova'.tr(),
'price': 'membershipPriceNova'.tr(),
'features': [
'membershipFeatureAllPrimary'.tr(),
'membershipFeatureAdvancedCustomization'.tr(),
'membershipFeatureEarlyAccess'.tr(),
],
'color': Colors.purple,
'color': Colors.indigo,
},
{
'id': 'solian.stellar.supernova',
'name': 'membershipTierSupernova'.tr(),
'price': 'membershipPriceSupernova'.tr(),
'features': [
'membershipFeatureAllNova'.tr(),
'membershipFeatureExclusiveContent'.tr(),
'membershipFeatureVipSupport'.tr(),
],
'color': Colors.orange,
},
];

View File

@ -341,7 +341,10 @@ class UpdateProfileScreen extends HookConsumerWidget {
),
TextFormField(
decoration: InputDecoration(labelText: 'bio'.tr()),
decoration: InputDecoration(
labelText: 'bio'.tr(),
alignLabelWithHint: true,
),
maxLines: null,
minLines: 3,
controller: bioController,

View File

@ -22,6 +22,7 @@ import 'package:island/widgets/account/status.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/safety/abuse_report_helper.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -53,21 +54,27 @@ Future<List<SnAccountBadge>> accountBadges(Ref ref, String uname) async {
@riverpod
Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
final account = await ref.watch(accountProvider(uname).future);
if (account.profile.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider(
CloudImageWidget.provider(
fileId: account.profile.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
final dominantColor = palette.dominantColor?.color;
if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
try {
final account = await ref.watch(accountProvider(uname).future);
if (account.profile.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider(
CloudImageWidget.provider(
fileId: account.profile.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
final dominantColor = palette.dominantColor?.color;
if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
} catch (_) {
return null;
}
}
@riverpod
Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async {
final userInfo = ref.watch(userInfoProvider);
if (userInfo.value == null) return null;
final account = await ref.watch(accountProvider(uname).future);
final apiClient = ref.watch(apiClientProvider);
try {
@ -83,6 +90,8 @@ Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async {
@riverpod
Future<SnRelationship?> accountRelationship(Ref ref, String uname) async {
final userInfo = ref.watch(userInfoProvider);
if (userInfo.value == null) return null;
final account = await ref.watch(accountProvider(uname).future);
final apiClient = ref.watch(apiClientProvider);
try {
@ -135,6 +144,23 @@ class AccountProfileScreen extends HookConsumerWidget {
}
}
Future<void> blockAction() async {
showLoadingModal(context);
try {
final client = ref.watch(apiClientProvider);
if (accountRelationship.value == null) {
await client.post('/relationships/${account.value!.id}/block');
} else {
await client.delete('/relationships/${account.value!.id}/block');
}
ref.invalidate(accountRelationshipProvider(name));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> directMessageAction() async {
if (!account.hasValue) return;
if (accountChat.value != null) {
@ -215,6 +241,8 @@ class AccountProfileScreen extends HookConsumerWidget {
];
}
final user = ref.watch(userInfoProvider);
return account.when(
data:
(data) => AppScaffold(
@ -375,40 +403,86 @@ class AccountProfileScreen extends HookConsumerWidget {
).padding(horizontal: 24),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(top: 24, bottom: 12),
),
if (user.value != null)
SliverToBoxAdapter(
child: const Divider(
height: 1,
).padding(top: 24, bottom: 12),
),
if (user.value != null)
SliverToBoxAdapter(
child: Row(
spacing: 8,
children: [
if (accountRelationship.value == null ||
accountRelationship.value!.status > -100)
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(
context,
).colorScheme.onSecondary,
),
),
onPressed: relationshipAction,
label:
Text(
accountRelationship.value == null
? 'addFriendShort'
: 'added',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.person_add)
: const Icon(Symbols.person_check),
),
),
if (accountRelationship.value == null ||
accountRelationship.value!.status <= -100)
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(
context,
).colorScheme.onSecondary,
),
),
onPressed: blockAction,
label:
Text(
accountRelationship.value == null
? 'blockUser'
: 'unblockUser',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.block)
: const Icon(Symbols.person_cancel),
),
),
],
).padding(horizontal: 16),
),
SliverToBoxAdapter(
child: Row(
spacing: 8,
children: [
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.onSecondary,
),
),
onPressed: relationshipAction,
label:
Text(
accountRelationship.value == null
? 'addFriendShort'
: 'added',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.person_add)
: const Icon(Symbols.person_check),
),
),
Expanded(
child: FilledButton.icon(
onPressed: directMessageAction,
@ -422,8 +496,25 @@ class AccountProfileScreen extends HookConsumerWidget {
).tr(),
),
),
IconButton.filled(
onPressed: () {
showAbuseReportSheet(
context,
resourceIdentifier: 'account/${data.id}',
);
},
icon: Icon(
Symbols.flag,
color: Theme.of(context).colorScheme.onError,
),
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
Theme.of(context).colorScheme.error,
),
),
),
],
).padding(horizontal: 16),
).padding(horizontal: 16, top: 4),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(top: 12),

View File

@ -268,7 +268,7 @@ class _AccountBadgesProviderElement
}
String _$accountAppbarForcegroundColorHash() =>
r'f654a7a5594eda1500906e9ad023c22772257a9b';
r'8ee0cae10817b77fb09548a482f5247662b4374c';
/// See also [accountAppbarForcegroundColor].
@ProviderFor(accountAppbarForcegroundColor)
@ -395,7 +395,7 @@ class _AccountAppbarForcegroundColorProviderElement
String get uname => (origin as AccountAppbarForcegroundColorProvider).uname;
}
String _$accountDirectChatHash() => r'60d0015fc2a3c8fc2190bb41d6818cf3027d9d0a';
String _$accountDirectChatHash() => r'3d28c8ba8079159f724fe3cd47bbe00db55cedcc';
/// See also [accountDirectChat].
@ProviderFor(accountDirectChat)
@ -517,7 +517,7 @@ class _AccountDirectChatProviderElement
}
String _$accountRelationshipHash() =>
r'cb7d0d3f8cd4f23ad9d2d529872c540dac483d4f';
r'0be2420e1f6a65b8dcead9617191471924aaf232';
/// See also [accountRelationship].
@ProviderFor(accountRelationship)

View File

@ -669,24 +669,46 @@ class EditChatScreen extends HookConsumerWidget {
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: const InputDecoration(labelText: 'Description'),
decoration: const InputDecoration(
labelText: 'Description',
alignLabelWithHint: true,
),
minLines: 3,
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
CheckboxListTile(
title: const Text('isPublic').tr(),
subtitle: const Text('isPublicHint').tr(),
value: isPublic.value,
onChanged: (value) => isPublic.value = value ?? false,
),
CheckboxListTile(
title: const Text('isCommunity').tr(),
subtitle: const Text('isCommunityHint').tr(),
value: isCommunity.value,
onChanged: (value) => isCommunity.value = value ?? false,
Card(
margin: EdgeInsets.zero,
child: Column(
children: [
CheckboxListTile(
secondary: const Icon(Symbols.public),
title: Text('publicChat').tr(),
subtitle: Text('publicChatDescription').tr(),
value: isPublic.value,
onChanged: (value) {
isPublic.value = value ?? true;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
CheckboxListTile(
secondary: const Icon(Symbols.travel_explore),
title: Text('communityChat').tr(),
subtitle: Text('communityChatDescription').tr(),
value: isCommunity.value,
onChanged: (value) {
isCommunity.value = value ?? false;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
],
),
),
const SizedBox(height: 16),
Align(

View File

@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -6,14 +7,20 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/services/responsive.dart';
import 'package:island/services/text.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.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 'hub.g.dart';
@ -26,6 +33,73 @@ Future<SnPublisherStats?> publisherStats(Ref ref, String? uname) async {
return SnPublisherStats.fromJson(resp.data);
}
@riverpod
Future<SnPublisherMember?> publisherIdentity(Ref ref, String uname) async {
try {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get('/publishers/$uname/members/me');
return SnPublisherMember.fromJson(response.data);
} catch (err) {
if (err is DioException && err.response?.statusCode == 404) {
return null; // No identity found, user is not a member
}
rethrow;
}
}
@riverpod
Future<Map<String, bool>> publisherFeatures(Ref ref, String? uname) async {
if (uname == null) return {};
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get('/publishers/$uname/features');
return Map<String, bool>.from(response.data);
}
@riverpod
Future<List<SnPublisherMember>> publisherInvites(Ref ref) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/publishers/invites');
return resp.data
.map((e) => SnPublisherMember.fromJson(e))
.cast<SnPublisherMember>()
.toList();
}
@riverpod
class PublisherMemberListNotifier extends _$PublisherMemberListNotifier
with CursorPagingNotifierMixin<SnPublisherMember> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnPublisherMember>> build(String uname) async {
return fetch();
}
@override
Future<CursorPagingData<SnPublisherMember>> fetch({String? cursor}) async {
final apiClient = ref.read(apiClientProvider);
final offset = cursor != null ? int.parse(cursor) : 0;
final response = await apiClient.get(
'/publishers/$uname/members',
queryParameters: {'offset': offset, 'take': _pageSize},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final members = data.map((e) => SnPublisherMember.fromJson(e)).toList();
final hasMore = offset + members.length < total;
final nextCursor = hasMore ? (offset + members.length).toString() : null;
return CursorPagingData(
items: members,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
class CreatorHubShellScreen extends StatelessWidget {
final Widget child;
const CreatorHubShellScreen({super.key, required this.child});
@ -34,15 +108,18 @@ class CreatorHubShellScreen extends StatelessWidget {
Widget build(BuildContext context) {
final isWide = isWideScreen(context);
if (isWide) {
return Row(
children: [
SizedBox(width: 360, child: const CreatorHubScreen(isAside: true)),
const VerticalDivider(width: 1),
Expanded(child: child),
],
return AppBackground(
isRoot: true,
child: Row(
children: [
SizedBox(width: 360, child: const CreatorHubScreen(isAside: true)),
const VerticalDivider(width: 1),
Expanded(child: child),
],
),
);
}
return child;
return AppBackground(isRoot: true, child: child);
}
}
@ -58,21 +135,20 @@ class CreatorHubScreen extends HookConsumerWidget {
}
final publishers = ref.watch(publishersManagedProvider);
final publisherInvites = ref.watch(publisherInvitesProvider);
final currentPublisher = useState<SnPublisher?>(
publishers.value?.firstOrNull,
);
void updatePublisher() {
context
.push('/creators/${currentPublisher.value!.name}/edit')
.then((value) async {
if (value == null) return;
final data = await ref.refresh(publishersManagedProvider.future);
currentPublisher.value =
data
.where((e) => e.id == currentPublisher.value!.id)
.firstOrNull;
});
context.push('/creators/${currentPublisher.value!.name}/edit').then((
value,
) async {
if (value == null) return;
final data = await ref.refresh(publishersManagedProvider.future);
currentPublisher.value =
data.where((e) => e.id == currentPublisher.value!.id).firstOrNull;
});
}
void deletePublisher() {
@ -120,12 +196,39 @@ class CreatorHubScreen extends HookConsumerWidget {
publisherStatsProvider(currentPublisher.value?.name),
);
final publisherFeatures = ref.watch(
publisherFeaturesProvider(currentPublisher.value?.name),
);
return AppScaffold(
noBackground: false,
appBar: AppBar(
leading: !isWide ? const PageBackButton() : null,
title: Text('creatorHub').tr(),
actions: [
IconButton(
icon: Badge(
label: Text(
publisherInvites.when(
data: (invites) => invites.length.toString(),
error: (_, _) => '0',
loading: () => '0',
),
),
isLabelVisible: publisherInvites.when(
data: (invites) => invites.isNotEmpty,
error: (_, _) => false,
loading: () => false,
),
child: const Icon(Symbols.email),
),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (_) => const _PublisherInviteSheet(),
);
},
),
DropdownButtonHideUnderline(
child: DropdownButton2<SnPublisher>(
alignment: Alignment.centerRight,
@ -203,7 +306,7 @@ class CreatorHubScreen extends HookConsumerWidget {
...(publishers.value?.map(
(publisher) => ListTile(
leading: ProfilePictureWidget(
fileId: publisher.picture?.id,
file: publisher.picture,
),
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
@ -221,9 +324,7 @@ class CreatorHubScreen extends HookConsumerWidget {
subtitle: Text('createPublisherHint').tr(),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context.push('/creators/publishers/new').then((
value,
) {
context.push('/creators/new').then((value) {
if (value != null) {
ref.invalidate(publishersManagedProvider);
}
@ -266,6 +367,86 @@ class CreatorHubScreen extends HookConsumerWidget {
);
},
),
ListTile(
minTileHeight: 48,
title: Text('publisherMembers').tr(),
trailing: const Icon(Symbols.chevron_right),
leading: const Icon(Symbols.group),
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => _PublisherMemberListSheet(
publisherUname:
currentPublisher.value!.name,
),
);
},
),
ListTile(
minTileHeight: 48,
title: const Text('Web Feeds').tr(),
trailing: const Icon(Symbols.chevron_right),
leading: const Icon(Symbols.rss_feed),
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
),
onTap: () {
context.push(
'/creators/${currentPublisher.value!.name}/feeds',
);
},
),
ExpansionTile(
title: Text('publisherFeatures').tr(),
leading: const Icon(Symbols.flag),
tilePadding: EdgeInsets.symmetric(horizontal: 24),
minTileHeight: 48,
children: [
...publisherFeatures.when(
data: (data) {
return data.entries.map((entry) {
final keyPrefix =
'publisherFeature${entry.key.capitalizeEachWord()}';
return ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
leading: Icon(
Symbols.circle,
color:
entry.value
? Colors.green
: Colors.red,
fill: 1,
size: 16,
).padding(left: 2, top: 4),
title: Text(keyPrefix).tr(),
subtitle: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text('${keyPrefix}Description').tr(),
if (!entry.value)
Text(
'${keyPrefix}Hint',
).tr().bold(),
],
),
isThreeLine: true,
);
}).toList();
},
error: (_, _) => [],
loading: () => [],
),
],
),
Divider(height: 1).padding(vertical: 8),
ListTile(
minTileHeight: 48,
@ -393,3 +574,482 @@ class _PublisherStatsWidget extends StatelessWidget {
);
}
}
class PublisherMemberState {
final List<SnPublisherMember> members;
final bool isLoading;
final int total;
final String? error;
const PublisherMemberState({
required this.members,
required this.isLoading,
required this.total,
this.error,
});
PublisherMemberState copyWith({
List<SnPublisherMember>? members,
bool? isLoading,
int? total,
String? error,
}) {
return PublisherMemberState(
members: members ?? this.members,
isLoading: isLoading ?? this.isLoading,
total: total ?? this.total,
error: error ?? this.error,
);
}
}
final publisherMemberStateProvider = StateNotifierProvider.family<
PublisherMemberNotifier,
PublisherMemberState,
String
>((ref, publisherUname) {
final apiClient = ref.watch(apiClientProvider);
return PublisherMemberNotifier(apiClient, publisherUname);
});
class PublisherMemberNotifier extends StateNotifier<PublisherMemberState> {
final String publisherUname;
final Dio _apiClient;
PublisherMemberNotifier(this._apiClient, this.publisherUname)
: super(
const PublisherMemberState(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(
'/publishers/$publisherUname/members',
queryParameters: {'offset': offset, 'take': take},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final members = data.map((e) => SnPublisherMember.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 PublisherMemberState(members: [], isLoading: false, total: 0);
}
}
class _PublisherMemberListSheet extends HookConsumerWidget {
final String publisherUname;
const _PublisherMemberListSheet({required this.publisherUname});
@override
Widget build(BuildContext context, WidgetRef ref) {
final publisherIdentity = ref.watch(
publisherIdentityProvider(publisherUname),
);
final memberListProvider = publisherMemberListNotifierProvider(
publisherUname,
);
final memberState = ref.watch(publisherMemberStateProvider(publisherUname));
final memberNotifier = ref.read(
publisherMemberStateProvider(publisherUname).notifier,
);
useEffect(() {
Future(() {
memberNotifier.loadMore();
});
return null;
}, []);
Future<void> invitePerson() async {
final result = await showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => const AccountPickerSheet(),
);
if (result == null) return;
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.post(
'/publishers/$publisherUname/invites',
data: {'related_user_id': result.id, 'role': 0},
);
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
}
}
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Column(
children: [
Padding(
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,
),
),
const Spacer(),
IconButton(
icon: const Icon(Symbols.person_add),
onPressed: invitePerson,
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
),
IconButton(
icon: const Icon(Symbols.refresh),
onPressed: () {
memberNotifier.reset();
memberNotifier.loadMore();
ref.invalidate(memberListProvider);
},
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
),
],
),
),
const Divider(height: 1),
Expanded(
child: PagingHelperView(
provider: memberListProvider,
futureRefreshable: memberListProvider.future,
notifierRefreshable: memberListProvider.notifier,
contentBuilder: (data, widgetCount, endItemView) {
return ListView.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == data.items.length) {
return endItemView;
}
final member = data.items[index];
return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id,
),
title: Row(
spacing: 6,
children: [
Flexible(child: Text(member.account!.nick)),
if (member.joinedAt == null)
const Icon(Symbols.pending_actions, size: 20),
],
),
subtitle: Row(
children: [
Text(
member.role >= 100
? 'permissionOwner'
: member.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
Text('·').bold().padding(horizontal: 6),
Expanded(child: Text("@${member.account!.name}")),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if ((publisherIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.edit),
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => _PublisherMemberRoleSheet(
publisherUname: publisherUname,
member: member,
),
).then((value) {
if (value != null) {
ref.invalidate(memberListProvider);
}
});
},
),
if ((publisherIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
showConfirmAlert(
'removePublisherMemberHint'.tr(),
'removePublisherMember'.tr(),
).then((confirm) async {
if (confirm != true) return;
try {
final apiClient = ref.watch(
apiClientProvider,
);
await apiClient.delete(
'/publishers/$publisherUname/members/${member.accountId}',
);
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
}
});
},
),
],
),
);
},
);
},
),
),
],
),
);
}
}
class _PublisherMemberRoleSheet extends HookConsumerWidget {
final String publisherUname;
final SnPublisherMember member;
const _PublisherMemberRoleSheet({
required this.publisherUname,
required this.member,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final roleController = useTextEditingController(
text: member.role.toString(),
);
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.only(
top: 16,
left: 20,
right: 16,
bottom: 12,
),
child: Row(
children: [
Text(
'memberRoleEdit'.tr(args: [member.account!.name]),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
),
const Spacer(),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(
minimumSize: const Size(36, 36),
),
),
],
),
),
const Divider(height: 1),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Autocomplete<int>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const [100, 50, 0];
}
final int? value = int.tryParse(textEditingValue.text);
if (value == null) return const [100, 50, 0];
return [100, 50, 0].where(
(option) =>
option.toString().contains(textEditingValue.text),
);
},
onSelected: (int selection) {
roleController.text = selection.toString();
},
fieldViewBuilder: (
context,
controller,
focusNode,
onFieldSubmitted,
) {
return TextField(
controller: controller,
focusNode: focusNode,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'memberRole'.tr(),
helperText: 'memberRoleHint'.tr(),
),
onTapOutside: (event) => focusNode.unfocus(),
);
},
),
const Gap(16),
FilledButton.icon(
onPressed: () async {
try {
final newRole = int.parse(roleController.text);
if (newRole < 0 || newRole > 100) {
throw 'Role must be between 0 and 100';
}
final apiClient = ref.read(apiClientProvider);
await apiClient.patch(
'/publishers/$publisherUname/members/${member.accountId}/role',
data: newRole,
);
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
}
},
icon: const Icon(Symbols.save),
label: const Text('saveChanges').tr(),
),
],
).padding(vertical: 16, horizontal: 24),
],
),
),
);
}
}
class _PublisherInviteSheet extends HookConsumerWidget {
const _PublisherInviteSheet();
@override
Widget build(BuildContext context, WidgetRef ref) {
final invites = ref.watch(publisherInvitesProvider);
Future<void> acceptInvite(SnPublisherMember invite) async {
try {
final client = ref.read(apiClientProvider);
await client.post(
'/publishers/invites/${invite.publisher!.name}/accept',
);
ref.invalidate(publisherInvitesProvider);
ref.invalidate(publishersManagedProvider);
} catch (err) {
showErrorAlert(err);
}
}
Future<void> declineInvite(SnPublisherMember invite) async {
try {
final client = ref.read(apiClientProvider);
await client.post(
'/publishers/invites/${invite.publisher!.name}/decline',
);
ref.invalidate(publisherInvitesProvider);
} catch (err) {
showErrorAlert(err);
}
}
return SheetScaffold(
titleText: 'invites'.tr(),
actions: [
IconButton(
icon: const Icon(Symbols.refresh),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
onPressed: () {
ref.invalidate(publisherInvitesProvider);
},
),
],
child: invites.when(
data:
(items) =>
items.isEmpty
? Center(
child:
Text(
'invitesEmpty',
textAlign: TextAlign.center,
).tr(),
)
: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final invite = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: invite.publisher!.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(invite.publisher!.nick),
subtitle:
Text(
invite.role >= 100
? 'permissionOwner'
: invite.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite),
),
],
),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(publisherInvitesProvider),
),
),
);
}
}

View File

@ -149,5 +149,422 @@ class _PublisherStatsProviderElement
String? get uname => (origin as PublisherStatsProvider).uname;
}
String _$publisherIdentityHash() => r'f7fd986a303a729ca5557022fceb37cd01fa17f3';
/// See also [publisherIdentity].
@ProviderFor(publisherIdentity)
const publisherIdentityProvider = PublisherIdentityFamily();
/// See also [publisherIdentity].
class PublisherIdentityFamily extends Family<AsyncValue<SnPublisherMember?>> {
/// See also [publisherIdentity].
const PublisherIdentityFamily();
/// See also [publisherIdentity].
PublisherIdentityProvider call(String uname) {
return PublisherIdentityProvider(uname);
}
@override
PublisherIdentityProvider getProviderOverride(
covariant PublisherIdentityProvider 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'publisherIdentityProvider';
}
/// See also [publisherIdentity].
class PublisherIdentityProvider
extends AutoDisposeFutureProvider<SnPublisherMember?> {
/// See also [publisherIdentity].
PublisherIdentityProvider(String uname)
: this._internal(
(ref) => publisherIdentity(ref as PublisherIdentityRef, uname),
from: publisherIdentityProvider,
name: r'publisherIdentityProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$publisherIdentityHash,
dependencies: PublisherIdentityFamily._dependencies,
allTransitiveDependencies:
PublisherIdentityFamily._allTransitiveDependencies,
uname: uname,
);
PublisherIdentityProvider._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<SnPublisherMember?> Function(PublisherIdentityRef provider) create,
) {
return ProviderOverride(
origin: this,
override: PublisherIdentityProvider._internal(
(ref) => create(ref as PublisherIdentityRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeFutureProviderElement<SnPublisherMember?> createElement() {
return _PublisherIdentityProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PublisherIdentityProvider && 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 PublisherIdentityRef on AutoDisposeFutureProviderRef<SnPublisherMember?> {
/// The parameter `uname` of this provider.
String get uname;
}
class _PublisherIdentityProviderElement
extends AutoDisposeFutureProviderElement<SnPublisherMember?>
with PublisherIdentityRef {
_PublisherIdentityProviderElement(super.provider);
@override
String get uname => (origin as PublisherIdentityProvider).uname;
}
String _$publisherFeaturesHash() => r'34db65d9a4b6b0c6961733ae79e67f25d5d111d3';
/// See also [publisherFeatures].
@ProviderFor(publisherFeatures)
const publisherFeaturesProvider = PublisherFeaturesFamily();
/// See also [publisherFeatures].
class PublisherFeaturesFamily extends Family<AsyncValue<Map<String, bool>>> {
/// See also [publisherFeatures].
const PublisherFeaturesFamily();
/// See also [publisherFeatures].
PublisherFeaturesProvider call(String? uname) {
return PublisherFeaturesProvider(uname);
}
@override
PublisherFeaturesProvider getProviderOverride(
covariant PublisherFeaturesProvider 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'publisherFeaturesProvider';
}
/// See also [publisherFeatures].
class PublisherFeaturesProvider
extends AutoDisposeFutureProvider<Map<String, bool>> {
/// See also [publisherFeatures].
PublisherFeaturesProvider(String? uname)
: this._internal(
(ref) => publisherFeatures(ref as PublisherFeaturesRef, uname),
from: publisherFeaturesProvider,
name: r'publisherFeaturesProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$publisherFeaturesHash,
dependencies: PublisherFeaturesFamily._dependencies,
allTransitiveDependencies:
PublisherFeaturesFamily._allTransitiveDependencies,
uname: uname,
);
PublisherFeaturesProvider._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<Map<String, bool>> Function(PublisherFeaturesRef provider) create,
) {
return ProviderOverride(
origin: this,
override: PublisherFeaturesProvider._internal(
(ref) => create(ref as PublisherFeaturesRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeFutureProviderElement<Map<String, bool>> createElement() {
return _PublisherFeaturesProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PublisherFeaturesProvider && 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 PublisherFeaturesRef on AutoDisposeFutureProviderRef<Map<String, bool>> {
/// The parameter `uname` of this provider.
String? get uname;
}
class _PublisherFeaturesProviderElement
extends AutoDisposeFutureProviderElement<Map<String, bool>>
with PublisherFeaturesRef {
_PublisherFeaturesProviderElement(super.provider);
@override
String? get uname => (origin as PublisherFeaturesProvider).uname;
}
String _$publisherInvitesHash() => r'488cd443407895ce11f4edff07cb6ea58f2aa018';
/// See also [publisherInvites].
@ProviderFor(publisherInvites)
final publisherInvitesProvider =
AutoDisposeFutureProvider<List<SnPublisherMember>>.internal(
publisherInvites,
name: r'publisherInvitesProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$publisherInvitesHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef PublisherInvitesRef =
AutoDisposeFutureProviderRef<List<SnPublisherMember>>;
String _$publisherMemberListNotifierHash() =>
r'237e8f39c9757a6cbdff817853c697539242ad2a';
abstract class _$PublisherMemberListNotifier
extends
BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPublisherMember>> {
late final String uname;
FutureOr<CursorPagingData<SnPublisherMember>> build(String uname);
}
/// See also [PublisherMemberListNotifier].
@ProviderFor(PublisherMemberListNotifier)
const publisherMemberListNotifierProvider = PublisherMemberListNotifierFamily();
/// See also [PublisherMemberListNotifier].
class PublisherMemberListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnPublisherMember>>> {
/// See also [PublisherMemberListNotifier].
const PublisherMemberListNotifierFamily();
/// See also [PublisherMemberListNotifier].
PublisherMemberListNotifierProvider call(String uname) {
return PublisherMemberListNotifierProvider(uname);
}
@override
PublisherMemberListNotifierProvider getProviderOverride(
covariant PublisherMemberListNotifierProvider 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'publisherMemberListNotifierProvider';
}
/// See also [PublisherMemberListNotifier].
class PublisherMemberListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
PublisherMemberListNotifier,
CursorPagingData<SnPublisherMember>
> {
/// See also [PublisherMemberListNotifier].
PublisherMemberListNotifierProvider(String uname)
: this._internal(
() => PublisherMemberListNotifier()..uname = uname,
from: publisherMemberListNotifierProvider,
name: r'publisherMemberListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$publisherMemberListNotifierHash,
dependencies: PublisherMemberListNotifierFamily._dependencies,
allTransitiveDependencies:
PublisherMemberListNotifierFamily._allTransitiveDependencies,
uname: uname,
);
PublisherMemberListNotifierProvider._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
FutureOr<CursorPagingData<SnPublisherMember>> runNotifierBuild(
covariant PublisherMemberListNotifier notifier,
) {
return notifier.build(uname);
}
@override
Override overrideWith(PublisherMemberListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: PublisherMemberListNotifierProvider._internal(
() => create()..uname = uname,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
PublisherMemberListNotifier,
CursorPagingData<SnPublisherMember>
>
createElement() {
return _PublisherMemberListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PublisherMemberListNotifierProvider && 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 PublisherMemberListNotifierRef
on
AutoDisposeAsyncNotifierProviderRef<
CursorPagingData<SnPublisherMember>
> {
/// The parameter `uname` of this provider.
String get uname;
}
class _PublisherMemberListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
PublisherMemberListNotifier,
CursorPagingData<SnPublisherMember>
>
with PublisherMemberListNotifierRef {
_PublisherMemberListNotifierProviderElement(super.provider);
@override
String get uname => (origin as PublisherMemberListNotifierProvider).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

@ -26,7 +26,7 @@ class CreatorPostListScreen extends HookConsumerWidget {
children: [
ListTile(
leading: const Icon(Symbols.edit),
title: Text('postContent'.tr()),
title: Text('Post'),
subtitle: Text('Create a regular post'),
onTap: () async {
Navigator.pop(context);

View File

@ -8,7 +8,7 @@ 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/file.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
@ -270,7 +270,10 @@ class EditPublisherScreen extends HookConsumerWidget {
),
TextFormField(
controller: bioController,
decoration: InputDecoration(labelText: 'bio'.tr()),
decoration: InputDecoration(
labelText: 'bio'.tr(),
alignLabelWithHint: true,
),
minLines: 3,
maxLines: null,
onTapOutside:

View File

@ -71,9 +71,7 @@ class SliverStickerPacksList extends HookConsumerWidget {
subtitle: Text(sticker.description),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context.push(
'/creators/$pubName/stickers/${sticker.id}',
);
context.push('/creators/$pubName/stickers/${sticker.id}');
},
);
},
@ -230,6 +228,7 @@ class EditStickerPacksScreen extends HookConsumerWidget {
decoration: InputDecoration(
labelText: 'description'.tr(),
border: const UnderlineInputBorder(),
alignLabelWithHint: true,
),
minLines: 3,
maxLines: null,

View File

@ -0,0 +1,288 @@
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/webfeed.dart';
import 'package:island/pods/webfeed.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class WebFeedNewScreen extends StatelessWidget {
final String pubName;
const WebFeedNewScreen({super.key, required this.pubName});
@override
Widget build(BuildContext context) {
return WebFeedEditScreen(pubName: pubName, feedId: null);
}
}
class WebFeedEditScreen extends HookConsumerWidget {
final String pubName;
final String? feedId;
const WebFeedEditScreen({super.key, required this.pubName, this.feedId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = useMemoized(() => GlobalKey<FormState>());
final titleController = useTextEditingController();
final urlController = useTextEditingController();
final descriptionController = useTextEditingController();
final isLoading = useState(false);
final isScrapEnabled = useState(false);
final saveFeed = useCallback(() async {
if (!formKey.currentState!.validate()) return;
isLoading.value = true;
try {
final feed = SnWebFeed(
id: feedId ?? '',
title: titleController.text,
url: urlController.text,
description: descriptionController.text,
config: SnWebFeedConfig(scrapPage: isScrapEnabled.value),
publisherId: pubName,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
);
await ref
.read(
webFeedNotifierProvider((
pubName: pubName,
feedId: feedId,
)).notifier,
)
.saveFeed(feed);
// Refresh the feed list
ref.invalidate(webFeedListProvider(pubName));
if (context.mounted) {
showSnackBar('Web feed saved successfully');
context.pop();
}
} catch (e) {
showErrorAlert(e);
} finally {
isLoading.value = false;
}
}, [pubName, feedId, isScrapEnabled.value, context]);
final deleteFeed = useCallback(() async {
final confirmed = await showConfirmAlert(
'Are you sure you want to delete this web feed? This action cannot be undone.',
'Delete Web Feed',
);
if (confirmed != true) return;
isLoading.value = true;
try {
await ref
.read(
webFeedNotifierProvider((
pubName: pubName,
feedId: feedId!,
)).notifier,
)
.deleteFeed();
ref.invalidate(webFeedListProvider(pubName));
if (context.mounted) {
showSnackBar('Web feed deleted successfully');
context.pop();
}
} catch (e) {
showErrorAlert(e);
} finally {
isLoading.value = false;
}
}, [pubName, feedId, context, ref]);
final feedAsync = ref.watch(
webFeedNotifierProvider((pubName: pubName, feedId: feedId)),
);
return feedAsync.when(
loading:
() =>
const Scaffold(body: Center(child: CircularProgressIndicator())),
error:
(error, stack) => Scaffold(
appBar: AppBar(title: const Text('Error')),
body: Center(child: Text('Error: $error')),
),
data: (feed) {
// Initialize form fields if they're empty and we have a feed
if (titleController.text.isEmpty) {
titleController.text = feed.title;
urlController.text = feed.url;
descriptionController.text = feed.description ?? '';
isScrapEnabled.value = feed.config.scrapPage;
}
return _buildForm(
context,
formKey: formKey,
titleController: titleController,
urlController: urlController,
descriptionController: descriptionController,
isScrapEnabled: isScrapEnabled.value,
onScrapEnabledChanged: (value) => isScrapEnabled.value = value,
onSave: saveFeed,
onDelete: deleteFeed,
isLoading: isLoading.value,
ref: ref,
hasFeedId: feedId != null,
);
},
);
}
Widget _buildForm(
BuildContext context, {
required WidgetRef ref,
required GlobalKey<FormState> formKey,
required TextEditingController titleController,
required TextEditingController urlController,
required TextEditingController descriptionController,
required bool isScrapEnabled,
required ValueChanged<bool> onScrapEnabledChanged,
required VoidCallback onSave,
required VoidCallback onDelete,
required bool isLoading,
required bool hasFeedId,
}) {
final scrapNow = useCallback(() async {
showLoadingModal(context);
try {
await ref
.read(
webFeedNotifierProvider((
pubName: pubName,
feedId: feedId!,
)).notifier,
)
.scrapFeed();
if (context.mounted) {
showSnackBar('Feed scraping successfully.');
}
} catch (e) {
showErrorAlert(e);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}, [pubName, feedId, ref, context]);
return AppScaffold(
appBar: AppBar(
title: Text(hasFeedId ? 'Edit Web Feed' : 'New Web Feed'),
actions: [
if (hasFeedId)
IconButton(
icon: const Icon(Symbols.delete_forever),
onPressed: isLoading ? null : onDelete,
),
const SizedBox(width: 8),
],
),
body: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
children: [
TextFormField(
controller: titleController,
decoration: const InputDecoration(labelText: 'Title'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a title';
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: urlController,
decoration: const InputDecoration(
labelText: 'URL',
hintText: 'https://example.com/feed',
),
keyboardType: TextInputType.url,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a URL';
}
final uri = Uri.tryParse(value);
if (uri == null || !uri.hasAbsolutePath) {
return 'Please enter a valid URL';
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: const InputDecoration(
labelText: 'Description',
alignLabelWithHint: true,
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
maxLines: 3,
),
const SizedBox(height: 24),
Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
title: const Text('Scrape web page for content'),
subtitle: const Text(
'When enabled, the system will attempt to extract full content from the web page',
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
value: isScrapEnabled,
onChanged: onScrapEnabledChanged,
),
],
),
),
const SizedBox(height: 20),
if (hasFeedId) ...[
FilledButton.tonalIcon(
onPressed: isLoading ? null : scrapNow,
icon: const Icon(Symbols.refresh),
label: const Text('Scrape Now'),
).alignment(Alignment.centerRight),
const SizedBox(height: 16),
],
FilledButton.icon(
onPressed: isLoading ? null : onSave,
icon: const Icon(Symbols.save),
label: Text('saveChanges').tr(),
).alignment(Alignment.centerRight),
],
).padding(all: 20),
),
),
);
}
}

View File

@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:island/pods/webfeed.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/empty_state.dart';
import 'package:material_symbols_icons/symbols.dart';
class WebFeedListScreen extends ConsumerWidget {
final String pubName;
const WebFeedListScreen({super.key, required this.pubName});
@override
Widget build(BuildContext context, WidgetRef ref) {
final feedsAsync = ref.watch(webFeedListProvider(pubName));
return AppScaffold(
appBar: AppBar(title: const Text('Web Feeds')),
floatingActionButton: FloatingActionButton(
child: const Icon(Symbols.add),
onPressed: () {
context.push('/creators/$pubName/feeds/new');
},
),
body: feedsAsync.when(
data: (feeds) {
if (feeds.isEmpty) {
return EmptyState(
icon: Symbols.rss_feed,
title: 'No Web Feeds',
description: 'Add a new web feed to get started',
);
}
return RefreshIndicator(
onRefresh: () => ref.refresh(webFeedListProvider(pubName).future),
child: ListView.builder(
padding: EdgeInsets.only(top: 8),
itemCount: feeds.length,
itemBuilder: (context, index) {
final feed = feeds[index];
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
child: ListTile(
leading: const Icon(Symbols.rss_feed, size: 32),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
title: Text(
feed.title,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
feed.url,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context.push('/creators/$pubName/feeds/${feed.id}');
},
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),
),
);
}
}

View File

@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
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';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'apps.g.dart';
@riverpod
Future<List<CustomApp>> customApps(Ref ref, String publisherName) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/developers/$publisherName/apps');
return resp.data.map((e) => CustomApp.fromJson(e)).cast<CustomApp>().toList();
}
class CustomAppsScreen extends HookConsumerWidget {
final String publisherName;
const CustomAppsScreen({super.key, required this.publisherName});
@override
Widget build(BuildContext context, WidgetRef ref) {
final apps = ref.watch(customAppsProvider(publisherName));
return AppScaffold(
appBar: AppBar(
title: Text('customApps').tr(),
actions: [
IconButton(
icon: const Icon(Symbols.add),
onPressed: () {
context.push('/developers/$publisherName/apps/new');
},
),
],
),
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),
child: Column(
children: [
SizedBox(
height: 150,
child: Stack(
fit: StackFit.expand,
children: [
if (app.background != null)
CloudFileWidget(
item: app.background!,
fit: BoxFit.cover,
).clipRRect(topLeft: 8, topRight: 8),
if (app.picture != null)
Positioned(
left: 16,
bottom: 16,
child: ProfilePictureWidget(
fileId: app.picture!.id,
radius: 40,
fallbackIcon: Symbols.apps,
),
),
],
),
),
ListTile(
title: Text(app.name),
subtitle: Text(
app.slug,
style: GoogleFonts.robotoMono(fontSize: 12),
),
contentPadding: EdgeInsets.only(left: 20, right: 12),
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.push(
'/developers/$publisherName/apps/${app.id}',
);
} else if (value == 'delete') {
showConfirmAlert(
'deleteCustomAppHint'.tr(),
'deleteCustomApp'.tr(),
).then((confirm) {
if (confirm) {
final client = ref.read(apiClientProvider);
client.delete(
'/developers/$publisherName/apps/${app.id}',
);
ref.invalidate(
customAppsProvider(publisherName),
);
}
});
}
},
),
),
],
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(customAppsProvider(publisherName)),
),
),
);
}
}

View File

@ -0,0 +1,151 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'apps.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$customAppsHash() => r'1dec11573b9d987c3adbdf4732b3781a6f40172a';
/// 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 [customApps].
@ProviderFor(customApps)
const customAppsProvider = CustomAppsFamily();
/// See also [customApps].
class CustomAppsFamily extends Family<AsyncValue<List<CustomApp>>> {
/// See also [customApps].
const CustomAppsFamily();
/// See also [customApps].
CustomAppsProvider call(String publisherName) {
return CustomAppsProvider(publisherName);
}
@override
CustomAppsProvider getProviderOverride(
covariant CustomAppsProvider provider,
) {
return call(provider.publisherName);
}
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'customAppsProvider';
}
/// See also [customApps].
class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
/// See also [customApps].
CustomAppsProvider(String publisherName)
: this._internal(
(ref) => customApps(ref as CustomAppsRef, publisherName),
from: customAppsProvider,
name: r'customAppsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$customAppsHash,
dependencies: CustomAppsFamily._dependencies,
allTransitiveDependencies: CustomAppsFamily._allTransitiveDependencies,
publisherName: publisherName,
);
CustomAppsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
}) : super.internal();
final String publisherName;
@override
Override overrideWith(
FutureOr<List<CustomApp>> Function(CustomAppsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: CustomAppsProvider._internal(
(ref) => create(ref as CustomAppsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
),
);
}
@override
AutoDisposeFutureProviderElement<List<CustomApp>> createElement() {
return _CustomAppsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is CustomAppsProvider && other.publisherName == publisherName;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin CustomAppsRef on AutoDisposeFutureProviderRef<List<CustomApp>> {
/// The parameter `publisherName` of this provider.
String get publisherName;
}
class _CustomAppsProviderElement
extends AutoDisposeFutureProviderElement<List<CustomApp>>
with CustomAppsRef {
_CustomAppsProviderElement(super.provider);
@override
String get publisherName => (origin as CustomAppsProvider).publisherName;
}
// 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,558 @@
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:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/custom_app.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/developers/apps.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';
import 'package:island/widgets/content/sheet.dart';
part 'edit_app.g.dart';
@riverpod
Future<CustomApp?> customApp(Ref ref, String publisherName, String id) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/developers/$publisherName/apps/$id');
return CustomApp.fromJson(resp.data);
}
class EditAppScreen extends HookConsumerWidget {
final String publisherName;
final String? id;
const EditAppScreen({super.key, required this.publisherName, this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null;
final app = isNew ? null : ref.watch(customAppProvider(publisherName, id!));
final formKey = useMemoized(() => GlobalKey<FormState>());
final submitting = useState(false);
final nameController = useTextEditingController();
final slugController = useTextEditingController();
final descriptionController = useTextEditingController();
final picture = useState<SnCloudFile?>(null);
final background = useState<SnCloudFile?>(null);
final enableLinks = useState(false); // Only for UI purposes
final homePageController = useTextEditingController();
final privacyPolicyController = useTextEditingController();
final termsController = useTextEditingController();
final oauthEnabled = useState(false);
final redirectUris = useState<List<String>>([]);
final postLogoutUris = useState<List<String>>([]);
final allowedScopes = useState<List<String>>([
'openid',
'profile',
'email',
]);
final allowedGrantTypes = useState<List<String>>([
'authorization_code',
'refresh_token',
]);
final requirePkce = useState(true);
final allowOfflineAccess = useState(false);
useEffect(() {
if (app?.value != null) {
nameController.text = app!.value!.name;
slugController.text = app.value!.slug;
descriptionController.text = app.value!.description ?? '';
picture.value = app.value!.picture;
background.value = app.value!.background;
homePageController.text = app.value!.links?.homePage ?? '';
privacyPolicyController.text = app.value!.links?.privacyPolicy ?? '';
termsController.text = app.value!.links?.termsOfService ?? '';
if (app.value!.oauthConfig != null) {
oauthEnabled.value = true;
redirectUris.value = app.value!.oauthConfig!.redirectUris;
postLogoutUris.value =
app.value!.oauthConfig!.postLogoutRedirectUris ?? [];
allowedScopes.value = app.value!.oauthConfig!.allowedScopes;
allowedGrantTypes.value = app.value!.oauthConfig!.allowedGrantTypes;
requirePkce.value = app.value!.oauthConfig!.requirePkce;
allowOfflineAccess.value = app.value!.oauthConfig!.allowOfflineAccess;
}
}
return null;
}, [app]);
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 showAddScopeDialog() {
final scopeController = TextEditingController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'addScope'.tr(),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: scopeController,
decoration: InputDecoration(labelText: 'scopeName'.tr()),
),
const SizedBox(height: 20),
FilledButton.tonalIcon(
onPressed: () {
if (scopeController.text.isNotEmpty) {
allowedScopes.value = [
...allowedScopes.value,
scopeController.text,
];
Navigator.pop(context);
}
},
icon: const Icon(Symbols.add),
label: Text('add').tr(),
),
],
),
),
),
);
}
void showAddRedirectUriDialog() {
final uriController = TextEditingController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'addRedirectUri'.tr(),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: uriController,
decoration: InputDecoration(
labelText: 'redirectUri'.tr(),
hintText: 'https://example.com/auth/callback',
helperText: 'redirectUriHint'.tr(),
helperMaxLines: 3,
),
keyboardType: TextInputType.url,
validator: (value) {
if (value == null || value.isEmpty) {
return 'uriRequired'.tr();
}
final uri = Uri.tryParse(value);
if (uri == null || !uri.hasAbsolutePath) {
return 'invalidUri'.tr();
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 20),
FilledButton.tonalIcon(
onPressed: () {
if (uriController.text.isNotEmpty) {
redirectUris.value = [
...redirectUris.value,
uriController.text,
];
Navigator.pop(context);
}
},
icon: const Icon(Symbols.add),
label: Text('add').tr(),
),
],
),
),
),
);
}
void performAction() async {
final client = ref.read(apiClientProvider);
final data = {
'name': nameController.text,
'slug': slugController.text,
'description': descriptionController.text,
'picture_id': picture.value?.id,
'background_id': background.value?.id,
'links': {
'home_page':
homePageController.text.isNotEmpty
? homePageController.text
: null,
'privacy_policy':
privacyPolicyController.text.isNotEmpty
? privacyPolicyController.text
: null,
'terms_of_service':
termsController.text.isNotEmpty ? termsController.text : null,
},
'oauth_config':
oauthEnabled.value
? {
'redirect_uris': redirectUris.value,
'post_logout_redirect_uris':
postLogoutUris.value.isNotEmpty
? postLogoutUris.value
: null,
'allowed_scopes': allowedScopes.value,
'allowed_grant_types': allowedGrantTypes.value,
'require_pkce': requirePkce.value,
'allow_offline_access': allowOfflineAccess.value,
}
: null,
};
if (isNew) {
await client.post('/developers/$publisherName/apps', data: data);
} else {
await client.patch('/developers/$publisherName/apps/$id', data: data);
}
ref.invalidate(customAppsProvider(publisherName));
if (context.mounted) {
Navigator.pop(context);
}
}
return AppScaffold(
appBar: AppBar(
title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
),
body:
app == null && !isNew
? const Center(child: CircularProgressIndicator())
: app?.hasError == true && !isNew
? ResponseErrorWidget(
error: app!.error,
onRetry:
() => ref.invalidate(customAppProvider(publisherName, 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.apps,
),
onTap: () {
setPicture('picture');
},
),
),
],
),
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(labelText: 'name'.tr()),
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
),
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
alignLabelWithHint: true,
),
maxLines: 3,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
const SizedBox(height: 16),
ExpansionPanelList(
expansionCallback: (index, isExpanded) {
switch (index) {
case 0:
enableLinks.value = isExpanded;
break;
case 1:
oauthEnabled.value = isExpanded;
break;
}
},
children: [
ExpansionPanel(
headerBuilder:
(context, isExpanded) =>
ListTile(title: Text('appLinks').tr()),
body: Column(
spacing: 16,
children: [
TextFormField(
controller: homePageController,
decoration: InputDecoration(
labelText: 'homePageUrl'.tr(),
hintText: 'https://example.com',
),
keyboardType: TextInputType.url,
),
TextFormField(
controller: privacyPolicyController,
decoration: InputDecoration(
labelText: 'privacyPolicyUrl'.tr(),
hintText: 'https://example.com/privacy',
),
keyboardType: TextInputType.url,
),
TextFormField(
controller: termsController,
decoration: InputDecoration(
labelText: 'termsOfServiceUrl'.tr(),
hintText: 'https://example.com/terms',
),
keyboardType: TextInputType.url,
),
],
).padding(horizontal: 16, bottom: 24),
isExpanded: enableLinks.value,
),
ExpansionPanel(
headerBuilder:
(context, isExpanded) => ListTile(
title: Text('oauthConfig').tr(),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('redirectUris'.tr()),
Card(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: Column(
children: [
...redirectUris.value.map(
(uri) => ListTile(
title: Text(uri),
trailing: IconButton(
icon: const Icon(
Symbols.delete,
),
onPressed: () {
redirectUris.value =
redirectUris.value
.where(
(u) => u != uri,
)
.toList();
},
),
),
),
if (redirectUris.value.isNotEmpty)
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.add),
title: Text('addRedirectUri'.tr()),
onTap: showAddRedirectUriDialog,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(8),
),
),
],
),
),
const SizedBox(height: 16),
Text('allowedScopes'.tr()),
Card(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: Column(
children: [
...allowedScopes.value.map(
(scope) => ListTile(
title: Text(scope),
trailing: IconButton(
icon: const Icon(
Symbols.delete,
),
onPressed: () {
allowedScopes.value =
allowedScopes.value
.where(
(s) => s != scope,
)
.toList();
},
),
),
),
if (allowedScopes.value.isNotEmpty)
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.add),
title: Text('add').tr(),
onTap: showAddScopeDialog,
),
],
),
),
const SizedBox(height: 16),
SwitchListTile(
title: Text('requirePkce'.tr()),
value: requirePkce.value,
onChanged:
(value) => requirePkce.value = value,
),
SwitchListTile(
title: Text('allowOfflineAccess'.tr()),
value: allowOfflineAccess.value,
onChanged:
(value) =>
allowOfflineAccess.value = value,
),
],
).padding(horizontal: 16, bottom: 24),
isExpanded: oauthEnabled.value,
),
],
),
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,161 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'edit_app.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$customAppHash() => r'aa4d1fb803c47a99cbacf6d91481f4fce3fda457';
/// 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 [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 id) {
return CustomAppProvider(publisherName, id);
}
@override
CustomAppProvider getProviderOverride(covariant CustomAppProvider provider) {
return call(provider.publisherName, 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'customAppProvider';
}
/// See also [customApp].
class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
/// See also [customApp].
CustomAppProvider(String publisherName, String id)
: this._internal(
(ref) => customApp(ref as CustomAppRef, publisherName, id),
from: customAppProvider,
name: r'customAppProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$customAppHash,
dependencies: CustomAppFamily._dependencies,
allTransitiveDependencies: CustomAppFamily._allTransitiveDependencies,
publisherName: publisherName,
id: id,
);
CustomAppProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.id,
}) : super.internal();
final String publisherName;
final String id;
@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,
id: id,
),
);
}
@override
AutoDisposeFutureProviderElement<CustomApp?> createElement() {
return _CustomAppProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is CustomAppProvider &&
other.publisherName == publisherName &&
other.id == id;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.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 CustomAppRef on AutoDisposeFutureProviderRef<CustomApp?> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `id` of this provider.
String get id;
}
class _CustomAppProviderElement
extends AutoDisposeFutureProviderElement<CustomApp?>
with CustomAppRef {
_CustomAppProviderElement(super.provider);
@override
String get publisherName => (origin as CustomAppProvider).publisherName;
@override
String get id => (origin as CustomAppProvider).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,386 @@
import 'package:dropdown_button2/dropdown_button2.dart';
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/models/developer.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.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 'hub.g.dart';
@riverpod
Future<DeveloperStats?> developerStats(Ref ref, String? uname) async {
if (uname == null) return null;
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/developers/$uname/stats');
return DeveloperStats.fromJson(resp.data);
}
@riverpod
Future<List<SnPublisher>> developers(Ref ref) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/developers');
return resp.data
.map((e) => SnPublisher.fromJson(e))
.cast<SnPublisher>()
.toList();
}
class DeveloperHubShellScreen extends StatelessWidget {
final Widget child;
const DeveloperHubShellScreen({super.key, required this.child});
@override
Widget build(BuildContext context) {
final isWide = isWideScreen(context);
if (isWide) {
return AppBackground(
isRoot: true,
child: Row(
children: [
SizedBox(
width: 360,
child: const DeveloperHubScreen(isAside: true),
),
const VerticalDivider(width: 1),
Expanded(child: child),
],
),
);
}
return AppBackground(isRoot: true, child: child);
}
}
class DeveloperHubScreen extends HookConsumerWidget {
final bool isAside;
const DeveloperHubScreen({super.key, this.isAside = false});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isWide = isWideScreen(context);
if (isWide && !isAside) {
return Container(color: Theme.of(context).colorScheme.surface);
}
final developers = ref.watch(developersProvider);
final currentDeveloper = useState<SnPublisher?>(
developers.value?.firstOrNull,
);
final List<DropdownMenuItem<SnPublisher>> developersMenu = developers.when(
data:
(data) =>
data
.map(
(item) => DropdownMenuItem<SnPublisher>(
value: item,
child: ListTile(
minTileHeight: 48,
leading: ProfilePictureWidget(
radius: 16,
fileId: item.picture?.id,
),
title: Text(item.nick),
subtitle: Text('@${item.name}'),
trailing:
currentDeveloper.value?.id == item.id
? const Icon(Icons.check)
: null,
contentPadding: EdgeInsets.symmetric(horizontal: 8),
),
),
)
.toList(),
loading: () => [],
error: (_, _) => [],
);
final developerStats = ref.watch(
developerStatsProvider(currentDeveloper.value?.name),
);
return AppScaffold(
noBackground: false,
appBar: AppBar(
leading: !isWide ? const PageBackButton() : null,
title: Text('developerHub').tr(),
actions: [
DropdownButtonHideUnderline(
child: DropdownButton2<SnPublisher>(
alignment: Alignment.centerRight,
value: currentDeveloper.value,
hint: CircleAvatar(
radius: 16,
child: Icon(
Symbols.person,
color: Theme.of(
context,
).colorScheme.onSecondaryContainer.withOpacity(0.9),
fill: 1,
),
).center().padding(right: 8),
items: [...developersMenu],
onChanged: (value) {
currentDeveloper.value = value;
},
selectedItemBuilder: (context) {
return [
...developersMenu.map(
(e) => ProfilePictureWidget(
radius: 16,
fileId: e.value?.picture?.id,
).center().padding(right: 8),
),
];
},
buttonStyleData: ButtonStyleData(
height: 40,
padding: const EdgeInsets.only(left: 14, right: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
),
),
dropdownStyleData: DropdownStyleData(
width: 320,
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
),
),
menuItemStyleData: const MenuItemStyleData(
height: 64,
padding: EdgeInsets.only(left: 14, right: 14),
),
iconStyleData: IconStyleData(
icon: Icon(Icons.arrow_drop_down),
iconSize: 19,
iconEnabledColor:
Theme.of(context).appBarTheme.foregroundColor!,
iconDisabledColor:
Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
const Gap(8),
],
),
body: developerStats.when(
data:
(stats) => SingleChildScrollView(
child:
currentDeveloper.value == null
? Column(
children: [
const Gap(24),
const Icon(Symbols.info, size: 32).padding(bottom: 4),
Text(
'developerHubUnselectedHint',
textAlign: TextAlign.center,
).tr(),
const Gap(24),
const Divider(height: 1),
...(developers.value?.map(
(developer) => ListTile(
leading: ProfilePictureWidget(
file: developer.picture,
),
title: Text(developer.nick),
subtitle: Text('@${developer.name}'),
onTap: () {
currentDeveloper.value = developer;
},
),
) ??
[]),
ListTile(
leading: const CircleAvatar(
child: Icon(Symbols.add),
),
title: Text('enrollDeveloper').tr(),
subtitle: Text('enrollDeveloperHint').tr(),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(_) => const _DeveloperEnrollmentSheet(),
).then((value) {
if (value == true) {
ref.invalidate(developersProvider);
}
});
},
),
],
)
: Column(
children: [
if (stats != null)
_DeveloperStatsWidget(
stats: stats,
).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(
horizontal: 24,
),
onTap: () {
context.push(
'/developers/${currentDeveloper.value!.name}/apps',
);
},
),
],
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry: () {
ref.invalidate(
developerStatsProvider(currentDeveloper.value?.name),
);
},
),
),
);
}
}
class _DeveloperStatsWidget extends StatelessWidget {
final DeveloperStats stats;
const _DeveloperStatsWidget({required this.stats});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
spacing: 8,
children: [
Row(
spacing: 8,
children: [
Expanded(
child: _buildStatsCard(
context,
stats.totalCustomApps.toString(),
'totalCustomApps',
),
),
],
),
],
),
);
}
Widget _buildStatsCard(
BuildContext context,
String statValue,
String statLabel,
) {
return Card(
margin: EdgeInsets.zero,
child: SizedBox(
height: 100,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
statValue,
style: Theme.of(context).textTheme.headlineMedium,
),
const Gap(4),
Text(
statLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
).tr(),
],
),
),
),
);
}
}
class _DeveloperEnrollmentSheet extends HookConsumerWidget {
const _DeveloperEnrollmentSheet();
@override
Widget build(BuildContext context, WidgetRef ref) {
final publishers = ref.watch(publishersManagedProvider);
Future<void> enroll(SnPublisher publisher) async {
try {
final client = ref.read(apiClientProvider);
await client.post('/developers/${publisher.name}/enroll');
if (context.mounted) {
Navigator.pop(context, true);
}
} catch (err) {
showErrorAlert(err);
}
}
return SheetScaffold(
titleText: 'enrollDeveloper'.tr(),
child: publishers.when(
data:
(items) =>
items.isEmpty
? Center(
child:
Text(
'noPublishersToEnroll',
textAlign: TextAlign.center,
).tr(),
)
: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final publisher = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: publisher.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
onTap: () => enroll(publisher),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(publishersManagedProvider),
),
),
);
}
}

View File

@ -0,0 +1,172 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'hub.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$developerStatsHash() => r'783398cbde09c3d956c3e20b02a1cebd1f8ab748';
/// 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 [developerStats].
@ProviderFor(developerStats)
const developerStatsProvider = DeveloperStatsFamily();
/// See also [developerStats].
class DeveloperStatsFamily extends Family<AsyncValue<DeveloperStats?>> {
/// See also [developerStats].
const DeveloperStatsFamily();
/// See also [developerStats].
DeveloperStatsProvider call(String? uname) {
return DeveloperStatsProvider(uname);
}
@override
DeveloperStatsProvider getProviderOverride(
covariant DeveloperStatsProvider 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'developerStatsProvider';
}
/// See also [developerStats].
class DeveloperStatsProvider
extends AutoDisposeFutureProvider<DeveloperStats?> {
/// See also [developerStats].
DeveloperStatsProvider(String? uname)
: this._internal(
(ref) => developerStats(ref as DeveloperStatsRef, uname),
from: developerStatsProvider,
name: r'developerStatsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$developerStatsHash,
dependencies: DeveloperStatsFamily._dependencies,
allTransitiveDependencies:
DeveloperStatsFamily._allTransitiveDependencies,
uname: uname,
);
DeveloperStatsProvider._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<DeveloperStats?> Function(DeveloperStatsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: DeveloperStatsProvider._internal(
(ref) => create(ref as DeveloperStatsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeFutureProviderElement<DeveloperStats?> createElement() {
return _DeveloperStatsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is DeveloperStatsProvider && 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 DeveloperStatsRef on AutoDisposeFutureProviderRef<DeveloperStats?> {
/// The parameter `uname` of this provider.
String? get uname;
}
class _DeveloperStatsProviderElement
extends AutoDisposeFutureProviderElement<DeveloperStats?>
with DeveloperStatsRef {
_DeveloperStatsProviderElement(super.provider);
@override
String? get uname => (origin as DeveloperStatsProvider).uname;
}
String _$developersHash() => r'f52639d3c21aafbf235c8ae33f35448baf2989a1';
/// See also [developers].
@ProviderFor(developers)
final developersProvider =
AutoDisposeFutureProvider<List<SnPublisher>>.internal(
developers,
name: r'developersProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$developersHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnPublisher>>;
// 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,12 @@
import 'package:flutter/material.dart';
import 'package:island/screens/developers/edit_app.dart';
class NewCustomAppScreen extends StatelessWidget {
final String publisherName;
const NewCustomAppScreen({super.key, required this.publisherName});
@override
Widget build(BuildContext context) {
return EditAppScreen(publisherName: publisherName);
}
}

View File

@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/article_detail.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/loading_indicator.dart';
import 'package:html2md/html2md.dart' as html2md;
class ArticleDetailScreen extends ConsumerWidget {
final String articleId;
const ArticleDetailScreen({super.key, required this.articleId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final articleAsync = ref.watch(articleDetailProvider(articleId));
return AppScaffold(
body: articleAsync.when(
data:
(article) => AppScaffold(
appBar: AppBar(
leading: const BackButton(),
title: Text(article.title),
),
body: _ArticleDetailContent(article: article),
),
loading: () => const Center(child: LoadingIndicator()),
error:
(error, stackTrace) =>
Center(child: Text('Failed to load article: $error')),
),
);
}
}
class _ArticleDetailContent extends HookConsumerWidget {
final SnWebArticle article;
const _ArticleDetailContent({required this.article});
@override
Widget build(BuildContext context, WidgetRef ref) {
final markdownContent = useMemoized(
() => html2md.convert(article.content ?? ''),
[article],
);
return SingleChildScrollView(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (article.preview?.imageUrl != null)
Image.network(
article.preview!.imageUrl!,
width: double.infinity,
height: 200,
fit: BoxFit.cover,
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
article.title,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
if (article.feed?.title != null)
Text(
article.feed!.title,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const Divider(height: 32),
if (article.content != null)
...MarkdownTextContent.buildGenerator(
isDark: Theme.of(context).brightness == Brightness.dark,
).buildWidgets(markdownContent)
else if (article.preview?.description != null)
Text(article.preview!.description!),
const Gap(24),
FilledButton(
onPressed:
() => launchUrlString(
article.url,
mode: LaunchMode.externalApplication,
),
child: const Text('Read Full Article'),
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,148 @@
import 'package:flutter/material.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:island/widgets/web_article_card.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
part 'articles.g.dart';
@riverpod
class ArticlesListNotifier extends _$ArticlesListNotifier
with CursorPagingNotifierMixin<SnWebArticle> {
static const int _pageSize = 20;
Map<String, dynamic> _params = {};
@override
Future<CursorPagingData<SnWebArticle>> build({
String? feedId,
String? publisherId,
}) async {
_params = {
if (feedId != null) 'feedId': feedId,
if (publisherId != null) 'publisherId': publisherId,
};
return fetch(cursor: null);
}
@override
Future<CursorPagingData<SnWebArticle>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final queryParams = {'limit': _pageSize, 'offset': offset, ..._params};
try {
final response = await client.get(
'/feeds/articles',
queryParameters: queryParams,
);
final List<dynamic> data = response.data;
final articles =
data
.map(
(json) => SnWebArticle.fromJson(json as Map<String, dynamic>),
)
.toList();
final total = int.tryParse(response.headers.value('X-Total') ?? '0') ?? 0;
final hasMore = offset + articles.length < total;
final nextCursor = hasMore ? (offset + articles.length).toString() : null;
return CursorPagingData(
items: articles,
hasMore: hasMore,
nextCursor: nextCursor,
);
} catch (e) {
debugPrint('Error fetching articles: $e');
rethrow;
}
}
}
class SliverArticlesList extends ConsumerWidget {
final String? feedId;
final String? publisherId;
final Color? backgroundColor;
final EdgeInsets? padding;
final Function? onRefresh;
const SliverArticlesList({
super.key,
this.feedId,
this.publisherId,
this.backgroundColor,
this.padding,
this.onRefresh,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return PagingHelperSliverView(
provider: articlesListNotifierProvider(
feedId: feedId,
publisherId: publisherId,
),
futureRefreshable:
articlesListNotifierProvider(
feedId: feedId,
publisherId: publisherId,
).future,
notifierRefreshable:
articlesListNotifierProvider(
feedId: feedId,
publisherId: publisherId,
).notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final article = data.items[index];
return WebArticleCard(article: article, showDetails: true);
},
),
);
}
}
class ArticlesScreen extends ConsumerWidget {
final String? feedId;
final String? publisherId;
final String? title;
const ArticlesScreen({super.key, this.feedId, this.publisherId, this.title});
@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,
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,206 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'articles.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$articlesListNotifierHash() =>
r'924f2344c3bbf0ff7b92fe69e88d3b64a534b538';
/// 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 _$ArticlesListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnWebArticle>> {
late final String? feedId;
late final String? publisherId;
FutureOr<CursorPagingData<SnWebArticle>> build({
String? feedId,
String? publisherId,
});
}
/// See also [ArticlesListNotifier].
@ProviderFor(ArticlesListNotifier)
const articlesListNotifierProvider = ArticlesListNotifierFamily();
/// See also [ArticlesListNotifier].
class ArticlesListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnWebArticle>>> {
/// See also [ArticlesListNotifier].
const ArticlesListNotifierFamily();
/// See also [ArticlesListNotifier].
ArticlesListNotifierProvider call({String? feedId, String? publisherId}) {
return ArticlesListNotifierProvider(
feedId: feedId,
publisherId: publisherId,
);
}
@override
ArticlesListNotifierProvider getProviderOverride(
covariant ArticlesListNotifierProvider provider,
) {
return call(feedId: provider.feedId, publisherId: provider.publisherId);
}
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'articlesListNotifierProvider';
}
/// See also [ArticlesListNotifier].
class ArticlesListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
ArticlesListNotifier,
CursorPagingData<SnWebArticle>
> {
/// See also [ArticlesListNotifier].
ArticlesListNotifierProvider({String? feedId, String? publisherId})
: this._internal(
() =>
ArticlesListNotifier()
..feedId = feedId
..publisherId = publisherId,
from: articlesListNotifierProvider,
name: r'articlesListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$articlesListNotifierHash,
dependencies: ArticlesListNotifierFamily._dependencies,
allTransitiveDependencies:
ArticlesListNotifierFamily._allTransitiveDependencies,
feedId: feedId,
publisherId: publisherId,
);
ArticlesListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.feedId,
required this.publisherId,
}) : super.internal();
final String? feedId;
final String? publisherId;
@override
FutureOr<CursorPagingData<SnWebArticle>> runNotifierBuild(
covariant ArticlesListNotifier notifier,
) {
return notifier.build(feedId: feedId, publisherId: publisherId);
}
@override
Override overrideWith(ArticlesListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: ArticlesListNotifierProvider._internal(
() =>
create()
..feedId = feedId
..publisherId = publisherId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
feedId: feedId,
publisherId: publisherId,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
ArticlesListNotifier,
CursorPagingData<SnWebArticle>
>
createElement() {
return _ArticlesListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ArticlesListNotifierProvider &&
other.feedId == feedId &&
other.publisherId == publisherId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, feedId.hashCode);
hash = _SystemHash.combine(hash, publisherId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin ArticlesListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnWebArticle>> {
/// The parameter `feedId` of this provider.
String? get feedId;
/// The parameter `publisherId` of this provider.
String? get publisherId;
}
class _ArticlesListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
ArticlesListNotifier,
CursorPagingData<SnWebArticle>
>
with ArticlesListNotifierRef {
_ArticlesListNotifierProviderElement(super.provider);
@override
String? get feedId => (origin as ArticlesListNotifierProvider).feedId;
@override
String? get publisherId =>
(origin as ArticlesListNotifierProvider).publisherId;
}
// 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,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/realm.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
@ -20,6 +22,7 @@ import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/realm/realm_card.dart';
import 'package:island/widgets/publisher/publisher_card.dart';
import 'package:island/widgets/web_article_card.dart';
import 'package:styled_widget/styled_widget.dart';
part 'explore.g.dart';
@ -90,37 +93,83 @@ class ExploreScreen extends HookConsumerWidget {
extendBody: false, // Prevent conflicts with tabs navigation
appBar: AppBar(
toolbarHeight: 0,
bottom: TabBar(
controller: tabController,
tabs: [
Tab(
child: Text(
'explore'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: Row(
children: [
Expanded(
child: TabBar(
controller: tabController,
tabAlignment: TabAlignment.start,
isScrollable: true,
dividerColor: Colors.transparent,
tabs: [
Tab(
icon: Tooltip(
message: 'explore'.tr(),
child: Icon(
Symbols.explore,
color:
Theme.of(
context,
).appBarTheme.foregroundColor!,
),
),
),
Tab(
icon: Tooltip(
message: 'exploreFilterSubscriptions'.tr(),
child: Icon(
Symbols.subscriptions,
color:
Theme.of(
context,
).appBarTheme.foregroundColor!,
),
),
),
Tab(
icon: Tooltip(
message: 'exploreFilterFriends'.tr(),
child: Icon(
Symbols.people,
color:
Theme.of(
context,
).appBarTheme.foregroundColor!,
),
),
),
],
),
),
IconButton(
onPressed: () {
context.push('/feeds/articles');
},
icon: Icon(
Symbols.auto_stories,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
tooltip: 'webArticlesStand'.tr(),
),
IconButton(
onPressed: () {
context.push('/posts/search');
},
icon: Icon(
Symbols.search,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
tooltip: 'search'.tr(),
),
],
)
.padding(horizontal: 8)
.border(
bottom: 1 / MediaQuery.of(context).devicePixelRatio,
color: Theme.of(context).dividerColor,
),
),
Tab(
child: Text(
'exploreFilterSubscriptions'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'exploreFilterFriends'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
@ -195,6 +244,7 @@ class _DiscoveryActivityItem extends StatelessWidget {
(switch (type) {
'realm' => 'discoverRealms',
'publisher' => 'discoverPublishers',
'article' => 'discoverWebArticles',
_ => 'unknown',
}).tr(),
style: Theme.of(context).textTheme.titleMedium,
@ -220,6 +270,11 @@ class _DiscoveryActivityItem extends StatelessWidget {
publisher: SnPublisher.fromJson(item['data']),
maxWidth: 280,
);
case 'article':
return WebArticleCard(
article: SnWebArticle.fromJson(item['data']),
maxWidth: 280,
);
default:
return Placeholder();
}
@ -252,7 +307,7 @@ class _ActivityListView extends HookConsumerWidget {
return CustomScrollView(
slivers: [
if (user.hasValue && !contentOnly)
if (user.value != null && !contentOnly)
SliverToBoxAdapter(child: CheckInWidget()),
SliverList.builder(
itemCount: widgetCount,
@ -283,7 +338,7 @@ class _ActivityListView extends HookConsumerWidget {
bottom: 16,
)
: null,
onRefresh: (_) {
onRefresh: () {
activitiesNotifier.forceRefresh();
},
onUpdate: (post) {
@ -341,7 +396,7 @@ class ActivityListNotifier extends _$ActivityListNotifier
if (cursor != null) 'cursor': cursor,
'take': take,
if (filter != null) 'filter': filter,
if (kDebugMode) 'debugInclude': 'realms,publishers',
if (kDebugMode) 'debugInclude': 'realms,publishers,articles',
};
final response = await client.get(

View File

@ -7,7 +7,7 @@ part of 'explore.dart';
// **************************************************************************
String _$activityListNotifierHash() =>
r'57e9dcec944a9f88f8508b69fc91342592f5b349';
r'98b62fb9b958023d2c9e320af7ec1f1244836f49';
/// Copied from Dart SDK
class _SystemHash {

View File

@ -4,19 +4,18 @@ import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/route.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:relative_time/relative_time.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:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
part 'notification.g.dart';
@ -180,36 +179,17 @@ class NotificationScreen extends HookConsumerWidget {
),
),
onTap: () {
if (notification.meta['link'] is String) {
final href = notification.meta['link'];
final uri = Uri.tryParse(href);
if (uri == null) {
showSnackBar(
'brokenLink'.tr(args: []),
action: SnackBarAction(
label: 'copyToClipboard'.tr(),
onPressed: () {
Clipboard.setData(ClipboardData(text: href));
clearSnackBar(context);
},
),
if (notification.meta['action_uri'] != null) {
var uri = notification.meta['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(
notification.meta['action_uri'],
);
return;
} else {
// External URLs
launchUrlString(uri);
}
if (uri.scheme == 'solian') {
context.push(
['', uri.host, ...uri.pathSegments].join('/'),
);
return;
}
showConfirmAlert(
'openLinkConfirmDescription'.tr(args: [href]),
'openLinkConfirm'.tr(),
).then((value) {
if (value) {
launchUrl(uri, mode: LaunchMode.externalApplication);
}
});
}
},
);

View File

@ -15,7 +15,7 @@ import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/compose_shared.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/publishers_modal.dart';
import 'package:island/screens/posts/detail.dart';
import 'package:island/screens/posts/post_detail.dart';
import 'package:island/widgets/post/compose_settings_sheet.dart';
import 'package:island/services/compose_storage_db.dart';
import 'package:island/widgets/post/draft_manager.dart';
@ -33,6 +33,8 @@ sealed class PostComposeInitialState with _$PostComposeInitialState {
String? content,
@Default([]) List<UniversalFile> attachments,
int? visibility,
SnPost? replyingTo,
SnPost? forwardingTo,
}) = _PostComposeInitialState;
factory PostComposeInitialState.fromJson(Map<String, dynamic> json) =>
@ -66,23 +68,22 @@ class PostEditScreen extends HookConsumerWidget {
class PostComposeScreen extends HookConsumerWidget {
final SnPost? originalPost;
final SnPost? repliedPost;
final SnPost? forwardedPost;
final int? type;
final PostComposeInitialState? initialState;
const PostComposeScreen({
super.key,
this.originalPost,
this.repliedPost,
this.forwardedPost,
this.type,
this.initialState,
this.originalPost,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Determine the compose type: auto-detect from edited post or use query parameter
final composeType = originalPost?.type ?? type ?? 0;
final repliedPost = initialState?.replyingTo ?? originalPost?.repliedPost;
final forwardedPost =
initialState?.forwardingTo ?? originalPost?.forwardedPost;
// If type is 1 (article), return ArticleComposeScreen
if (composeType == 1) {
@ -136,7 +137,10 @@ class PostComposeScreen extends HookConsumerWidget {
// Initialize publisher once when data is available
useEffect(() {
if (publishers.value?.isNotEmpty ?? false) {
state.currentPublisher.value = publishers.value!.first;
if (state.currentPublisher.value == null) {
// If no publisher is set, use the first available one
state.currentPublisher.value = publishers.value!.first;
}
}
return null;
}, [publishers]);
@ -480,8 +484,10 @@ class PostComposeScreen extends HookConsumerWidget {
Widget _buildInfoBanner(BuildContext context) {
// When editing, preserve the original replied/forwarded post references
final effectiveRepliedPost = repliedPost ?? originalPost?.repliedPost;
final effectiveForwardedPost = forwardedPost ?? originalPost?.forwardedPost;
final effectiveRepliedPost =
initialState?.replyingTo ?? originalPost?.repliedPost;
final effectiveForwardedPost =
initialState?.forwardingTo ?? originalPost?.forwardedPost;
// Show editing banner when editing a post
if (originalPost != null) {
@ -497,15 +503,15 @@ class PostComposeScreen extends HookConsumerWidget {
size: 16,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
const Gap(4),
const Gap(8),
Text(
'edit'.tr(),
'postEditing'.tr(),
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
],
).padding(all: 16),
).padding(horizontal: 16, vertical: 8),
),
// Show reply/forward banners below editing banner if they exist
if (effectiveRepliedPost != null)
@ -615,6 +621,7 @@ class PostComposeScreen extends HookConsumerWidget {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder:
(context) => DraggableScrollableSheet(
initialChildSize: 0.7,

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$PostComposeInitialState {
String? get title; String? get description; String? get content; List<UniversalFile> get attachments; int? get visibility;
String? get title; String? get description; String? get content; List<UniversalFile> get attachments; int? get visibility; SnPost? get replyingTo; SnPost? get forwardingTo;
/// Create a copy of PostComposeInitialState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -29,16 +29,16 @@ $PostComposeInitialStateCopyWith<PostComposeInitialState> get copyWith => _$Post
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is PostComposeInitialState&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.visibility, visibility) || other.visibility == visibility));
return identical(this, other) || (other.runtimeType == runtimeType&&other is PostComposeInitialState&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.replyingTo, replyingTo) || other.replyingTo == replyingTo)&&(identical(other.forwardingTo, forwardingTo) || other.forwardingTo == forwardingTo));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,title,description,content,const DeepCollectionEquality().hash(attachments),visibility);
int get hashCode => Object.hash(runtimeType,title,description,content,const DeepCollectionEquality().hash(attachments),visibility,replyingTo,forwardingTo);
@override
String toString() {
return 'PostComposeInitialState(title: $title, description: $description, content: $content, attachments: $attachments, visibility: $visibility)';
return 'PostComposeInitialState(title: $title, description: $description, content: $content, attachments: $attachments, visibility: $visibility, replyingTo: $replyingTo, forwardingTo: $forwardingTo)';
}
@ -49,11 +49,11 @@ abstract mixin class $PostComposeInitialStateCopyWith<$Res> {
factory $PostComposeInitialStateCopyWith(PostComposeInitialState value, $Res Function(PostComposeInitialState) _then) = _$PostComposeInitialStateCopyWithImpl;
@useResult
$Res call({
String? title, String? description, String? content, List<UniversalFile> attachments, int? visibility
String? title, String? description, String? content, List<UniversalFile> attachments, int? visibility, SnPost? replyingTo, SnPost? forwardingTo
});
$SnPostCopyWith<$Res>? get replyingTo;$SnPostCopyWith<$Res>? get forwardingTo;
}
/// @nodoc
@ -66,17 +66,43 @@ class _$PostComposeInitialStateCopyWithImpl<$Res>
/// Create a copy of PostComposeInitialState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? title = freezed,Object? description = freezed,Object? content = freezed,Object? attachments = null,Object? visibility = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? title = freezed,Object? description = freezed,Object? content = freezed,Object? attachments = null,Object? visibility = freezed,Object? replyingTo = freezed,Object? forwardingTo = freezed,}) {
return _then(_self.copyWith(
title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable
as List<UniversalFile>,visibility: freezed == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable
as int?,
as int?,replyingTo: freezed == replyingTo ? _self.replyingTo : replyingTo // ignore: cast_nullable_to_non_nullable
as SnPost?,forwardingTo: freezed == forwardingTo ? _self.forwardingTo : forwardingTo // ignore: cast_nullable_to_non_nullable
as SnPost?,
));
}
/// Create a copy of PostComposeInitialState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostCopyWith<$Res>? get replyingTo {
if (_self.replyingTo == null) {
return null;
}
return $SnPostCopyWith<$Res>(_self.replyingTo!, (value) {
return _then(_self.copyWith(replyingTo: value));
});
}/// Create a copy of PostComposeInitialState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostCopyWith<$Res>? get forwardingTo {
if (_self.forwardingTo == null) {
return null;
}
return $SnPostCopyWith<$Res>(_self.forwardingTo!, (value) {
return _then(_self.copyWith(forwardingTo: value));
});
}
}
@ -84,7 +110,7 @@ as int?,
@JsonSerializable()
class _PostComposeInitialState implements PostComposeInitialState {
const _PostComposeInitialState({this.title, this.description, this.content, final List<UniversalFile> attachments = const [], this.visibility}): _attachments = attachments;
const _PostComposeInitialState({this.title, this.description, this.content, final List<UniversalFile> attachments = const [], this.visibility, this.replyingTo, this.forwardingTo}): _attachments = attachments;
factory _PostComposeInitialState.fromJson(Map<String, dynamic> json) => _$PostComposeInitialStateFromJson(json);
@override final String? title;
@ -98,6 +124,8 @@ class _PostComposeInitialState implements PostComposeInitialState {
}
@override final int? visibility;
@override final SnPost? replyingTo;
@override final SnPost? forwardingTo;
/// Create a copy of PostComposeInitialState
/// with the given fields replaced by the non-null parameter values.
@ -112,16 +140,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostComposeInitialState&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.visibility, visibility) || other.visibility == visibility));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostComposeInitialState&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.replyingTo, replyingTo) || other.replyingTo == replyingTo)&&(identical(other.forwardingTo, forwardingTo) || other.forwardingTo == forwardingTo));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,title,description,content,const DeepCollectionEquality().hash(_attachments),visibility);
int get hashCode => Object.hash(runtimeType,title,description,content,const DeepCollectionEquality().hash(_attachments),visibility,replyingTo,forwardingTo);
@override
String toString() {
return 'PostComposeInitialState(title: $title, description: $description, content: $content, attachments: $attachments, visibility: $visibility)';
return 'PostComposeInitialState(title: $title, description: $description, content: $content, attachments: $attachments, visibility: $visibility, replyingTo: $replyingTo, forwardingTo: $forwardingTo)';
}
@ -132,11 +160,11 @@ abstract mixin class _$PostComposeInitialStateCopyWith<$Res> implements $PostCom
factory _$PostComposeInitialStateCopyWith(_PostComposeInitialState value, $Res Function(_PostComposeInitialState) _then) = __$PostComposeInitialStateCopyWithImpl;
@override @useResult
$Res call({
String? title, String? description, String? content, List<UniversalFile> attachments, int? visibility
String? title, String? description, String? content, List<UniversalFile> attachments, int? visibility, SnPost? replyingTo, SnPost? forwardingTo
});
@override $SnPostCopyWith<$Res>? get replyingTo;@override $SnPostCopyWith<$Res>? get forwardingTo;
}
/// @nodoc
@ -149,18 +177,44 @@ class __$PostComposeInitialStateCopyWithImpl<$Res>
/// Create a copy of PostComposeInitialState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? title = freezed,Object? description = freezed,Object? content = freezed,Object? attachments = null,Object? visibility = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? title = freezed,Object? description = freezed,Object? content = freezed,Object? attachments = null,Object? visibility = freezed,Object? replyingTo = freezed,Object? forwardingTo = freezed,}) {
return _then(_PostComposeInitialState(
title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable
as List<UniversalFile>,visibility: freezed == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable
as int?,
as int?,replyingTo: freezed == replyingTo ? _self.replyingTo : replyingTo // ignore: cast_nullable_to_non_nullable
as SnPost?,forwardingTo: freezed == forwardingTo ? _self.forwardingTo : forwardingTo // ignore: cast_nullable_to_non_nullable
as SnPost?,
));
}
/// Create a copy of PostComposeInitialState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostCopyWith<$Res>? get replyingTo {
if (_self.replyingTo == null) {
return null;
}
return $SnPostCopyWith<$Res>(_self.replyingTo!, (value) {
return _then(_self.copyWith(replyingTo: value));
});
}/// Create a copy of PostComposeInitialState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostCopyWith<$Res>? get forwardingTo {
if (_self.forwardingTo == null) {
return null;
}
return $SnPostCopyWith<$Res>(_self.forwardingTo!, (value) {
return _then(_self.copyWith(forwardingTo: value));
});
}
}
// dart format on

View File

@ -18,6 +18,14 @@ _PostComposeInitialState _$PostComposeInitialStateFromJson(
.toList() ??
const [],
visibility: (json['visibility'] as num?)?.toInt(),
replyingTo:
json['replying_to'] == null
? null
: SnPost.fromJson(json['replying_to'] as Map<String, dynamic>),
forwardingTo:
json['forwarding_to'] == null
? null
: SnPost.fromJson(json['forwarding_to'] as Map<String, dynamic>),
);
Map<String, dynamic> _$PostComposeInitialStateToJson(
@ -28,4 +36,6 @@ Map<String, dynamic> _$PostComposeInitialStateToJson(
'content': instance.content,
'attachments': instance.attachments.map((e) => e.toJson()).toList(),
'visibility': instance.visibility,
'replying_to': instance.replyingTo?.toJson(),
'forwarding_to': instance.forwardingTo?.toJson(),
};

View File

@ -12,7 +12,7 @@ import 'package:island/models/post.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/screens/posts/detail.dart';
import 'package:island/screens/posts/post_detail.dart';
import 'package:island/widgets/content/attachment_preview.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown.dart';
@ -238,7 +238,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
children: [
// Publisher row
Card(
margin: EdgeInsets.only(bottom: 8),
margin: EdgeInsets.only(top: 8),
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(12),
@ -265,12 +265,22 @@ class ArticleComposeScreen extends HookConsumerWidget {
});
},
),
const Gap(12),
Text(
state.currentPublisher.value?.name ??
'postPublisherUnselected'.tr(),
style: theme.textTheme.bodyMedium,
),
const Gap(16),
if (state.currentPublisher.value == null)
Text(
'postPublisherUnselected'.tr(),
style: theme.textTheme.bodyMedium,
)
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(state.currentPublisher.value!.nick).bold(),
Text(
'@${state.currentPublisher.value!.name}',
).fontSize(12),
],
),
],
),
),
@ -311,8 +321,15 @@ class ArticleComposeScreen extends HookConsumerWidget {
builder: (context, attachments, _) {
if (attachments.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Gap(16),
Text(
'articleAttachmentHint'.tr(),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
).padding(bottom: 8),
ValueListenableBuilder<Map<int, double>>(
valueListenable: state.attachmentProgress,
builder: (context, progressMap, _) {
@ -322,8 +339,8 @@ class ArticleComposeScreen extends HookConsumerWidget {
children: [
for (var idx = 0; idx < attachments.length; idx++)
SizedBox(
width: 120,
height: 120,
width: 280,
height: 280,
child: AttachmentPreview(
item: attachments[idx],
progress: progressMap[idx],
@ -348,6 +365,12 @@ class ArticleComposeScreen extends HookConsumerWidget {
delta,
);
},
onInsert:
() => ComposeLogic.insertAttachment(
ref,
state,
idx,
),
),
),
],

View File

@ -12,7 +12,7 @@ import 'package:island/widgets/post/post_replies.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'detail.g.dart';
part 'post_detail.g.dart';
@riverpod
Future<SnPost?> post(Ref ref, String id) async {
@ -21,20 +21,44 @@ Future<SnPost?> post(Ref ref, String id) async {
return SnPost.fromJson(resp.data);
}
final postStateProvider =
StateNotifierProvider.family<PostState, AsyncValue<SnPost?>, String>(
(ref, id) => PostState(ref, id),
);
class PostState extends StateNotifier<AsyncValue<SnPost?>> {
final Ref _ref;
final String _id;
PostState(this._ref, this._id) : super(const AsyncValue.loading()) {
// Initialize with the initial post data
_ref.listen<AsyncValue<SnPost?>>(
postProvider(_id),
(_, next) => state = next,
);
}
void updatePost(SnPost? newPost) {
if (newPost != null) {
state = AsyncData(newPost);
}
}
}
class PostDetailScreen extends HookConsumerWidget {
final String id;
const PostDetailScreen({super.key, required this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final post = ref.watch(postProvider(id));
final postState = ref.watch(postStateProvider(id));
final user = ref.watch(userInfoProvider);
final isWide = isWideScreen(context);
return AppScaffold(
appBar: AppBar(title: const Text('Post')),
body: post.when(
body: postState.when(
data: (post) {
return Stack(
fit: StackFit.expand,
@ -49,6 +73,12 @@ class PostDetailScreen extends HookConsumerWidget {
isOpenable: false,
isFullPost: true,
backgroundColor: isWide ? Colors.transparent : null,
onUpdate: (newItem) {
// Update the local state with the new post data
ref
.read(postStateProvider(id).notifier)
.updatePost(newItem);
},
),
const Divider(height: 1),
],
@ -65,16 +95,25 @@ class PostDetailScreen extends HookConsumerWidget {
right: 0,
child: Material(
elevation: 2,
child: PostQuickReply(
parent: post,
onPosted: () {
ref.invalidate(postRepliesNotifierProvider(id));
},
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
top: 16,
horizontal: 16,
),
child: postState
.when(
data:
(post) => PostQuickReply(
parent: post!,
onPosted: () {
ref.invalidate(
postRepliesNotifierProvider(id),
);
},
),
loading: () => const SizedBox.shrink(),
error: (_, _) => const SizedBox.shrink(),
)
.padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
top: 16,
horizontal: 16,
),
),
),
],

View File

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'detail.dart';
part of 'post_detail.dart';
// **************************************************************************
// RiverpodGenerator

View File

@ -0,0 +1,166 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
final postSearchNotifierProvider = StateNotifierProvider.autoDispose<
PostSearchNotifier,
AsyncValue<CursorPagingData<SnPost>>
>((ref) => PostSearchNotifier(ref));
class PostSearchNotifier
extends StateNotifier<AsyncValue<CursorPagingData<SnPost>>> {
final AutoDisposeRef ref;
static const int _pageSize = 20;
String _currentQuery = '';
bool _isLoading = false;
PostSearchNotifier(this.ref) : super(const AsyncValue.loading()) {
state = const AsyncValue.data(
CursorPagingData(items: [], hasMore: false, nextCursor: null),
);
}
Future<void> search(String query) async {
if (_isLoading) return;
_currentQuery = query.trim();
if (_currentQuery.isEmpty) {
state = AsyncValue.data(
CursorPagingData(items: [], hasMore: false, nextCursor: null),
);
return;
}
await fetch(cursor: null);
}
Future<void> fetch({String? cursor}) async {
if (_isLoading) return;
_isLoading = true;
state = const AsyncValue.loading();
try {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/posts/search',
queryParameters: {
'query': _currentQuery,
'offset': offset,
'take': _pageSize,
'useVector': true,
},
);
final data = response.data as List;
final posts = data.map((json) => SnPost.fromJson(json)).toList();
final hasMore = posts.length == _pageSize;
final nextCursor = hasMore ? (offset + posts.length).toString() : null;
state = AsyncValue.data(
CursorPagingData(
items: posts,
hasMore: hasMore,
nextCursor: nextCursor,
),
);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
} finally {
_isLoading = false;
}
}
}
class PostSearchScreen extends ConsumerStatefulWidget {
const PostSearchScreen({super.key});
@override
ConsumerState<PostSearchScreen> createState() => _PostSearchScreenState();
}
class _PostSearchScreenState extends ConsumerState<PostSearchScreen> {
final _searchController = TextEditingController();
final _debounce = Duration(milliseconds: 500);
Timer? _debounceTimer;
@override
void dispose() {
_searchController.dispose();
_debounceTimer?.cancel();
super.dispose();
}
void _onSearchChanged(String query) {
if (_debounceTimer?.isActive ?? false) _debounceTimer!.cancel();
_debounceTimer = Timer(_debounce, () {
ref.read(postSearchNotifierProvider.notifier).search(query);
});
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
title: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search posts...',
border: InputBorder.none,
hintStyle: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
onChanged: _onSearchChanged,
onSubmitted: (value) {
ref.read(postSearchNotifierProvider.notifier).search(value);
},
autofocus: true,
),
),
body: Consumer(
builder: (context, ref, child) {
final searchState = ref.watch(postSearchNotifierProvider);
return searchState.when(
data: (data) {
if (data.items.isEmpty && _searchController.text.isNotEmpty) {
return const Center(child: Text('No results found'));
}
return ListView.builder(
itemCount: data.items.length + (data.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= data.items.length) {
ref
.read(postSearchNotifierProvider.notifier)
.fetch(cursor: data.nextCursor);
return const Center(child: CircularProgressIndicator());
}
final post = data.items[index];
return Column(
children: [PostItem(item: post), const Divider(height: 1)],
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
);
},
),
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
@ -186,7 +187,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
),
onTap: () {
Navigator.pop(context, true);
context.push('/account/${data.name}');
context.push('/account/${data.account?.name}');
},
),
Expanded(

View File

@ -400,7 +400,7 @@ class _PublisherSubscriptionStatusProviderElement
}
String _$publisherAppbarForcegroundColorHash() =>
r'3ff2eebb48d3f3af1907052f471e648f5b14b13c';
r'd781a806a242aea5c1609ec98c97c52fdd9f7db1';
/// See also [publisherAppbarForcegroundColor].
@ProviderFor(publisherAppbarForcegroundColor)

View File

@ -22,7 +22,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
part 'detail.g.dart';
part 'realm_detail.g.dart';
@riverpod
Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async {
@ -77,6 +77,7 @@ class RealmDetailScreen extends HookConsumerWidget {
);
return AppScaffold(
noBackground: false,
body: realmState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),

View File

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'detail.dart';
part of 'realm_detail.dart';
// **************************************************************************
// RiverpodGenerator

View File

@ -294,9 +294,9 @@ class EditRealmScreen extends HookConsumerWidget {
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
@ -344,24 +344,46 @@ class EditRealmScreen extends HookConsumerWidget {
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(labelText: 'description'.tr()),
decoration: InputDecoration(
labelText: 'description'.tr(),
alignLabelWithHint: true,
),
minLines: 3,
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
CheckboxListTile(
title: const Text('isPublic').tr(),
subtitle: const Text('isPublicHint').tr(),
value: isPublic.value,
onChanged: (value) => isPublic.value = value ?? false,
),
CheckboxListTile(
title: const Text('isCommunity').tr(),
subtitle: const Text('isCommunityHint').tr(),
value: isCommunity.value,
onChanged: (value) => isCommunity.value = value ?? false,
Card(
margin: EdgeInsets.zero,
child: Column(
children: [
CheckboxListTile(
secondary: const Icon(Symbols.public),
title: Text('publicRealm').tr(),
subtitle: Text('publicRealmDescription').tr(),
value: isPublic.value,
onChanged: (value) {
isPublic.value = value ?? true;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
CheckboxListTile(
secondary: const Icon(Symbols.travel_explore),
title: Text('communityRealm').tr(),
subtitle: Text('communityRealmDescription').tr(),
value: isCommunity.value,
onChanged: (value) {
isCommunity.value = value ?? false;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
],
),
),
const SizedBox(height: 16),
Align(
@ -435,47 +457,47 @@ class _RealmInviteSheet extends HookConsumerWidget {
(items) =>
items.isEmpty
? Center(
child:
Text(
'invitesEmpty',
textAlign: TextAlign.center,
).tr(),
)
child:
Text(
'invitesEmpty',
textAlign: TextAlign.center,
).tr(),
)
: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final invite = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: invite.realm!.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(invite.realm!.name),
subtitle:
Text(
invite.role >= 100
? 'permissionOwner'
: invite.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite),
),
],
),
);
},
),
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final invite = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: invite.realm!.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(invite.realm!.name),
subtitle:
Text(
invite.role >= 100
? 'permissionOwner'
: invite.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite),
),
],
),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
@ -485,4 +507,4 @@ class _RealmInviteSheet extends HookConsumerWidget {
),
);
}
}
}

View File

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/abuse_report.dart';
import 'package:island/models/abuse_report_type.dart';
import 'package:island/services/abuse_report_service.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:styled_widget/styled_widget.dart';
class AbuseReportDetailScreen extends ConsumerStatefulWidget {
final String reportId;
const AbuseReportDetailScreen({super.key, required this.reportId});
@override
ConsumerState<AbuseReportDetailScreen> createState() =>
_AbuseReportDetailScreenState();
}
class _AbuseReportDetailScreenState
extends ConsumerState<AbuseReportDetailScreen> {
Future<SnAbuseReport>? _reportFuture;
@override
void initState() {
super.initState();
_reportFuture = ref
.read(abuseReportServiceProvider)
.getReport(widget.reportId);
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: const Text('Abuse Report Details')),
body: FutureBuilder<SnAbuseReport>(
future: _reportFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
final report = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow(context, 'Report ID', report.id),
_buildDetailRow(
context,
'Resource Identifier',
report.resourceIdentifier,
),
_buildDetailRow(
context,
'Type',
AbuseReportType.fromValue(report.type).displayName,
),
_buildDetailRow(context, 'Reason', report.reason),
_buildDetailRow(
context,
'Resolved At',
report.resolvedAt?.toString() ?? 'N/A',
),
_buildDetailRow(
context,
'Resolution',
report.resolution ?? 'N/A',
),
_buildDetailRow(context, 'Account ID', report.accountId),
_buildDetailRow(
context,
'Created At',
report.createdAt.toString(),
),
_buildDetailRow(
context,
'Updated At',
report.updatedAt.toString(),
),
],
),
);
} else {
return const Center(child: Text('No data'));
}
},
),
);
}
Widget _buildDetailRow(BuildContext context, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: Theme.of(context).textTheme.titleMedium).bold(),
Text(value, style: Theme.of(context).textTheme.bodyLarge),
],
),
);
}
}

View File

@ -0,0 +1,153 @@
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/abuse_report.dart';
import 'package:island/models/abuse_report_type.dart';
import 'package:island/services/abuse_report_service.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/safety/abuse_report_helper.dart';
class AbuseReportListScreen extends ConsumerStatefulWidget {
const AbuseReportListScreen({super.key});
@override
ConsumerState<AbuseReportListScreen> createState() =>
_AbuseReportListScreenState();
}
class _AbuseReportListScreenState extends ConsumerState<AbuseReportListScreen> {
Future<List<SnAbuseReport>>? _reportsFuture;
@override
void initState() {
super.initState();
_reportsFuture = ref.read(abuseReportServiceProvider).getReports();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: Text('abuseReports').tr()),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
showAbuseReportSheet(context, resourceIdentifier: 'unidentified');
},
),
body: FutureBuilder<List<SnAbuseReport>>(
future: _reportsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
final reports = snapshot.data!;
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: reports.length,
itemBuilder: (context, index) {
final report = reports[index];
return Card(
elevation: 2,
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: InkWell(
onTap: () {
context.push('/safety/reports/me/${report.id}');
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
report.reason,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'ID',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
report.id,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Type',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
AbuseReportType.fromValue(
report.type,
).displayName,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Created at',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
'${report.createdAt.formatRelative(context)} · ${report.createdAt.formatSystem()}',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Status',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
report.resolvedAt != null
? 'Resolved'
: 'Unresolved',
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(
color:
report.resolvedAt != null
? Colors.green
: Colors.orange,
),
),
],
),
],
),
),
),
);
},
);
} else {
return const Center(child: Text('No data'));
}
},
),
);
}
}

View File

@ -341,7 +341,6 @@ class SettingsScreen extends HookConsumerWidget {
];
final behaviorSettings = [
// Auto translate settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsAutoTranslate').tr(),

View File

@ -0,0 +1,25 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/abuse_report.dart';
import 'package:island/pods/network.dart';
final abuseReportServiceProvider = Provider<AbuseReportService>((ref) {
return AbuseReportService(ref);
});
class AbuseReportService {
final Ref ref;
AbuseReportService(this.ref);
Future<SnAbuseReport> getReport(String id) async {
final response =
await ref.read(apiClientProvider).get('/safety/reports/me/$id');
return SnAbuseReport.fromJson(response.data);
}
Future<List<SnAbuseReport>> getReports() async {
final response = await ref.read(apiClientProvider).get('/safety/reports/me');
return (response.data as List)
.map((json) => SnAbuseReport.fromJson(json))
.toList();
}
}

View File

@ -63,9 +63,11 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
});
}
Future<void> subscribePushNotification(Dio apiClient) async {
Future<void> subscribePushNotification(
Dio apiClient, {
bool detailedErrors = false,
}) async {
await FirebaseMessaging.instance.requestPermission(
provisional: true,
alert: true,
badge: true,
sound: true,
@ -97,6 +99,8 @@ Future<void> subscribePushNotification(Dio apiClient) async {
deviceToken,
!kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
);
} else if (detailedErrors) {
throw Exception("Failed to get device token for push notifications.");
}
}

View File

@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/pods/config.dart';
import 'package:island/widgets/tour/techincal_review_intro.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tour.g.dart';
@ -12,7 +11,7 @@ part 'tour.freezed.dart';
const kAppTourStatusKey = "app_tour_statuses";
const List<Tour> kAllTours = [
Tour(id: 'technical_review_intro', isStartup: true),
// Tour(id: 'technical_review_intro', isStartup: true),
];
@freezed
@ -22,7 +21,7 @@ sealed class Tour with _$Tour {
const factory Tour({required String id, required bool isStartup}) = _Tour;
Widget get widget => switch (id) {
'technical_review_intro' => const TechicalReviewIntroWidget(),
// 'technical_review_intro' => const TechicalReviewIntroWidget(),
_ => throw UnimplementedError(),
};
}

View File

@ -0,0 +1,22 @@
String getAbuseReportTypeString(int type) {
switch (type) {
case 0:
return 'Copyright';
case 1:
return 'Harassment';
case 2:
return 'Impersonation';
case 3:
return 'Offensive Content';
case 4:
return 'Spam';
case 5:
return 'Privacy Violation';
case 6:
return 'Illegal Content';
case 7:
return 'Other';
default:
return 'Unknown';
}
}

View File

@ -21,11 +21,23 @@ class AccountName extends StatelessWidget {
@override
Widget build(BuildContext context) {
var nameStyle = (style ?? TextStyle());
if (account.profile.stellarMembership != null) {
nameStyle = nameStyle.copyWith(
color: (switch (account.profile.stellarMembership!.identifier) {
'solian.stellar.primary' => Colors.blueAccent,
'solian.stellar.nova' => Colors.indigoAccent,
'solian.stellar.supernova' => Colors.amberAccent,
_ => null,
}),
);
}
return Row(
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
Flexible(child: Text(account.nick, style: style)),
Flexible(child: Text(account.nick, style: nameStyle)),
if (account.profile.stellarMembership != null)
StellarMembershipMark(membership: account.profile.stellarMembership!),
if (account.profile.verification != null)
@ -87,36 +99,23 @@ class StellarMembershipMark extends StatelessWidget {
Color _getMembershipTierColor(String identifier) {
switch (identifier) {
case 'solian.stellar.primary':
return Colors.amber;
case 'solian.stellar.nova':
return Colors.blue;
case 'solian.stellar.nova':
return Colors.indigo;
case 'solian.stellar.supernova':
return Colors.purple;
return Colors.amber;
default:
return Colors.grey;
}
}
IconData _getMembershipTierIcon(String identifier) {
switch (identifier) {
case 'solian.stellar.primary':
return Symbols.star;
case 'solian.stellar.nova':
return Symbols.auto_awesome;
case 'solian.stellar.supernova':
return Symbols.diamond;
default:
return Symbols.workspace_premium;
}
}
@override
Widget build(BuildContext context) {
if (!membership.isActive) return const SizedBox.shrink();
final tierName = _getMembershipTierName(membership.identifier);
final tierColor = _getMembershipTierColor(membership.identifier);
final tierIcon = _getMembershipTierIcon(membership.identifier);
final tierIcon = Symbols.award_star;
return Tooltip(
richMessage: TextSpan(
@ -124,7 +123,7 @@ class StellarMembershipMark extends StatelessWidget {
children: [
TextSpan(text: '\n'),
TextSpan(
text: 'currentMembership'.tr(args: [tierName]),
text: 'currentMembershipMember'.tr(args: [tierName]),
style: TextStyle(fontWeight: FontWeight.normal),
),
],

View File

@ -59,7 +59,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget {
},
options: Options(method: initialStatus == null ? 'POST' : 'PATCH'),
);
if (user.hasValue) {
if (user.value != null) {
ref.invalidate(accountStatusProvider(user.value!.name));
}
if (!context.mounted) return;

View File

@ -350,7 +350,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
return AnimatedPositioned(
duration: Duration(milliseconds: 1850),
top:
!user.hasValue ||
user.value == null ||
user.value == null ||
websocketState == WebSocketState.connected()
? -indicatorHeight
@ -362,7 +362,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
child: IgnorePointer(
child: Material(
elevation:
!user.hasValue || websocketState == WebSocketState.connected()
user.value == null || websocketState == WebSocketState.connected()
? 0
: 4,
child: AnimatedContainer(

View File

@ -0,0 +1 @@

View File

@ -44,7 +44,7 @@ class AudioCallButton extends HookConsumerWidget {
try {
await apiClient.post('/chat/realtime/$roomId');
if (context.mounted) {
context.push('/chat/call/roomId');
context.push('/chat/call/$roomId');
}
} catch (e) {
showErrorAlert(e);
@ -96,7 +96,7 @@ class AudioCallButton extends HookConsumerWidget {
tooltip: 'Join Ongoing Call',
onPressed: () {
if (context.mounted) {
context.push('/chat/call/roomId');
context.push('/chat/$roomId/call');
}
},
);

View File

@ -360,7 +360,7 @@ class CallOverlayBar extends HookConsumerWidget {
).padding(all: 16),
),
onTap: () {
context.push('/chat/call/callNotifier.roomId!');
context.push('/chat/call/${callNotifier.roomId!}');
},
);
}

View File

@ -15,6 +15,7 @@ class AttachmentPreview extends StatelessWidget {
final double? progress;
final Function(int)? onMove;
final Function? onDelete;
final Function? onInsert;
final Function? onRequestUpload;
const AttachmentPreview({
super.key,
@ -23,6 +24,7 @@ class AttachmentPreview extends StatelessWidget {
this.onRequestUpload,
this.onMove,
this.onDelete,
this.onInsert,
});
@override
@ -104,7 +106,11 @@ class AttachmentPreview extends StatelessWidget {
style: TextStyle(color: Colors.white),
),
Gap(6),
Center(child: LinearProgressIndicator(value: progress)),
Center(
child: LinearProgressIndicator(
value: progress != null ? progress! / 100.0 : null,
),
),
],
),
),
@ -166,6 +172,18 @@ class AttachmentPreview extends StatelessWidget {
onMove?.call(1);
},
),
if (onInsert != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.add,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onInsert?.call();
},
),
],
),
),

View File

@ -13,6 +13,7 @@ import 'package:island/pods/config.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/content/sheet.dart';
import 'package:path/path.dart' show extension;
import 'package:path_provider/path_provider.dart';
import 'package:photo_view/photo_view.dart';
@ -125,6 +126,7 @@ class CloudFileList extends HookConsumerWidget {
if (!disableZoomIn) {
context.pushTransparentRoute(
CloudFileZoomIn(item: files[i], heroTag: heroTags[i]),
rootNavigator: true,
);
}
},
@ -209,6 +211,124 @@ class CloudFileZoomIn extends HookConsumerWidget {
}
}
Widget buildInfoRow(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
child: Row(
children: [
Icon(
icon,
size: 20,
color: Theme.of(context).colorScheme.onSurface,
),
const SizedBox(width: 12),
Text(
label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
const Spacer(),
Flexible(
child: Text(
value,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
textAlign: TextAlign.end,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
String formatFileSize(int bytes) {
if (bytes <= 0) return '0 B';
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
}
void showInfoSheet() {
final theme = Theme.of(context);
final exifData = item.fileMeta?['exif'] as Map<String, dynamic>? ?? {};
showModalBottomSheet(
useRootNavigator: true,
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'File Information',
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildInfoRow(Icons.description, 'Name', item.name),
const Divider(height: 1),
buildInfoRow(
Icons.storage,
'Size',
formatFileSize(item.size),
),
const Divider(height: 1),
buildInfoRow(
Icons.category,
'Type',
item.mimeType?.toUpperCase() ?? 'UNKNOWN',
),
if (exifData.isNotEmpty) ...[
const SizedBox(height: 16),
Text(
'EXIF Data',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
).padding(horizontal: 24),
const SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...exifData.entries.map(
(entry) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${entry.key.contains('-') ? entry.key.split('-').last : entry.key}: ',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
Expanded(
child: Text(
'${entry.value}'.isNotEmpty
? '${entry.value}'
: 'N/A',
style: theme.textTheme.bodyMedium,
),
),
],
),
),
),
],
).padding(horizontal: 24),
],
const SizedBox(height: 16),
],
),
),
),
);
}
return DismissiblePage(
isFullScreen: true,
backgroundColor: Colors.transparent,
@ -287,8 +407,22 @@ class CloudFileZoomIn extends HookConsumerWidget {
left: 16,
right: 16,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Icon(
Icons.info_outline,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
onPressed: showInfoSheet,
),
Spacer(),
IconButton(
icon: Icon(Icons.remove, color: Colors.white),
onPressed: () {

View File

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@ -6,11 +7,14 @@ import 'package:flutter_highlight/themes/a11y-dark.dart';
import 'package:flutter_highlight/themes/a11y-light.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown_latex.dart';
import 'package:markdown/markdown.dart' as markdown;
import 'package:markdown_widget/markdown_widget.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher.dart';
import 'image.dart';
@ -23,6 +27,7 @@ class MarkdownTextContent extends HookConsumerWidget {
final TextStyle? linkStyle;
final EdgeInsets? linesMargin;
final bool isSelectable;
final List<SnCloudFile>? attachments;
const MarkdownTextContent({
super.key,
@ -33,6 +38,7 @@ class MarkdownTextContent extends HookConsumerWidget {
this.linkStyle,
this.isSelectable = false,
this.linesMargin,
this.attachments,
});
@override
@ -74,9 +80,7 @@ class MarkdownTextContent extends HookConsumerWidget {
final url = Uri.tryParse(href);
if (url != null) {
if (url.scheme == 'solian') {
context.push(
['', url.host, ...url.pathSegments].join('/'),
);
context.push(['', url.host, ...url.pathSegments].join('/'));
return;
}
final whitelistDomains = ['solian.app', 'solsynth.dev'];
@ -111,6 +115,29 @@ class MarkdownTextContent extends HookConsumerWidget {
final uri = Uri.parse(url);
if (uri.scheme == 'solian') {
switch (uri.host) {
case 'files':
final file = attachments?.firstWhereOrNull(
(file) => file.id == uri.pathSegments[0],
);
if (file == null) {
return const SizedBox.shrink();
}
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: CloudFileWidget(
item: file,
fit: BoxFit.cover,
).clipRRect(all: 8),
),
);
case 'stickers':
final size = doesEnlargeSticker ? 96.0 : 24.0;
return ClipRRect(
@ -134,26 +161,36 @@ class MarkdownTextContent extends HookConsumerWidget {
);
}
}
final content = UniversalImage(
uri: uri.toString(),
fit: BoxFit.cover,
final content = ConstrainedBox(
constraints: BoxConstraints(maxHeight: 360),
child: UniversalImage(uri: uri.toString(), fit: BoxFit.contain),
);
return content;
},
),
],
),
generator: MarkdownGenerator(
generators: [latexGenerator],
inlineSyntaxList: [
_UserNameCardInlineSyntax(),
_StickerInlineSyntax(),
LatexSyntax(isDark),
],
linesMargin: linesMargin ?? EdgeInsets.symmetric(vertical: 4),
generator: MarkdownTextContent.buildGenerator(
isDark: isDark,
linesMargin: linesMargin,
),
);
}
static MarkdownGenerator buildGenerator({
bool isDark = false,
EdgeInsets? linesMargin,
}) {
return MarkdownGenerator(
generators: [latexGenerator],
inlineSyntaxList: [
_UserNameCardInlineSyntax(),
_StickerInlineSyntax(),
LatexSyntax(isDark),
],
linesMargin: linesMargin ?? EdgeInsets.symmetric(vertical: 4),
);
}
}
class _UserNameCardInlineSyntax extends markdown.InlineSyntax {

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
class EmptyState extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final Widget? action;
const EmptyState({
super.key,
required this.icon,
required this.title,
required this.description,
this.action,
});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 64,
color: Theme.of(context).colorScheme.outline,
),
const SizedBox(height: 16),
Text(
title,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
description,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
if (action != null) ...[
const SizedBox(height: 24),
action!,
],
],
),
),
);
}
}

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
/// A simple loading indicator widget that can be used throughout the app
class LoadingIndicator extends StatelessWidget {
/// The size of the loading indicator
final double size;
/// The color of the loading indicator
final Color? color;
/// Creates a loading indicator
const LoadingIndicator({
super.key,
this.size = 24.0,
this.color,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor: color != null
? AlwaysStoppedAnimation<Color>(
color!,
)
: null,
),
);
}
}

View File

@ -286,7 +286,7 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> {
}
String _formatCurrency(int amount, String currency) {
final value = amount / 100.0;
final value = amount;
return '${value.toStringAsFixed(2)} $currency';
}

View File

@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/file.dart';
@ -97,19 +98,11 @@ class ComposeLogic {
descriptionController: TextEditingController(
text: originalPost?.description,
),
contentController: TextEditingController(
text:
originalPost?.content ??
(forwardedPost != null
? '''> ${forwardedPost.content}
'''
: null),
),
contentController: TextEditingController(text: originalPost?.content),
visibility: ValueNotifier<int>(originalPost?.visibility ?? 0),
submitting: ValueNotifier<bool>(false),
attachmentProgress: ValueNotifier<Map<int, double>>({}),
currentPublisher: ValueNotifier<SnPublisher?>(null),
currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher),
tagsController: tagsController,
categoriesController: categoriesController,
draftId: id,
@ -481,6 +474,23 @@ class ComposeLogic {
state.attachments.value = clone;
}
static void insertAttachment(WidgetRef ref, ComposeState state, int index) {
final attachment = state.attachments.value[index];
if (!attachment.isOnCloud) {
return;
}
final cloudFile = attachment.data as SnCloudFile;
final markdown = '![${cloudFile.name}](solian://files/${cloudFile.id})';
final controller = state.contentController;
final text = controller.text;
final selection = controller.selection;
final newText = text.replaceRange(selection.start, selection.end, markdown);
controller.text = newText;
controller.selection = TextSelection.fromPosition(
TextPosition(offset: selection.start + markdown.length),
);
}
static Future<void> performAction(
WidgetRef ref,
ComposeState state,

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