Compare commits
	
		
			45 Commits
		
	
	
		
			3.0.0+107
			...
			27bc17079e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 27bc17079e | ||
|  | 295188459b | ||
|  | 66115258a7 | ||
|  | 2cf2c515b4 | ||
|  | 925cb2b423 | ||
|  | 0a2804a404 | ||
|  | 12bbcbf69c | ||
|  | 52ce490725 | ||
| 82067fb3aa | |||
| 007acedf29 | |||
| 8e903ec6c1 | |||
| b55e56c3c4 | |||
| 6f9de431b1 | |||
| a8efd26262 | |||
| e367fc3f5c | |||
| 8a1af120ea | |||
| f03f0181f8 | |||
| 6c7d42c31a | |||
| d6c829c26a | |||
| 666a2dfbf5 | |||
| fd979c3a35 | |||
| 847fc6e864 | |||
| 356b7bf01a | |||
| 450d5ebc81 | |||
| f04285848f | |||
| c4becb0a05 | |||
| d22619396b | |||
| fe8640a6db | |||
| ff475d43dd | |||
| 9e8f6d57df | |||
| 79227a12e2 | |||
| a23dcfe702 | |||
| 243ecb3f71 | |||
| b8dec9f798 | |||
| 536375729f | |||
| 5939a1dc5b | |||
| 9d115a5712 | |||
| f511612a53 | |||
| 180fbcc558 | |||
| 047cb9dc0d | |||
| 786f851a97 | |||
| 4deff5a920 | |||
| 0361f031db | |||
| e90b35f19f | |||
| f2829b2012 | 
							
								
								
									
										5
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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*" | ||||
|   | ||||
| @@ -57,6 +57,9 @@ android { | ||||
|  | ||||
| dependencies { | ||||
|     implementation("com.google.android.material:material:1.12.0") | ||||
|     implementation("com.github.bumptech.glide:glide:4.16.0") | ||||
|     implementation("com.squareup.okhttp3:okhttp:4.12.0") | ||||
|     implementation("com.google.firebase:firebase-messaging-ktx") | ||||
| } | ||||
|  | ||||
| flutter { | ||||
|   | ||||
| @@ -46,12 +46,37 @@ | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data android:mimeType="*/*" /> | ||||
|                 <data android:mimeType="image/*" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND_MULTIPLE" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data android:mimeType="*/*" /> | ||||
|                 <data android:mimeType="image/*" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data android:mimeType="video/*" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND_MULTIPLE" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data android:mimeType="video/*" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data android:mimeType="text/*" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data android:mimeType="application/*" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND_MULTIPLE" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data android:mimeType="application/*" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|  | ||||
| @@ -70,6 +95,19 @@ | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|  | ||||
|         <receiver | ||||
|             android:name=".receiver.ReplyReceiver" | ||||
|             android:enabled="true" | ||||
|             android:exported="true" /> | ||||
|  | ||||
|         <service | ||||
|             android:name=".service.MessagingService" | ||||
|             android:exported="false"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="com.google.firebase.MESSAGING_EVENT" /> | ||||
|             </intent-filter> | ||||
|         </service> | ||||
|  | ||||
|         <provider | ||||
|             android:name="androidx.core.content.FileProvider" | ||||
|             android:authorities="dev.solsynth.solian.provider" | ||||
|   | ||||
| @@ -1,14 +0,0 @@ | ||||
| package dev.solsynth.solian | ||||
|  | ||||
| import io.flutter.embedding.android.FlutterActivity | ||||
| import io.flutter.embedding.engine.FlutterEngine | ||||
| import io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin | ||||
|  | ||||
| class MainActivity : FlutterActivity() | ||||
| { | ||||
|     override fun configureFlutterEngine(flutterEngine: FlutterEngine) { | ||||
|         super.configureFlutterEngine(flutterEngine) | ||||
|         // https://github.com/flutter/flutter/issues/153075#issuecomment-2693189362 | ||||
|         flutterEngine.plugins.add(LegacySharedPreferencesPlugin()) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +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") | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,47 @@ | ||||
| package dev.solsynth.solian.network | ||||
|  | ||||
| import android.content.Context | ||||
| 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, message: String, replyTo: String, callback: (Boolean) -> Unit) { | ||||
|         val token = sharedPreferences.getString("flutter.token", null) | ||||
|         if (token == null) { | ||||
|             callback(false) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         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("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) { | ||||
|                 callback(false) | ||||
|             } | ||||
|  | ||||
|             override fun onResponse(call: Call, response: Response) { | ||||
|                 callback(response.isSuccessful) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| package dev.solsynth.solian.receiver | ||||
|  | ||||
| import android.app.NotificationManager | ||||
| import android.content.BroadcastReceiver | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import androidx.core.app.RemoteInput | ||||
| import dev.solsynth.solian.network.ApiClient | ||||
|  | ||||
| class ReplyReceiver : BroadcastReceiver() { | ||||
|     override fun onReceive(context: Context, intent: Intent) { | ||||
|         val remoteInput = RemoteInput.getResultsFromIntent(intent) | ||||
|         if (remoteInput != null) { | ||||
|             val replyText = remoteInput.getCharSequence("key_text_reply").toString() | ||||
|             val roomId = intent.getStringExtra("room_id") | ||||
|             val messageId = intent.getStringExtra("message_id") | ||||
|             val notificationId = intent.getIntExtra("notification_id", 0) | ||||
|  | ||||
|             if (roomId != null && messageId != null) { | ||||
|                 ApiClient(context).sendMessage(roomId, replyText, messageId) { | ||||
|                     val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | ||||
|                     notificationManager.cancel(notificationId) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,102 @@ | ||||
| package dev.solsynth.solian.service | ||||
|  | ||||
| import android.app.PendingIntent | ||||
| import android.content.Intent | ||||
| import android.graphics.Bitmap | ||||
| import android.graphics.drawable.Drawable | ||||
| import android.os.Build | ||||
| import androidx.core.app.NotificationCompat | ||||
| import androidx.core.app.NotificationManagerCompat | ||||
| import androidx.core.app.RemoteInput | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.request.target.CustomTarget | ||||
| import com.bumptech.glide.request.transition.Transition | ||||
| import com.google.firebase.messaging.FirebaseMessagingService | ||||
| import com.google.firebase.messaging.RemoteMessage | ||||
| import dev.solsynth.solian.MainActivity | ||||
| import dev.solsynth.solian.receiver.ReplyReceiver | ||||
| import org.json.JSONObject | ||||
|  | ||||
| class MessagingService: FirebaseMessagingService() { | ||||
|     override fun onMessageReceived(remoteMessage: RemoteMessage) { | ||||
|         val type = remoteMessage.data["type"] | ||||
|         if (type == "messages.new") { | ||||
|             handleMessageNotification(remoteMessage) | ||||
|         } else { | ||||
|             // Handle other notification types | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun handleMessageNotification(remoteMessage: RemoteMessage) { | ||||
|         val data = remoteMessage.data | ||||
|         val metaString = data["meta"] ?: return | ||||
|         val meta = JSONObject(metaString) | ||||
|  | ||||
|         val pfp = meta.optString("pfp", null) | ||||
|         val roomId = meta.optString("room_id", null) | ||||
|         val messageId = meta.optString("message_id", null) | ||||
|  | ||||
|         val notificationId = System.currentTimeMillis().toInt() | ||||
|  | ||||
|         val replyLabel = "Reply" | ||||
|         val remoteInput = RemoteInput.Builder("key_text_reply") | ||||
|             .setLabel(replyLabel) | ||||
|             .build() | ||||
|  | ||||
|         val replyIntent = Intent(this, ReplyReceiver::class.java).apply { | ||||
|             putExtra("room_id", roomId) | ||||
|             putExtra("message_id", messageId) | ||||
|             putExtra("notification_id", notificationId) | ||||
|         } | ||||
|  | ||||
|         val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||||
|             PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE | ||||
|         } else { | ||||
|             PendingIntent.FLAG_UPDATE_CURRENT | ||||
|         } | ||||
|  | ||||
|         val replyPendingIntent = PendingIntent.getBroadcast( | ||||
|             applicationContext, | ||||
|             notificationId, | ||||
|             replyIntent, | ||||
|             pendingIntentFlags | ||||
|         ) | ||||
|  | ||||
|         val action = NotificationCompat.Action.Builder( | ||||
|             android.R.drawable.ic_menu_send, | ||||
|             replyLabel, | ||||
|             replyPendingIntent | ||||
|         ) | ||||
|             .addRemoteInput(remoteInput) | ||||
|             .build() | ||||
|  | ||||
|         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") | ||||
|             .setSmallIcon(android.R.drawable.ic_dialog_info) | ||||
|             .setContentTitle(remoteMessage.notification?.title) | ||||
|             .setContentText(remoteMessage.notification?.body) | ||||
|             .setPriority(NotificationCompat.PRIORITY_HIGH) | ||||
|             .setContentIntent(pendingIntent) | ||||
|             .addAction(action) | ||||
|  | ||||
|         if (pfp != null) { | ||||
|             Glide.with(applicationContext) | ||||
|                 .asBitmap() | ||||
|                 .load(pfp) | ||||
|                 .into(object : CustomTarget<Bitmap>() { | ||||
|                     override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) { | ||||
|                         notificationBuilder.setLargeIcon(resource) | ||||
|                         NotificationManagerCompat.from(applicationContext).notify(notificationId, notificationBuilder.build()) | ||||
|                     } | ||||
|  | ||||
|                     override fun onLoadCleared(placeholder: Drawable?) {} | ||||
|                 }) | ||||
|         } else { | ||||
|             NotificationManagerCompat.from(this).notify(notificationId, notificationBuilder.build()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -48,28 +48,6 @@ | ||||
|     "deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.", | ||||
|     "somethingWentWrong": "Something went wrong...", | ||||
|     "deletePost": "Delete Post", | ||||
|   "safetyReport": "Report", | ||||
|   "safetyReportTitle": "Safety Report", | ||||
|   "safetyReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.", | ||||
|   "safetyReportType": "Report Type", | ||||
|   "safetyReportReason": "Additional Details", | ||||
|   "safetyReportReasonHint": "Please provide more details about the issue...", | ||||
|   "safetyReportSubmit": "Submit Report", | ||||
|   "safetyReportSubmitting": "Submitting...", | ||||
|   "safetyReportSuccess": "Report submitted successfully. Thank you for helping keep our community safe.", | ||||
|   "safetyReportError": "Failed to submit report. Please try again.", | ||||
|   "safetyReportReasonRequired": "Please provide details about the issue", | ||||
|   "safetyReportTypeSpam": "Spam or Misleading", | ||||
|   "safetyReportTypeHarassment": "Harassment or Abuse", | ||||
|   "safetyReportTypeHateSpeech": "Hate Speech", | ||||
|   "safetyReportTypeViolence": "Violence or Threats", | ||||
|   "safetyReportTypeAdultContent": "Adult Content", | ||||
|   "safetyReportTypeIntellectualProperty": "Intellectual Property Violation", | ||||
|   "safetyReportTypeOther": "Other", | ||||
|   "safetyReportTypeInappropriate": "Inappropriate Content", | ||||
|   "safetyReportTypeCopyright": "Copyright Violation", | ||||
|   "safetyReportSuccessTitle": "Report Submitted", | ||||
|   "safetyReportErrorTitle": "Error", | ||||
|     "deletePostHint": "Are you sure to delete this post?", | ||||
|     "copyLink": "Copy Link", | ||||
|     "postCreateAccountTitle": "Thanks for joining!", | ||||
| @@ -427,27 +405,6 @@ | ||||
|     "settingsKeyboardShortcutNewMessage": "New Message", | ||||
|     "settingsKeyboardShortcutCloseDialog": "Close Dialog", | ||||
|     "close": "Close", | ||||
|   "drafts": "Drafts", | ||||
|   "noDrafts": "No drafts yet", | ||||
|   "articleDrafts": "Article drafts", | ||||
|   "postDrafts": "Post drafts", | ||||
|   "saveDraft": "Save draft", | ||||
|   "draftSaved": "Draft saved", | ||||
|   "draftSaveFailed": "Failed to save draft", | ||||
|   "clearAllDrafts": "Clear All Drafts", | ||||
|   "clearAllDraftsConfirm": "Are you sure you want to delete all drafts? This action cannot be undone.", | ||||
|   "clearAll": "Clear All", | ||||
|   "untitled": "Untitled", | ||||
|   "noContent": "No content", | ||||
|   "justNow": "Just now", | ||||
|   "minutesAgo": "{} minutes ago", | ||||
|   "hoursAgo": "{} hours ago", | ||||
|   "daysAgo": "{} days ago", | ||||
|   "public": "Public", | ||||
|   "unlisted": "Unlisted", | ||||
|   "friends": "Friends", | ||||
|   "selected": "Selected", | ||||
|   "private": "Private", | ||||
|     "contactMethod": "Contact Method", | ||||
|     "contactMethodType": "Contact Type", | ||||
|     "contactMethodTypeEmail": "Email", | ||||
| @@ -465,7 +422,6 @@ | ||||
|     "contactMethodDelete": "Delete Contact", | ||||
|     "contactMethodNew": "New Contact Method", | ||||
|     "contactMethodContentEmpty": "Contact content cannot be empty", | ||||
|   "postContentEmpty": "Post 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", | ||||
| @@ -565,50 +521,5 @@ | ||||
|     "orderId": "Order ID", | ||||
|     "enterOrderId": "Enter your order ID", | ||||
|     "restore": "Restore", | ||||
|   "keyboardShortcuts": "Keyboard Shortcuts", | ||||
|   "share": "Share", | ||||
|   "sharePost": "Share Post", | ||||
|   "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!", | ||||
|   "navigateToChat": "Navigate to Chat", | ||||
|   "wouldYouLikeToNavigateToChat": "Would you like to navigate to the chat?", | ||||
|   "abuseReport": "Report", | ||||
|   "abuseReportTitle": "Report Content", | ||||
|   "abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.", | ||||
|   "abuseReportType": "Report Type", | ||||
|   "abuseReportReason": "Additional Details", | ||||
|   "abuseReportReasonHint": "Please provide more details about the issue...", | ||||
|   "abuseReportSubmit": "Submit Report", | ||||
|   "abuseReportSuccess": "Report submitted successfully. Thank you for helping keep our community safe.", | ||||
|   "abuseReportError": "Failed to submit report. Please try again.", | ||||
|   "abuseReportReasonRequired": "Please provide details about the issue", | ||||
|   "abuseReportSuccessTitle": "Report Submitted", | ||||
|   "abuseReportErrorTitle": "Error", | ||||
|   "abuseReportTypeSpam": "Spam or Misleading", | ||||
|   "abuseReportTypeHarassment": "Harassment or Abuse", | ||||
|   "abuseReportTypeInappropriate": "Inappropriate Content", | ||||
|   "abuseReportTypeViolence": "Violence or Threats", | ||||
|   "abuseReportTypeCopyright": "Copyright Violation", | ||||
|   "abuseReportTypeImpersonation": "Impersonation", | ||||
|   "abuseReportTypeOffensiveContent": "Offensive Content", | ||||
|   "abuseReportTypePrivacyViolation": "Privacy Violation", | ||||
|   "abuseReportTypeIllegalContent": "Illegal Content", | ||||
|   "abuseReportTypeOther": "Other" | ||||
|     "keyboardShortcuts": "Keyboard Shortcuts" | ||||
| } | ||||
| @@ -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": "首篇帖子", | ||||
| @@ -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,80 @@ | ||||
|     "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": "系统分享失败:{}", | ||||
|   "copiedToClipboard": "已复制到剪贴板", | ||||
|   "failedToCopy": "复制失败:{}", | ||||
|   "noChatRoomsAvailable": "没有可用的聊天室", | ||||
|   "failedToLoadChats": "加载聊天失败", | ||||
|   "unknownChat": "未知聊天" | ||||
|     "membershipPurchaseSuccess": "好耶,会员购买成功!", | ||||
|     "paymentError": "付款失败: {error}", | ||||
|     "usePinInstead": "使用 PIN 码", | ||||
|     "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": "恢复购买", | ||||
|     "restorePurchaseDescription": "输入您付款的提供商和订单 ID 以恢复您的购买。", | ||||
|     "provider": "平台", | ||||
|     "selectProvider": "选择一个平台", | ||||
|     "orderId": "订单 ID", | ||||
|     "enterOrderId": "输入您的订单 ID", | ||||
|     "restore": "恢复", | ||||
|     "keyboardShortcuts": "键盘快捷键" | ||||
| } | ||||
| @@ -10,6 +10,8 @@ | ||||
|     "loginEnterPassword": "輸入驗證碼", | ||||
|     "loginSuccess": "已登入為 {}", | ||||
|     "loginGreeting": "歡迎回來!", | ||||
|     "loginOr": "Or login with\nthird parties", | ||||
|     "loginInProgress": "Logging you in...", | ||||
|     "username": "使用者名稱", | ||||
|     "usernameCannotChangeHint": "使用者名稱建立後無法更改。", | ||||
|     "usernameLookupHint": "您也可以輸入電子郵件地址。", | ||||
| @@ -63,6 +65,8 @@ | ||||
|     "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": "結識志同道合的朋友、建立社群等等。", | ||||
| @@ -70,9 +74,10 @@ | ||||
|     "deleteRealm": "刪除領域", | ||||
|     "deleteRealmHint": "確定要刪除此領域嗎?這也將刪除該領域下的所有頻道、發佈者和貼文。", | ||||
|     "explore": "探索", | ||||
|     "exploreFilterSubscriptions": "Subscriptions", | ||||
|     "exploreFilterFriends": "Friends", | ||||
|     "account": "帳號", | ||||
|     "name": "名稱", | ||||
|   "description": "描述", | ||||
|     "slug": "代稱", | ||||
|     "slugHint": "此代稱將用於 URL 以存取此資源,它應該是獨一無二且 URL 安全的。", | ||||
|     "createChatRoom": "建立聊天室", | ||||
| @@ -98,13 +103,20 @@ | ||||
|     "permissionModerator": "版主", | ||||
|     "permissionMember": "成員", | ||||
|     "reply": "回覆", | ||||
|     "repliesCount": { | ||||
|         "zero": "No reply", | ||||
|         "one": "{} reply", | ||||
|         "other": "{} replies" | ||||
|     }, | ||||
|     "forward": "轉發", | ||||
|     "repliedTo": "回覆了", | ||||
|     "forwarded": "轉發了", | ||||
|     "hasAttachments": { | ||||
|         "one": "{} attachment", | ||||
|         "other": "{}個附件" | ||||
|     }, | ||||
|     "postHasAttachments": { | ||||
|         "one": "{} attachment", | ||||
|         "other": "{}個附件" | ||||
|     }, | ||||
|     "edited": "已編輯", | ||||
| @@ -112,6 +124,7 @@ | ||||
|     "addPhoto": "新增照片", | ||||
|     "addFile": "新增檔案", | ||||
|     "createDirectMessage": "建立新私人訊息", | ||||
|     "gotoDirectMessage": "Go to DM", | ||||
|     "react": "反應", | ||||
|     "reactions": { | ||||
|         "zero": "反應", | ||||
| @@ -124,6 +137,25 @@ | ||||
|     "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": "透過簽到獲取您的財富提示和每日獎勵。", | ||||
| @@ -132,14 +164,11 @@ | ||||
|     "checkInResultLevel2": "一個普通的日子", | ||||
|     "checkInResultLevel3": "好運", | ||||
|     "checkInResultLevel4": "最佳運氣", | ||||
|   "checkInResultLevelShort0": "最差", | ||||
|   "checkInResultLevelShort1": "壞", | ||||
|   "checkInResultLevelShort2": "普通", | ||||
|   "checkInResultLevelShort3": "好", | ||||
|   "checkInResultLevelShort4": "最佳", | ||||
|     "checkInActivityTitle": "{} 在 {} 簽到並獲得了 {}", | ||||
|     "eventCalander": "活動日曆", | ||||
|     "eventCalanderEmpty": "該日無活動。", | ||||
|     "fortuneGraph": "Fortune Trend", | ||||
|     "noFortuneData": "No fortune data available for this month.", | ||||
|     "creatorHub": "創作者中心", | ||||
|     "creatorHubDescription": "管理貼文、分析等。", | ||||
|     "developerPortal": "開發者入口", | ||||
| @@ -231,6 +260,7 @@ | ||||
|     "creatorHubUnselectedHint": "選擇/建立一個發佈者以開始使用。", | ||||
|     "relationships": "關係", | ||||
|     "addFriend": "傳送好友邀請", | ||||
|     "addFriendShort": "Add as Friend", | ||||
|     "addFriendHint": "將朋友新增到您的關係清單。", | ||||
|     "pendingRequest": "待處理", | ||||
|     "waitingRequest": "等待中", | ||||
| @@ -275,11 +305,13 @@ | ||||
|     "posts": "貼文", | ||||
|     "settingsBackgroundImage": "背景圖片", | ||||
|     "settingsBackgroundImageClear": "清除背景圖片", | ||||
|     "settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image", | ||||
|     "messageNone": "沒有內容可顯示", | ||||
|     "unreadMessages": { | ||||
|         "one": "{} 條未讀訊息", | ||||
|         "other": "{} 條未讀訊息" | ||||
|     }, | ||||
|     "chatBreakNone": "無", | ||||
|     "settingsRealmCompactView": "精簡領域視圖", | ||||
|     "settingsMixedFeed": "混合動態", | ||||
|     "settingsAutoTranslate": "自動翻譯", | ||||
| @@ -287,11 +319,118 @@ | ||||
|     "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": "全部", | ||||
| @@ -307,7 +446,47 @@ | ||||
|     "chatBreakCleared": "聊天暫停已清除。", | ||||
|     "chatBreakCustom": "自訂時長", | ||||
|     "chatBreakEnterMinutes": "輸入分鐘數", | ||||
|   "chatBreakNone": "無", | ||||
|     "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": "等級進度", | ||||
| @@ -335,37 +514,12 @@ | ||||
|     "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": "未知聊天" | ||||
|     "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" | ||||
| } | ||||
| @@ -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,11 +80,13 @@ 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): | ||||
|     - Flutter | ||||
|   - flutter_secure_storage (3.3.1): | ||||
|   - flutter_secure_storage (6.0.0): | ||||
|     - Flutter | ||||
|   - flutter_timezone (0.0.1): | ||||
|     - Flutter | ||||
| @@ -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,18 +361,19 @@ 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: 50035aef357c5a8bdd67fd6bc81370d46efc4d16 | ||||
|   flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 | ||||
|   flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 | ||||
|   flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 | ||||
|   flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117 | ||||
| @@ -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 | ||||
|   | ||||
| @@ -857,7 +857,7 @@ | ||||
| 				INFOPLIST_FILE = SolianShareExtension/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension; | ||||
| 				INFOPLIST_KEY_NSHumanReadableCopyright = ""; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 18.5; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| @@ -900,7 +900,7 @@ | ||||
| 				INFOPLIST_FILE = SolianShareExtension/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension; | ||||
| 				INFOPLIST_KEY_NSHumanReadableCopyright = ""; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 18.5; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| @@ -940,7 +940,7 @@ | ||||
| 				INFOPLIST_FILE = SolianShareExtension/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension; | ||||
| 				INFOPLIST_KEY_NSHumanReadableCopyright = ""; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 18.5; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| @@ -979,7 +979,7 @@ | ||||
| 				INFOPLIST_FILE = SolianNotificationService/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService; | ||||
| 				INFOPLIST_KEY_NSHumanReadableCopyright = ""; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 18.5; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| @@ -1021,7 +1021,7 @@ | ||||
| 				INFOPLIST_FILE = SolianNotificationService/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService; | ||||
| 				INFOPLIST_KEY_NSHumanReadableCopyright = ""; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 18.5; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| @@ -1060,7 +1060,7 @@ | ||||
| 				INFOPLIST_FILE = SolianNotificationService/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService; | ||||
| 				INFOPLIST_KEY_NSHumanReadableCopyright = ""; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 18.5; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
|   | ||||
| @@ -11,6 +11,21 @@ import UIKit | ||||
|     ) -> Bool { | ||||
|         UNUserNotificationCenter.current().delegate = notifyDelegate | ||||
|          | ||||
|         let replyableMessageCategory = UNNotificationCategory( | ||||
|             identifier: "REPLYABLE_MESSAGE", | ||||
|             actions: [ | ||||
|                 UNTextInputNotificationAction( | ||||
|                     identifier: "reply_action", | ||||
|                     title: "Reply", | ||||
|                     options: [] | ||||
|                 ), | ||||
|             ], | ||||
|             intentIdentifiers: [], | ||||
|             options: [] | ||||
|         ) | ||||
|          | ||||
|         UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory]) | ||||
|          | ||||
|         GeneratedPluginRegistrant.register(with: self) | ||||
|         return super.application(application, didFinishLaunchingWithOptions: launchOptions) | ||||
|     } | ||||
|   | ||||
| @@ -10,14 +10,26 @@ import Alamofire | ||||
|  | ||||
| class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate { | ||||
|     func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { | ||||
|         if let textResponse = response as? UNTextInputNotificationResponse { | ||||
|             let content = response.notification.request.content | ||||
|             guard let metadata = content.userInfo["meta"] as? [AnyHashable: Any] else { | ||||
|         guard let textResponse = response as? UNTextInputNotificationResponse else { | ||||
|             completionHandler() | ||||
|             return | ||||
|         } | ||||
|  | ||||
|             var token: String? = UserDefaults.standard.getFlutterToken() | ||||
|             if token == nil { | ||||
|         let content = response.notification.request.content | ||||
|          | ||||
|         // Only handle replies for new messages | ||||
|         guard let notificationType = content.userInfo["type"] as? String, notificationType == "messages.new" else { | ||||
|             completionHandler() | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         guard let metadata = content.userInfo["meta"] as? [AnyHashable: Any] else { | ||||
|             completionHandler() | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         guard let token = UserDefaults.standard.getFlutterToken() else { | ||||
|             completionHandler() | ||||
|             return | ||||
|         } | ||||
|          | ||||
| @@ -30,7 +42,7 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate { | ||||
|         ] | ||||
|          | ||||
|         AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: HTTPHeaders( | ||||
|                 [HTTPHeader(name: "Authorization", value: "AtField \(token!)")] | ||||
|             [HTTPHeader(name: "Authorization", value: "AtField \(token)")] | ||||
|         )) | ||||
|             .validate() | ||||
|             .responseString { response in | ||||
| @@ -41,9 +53,8 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate { | ||||
|                     print("Failed to send chat reply message: \(error)") | ||||
|                     break | ||||
|                 } | ||||
|                 } | ||||
|         } | ||||
|          | ||||
|                 // Call completion handler after network request is finished | ||||
|                 completionHandler() | ||||
|             } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -60,21 +60,7 @@ class NotificationService: UNNotificationServiceExtension { | ||||
|          | ||||
|         let pfpIdentifier = meta["pfp"] as? String | ||||
|          | ||||
|         let replyableMessageCategory = UNNotificationCategory( | ||||
|             identifier: content.categoryIdentifier, | ||||
|             actions: [ | ||||
|                 UNTextInputNotificationAction( | ||||
|                     identifier: "reply_action", | ||||
|                     title: "Reply", | ||||
|                     options: [] | ||||
|                 ), | ||||
|             ], | ||||
|             intentIdentifiers: [], | ||||
|             options: [] | ||||
|         ) | ||||
|          | ||||
|         UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory]) | ||||
|         content.categoryIdentifier = replyableMessageCategory.identifier | ||||
|         content.categoryIdentifier = "REPLYABLE_MESSAGE" | ||||
|          | ||||
|         let metaCopy = meta as? [String: Any] ?? [:] | ||||
|         let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil | ||||
|   | ||||
| @@ -71,25 +71,32 @@ class MessageRepository { | ||||
|     bool synced = false, | ||||
|   }) async { | ||||
|     try { | ||||
|       // For initial load, fetch latest messages in the background to sync. | ||||
|       if (offset == 0 && !synced) { | ||||
|         // Not awaiting this is intentional, for a quicker UI response. | ||||
|         // The UI should rely on a stream from the database to get updates. | ||||
|         _fetchAndCacheMessages(room.id, offset: 0, take: take).catchError((_) { | ||||
|           // Best effort, errors will be handled by later fetches. | ||||
|           return <LocalChatMessage>[]; | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       final localMessages = await _getCachedMessages( | ||||
|         room.id, | ||||
|         offset: offset, | ||||
|         take: take, | ||||
|       ); | ||||
|  | ||||
|       // If it already synced with the remote, skip this | ||||
|       if (offset == 0 && !synced) { | ||||
|         // Fetch latest messages | ||||
|         _fetchAndCacheMessages(room.id, offset: offset, take: take); | ||||
|  | ||||
|       // If local cache has messages, return them. This is the common case for scrolling up. | ||||
|       if (localMessages.isNotEmpty) { | ||||
|         return localMessages; | ||||
|       } | ||||
|       } | ||||
|  | ||||
|       // If local cache is empty, we've probably reached the end of cached history. | ||||
|       // Fetch from remote. This will also be hit on first load if cache is empty. | ||||
|       return await _fetchAndCacheMessages(room.id, offset: offset, take: take); | ||||
|     } catch (e) { | ||||
|       // If API fails but we have local messages, return them | ||||
|       // Final fallback to cache in case of network errors during fetch. | ||||
|       final localMessages = await _getCachedMessages( | ||||
|         room.id, | ||||
|         offset: offset, | ||||
| @@ -117,24 +124,26 @@ class MessageRepository { | ||||
|     final dbLocalMessages = | ||||
|         dbMessages.map(_database.companionToMessage).toList(); | ||||
|  | ||||
|     // Combine with pending messages | ||||
|     // Combine with pending messages for the first page | ||||
|     if (offset == 0) { | ||||
|       final pendingForRoom = | ||||
|           pendingMessages.values.where((msg) => msg.roomId == roomId).toList(); | ||||
|  | ||||
|     // Sort by timestamp descending (newest first) | ||||
|       final allMessages = [...pendingForRoom, ...dbLocalMessages]; | ||||
|       allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); | ||||
|  | ||||
|     // Apply pagination | ||||
|     if (offset >= allMessages.length) { | ||||
|       return []; | ||||
|       // Remove duplicates by ID, preserving the order | ||||
|       final uniqueMessages = <LocalChatMessage>[]; | ||||
|       final seenIds = <String>{}; | ||||
|       for (final message in allMessages) { | ||||
|         if (seenIds.add(message.id)) { | ||||
|           uniqueMessages.add(message); | ||||
|         } | ||||
|       } | ||||
|       return uniqueMessages; | ||||
|     } | ||||
|  | ||||
|     final end = | ||||
|         (offset + take) > allMessages.length | ||||
|             ? allMessages.length | ||||
|             : (offset + take); | ||||
|     return allMessages.sublist(offset, end); | ||||
|     return dbLocalMessages; | ||||
|   } | ||||
|  | ||||
|   Future<List<LocalChatMessage>> _fetchAndCacheMessages( | ||||
|   | ||||
| @@ -7,6 +7,7 @@ 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:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:image_picker_android/image_picker_android.dart'; | ||||
| @@ -18,7 +19,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
| import 'package:island/route.dart'; | ||||
| import 'package:island/screens/tabs.dart'; | ||||
|  | ||||
| import 'package:island/services/notify.dart'; | ||||
| import 'package:island/services/timezone.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| @@ -29,6 +30,12 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface. | ||||
| import 'package:flutter_native_splash/flutter_native_splash.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| @pragma('vm:entry-point') | ||||
| Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { | ||||
|   await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); | ||||
|   log('Handling a background message: ${message.messageId}'); | ||||
| } | ||||
|  | ||||
| void main() async { | ||||
|   final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); | ||||
|   if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { | ||||
| @@ -43,6 +50,7 @@ void main() async { | ||||
|     await Firebase.initializeApp( | ||||
|       options: DefaultFirebaseOptions.currentPlatform, | ||||
|     ); | ||||
|     FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); | ||||
|     log("[SplashScreen] Firebase is ready!"); | ||||
|   } catch (err) { | ||||
|     showErrorAlert(err); | ||||
| @@ -125,7 +133,7 @@ void main() async { | ||||
|   ); | ||||
| } | ||||
|  | ||||
| final appRouter = AppRouter(); | ||||
| // Router will be provided through Riverpod | ||||
|  | ||||
| final globalOverlay = GlobalKey<OverlayState>(); | ||||
|  | ||||
| @@ -141,7 +149,8 @@ class IslandApp extends HookConsumerWidget { | ||||
|         var uri = notification.data['action_uri'] as String; | ||||
|         if (uri.startsWith('/')) { | ||||
|           // In-app routes | ||||
|           appRouter.pushPath(notification.data['action_uri']); | ||||
|           final router = ref.read(routerProvider); | ||||
|           router.go(notification.data['action_uri']); | ||||
|         } else { | ||||
|           // External links | ||||
|           launchUrlString(uri); | ||||
| @@ -150,17 +159,52 @@ class IslandApp extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     useEffect(() { | ||||
|       Future(() async { | ||||
|         RemoteMessage? initialMessage = | ||||
|             await FirebaseMessaging.instance.getInitialMessage(); | ||||
|         if (initialMessage != null) { | ||||
|           handleMessage(initialMessage); | ||||
|       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); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|         FirebaseMessaging.onMessageOpenedApp.listen(handleMessage); | ||||
|       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); | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|       return null; | ||||
|       // When the app is opened from a terminated state. | ||||
|       FirebaseMessaging.instance.getInitialMessage().then((message) { | ||||
|         if (message != null) { | ||||
|           handleMessage(message); | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|       // When the app is in the background and opened. | ||||
|       final onMessageOpenedAppSubscription = FirebaseMessaging | ||||
|           .onMessageOpenedApp | ||||
|           .listen(handleMessage); | ||||
|  | ||||
|       // When the app is in the foreground. | ||||
|       final onMessageSubscription = FirebaseMessaging.onMessage.listen(( | ||||
|         message, | ||||
|       ) { | ||||
|         log('Foreground message received: ${message.messageId}'); | ||||
|         handleMessage(message); | ||||
|       }); | ||||
|  | ||||
|       return () { | ||||
|         onMessageOpenedAppSubscription.cancel(); | ||||
|         onMessageSubscription.cancel(); | ||||
|       }; | ||||
|     }, []); | ||||
|  | ||||
|     useEffect(() { | ||||
| @@ -183,20 +227,13 @@ class IslandApp extends HookConsumerWidget { | ||||
|       return null; | ||||
|     }, []); | ||||
|  | ||||
|     final router = ref.watch(routerProvider); | ||||
|  | ||||
|     return MaterialApp.router( | ||||
|       theme: theme?.light, | ||||
|       darkTheme: theme?.dark, | ||||
|       themeMode: ThemeMode.system, | ||||
|       routerConfig: appRouter.config( | ||||
|         navigatorObservers: | ||||
|             () => [ | ||||
|               TabNavigationObserver( | ||||
|                 onChange: (route) { | ||||
|                   ref.read(currentRouteProvider.notifier).state = route; | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|       ), | ||||
|       routerConfig: router, | ||||
|       supportedLocales: context.supportedLocales, | ||||
|       localizationsDelegates: [ | ||||
|         ...context.localizationDelegates, | ||||
| @@ -210,10 +247,8 @@ class IslandApp extends HookConsumerWidget { | ||||
|           initialEntries: [ | ||||
|             OverlayEntry( | ||||
|               builder: | ||||
|                   (_) => WindowScaffold( | ||||
|                     router: appRouter, | ||||
|                     child: child ?? const SizedBox.shrink(), | ||||
|                   ), | ||||
|                   (_) => | ||||
|                       WindowScaffold(child: child ?? const SizedBox.shrink()), | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|   | ||||
							
								
								
									
										34
									
								
								lib/models/auto_completion.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								lib/models/auto_completion.dart
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
							
								
								
									
										410
									
								
								lib/models/auto_completion.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										410
									
								
								lib/models/auto_completion.freezed.dart
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										63
									
								
								lib/models/auto_completion.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								lib/models/auto_completion.g.dart
									
									
									
									
									
										Normal 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, | ||||
|     }; | ||||
| @@ -13,7 +13,8 @@ sealed class SnChatRoom with _$SnChatRoom { | ||||
|     required String? name, | ||||
|     required String? description, | ||||
|     required int type, | ||||
|     required bool isPublic, | ||||
|     @Default(false) bool isPublic, | ||||
|     @Default(false) bool isCommunity, | ||||
|     required SnCloudFile? picture, | ||||
|     required SnCloudFile? background, | ||||
|     required String? realmId, | ||||
|   | ||||
| @@ -16,7 +16,7 @@ T _$identity<T>(T value) => value; | ||||
| /// @nodoc | ||||
| mixin _$SnChatRoom { | ||||
|  | ||||
|  String get id; String? get name; String? get description; int get type; bool get isPublic; SnCloudFile? get picture; SnCloudFile? get background; String? get realmId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List<SnChatMember>? get members; | ||||
|  String get id; String? get name; String? get description; int get type; bool get isPublic; bool get isCommunity; SnCloudFile? get picture; SnCloudFile? get background; String? get realmId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List<SnChatMember>? get members; | ||||
| /// Create a copy of SnChatRoom | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -29,16 +29,16 @@ $SnChatRoomCopyWith<SnChatRoom> get copyWith => _$SnChatRoomCopyWithImpl<SnChatR | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other.members, members)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other.members, members)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,picture,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(members)); | ||||
| int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,isCommunity,picture,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(members)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, picture: $picture, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)'; | ||||
|   return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, isCommunity: $isCommunity, picture: $picture, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -49,7 +49,7 @@ abstract mixin class $SnChatRoomCopyWith<$Res>  { | ||||
|   factory $SnChatRoomCopyWith(SnChatRoom value, $Res Function(SnChatRoom) _then) = _$SnChatRoomCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String? name, String? description, int type, bool isPublic, SnCloudFile? picture, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members | ||||
|  String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -66,13 +66,14 @@ class _$SnChatRoomCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnChatRoom | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? isCommunity = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: freezed == 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?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||
| as int,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable | ||||
| as bool,isCommunity: null == isCommunity ? _self.isCommunity : isCommunity // ignore: cast_nullable_to_non_nullable | ||||
| as bool,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?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable | ||||
| @@ -128,14 +129,15 @@ $SnRealmCopyWith<$Res>? get realm { | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnChatRoom implements SnChatRoom { | ||||
|   const _SnChatRoom({required this.id, required this.name, required this.description, required this.type, required this.isPublic, required this.picture, required this.background, required this.realmId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt, required final  List<SnChatMember>? members}): _members = members; | ||||
|   const _SnChatRoom({required this.id, required this.name, required this.description, required this.type, this.isPublic = false, this.isCommunity = false, required this.picture, required this.background, required this.realmId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt, required final  List<SnChatMember>? members}): _members = members; | ||||
|   factory _SnChatRoom.fromJson(Map<String, dynamic> json) => _$SnChatRoomFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String? name; | ||||
| @override final  String? description; | ||||
| @override final  int type; | ||||
| @override final  bool isPublic; | ||||
| @override@JsonKey() final  bool isPublic; | ||||
| @override@JsonKey() final  bool isCommunity; | ||||
| @override final  SnCloudFile? picture; | ||||
| @override final  SnCloudFile? background; | ||||
| @override final  String? realmId; | ||||
| @@ -166,16 +168,16 @@ Map<String, dynamic> toJson() { | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._members, _members)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._members, _members)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,picture,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_members)); | ||||
| int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,isCommunity,picture,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_members)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, picture: $picture, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)'; | ||||
|   return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, isCommunity: $isCommunity, picture: $picture, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -186,7 +188,7 @@ abstract mixin class _$SnChatRoomCopyWith<$Res> implements $SnChatRoomCopyWith<$ | ||||
|   factory _$SnChatRoomCopyWith(_SnChatRoom value, $Res Function(_SnChatRoom) _then) = __$SnChatRoomCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String? name, String? description, int type, bool isPublic, SnCloudFile? picture, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members | ||||
|  String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -203,13 +205,14 @@ class __$SnChatRoomCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnChatRoom | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? isCommunity = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { | ||||
|   return _then(_SnChatRoom( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: freezed == 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?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||
| as int,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable | ||||
| as bool,isCommunity: null == isCommunity ? _self.isCommunity : isCommunity // ignore: cast_nullable_to_non_nullable | ||||
| as bool,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?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable | ||||
|   | ||||
| @@ -11,7 +11,8 @@ _SnChatRoom _$SnChatRoomFromJson(Map<String, dynamic> json) => _SnChatRoom( | ||||
|   name: json['name'] as String?, | ||||
|   description: json['description'] as String?, | ||||
|   type: (json['type'] as num).toInt(), | ||||
|   isPublic: json['is_public'] as bool, | ||||
|   isPublic: json['is_public'] as bool? ?? false, | ||||
|   isCommunity: json['is_community'] as bool? ?? false, | ||||
|   picture: | ||||
|       json['picture'] == null | ||||
|           ? null | ||||
| @@ -44,6 +45,7 @@ Map<String, dynamic> _$SnChatRoomToJson(_SnChatRoom instance) => | ||||
|       'description': instance.description, | ||||
|       'type': instance.type, | ||||
|       'is_public': instance.isPublic, | ||||
|       'is_community': instance.isCommunity, | ||||
|       'picture': instance.picture?.toJson(), | ||||
|       'background': instance.background?.toJson(), | ||||
|       'realm_id': instance.realmId, | ||||
|   | ||||
							
								
								
									
										71
									
								
								lib/models/custom_app.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								lib/models/custom_app.dart
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
							
								
								
									
										771
									
								
								lib/models/custom_app.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										771
									
								
								lib/models/custom_app.freezed.dart
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										137
									
								
								lib/models/custom_app.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								lib/models/custom_app.g.dart
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										14
									
								
								lib/models/developer.dart
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
							
								
								
									
										148
									
								
								lib/models/developer.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								lib/models/developer.freezed.dart
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										15
									
								
								lib/models/developer.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								lib/models/developer.g.dart
									
									
									
									
									
										Normal 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}; | ||||
| @@ -1,6 +1,8 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:island/models/file.dart'; | ||||
| import 'package:island/models/user.dart'; | ||||
| import 'package:island/models/post_category.dart'; | ||||
| import 'package:island/models/post_tag.dart'; | ||||
| import 'package:island/models/publisher.dart'; | ||||
|  | ||||
| part 'post.freezed.dart'; | ||||
| part 'post.g.dart'; | ||||
| @@ -30,11 +32,11 @@ 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<dynamic> tags, | ||||
|     @Default([]) List<dynamic> categories, | ||||
|     @Default([]) List<PostTag> tags, | ||||
|     @Default([]) List<PostCategory> categories, | ||||
|     @Default([]) List<dynamic> collections, | ||||
|     @Default(null) DateTime? createdAt, | ||||
|     @Default(null) DateTime? updatedAt, | ||||
| @@ -45,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({ | ||||
|   | ||||
| @@ -16,7 +16,7 @@ T _$identity<T>(T value) => value; | ||||
| /// @nodoc | ||||
| mixin _$SnPost { | ||||
|  | ||||
|  String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; List<dynamic> get reactions; List<dynamic> get tags; List<dynamic> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated; | ||||
|  String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; List<dynamic> get reactions; List<PostTag> get tags; List<PostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated; | ||||
| /// Create a copy of SnPost | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -49,7 +49,7 @@ abstract mixin class $SnPostCopyWith<$Res>  { | ||||
|   factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<dynamic> tags, List<dynamic> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -94,8 +94,8 @@ as List<SnCloudFile>,publisher: null == publisher ? _self.publisher : publisher | ||||
| as SnPublisher,reactionsCount: null == reactionsCount ? _self.reactionsCount : reactionsCount // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, int>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,categories: null == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,collections: null == collections ? _self.collections : collections // ignore: cast_nullable_to_non_nullable | ||||
| as List<PostTag>,categories: null == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable | ||||
| as List<PostCategory>,collections: null == collections ? _self.collections : collections // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,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 | ||||
| @@ -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<dynamic> tags = const [], final  List<dynamic> 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; | ||||
| @@ -210,15 +210,15 @@ class _SnPost implements SnPost { | ||||
|   return EqualUnmodifiableListView(_reactions); | ||||
| } | ||||
|  | ||||
|  final  List<dynamic> _tags; | ||||
| @override@JsonKey() List<dynamic> get tags { | ||||
|  final  List<PostTag> _tags; | ||||
| @override@JsonKey() List<PostTag> get tags { | ||||
|   if (_tags is EqualUnmodifiableListView) return _tags; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_tags); | ||||
| } | ||||
|  | ||||
|  final  List<dynamic> _categories; | ||||
| @override@JsonKey() List<dynamic> get categories { | ||||
|  final  List<PostCategory> _categories; | ||||
| @override@JsonKey() List<PostCategory> get categories { | ||||
|   if (_categories is EqualUnmodifiableListView) return _categories; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_categories); | ||||
| @@ -269,7 +269,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> { | ||||
|   factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<dynamic> tags, List<dynamic> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -314,8 +314,8 @@ as List<SnCloudFile>,publisher: null == publisher ? _self.publisher : publisher | ||||
| as SnPublisher,reactionsCount: null == reactionsCount ? _self._reactionsCount : reactionsCount // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, int>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,categories: null == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,collections: null == collections ? _self._collections : collections // ignore: cast_nullable_to_non_nullable | ||||
| as List<PostTag>,categories: null == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable | ||||
| as List<PostCategory>,collections: null == collections ? _self._collections : collections // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,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 | ||||
| @@ -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 { | ||||
|  | ||||
|   | ||||
| @@ -48,18 +48,23 @@ _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()), | ||||
|       ) ?? | ||||
|       const {}, | ||||
|   reactions: json['reactions'] as List<dynamic>? ?? const [], | ||||
|   tags: json['tags'] as List<dynamic>? ?? const [], | ||||
|   categories: json['categories'] as List<dynamic>? ?? const [], | ||||
|   tags: | ||||
|       (json['tags'] as List<dynamic>?) | ||||
|           ?.map((e) => PostTag.fromJson(e as Map<String, dynamic>)) | ||||
|           .toList() ?? | ||||
|       const [], | ||||
|   categories: | ||||
|       (json['categories'] as List<dynamic>?) | ||||
|           ?.map((e) => PostCategory.fromJson(e as Map<String, dynamic>)) | ||||
|           .toList() ?? | ||||
|       const [], | ||||
|   collections: json['collections'] as List<dynamic>? ?? const [], | ||||
|   createdAt: | ||||
|       json['created_at'] == null | ||||
| @@ -102,8 +107,8 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{ | ||||
|   'publisher': instance.publisher.toJson(), | ||||
|   'reactions_count': instance.reactionsCount, | ||||
|   'reactions': instance.reactions, | ||||
|   'tags': instance.tags, | ||||
|   'categories': instance.categories, | ||||
|   'tags': instance.tags.map((e) => e.toJson()).toList(), | ||||
|   'categories': instance.categories.map((e) => e.toJson()).toList(), | ||||
|   'collections': instance.collections, | ||||
|   'created_at': instance.createdAt?.toIso8601String(), | ||||
|   'updated_at': instance.updatedAt?.toIso8601String(), | ||||
| @@ -111,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(), | ||||
|   | ||||
							
								
								
									
										19
									
								
								lib/models/post_category.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								lib/models/post_category.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
|  | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
|  | ||||
| part 'post_category.freezed.dart'; | ||||
| part 'post_category.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| sealed class PostCategory with _$PostCategory { | ||||
|   const factory PostCategory({ | ||||
|     required String id, | ||||
|     required String slug, | ||||
|     String? name, | ||||
|     @Default([]) List<SnPost> posts, | ||||
|   }) = _PostCategory; | ||||
|  | ||||
|   factory PostCategory.fromJson(Map<String, dynamic> json) => | ||||
|       _$PostCategoryFromJson(json); | ||||
| } | ||||
							
								
								
									
										163
									
								
								lib/models/post_category.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								lib/models/post_category.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| // 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 'post_category.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$PostCategory { | ||||
|  | ||||
|  String get id; String get slug; String? get name; List<SnPost> get posts; | ||||
| /// Create a copy of PostCategory | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $PostCategoryCopyWith<PostCategory> get copyWith => _$PostCategoryCopyWithImpl<PostCategory>(this as PostCategory, _$identity); | ||||
|  | ||||
|   /// Serializes this PostCategory to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is PostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(posts)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'PostCategory(id: $id, slug: $slug, name: $name, posts: $posts)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $PostCategoryCopyWith<$Res>  { | ||||
|   factory $PostCategoryCopyWith(PostCategory value, $Res Function(PostCategory) _then) = _$PostCategoryCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String slug, String? name, List<SnPost> posts | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$PostCategoryCopyWithImpl<$Res> | ||||
|     implements $PostCategoryCopyWith<$Res> { | ||||
|   _$PostCategoryCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final PostCategory _self; | ||||
|   final $Res Function(PostCategory) _then; | ||||
|  | ||||
| /// Create a copy of PostCategory | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String?,posts: null == posts ? _self.posts : posts // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnPost>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _PostCategory implements PostCategory { | ||||
|   const _PostCategory({required this.id, required this.slug, this.name, final  List<SnPost> posts = const []}): _posts = posts; | ||||
|   factory _PostCategory.fromJson(Map<String, dynamic> json) => _$PostCategoryFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String slug; | ||||
| @override final  String? name; | ||||
|  final  List<SnPost> _posts; | ||||
| @override@JsonKey() List<SnPost> get posts { | ||||
|   if (_posts is EqualUnmodifiableListView) return _posts; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_posts); | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Create a copy of PostCategory | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$PostCategoryCopyWith<_PostCategory> get copyWith => __$PostCategoryCopyWithImpl<_PostCategory>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$PostCategoryToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(_posts)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'PostCategory(id: $id, slug: $slug, name: $name, posts: $posts)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$PostCategoryCopyWith<$Res> implements $PostCategoryCopyWith<$Res> { | ||||
|   factory _$PostCategoryCopyWith(_PostCategory value, $Res Function(_PostCategory) _then) = __$PostCategoryCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String slug, String? name, List<SnPost> posts | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$PostCategoryCopyWithImpl<$Res> | ||||
|     implements _$PostCategoryCopyWith<$Res> { | ||||
|   __$PostCategoryCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _PostCategory _self; | ||||
|   final $Res Function(_PostCategory) _then; | ||||
|  | ||||
| /// Create a copy of PostCategory | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) { | ||||
|   return _then(_PostCategory( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String?,posts: null == posts ? _self._posts : posts // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnPost>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
							
								
								
									
										27
									
								
								lib/models/post_category.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								lib/models/post_category.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'post_category.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _PostCategory _$PostCategoryFromJson(Map<String, dynamic> json) => | ||||
|     _PostCategory( | ||||
|       id: json['id'] as String, | ||||
|       slug: json['slug'] as String, | ||||
|       name: json['name'] as String?, | ||||
|       posts: | ||||
|           (json['posts'] as List<dynamic>?) | ||||
|               ?.map((e) => SnPost.fromJson(e as Map<String, dynamic>)) | ||||
|               .toList() ?? | ||||
|           const [], | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$PostCategoryToJson(_PostCategory instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'slug': instance.slug, | ||||
|       'name': instance.name, | ||||
|       'posts': instance.posts.map((e) => e.toJson()).toList(), | ||||
|     }; | ||||
							
								
								
									
										19
									
								
								lib/models/post_tag.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								lib/models/post_tag.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
|  | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
|  | ||||
| part 'post_tag.freezed.dart'; | ||||
| part 'post_tag.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| sealed class PostTag with _$PostTag { | ||||
|   const factory PostTag({ | ||||
|     required String id, | ||||
|     required String slug, | ||||
|     String? name, | ||||
|     @Default([]) List<SnPost> posts, | ||||
|   }) = _PostTag; | ||||
|  | ||||
|   factory PostTag.fromJson(Map<String, dynamic> json) => | ||||
|       _$PostTagFromJson(json); | ||||
| } | ||||
							
								
								
									
										163
									
								
								lib/models/post_tag.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								lib/models/post_tag.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| // 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 'post_tag.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$PostTag { | ||||
|  | ||||
|  String get id; String get slug; String? get name; List<SnPost> get posts; | ||||
| /// Create a copy of PostTag | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $PostTagCopyWith<PostTag> get copyWith => _$PostTagCopyWithImpl<PostTag>(this as PostTag, _$identity); | ||||
|  | ||||
|   /// Serializes this PostTag to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is PostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(posts)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'PostTag(id: $id, slug: $slug, name: $name, posts: $posts)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $PostTagCopyWith<$Res>  { | ||||
|   factory $PostTagCopyWith(PostTag value, $Res Function(PostTag) _then) = _$PostTagCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String slug, String? name, List<SnPost> posts | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$PostTagCopyWithImpl<$Res> | ||||
|     implements $PostTagCopyWith<$Res> { | ||||
|   _$PostTagCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final PostTag _self; | ||||
|   final $Res Function(PostTag) _then; | ||||
|  | ||||
| /// Create a copy of PostTag | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String?,posts: null == posts ? _self.posts : posts // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnPost>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _PostTag implements PostTag { | ||||
|   const _PostTag({required this.id, required this.slug, this.name, final  List<SnPost> posts = const []}): _posts = posts; | ||||
|   factory _PostTag.fromJson(Map<String, dynamic> json) => _$PostTagFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String slug; | ||||
| @override final  String? name; | ||||
|  final  List<SnPost> _posts; | ||||
| @override@JsonKey() List<SnPost> get posts { | ||||
|   if (_posts is EqualUnmodifiableListView) return _posts; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_posts); | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Create a copy of PostTag | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$PostTagCopyWith<_PostTag> get copyWith => __$PostTagCopyWithImpl<_PostTag>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$PostTagToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(_posts)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'PostTag(id: $id, slug: $slug, name: $name, posts: $posts)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$PostTagCopyWith<$Res> implements $PostTagCopyWith<$Res> { | ||||
|   factory _$PostTagCopyWith(_PostTag value, $Res Function(_PostTag) _then) = __$PostTagCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String slug, String? name, List<SnPost> posts | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$PostTagCopyWithImpl<$Res> | ||||
|     implements _$PostTagCopyWith<$Res> { | ||||
|   __$PostTagCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _PostTag _self; | ||||
|   final $Res Function(_PostTag) _then; | ||||
|  | ||||
| /// Create a copy of PostTag | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) { | ||||
|   return _then(_PostTag( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String?,posts: null == posts ? _self._posts : posts // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnPost>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
							
								
								
									
										25
									
								
								lib/models/post_tag.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								lib/models/post_tag.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'post_tag.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _PostTag _$PostTagFromJson(Map<String, dynamic> json) => _PostTag( | ||||
|   id: json['id'] as String, | ||||
|   slug: json['slug'] as String, | ||||
|   name: json['name'] as String?, | ||||
|   posts: | ||||
|       (json['posts'] as List<dynamic>?) | ||||
|           ?.map((e) => SnPost.fromJson(e as Map<String, dynamic>)) | ||||
|           .toList() ?? | ||||
|       const [], | ||||
| ); | ||||
|  | ||||
| Map<String, dynamic> _$PostTagToJson(_PostTag instance) => <String, dynamic>{ | ||||
|   'id': instance.id, | ||||
|   'slug': instance.slug, | ||||
|   'name': instance.name, | ||||
|   'posts': instance.posts.map((e) => e.toJson()).toList(), | ||||
| }; | ||||
							
								
								
									
										47
									
								
								lib/models/publisher.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								lib/models/publisher.dart
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
							
								
								
									
										488
									
								
								lib/models/publisher.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										488
									
								
								lib/models/publisher.freezed.dart
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										103
									
								
								lib/models/publisher.g.dart
									
									
									
									
									
										Normal 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(), | ||||
|     }; | ||||
| @@ -10,8 +10,8 @@ sealed class SnRealm with _$SnRealm { | ||||
|   const factory SnRealm({ | ||||
|     required String id, | ||||
|     required String slug, | ||||
|     required String name, | ||||
|     required String description, | ||||
|     @Default('') String name, | ||||
|     @Default('') String description, | ||||
|     required String? verifiedAs, | ||||
|     required DateTime? verifiedAt, | ||||
|     required bool isCommunity, | ||||
|   | ||||
| @@ -117,13 +117,13 @@ $SnCloudFileCopyWith<$Res>? get background { | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnRealm implements SnRealm { | ||||
|   const _SnRealm({required this.id, required this.slug, required this.name, required this.description, required this.verifiedAs, required this.verifiedAt, required this.isCommunity, required this.isPublic, required this.picture, required this.background, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}); | ||||
|   const _SnRealm({required this.id, required this.slug, this.name = '', this.description = '', required this.verifiedAs, required this.verifiedAt, required this.isCommunity, required this.isPublic, required this.picture, required this.background, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}); | ||||
|   factory _SnRealm.fromJson(Map<String, dynamic> json) => _$SnRealmFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String slug; | ||||
| @override final  String name; | ||||
| @override final  String description; | ||||
| @override@JsonKey() final  String name; | ||||
| @override@JsonKey() final  String description; | ||||
| @override final  String? verifiedAs; | ||||
| @override final  DateTime? verifiedAt; | ||||
| @override final  bool isCommunity; | ||||
|   | ||||
| @@ -9,8 +9,8 @@ part of 'realm.dart'; | ||||
| _SnRealm _$SnRealmFromJson(Map<String, dynamic> json) => _SnRealm( | ||||
|   id: json['id'] as String, | ||||
|   slug: json['slug'] as String, | ||||
|   name: json['name'] as String, | ||||
|   description: json['description'] as String, | ||||
|   name: json['name'] as String? ?? '', | ||||
|   description: json['description'] as String? ?? '', | ||||
|   verifiedAs: json['verified_as'] as String?, | ||||
|   verifiedAt: | ||||
|       json['verified_at'] == null | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										64
									
								
								lib/models/webfeed.dart
									
									
									
									
									
										Normal 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>); | ||||
| } | ||||
							
								
								
									
										584
									
								
								lib/models/webfeed.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										584
									
								
								lib/models/webfeed.freezed.dart
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										103
									
								
								lib/models/webfeed.g.dart
									
									
									
									
									
										Normal 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(), | ||||
|     }; | ||||
							
								
								
									
										31
									
								
								lib/pods/article_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								lib/pods/article_detail.dart
									
									
									
									
									
										Normal 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'); | ||||
|     } | ||||
|   }, | ||||
| ); | ||||
							
								
								
									
										1
									
								
								lib/pods/article_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								lib/pods/article_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
|  | ||||
| @@ -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); | ||||
| @@ -32,7 +27,6 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> { | ||||
|     state = const AsyncValue.data(null); | ||||
|     final prefs = _ref.read(sharedPreferencesProvider); | ||||
|     await prefs.remove(kTokenPairStoreKey); | ||||
|     _ref.invalidate(userInfoProvider); | ||||
|     _ref.invalidate(tokenProvider); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										123
									
								
								lib/pods/webfeed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								lib/pods/webfeed.dart
									
									
									
									
									
										Normal 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, | ||||
|     ); | ||||
							
								
								
									
										499
									
								
								lib/route.dart
									
									
									
									
									
								
							
							
						
						
									
										499
									
								
								lib/route.dart
									
									
									
									
									
								
							| @@ -1,98 +1,433 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| 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/article_detail_screen.dart'; | ||||
| import 'package:island/screens/account.dart'; | ||||
| import 'package:island/screens/notification.dart'; | ||||
| import 'package:island/screens/wallet.dart'; | ||||
| import 'package:island/screens/account/relationship.dart'; | ||||
| import 'package:island/screens/account/profile.dart'; | ||||
| import 'package:island/screens/account/me/update.dart'; | ||||
| import 'package:island/screens/account/leveling.dart'; | ||||
| import 'package:island/screens/account/me/settings.dart'; | ||||
| import 'package:island/screens/chat/chat.dart'; | ||||
| import 'package:island/screens/chat/room.dart'; | ||||
| import 'package:island/screens/chat/room_detail.dart'; | ||||
| import 'package:island/screens/chat/call.dart'; | ||||
| import 'package:island/screens/creators/hub.dart'; | ||||
| import 'package:island/screens/creators/posts/post_manage_list.dart'; | ||||
| import 'package:island/screens/creators/stickers/stickers.dart'; | ||||
| import 'package:island/screens/creators/stickers/pack_detail.dart'; | ||||
| import 'package:island/screens/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/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/realm_detail.dart'; | ||||
| import 'package:island/screens/account/event_calendar.dart'; | ||||
| import 'package:island/screens/discovery/realms.dart'; | ||||
|  | ||||
| @AutoRouterConfig(replaceInRouteName: 'Screen|Page,Route') | ||||
| class AppRouter extends RootStackRouter { | ||||
|   @override | ||||
|   RouteType get defaultRouteType => RouteType.adaptive(); | ||||
| // Shell route keys for nested navigation | ||||
| final rootNavigatorKey = GlobalKey<NavigatorState>(); | ||||
| final _shellNavigatorKey = GlobalKey<NavigatorState>(); | ||||
| final _tabsShellKey = GlobalKey<NavigatorState>(); | ||||
|  | ||||
|   @override | ||||
|   List<AutoRoute> get routes => [ | ||||
|     AutoRoute(path: '/', page: AppWrapper.page, children: _appRoutes), | ||||
|   ]; | ||||
|  | ||||
|   List<AutoRoute> get _appRoutes => [ | ||||
| // Provider for the router | ||||
| final routerProvider = Provider<GoRouter>((ref) { | ||||
|   return GoRouter( | ||||
|     navigatorKey: rootNavigatorKey, | ||||
|     initialLocation: '/', | ||||
|     routes: [ | ||||
|       ShellRoute( | ||||
|         navigatorKey: _shellNavigatorKey, | ||||
|         builder: (context, state, child) { | ||||
|           return AppWrapper(child: child); | ||||
|         }, | ||||
|         routes: [ | ||||
|           // Standalone routes without bottom navigation | ||||
|     AutoRoute(page: PostComposeRoute.page, path: 'posts/compose'), | ||||
|     AutoRoute(page: PostEditRoute.page, path: 'posts/:id/edit'), | ||||
|     AutoRoute(page: CallRoute.page, path: 'chat/:id/call'), | ||||
|     AutoRoute(page: EventCalanderRoute.page, path: 'account/:name/calendar'), | ||||
|           GoRoute( | ||||
|             path: '/posts/compose', | ||||
|             builder: | ||||
|                 (context, state) => PostComposeScreen( | ||||
|                   initialState: state.extra as PostComposeInitialState?, | ||||
|                 ), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/posts/:id/edit', | ||||
|             builder: (context, state) { | ||||
|               final id = state.pathParameters['id']!; | ||||
|               return PostEditScreen(id: id); | ||||
|             }, | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/chat/:id/call', | ||||
|             builder: (context, state) { | ||||
|               final id = state.pathParameters['id']!; | ||||
|               return CallScreen(roomId: id); | ||||
|             }, | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/account/:name/calendar', | ||||
|             builder: (context, state) { | ||||
|               final name = state.pathParameters['name']!; | ||||
|               return EventCalanderScreen(name: name); | ||||
|             }, | ||||
|           ), | ||||
|           ShellRoute( | ||||
|             builder: | ||||
|                 (context, state, child) => CreatorHubShellScreen(child: child), | ||||
|             routes: [ | ||||
|               GoRoute( | ||||
|                 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: '/creators/:name/stickers', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   return StickersScreen(pubName: name); | ||||
|                 }, | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/creators/:name/stickers/new', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   return NewStickerPacksScreen(pubName: name); | ||||
|                 }, | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/creators/:name/stickers/:packId/edit', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   final packId = state.pathParameters['packId']!; | ||||
|                   return EditStickerPacksScreen(pubName: name, packId: packId); | ||||
|                 }, | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/creators/:name/stickers/:packId', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   final packId = state.pathParameters['packId']!; | ||||
|                   return StickerPackDetailScreen(pubName: name, id: packId); | ||||
|                 }, | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/creators/:name/stickers/:packId/new', | ||||
|                 builder: (context, state) { | ||||
|                   final packId = state.pathParameters['packId']!; | ||||
|                   return NewStickersScreen(packId: packId); | ||||
|                 }, | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/creators/:name/stickers/:packId/:id/edit', | ||||
|                 builder: (context, state) { | ||||
|                   final packId = state.pathParameters['packId']!; | ||||
|                   final id = state.pathParameters['id']!; | ||||
|                   return EditStickersScreen(id: id, packId: packId); | ||||
|                 }, | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/creators/new', | ||||
|                 builder: (context, state) => const NewPublisherScreen(), | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/creators/:name/edit', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   return EditPublisherScreen(name: name); | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|           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']!, | ||||
|                     ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|  | ||||
|     // Main tabs with bottom navigation and shell routes for desktop layout | ||||
|     AutoRoute( | ||||
|       page: TabsRoute.page, | ||||
|       path: '', | ||||
|       children: [ | ||||
|         AutoRoute( | ||||
|           page: ExploreShellRoute.page, | ||||
|           path: '', | ||||
|           children: [ | ||||
|             AutoRoute(page: ExploreRoute.page, path: ''), | ||||
|             AutoRoute(page: PostDetailRoute.page, path: 'posts/:id'), | ||||
|             AutoRoute( | ||||
|               page: PublisherProfileRoute.page, | ||||
|               path: 'publishers/:name', | ||||
|           // 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( | ||||
|             path: '/auth/login', | ||||
|             builder: (context, state) => const LoginScreen(), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/auth/create-account', | ||||
|             builder: (context, state) => const CreateAccountScreen(), | ||||
|           ), | ||||
|  | ||||
|           // Other routes | ||||
|           GoRoute( | ||||
|             path: '/settings', | ||||
|             builder: (context, state) => const SettingsScreen(), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/about', | ||||
|             builder: (context, state) => const AboutScreen(), | ||||
|           ), | ||||
|  | ||||
|           // Main tabs with TabsScreen shell | ||||
|           ShellRoute( | ||||
|             navigatorKey: _tabsShellKey, | ||||
|             builder: (context, state, child) { | ||||
|               return TabsScreen(child: child); | ||||
|             }, | ||||
|             routes: [ | ||||
|               // Explore tab | ||||
|               ShellRoute( | ||||
|                 builder: | ||||
|                     (context, state, child) => ExploreShellScreen(child: child), | ||||
|                 routes: [ | ||||
|                   GoRoute( | ||||
|                     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', | ||||
|                     builder: (context, state) { | ||||
|                       final name = state.pathParameters['name']!; | ||||
|                       return PublisherProfileScreen(name: name); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/discovery/realms', | ||||
|                     builder: (context, state) => const DiscoveryRealmsScreen(), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|         AutoRoute( | ||||
|           page: AccountShellRoute.page, | ||||
|           path: 'account', | ||||
|           children: [ | ||||
|             AutoRoute(page: AccountRoute.page, path: ''), | ||||
|             AutoRoute(page: NotificationRoute.page, path: 'notifications'), | ||||
|             AutoRoute(page: WalletRoute.page, path: 'wallet'), | ||||
|             AutoRoute(page: RelationshipRoute.page, path: 'relationships'), | ||||
|             AutoRoute(page: AccountProfileRoute.page, path: ':name'), | ||||
|             AutoRoute(page: UpdateProfileRoute.page, path: 'me/update'), | ||||
|             AutoRoute(page: LevelingRoute.page, path: 'me/leveling'), | ||||
|             AutoRoute(page: AccountSettingsRoute.page, path: 'settings'), | ||||
|  | ||||
|               // Chat tab | ||||
|               ShellRoute( | ||||
|                 builder: | ||||
|                     (context, state, child) => ChatShellScreen(child: child), | ||||
|                 routes: [ | ||||
|                   GoRoute( | ||||
|                     path: '/chat', | ||||
|                     builder: (context, state) => const ChatListScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/chat/new', | ||||
|                     builder: (context, state) => const NewChatScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/chat/:id', | ||||
|                     builder: (context, state) { | ||||
|                       final id = state.pathParameters['id']!; | ||||
|                       return ChatRoomScreen(id: id); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/chat/:id/edit', | ||||
|                     builder: (context, state) { | ||||
|                       final id = state.pathParameters['id']!; | ||||
|                       return EditChatScreen(id: id); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/chat/:id/detail', | ||||
|                     builder: (context, state) { | ||||
|                       final id = state.pathParameters['id']!; | ||||
|                       return ChatDetailScreen(id: id); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|         AutoRoute(page: RealmListRoute.page, path: 'realms'), | ||||
|         AutoRoute( | ||||
|           page: ChatShellRoute.page, | ||||
|           path: 'chat', | ||||
|           children: [ | ||||
|             AutoRoute(page: ChatListRoute.page, path: ''), | ||||
|             AutoRoute(page: ChatRoomRoute.page, path: ':id'), | ||||
|             AutoRoute(page: NewChatRoute.page, path: 'new'), | ||||
|             AutoRoute(page: EditChatRoute.page, path: ':id/edit'), | ||||
|             AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'), | ||||
|  | ||||
|               // Realms tab | ||||
|               GoRoute( | ||||
|                 path: '/realms', | ||||
|                 builder: (context, state) => const RealmListScreen(), | ||||
|                 routes: [ | ||||
|                   GoRoute( | ||||
|                     path: 'new', | ||||
|                     builder: (context, state) => const NewRealmScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: ':slug', | ||||
|                     builder: (context, state) { | ||||
|                       final slug = state.pathParameters['slug']!; | ||||
|                       return RealmDetailScreen(slug: slug); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: ':slug/edit', | ||||
|                     builder: (context, state) { | ||||
|                       final slug = state.pathParameters['slug']!; | ||||
|                       return EditRealmScreen(slug: slug); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|  | ||||
|               // Account tab | ||||
|               ShellRoute( | ||||
|                 builder: | ||||
|                     (context, state, child) => AccountShellScreen(child: child), | ||||
|                 routes: [ | ||||
|                   GoRoute( | ||||
|                     path: '/account', | ||||
|                     builder: (context, state) => const AccountScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/account/notifications', | ||||
|                     builder: (context, state) => const NotificationScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/account/wallet', | ||||
|                     builder: (context, state) => const WalletScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/account/relationships', | ||||
|                     builder: (context, state) => const RelationshipScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/account/:name', | ||||
|                     builder: (context, state) { | ||||
|                       final name = state.pathParameters['name']!; | ||||
|                       return AccountProfileScreen(name: name); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/account/me/update', | ||||
|                     builder: (context, state) => const UpdateProfileScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/account/me/leveling', | ||||
|                     builder: (context, state) => const LevelingScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/account/settings', | ||||
|                     builder: (context, state) => const AccountSettingsScreen(), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|     AutoRoute( | ||||
|       page: CreatorHubShellRoute.page, | ||||
|       path: 'creators', | ||||
|       children: [ | ||||
|         AutoRoute(page: CreatorHubRoute.page, path: ''), | ||||
|         AutoRoute(page: CreatorPostListRoute.page, path: ':name/posts'), | ||||
|         AutoRoute(page: StickersRoute.page, path: ':name/stickers'), | ||||
|         AutoRoute(page: NewStickerPacksRoute.page, path: ':name/stickers/new'), | ||||
|         AutoRoute( | ||||
|           page: EditStickerPacksRoute.page, | ||||
|           path: ':name/stickers/:packId/edit', | ||||
|         ), | ||||
|         AutoRoute( | ||||
|           page: StickerPackDetailRoute.page, | ||||
|           path: ':name/stickers/:packId', | ||||
|         ), | ||||
|         AutoRoute(page: NewStickersRoute.page, path: ':name/stickers/new'), | ||||
|         AutoRoute( | ||||
|           page: EditStickersRoute.page, | ||||
|           path: ':name/stickers/:id/edit', | ||||
|         ), | ||||
|         AutoRoute(page: NewPublisherRoute.page, path: 'new'), | ||||
|         AutoRoute(page: EditPublisherRoute.page, path: ':name/edit'), | ||||
|         ], | ||||
|       ), | ||||
|     AutoRoute(page: LoginRoute.page, path: 'auth/login'), | ||||
|     AutoRoute(page: CreateAccountRoute.page, path: 'auth/create-account'), | ||||
|     AutoRoute(page: SettingsRoute.page, path: 'settings'), | ||||
|     AutoRoute(page: NewRealmRoute.page, path: 'realms/new'), | ||||
|     AutoRoute(page: RealmDetailRoute.page, path: 'realms/:slug'), | ||||
|     AutoRoute(page: EditRealmRoute.page, path: 'realms/:slug/edit'), | ||||
|   ]; | ||||
|     ], | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| // Navigation helper functions | ||||
| class AppRouter { | ||||
|   static GoRouter of(BuildContext context) { | ||||
|     return GoRouter.of(context); | ||||
|   } | ||||
|  | ||||
|   static void go(BuildContext context, String path) { | ||||
|     context.go(path); | ||||
|   } | ||||
|  | ||||
|   static void push(BuildContext context, String path) { | ||||
|     context.push(path); | ||||
|   } | ||||
|  | ||||
|   static void pop(BuildContext context) { | ||||
|     context.pop(); | ||||
|   } | ||||
|  | ||||
|   static bool canPop(BuildContext context) { | ||||
|     return context.canPop(); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										1773
									
								
								lib/route.gr.dart
									
									
									
									
									
								
							
							
						
						
									
										1773
									
								
								lib/route.gr.dart
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										300
									
								
								lib/screens/about.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								lib/screens/about.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,300 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:package_info_plus/package_info_plus.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart'; | ||||
|  | ||||
| class AboutScreen extends StatefulWidget { | ||||
|   const AboutScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<AboutScreen> createState() => _AboutScreenState(); | ||||
| } | ||||
|  | ||||
| class _AboutScreenState extends State<AboutScreen> { | ||||
|   PackageInfo _packageInfo = PackageInfo( | ||||
|     appName: 'Island', | ||||
|     packageName: 'com.example.island', | ||||
|     version: '1.0.0', | ||||
|     buildNumber: '1', | ||||
|   ); | ||||
|   bool _isLoading = true; | ||||
|   String? _errorMessage; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _initPackageInfo(); | ||||
|   } | ||||
|  | ||||
|   Future<void> _initPackageInfo() async { | ||||
|     try { | ||||
|       final info = await PackageInfo.fromPlatform(); | ||||
|       if (mounted) { | ||||
|         setState(() { | ||||
|           _packageInfo = info; | ||||
|           _isLoading = false; | ||||
|         }); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       if (mounted) { | ||||
|         setState(() { | ||||
|           _errorMessage = 'Failed to load package info: $e'; | ||||
|           _isLoading = false; | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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 Scaffold( | ||||
|       appBar: AppBar(title: const Text('About'), 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( | ||||
|                       'Version ${_packageInfo.version} (${_packageInfo.buildNumber})', | ||||
|                       style: theme.textTheme.bodyMedium?.copyWith( | ||||
|                         color: theme.textTheme.bodySmall?.color, | ||||
|                       ), | ||||
|                     ), | ||||
|                     const SizedBox(height: 32), | ||||
|  | ||||
|                     // App Info Card | ||||
|                     _buildSection( | ||||
|                       context, | ||||
|                       title: 'App Information', | ||||
|                       children: [ | ||||
|                         _buildInfoItem( | ||||
|                           context, | ||||
|                           icon: Icons.info_outline, | ||||
|                           label: 'Package Name', | ||||
|                           value: _packageInfo.packageName, | ||||
|                         ), | ||||
|                         _buildInfoItem( | ||||
|                           context, | ||||
|                           icon: Icons.update, | ||||
|                           label: 'Version', | ||||
|                           value: _packageInfo.version, | ||||
|                         ), | ||||
|                         _buildInfoItem( | ||||
|                           context, | ||||
|                           icon: Icons.build, | ||||
|                           label: 'Build Number', | ||||
|                           value: _packageInfo.buildNumber, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|  | ||||
|                     const SizedBox(height: 16), | ||||
|  | ||||
|                     // Links Card | ||||
|                     _buildSection( | ||||
|                       context, | ||||
|                       title: 'Links', | ||||
|                       children: [ | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Icons.privacy_tip_outlined, | ||||
|                           title: 'Privacy Policy', | ||||
|                           onTap: | ||||
|                               () => _launchURL( | ||||
|                                 'https://solsynth.dev/terms/privacy-policy', | ||||
|                               ), | ||||
|                         ), | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Icons.description_outlined, | ||||
|                           title: 'Terms of Service', | ||||
|                           onTap: | ||||
|                               () => _launchURL( | ||||
|                                 'https://example.com/terms/basic-law', | ||||
|                               ), | ||||
|                         ), | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Icons.code, | ||||
|                           title: 'Open Source Licenses', | ||||
|                           onTap: () { | ||||
|                             showLicensePage( | ||||
|                               context: context, | ||||
|                               applicationName: _packageInfo.appName, | ||||
|                               applicationVersion: | ||||
|                                   'Version ${_packageInfo.version}', | ||||
|                             ); | ||||
|                           }, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|  | ||||
|                     const SizedBox(height: 16), | ||||
|  | ||||
|                     // Developer Info | ||||
|                     _buildSection( | ||||
|                       context, | ||||
|                       title: 'Developer', | ||||
|                       children: [ | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Icons.email_outlined, | ||||
|                           title: 'Contact Us', | ||||
|                           subtitle: 'lily@solsynth.dev', | ||||
|                           onTap: () => _launchURL('mailto:lily@solsynth.dev'), | ||||
|                         ), | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Icons.copyright, | ||||
|                           title: 'License', | ||||
|                           subtitle: | ||||
|                               'Copyright reserved © ${DateTime.now().year} Solsynth\nGNU Affero General Public License v3.0', | ||||
|                           onTap: | ||||
|                               () => _launchURL( | ||||
|                                 'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt', | ||||
|                               ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|  | ||||
|                     const SizedBox(height: 32), | ||||
|  | ||||
|                     // Copyright | ||||
|                     Padding( | ||||
|                       padding: const EdgeInsets.all(16.0), | ||||
|                       child: Text( | ||||
|                         '© ${DateTime.now().year} ${_packageInfo.appName}. All rights reserved.', | ||||
|                         style: theme.textTheme.bodySmall, | ||||
|                         textAlign: TextAlign.center, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   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, | ||||
|   }) { | ||||
|     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, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|           if (value.startsWith('http') || value.contains('@')) | ||||
|             IconButton( | ||||
|               icon: const Icon(Icons.copy, size: 16), | ||||
|               onPressed: () { | ||||
|                 Clipboard.setData(ClipboardData(text: value)); | ||||
|                 ScaffoldMessenger.of(context).showSnackBar( | ||||
|                   const SnackBar(content: Text('Copied to clipboard')), | ||||
|                 ); | ||||
|               }, | ||||
|               padding: EdgeInsets.zero, | ||||
|               constraints: const BoxConstraints(), | ||||
|               tooltip: 'Copy to clipboard', | ||||
|             ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildListTile( | ||||
|     BuildContext context, { | ||||
|     required IconData icon, | ||||
|     required String title, | ||||
|     String? subtitle, | ||||
|     required VoidCallback onTap, | ||||
|   }) { | ||||
|     return Column( | ||||
|       children: [ | ||||
|         ListTile( | ||||
|           leading: Icon(icon), | ||||
|           title: Text(title), | ||||
|           subtitle: subtitle != null ? Text(subtitle) : null, | ||||
|           trailing: const Icon(Icons.chevron_right), | ||||
|           onTap: onTap, | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|           minLeadingWidth: 24, | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,14 +1,13 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/message.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/notification.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/account/account_name.dart'; | ||||
| @@ -19,9 +18,9 @@ import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class AccountShellScreen extends HookConsumerWidget { | ||||
|   const AccountShellScreen({super.key}); | ||||
|   final Widget child; | ||||
|   const AccountShellScreen({super.key, required this.child}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -34,17 +33,16 @@ class AccountShellScreen extends HookConsumerWidget { | ||||
|           children: [ | ||||
|             Flexible(flex: 2, child: AccountScreen(isAside: true)), | ||||
|             VerticalDivider(width: 1), | ||||
|             Flexible(flex: 3, child: AutoRouter()), | ||||
|             Flexible(flex: 3, child: child), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return AppBackground(isRoot: true, child: AutoRouter()); | ||||
|     return AppBackground(isRoot: true, child: child); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class AccountScreen extends HookConsumerWidget { | ||||
|   final bool isAside; | ||||
|   const AccountScreen({super.key, this.isAside = false}); | ||||
| @@ -100,9 +98,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                           radius: 24, | ||||
|                         ), | ||||
|                         onTap: () { | ||||
|                           context.router.push( | ||||
|                             AccountProfileRoute(name: user.value!.name), | ||||
|                           ); | ||||
|                           context.push('/account/${user.value!.name}'); | ||||
|                         }, | ||||
|                       ), | ||||
|                       Expanded( | ||||
| @@ -147,7 +143,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                 progress: user.value!.profile.levelingProgress, | ||||
|               ), | ||||
|               onTap: () { | ||||
|                 context.router.push(LevelingRoute()); | ||||
|                 context.push('/account/me/leveling'); | ||||
|               }, | ||||
|             ).padding(horizontal: 12), | ||||
|             Row( | ||||
| @@ -165,7 +161,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                         ], | ||||
|                       ).padding(horizontal: 16, vertical: 12), | ||||
|                       onTap: () { | ||||
|                         context.router.push(CreatorHubShellRoute()); | ||||
|                         context.push('/creators'); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ).height(140), | ||||
| @@ -182,7 +178,9 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                           Text('developerPortalDescription').tr(), | ||||
|                         ], | ||||
|                       ).padding(horizontal: 16, vertical: 12), | ||||
|                       onTap: () {}, | ||||
|                       onTap: () { | ||||
|                         context.push('/developers'); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ).height(140), | ||||
|                 ), | ||||
| @@ -204,7 +202,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                 ], | ||||
|               ), | ||||
|               onTap: () { | ||||
|                 context.router.push(NotificationRoute()); | ||||
|                 context.push('/account/notifications'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
| @@ -214,7 +212,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('wallet').tr(), | ||||
|               onTap: () { | ||||
|                 context.router.push(WalletRoute()); | ||||
|                 context.push('/account/wallet'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
| @@ -224,7 +222,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('relationships').tr(), | ||||
|               onTap: () { | ||||
|                 context.router.push(RelationshipRoute()); | ||||
|                 context.push('/account/relationship'); | ||||
|               }, | ||||
|             ), | ||||
|             const Divider(height: 1).padding(vertical: 8), | ||||
| @@ -235,7 +233,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('appSettings').tr(), | ||||
|               onTap: () { | ||||
|                 context.router.push(SettingsRoute()); | ||||
|                 context.push('/settings'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
| @@ -245,7 +243,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('updateYourProfile').tr(), | ||||
|               onTap: () { | ||||
|                 context.router.push(UpdateProfileRoute()); | ||||
|                 context.push('/account/me/update'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
| @@ -255,7 +253,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('accountSettings').tr(), | ||||
|               onTap: () { | ||||
|                 context.router.push(AccountSettingsRoute()); | ||||
|                 context.push('/account/me/settings'); | ||||
|               }, | ||||
|             ), | ||||
|             if (kDebugMode) const Divider(height: 1).padding(vertical: 8), | ||||
| @@ -283,6 +281,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), | ||||
| @@ -320,7 +328,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget { | ||||
|                   child: Card( | ||||
|                     child: InkWell( | ||||
|                       onTap: () { | ||||
|                         context.router.push(CreateAccountRoute()); | ||||
|                         context.push('/auth/create'); | ||||
|                       }, | ||||
|                       child: Padding( | ||||
|                         padding: const EdgeInsets.all(16), | ||||
| @@ -342,7 +350,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget { | ||||
|                   child: Card( | ||||
|                     child: InkWell( | ||||
|                       onTap: () { | ||||
|                         context.router.push(LoginRoute()); | ||||
|                         context.push('/auth/login'); | ||||
|                       }, | ||||
|                       child: Padding( | ||||
|                         padding: const EdgeInsets.all(16), | ||||
| @@ -361,7 +369,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget { | ||||
|                 const Gap(8), | ||||
|                 TextButton( | ||||
|                   onPressed: () { | ||||
|                     context.router.push(SettingsRoute()); | ||||
|                     context.push('/settings'); | ||||
|                   }, | ||||
|                   child: Text('appSettings').tr(), | ||||
|                 ).center(), | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -12,10 +11,9 @@ import 'package:island/widgets/account/event_calendar.dart'; | ||||
| import 'package:island/widgets/account/fortune_graph.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class EventCalanderScreen extends HookConsumerWidget { | ||||
|   final String name; | ||||
|   const EventCalanderScreen({super.key, @PathParam("name") required this.name}); | ||||
|   const EventCalanderScreen({super.key, required this.name}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| @@ -31,7 +30,6 @@ Future<SnWalletSubscription?> accountStellarSubscription(Ref ref) async { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class LevelingScreen extends HookConsumerWidget { | ||||
|   const LevelingScreen({super.key}); | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:auto_route/annotations.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| @@ -51,7 +50,6 @@ Future<List<SnAccountConnection>> accountConnections(Ref ref) async { | ||||
|       .toList(); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class AccountSettingsScreen extends HookConsumerWidget { | ||||
|   const AccountSettingsScreen({super.key}); | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:croppy/croppy.dart' hide cropImage; | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| @@ -20,7 +19,6 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| const kServerSupportedLanguages = {'en-US': 'en-us', 'zh-CN': 'zh-hans'}; | ||||
|  | ||||
| @RoutePage() | ||||
| class UpdateProfileScreen extends HookConsumerWidget { | ||||
|   const UpdateProfileScreen({super.key}); | ||||
|  | ||||
| @@ -343,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, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/chat.dart'; | ||||
| @@ -53,6 +53,7 @@ Future<List<SnAccountBadge>> accountBadges(Ref ref, String uname) async { | ||||
|  | ||||
| @riverpod | ||||
| Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async { | ||||
|   try { | ||||
|     final account = await ref.watch(accountProvider(uname).future); | ||||
|     if (account.profile.background == null) return null; | ||||
|     final palette = await PaletteGenerator.fromImageProvider( | ||||
| @@ -64,6 +65,9 @@ Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async { | ||||
|     final dominantColor = palette.dominantColor?.color; | ||||
|     if (dominantColor == null) return null; | ||||
|     return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; | ||||
|   } catch (_) { | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| @@ -96,13 +100,9 @@ Future<SnRelationship?> accountRelationship(Ref ref, String uname) async { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class AccountProfileScreen extends HookConsumerWidget { | ||||
|   final String name; | ||||
|   const AccountProfileScreen({ | ||||
|     super.key, | ||||
|     @PathParam("name") required this.name, | ||||
|   }); | ||||
|   const AccountProfileScreen({super.key, required this.name}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -142,7 +142,7 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|     Future<void> directMessageAction() async { | ||||
|       if (!account.hasValue) return; | ||||
|       if (accountChat.value != null) { | ||||
|         context.router.pushPath('/chat/${accountChat.value!.id}'); | ||||
|         context.push('/chat/${accountChat.value!.id}'); | ||||
|         return; | ||||
|       } | ||||
|       showLoadingModal(context); | ||||
| @@ -153,7 +153,7 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|           data: {'related_user_id': account.value!.id}, | ||||
|         ); | ||||
|         final chat = SnChatRoom.fromJson(resp.data); | ||||
|         if (context.mounted) context.router.pushPath('/chat/${chat.id}'); | ||||
|         if (context.mounted) context.push('/chat/${chat.id}'); | ||||
|         ref.invalidate(accountDirectChatProvider(name)); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|   | ||||
| @@ -268,7 +268,7 @@ class _AccountBadgesProviderElement | ||||
| } | ||||
|  | ||||
| String _$accountAppbarForcegroundColorHash() => | ||||
|     r'f654a7a5594eda1500906e9ad023c22772257a9b'; | ||||
|     r'8ee0cae10817b77fb09548a482f5247662b4374c'; | ||||
|  | ||||
| /// See also [accountAppbarForcegroundColor]. | ||||
| @ProviderFor(accountAppbarForcegroundColor) | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| @@ -204,7 +203,6 @@ class RelationshipListTile extends StatelessWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class RelationshipScreen extends HookConsumerWidget { | ||||
|   const RelationshipScreen({super.key}); | ||||
|  | ||||
| @@ -217,6 +215,7 @@ class RelationshipScreen extends HookConsumerWidget { | ||||
|     Future<void> addFriend() async { | ||||
|       final result = await showModalBottomSheet( | ||||
|         context: context, | ||||
|         useRootNavigator: true, | ||||
|         builder: (context) => AccountPickerSheet(), | ||||
|       ); | ||||
|       if (result == null) return; | ||||
|   | ||||
							
								
								
									
										105
									
								
								lib/screens/article_detail_screen.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								lib/screens/article_detail_screen.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| 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: 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), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,12 +1,11 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:email_validator/email_validator.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/account/me/update.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| @@ -16,7 +15,6 @@ import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| import 'captcha.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class CreateAccountScreen extends HookConsumerWidget { | ||||
|   const CreateAccountScreen({super.key}); | ||||
|  | ||||
| @@ -307,7 +305,7 @@ class _PostCreateModal extends HookConsumerWidget { | ||||
|             TextButton( | ||||
|               onPressed: () { | ||||
|                 Navigator.pop(context); | ||||
|                 context.router.replace(LoginRoute()); | ||||
|                 context.pushReplacement('/auth/login'); | ||||
|               }, | ||||
|               child: Text('login'.tr()), | ||||
|             ), | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import 'dart:io'; | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:animations/animations.dart'; | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:device_info_plus/device_info_plus.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| @@ -43,7 +42,6 @@ final Map<int, (String, String, IconData)> kFactorTypes = { | ||||
|   4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm), | ||||
| }; | ||||
|  | ||||
| @RoutePage() | ||||
| class LoginScreen extends HookConsumerWidget { | ||||
|   const LoginScreen({super.key}); | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/annotations.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -14,10 +13,9 @@ import 'package:livekit_client/livekit_client.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class CallScreen extends HookConsumerWidget { | ||||
|   final String roomId; | ||||
|   const CallScreen({super.key, @PathParam('id') required this.roomId}); | ||||
|   const CallScreen({super.key, required this.roomId}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:croppy/croppy.dart' hide cropImage; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| @@ -15,7 +15,6 @@ import 'package:island/pods/call.dart'; | ||||
| import 'package:island/pods/chat_summary.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/realm/realms.dart'; | ||||
| import 'package:island/services/file.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| @@ -173,9 +172,9 @@ Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async { | ||||
|       .toList(); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class ChatShellScreen extends HookConsumerWidget { | ||||
|   const ChatShellScreen({super.key}); | ||||
|   final Widget child; | ||||
|   const ChatShellScreen({super.key, required this.child}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -187,18 +186,17 @@ class ChatShellScreen extends HookConsumerWidget { | ||||
|         child: Row( | ||||
|           children: [ | ||||
|             Flexible(flex: 2, child: ChatListScreen(isAside: true)), | ||||
|             VerticalDivider(width: 1), | ||||
|             Flexible(flex: 4, child: AutoRouter()), | ||||
|             const VerticalDivider(width: 1), | ||||
|             Flexible(flex: 4, child: child), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return AppBackground(isRoot: true, child: AutoRouter()); | ||||
|     return AppBackground(isRoot: true, child: child); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class ChatListScreen extends HookConsumerWidget { | ||||
|   final bool isAside; | ||||
|   const ChatListScreen({super.key, this.isAside = false}); | ||||
| @@ -229,7 +227,8 @@ class ChatListScreen extends HookConsumerWidget { | ||||
|     Future<void> createDirectMessage() async { | ||||
|       final result = await showModalBottomSheet( | ||||
|         context: context, | ||||
|         builder: (context) => AccountPickerSheet(), | ||||
|         useRootNavigator: true, | ||||
|         builder: (context) => const AccountPickerSheet(), | ||||
|       ); | ||||
|       if (result == null) return; | ||||
|       final client = ref.read(apiClientProvider); | ||||
| @@ -244,7 +243,7 @@ class ChatListScreen extends HookConsumerWidget { | ||||
|     return AppScaffold( | ||||
|       extendBody: false, // Prevent conflicts with tabs navigation | ||||
|       appBar: AppBar( | ||||
|         title: Text('chat').tr(), | ||||
|         title: const Text('chat').tr(), | ||||
|         bottom: TabBar( | ||||
|           controller: tabController, | ||||
|           tabs: [ | ||||
| @@ -298,7 +297,7 @@ class ChatListScreen extends HookConsumerWidget { | ||||
|               showModalBottomSheet( | ||||
|                 isScrollControlled: true, | ||||
|                 context: context, | ||||
|                 builder: (context) => _ChatInvitesSheet(), | ||||
|                 builder: (context) => const _ChatInvitesSheet(), | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
| @@ -309,17 +308,18 @@ class ChatListScreen extends HookConsumerWidget { | ||||
|         onPressed: () { | ||||
|           showModalBottomSheet( | ||||
|             context: context, | ||||
|             useRootNavigator: true, | ||||
|             builder: | ||||
|                 (context) => Column( | ||||
|                   mainAxisSize: MainAxisSize.min, | ||||
|                   crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                   children: [ | ||||
|                     ListTile( | ||||
|                       title: Text('createChatRoom').tr(), | ||||
|                       title: const Text('createChatRoom').tr(), | ||||
|                       leading: const Icon(Symbols.add), | ||||
|                       onTap: () { | ||||
|                         Navigator.pop(context); | ||||
|                         context.pushRoute(NewChatRoute()).then((value) { | ||||
|                         context.push('/chat/new').then((value) { | ||||
|                           if (value != null) { | ||||
|                             ref.invalidate(chatroomsJoinedProvider); | ||||
|                           } | ||||
| @@ -327,7 +327,7 @@ class ChatListScreen extends HookConsumerWidget { | ||||
|                       }, | ||||
|                     ), | ||||
|                     ListTile( | ||||
|                       title: Text('createDirectMessage').tr(), | ||||
|                       title: const Text('createDirectMessage').tr(), | ||||
|                       leading: const Icon(Symbols.person), | ||||
|                       onTap: () { | ||||
|                         Navigator.pop(context); | ||||
| @@ -400,16 +400,7 @@ class ChatListScreen extends HookConsumerWidget { | ||||
|                               room: item, | ||||
|                               isDirect: item.type == 1, | ||||
|                               onTap: () { | ||||
|                                 if (context.router.topRoute.name == | ||||
|                                     ChatRoomRoute.name) { | ||||
|                                   context.router.replace( | ||||
|                                     ChatRoomRoute(id: item.id), | ||||
|                                   ); | ||||
|                                 } else { | ||||
|                                   context.router.push( | ||||
|                                     ChatRoomRoute(id: item.id), | ||||
|                                   ); | ||||
|                                 } | ||||
|                                 context.push('/chat/${item.id}'); | ||||
|                               }, | ||||
|                             ); | ||||
|                           }, | ||||
| @@ -443,33 +434,45 @@ class ChatListScreen extends HookConsumerWidget { | ||||
| @riverpod | ||||
| Future<SnChatRoom?> chatroom(Ref ref, String? identifier) async { | ||||
|   if (identifier == null) return null; | ||||
|   try { | ||||
|     final client = ref.watch(apiClientProvider); | ||||
|     final resp = await client.get('/chat/$identifier'); | ||||
|     return SnChatRoom.fromJson(resp.data); | ||||
|   } catch (err) { | ||||
|     if (err is DioException && err.response?.statusCode == 404) { | ||||
|       return null; // Chat room not found | ||||
|     } | ||||
|     rethrow; // Rethrow other errors | ||||
|   } | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<SnChatMember?> chatroomIdentity(Ref ref, String? identifier) async { | ||||
|   if (identifier == null) return null; | ||||
|   try { | ||||
|     final client = ref.watch(apiClientProvider); | ||||
|     final resp = await client.get('/chat/$identifier/members/me'); | ||||
|     return SnChatMember.fromJson(resp.data); | ||||
|   } catch (err) { | ||||
|     if (err is DioException && err.response?.statusCode == 404) { | ||||
|       return null; // Chat member not found | ||||
|     } | ||||
|     rethrow; // Rethrow other errors | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class NewChatScreen extends StatelessWidget { | ||||
|   const NewChatScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return EditChatScreen(); | ||||
|     return const EditChatScreen(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class EditChatScreen extends HookConsumerWidget { | ||||
|   final String? id; | ||||
|   const EditChatScreen({super.key, @PathParam("id") this.id}); | ||||
|   const EditChatScreen({super.key, this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -481,6 +484,8 @@ class EditChatScreen extends HookConsumerWidget { | ||||
|     final descriptionController = useTextEditingController(); | ||||
|     final picture = useState<SnCloudFile?>(null); | ||||
|     final background = useState<SnCloudFile?>(null); | ||||
|     final isPublic = useState(true); | ||||
|     final isCommunity = useState(false); | ||||
|  | ||||
|     final chat = ref.watch(chatroomProvider(id)); | ||||
|  | ||||
| @@ -493,12 +498,14 @@ class EditChatScreen extends HookConsumerWidget { | ||||
|         descriptionController.text = chat.value!.description ?? ''; | ||||
|         picture.value = chat.value!.picture; | ||||
|         background.value = chat.value!.background; | ||||
|         isPublic.value = chat.value!.isPublic; | ||||
|         isCommunity.value = chat.value!.isCommunity; | ||||
|         currentRealm.value = joinedRealms.value?.firstWhereOrNull( | ||||
|           (realm) => realm.id == chat.value!.realmId, | ||||
|         ); | ||||
|       } | ||||
|       return; | ||||
|     }, [chat]); | ||||
|     }, [chat, joinedRealms]); | ||||
|  | ||||
|     void setPicture(String position) async { | ||||
|       showLoadingModal(context); | ||||
| @@ -516,9 +523,9 @@ class EditChatScreen extends HookConsumerWidget { | ||||
|         image: result, | ||||
|         allowedAspectRatios: [ | ||||
|           if (position == 'background') | ||||
|             CropAspectRatio(height: 7, width: 16) | ||||
|             const CropAspectRatio(height: 7, width: 16) | ||||
|           else | ||||
|             CropAspectRatio(height: 1, width: 1), | ||||
|             const CropAspectRatio(height: 1, width: 1), | ||||
|         ], | ||||
|       ); | ||||
|       if (result == null) { | ||||
| @@ -575,11 +582,13 @@ class EditChatScreen extends HookConsumerWidget { | ||||
|             'background_id': background.value?.id, | ||||
|             'picture_id': picture.value?.id, | ||||
|             'realm_id': currentRealm.value?.id, | ||||
|             'is_public': isPublic.value, | ||||
|             'is_community': isCommunity.value, | ||||
|           }, | ||||
|           options: Options(method: id == null ? 'POST' : 'PATCH'), | ||||
|         ); | ||||
|         if (context.mounted) { | ||||
|           context.maybePop(SnChatRoom.fromJson(resp.data)); | ||||
|           context.pop(SnChatRoom.fromJson(resp.data)); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
| @@ -660,13 +669,48 @@ 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), | ||||
|                 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( | ||||
|                   alignment: Alignment.centerRight, | ||||
|                   child: TextButton.icon( | ||||
| @@ -767,7 +811,7 @@ class _ChatInvitesSheet extends HookConsumerWidget { | ||||
|                               ), | ||||
|                               if (invite.chatRoom!.type == 1) | ||||
|                                 Badge( | ||||
|                                   label: Text('directMessage').tr(), | ||||
|                                   label: const Text('directMessage').tr(), | ||||
|                                   backgroundColor: | ||||
|                                       Theme.of(context).colorScheme.primary, | ||||
|                                   textColor: | ||||
|   | ||||
| @@ -25,7 +25,7 @@ final chatroomsJoinedProvider = | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| typedef ChatroomsJoinedRef = AutoDisposeFutureProviderRef<List<SnChatRoom>>; | ||||
| String _$chatroomHash() => r'dce3c0fc407f178bb7c306a08b9fa545795a9205'; | ||||
| String _$chatroomHash() => r'8dac7aaac50932e6dd213039102d43c1cf5f1d4e'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
| @@ -164,7 +164,7 @@ class _ChatroomProviderElement | ||||
|   String? get identifier => (origin as ChatroomProvider).identifier; | ||||
| } | ||||
|  | ||||
| String _$chatroomIdentityHash() => r'4c349ea4265df7b0498cf26c82dbaabe3d868727'; | ||||
| String _$chatroomIdentityHash() => r'ad6ad09b6fc4cf7c4abe146ea97f8e364a3d4fd0'; | ||||
|  | ||||
| /// See also [chatroomIdentity]. | ||||
| @ProviderFor(chatroomIdentity) | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| @@ -18,7 +18,6 @@ import 'package:island/pods/config.dart'; | ||||
| import 'package:island/pods/database.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| @@ -288,15 +287,76 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class ChatRoomScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const ChatRoomScreen({super.key, @PathParam("id") required this.id}); | ||||
|   const ChatRoomScreen({super.key, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final chatRoom = ref.watch(chatroomProvider(id)); | ||||
|     final chatIdentity = ref.watch(chatroomIdentityProvider(id)); | ||||
|  | ||||
|     if (chatIdentity.isLoading || chatRoom.isLoading) { | ||||
|       return AppScaffold( | ||||
|         appBar: AppBar(leading: const PageBackButton()), | ||||
|         body: CircularProgressIndicator().center(), | ||||
|       ); | ||||
|     } else if (chatIdentity.value == null) { | ||||
|       // Identity was not found, user was not joined | ||||
|       return AppScaffold( | ||||
|         appBar: AppBar(leading: const PageBackButton()), | ||||
|         body: Center( | ||||
|           child: | ||||
|               ConstrainedBox( | ||||
|                 constraints: const BoxConstraints(maxWidth: 280), | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   mainAxisAlignment: MainAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     Icon( | ||||
|                       chatRoom.value?.isCommunity == true | ||||
|                           ? Symbols.person_add | ||||
|                           : Symbols.person_remove, | ||||
|                       size: 36, | ||||
|                       fill: 1, | ||||
|                     ).padding(bottom: 4), | ||||
|                     Text('chatNotJoined').tr(), | ||||
|                     if (chatRoom.value?.isCommunity != true) | ||||
|                       Text( | ||||
|                         'chatUnableJoin', | ||||
|                         textAlign: TextAlign.center, | ||||
|                       ).tr().bold() | ||||
|                     else | ||||
|                       FilledButton.tonalIcon( | ||||
|                         onPressed: () async { | ||||
|                           try { | ||||
|                             showLoadingModal(context); | ||||
|                             final apiClient = ref.read(apiClientProvider); | ||||
|                             if (chatRoom.value == null) { | ||||
|                               hideLoadingModal(context); | ||||
|                               return; | ||||
|                             } | ||||
|  | ||||
|                             await apiClient.post( | ||||
|                               '/chat/${chatRoom.value!.id}/members/me', | ||||
|                             ); | ||||
|                             ref.invalidate(chatroomIdentityProvider(id)); | ||||
|                           } catch (err) { | ||||
|                             showErrorAlert(err); | ||||
|                           } finally { | ||||
|                             if (context.mounted) hideLoadingModal(context); | ||||
|                           } | ||||
|                         }, | ||||
|                         label: Text('chatJoin').tr(), | ||||
|                         icon: const Icon(Icons.add), | ||||
|                       ).padding(top: 8), | ||||
|                   ], | ||||
|                 ), | ||||
|               ).center(), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     final messages = ref.watch(messagesNotifierProvider(id)); | ||||
|     final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier); | ||||
|     final ws = ref.watch(websocketProvider); | ||||
| @@ -431,6 +491,28 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|       return () => subscription.cancel(); | ||||
|     }, [ws, chatRoom]); | ||||
|  | ||||
|     useEffect(() { | ||||
|       final wsState = ref.read(websocketStateProvider.notifier); | ||||
|       wsState.sendMessage( | ||||
|         jsonEncode( | ||||
|           WebSocketPacket( | ||||
|             type: 'messages.subscribe', | ||||
|             data: {'chat_room_id': id}, | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
|       return () { | ||||
|         wsState.sendMessage( | ||||
|           jsonEncode( | ||||
|             WebSocketPacket( | ||||
|               type: 'messages.unsubscribe', | ||||
|               data: {'chat_room_id': id}, | ||||
|             ), | ||||
|           ), | ||||
|         ); | ||||
|       }; | ||||
|     }, [id]); | ||||
|  | ||||
|     Future<void> pickPhotoMedia() async { | ||||
|       final result = await ref | ||||
|           .watch(imagePickerProvider) | ||||
| @@ -605,7 +687,7 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|           IconButton( | ||||
|             icon: const Icon(Icons.more_vert), | ||||
|             onPressed: () { | ||||
|               context.router.push(ChatDetailRoute(id: id)); | ||||
|               context.push('/chat/$id/detail'); | ||||
|             }, | ||||
|           ), | ||||
|           const Gap(8), | ||||
|   | ||||
| @@ -1,14 +1,13 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/chat.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/chat/chat.dart'; | ||||
| import 'package:island/widgets/account/account_picker.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| @@ -23,10 +22,9 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
| part 'room_detail.freezed.dart'; | ||||
| part 'room_detail.g.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class ChatDetailScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const ChatDetailScreen({super.key, @PathParam("id") required this.id}); | ||||
|   const ChatDetailScreen({super.key, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -391,7 +389,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget { | ||||
|             if ((chatIdentity.value?.role ?? 0) >= 50) | ||||
|               PopupMenuItem( | ||||
|                 onTap: () { | ||||
|                   context.router.replace(EditChatRoute(id: id)); | ||||
|                   context.pushReplacement('/chat/$id/edit'); | ||||
|                 }, | ||||
|                 child: Row( | ||||
|                   children: [ | ||||
| @@ -426,9 +424,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget { | ||||
|                       client.delete('/chat/$id'); | ||||
|                       ref.invalidate(chatroomsJoinedProvider); | ||||
|                       if (context.mounted) { | ||||
|                         context.router.popUntil( | ||||
|                           (route) => route is ChatRoomRoute, | ||||
|                         ); | ||||
|                         context.pop(); | ||||
|                       } | ||||
|                     } | ||||
|                   }); | ||||
| @@ -461,9 +457,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget { | ||||
|                       client.delete('/chat/$id/members/me'); | ||||
|                       ref.invalidate(chatroomsJoinedProvider); | ||||
|                       if (context.mounted) { | ||||
|                         context.router.popUntil( | ||||
|                           (route) => route is ChatRoomRoute, | ||||
|                         ); | ||||
|                         context.pop(); | ||||
|                       } | ||||
|                     } | ||||
|                   }); | ||||
| @@ -590,8 +584,8 @@ class _ChatMemberListSheet extends HookConsumerWidget { | ||||
|  | ||||
|     Future<void> invitePerson() async { | ||||
|       final result = await showModalBottomSheet( | ||||
|         isScrollControlled: true, | ||||
|         context: context, | ||||
|         useRootNavigator: true, | ||||
|         builder: (context) => const AccountPickerSheet(), | ||||
|       ); | ||||
|       if (result == null) return; | ||||
|   | ||||
| @@ -1,20 +1,26 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/models/publisher.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.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'; | ||||
| @@ -27,9 +33,76 @@ Future<SnPublisherStats?> publisherStats(Ref ref, String? uname) async { | ||||
|   return SnPublisherStats.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| @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 { | ||||
|   const CreatorHubShellScreen({super.key}); | ||||
|   final Widget child; | ||||
|   const CreatorHubShellScreen({super.key, required this.child}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -39,15 +112,14 @@ class CreatorHubShellScreen extends StatelessWidget { | ||||
|         children: [ | ||||
|           SizedBox(width: 360, child: const CreatorHubScreen(isAside: true)), | ||||
|           const VerticalDivider(width: 1), | ||||
|           Expanded(child: AutoRouter()), | ||||
|           Expanded(child: child), | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|     return AutoRouter(); | ||||
|     return child; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class CreatorHubScreen extends HookConsumerWidget { | ||||
|   final bool isAside; | ||||
|   const CreatorHubScreen({super.key, this.isAside = false}); | ||||
| @@ -60,20 +132,19 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     final publishers = ref.watch(publishersManagedProvider); | ||||
|     final publisherInvites = ref.watch(publisherInvitesProvider); | ||||
|     final currentPublisher = useState<SnPublisher?>( | ||||
|       publishers.value?.firstOrNull, | ||||
|     ); | ||||
|  | ||||
|     void updatePublisher() { | ||||
|       context.router | ||||
|           .push(EditPublisherRoute(name: currentPublisher.value!.name)) | ||||
|           .then((value) async { | ||||
|       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; | ||||
|             data.where((e) => e.id == currentPublisher.value!.id).firstOrNull; | ||||
|       }); | ||||
|     } | ||||
|  | ||||
| @@ -122,12 +193,40 @@ 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, | ||||
| @@ -205,7 +304,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}'), | ||||
| @@ -223,7 +322,7 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|                             subtitle: Text('createPublisherHint').tr(), | ||||
|                             trailing: const Icon(Symbols.chevron_right), | ||||
|                             onTap: () { | ||||
|                               context.router.push(NewPublisherRoute()).then(( | ||||
|                               context.push('/creators/publishers/new').then(( | ||||
|                                 value, | ||||
|                               ) { | ||||
|                                 if (value != null) { | ||||
| @@ -249,10 +348,8 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|                               horizontal: 24, | ||||
|                             ), | ||||
|                             onTap: () { | ||||
|                               context.router.push( | ||||
|                                 StickersRoute( | ||||
|                                   pubName: currentPublisher.value!.name, | ||||
|                                 ), | ||||
|                               context.push( | ||||
|                                 '/creators/${currentPublisher.value!.name}/stickers', | ||||
|                               ); | ||||
|                             }, | ||||
|                           ), | ||||
| @@ -265,13 +362,91 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|                               horizontal: 24, | ||||
|                             ), | ||||
|                             onTap: () { | ||||
|                               context.router.push( | ||||
|                                 CreatorPostListRoute( | ||||
|                                   pubName: currentPublisher.value!.name, | ||||
|                               context.push( | ||||
|                                 '/creators/${currentPublisher.value!.name}/posts', | ||||
|                               ); | ||||
|                             }, | ||||
|                           ), | ||||
|                           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, | ||||
| @@ -399,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), | ||||
|             ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| @@ -8,13 +8,9 @@ import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:island/widgets/post/post_list.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| 
 | ||||
| @RoutePage() | ||||
| class CreatorPostListScreen extends HookConsumerWidget { | ||||
|   final String pubName; | ||||
|   const CreatorPostListScreen({ | ||||
|     super.key, | ||||
|     @PathParam('name') required this.pubName, | ||||
|   }); | ||||
|   const CreatorPostListScreen({super.key, required this.pubName}); | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -34,7 +30,7 @@ class CreatorPostListScreen extends HookConsumerWidget { | ||||
|                     subtitle: Text('Create a regular post'), | ||||
|                     onTap: () async { | ||||
|                       Navigator.pop(context); | ||||
|                       final result = await context.router.pushPath( | ||||
|                       final result = await context.push( | ||||
|                         '/posts/compose?type=0', | ||||
|                       ); | ||||
|                       if (result == true) { | ||||
| @@ -48,7 +44,7 @@ class CreatorPostListScreen extends HookConsumerWidget { | ||||
|                     subtitle: Text('Create a detailed article'), | ||||
|                     onTap: () async { | ||||
|                       Navigator.pop(context); | ||||
|                       final result = await context.router.pushPath( | ||||
|                       final result = await context.push( | ||||
|                         '/posts/compose?type=1', | ||||
|                       ); | ||||
|                       if (result == true) { | ||||
| @@ -1,14 +1,14 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:croppy/croppy.dart' hide cropImage; | ||||
| import 'package:dio/dio.dart'; | ||||
| 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: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'; | ||||
| @@ -44,7 +44,6 @@ Future<SnPublisher?> publisher(Ref ref, String? identifier) async { | ||||
|   return SnPublisher.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class NewPublisherScreen extends StatelessWidget { | ||||
|   const NewPublisherScreen({super.key}); | ||||
|  | ||||
| @@ -54,10 +53,9 @@ class NewPublisherScreen extends StatelessWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class EditPublisherScreen extends HookConsumerWidget { | ||||
|   final String? name; | ||||
|   const EditPublisherScreen({super.key, @PathParam('id') this.name}); | ||||
|   const EditPublisherScreen({super.key, this.name}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -177,7 +175,7 @@ class EditPublisherScreen extends HookConsumerWidget { | ||||
|           options: Options(method: name == null ? 'POST' : 'PATCH'), | ||||
|         ); | ||||
|         if (context.mounted) { | ||||
|           context.maybePop(SnPublisher.fromJson(resp.data)); | ||||
|           context.pop(SnPublisher.fromJson(resp.data)); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
| @@ -272,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: | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| 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:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| @@ -10,7 +10,6 @@ import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/sticker.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/creators/stickers/stickers.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| @@ -34,14 +33,13 @@ Future<List<SnSticker>> stickerPackContent(Ref ref, String packId) async { | ||||
|       .toList(); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class StickerPackDetailScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   final String pubName; | ||||
|   const StickerPackDetailScreen({ | ||||
|     super.key, | ||||
|     @PathParam('name') required this.pubName, | ||||
|     @PathParam('packId') required this.id, | ||||
|     required this.pubName, | ||||
|     required this.id, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -76,7 +74,7 @@ class StickerPackDetailScreen extends HookConsumerWidget { | ||||
|           IconButton( | ||||
|             icon: const Icon(Symbols.add_circle), | ||||
|             onPressed: () { | ||||
|               AutoRouter.of(context).push(NewStickersRoute(packId: id)).then(( | ||||
|               context.push('/creators/stickers/$id/new').then(( | ||||
|                 value, | ||||
|               ) { | ||||
|                 if (value != null) { | ||||
| @@ -175,12 +173,9 @@ class StickerPackDetailScreen extends HookConsumerWidget { | ||||
|                                         title: 'edit'.tr(), | ||||
|                                         image: MenuImage.icon(Symbols.edit), | ||||
|                                         callback: () { | ||||
|                                           context.router | ||||
|                                           context | ||||
|                                               .push( | ||||
|                                                 EditStickersRoute( | ||||
|                                                   packId: id, | ||||
|                                                   id: sticker.id, | ||||
|                                                 ), | ||||
|                                                 '/creators/stickers/$id/edit/${sticker.id}', | ||||
|                                               ) | ||||
|                                               .then((value) { | ||||
|                                                 if (value != null) { | ||||
| @@ -264,8 +259,8 @@ class _StickerPackActionMenu extends HookConsumerWidget { | ||||
|           (context) => [ | ||||
|             PopupMenuItem( | ||||
|               onTap: () { | ||||
|                 context.router.push( | ||||
|                   EditStickerPacksRoute(pubName: pubName, packId: packId), | ||||
|                 context.push( | ||||
|                   '/creators/$pubName/stickers/$packId/edit', | ||||
|                 ); | ||||
|               }, | ||||
|               child: Row( | ||||
| @@ -299,7 +294,7 @@ class _StickerPackActionMenu extends HookConsumerWidget { | ||||
|                     final client = ref.watch(apiClientProvider); | ||||
|                     client.delete('/stickers/$packId'); | ||||
|                     ref.invalidate(stickerPacksNotifierProvider); | ||||
|                     if (context.mounted) context.router.maybePop(true); | ||||
|                     if (context.mounted) context.pop(true); | ||||
|                   } | ||||
|                 }); | ||||
|               }, | ||||
| @@ -331,13 +326,9 @@ Future<SnSticker?> stickerPackSticker( | ||||
|   return SnSticker.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class NewStickersScreen extends StatelessWidget { | ||||
|   final String packId; | ||||
|   const NewStickersScreen({ | ||||
|     super.key, | ||||
|     @PathParam('packId') required this.packId, | ||||
|   }); | ||||
|   const NewStickersScreen({super.key, required this.packId}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -345,15 +336,10 @@ class NewStickersScreen extends StatelessWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class EditStickersScreen extends HookConsumerWidget { | ||||
|   final String packId; | ||||
|   final String? id; | ||||
|   const EditStickersScreen({ | ||||
|     super.key, | ||||
|     @PathParam("packId") required this.packId, | ||||
|     @PathParam("id") required this.id, | ||||
|   }); | ||||
|   const EditStickersScreen({super.key, required this.packId, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|   | ||||
| @@ -1,13 +1,12 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/sticker.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| @@ -17,10 +16,9 @@ import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
|  | ||||
| part 'stickers.g.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class StickersScreen extends HookConsumerWidget { | ||||
|   final String pubName; | ||||
|   const StickersScreen({super.key, @PathParam("name") required this.pubName}); | ||||
|   const StickersScreen({super.key, required this.pubName}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -30,7 +28,7 @@ class StickersScreen extends HookConsumerWidget { | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             onPressed: () { | ||||
|               context.router.push(NewStickerPacksRoute(pubName: pubName)).then(( | ||||
|               context.push('/creators/stickers/new?pubName=pubName').then(( | ||||
|                 value, | ||||
|               ) { | ||||
|                 if (value != null) { | ||||
| @@ -73,9 +71,7 @@ class SliverStickerPacksList extends HookConsumerWidget { | ||||
|                 subtitle: Text(sticker.description), | ||||
|                 trailing: const Icon(Symbols.chevron_right), | ||||
|                 onTap: () { | ||||
|                   context.router.push( | ||||
|                     StickerPackDetailRoute(pubName: pubName, id: sticker.id), | ||||
|                   ); | ||||
|                   context.push('/creators/$pubName/stickers/${sticker.id}'); | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
| @@ -137,13 +133,9 @@ Future<SnStickerPack?> stickerPack(Ref ref, String? packId) async { | ||||
|   return SnStickerPack.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class NewStickerPacksScreen extends HookConsumerWidget { | ||||
|   final String pubName; | ||||
|   const NewStickerPacksScreen({ | ||||
|     super.key, | ||||
|     @PathParam("name") required this.pubName, | ||||
|   }); | ||||
|   const NewStickerPacksScreen({super.key, required this.pubName}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -151,15 +143,10 @@ class NewStickerPacksScreen extends HookConsumerWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class EditStickerPacksScreen extends HookConsumerWidget { | ||||
|   final String pubName; | ||||
|   final String? packId; | ||||
|   const EditStickerPacksScreen({ | ||||
|     super.key, | ||||
|     @PathParam("name") required this.pubName, | ||||
|     @PathParam("packId") this.packId, | ||||
|   }); | ||||
|   const EditStickerPacksScreen({super.key, required this.pubName, this.packId}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -200,7 +187,7 @@ class EditStickerPacksScreen extends HookConsumerWidget { | ||||
|           ), | ||||
|         ); | ||||
|         if (!context.mounted) return; | ||||
|         context.router.maybePop(SnStickerPack.fromJson(resp.data)); | ||||
|         context.pop(SnStickerPack.fromJson(resp.data)); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|       } finally { | ||||
| @@ -241,6 +228,7 @@ class EditStickerPacksScreen extends HookConsumerWidget { | ||||
|                   decoration: InputDecoration( | ||||
|                     labelText: 'description'.tr(), | ||||
|                     border: const UnderlineInputBorder(), | ||||
|                     alignLabelWithHint: true, | ||||
|                   ), | ||||
|                   minLines: 3, | ||||
|                   maxLines: null, | ||||
|   | ||||
							
								
								
									
										287
									
								
								lib/screens/creators/webfeed/webfeed_edit.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								lib/screens/creators/webfeed/webfeed_edit.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,287 @@ | ||||
| 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: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 Scaffold( | ||||
|       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), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										78
									
								
								lib/screens/creators/webfeed/webfeed_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								lib/screens/creators/webfeed/webfeed_list.dart
									
									
									
									
									
										Normal 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')), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										162
									
								
								lib/screens/developers/apps.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								lib/screens/developers/apps.dart
									
									
									
									
									
										Normal 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)), | ||||
|             ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										151
									
								
								lib/screens/developers/apps.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								lib/screens/developers/apps.g.dart
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										558
									
								
								lib/screens/developers/edit_app.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										558
									
								
								lib/screens/developers/edit_app.dart
									
									
									
									
									
										Normal 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), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										161
									
								
								lib/screens/developers/edit_app.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								lib/screens/developers/edit_app.g.dart
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										380
									
								
								lib/screens/developers/hub.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										380
									
								
								lib/screens/developers/hub.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,380 @@ | ||||
| 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 Row( | ||||
|         children: [ | ||||
|           SizedBox(width: 360, child: const DeveloperHubScreen(isAside: true)), | ||||
|           const VerticalDivider(width: 1), | ||||
|           Expanded(child: child), | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|     return 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), | ||||
|             ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										172
									
								
								lib/screens/developers/hub.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								lib/screens/developers/hub.g.dart
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										12
									
								
								lib/screens/developers/new_app.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								lib/screens/developers/new_app.dart
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										142
									
								
								lib/screens/discovery/articles.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								lib/screens/discovery/articles.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| 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/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 Scaffold( | ||||
|       appBar: AppBar(title: Text(title ?? 'Articles')), | ||||
|       body: CustomScrollView( | ||||
|         slivers: [ | ||||
|           SliverPadding( | ||||
|             padding: const EdgeInsets.only(top: 8, left: 8, right: 8), | ||||
|             sliver: SliverArticlesList( | ||||
|               feedId: feedId, | ||||
|               publisherId: publisherId, | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										206
									
								
								lib/screens/discovery/articles.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								lib/screens/discovery/articles.g.dart
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										64
									
								
								lib/screens/discovery/realms.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								lib/screens/discovery/realms.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| 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:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/realm/realm_list.dart'; | ||||
| import 'dart:async'; | ||||
|  | ||||
| class DiscoveryRealmsScreen extends HookConsumerWidget { | ||||
|   const DiscoveryRealmsScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     Timer? debounceTimer; | ||||
|     final searchController = useTextEditingController(); | ||||
|     final currentQuery = useState<String?>(null); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: Text('discoverRealms'.tr())), | ||||
|       body: Stack( | ||||
|         children: [ | ||||
|           CustomScrollView( | ||||
|             slivers: [ | ||||
|               SliverGap(80), | ||||
|               SliverRealmList( | ||||
|                 query: currentQuery.value, | ||||
|                 key: ValueKey(currentQuery.value), | ||||
|               ), | ||||
|               SliverGap(MediaQuery.of(context).padding.bottom + 16), | ||||
|             ], | ||||
|           ), | ||||
|           Positioned( | ||||
|             top: 0, | ||||
|             left: 0, | ||||
|             right: 0, | ||||
|             child: Padding( | ||||
|               padding: const EdgeInsets.all(16), | ||||
|               child: SearchBar( | ||||
|                 elevation: WidgetStateProperty.all(4), | ||||
|                 controller: searchController, | ||||
|                 hintText: 'search'.tr(), | ||||
|                 leading: const Icon(Icons.search), | ||||
|                 padding: WidgetStateProperty.all( | ||||
|                   const EdgeInsets.symmetric(horizontal: 24), | ||||
|                 ), | ||||
|                 onChanged: (value) { | ||||
|                   if (debounceTimer?.isActive ?? false) { | ||||
|                     debounceTimer?.cancel(); | ||||
|                   } | ||||
|                   debounceTimer = Timer(const Duration(milliseconds: 300), () { | ||||
|                     if (currentQuery.value != value) { | ||||
|                       currentQuery.value = value; | ||||
|                     } | ||||
|                   }); | ||||
|                 }, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,34 +1,39 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_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/route.gr.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/widgets/check_in.dart'; | ||||
| import 'package:island/widgets/post/post_item.dart'; | ||||
| import 'package:island/widgets/tour/tour.dart'; | ||||
| import 'package:island/screens/tabs.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: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'; | ||||
|  | ||||
| @RoutePage() | ||||
| class ExploreShellScreen extends ConsumerWidget { | ||||
|   const ExploreShellScreen({super.key}); | ||||
| class ExploreShellScreen extends HookConsumerWidget { | ||||
|   final Widget child; | ||||
|   const ExploreShellScreen({super.key, required this.child}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final isWide = isWideScreen(context); | ||||
|     final isWide = MediaQuery.of(context).size.width > 640; | ||||
|  | ||||
|     if (isWide) { | ||||
|       return AppBackground( | ||||
| @@ -37,17 +42,16 @@ class ExploreShellScreen extends ConsumerWidget { | ||||
|           children: [ | ||||
|             Flexible(flex: 2, child: ExploreScreen(isAside: true)), | ||||
|             VerticalDivider(width: 1), | ||||
|             Flexible(flex: 3, child: AutoRouter()), | ||||
|             Flexible(flex: 3, child: child), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return AppBackground(isRoot: true, child: AutoRouter()); | ||||
|     return AppBackground(isRoot: true, child: child); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class ExploreScreen extends HookConsumerWidget { | ||||
|   final bool isAside; | ||||
|   const ExploreScreen({super.key, this.isAside = false}); | ||||
| @@ -85,48 +89,93 @@ class ExploreScreen extends HookConsumerWidget { | ||||
|       activityListNotifierProvider(currentFilter.value).notifier, | ||||
|     ); | ||||
|  | ||||
|     return TourTriggerWidget( | ||||
|       child: AppScaffold( | ||||
|     return AppScaffold( | ||||
|       extendBody: false, // Prevent conflicts with tabs navigation | ||||
|       appBar: AppBar( | ||||
|         toolbarHeight: 0, | ||||
|           bottom: TabBar( | ||||
|         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( | ||||
|                 child: Text( | ||||
|                   'explore'.tr(), | ||||
|                   textAlign: TextAlign.center, | ||||
|                   style: TextStyle( | ||||
|                     color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                           icon: Tooltip( | ||||
|                             message: 'explore'.tr(), | ||||
|                             child: Icon( | ||||
|                               Symbols.explore, | ||||
|                               color: | ||||
|                                   Theme.of( | ||||
|                                     context, | ||||
|                                   ).appBarTheme.foregroundColor!, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                         Tab( | ||||
|                 child: Text( | ||||
|                   'exploreFilterSubscriptions'.tr(), | ||||
|                   textAlign: TextAlign.center, | ||||
|                   style: TextStyle( | ||||
|                     color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                           icon: Tooltip( | ||||
|                             message: 'exploreFilterSubscriptions'.tr(), | ||||
|                             child: Icon( | ||||
|                               Symbols.subscriptions, | ||||
|                               color: | ||||
|                                   Theme.of( | ||||
|                                     context, | ||||
|                                   ).appBarTheme.foregroundColor!, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                         Tab( | ||||
|                 child: Text( | ||||
|                   'exploreFilterFriends'.tr(), | ||||
|                   textAlign: TextAlign.center, | ||||
|                   style: TextStyle( | ||||
|                     color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                           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, | ||||
|               ), | ||||
|         ), | ||||
|       ), | ||||
|       floatingActionButton: FloatingActionButton( | ||||
|         heroTag: Key("explore-page-fab"), | ||||
|         onPressed: () { | ||||
|             context.router.push(PostComposeRoute()).then((value) { | ||||
|           context.push('/posts/compose').then((value) { | ||||
|             if (value != null) { | ||||
|               activitiesNotifier.forceRefresh(); | ||||
|             } | ||||
| @@ -137,13 +186,13 @@ class ExploreScreen extends HookConsumerWidget { | ||||
|       floatingActionButtonLocation: TabbedFabLocation(context), | ||||
|       body: TabBarView( | ||||
|         controller: tabController, | ||||
|         physics: const NeverScrollableScrollPhysics(), | ||||
|         children: [ | ||||
|           _buildActivityList(ref, null), | ||||
|           _buildActivityList(ref, 'subscriptions'), | ||||
|           _buildActivityList(ref, 'friends'), | ||||
|         ], | ||||
|       ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -173,6 +222,70 @@ class ExploreScreen extends HookConsumerWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _DiscoveryActivityItem extends StatelessWidget { | ||||
|   final Map<String, dynamic> data; | ||||
|  | ||||
|   const _DiscoveryActivityItem({required this.data}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final items = data['items'] as List; | ||||
|     final type = items.firstOrNull?['type'] ?? 'unknown'; | ||||
|  | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         Row( | ||||
|           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|           children: [ | ||||
|             const Icon(Symbols.explore, size: 19), | ||||
|             const Gap(8), | ||||
|             Text( | ||||
|               (switch (type) { | ||||
|                 'realm' => 'discoverRealms', | ||||
|                 'publisher' => 'discoverPublishers', | ||||
|                 'article' => 'discoverWebArticles', | ||||
|                 _ => 'unknown', | ||||
|               }).tr(), | ||||
|               style: Theme.of(context).textTheme.titleMedium, | ||||
|             ).padding(top: 1), | ||||
|           ], | ||||
|         ).padding(horizontal: 20, top: 8, bottom: 4), | ||||
|         SizedBox( | ||||
|           height: 180, | ||||
|           child: ListView.builder( | ||||
|             scrollDirection: Axis.horizontal, | ||||
|             itemCount: items.length, | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 8), | ||||
|             itemBuilder: (context, index) { | ||||
|               final item = items[index]; | ||||
|               switch (type) { | ||||
|                 case 'realm': | ||||
|                   return RealmCard( | ||||
|                     realm: SnRealm.fromJson(item['data']), | ||||
|                     maxWidth: 280, | ||||
|                   ); | ||||
|                 case 'publisher': | ||||
|                   return PublisherCard( | ||||
|                     publisher: SnPublisher.fromJson(item['data']), | ||||
|                     maxWidth: 280, | ||||
|                   ); | ||||
|                 case 'article': | ||||
|                   return WebArticleCard( | ||||
|                     article: SnWebArticle.fromJson(item['data']), | ||||
|                     maxWidth: 280, | ||||
|                   ); | ||||
|                 default: | ||||
|                   return Placeholder(); | ||||
|               } | ||||
|             }, | ||||
|           ), | ||||
|         ).padding(bottom: 4), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _ActivityListView extends HookConsumerWidget { | ||||
|   final CursorPagingData<SnActivity> data; | ||||
|   final int widgetCount; | ||||
| @@ -216,10 +329,14 @@ class _ActivityListView extends HookConsumerWidget { | ||||
|                 itemWidget = PostItem( | ||||
|                   backgroundColor: | ||||
|                       isWideScreen(context) ? Colors.transparent : null, | ||||
|                   item: SnPost.fromJson(item.data), | ||||
|                   item: SnPost.fromJson(item.data!), | ||||
|                   padding: | ||||
|                       isReply | ||||
|                           ? EdgeInsets.only(left: 16, right: 16, bottom: 16) | ||||
|                           ? const EdgeInsets.only( | ||||
|                             left: 16, | ||||
|                             right: 16, | ||||
|                             bottom: 16, | ||||
|                           ) | ||||
|                           : null, | ||||
|                   onRefresh: (_) { | ||||
|                     activitiesNotifier.forceRefresh(); | ||||
| @@ -247,6 +364,9 @@ class _ActivityListView extends HookConsumerWidget { | ||||
|                   ); | ||||
|                 } | ||||
|                 break; | ||||
|               case 'discovery': | ||||
|                 itemWidget = _DiscoveryActivityItem(data: item.data!); | ||||
|                 break; | ||||
|               default: | ||||
|                 itemWidget = const Placeholder(); | ||||
|             } | ||||
| @@ -276,6 +396,7 @@ class ActivityListNotifier extends _$ActivityListNotifier | ||||
|       if (cursor != null) 'cursor': cursor, | ||||
|       'take': take, | ||||
|       if (filter != null) 'filter': filter, | ||||
|       if (kDebugMode) 'debugInclude': 'realms,publishers,articles', | ||||
|     }; | ||||
|  | ||||
|     final response = await client.get( | ||||
|   | ||||
| @@ -7,7 +7,7 @@ part of 'explore.dart'; | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$activityListNotifierHash() => | ||||
|     r'14ec2f211c86e1e64a9a34b142d0e8f78ff6361a'; | ||||
|     r'98b62fb9b958023d2c9e320af7ec1f1244836f49'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
| @@ -1,22 +1,21 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:go_router/go_router.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'; | ||||
|  | ||||
| @@ -107,7 +106,6 @@ class NotificationListNotifier extends _$NotificationListNotifier | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class NotificationScreen extends HookConsumerWidget { | ||||
|   const NotificationScreen({super.key}); | ||||
|  | ||||
| @@ -181,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.router.pushPath( | ||||
|                           ['', uri.host, ...uri.pathSegments].join('/'), | ||||
|                         ); | ||||
|                         return; | ||||
|                       } | ||||
|                       showConfirmAlert( | ||||
|                         'openLinkConfirmDescription'.tr(args: [href]), | ||||
|                         'openLinkConfirm'.tr(), | ||||
|                       ).then((value) { | ||||
|                         if (value) { | ||||
|                           launchUrl(uri, mode: LaunchMode.externalApplication); | ||||
|                         } | ||||
|                       }); | ||||
|                     } | ||||
|                   }, | ||||
|                 ); | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -16,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'; | ||||
| @@ -40,10 +39,9 @@ sealed class PostComposeInitialState with _$PostComposeInitialState { | ||||
|       _$PostComposeInitialStateFromJson(json); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class PostEditScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const PostEditScreen({super.key, @PathParam('id') required this.id}); | ||||
|   const PostEditScreen({super.key, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -66,7 +64,6 @@ class PostEditScreen extends HookConsumerWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class PostComposeScreen extends HookConsumerWidget { | ||||
|   final SnPost? originalPost; | ||||
|   final SnPost? repliedPost; | ||||
| @@ -78,7 +75,7 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|     this.originalPost, | ||||
|     this.repliedPost, | ||||
|     this.forwardedPost, | ||||
|     @QueryParam('type') this.type, | ||||
|     this.type, | ||||
|     this.initialState, | ||||
|   }); | ||||
|  | ||||
| @@ -106,15 +103,32 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|         originalPost: originalPost, | ||||
|         forwardedPost: effectiveForwardedPost, | ||||
|         repliedPost: effectiveRepliedPost, | ||||
|         postType: 0, // Regular post type | ||||
|       ), | ||||
|       [originalPost, effectiveForwardedPost, effectiveRepliedPost], | ||||
|     ); | ||||
|  | ||||
|     // Add a listener to the entire state to trigger rebuilds | ||||
|     final stateNotifier = useMemoized( | ||||
|       () => Listenable.merge([ | ||||
|         state.titleController, | ||||
|         state.descriptionController, | ||||
|         state.contentController, | ||||
|         state.visibility, | ||||
|         state.attachments, | ||||
|         state.attachmentProgress, | ||||
|         state.currentPublisher, | ||||
|         state.submitting, | ||||
|       ]), | ||||
|       [state], | ||||
|     ); | ||||
|     useListenable(stateNotifier); | ||||
|  | ||||
|     // Start auto-save when component mounts | ||||
|     useEffect(() { | ||||
|       if (originalPost == null) { | ||||
|         // Only auto-save for new posts, not edits | ||||
|         state.startAutoSave(ref, postType: 0); | ||||
|         state.startAutoSave(ref); | ||||
|       } | ||||
|       return () => state.stopAutoSave(); | ||||
|     }, [state]); | ||||
| @@ -153,13 +167,18 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|         final drafts = ref.read(composeStorageNotifierProvider); | ||||
|         if (drafts.isNotEmpty) { | ||||
|           final mostRecentDraft = drafts.values.reduce( | ||||
|             (a, b) => (a.updatedAt ?? DateTime(0)).isAfter(b.updatedAt ?? DateTime(0)) ? a : b, | ||||
|             (a, b) => | ||||
|                 (a.updatedAt ?? DateTime(0)).isAfter(b.updatedAt ?? DateTime(0)) | ||||
|                     ? a | ||||
|                     : b, | ||||
|           ); | ||||
|  | ||||
|           // Only load if the draft has meaningful content | ||||
|           if (mostRecentDraft.content?.isNotEmpty == true || mostRecentDraft.title?.isNotEmpty == true) { | ||||
|           if (mostRecentDraft.content?.isNotEmpty == true || | ||||
|               mostRecentDraft.title?.isNotEmpty == true) { | ||||
|             state.titleController.text = mostRecentDraft.title ?? ''; | ||||
|             state.descriptionController.text = mostRecentDraft.description ?? ''; | ||||
|             state.descriptionController.text = | ||||
|                 mostRecentDraft.description ?? ''; | ||||
|             state.contentController.text = mostRecentDraft.content ?? ''; | ||||
|             state.visibility.value = mostRecentDraft.visibility; | ||||
|           } | ||||
| @@ -187,6 +206,8 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|               titleController: state.titleController, | ||||
|               descriptionController: state.descriptionController, | ||||
|               visibility: state.visibility, | ||||
|               tagsController: state.tagsController, | ||||
|               categoriesController: state.categoriesController, | ||||
|               onVisibilityChanged: () { | ||||
|                 // Trigger rebuild if needed | ||||
|               }, | ||||
| @@ -206,9 +227,7 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|         ), | ||||
|         itemCount: state.attachments.value.length, | ||||
|         itemBuilder: (context, idx) { | ||||
|           return ValueListenableBuilder<Map<int, double>>( | ||||
|             valueListenable: state.attachmentProgress, | ||||
|             builder: (context, progressMap, _) { | ||||
|           final progressMap = state.attachmentProgress.value; | ||||
|           return AttachmentPreview( | ||||
|             item: state.attachments.value[idx], | ||||
|             progress: progressMap[idx], | ||||
| @@ -225,8 +244,6 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|           ); | ||||
|         }, | ||||
|       ); | ||||
|         }, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     Widget buildNarrowAttachmentList() { | ||||
| @@ -235,9 +252,8 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|           for (var idx = 0; idx < state.attachments.value.length; idx++) | ||||
|             Container( | ||||
|               margin: const EdgeInsets.only(bottom: 8), | ||||
|               child: ValueListenableBuilder<Map<int, double>>( | ||||
|                 valueListenable: state.attachmentProgress, | ||||
|                 builder: (context, progressMap, _) { | ||||
|               child: () { | ||||
|                 final progressMap = state.attachmentProgress.value; | ||||
|                 return AttachmentPreview( | ||||
|                   item: state.attachments.value[idx], | ||||
|                   progress: progressMap[idx], | ||||
| @@ -253,8 +269,7 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|                     ); | ||||
|                   }, | ||||
|                 ); | ||||
|                 }, | ||||
|               ), | ||||
|               }(), | ||||
|             ), | ||||
|         ], | ||||
|       ); | ||||
| @@ -290,7 +305,8 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|                               state.titleController.text = draft.title ?? ''; | ||||
|                               state.descriptionController.text = | ||||
|                                   draft.description ?? ''; | ||||
|                               state.contentController.text = draft.content ?? ''; | ||||
|                               state.contentController.text = | ||||
|                                   draft.content ?? ''; | ||||
|                               state.visibility.value = draft.visibility; | ||||
|                             } | ||||
|                           }, | ||||
| @@ -309,12 +325,9 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|               onPressed: showSettingsSheet, | ||||
|               tooltip: 'postSettings'.tr(), | ||||
|             ), | ||||
|             ValueListenableBuilder<bool>( | ||||
|               valueListenable: state.submitting, | ||||
|               builder: (context, submitting, _) { | ||||
|                 return IconButton( | ||||
|             IconButton( | ||||
|               onPressed: | ||||
|                       submitting | ||||
|                   state.submitting.value | ||||
|                       ? null | ||||
|                       : () => ComposeLogic.performAction( | ||||
|                         ref, | ||||
| @@ -323,10 +336,9 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|                         originalPost: originalPost, | ||||
|                         repliedPost: repliedPost, | ||||
|                         forwardedPost: forwardedPost, | ||||
|                             postType: 0, // Regular post type | ||||
|                       ), | ||||
|               icon: | ||||
|                       submitting | ||||
|                   state.submitting.value | ||||
|                       ? SizedBox( | ||||
|                         width: 28, | ||||
|                         height: 28, | ||||
| @@ -336,12 +348,8 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|                         ), | ||||
|                       ).center() | ||||
|                       : Icon( | ||||
|                             originalPost != null | ||||
|                                 ? Symbols.edit | ||||
|                                 : Symbols.upload, | ||||
|                         originalPost != null ? Symbols.edit : Symbols.upload, | ||||
|                       ), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|             const Gap(8), | ||||
|           ], | ||||
| @@ -402,7 +410,6 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|                                     originalPost: originalPost, | ||||
|                                     repliedPost: repliedPost, | ||||
|                                     forwardedPost: forwardedPost, | ||||
|                                     postType: 0, // Regular post type | ||||
|                                   ), | ||||
|                               child: TextField( | ||||
|                                 controller: state.contentController, | ||||
| @@ -423,22 +430,17 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|                             const Gap(8), | ||||
|  | ||||
|                             // Attachments preview | ||||
|                             ValueListenableBuilder<List<UniversalFile>>( | ||||
|                               valueListenable: state.attachments, | ||||
|                               builder: (context, attachments, _) { | ||||
|                                 if (attachments.isEmpty) { | ||||
|                                   return const SizedBox.shrink(); | ||||
|                                 } | ||||
|                                 return LayoutBuilder( | ||||
|                             if (state.attachments.value.isNotEmpty) | ||||
|                               LayoutBuilder( | ||||
|                                 builder: (context, constraints) { | ||||
|                                   final isWide = isWideScreen(context); | ||||
|                                   return isWide | ||||
|                                       ? buildWideAttachmentGrid() | ||||
|                                       : buildNarrowAttachmentList(); | ||||
|                                 }, | ||||
|                                 ); | ||||
|                               }, | ||||
|                             ), | ||||
|                               ) | ||||
|                             else | ||||
|                               const SizedBox.shrink(), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| @@ -13,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'; | ||||
| @@ -26,10 +25,9 @@ import 'package:island/widgets/post/draft_manager.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class ArticleEditScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const ArticleEditScreen({super.key, @PathParam('id') required this.id}); | ||||
|   const ArticleEditScreen({super.key, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -50,7 +48,6 @@ class ArticleEditScreen extends HookConsumerWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class ArticleComposeScreen extends HookConsumerWidget { | ||||
|   final SnPost? originalPost; | ||||
|  | ||||
| @@ -63,7 +60,10 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|  | ||||
|     final publishers = ref.watch(publishersManagedProvider); | ||||
|     final state = useMemoized( | ||||
|       () => ComposeLogic.createState(originalPost: originalPost), | ||||
|       () => ComposeLogic.createState( | ||||
|         originalPost: originalPost, | ||||
|         postType: 1, // Article type | ||||
|       ), | ||||
|       [originalPost], | ||||
|     ); | ||||
|  | ||||
| @@ -73,7 +73,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|       if (originalPost == null) { | ||||
|         // Only auto-save for new articles, not edits | ||||
|         autoSaveTimer = Timer.periodic(const Duration(seconds: 3), (_) { | ||||
|           ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1); | ||||
|           ComposeLogic.saveDraftWithoutUpload(ref, state); | ||||
|         }); | ||||
|       } | ||||
|       return () { | ||||
| @@ -81,7 +81,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|         state.stopAutoSave(); | ||||
|         // Save final draft before disposing | ||||
|         if (originalPost == null) { | ||||
|           ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1); | ||||
|           ComposeLogic.saveDraftWithoutUpload(ref, state); | ||||
|         } | ||||
|         ComposeLogic.dispose(state); | ||||
|         autoSaveTimer?.cancel(); | ||||
| @@ -143,6 +143,8 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|               titleController: state.titleController, | ||||
|               descriptionController: state.descriptionController, | ||||
|               visibility: state.visibility, | ||||
|               tagsController: state.tagsController, | ||||
|               categoriesController: state.categoriesController, | ||||
|               onVisibilityChanged: () { | ||||
|                 // Trigger rebuild if needed | ||||
|               }, | ||||
| @@ -363,7 +365,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|     return PopScope( | ||||
|       onPopInvoked: (_) { | ||||
|         if (originalPost == null) { | ||||
|           ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1); | ||||
|           ComposeLogic.saveDraftWithoutUpload(ref, state); | ||||
|         } | ||||
|       }, | ||||
|       child: AppScaffold( | ||||
| @@ -411,7 +413,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|               ), | ||||
|             IconButton( | ||||
|               icon: const Icon(Symbols.save), | ||||
|               onPressed: () => ComposeLogic.saveDraft(ref, state, postType: 1), | ||||
|               onPressed: () => ComposeLogic.saveDraft(ref, state), | ||||
|               tooltip: 'saveDraft'.tr(), | ||||
|             ), | ||||
|             IconButton( | ||||
| @@ -438,7 +440,6 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|                             state, | ||||
|                             context, | ||||
|                             originalPost: originalPost, | ||||
|                             postType: 1, // Article type | ||||
|                           ), | ||||
|                   icon: | ||||
|                       submitting | ||||
| @@ -531,18 +532,17 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|     if (isPaste && isModifierPressed) { | ||||
|       ComposeLogic.handlePaste(state); | ||||
|     } else if (isSave && isModifierPressed) { | ||||
|       ComposeLogic.saveDraft(ref, state, postType: 1); | ||||
|       ComposeLogic.saveDraft(ref, state); | ||||
|       ComposeLogic.saveDraft(ref, state); | ||||
|     } else if (isSubmit && isModifierPressed && !state.submitting.value) { | ||||
|       ComposeLogic.performAction( | ||||
|         ref, | ||||
|         state, | ||||
|         context, | ||||
|         originalPost: originalPost, | ||||
|         postType: 1, // Article type | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Helper method to save article draft | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| @@ -10,10 +9,11 @@ import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/post/post_item.dart'; | ||||
| import 'package:island/widgets/post/post_quick_reply.dart'; | ||||
| import 'package:island/widgets/post/post_replies.dart'; | ||||
| import 'package:flutter_riverpod/flutter_riverpod.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 { | ||||
| @@ -22,21 +22,43 @@ Future<SnPost?> post(Ref ref, String id) async { | ||||
|   return SnPost.fromJson(resp.data); | ||||
| } | ||||
| 
 | ||||
| @RoutePage() | ||||
| 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, @PathParam('id') required this.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, | ||||
| @@ -51,6 +73,10 @@ 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), | ||||
|                       ], | ||||
| @@ -67,11 +93,15 @@ class PostDetailScreen extends HookConsumerWidget { | ||||
|                   right: 0, | ||||
|                   child: Material( | ||||
|                     elevation: 2, | ||||
|                     child: PostQuickReply( | ||||
|                       parent: post, | ||||
|                     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, | ||||
| @@ -1,6 +1,6 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'detail.dart'; | ||||
| part of 'post_detail.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
							
								
								
									
										165
									
								
								lib/screens/posts/post_search.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								lib/screens/posts/post_search.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
| 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/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 Scaffold( | ||||
|       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')), | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,11 +1,12 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| 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: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'; | ||||
| @@ -54,6 +55,7 @@ Future<SnSubscriptionStatus> publisherSubscriptionStatus( | ||||
|  | ||||
| @riverpod | ||||
| Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async { | ||||
|   try { | ||||
|     final publisher = await ref.watch(publisherProvider(pubName).future); | ||||
|     if (publisher.background == null) return null; | ||||
|     final palette = await PaletteGenerator.fromImageProvider( | ||||
| @@ -65,15 +67,14 @@ Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async { | ||||
|     final dominantColor = palette.dominantColor?.color; | ||||
|     if (dominantColor == null) return null; | ||||
|     return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; | ||||
|   } catch (_) { | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class PublisherProfileScreen extends HookConsumerWidget { | ||||
|   final String name; | ||||
|   const PublisherProfileScreen({ | ||||
|     super.key, | ||||
|     @PathParam("name") required this.name, | ||||
|   }); | ||||
|   const PublisherProfileScreen({super.key, required this.name}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -186,7 +187,7 @@ class PublisherProfileScreen extends HookConsumerWidget { | ||||
|                         ), | ||||
|                         onTap: () { | ||||
|                           Navigator.pop(context, true); | ||||
|                           context.router.pushPath('/account/${data.name}'); | ||||
|                           context.push('/account/${data.name}'); | ||||
|                         }, | ||||
|                       ), | ||||
|                       Expanded( | ||||
|   | ||||
| @@ -400,7 +400,7 @@ class _PublisherSubscriptionStatusProviderElement | ||||
| } | ||||
|  | ||||
| String _$publisherAppbarForcegroundColorHash() => | ||||
|     r'3ff2eebb48d3f3af1907052f471e648f5b14b13c'; | ||||
|     r'd781a806a242aea5c1609ec98c97c52fdd9f7db1'; | ||||
|  | ||||
| /// See also [publisherAppbarForcegroundColor]. | ||||
| @ProviderFor(publisherAppbarForcegroundColor) | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user