Compare commits

..

21 Commits

Author SHA1 Message Date
f04285848f 🐛 Fix upload file in share sheet 2025-06-29 18:03:18 +08:00
c4becb0a05 Publisher members 2025-06-28 18:57:32 +08:00
d22619396b 🐛 Fix linux builds 2025-06-28 11:45:50 +08:00
fe8640a6db 🚀 Launch 3.0.0+109 2025-06-28 02:48:15 +08:00
ff475d43dd 🐛 Dozen of hot fixes 2025-06-28 02:40:50 +08:00
9e8f6d57df 🚀 Launch 3.0.0+108 2025-06-28 01:53:07 +08:00
79227a12e2 Android notification extension 2025-06-28 01:28:44 +08:00
a23dcfe702 🐛 Bug fixes android sharing, ios notifications and more 2025-06-28 01:10:44 +08:00
243ecb3f71 Searchable realms 2025-06-28 00:47:03 +08:00
b8dec9f798 Joindable chat, detailed realms, discovery mixed into explore
🐛 bunch of bugs fixes
2025-06-28 00:24:43 +08:00
536375729f 💄 Optimize the notification snackbar position 2025-06-27 22:18:16 +08:00
5939a1dc5b Can make the chat and realm public 2025-06-27 21:52:57 +08:00
9d115a5712 Realm discovery and more detailed realm 2025-06-27 21:10:53 +08:00
f511612a53 Chat rooms in realm detail page 2025-06-27 17:54:29 +08:00
180fbcc558 Join the realm by user own 2025-06-27 17:30:42 +08:00
047cb9dc0d Realms discovery in explore 2025-06-27 02:56:58 +08:00
786f851a97 🐛 Fixes on post route 2025-06-27 02:35:06 +08:00
4deff5a920 Post tags 2025-06-27 02:31:21 +08:00
0361f031db Post editor tags 2025-06-27 00:56:07 +08:00
e90b35f19f 🐛 Fix share sheet 2025-06-26 14:45:44 +08:00
f2829b2012 ♻️ Refactor router, moved from auto_router to go_router 2025-06-26 14:13:44 +08:00
103 changed files with 4964 additions and 3362 deletions

View File

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

View File

@ -57,6 +57,9 @@ android {
dependencies { dependencies {
implementation("com.google.android.material:material:1.12.0") 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 { flutter {

View File

@ -46,12 +46,37 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" /> <data android:mimeType="image/*" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" /> <action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" /> <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> </intent-filter>
</activity> </activity>
@ -70,6 +95,19 @@
</intent-filter> </intent-filter>
</activity> </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 <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="dev.solsynth.solian.provider" android:authorities="dev.solsynth.solian.provider"

View File

@ -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())
}
}

View File

@ -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")
}
}
}

View File

@ -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)
}
})
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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())
}
}
}

View File

@ -98,6 +98,8 @@
"explore": "Explore", "explore": "Explore",
"exploreFilterSubscriptions": "Subscriptions", "exploreFilterSubscriptions": "Subscriptions",
"exploreFilterFriends": "Friends", "exploreFilterFriends": "Friends",
"discover": "Discover",
"joinRealm": "Join Realm",
"account": "Account", "account": "Account",
"name": "Name", "name": "Name",
"slug": "Slug", "slug": "Slug",
@ -307,6 +309,8 @@
"removeChatMemberHint": "Are you sure to remove this member from the room?", "removeChatMemberHint": "Are you sure to remove this member from the room?",
"removeRealmMember": "Remove Realm Member", "removeRealmMember": "Remove Realm Member",
"removeRealmMemberHint": "Are you sure to remove this member from the realm?", "removeRealmMemberHint": "Are you sure to remove this member from the realm?",
"removePublisherMember": "Remove Publisher Member",
"removePublisherMemberHint": "Are you sure to remove this member from the publisher?",
"memberRole": "Member Role", "memberRole": "Member Role",
"memberRoleHint": "Greater number has higher permission.", "memberRoleHint": "Greater number has higher permission.",
"memberRoleEdit": "Edit role for @{}", "memberRoleEdit": "Edit role for @{}",
@ -405,15 +409,15 @@
"lastActiveAt": "Last active at {}", "lastActiveAt": "Last active at {}",
"authDeviceLogout": "Logout", "authDeviceLogout": "Logout",
"authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.", "authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.",
"typingHint": {
"one": "{} is typing...",
"other": "{} are typing..."
},
"authDeviceEditLabel": "Edit Label", "authDeviceEditLabel": "Edit Label",
"authDeviceLabelTitle": "Edit Device Label", "authDeviceLabelTitle": "Edit Device Label",
"authDeviceLabelHint": "Enter a name for this device", "authDeviceLabelHint": "Enter a name for this device",
"authDeviceSwipeEditHint": "Swipe left to edit label", "authDeviceSwipeEditHint": "Swipe left to edit label",
"authDeviceSwipeLogoutHint": "Swipe right to logout device", "authDeviceSwipeLogoutHint": "Swipe right to logout device",
"typingHint": {
"one": "{} is typing...",
"other": "{} are typing..."
},
"settingsAppearance": "Appearance", "settingsAppearance": "Appearance",
"settingsServer": "Server", "settingsServer": "Server",
"settingsBehavior": "Behavior", "settingsBehavior": "Behavior",
@ -587,6 +591,11 @@
"addAdditionalMessage": "Add additional message...", "addAdditionalMessage": "Add additional message...",
"uploadingFiles": "Uploading files...", "uploadingFiles": "Uploading files...",
"sharedSuccessfully": "Shared successfully!", "sharedSuccessfully": "Shared successfully!",
"shareSuccess": "Shared successfully!",
"shareToSpecificChatSuccess": "Shared to {} successfully!",
"wouldYouLikeToGoToChat": "Would you like to go to the chat?",
"no": "No",
"yes": "Yes",
"navigateToChat": "Navigate to Chat", "navigateToChat": "Navigate to Chat",
"wouldYouLikeToNavigateToChat": "Would you like to navigate to the chat?", "wouldYouLikeToNavigateToChat": "Would you like to navigate to the chat?",
"abuseReport": "Report", "abuseReport": "Report",
@ -610,5 +619,17 @@
"abuseReportTypeOffensiveContent": "Offensive Content", "abuseReportTypeOffensiveContent": "Offensive Content",
"abuseReportTypePrivacyViolation": "Privacy Violation", "abuseReportTypePrivacyViolation": "Privacy Violation",
"abuseReportTypeIllegalContent": "Illegal Content", "abuseReportTypeIllegalContent": "Illegal Content",
"abuseReportTypeOther": "Other" "abuseReportTypeOther": "Other",
"tags": "Tags",
"tagsHint": "Enter tags, separated by commas",
"categories": "Categories",
"categoriesHint": "Enter categories, separated by commas",
"chatNotJoined": "You have not joined this chat yet.",
"chatUnableJoin": "You can't join this chat due to it's access control settings.",
"chatJoin": "Join the Chat",
"realmJoin": "Join the Realm",
"realmJoinSuccess": "Successfully joined the realm.",
"discoverRealms": "Discover Realms",
"discoverPublishers": "Discover Publishers",
"search": "Search"
} }

View File

@ -84,7 +84,7 @@ PODS:
- Flutter - Flutter
- flutter_platform_alert (0.0.1): - flutter_platform_alert (0.0.1):
- Flutter - Flutter
- flutter_secure_storage (3.3.1): - flutter_secure_storage (6.0.0):
- Flutter - Flutter
- flutter_timezone (0.0.1): - flutter_timezone (0.0.1):
- Flutter - Flutter
@ -362,7 +362,7 @@ SPEC CHECKSUMS:
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3 flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
flutter_secure_storage: 50035aef357c5a8bdd67fd6bc81370d46efc4d16 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117 flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117

View File

@ -857,7 +857,7 @@
INFOPLIST_FILE = SolianShareExtension/Info.plist; INFOPLIST_FILE = SolianShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension; INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.5; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -900,7 +900,7 @@
INFOPLIST_FILE = SolianShareExtension/Info.plist; INFOPLIST_FILE = SolianShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension; INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.5; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -940,7 +940,7 @@
INFOPLIST_FILE = SolianShareExtension/Info.plist; INFOPLIST_FILE = SolianShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension; INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.5; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -979,7 +979,7 @@
INFOPLIST_FILE = SolianNotificationService/Info.plist; INFOPLIST_FILE = SolianNotificationService/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService; INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.5; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -1021,7 +1021,7 @@
INFOPLIST_FILE = SolianNotificationService/Info.plist; INFOPLIST_FILE = SolianNotificationService/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService; INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.5; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -1060,7 +1060,7 @@
INFOPLIST_FILE = SolianNotificationService/Info.plist; INFOPLIST_FILE = SolianNotificationService/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService; INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.5; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",

View File

@ -11,6 +11,21 @@ import UIKit
) -> Bool { ) -> Bool {
UNUserNotificationCenter.current().delegate = notifyDelegate 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) GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }

View File

@ -10,40 +10,51 @@ import Alamofire
class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate { class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
if let textResponse = response as? UNTextInputNotificationResponse { guard let textResponse = response as? UNTextInputNotificationResponse else {
let content = response.notification.request.content completionHandler()
guard let metadata = content.userInfo["meta"] as? [AnyHashable: Any] else { return
return }
}
let content = response.notification.request.content
var token: String? = UserDefaults.standard.getFlutterToken()
if token == nil { // Only handle replies for new messages
return guard let notificationType = content.userInfo["type"] as? String, notificationType == "messages.new" else {
} completionHandler()
return
let serverUrl = UserDefaults.standard.getServerUrl() }
let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages"
guard let metadata = content.userInfo["meta"] as? [AnyHashable: Any] else {
let parameters: [String: Any?] = [ completionHandler()
"content": textResponse.userText, return
"replied_message_id": metadata["message_id"]
]
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: HTTPHeaders(
[HTTPHeader(name: "Authorization", value: "AtField \(token!)")]
))
.validate()
.responseString { response in
switch response.result {
case .success(_):
break
case .failure(let error):
print("Failed to send chat reply message: \(error)")
break
}
}
} }
completionHandler() guard let token = UserDefaults.standard.getFlutterToken() else {
completionHandler()
return
}
let serverUrl = UserDefaults.standard.getServerUrl()
let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages"
let parameters: [String: Any?] = [
"content": textResponse.userText,
"replied_message_id": metadata["message_id"]
]
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: HTTPHeaders(
[HTTPHeader(name: "Authorization", value: "AtField \(token)")]
))
.validate()
.responseString { response in
switch response.result {
case .success(_):
break
case .failure(let error):
print("Failed to send chat reply message: \(error)")
break
}
// Call completion handler after network request is finished
completionHandler()
}
} }
} }

View File

@ -60,21 +60,7 @@ class NotificationService: UNNotificationServiceExtension {
let pfpIdentifier = meta["pfp"] as? String let pfpIdentifier = meta["pfp"] as? String
let replyableMessageCategory = UNNotificationCategory( content.categoryIdentifier = "REPLYABLE_MESSAGE"
identifier: content.categoryIdentifier,
actions: [
UNTextInputNotificationAction(
identifier: "reply_action",
title: "Reply",
options: []
),
],
intentIdentifiers: [],
options: []
)
UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory])
content.categoryIdentifier = replyableMessageCategory.identifier
let metaCopy = meta as? [String: Any] ?? [:] let metaCopy = meta as? [String: Any] ?? [:]
let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil

View File

@ -71,25 +71,32 @@ class MessageRepository {
bool synced = false, bool synced = false,
}) async { }) async {
try { 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( final localMessages = await _getCachedMessages(
room.id, room.id,
offset: offset, offset: offset,
take: take, take: take,
); );
// If it already synced with the remote, skip this // If local cache has messages, return them. This is the common case for scrolling up.
if (offset == 0 && !synced) { if (localMessages.isNotEmpty) {
// Fetch latest messages return localMessages;
_fetchAndCacheMessages(room.id, offset: offset, take: take);
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); return await _fetchAndCacheMessages(room.id, offset: offset, take: take);
} catch (e) { } 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( final localMessages = await _getCachedMessages(
room.id, room.id,
offset: offset, offset: offset,
@ -117,24 +124,26 @@ class MessageRepository {
final dbLocalMessages = final dbLocalMessages =
dbMessages.map(_database.companionToMessage).toList(); dbMessages.map(_database.companionToMessage).toList();
// Combine with pending messages // Combine with pending messages for the first page
final pendingForRoom = if (offset == 0) {
pendingMessages.values.where((msg) => msg.roomId == roomId).toList(); final pendingForRoom =
pendingMessages.values.where((msg) => msg.roomId == roomId).toList();
// Sort by timestamp descending (newest first) final allMessages = [...pendingForRoom, ...dbLocalMessages];
final allMessages = [...pendingForRoom, ...dbLocalMessages]; allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
// Apply pagination // Remove duplicates by ID, preserving the order
if (offset >= allMessages.length) { final uniqueMessages = <LocalChatMessage>[];
return []; final seenIds = <String>{};
for (final message in allMessages) {
if (seenIds.add(message.id)) {
uniqueMessages.add(message);
}
}
return uniqueMessages;
} }
final end = return dbLocalMessages;
(offset + take) > allMessages.length
? allMessages.length
: (offset + take);
return allMessages.sublist(offset, end);
} }
Future<List<LocalChatMessage>> _fetchAndCacheMessages( Future<List<LocalChatMessage>> _fetchAndCacheMessages(

View File

@ -7,6 +7,7 @@ import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker_android/image_picker_android.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/userinfo.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/route.dart'; import 'package:island/route.dart';
import 'package:island/screens/tabs.dart';
import 'package:island/services/notify.dart'; import 'package:island/services/notify.dart';
import 'package:island/services/timezone.dart'; import 'package:island/services/timezone.dart';
import 'package:island/widgets/alert.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:flutter_native_splash/flutter_native_splash.dart';
import 'package:url_launcher/url_launcher_string.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 { void main() async {
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
@ -43,6 +50,7 @@ void main() async {
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
log("[SplashScreen] Firebase is ready!"); log("[SplashScreen] Firebase is ready!");
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
@ -125,7 +133,7 @@ void main() async {
); );
} }
final appRouter = AppRouter(); // Router will be provided through Riverpod
final globalOverlay = GlobalKey<OverlayState>(); final globalOverlay = GlobalKey<OverlayState>();
@ -141,7 +149,8 @@ class IslandApp extends HookConsumerWidget {
var uri = notification.data['action_uri'] as String; var uri = notification.data['action_uri'] as String;
if (uri.startsWith('/')) { if (uri.startsWith('/')) {
// In-app routes // In-app routes
appRouter.pushPath(notification.data['action_uri']); final router = ref.read(routerProvider);
router.go(notification.data['action_uri']);
} else { } else {
// External links // External links
launchUrlString(uri); launchUrlString(uri);
@ -150,17 +159,52 @@ class IslandApp extends HookConsumerWidget {
} }
useEffect(() { useEffect(() {
Future(() async { const channel = MethodChannel('dev.solsynth.solian/notifications');
RemoteMessage? initialMessage =
await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
handleMessage(initialMessage);
}
FirebaseMessaging.onMessageOpenedApp.listen(handleMessage); Future<void> handleInitialLink() async {
final String? link = await channel.invokeMethod('initialLink');
if (link != null) {
final router = ref.read(routerProvider);
router.go(link);
}
}
if (!kIsWeb && Platform.isAndroid) {
handleInitialLink();
}
channel.setMethodCallHandler((call) async {
if (call.method == 'newLink') {
final String link = call.arguments;
final router = ref.read(routerProvider);
router.go(link);
}
}); });
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(() { useEffect(() {
@ -183,20 +227,13 @@ class IslandApp extends HookConsumerWidget {
return null; return null;
}, []); }, []);
final router = ref.watch(routerProvider);
return MaterialApp.router( return MaterialApp.router(
theme: theme?.light, theme: theme?.light,
darkTheme: theme?.dark, darkTheme: theme?.dark,
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
routerConfig: appRouter.config( routerConfig: router,
navigatorObservers:
() => [
TabNavigationObserver(
onChange: (route) {
ref.read(currentRouteProvider.notifier).state = route;
},
),
],
),
supportedLocales: context.supportedLocales, supportedLocales: context.supportedLocales,
localizationsDelegates: [ localizationsDelegates: [
...context.localizationDelegates, ...context.localizationDelegates,
@ -210,10 +247,8 @@ class IslandApp extends HookConsumerWidget {
initialEntries: [ initialEntries: [
OverlayEntry( OverlayEntry(
builder: builder:
(_) => WindowScaffold( (_) =>
router: appRouter, WindowScaffold(child: child ?? const SizedBox.shrink()),
child: child ?? const SizedBox.shrink(),
),
), ),
], ],
); );

View File

@ -13,7 +13,8 @@ sealed class SnChatRoom with _$SnChatRoom {
required String? name, required String? name,
required String? description, required String? description,
required int type, required int type,
required bool isPublic, @Default(false) bool isPublic,
@Default(false) bool isCommunity,
required SnCloudFile? picture, required SnCloudFile? picture,
required SnCloudFile? background, required SnCloudFile? background,
required String? realmId, required String? realmId,

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$SnChatRoom { 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 /// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -29,16 +29,16 @@ $SnChatRoomCopyWith<SnChatRoom> get copyWith => _$SnChatRoomCopyWithImpl<SnChatR
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory $SnChatRoomCopyWith(SnChatRoom value, $Res Function(SnChatRoom) _then) = _$SnChatRoomCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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,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?,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 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 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 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?,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 as SnCloudFile?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
@ -128,14 +129,15 @@ $SnRealmCopyWith<$Res>? get realm {
@JsonSerializable() @JsonSerializable()
class _SnChatRoom implements SnChatRoom { 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); factory _SnChatRoom.fromJson(Map<String, dynamic> json) => _$SnChatRoomFromJson(json);
@override final String id; @override final String id;
@override final String? name; @override final String? name;
@override final String? description; @override final String? description;
@override final int type; @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? picture;
@override final SnCloudFile? background; @override final SnCloudFile? background;
@override final String? realmId; @override final String? realmId;
@ -166,16 +168,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory _$SnChatRoomCopyWith(_SnChatRoom value, $Res Function(_SnChatRoom) _then) = __$SnChatRoomCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_SnChatRoom(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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,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?,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 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 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 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?,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 as SnCloudFile?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable

View File

@ -11,7 +11,8 @@ _SnChatRoom _$SnChatRoomFromJson(Map<String, dynamic> json) => _SnChatRoom(
name: json['name'] as String?, name: json['name'] as String?,
description: json['description'] as String?, description: json['description'] as String?,
type: (json['type'] as num).toInt(), 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: picture:
json['picture'] == null json['picture'] == null
? null ? null
@ -44,6 +45,7 @@ Map<String, dynamic> _$SnChatRoomToJson(_SnChatRoom instance) =>
'description': instance.description, 'description': instance.description,
'type': instance.type, 'type': instance.type,
'is_public': instance.isPublic, 'is_public': instance.isPublic,
'is_community': instance.isCommunity,
'picture': instance.picture?.toJson(), 'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(), 'background': instance.background?.toJson(),
'realm_id': instance.realmId, 'realm_id': instance.realmId,

View File

@ -1,6 +1,8 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.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.freezed.dart';
part 'post.g.dart'; part 'post.g.dart';
@ -30,11 +32,11 @@ sealed class SnPost with _$SnPost {
String? forwardedPostId, String? forwardedPostId,
SnPost? forwardedPost, SnPost? forwardedPost,
@Default([]) List<SnCloudFile> attachments, @Default([]) List<SnCloudFile> attachments,
@Default(SnPublisher()) SnPublisher publisher, required SnPublisher publisher,
@Default({}) Map<String, int> reactionsCount, @Default({}) Map<String, int> reactionsCount,
@Default([]) List<dynamic> reactions, @Default([]) List<dynamic> reactions,
@Default([]) List<dynamic> tags, @Default([]) List<PostTag> tags,
@Default([]) List<dynamic> categories, @Default([]) List<PostCategory> categories,
@Default([]) List<dynamic> collections, @Default([]) List<dynamic> collections,
@Default(null) DateTime? createdAt, @Default(null) DateTime? createdAt,
@Default(null) DateTime? updatedAt, @Default(null) DateTime? updatedAt,
@ -45,29 +47,6 @@ sealed class SnPost with _$SnPost {
factory SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json); 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 @freezed
sealed class SnPublisherStats with _$SnPublisherStats { sealed class SnPublisherStats with _$SnPublisherStats {
const factory SnPublisherStats({ const factory SnPublisherStats({

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$SnPost { 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 /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -49,7 +49,7 @@ abstract mixin class $SnPostCopyWith<$Res> {
factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl; factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl;
@useResult @useResult
$Res call({ $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 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 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>,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<PostTag>,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<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 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?,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?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
@ -156,7 +156,7 @@ $SnPublisherCopyWith<$Res> get publisher {
@JsonSerializable() @JsonSerializable()
class _SnPost implements SnPost { 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); factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json);
@override final String id; @override final String id;
@ -195,7 +195,7 @@ class _SnPost implements SnPost {
return EqualUnmodifiableListView(_attachments); return EqualUnmodifiableListView(_attachments);
} }
@override@JsonKey() final SnPublisher publisher; @override final SnPublisher publisher;
final Map<String, int> _reactionsCount; final Map<String, int> _reactionsCount;
@override@JsonKey() Map<String, int> get reactionsCount { @override@JsonKey() Map<String, int> get reactionsCount {
if (_reactionsCount is EqualUnmodifiableMapView) return _reactionsCount; if (_reactionsCount is EqualUnmodifiableMapView) return _reactionsCount;
@ -210,15 +210,15 @@ class _SnPost implements SnPost {
return EqualUnmodifiableListView(_reactions); return EqualUnmodifiableListView(_reactions);
} }
final List<dynamic> _tags; final List<PostTag> _tags;
@override@JsonKey() List<dynamic> get tags { @override@JsonKey() List<PostTag> get tags {
if (_tags is EqualUnmodifiableListView) return _tags; if (_tags is EqualUnmodifiableListView) return _tags;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_tags); return EqualUnmodifiableListView(_tags);
} }
final List<dynamic> _categories; final List<PostCategory> _categories;
@override@JsonKey() List<dynamic> get categories { @override@JsonKey() List<PostCategory> get categories {
if (_categories is EqualUnmodifiableListView) return _categories; if (_categories is EqualUnmodifiableListView) return _categories;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_categories); return EqualUnmodifiableListView(_categories);
@ -269,7 +269,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl; factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 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 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>,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<PostTag>,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<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 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?,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?,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 /// @nodoc
mixin _$SnPublisherStats { mixin _$SnPublisherStats {

View File

@ -48,18 +48,23 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
?.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>)) ?.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
const [], const [],
publisher: publisher: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
json['publisher'] == null
? const SnPublisher()
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
reactionsCount: reactionsCount:
(json['reactions_count'] as Map<String, dynamic>?)?.map( (json['reactions_count'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, (e as num).toInt()), (k, e) => MapEntry(k, (e as num).toInt()),
) ?? ) ??
const {}, const {},
reactions: json['reactions'] as List<dynamic>? ?? const [], reactions: json['reactions'] as List<dynamic>? ?? const [],
tags: json['tags'] as List<dynamic>? ?? const [], tags:
categories: json['categories'] as List<dynamic>? ?? const [], (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 [], collections: json['collections'] as List<dynamic>? ?? const [],
createdAt: createdAt:
json['created_at'] == null json['created_at'] == null
@ -102,8 +107,8 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'publisher': instance.publisher.toJson(), 'publisher': instance.publisher.toJson(),
'reactions_count': instance.reactionsCount, 'reactions_count': instance.reactionsCount,
'reactions': instance.reactions, 'reactions': instance.reactions,
'tags': instance.tags, 'tags': instance.tags.map((e) => e.toJson()).toList(),
'categories': instance.categories, 'categories': instance.categories.map((e) => e.toJson()).toList(),
'collections': instance.collections, 'collections': instance.collections,
'created_at': instance.createdAt?.toIso8601String(), 'created_at': instance.createdAt?.toIso8601String(),
'updated_at': instance.updatedAt?.toIso8601String(), 'updated_at': instance.updatedAt?.toIso8601String(),
@ -111,64 +116,6 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'is_truncated': instance.isTruncated, '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 _$SnPublisherStatsFromJson(Map<String, dynamic> json) =>
_SnPublisherStats( _SnPublisherStats(
postsCreated: (json['posts_created'] as num).toInt(), postsCreated: (json['posts_created'] as num).toInt(),

View 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);
}

View 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

View 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
View 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);
}

View 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

View 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
View File

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

View File

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

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

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

View File

@ -10,8 +10,8 @@ sealed class SnRealm with _$SnRealm {
const factory SnRealm({ const factory SnRealm({
required String id, required String id,
required String slug, required String slug,
required String name, @Default('') String name,
required String description, @Default('') String description,
required String? verifiedAs, required String? verifiedAs,
required DateTime? verifiedAt, required DateTime? verifiedAt,
required bool isCommunity, required bool isCommunity,

View File

@ -117,13 +117,13 @@ $SnCloudFileCopyWith<$Res>? get background {
@JsonSerializable() @JsonSerializable()
class _SnRealm implements SnRealm { 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); factory _SnRealm.fromJson(Map<String, dynamic> json) => _$SnRealmFromJson(json);
@override final String id; @override final String id;
@override final String slug; @override final String slug;
@override final String name; @override@JsonKey() final String name;
@override final String description; @override@JsonKey() final String description;
@override final String? verifiedAs; @override final String? verifiedAs;
@override final DateTime? verifiedAt; @override final DateTime? verifiedAt;
@override final bool isCommunity; @override final bool isCommunity;

View File

@ -9,8 +9,8 @@ part of 'realm.dart';
_SnRealm _$SnRealmFromJson(Map<String, dynamic> json) => _SnRealm( _SnRealm _$SnRealmFromJson(Map<String, dynamic> json) => _SnRealm(
id: json['id'] as String, id: json['id'] as String,
slug: json['slug'] as String, slug: json['slug'] as String,
name: json['name'] as String, name: json['name'] as String? ?? '',
description: json['description'] as String, description: json['description'] as String? ?? '',
verifiedAs: json['verified_as'] as String?, verifiedAs: json['verified_as'] as String?,
verifiedAt: verifiedAt:
json['verified_at'] == null json['verified_at'] == null

View File

@ -1,6 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.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.freezed.dart';
part 'sticker.g.dart'; part 'sticker.g.dart';

View File

@ -11,11 +11,6 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
UserInfoNotifier(this._ref) : super(const AsyncValue.data(null)); 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 { Future<void> fetchUser() async {
try { try {
final client = _ref.read(apiClientProvider); final client = _ref.read(apiClientProvider);
@ -32,7 +27,6 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
state = const AsyncValue.data(null); state = const AsyncValue.data(null);
final prefs = _ref.read(sharedPreferencesProvider); final prefs = _ref.read(sharedPreferencesProvider);
await prefs.remove(kTokenPairStoreKey); await prefs.remove(kTokenPairStoreKey);
_ref.invalidate(userInfoProvider);
_ref.invalidate(tokenProvider); _ref.invalidate(tokenProvider);
} }
} }

View File

@ -1,98 +1,343 @@
import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart';
import 'package:island/route.gr.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/widgets/app_wrapper.dart';
import 'package:island/screens/tabs.dart';
@AutoRouterConfig(replaceInRouteName: 'Screen|Page,Route') import 'package:island/screens/explore.dart';
class AppRouter extends RootStackRouter { import 'package:island/screens/account.dart';
@override import 'package:island/screens/notification.dart';
RouteType get defaultRouteType => RouteType.adaptive(); 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/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/posts/compose.dart';
import 'package:island/screens/posts/detail.dart';
import 'package:island/screens/posts/pub_profile.dart';
import 'package:island/screens/auth/login.dart';
import 'package:island/screens/auth/create_account.dart';
import 'package:island/screens/settings.dart';
import 'package:island/screens/realm/realms.dart';
import 'package:island/screens/realm/detail.dart';
import 'package:island/screens/account/event_calendar.dart';
import 'package:island/screens/discovery/realms.dart';
@override // Shell route keys for nested navigation
List<AutoRoute> get routes => [ final rootNavigatorKey = GlobalKey<NavigatorState>();
AutoRoute(path: '/', page: AppWrapper.page, children: _appRoutes), final _shellNavigatorKey = GlobalKey<NavigatorState>();
]; final _tabsShellKey = GlobalKey<NavigatorState>();
List<AutoRoute> get _appRoutes => [ // Provider for the router
// Standalone routes without bottom navigation final routerProvider = Provider<GoRouter>((ref) {
AutoRoute(page: PostComposeRoute.page, path: 'posts/compose'), return GoRouter(
AutoRoute(page: PostEditRoute.page, path: 'posts/:id/edit'), navigatorKey: rootNavigatorKey,
AutoRoute(page: CallRoute.page, path: 'chat/:id/call'), initialLocation: '/',
AutoRoute(page: EventCalanderRoute.page, path: 'account/:name/calendar'), routes: [
ShellRoute(
// Main tabs with bottom navigation and shell routes for desktop layout navigatorKey: _shellNavigatorKey,
AutoRoute( builder: (context, state, child) {
page: TabsRoute.page, return AppWrapper(child: child);
path: '', },
children: [ routes: [
AutoRoute( // Standalone routes without bottom navigation
page: ExploreShellRoute.page, GoRoute(
path: '', path: '/posts/compose',
children: [ builder:
AutoRoute(page: ExploreRoute.page, path: ''), (context, state) => PostComposeScreen(
AutoRoute(page: PostDetailRoute.page, path: 'posts/:id'), initialState: state.extra as PostComposeInitialState?,
AutoRoute( ),
page: PublisherProfileRoute.page, ),
path: 'publishers/:name', GoRoute(
), path: '/posts/:id/edit',
], builder: (context, state) {
), final id = state.pathParameters['id']!;
AutoRoute( return PostEditScreen(id: id);
page: AccountShellRoute.page, },
path: 'account', ),
children: [ GoRoute(
AutoRoute(page: AccountRoute.page, path: ''), path: '/chat/:id/call',
AutoRoute(page: NotificationRoute.page, path: 'notifications'), builder: (context, state) {
AutoRoute(page: WalletRoute.page, path: 'wallet'), final id = state.pathParameters['id']!;
AutoRoute(page: RelationshipRoute.page, path: 'relationships'), return CallScreen(roomId: id);
AutoRoute(page: AccountProfileRoute.page, path: ':name'), },
AutoRoute(page: UpdateProfileRoute.page, path: 'me/update'), ),
AutoRoute(page: LevelingRoute.page, path: 'me/leveling'), GoRoute(
AutoRoute(page: AccountSettingsRoute.page, path: 'settings'), path: '/account/:name/calendar',
], builder: (context, state) {
), final name = state.pathParameters['name']!;
AutoRoute(page: RealmListRoute.page, path: 'realms'), return EventCalanderScreen(name: name);
AutoRoute( },
page: ChatShellRoute.page, ),
path: 'chat', ShellRoute(
children: [ builder:
AutoRoute(page: ChatListRoute.page, path: ''), (context, state, child) => CreatorHubShellScreen(child: child),
AutoRoute(page: ChatRoomRoute.page, path: ':id'), routes: [
AutoRoute(page: NewChatRoute.page, path: 'new'), GoRoute(
AutoRoute(page: EditChatRoute.page, path: ':id/edit'), path: '/creators',
AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'), builder: (context, state) => const CreatorHubScreen(),
], ),
), GoRoute(
], path: '/creators/:name/posts',
), builder: (context, state) {
AutoRoute( final name = state.pathParameters['name']!;
page: CreatorHubShellRoute.page, return CreatorPostListScreen(pubName: name);
path: 'creators', },
children: [ ),
AutoRoute(page: CreatorHubRoute.page, path: ''), GoRoute(
AutoRoute(page: CreatorPostListRoute.page, path: ':name/posts'), path: '/creators/:name/stickers',
AutoRoute(page: StickersRoute.page, path: ':name/stickers'), builder: (context, state) {
AutoRoute(page: NewStickerPacksRoute.page, path: ':name/stickers/new'), final name = state.pathParameters['name']!;
AutoRoute( return StickersScreen(pubName: name);
page: EditStickerPacksRoute.page, },
path: ':name/stickers/:packId/edit', ),
), GoRoute(
AutoRoute( path: '/creators/:name/stickers/new',
page: StickerPackDetailRoute.page, builder: (context, state) {
path: ':name/stickers/:packId', final name = state.pathParameters['name']!;
), return NewStickerPacksScreen(pubName: name);
AutoRoute(page: NewStickersRoute.page, path: ':name/stickers/new'), },
AutoRoute( ),
page: EditStickersRoute.page, GoRoute(
path: ':name/stickers/:id/edit', path: '/creators/:name/stickers/:packId/edit',
), builder: (context, state) {
AutoRoute(page: NewPublisherRoute.page, path: 'new'), final name = state.pathParameters['name']!;
AutoRoute(page: EditPublisherRoute.page, path: ':name/edit'), final packId = state.pathParameters['packId']!;
], return EditStickerPacksScreen(pubName: name, packId: packId);
), },
AutoRoute(page: LoginRoute.page, path: 'auth/login'), ),
AutoRoute(page: CreateAccountRoute.page, path: 'auth/create-account'), GoRoute(
AutoRoute(page: SettingsRoute.page, path: 'settings'), path: '/creators/:name/stickers/:packId',
AutoRoute(page: NewRealmRoute.page, path: 'realms/new'), builder: (context, state) {
AutoRoute(page: RealmDetailRoute.page, path: 'realms/:slug'), final name = state.pathParameters['name']!;
AutoRoute(page: EditRealmRoute.page, path: 'realms/:slug/edit'), 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);
},
),
],
),
// 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(),
),
// 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/: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(),
),
],
),
// 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);
},
),
],
),
// 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(),
),
],
),
],
),
],
),
],
);
});
// 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();
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,13 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/message.dart'; import 'package:island/pods/message.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/notification.dart'; import 'package:island/screens/notification.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_name.dart'; import 'package:island/widgets/account/account_name.dart';
@ -19,9 +18,9 @@ import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@RoutePage()
class AccountShellScreen extends HookConsumerWidget { class AccountShellScreen extends HookConsumerWidget {
const AccountShellScreen({super.key}); final Widget child;
const AccountShellScreen({super.key, required this.child});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -34,17 +33,16 @@ class AccountShellScreen extends HookConsumerWidget {
children: [ children: [
Flexible(flex: 2, child: AccountScreen(isAside: true)), Flexible(flex: 2, child: AccountScreen(isAside: true)),
VerticalDivider(width: 1), 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 { class AccountScreen extends HookConsumerWidget {
final bool isAside; final bool isAside;
const AccountScreen({super.key, this.isAside = false}); const AccountScreen({super.key, this.isAside = false});
@ -100,9 +98,7 @@ class AccountScreen extends HookConsumerWidget {
radius: 24, radius: 24,
), ),
onTap: () { onTap: () {
context.router.push( context.push('/account/${user.value!.name}');
AccountProfileRoute(name: user.value!.name),
);
}, },
), ),
Expanded( Expanded(
@ -147,7 +143,7 @@ class AccountScreen extends HookConsumerWidget {
progress: user.value!.profile.levelingProgress, progress: user.value!.profile.levelingProgress,
), ),
onTap: () { onTap: () {
context.router.push(LevelingRoute()); context.push('/account/me/leveling');
}, },
).padding(horizontal: 12), ).padding(horizontal: 12),
Row( Row(
@ -165,7 +161,7 @@ class AccountScreen extends HookConsumerWidget {
], ],
).padding(horizontal: 16, vertical: 12), ).padding(horizontal: 16, vertical: 12),
onTap: () { onTap: () {
context.router.push(CreatorHubShellRoute()); context.push('/creators');
}, },
), ),
).height(140), ).height(140),
@ -204,7 +200,7 @@ class AccountScreen extends HookConsumerWidget {
], ],
), ),
onTap: () { onTap: () {
context.router.push(NotificationRoute()); context.push('/account/notifications');
}, },
), ),
ListTile( ListTile(
@ -214,7 +210,7 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('wallet').tr(), title: Text('wallet').tr(),
onTap: () { onTap: () {
context.router.push(WalletRoute()); context.push('/account/wallet');
}, },
), ),
ListTile( ListTile(
@ -224,7 +220,7 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('relationships').tr(), title: Text('relationships').tr(),
onTap: () { onTap: () {
context.router.push(RelationshipRoute()); context.push('/account/relationship');
}, },
), ),
const Divider(height: 1).padding(vertical: 8), const Divider(height: 1).padding(vertical: 8),
@ -235,7 +231,7 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('appSettings').tr(), title: Text('appSettings').tr(),
onTap: () { onTap: () {
context.router.push(SettingsRoute()); context.push('/settings');
}, },
), ),
ListTile( ListTile(
@ -245,7 +241,7 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('updateYourProfile').tr(), title: Text('updateYourProfile').tr(),
onTap: () { onTap: () {
context.router.push(UpdateProfileRoute()); context.push('/account/me/update');
}, },
), ),
ListTile( ListTile(
@ -255,7 +251,7 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('accountSettings').tr(), title: Text('accountSettings').tr(),
onTap: () { onTap: () {
context.router.push(AccountSettingsRoute()); context.push('/account/me/settings');
}, },
), ),
if (kDebugMode) const Divider(height: 1).padding(vertical: 8), if (kDebugMode) const Divider(height: 1).padding(vertical: 8),
@ -320,7 +316,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
child: Card( child: Card(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
context.router.push(CreateAccountRoute()); context.push('/auth/create');
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -342,7 +338,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
child: Card( child: Card(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
context.router.push(LoginRoute()); context.push('/auth/login');
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -361,7 +357,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
const Gap(8), const Gap(8),
TextButton( TextButton(
onPressed: () { onPressed: () {
context.router.push(SettingsRoute()); context.push('/settings');
}, },
child: Text('appSettings').tr(), child: Text('appSettings').tr(),
).center(), ).center(),

View File

@ -1,4 +1,3 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -12,10 +11,9 @@ import 'package:island/widgets/account/event_calendar.dart';
import 'package:island/widgets/account/fortune_graph.dart'; import 'package:island/widgets/account/fortune_graph.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@RoutePage()
class EventCalanderScreen extends HookConsumerWidget { class EventCalanderScreen extends HookConsumerWidget {
final String name; final String name;
const EventCalanderScreen({super.key, @PathParam("name") required this.name}); const EventCalanderScreen({super.key, required this.name});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {

View File

@ -1,4 +1,3 @@
import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@ -31,7 +30,6 @@ Future<SnWalletSubscription?> accountStellarSubscription(Ref ref) async {
} }
} }
@RoutePage()
class LevelingScreen extends HookConsumerWidget { class LevelingScreen extends HookConsumerWidget {
const LevelingScreen({super.key}); const LevelingScreen({super.key});

View File

@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:auto_route/annotations.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -51,7 +50,6 @@ Future<List<SnAccountConnection>> accountConnections(Ref ref) async {
.toList(); .toList();
} }
@RoutePage()
class AccountSettingsScreen extends HookConsumerWidget { class AccountSettingsScreen extends HookConsumerWidget {
const AccountSettingsScreen({super.key}); const AccountSettingsScreen({super.key});

View File

@ -1,4 +1,3 @@
import 'package:auto_route/auto_route.dart';
import 'package:croppy/croppy.dart' hide cropImage; import 'package:croppy/croppy.dart' hide cropImage;
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.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'}; const kServerSupportedLanguages = {'en-US': 'en-us', 'zh-CN': 'zh-hans'};
@RoutePage()
class UpdateProfileScreen extends HookConsumerWidget { class UpdateProfileScreen extends HookConsumerWidget {
const UpdateProfileScreen({super.key}); const UpdateProfileScreen({super.key});

View File

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/chat.dart'; import 'package:island/models/chat.dart';
@ -53,17 +53,21 @@ Future<List<SnAccountBadge>> accountBadges(Ref ref, String uname) async {
@riverpod @riverpod
Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async { Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
final account = await ref.watch(accountProvider(uname).future); try {
if (account.profile.background == null) return null; final account = await ref.watch(accountProvider(uname).future);
final palette = await PaletteGenerator.fromImageProvider( if (account.profile.background == null) return null;
CloudImageWidget.provider( final palette = await PaletteGenerator.fromImageProvider(
fileId: account.profile.background!.id, CloudImageWidget.provider(
serverUrl: ref.watch(serverUrlProvider), fileId: account.profile.background!.id,
), serverUrl: ref.watch(serverUrlProvider),
); ),
final dominantColor = palette.dominantColor?.color; );
if (dominantColor == null) return null; final dominantColor = palette.dominantColor?.color;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
} catch (_) {
return null;
}
} }
@riverpod @riverpod
@ -96,13 +100,9 @@ Future<SnRelationship?> accountRelationship(Ref ref, String uname) async {
} }
} }
@RoutePage()
class AccountProfileScreen extends HookConsumerWidget { class AccountProfileScreen extends HookConsumerWidget {
final String name; final String name;
const AccountProfileScreen({ const AccountProfileScreen({super.key, required this.name});
super.key,
@PathParam("name") required this.name,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -142,7 +142,7 @@ class AccountProfileScreen extends HookConsumerWidget {
Future<void> directMessageAction() async { Future<void> directMessageAction() async {
if (!account.hasValue) return; if (!account.hasValue) return;
if (accountChat.value != null) { if (accountChat.value != null) {
context.router.pushPath('/chat/${accountChat.value!.id}'); context.push('/chat/${accountChat.value!.id}');
return; return;
} }
showLoadingModal(context); showLoadingModal(context);
@ -153,7 +153,7 @@ class AccountProfileScreen extends HookConsumerWidget {
data: {'related_user_id': account.value!.id}, data: {'related_user_id': account.value!.id},
); );
final chat = SnChatRoom.fromJson(resp.data); 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)); ref.invalidate(accountDirectChatProvider(name));
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);

View File

@ -268,7 +268,7 @@ class _AccountBadgesProviderElement
} }
String _$accountAppbarForcegroundColorHash() => String _$accountAppbarForcegroundColorHash() =>
r'f654a7a5594eda1500906e9ad023c22772257a9b'; r'8ee0cae10817b77fb09548a482f5247662b4374c';
/// See also [accountAppbarForcegroundColor]. /// See also [accountAppbarForcegroundColor].
@ProviderFor(accountAppbarForcegroundColor) @ProviderFor(accountAppbarForcegroundColor)

View File

@ -1,4 +1,3 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -204,7 +203,6 @@ class RelationshipListTile extends StatelessWidget {
} }
} }
@RoutePage()
class RelationshipScreen extends HookConsumerWidget { class RelationshipScreen extends HookConsumerWidget {
const RelationshipScreen({super.key}); const RelationshipScreen({super.key});
@ -217,6 +215,7 @@ class RelationshipScreen extends HookConsumerWidget {
Future<void> addFriend() async { Future<void> addFriend() async {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
context: context, context: context,
useRootNavigator: true,
builder: (context) => AccountPickerSheet(), builder: (context) => AccountPickerSheet(),
); );
if (result == null) return; if (result == null) return;

View File

@ -1,12 +1,11 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:email_validator/email_validator.dart'; import 'package:email_validator/email_validator.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/account/me/update.dart'; import 'package:island/screens/account/me/update.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
@ -16,7 +15,6 @@ import 'package:url_launcher/url_launcher_string.dart';
import 'captcha.dart'; import 'captcha.dart';
@RoutePage()
class CreateAccountScreen extends HookConsumerWidget { class CreateAccountScreen extends HookConsumerWidget {
const CreateAccountScreen({super.key}); const CreateAccountScreen({super.key});
@ -307,7 +305,7 @@ class _PostCreateModal extends HookConsumerWidget {
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
context.router.replace(LoginRoute()); context.pushReplacement('/auth/login');
}, },
child: Text('login'.tr()), child: Text('login'.tr()),
), ),

View File

@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:auto_route/auto_route.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.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), 4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm),
}; };
@RoutePage()
class LoginScreen extends HookConsumerWidget { class LoginScreen extends HookConsumerWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});

View File

@ -1,4 +1,3 @@
import 'package:auto_route/annotations.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -14,10 +13,9 @@ import 'package:livekit_client/livekit_client.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@RoutePage()
class CallScreen extends HookConsumerWidget { class CallScreen extends HookConsumerWidget {
final String roomId; final String roomId;
const CallScreen({super.key, @PathParam('id') required this.roomId}); const CallScreen({super.key, required this.roomId});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {

View File

@ -1,9 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:croppy/croppy.dart' hide cropImage; import 'package:croppy/croppy.dart' hide cropImage;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/chat_summary.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/realm/realms.dart'; import 'package:island/screens/realm/realms.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
@ -173,9 +172,9 @@ Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
.toList(); .toList();
} }
@RoutePage()
class ChatShellScreen extends HookConsumerWidget { class ChatShellScreen extends HookConsumerWidget {
const ChatShellScreen({super.key}); final Widget child;
const ChatShellScreen({super.key, required this.child});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -187,18 +186,17 @@ class ChatShellScreen extends HookConsumerWidget {
child: Row( child: Row(
children: [ children: [
Flexible(flex: 2, child: ChatListScreen(isAside: true)), Flexible(flex: 2, child: ChatListScreen(isAside: true)),
VerticalDivider(width: 1), const VerticalDivider(width: 1),
Flexible(flex: 4, child: AutoRouter()), Flexible(flex: 4, child: child),
], ],
), ),
); );
} }
return AppBackground(isRoot: true, child: AutoRouter()); return AppBackground(isRoot: true, child: child);
} }
} }
@RoutePage()
class ChatListScreen extends HookConsumerWidget { class ChatListScreen extends HookConsumerWidget {
final bool isAside; final bool isAside;
const ChatListScreen({super.key, this.isAside = false}); const ChatListScreen({super.key, this.isAside = false});
@ -229,7 +227,8 @@ class ChatListScreen extends HookConsumerWidget {
Future<void> createDirectMessage() async { Future<void> createDirectMessage() async {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
context: context, context: context,
builder: (context) => AccountPickerSheet(), useRootNavigator: true,
builder: (context) => const AccountPickerSheet(),
); );
if (result == null) return; if (result == null) return;
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
@ -244,7 +243,7 @@ class ChatListScreen extends HookConsumerWidget {
return AppScaffold( return AppScaffold(
extendBody: false, // Prevent conflicts with tabs navigation extendBody: false, // Prevent conflicts with tabs navigation
appBar: AppBar( appBar: AppBar(
title: Text('chat').tr(), title: const Text('chat').tr(),
bottom: TabBar( bottom: TabBar(
controller: tabController, controller: tabController,
tabs: [ tabs: [
@ -298,7 +297,7 @@ class ChatListScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: (context) => _ChatInvitesSheet(), builder: (context) => const _ChatInvitesSheet(),
); );
}, },
), ),
@ -309,17 +308,18 @@ class ChatListScreen extends HookConsumerWidget {
onPressed: () { onPressed: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
useRootNavigator: true,
builder: builder:
(context) => Column( (context) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
ListTile( ListTile(
title: Text('createChatRoom').tr(), title: const Text('createChatRoom').tr(),
leading: const Icon(Symbols.add), leading: const Icon(Symbols.add),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
context.pushRoute(NewChatRoute()).then((value) { context.push('/chat/new').then((value) {
if (value != null) { if (value != null) {
ref.invalidate(chatroomsJoinedProvider); ref.invalidate(chatroomsJoinedProvider);
} }
@ -327,7 +327,7 @@ class ChatListScreen extends HookConsumerWidget {
}, },
), ),
ListTile( ListTile(
title: Text('createDirectMessage').tr(), title: const Text('createDirectMessage').tr(),
leading: const Icon(Symbols.person), leading: const Icon(Symbols.person),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
@ -400,16 +400,7 @@ class ChatListScreen extends HookConsumerWidget {
room: item, room: item,
isDirect: item.type == 1, isDirect: item.type == 1,
onTap: () { onTap: () {
if (context.router.topRoute.name == context.push('/chat/${item.id}');
ChatRoomRoute.name) {
context.router.replace(
ChatRoomRoute(id: item.id),
);
} else {
context.router.push(
ChatRoomRoute(id: item.id),
);
}
}, },
); );
}, },
@ -443,33 +434,45 @@ class ChatListScreen extends HookConsumerWidget {
@riverpod @riverpod
Future<SnChatRoom?> chatroom(Ref ref, String? identifier) async { Future<SnChatRoom?> chatroom(Ref ref, String? identifier) async {
if (identifier == null) return null; if (identifier == null) return null;
final client = ref.watch(apiClientProvider); try {
final resp = await client.get('/chat/$identifier'); final client = ref.watch(apiClientProvider);
return SnChatRoom.fromJson(resp.data); 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 @riverpod
Future<SnChatMember?> chatroomIdentity(Ref ref, String? identifier) async { Future<SnChatMember?> chatroomIdentity(Ref ref, String? identifier) async {
if (identifier == null) return null; if (identifier == null) return null;
final client = ref.watch(apiClientProvider); try {
final resp = await client.get('/chat/$identifier/members/me'); final client = ref.watch(apiClientProvider);
return SnChatMember.fromJson(resp.data); 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 { class NewChatScreen extends StatelessWidget {
const NewChatScreen({super.key}); const NewChatScreen({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return EditChatScreen(); return const EditChatScreen();
} }
} }
@RoutePage()
class EditChatScreen extends HookConsumerWidget { class EditChatScreen extends HookConsumerWidget {
final String? id; final String? id;
const EditChatScreen({super.key, @PathParam("id") this.id}); const EditChatScreen({super.key, this.id});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -481,6 +484,8 @@ class EditChatScreen extends HookConsumerWidget {
final descriptionController = useTextEditingController(); final descriptionController = useTextEditingController();
final picture = useState<SnCloudFile?>(null); final picture = useState<SnCloudFile?>(null);
final background = useState<SnCloudFile?>(null); final background = useState<SnCloudFile?>(null);
final isPublic = useState(true);
final isCommunity = useState(false);
final chat = ref.watch(chatroomProvider(id)); final chat = ref.watch(chatroomProvider(id));
@ -493,12 +498,14 @@ class EditChatScreen extends HookConsumerWidget {
descriptionController.text = chat.value!.description ?? ''; descriptionController.text = chat.value!.description ?? '';
picture.value = chat.value!.picture; picture.value = chat.value!.picture;
background.value = chat.value!.background; background.value = chat.value!.background;
isPublic.value = chat.value!.isPublic;
isCommunity.value = chat.value!.isCommunity;
currentRealm.value = joinedRealms.value?.firstWhereOrNull( currentRealm.value = joinedRealms.value?.firstWhereOrNull(
(realm) => realm.id == chat.value!.realmId, (realm) => realm.id == chat.value!.realmId,
); );
} }
return; return;
}, [chat]); }, [chat, joinedRealms]);
void setPicture(String position) async { void setPicture(String position) async {
showLoadingModal(context); showLoadingModal(context);
@ -516,9 +523,9 @@ class EditChatScreen extends HookConsumerWidget {
image: result, image: result,
allowedAspectRatios: [ allowedAspectRatios: [
if (position == 'background') if (position == 'background')
CropAspectRatio(height: 7, width: 16) const CropAspectRatio(height: 7, width: 16)
else else
CropAspectRatio(height: 1, width: 1), const CropAspectRatio(height: 1, width: 1),
], ],
); );
if (result == null) { if (result == null) {
@ -575,11 +582,13 @@ class EditChatScreen extends HookConsumerWidget {
'background_id': background.value?.id, 'background_id': background.value?.id,
'picture_id': picture.value?.id, 'picture_id': picture.value?.id,
'realm_id': currentRealm.value?.id, 'realm_id': currentRealm.value?.id,
'is_public': isPublic.value,
'is_community': isCommunity.value,
}, },
options: Options(method: id == null ? 'POST' : 'PATCH'), options: Options(method: id == null ? 'POST' : 'PATCH'),
); );
if (context.mounted) { if (context.mounted) {
context.maybePop(SnChatRoom.fromJson(resp.data)); context.pop(SnChatRoom.fromJson(resp.data));
} }
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
@ -667,6 +676,19 @@ class EditChatScreen extends HookConsumerWidget {
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
CheckboxListTile(
title: const Text('isPublic').tr(),
subtitle: const Text('isPublicHint').tr(),
value: isPublic.value,
onChanged: (value) => isPublic.value = value ?? false,
),
CheckboxListTile(
title: const Text('isCommunity').tr(),
subtitle: const Text('isCommunityHint').tr(),
value: isCommunity.value,
onChanged: (value) => isCommunity.value = value ?? false,
),
const SizedBox(height: 16),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: TextButton.icon( child: TextButton.icon(
@ -767,7 +789,7 @@ class _ChatInvitesSheet extends HookConsumerWidget {
), ),
if (invite.chatRoom!.type == 1) if (invite.chatRoom!.type == 1)
Badge( Badge(
label: Text('directMessage').tr(), label: const Text('directMessage').tr(),
backgroundColor: backgroundColor:
Theme.of(context).colorScheme.primary, Theme.of(context).colorScheme.primary,
textColor: textColor:

View File

@ -25,7 +25,7 @@ final chatroomsJoinedProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef ChatroomsJoinedRef = AutoDisposeFutureProviderRef<List<SnChatRoom>>; typedef ChatroomsJoinedRef = AutoDisposeFutureProviderRef<List<SnChatRoom>>;
String _$chatroomHash() => r'dce3c0fc407f178bb7c306a08b9fa545795a9205'; String _$chatroomHash() => r'8dac7aaac50932e6dd213039102d43c1cf5f1d4e';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@ -164,7 +164,7 @@ class _ChatroomProviderElement
String? get identifier => (origin as ChatroomProvider).identifier; String? get identifier => (origin as ChatroomProvider).identifier;
} }
String _$chatroomIdentityHash() => r'4c349ea4265df7b0498cf26c82dbaabe3d868727'; String _$chatroomIdentityHash() => r'ad6ad09b6fc4cf7c4abe146ea97f8e364a3d4fd0';
/// See also [chatroomIdentity]. /// See also [chatroomIdentity].
@ProviderFor(chatroomIdentity) @ProviderFor(chatroomIdentity)

View File

@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@ -18,7 +18,6 @@ import 'package:island/pods/config.dart';
import 'package:island/pods/database.dart'; import 'package:island/pods/database.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/route.gr.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
@ -288,15 +287,76 @@ class MessagesNotifier extends _$MessagesNotifier {
} }
} }
@RoutePage()
class ChatRoomScreen extends HookConsumerWidget { class ChatRoomScreen extends HookConsumerWidget {
final String id; final String id;
const ChatRoomScreen({super.key, @PathParam("id") required this.id}); const ChatRoomScreen({super.key, required this.id});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final chatRoom = ref.watch(chatroomProvider(id)); final chatRoom = ref.watch(chatroomProvider(id));
final chatIdentity = ref.watch(chatroomIdentityProvider(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 messages = ref.watch(messagesNotifierProvider(id));
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier); final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
final ws = ref.watch(websocketProvider); final ws = ref.watch(websocketProvider);
@ -431,6 +491,28 @@ class ChatRoomScreen extends HookConsumerWidget {
return () => subscription.cancel(); return () => subscription.cancel();
}, [ws, chatRoom]); }, [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 { Future<void> pickPhotoMedia() async {
final result = await ref final result = await ref
.watch(imagePickerProvider) .watch(imagePickerProvider)
@ -605,7 +687,7 @@ class ChatRoomScreen extends HookConsumerWidget {
IconButton( IconButton(
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
onPressed: () { onPressed: () {
context.router.push(ChatDetailRoute(id: id)); context.push('/chat/$id/detail');
}, },
), ),
const Gap(8), const Gap(8),

View File

@ -1,14 +1,13 @@
import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/chat.dart'; import 'package:island/models/chat.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/chat/chat.dart'; import 'package:island/screens/chat/chat.dart';
import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.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.freezed.dart';
part 'room_detail.g.dart'; part 'room_detail.g.dart';
@RoutePage()
class ChatDetailScreen extends HookConsumerWidget { class ChatDetailScreen extends HookConsumerWidget {
final String id; final String id;
const ChatDetailScreen({super.key, @PathParam("id") required this.id}); const ChatDetailScreen({super.key, required this.id});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -391,7 +389,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
if ((chatIdentity.value?.role ?? 0) >= 50) if ((chatIdentity.value?.role ?? 0) >= 50)
PopupMenuItem( PopupMenuItem(
onTap: () { onTap: () {
context.router.replace(EditChatRoute(id: id)); context.pushReplacement('/chat/$id/edit');
}, },
child: Row( child: Row(
children: [ children: [
@ -426,9 +424,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
client.delete('/chat/$id'); client.delete('/chat/$id');
ref.invalidate(chatroomsJoinedProvider); ref.invalidate(chatroomsJoinedProvider);
if (context.mounted) { if (context.mounted) {
context.router.popUntil( context.pop();
(route) => route is ChatRoomRoute,
);
} }
} }
}); });
@ -461,9 +457,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
client.delete('/chat/$id/members/me'); client.delete('/chat/$id/members/me');
ref.invalidate(chatroomsJoinedProvider); ref.invalidate(chatroomsJoinedProvider);
if (context.mounted) { if (context.mounted) {
context.router.popUntil( context.pop();
(route) => route is ChatRoomRoute,
);
} }
} }
}); });
@ -590,8 +584,8 @@ class _ChatMemberListSheet extends HookConsumerWidget {
Future<void> invitePerson() async { Future<void> invitePerson() async {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
isScrollControlled: true,
context: context, context: context,
useRootNavigator: true,
builder: (context) => const AccountPickerSheet(), builder: (context) => const AccountPickerSheet(),
); );
if (result == null) return; if (result == null) return;

View File

@ -1,20 +1,25 @@
import 'package:auto_route/auto_route.dart'; import 'package:dio/dio.dart';
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/creators/publishers.dart'; import 'package:island/screens/creators/publishers.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.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:styled_widget/styled_widget.dart';
part 'hub.g.dart'; part 'hub.g.dart';
@ -27,9 +32,68 @@ Future<SnPublisherStats?> publisherStats(Ref ref, String? uname) async {
return SnPublisherStats.fromJson(resp.data); 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<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 { class CreatorHubShellScreen extends StatelessWidget {
const CreatorHubShellScreen({super.key}); final Widget child;
const CreatorHubShellScreen({super.key, required this.child});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -39,15 +103,14 @@ class CreatorHubShellScreen extends StatelessWidget {
children: [ children: [
SizedBox(width: 360, child: const CreatorHubScreen(isAside: true)), SizedBox(width: 360, child: const CreatorHubScreen(isAside: true)),
const VerticalDivider(width: 1), const VerticalDivider(width: 1),
Expanded(child: AutoRouter()), Expanded(child: child),
], ],
); );
} }
return AutoRouter(); return child;
} }
} }
@RoutePage()
class CreatorHubScreen extends HookConsumerWidget { class CreatorHubScreen extends HookConsumerWidget {
final bool isAside; final bool isAside;
const CreatorHubScreen({super.key, this.isAside = false}); const CreatorHubScreen({super.key, this.isAside = false});
@ -60,21 +123,20 @@ class CreatorHubScreen extends HookConsumerWidget {
} }
final publishers = ref.watch(publishersManagedProvider); final publishers = ref.watch(publishersManagedProvider);
final publisherInvites = ref.watch(publisherInvitesProvider);
final currentPublisher = useState<SnPublisher?>( final currentPublisher = useState<SnPublisher?>(
publishers.value?.firstOrNull, publishers.value?.firstOrNull,
); );
void updatePublisher() { void updatePublisher() {
context.router context.push('/creators/${currentPublisher.value!.name}/edit').then((
.push(EditPublisherRoute(name: currentPublisher.value!.name)) value,
.then((value) async { ) async {
if (value == null) return; if (value == null) return;
final data = await ref.refresh(publishersManagedProvider.future); final data = await ref.refresh(publishersManagedProvider.future);
currentPublisher.value = currentPublisher.value =
data data.where((e) => e.id == currentPublisher.value!.id).firstOrNull;
.where((e) => e.id == currentPublisher.value!.id) });
.firstOrNull;
});
} }
void deletePublisher() { void deletePublisher() {
@ -128,6 +190,30 @@ class CreatorHubScreen extends HookConsumerWidget {
leading: !isWide ? const PageBackButton() : null, leading: !isWide ? const PageBackButton() : null,
title: Text('creatorHub').tr(), title: Text('creatorHub').tr(),
actions: [ 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( DropdownButtonHideUnderline(
child: DropdownButton2<SnPublisher>( child: DropdownButton2<SnPublisher>(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
@ -205,7 +291,7 @@ class CreatorHubScreen extends HookConsumerWidget {
...(publishers.value?.map( ...(publishers.value?.map(
(publisher) => ListTile( (publisher) => ListTile(
leading: ProfilePictureWidget( leading: ProfilePictureWidget(
fileId: publisher.picture?.id, file: publisher.picture,
), ),
title: Text(publisher.nick), title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'), subtitle: Text('@${publisher.name}'),
@ -223,7 +309,7 @@ class CreatorHubScreen extends HookConsumerWidget {
subtitle: Text('createPublisherHint').tr(), subtitle: Text('createPublisherHint').tr(),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
context.router.push(NewPublisherRoute()).then(( context.push('/creators/publishers/new').then((
value, value,
) { ) {
if (value != null) { if (value != null) {
@ -249,10 +335,8 @@ class CreatorHubScreen extends HookConsumerWidget {
horizontal: 24, horizontal: 24,
), ),
onTap: () { onTap: () {
context.router.push( context.push(
StickersRoute( '/creators/${currentPublisher.value!.name}/stickers',
pubName: currentPublisher.value!.name,
),
); );
}, },
), ),
@ -265,10 +349,34 @@ class CreatorHubScreen extends HookConsumerWidget {
horizontal: 24, horizontal: 24,
), ),
onTap: () { onTap: () {
context.router.push( context.push(
CreatorPostListRoute( '/creators/${currentPublisher.value!.name}/posts',
pubName: currentPublisher.value!.name, );
), },
),
ListTile(
minTileHeight: 48,
title: Text('members').plural(
ref
.watch(publisherMemberStateProvider(
currentPublisher.value!.name,
))
.total,
),
trailing: Icon(Symbols.chevron_right),
leading: const Icon(Symbols.group),
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => _PublisherMemberListSheet(
publisherUname:
currentPublisher.value!.name,
),
); );
}, },
), ),
@ -399,3 +507,483 @@ class _PublisherStatsWidget extends StatelessWidget {
); );
} }
} }
class PublisherMemberState {
final List<SnPublisherMember> members;
final bool isLoading;
final int total;
final String? error;
const PublisherMemberState({
required this.members,
required this.isLoading,
required this.total,
this.error,
});
PublisherMemberState copyWith({
List<SnPublisherMember>? members,
bool? isLoading,
int? total,
String? error,
}) {
return PublisherMemberState(
members: members ?? this.members,
isLoading: isLoading ?? this.isLoading,
total: total ?? this.total,
error: error ?? this.error,
);
}
}
final publisherMemberStateProvider = StateNotifierProvider.family<
PublisherMemberNotifier, PublisherMemberState, String>(
(ref, publisherUname) {
final apiClient = ref.watch(apiClientProvider);
return PublisherMemberNotifier(apiClient, publisherUname);
},
);
class PublisherMemberNotifier extends StateNotifier<PublisherMemberState> {
final String publisherUname;
final Dio _apiClient;
PublisherMemberNotifier(this._apiClient, this.publisherUname)
: super(const PublisherMemberState(
members: [],
isLoading: false,
total: 0,
));
Future<void> loadMore({int offset = 0, int take = 20}) async {
if (state.isLoading) return;
if (state.total > 0 && state.members.length >= state.total) return;
state = state.copyWith(isLoading: true, error: null);
try {
final response = await _apiClient.get(
'/publishers/$publisherUname/members',
queryParameters: {'offset': offset, 'take': take},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final members =
data.map((e) => SnPublisherMember.fromJson(e)).toList();
state = state.copyWith(
members: [...state.members, ...members],
total: total,
isLoading: false,
);
} catch (e) {
state = state.copyWith(error: e.toString(), isLoading: false);
}
}
void reset() {
state = const PublisherMemberState(members: [], isLoading: false, total: 0);
}
}
class _PublisherMemberListSheet extends HookConsumerWidget {
final String publisherUname;
const _PublisherMemberListSheet({required this.publisherUname});
@override
Widget build(BuildContext context, WidgetRef ref) {
final publisherIdentity = ref.watch(
publisherIdentityProvider(publisherUname),
);
final memberListProvider =
publisherMemberListNotifierProvider(publisherUname);
final memberState = ref.watch(publisherMemberStateProvider(publisherUname));
final memberNotifier = ref.read(
publisherMemberStateProvider(publisherUname).notifier,
);
useEffect(() {
Future(() {
memberNotifier.loadMore();
});
return null;
}, []);
Future<void> invitePerson() async {
final result = await showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => const AccountPickerSheet(),
);
if (result == null) return;
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.post(
'/publishers/$publisherUname/invites',
data: {'related_user_id': result.id, 'role': 0},
);
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
}
}
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Column(
children: [
Padding(
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
child: Row(
children: [
Text(
'members'.plural(memberState.total),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
),
const Spacer(),
IconButton(
icon: const Icon(Symbols.person_add),
onPressed: invitePerson,
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
),
IconButton(
icon: const Icon(Symbols.refresh),
onPressed: () {
memberNotifier.reset();
memberNotifier.loadMore();
ref.invalidate(memberListProvider);
},
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
),
],
),
),
const Divider(height: 1),
Expanded(
child: PagingHelperView(
provider: memberListProvider,
futureRefreshable: memberListProvider.future,
notifierRefreshable: memberListProvider.notifier,
contentBuilder: (data, widgetCount, endItemView) {
return ListView.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == data.items.length) {
return endItemView;
}
final member = data.items[index];
return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id,
),
title: Row(
spacing: 6,
children: [
Flexible(child: Text(member.account!.nick)),
if (member.joinedAt == null)
const Icon(Symbols.pending_actions, size: 20),
],
),
subtitle: Row(
children: [
Text(
member.role >= 100
? 'permissionOwner'
: member.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
Text('·').bold().padding(horizontal: 6),
Expanded(child: Text("@${member.account!.name}")),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if ((publisherIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.edit),
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => _PublisherMemberRoleSheet(
publisherUname: publisherUname,
member: member,
),
).then((value) {
if (value != null) {
ref.invalidate(memberListProvider);
}
});
},
),
if ((publisherIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
showConfirmAlert(
'removePublisherMemberHint'.tr(),
'removePublisherMember'.tr(),
).then((confirm) async {
if (confirm != true) return;
try {
final apiClient = ref.watch(
apiClientProvider,
);
await apiClient.delete(
'/publishers/$publisherUname/members/${member.accountId}',
);
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
}
});
},
),
],
),
);
},
);
},
),
),
],
),
);
}
}
class _PublisherMemberRoleSheet extends HookConsumerWidget {
final String publisherUname;
final SnPublisherMember member;
const _PublisherMemberRoleSheet({
required this.publisherUname,
required this.member,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final roleController = useTextEditingController(
text: member.role.toString(),
);
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.only(
top: 16,
left: 20,
right: 16,
bottom: 12,
),
child: Row(
children: [
Text(
'memberRoleEdit'.tr(args: [member.account!.name]),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
),
const Spacer(),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(
minimumSize: const Size(36, 36),
),
),
],
),
),
const Divider(height: 1),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Autocomplete<int>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const [100, 50, 0];
}
final int? value = int.tryParse(textEditingValue.text);
if (value == null) return const [100, 50, 0];
return [100, 50, 0].where(
(option) =>
option.toString().contains(textEditingValue.text),
);
},
onSelected: (int selection) {
roleController.text = selection.toString();
},
fieldViewBuilder: (
context,
controller,
focusNode,
onFieldSubmitted,
) {
return TextField(
controller: controller,
focusNode: focusNode,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'memberRole'.tr(),
helperText: 'memberRoleHint'.tr(),
),
onTapOutside: (event) => focusNode.unfocus(),
);
},
),
const Gap(16),
FilledButton.icon(
onPressed: () async {
try {
final newRole = int.parse(roleController.text);
if (newRole < 0 || newRole > 100) {
throw 'Role must be between 0 and 100';
}
final apiClient = ref.read(apiClientProvider);
await apiClient.patch(
'/publishers/$publisherUname/members/${member.accountId}/role',
data: newRole,
);
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
}
},
icon: const Icon(Symbols.save),
label: const Text('saveChanges').tr(),
),
],
).padding(vertical: 16, horizontal: 24),
],
),
),
);
}
}
class _PublisherInviteSheet extends HookConsumerWidget {
const _PublisherInviteSheet();
@override
Widget build(BuildContext context, WidgetRef ref) {
final invites = ref.watch(publisherInvitesProvider);
Future<void> acceptInvite(SnPublisherMember invite) async {
try {
final client = ref.read(apiClientProvider);
await client.post(
'/publishers/invites/${invite.publisher!.name}/accept',
);
ref.invalidate(publisherInvitesProvider);
ref.invalidate(publishersManagedProvider);
} catch (err) {
showErrorAlert(err);
}
}
Future<void> declineInvite(SnPublisherMember invite) async {
try {
final client = ref.read(apiClientProvider);
await client.post(
'/publishers/invites/${invite.publisher!.name}/decline',
);
ref.invalidate(publisherInvitesProvider);
} catch (err) {
showErrorAlert(err);
}
}
return SheetScaffold(
titleText: 'invites'.tr(),
actions: [
IconButton(
icon: const Icon(Symbols.refresh),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
onPressed: () {
ref.invalidate(publisherInvitesProvider);
},
),
],
child: invites.when(
data:
(items) =>
items.isEmpty
? Center(
child:
Text(
'invitesEmpty',
textAlign: TextAlign.center,
).tr(),
)
: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final invite = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: invite.publisher!.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(invite.publisher!.nick),
subtitle:
Text(
invite.role >= 100
? 'permissionOwner'
: invite.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite),
),
],
),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(publisherInvitesProvider),
),
),
);
}
}

View File

@ -149,5 +149,300 @@ class _PublisherStatsProviderElement
String? get uname => (origin as PublisherStatsProvider).uname; 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 _$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: 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 // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,6 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/widgets/app_scaffold.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:island/widgets/post/post_list.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@RoutePage()
class CreatorPostListScreen extends HookConsumerWidget { class CreatorPostListScreen extends HookConsumerWidget {
final String pubName; final String pubName;
const CreatorPostListScreen({ const CreatorPostListScreen({super.key, required this.pubName});
super.key,
@PathParam('name') required this.pubName,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -34,7 +30,7 @@ class CreatorPostListScreen extends HookConsumerWidget {
subtitle: Text('Create a regular post'), subtitle: Text('Create a regular post'),
onTap: () async { onTap: () async {
Navigator.pop(context); Navigator.pop(context);
final result = await context.router.pushPath( final result = await context.push(
'/posts/compose?type=0', '/posts/compose?type=0',
); );
if (result == true) { if (result == true) {
@ -48,7 +44,7 @@ class CreatorPostListScreen extends HookConsumerWidget {
subtitle: Text('Create a detailed article'), subtitle: Text('Create a detailed article'),
onTap: () async { onTap: () async {
Navigator.pop(context); Navigator.pop(context);
final result = await context.router.pushPath( final result = await context.push(
'/posts/compose?type=1', '/posts/compose?type=1',
); );
if (result == true) { if (result == true) {

View File

@ -1,14 +1,14 @@
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:croppy/croppy.dart' hide cropImage; import 'package:croppy/croppy.dart' hide cropImage;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.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/models/realm.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
@ -44,7 +44,6 @@ Future<SnPublisher?> publisher(Ref ref, String? identifier) async {
return SnPublisher.fromJson(resp.data); return SnPublisher.fromJson(resp.data);
} }
@RoutePage()
class NewPublisherScreen extends StatelessWidget { class NewPublisherScreen extends StatelessWidget {
const NewPublisherScreen({super.key}); const NewPublisherScreen({super.key});
@ -54,10 +53,9 @@ class NewPublisherScreen extends StatelessWidget {
} }
} }
@RoutePage()
class EditPublisherScreen extends HookConsumerWidget { class EditPublisherScreen extends HookConsumerWidget {
final String? name; final String? name;
const EditPublisherScreen({super.key, @PathParam('id') this.name}); const EditPublisherScreen({super.key, this.name});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -177,7 +175,7 @@ class EditPublisherScreen extends HookConsumerWidget {
options: Options(method: name == null ? 'POST' : 'PATCH'), options: Options(method: name == null ? 'POST' : 'PATCH'),
); );
if (context.mounted) { if (context.mounted) {
context.maybePop(SnPublisher.fromJson(resp.data)); context.pop(SnPublisher.fromJson(resp.data));
} }
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);

View File

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.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:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/sticker.dart'; import 'package:island/models/sticker.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/creators/stickers/stickers.dart'; import 'package:island/screens/creators/stickers/stickers.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
@ -34,14 +33,13 @@ Future<List<SnSticker>> stickerPackContent(Ref ref, String packId) async {
.toList(); .toList();
} }
@RoutePage()
class StickerPackDetailScreen extends HookConsumerWidget { class StickerPackDetailScreen extends HookConsumerWidget {
final String id; final String id;
final String pubName; final String pubName;
const StickerPackDetailScreen({ const StickerPackDetailScreen({
super.key, super.key,
@PathParam('name') required this.pubName, required this.pubName,
@PathParam('packId') required this.id, required this.id,
}); });
@override @override
@ -76,7 +74,7 @@ class StickerPackDetailScreen extends HookConsumerWidget {
IconButton( IconButton(
icon: const Icon(Symbols.add_circle), icon: const Icon(Symbols.add_circle),
onPressed: () { onPressed: () {
AutoRouter.of(context).push(NewStickersRoute(packId: id)).then(( context.push('/creators/stickers/$id/new').then((
value, value,
) { ) {
if (value != null) { if (value != null) {
@ -175,12 +173,9 @@ class StickerPackDetailScreen extends HookConsumerWidget {
title: 'edit'.tr(), title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit), image: MenuImage.icon(Symbols.edit),
callback: () { callback: () {
context.router context
.push( .push(
EditStickersRoute( '/creators/stickers/$id/edit/${sticker.id}',
packId: id,
id: sticker.id,
),
) )
.then((value) { .then((value) {
if (value != null) { if (value != null) {
@ -264,8 +259,8 @@ class _StickerPackActionMenu extends HookConsumerWidget {
(context) => [ (context) => [
PopupMenuItem( PopupMenuItem(
onTap: () { onTap: () {
context.router.push( context.push(
EditStickerPacksRoute(pubName: pubName, packId: packId), '/creators/$pubName/stickers/$packId/edit',
); );
}, },
child: Row( child: Row(
@ -299,7 +294,7 @@ class _StickerPackActionMenu extends HookConsumerWidget {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
client.delete('/stickers/$packId'); client.delete('/stickers/$packId');
ref.invalidate(stickerPacksNotifierProvider); 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); return SnSticker.fromJson(resp.data);
} }
@RoutePage()
class NewStickersScreen extends StatelessWidget { class NewStickersScreen extends StatelessWidget {
final String packId; final String packId;
const NewStickersScreen({ const NewStickersScreen({super.key, required this.packId});
super.key,
@PathParam('packId') required this.packId,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -345,15 +336,10 @@ class NewStickersScreen extends StatelessWidget {
} }
} }
@RoutePage()
class EditStickersScreen extends HookConsumerWidget { class EditStickersScreen extends HookConsumerWidget {
final String packId; final String packId;
final String? id; final String? id;
const EditStickersScreen({ const EditStickersScreen({super.key, required this.packId, required this.id});
super.key,
@PathParam("packId") required this.packId,
@PathParam("id") required this.id,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {

View File

@ -1,13 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/sticker.dart'; import 'package:island/models/sticker.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.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'; part 'stickers.g.dart';
@RoutePage()
class StickersScreen extends HookConsumerWidget { class StickersScreen extends HookConsumerWidget {
final String pubName; final String pubName;
const StickersScreen({super.key, @PathParam("name") required this.pubName}); const StickersScreen({super.key, required this.pubName});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -30,7 +28,7 @@ class StickersScreen extends HookConsumerWidget {
actions: [ actions: [
IconButton( IconButton(
onPressed: () { onPressed: () {
context.router.push(NewStickerPacksRoute(pubName: pubName)).then(( context.push('/creators/stickers/new?pubName=pubName').then((
value, value,
) { ) {
if (value != null) { if (value != null) {
@ -73,8 +71,8 @@ class SliverStickerPacksList extends HookConsumerWidget {
subtitle: Text(sticker.description), subtitle: Text(sticker.description),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
context.router.push( context.push(
StickerPackDetailRoute(pubName: pubName, id: sticker.id), '/creators/$pubName/stickers/${sticker.id}',
); );
}, },
); );
@ -137,13 +135,9 @@ Future<SnStickerPack?> stickerPack(Ref ref, String? packId) async {
return SnStickerPack.fromJson(resp.data); return SnStickerPack.fromJson(resp.data);
} }
@RoutePage()
class NewStickerPacksScreen extends HookConsumerWidget { class NewStickerPacksScreen extends HookConsumerWidget {
final String pubName; final String pubName;
const NewStickerPacksScreen({ const NewStickerPacksScreen({super.key, required this.pubName});
super.key,
@PathParam("name") required this.pubName,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -151,15 +145,10 @@ class NewStickerPacksScreen extends HookConsumerWidget {
} }
} }
@RoutePage()
class EditStickerPacksScreen extends HookConsumerWidget { class EditStickerPacksScreen extends HookConsumerWidget {
final String pubName; final String pubName;
final String? packId; final String? packId;
const EditStickerPacksScreen({ const EditStickerPacksScreen({super.key, required this.pubName, this.packId});
super.key,
@PathParam("name") required this.pubName,
@PathParam("packId") this.packId,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -200,7 +189,7 @@ class EditStickerPacksScreen extends HookConsumerWidget {
), ),
); );
if (!context.mounted) return; if (!context.mounted) return;
context.router.maybePop(SnStickerPack.fromJson(resp.data)); context.pop(SnStickerPack.fromJson(resp.data));
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
} finally { } finally {

View 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;
}
});
},
),
),
),
],
),
);
}
}

View File

@ -1,34 +1,37 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart'; import 'package:island/models/activity.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/widgets/check_in.dart'; import 'package:island/widgets/check_in.dart';
import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/tour/tour.dart';
import 'package:island/screens/tabs.dart'; import 'package:island/screens/tabs.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/pods/network.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:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
part 'explore.g.dart'; part 'explore.g.dart';
@RoutePage() class ExploreShellScreen extends HookConsumerWidget {
class ExploreShellScreen extends ConsumerWidget { final Widget child;
const ExploreShellScreen({super.key}); const ExploreShellScreen({super.key, required this.child});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isWide = isWideScreen(context); final isWide = MediaQuery.of(context).size.width > 640;
if (isWide) { if (isWide) {
return AppBackground( return AppBackground(
@ -37,17 +40,16 @@ class ExploreShellScreen extends ConsumerWidget {
children: [ children: [
Flexible(flex: 2, child: ExploreScreen(isAside: true)), Flexible(flex: 2, child: ExploreScreen(isAside: true)),
VerticalDivider(width: 1), 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 { class ExploreScreen extends HookConsumerWidget {
final bool isAside; final bool isAside;
const ExploreScreen({super.key, this.isAside = false}); const ExploreScreen({super.key, this.isAside = false});
@ -85,65 +87,64 @@ class ExploreScreen extends HookConsumerWidget {
activityListNotifierProvider(currentFilter.value).notifier, activityListNotifierProvider(currentFilter.value).notifier,
); );
return TourTriggerWidget( return AppScaffold(
child: AppScaffold( extendBody: false, // Prevent conflicts with tabs navigation
extendBody: false, // Prevent conflicts with tabs navigation appBar: AppBar(
appBar: AppBar( toolbarHeight: 0,
toolbarHeight: 0, bottom: TabBar(
bottom: TabBar(
controller: tabController,
tabs: [
Tab(
child: Text(
'explore'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'exploreFilterSubscriptions'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'exploreFilterFriends'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
heroTag: Key("explore-page-fab"),
onPressed: () {
context.router.push(PostComposeRoute()).then((value) {
if (value != null) {
activitiesNotifier.forceRefresh();
}
});
},
child: const Icon(Symbols.edit),
),
floatingActionButtonLocation: TabbedFabLocation(context),
body: TabBarView(
controller: tabController, controller: tabController,
children: [ tabs: [
_buildActivityList(ref, null), Tab(
_buildActivityList(ref, 'subscriptions'), child: Text(
_buildActivityList(ref, 'friends'), 'explore'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'exploreFilterSubscriptions'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'exploreFilterFriends'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
], ],
), ),
), ),
floatingActionButton: FloatingActionButton(
heroTag: Key("explore-page-fab"),
onPressed: () {
context.push('/posts/compose').then((value) {
if (value != null) {
activitiesNotifier.forceRefresh();
}
});
},
child: const Icon(Symbols.edit),
),
floatingActionButtonLocation: TabbedFabLocation(context),
body: TabBarView(
controller: tabController,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildActivityList(ref, null),
_buildActivityList(ref, 'subscriptions'),
_buildActivityList(ref, 'friends'),
],
),
); );
} }
@ -173,6 +174,64 @@ 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',
_ => '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,
);
default:
return Placeholder();
}
},
),
).padding(bottom: 4),
],
);
}
}
class _ActivityListView extends HookConsumerWidget { class _ActivityListView extends HookConsumerWidget {
final CursorPagingData<SnActivity> data; final CursorPagingData<SnActivity> data;
final int widgetCount; final int widgetCount;
@ -216,10 +275,14 @@ class _ActivityListView extends HookConsumerWidget {
itemWidget = PostItem( itemWidget = PostItem(
backgroundColor: backgroundColor:
isWideScreen(context) ? Colors.transparent : null, isWideScreen(context) ? Colors.transparent : null,
item: SnPost.fromJson(item.data), item: SnPost.fromJson(item.data!),
padding: padding:
isReply isReply
? EdgeInsets.only(left: 16, right: 16, bottom: 16) ? const EdgeInsets.only(
left: 16,
right: 16,
bottom: 16,
)
: null, : null,
onRefresh: (_) { onRefresh: (_) {
activitiesNotifier.forceRefresh(); activitiesNotifier.forceRefresh();
@ -247,6 +310,9 @@ class _ActivityListView extends HookConsumerWidget {
); );
} }
break; break;
case 'discovery':
itemWidget = _DiscoveryActivityItem(data: item.data!);
break;
default: default:
itemWidget = const Placeholder(); itemWidget = const Placeholder();
} }
@ -276,6 +342,7 @@ class ActivityListNotifier extends _$ActivityListNotifier
if (cursor != null) 'cursor': cursor, if (cursor != null) 'cursor': cursor,
'take': take, 'take': take,
if (filter != null) 'filter': filter, if (filter != null) 'filter': filter,
if (kDebugMode) 'debugInclude': 'realms,publishers',
}; };
final response = await client.get( final response = await client.get(

View File

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

View File

@ -1,22 +1,21 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart'; import 'package:island/models/user.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.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/app_scaffold.dart';
import 'package:island/widgets/content/markdown.dart'; import 'package:island/widgets/content/markdown.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.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'; part 'notification.g.dart';
@ -107,7 +106,6 @@ class NotificationListNotifier extends _$NotificationListNotifier
} }
} }
@RoutePage()
class NotificationScreen extends HookConsumerWidget { class NotificationScreen extends HookConsumerWidget {
const NotificationScreen({super.key}); const NotificationScreen({super.key});
@ -181,36 +179,17 @@ class NotificationScreen extends HookConsumerWidget {
), ),
), ),
onTap: () { onTap: () {
if (notification.meta['link'] is String) { if (notification.meta['action_uri'] != null) {
final href = notification.meta['link']; var uri = notification.meta['action_uri'] as String;
final uri = Uri.tryParse(href); if (uri.startsWith('/')) {
if (uri == null) { // In-app routes
showSnackBar( rootNavigatorKey.currentContext?.push(
'brokenLink'.tr(args: []), notification.meta['action_uri'],
action: SnackBarAction(
label: 'copyToClipboard'.tr(),
onPressed: () {
Clipboard.setData(ClipboardData(text: href));
clearSnackBar(context);
},
),
); );
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);
}
});
} }
}, },
); );

View File

@ -1,4 +1,3 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -40,10 +39,9 @@ sealed class PostComposeInitialState with _$PostComposeInitialState {
_$PostComposeInitialStateFromJson(json); _$PostComposeInitialStateFromJson(json);
} }
@RoutePage()
class PostEditScreen extends HookConsumerWidget { class PostEditScreen extends HookConsumerWidget {
final String id; final String id;
const PostEditScreen({super.key, @PathParam('id') required this.id}); const PostEditScreen({super.key, required this.id});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -66,7 +64,6 @@ class PostEditScreen extends HookConsumerWidget {
} }
} }
@RoutePage()
class PostComposeScreen extends HookConsumerWidget { class PostComposeScreen extends HookConsumerWidget {
final SnPost? originalPost; final SnPost? originalPost;
final SnPost? repliedPost; final SnPost? repliedPost;
@ -78,7 +75,7 @@ class PostComposeScreen extends HookConsumerWidget {
this.originalPost, this.originalPost,
this.repliedPost, this.repliedPost,
this.forwardedPost, this.forwardedPost,
@QueryParam('type') this.type, this.type,
this.initialState, this.initialState,
}); });
@ -106,15 +103,32 @@ class PostComposeScreen extends HookConsumerWidget {
originalPost: originalPost, originalPost: originalPost,
forwardedPost: effectiveForwardedPost, forwardedPost: effectiveForwardedPost,
repliedPost: effectiveRepliedPost, repliedPost: effectiveRepliedPost,
postType: 0, // Regular post type
), ),
[originalPost, effectiveForwardedPost, effectiveRepliedPost], [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 // Start auto-save when component mounts
useEffect(() { useEffect(() {
if (originalPost == null) { if (originalPost == null) {
// Only auto-save for new posts, not edits // Only auto-save for new posts, not edits
state.startAutoSave(ref, postType: 0); state.startAutoSave(ref);
} }
return () => state.stopAutoSave(); return () => state.stopAutoSave();
}, [state]); }, [state]);
@ -153,13 +167,18 @@ class PostComposeScreen extends HookConsumerWidget {
final drafts = ref.read(composeStorageNotifierProvider); final drafts = ref.read(composeStorageNotifierProvider);
if (drafts.isNotEmpty) { if (drafts.isNotEmpty) {
final mostRecentDraft = drafts.values.reduce( 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 // 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.titleController.text = mostRecentDraft.title ?? '';
state.descriptionController.text = mostRecentDraft.description ?? ''; state.descriptionController.text =
mostRecentDraft.description ?? '';
state.contentController.text = mostRecentDraft.content ?? ''; state.contentController.text = mostRecentDraft.content ?? '';
state.visibility.value = mostRecentDraft.visibility; state.visibility.value = mostRecentDraft.visibility;
} }
@ -187,6 +206,8 @@ class PostComposeScreen extends HookConsumerWidget {
titleController: state.titleController, titleController: state.titleController,
descriptionController: state.descriptionController, descriptionController: state.descriptionController,
visibility: state.visibility, visibility: state.visibility,
tagsController: state.tagsController,
categoriesController: state.categoriesController,
onVisibilityChanged: () { onVisibilityChanged: () {
// Trigger rebuild if needed // Trigger rebuild if needed
}, },
@ -206,22 +227,18 @@ class PostComposeScreen extends HookConsumerWidget {
), ),
itemCount: state.attachments.value.length, itemCount: state.attachments.value.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
return ValueListenableBuilder<Map<int, double>>( final progressMap = state.attachmentProgress.value;
valueListenable: state.attachmentProgress, return AttachmentPreview(
builder: (context, progressMap, _) { item: state.attachments.value[idx],
return AttachmentPreview( progress: progressMap[idx],
item: state.attachments.value[idx], onRequestUpload:
progress: progressMap[idx], () => ComposeLogic.uploadAttachment(ref, state, idx),
onRequestUpload: onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx),
() => ComposeLogic.uploadAttachment(ref, state, idx), onMove: (delta) {
onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx), state.attachments.value = ComposeLogic.moveAttachment(
onMove: (delta) { state.attachments.value,
state.attachments.value = ComposeLogic.moveAttachment( idx,
state.attachments.value, delta,
idx,
delta,
);
},
); );
}, },
); );
@ -235,26 +252,24 @@ class PostComposeScreen extends HookConsumerWidget {
for (var idx = 0; idx < state.attachments.value.length; idx++) for (var idx = 0; idx < state.attachments.value.length; idx++)
Container( Container(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
child: ValueListenableBuilder<Map<int, double>>( child: () {
valueListenable: state.attachmentProgress, final progressMap = state.attachmentProgress.value;
builder: (context, progressMap, _) { return AttachmentPreview(
return AttachmentPreview( item: state.attachments.value[idx],
item: state.attachments.value[idx], progress: progressMap[idx],
progress: progressMap[idx], onRequestUpload:
onRequestUpload: () => ComposeLogic.uploadAttachment(ref, state, idx),
() => ComposeLogic.uploadAttachment(ref, state, idx), onDelete:
onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx),
() => ComposeLogic.deleteAttachment(ref, state, idx), onMove: (delta) {
onMove: (delta) { state.attachments.value = ComposeLogic.moveAttachment(
state.attachments.value = ComposeLogic.moveAttachment( state.attachments.value,
state.attachments.value, idx,
idx, delta,
delta, );
); },
}, );
); }(),
},
),
), ),
], ],
); );
@ -290,7 +305,8 @@ class PostComposeScreen extends HookConsumerWidget {
state.titleController.text = draft.title ?? ''; state.titleController.text = draft.title ?? '';
state.descriptionController.text = state.descriptionController.text =
draft.description ?? ''; draft.description ?? '';
state.contentController.text = draft.content ?? ''; state.contentController.text =
draft.content ?? '';
state.visibility.value = draft.visibility; state.visibility.value = draft.visibility;
} }
}, },
@ -309,39 +325,31 @@ class PostComposeScreen extends HookConsumerWidget {
onPressed: showSettingsSheet, onPressed: showSettingsSheet,
tooltip: 'postSettings'.tr(), tooltip: 'postSettings'.tr(),
), ),
ValueListenableBuilder<bool>( IconButton(
valueListenable: state.submitting, onPressed:
builder: (context, submitting, _) { state.submitting.value
return IconButton( ? null
onPressed: : () => ComposeLogic.performAction(
submitting ref,
? null state,
: () => ComposeLogic.performAction( context,
ref, originalPost: originalPost,
state, repliedPost: repliedPost,
context, forwardedPost: forwardedPost,
originalPost: originalPost, ),
repliedPost: repliedPost, icon:
forwardedPost: forwardedPost, state.submitting.value
postType: 0, // Regular post type ? SizedBox(
), width: 28,
icon: height: 28,
submitting child: const CircularProgressIndicator(
? SizedBox( color: Colors.white,
width: 28, strokeWidth: 2.5,
height: 28, ),
child: const CircularProgressIndicator( ).center()
color: Colors.white, : Icon(
strokeWidth: 2.5, originalPost != null ? Symbols.edit : Symbols.upload,
), ),
).center()
: Icon(
originalPost != null
? Symbols.edit
: Symbols.upload,
),
);
},
), ),
const Gap(8), const Gap(8),
], ],
@ -402,7 +410,6 @@ class PostComposeScreen extends HookConsumerWidget {
originalPost: originalPost, originalPost: originalPost,
repliedPost: repliedPost, repliedPost: repliedPost,
forwardedPost: forwardedPost, forwardedPost: forwardedPost,
postType: 0, // Regular post type
), ),
child: TextField( child: TextField(
controller: state.contentController, controller: state.contentController,
@ -423,22 +430,17 @@ class PostComposeScreen extends HookConsumerWidget {
const Gap(8), const Gap(8),
// Attachments preview // Attachments preview
ValueListenableBuilder<List<UniversalFile>>( if (state.attachments.value.isNotEmpty)
valueListenable: state.attachments, LayoutBuilder(
builder: (context, attachments, _) { builder: (context, constraints) {
if (attachments.isEmpty) { final isWide = isWideScreen(context);
return const SizedBox.shrink(); return isWide
} ? buildWideAttachmentGrid()
return LayoutBuilder( : buildNarrowAttachmentList();
builder: (context, constraints) { },
final isWide = isWideScreen(context); )
return isWide else
? buildWideAttachmentGrid() const SizedBox.shrink(),
: buildNarrowAttachmentList();
},
);
},
),
], ],
), ),
), ),

View File

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -26,10 +25,9 @@ import 'package:island/widgets/post/draft_manager.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@RoutePage()
class ArticleEditScreen extends HookConsumerWidget { class ArticleEditScreen extends HookConsumerWidget {
final String id; final String id;
const ArticleEditScreen({super.key, @PathParam('id') required this.id}); const ArticleEditScreen({super.key, required this.id});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -50,7 +48,6 @@ class ArticleEditScreen extends HookConsumerWidget {
} }
} }
@RoutePage()
class ArticleComposeScreen extends HookConsumerWidget { class ArticleComposeScreen extends HookConsumerWidget {
final SnPost? originalPost; final SnPost? originalPost;
@ -63,7 +60,10 @@ class ArticleComposeScreen extends HookConsumerWidget {
final publishers = ref.watch(publishersManagedProvider); final publishers = ref.watch(publishersManagedProvider);
final state = useMemoized( final state = useMemoized(
() => ComposeLogic.createState(originalPost: originalPost), () => ComposeLogic.createState(
originalPost: originalPost,
postType: 1, // Article type
),
[originalPost], [originalPost],
); );
@ -73,7 +73,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
if (originalPost == null) { if (originalPost == null) {
// Only auto-save for new articles, not edits // Only auto-save for new articles, not edits
autoSaveTimer = Timer.periodic(const Duration(seconds: 3), (_) { autoSaveTimer = Timer.periodic(const Duration(seconds: 3), (_) {
ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1); ComposeLogic.saveDraftWithoutUpload(ref, state);
}); });
} }
return () { return () {
@ -81,7 +81,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
state.stopAutoSave(); state.stopAutoSave();
// Save final draft before disposing // Save final draft before disposing
if (originalPost == null) { if (originalPost == null) {
ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1); ComposeLogic.saveDraftWithoutUpload(ref, state);
} }
ComposeLogic.dispose(state); ComposeLogic.dispose(state);
autoSaveTimer?.cancel(); autoSaveTimer?.cancel();
@ -143,6 +143,8 @@ class ArticleComposeScreen extends HookConsumerWidget {
titleController: state.titleController, titleController: state.titleController,
descriptionController: state.descriptionController, descriptionController: state.descriptionController,
visibility: state.visibility, visibility: state.visibility,
tagsController: state.tagsController,
categoriesController: state.categoriesController,
onVisibilityChanged: () { onVisibilityChanged: () {
// Trigger rebuild if needed // Trigger rebuild if needed
}, },
@ -363,7 +365,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
return PopScope( return PopScope(
onPopInvoked: (_) { onPopInvoked: (_) {
if (originalPost == null) { if (originalPost == null) {
ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1); ComposeLogic.saveDraftWithoutUpload(ref, state);
} }
}, },
child: AppScaffold( child: AppScaffold(
@ -411,7 +413,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
), ),
IconButton( IconButton(
icon: const Icon(Symbols.save), icon: const Icon(Symbols.save),
onPressed: () => ComposeLogic.saveDraft(ref, state, postType: 1), onPressed: () => ComposeLogic.saveDraft(ref, state),
tooltip: 'saveDraft'.tr(), tooltip: 'saveDraft'.tr(),
), ),
IconButton( IconButton(
@ -438,7 +440,6 @@ class ArticleComposeScreen extends HookConsumerWidget {
state, state,
context, context,
originalPost: originalPost, originalPost: originalPost,
postType: 1, // Article type
), ),
icon: icon:
submitting submitting
@ -531,18 +532,17 @@ class ArticleComposeScreen extends HookConsumerWidget {
if (isPaste && isModifierPressed) { if (isPaste && isModifierPressed) {
ComposeLogic.handlePaste(state); ComposeLogic.handlePaste(state);
} else if (isSave && isModifierPressed) { } 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) { } else if (isSubmit && isModifierPressed && !state.submitting.value) {
ComposeLogic.performAction( ComposeLogic.performAction(
ref, ref,
state, state,
context, context,
originalPost: originalPost, originalPost: originalPost,
postType: 1, // Article type
); );
} }
} }
// Helper method to save article draft // Helper method to save article draft
} }

View File

@ -1,4 +1,3 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -22,10 +21,9 @@ Future<SnPost?> post(Ref ref, String id) async {
return SnPost.fromJson(resp.data); return SnPost.fromJson(resp.data);
} }
@RoutePage()
class PostDetailScreen extends HookConsumerWidget { class PostDetailScreen extends HookConsumerWidget {
final String id; final String id;
const PostDetailScreen({super.key, @PathParam('id') required this.id}); const PostDetailScreen({super.key, required this.id});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {

View File

@ -1,11 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/user.dart'; import 'package:island/models/user.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
@ -54,26 +55,26 @@ Future<SnSubscriptionStatus> publisherSubscriptionStatus(
@riverpod @riverpod
Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async { Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async {
final publisher = await ref.watch(publisherProvider(pubName).future); try {
if (publisher.background == null) return null; final publisher = await ref.watch(publisherProvider(pubName).future);
final palette = await PaletteGenerator.fromImageProvider( if (publisher.background == null) return null;
CloudImageWidget.provider( final palette = await PaletteGenerator.fromImageProvider(
fileId: publisher.background!.id, CloudImageWidget.provider(
serverUrl: ref.watch(serverUrlProvider), fileId: publisher.background!.id,
), serverUrl: ref.watch(serverUrlProvider),
); ),
final dominantColor = palette.dominantColor?.color; );
if (dominantColor == null) return null; final dominantColor = palette.dominantColor?.color;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
} catch (_) {
return null;
}
} }
@RoutePage()
class PublisherProfileScreen extends HookConsumerWidget { class PublisherProfileScreen extends HookConsumerWidget {
final String name; final String name;
const PublisherProfileScreen({ const PublisherProfileScreen({super.key, required this.name});
super.key,
@PathParam("name") required this.name,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -186,7 +187,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
), ),
onTap: () { onTap: () {
Navigator.pop(context, true); Navigator.pop(context, true);
context.router.pushPath('/account/${data.name}'); context.push('/account/${data.name}');
}, },
), ),
Expanded( Expanded(

View File

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

View File

@ -1,13 +1,17 @@
import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:island/screens/chat/chat.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:island/models/chat.dart';
import 'package:island/services/color.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart'; import 'package:island/pods/config.dart';
import 'package:island/screens/realm/realms.dart'; import 'package:island/screens/realm/realms.dart';
import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
@ -21,23 +25,53 @@ import 'package:styled_widget/styled_widget.dart';
part 'detail.g.dart'; part 'detail.g.dart';
@riverpod @riverpod
Future<SnRealmMember?> realmIdentity(Ref ref, String realmSlug) async { Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async {
final apiClient = ref.watch(apiClientProvider); final realm = await ref.watch(realmProvider(realmSlug).future);
final response = await apiClient.get('/realms/$realmSlug/members/me'); if (realm?.background == null) return null;
return SnRealmMember.fromJson(response.data); final palette = await PaletteGenerator.fromImageProvider(
CloudImageWidget.provider(
fileId: realm!.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
final dominantColor = palette.dominantColor?.color;
if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
}
@riverpod
Future<SnRealmMember?> realmIdentity(Ref ref, String realmSlug) async {
try {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get('/realms/$realmSlug/members/me');
return SnRealmMember.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<List<SnChatRoom>> realmChatRooms(Ref ref, String realmSlug) async {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get('/realms/$realmSlug/chat');
return (response.data as List).map((e) => SnChatRoom.fromJson(e)).toList();
} }
@RoutePage()
class RealmDetailScreen extends HookConsumerWidget { class RealmDetailScreen extends HookConsumerWidget {
final String slug; final String slug;
const RealmDetailScreen({super.key, @PathParam("slug") required this.slug});
const RealmDetailScreen({super.key, required this.slug});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final realmState = ref.watch(realmProvider(slug)); final realmState = ref.watch(realmProvider(slug));
final appbarColor = ref.watch(realmAppbarForegroundColorProvider(slug));
const iconShadow = Shadow( final iconShadow = Shadow(
color: Colors.black54, color: appbarColor.value?.invert ?? Colors.black54,
blurRadius: 5.0, blurRadius: 5.0,
offset: Offset(1.0, 1.0), offset: Offset(1.0, 1.0),
); );
@ -52,7 +86,11 @@ class RealmDetailScreen extends HookConsumerWidget {
SliverAppBar( SliverAppBar(
expandedHeight: 180, expandedHeight: 180,
pinned: true, pinned: true,
leading: PageBackButton(shadows: [iconShadow]), foregroundColor: appbarColor.value,
leading: PageBackButton(
color: appbarColor.value,
shadows: [iconShadow],
),
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: background:
realm!.background?.id != null realm!.background?.id != null
@ -64,14 +102,16 @@ class RealmDetailScreen extends HookConsumerWidget {
title: Text( title: Text(
realm.name, realm.name,
style: TextStyle( style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor, color:
appbarColor.value ??
Theme.of(context).appBarTheme.foregroundColor,
shadows: [iconShadow], shadows: [iconShadow],
), ),
), ),
), ),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.people, shadows: [iconShadow]), icon: Icon(Icons.people, shadows: [iconShadow]),
onPressed: () { onPressed: () {
showModalBottomSheet( showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
@ -87,18 +127,97 @@ class RealmDetailScreen extends HookConsumerWidget {
], ],
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: ref
padding: const EdgeInsets.all(16.0), .watch(realmIdentityProvider(slug))
child: Column( .when(
crossAxisAlignment: CrossAxisAlignment.start, loading: () => const SizedBox.shrink(),
children: [ error: (_, _) => const SizedBox.shrink(),
Text( data:
realm.description, (identity) => Column(
style: const TextStyle(fontSize: 16), crossAxisAlignment: CrossAxisAlignment.stretch,
), children: [
], ExpansionTile(
), title: const Text('description').tr(),
), initiallyExpanded: identity == null,
tilePadding: EdgeInsets.symmetric(
horizontal: 20,
),
expandedCrossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
Text(
realm.description,
style: const TextStyle(fontSize: 16),
).padding(
horizontal: 20,
bottom: 16,
top: 8,
),
],
),
if (identity == null && realm.isCommunity)
FilledButton.tonalIcon(
onPressed: () async {
try {
final apiClient = ref.read(
apiClientProvider,
);
await apiClient.post(
'/realms/$slug/members/me',
);
ref.invalidate(
realmIdentityProvider(slug),
);
ref.invalidate(realmsJoinedProvider);
showSnackBar('realmJoinSuccess'.tr());
} catch (err) {
showErrorAlert(err);
}
},
icon: const Icon(Symbols.add),
label: const Text('realmJoin').tr(),
).padding(horizontal: 16, vertical: 16)
else
const SizedBox.shrink(),
],
),
),
),
const SliverToBoxAdapter(child: Divider(height: 1)),
Consumer(
builder: (context, ref, _) {
final chatRooms = ref.watch(realmChatRoomsProvider(slug));
return chatRooms.when(
loading:
() => const SliverToBoxAdapter(
child: Center(child: CircularProgressIndicator()),
),
error:
(error, _) => SliverToBoxAdapter(
child: Center(child: Text('Error: $error')),
),
data: (rooms) {
if (rooms.isEmpty) {
return const SliverToBoxAdapter(
child: SizedBox.shrink(),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate((
context,
index,
) {
return ChatRoomListTile(
room: rooms[index],
onTap: () {
context.push('/chat/${rooms[index].id}');
},
);
}, childCount: rooms.length),
);
},
);
},
), ),
], ],
), ),
@ -115,8 +234,8 @@ class _RealmActionMenu extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final realmIdentityAsync = ref.watch(realmIdentityProvider(realmSlug)); final realmIdentity = ref.watch(realmIdentityProvider(realmSlug));
final isModerator = realmIdentityAsync.when( final isModerator = realmIdentity.when(
data: (identity) => (identity?.role ?? 0) >= 50, data: (identity) => (identity?.role ?? 0) >= 50,
loading: () => false, loading: () => false,
error: (_, _) => false, error: (_, _) => false,
@ -129,7 +248,7 @@ class _RealmActionMenu extends HookConsumerWidget {
if (isModerator) if (isModerator)
PopupMenuItem( PopupMenuItem(
onTap: () { onTap: () {
context.router.replace(EditRealmRoute(slug: realmSlug)); context.pushReplacement('/realms/$realmSlug/edit');
}, },
child: Row( child: Row(
children: [ children: [
@ -142,7 +261,7 @@ class _RealmActionMenu extends HookConsumerWidget {
], ],
), ),
), ),
realmIdentityAsync.when( realmIdentity.when(
data: data:
(identity) => (identity) =>
(identity?.role ?? 0) >= 100 (identity?.role ?? 0) >= 100
@ -167,7 +286,7 @@ class _RealmActionMenu extends HookConsumerWidget {
client.delete('/realms/$realmSlug'); client.delete('/realms/$realmSlug');
ref.invalidate(realmsJoinedProvider); ref.invalidate(realmsJoinedProvider);
if (context.mounted) { if (context.mounted) {
context.router.maybePop(true); context.pop(true);
} }
} }
}); });
@ -201,7 +320,7 @@ class _RealmActionMenu extends HookConsumerWidget {
); );
ref.invalidate(realmsJoinedProvider); ref.invalidate(realmsJoinedProvider);
if (context.mounted) { if (context.mounted) {
context.router.maybePop(true); context.pop(true);
} }
} }
}); });
@ -239,7 +358,7 @@ class _RealmActionMenu extends HookConsumerWidget {
client.delete('/realms/$realmSlug/members/me'); client.delete('/realms/$realmSlug/members/me');
ref.invalidate(realmsJoinedProvider); ref.invalidate(realmsJoinedProvider);
if (context.mounted) { if (context.mounted) {
context.router.maybePop(true); context.pop(true);
} }
} }
}); });

View File

@ -6,7 +6,8 @@ part of 'detail.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$realmIdentityHash() => r'eac6e829b5b46bcfadbf201ab6f918d78c894b9f'; String _$realmAppbarForegroundColorHash() =>
r'14b5563d861996ea182d0d2db7aa5c2bb3bbaf48';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@ -29,6 +30,133 @@ class _SystemHash {
} }
} }
/// See also [realmAppbarForegroundColor].
@ProviderFor(realmAppbarForegroundColor)
const realmAppbarForegroundColorProvider = RealmAppbarForegroundColorFamily();
/// See also [realmAppbarForegroundColor].
class RealmAppbarForegroundColorFamily extends Family<AsyncValue<Color?>> {
/// See also [realmAppbarForegroundColor].
const RealmAppbarForegroundColorFamily();
/// See also [realmAppbarForegroundColor].
RealmAppbarForegroundColorProvider call(String realmSlug) {
return RealmAppbarForegroundColorProvider(realmSlug);
}
@override
RealmAppbarForegroundColorProvider getProviderOverride(
covariant RealmAppbarForegroundColorProvider provider,
) {
return call(provider.realmSlug);
}
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'realmAppbarForegroundColorProvider';
}
/// See also [realmAppbarForegroundColor].
class RealmAppbarForegroundColorProvider
extends AutoDisposeFutureProvider<Color?> {
/// See also [realmAppbarForegroundColor].
RealmAppbarForegroundColorProvider(String realmSlug)
: this._internal(
(ref) => realmAppbarForegroundColor(
ref as RealmAppbarForegroundColorRef,
realmSlug,
),
from: realmAppbarForegroundColorProvider,
name: r'realmAppbarForegroundColorProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$realmAppbarForegroundColorHash,
dependencies: RealmAppbarForegroundColorFamily._dependencies,
allTransitiveDependencies:
RealmAppbarForegroundColorFamily._allTransitiveDependencies,
realmSlug: realmSlug,
);
RealmAppbarForegroundColorProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.realmSlug,
}) : super.internal();
final String realmSlug;
@override
Override overrideWith(
FutureOr<Color?> Function(RealmAppbarForegroundColorRef provider) create,
) {
return ProviderOverride(
origin: this,
override: RealmAppbarForegroundColorProvider._internal(
(ref) => create(ref as RealmAppbarForegroundColorRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
realmSlug: realmSlug,
),
);
}
@override
AutoDisposeFutureProviderElement<Color?> createElement() {
return _RealmAppbarForegroundColorProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is RealmAppbarForegroundColorProvider &&
other.realmSlug == realmSlug;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, realmSlug.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin RealmAppbarForegroundColorRef on AutoDisposeFutureProviderRef<Color?> {
/// The parameter `realmSlug` of this provider.
String get realmSlug;
}
class _RealmAppbarForegroundColorProviderElement
extends AutoDisposeFutureProviderElement<Color?>
with RealmAppbarForegroundColorRef {
_RealmAppbarForegroundColorProviderElement(super.provider);
@override
String get realmSlug =>
(origin as RealmAppbarForegroundColorProvider).realmSlug;
}
String _$realmIdentityHash() => r'308d43eef8a6145c762d27bdf7e12e27149524db';
/// See also [realmIdentity]. /// See also [realmIdentity].
@ProviderFor(realmIdentity) @ProviderFor(realmIdentity)
const realmIdentityProvider = RealmIdentityFamily(); const realmIdentityProvider = RealmIdentityFamily();
@ -148,6 +276,128 @@ class _RealmIdentityProviderElement
String get realmSlug => (origin as RealmIdentityProvider).realmSlug; String get realmSlug => (origin as RealmIdentityProvider).realmSlug;
} }
String _$realmChatRoomsHash() => r'8207c1e6f0922323967f208efeed027e943039cc';
/// See also [realmChatRooms].
@ProviderFor(realmChatRooms)
const realmChatRoomsProvider = RealmChatRoomsFamily();
/// See also [realmChatRooms].
class RealmChatRoomsFamily extends Family<AsyncValue<List<SnChatRoom>>> {
/// See also [realmChatRooms].
const RealmChatRoomsFamily();
/// See also [realmChatRooms].
RealmChatRoomsProvider call(String realmSlug) {
return RealmChatRoomsProvider(realmSlug);
}
@override
RealmChatRoomsProvider getProviderOverride(
covariant RealmChatRoomsProvider provider,
) {
return call(provider.realmSlug);
}
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'realmChatRoomsProvider';
}
/// See also [realmChatRooms].
class RealmChatRoomsProvider
extends AutoDisposeFutureProvider<List<SnChatRoom>> {
/// See also [realmChatRooms].
RealmChatRoomsProvider(String realmSlug)
: this._internal(
(ref) => realmChatRooms(ref as RealmChatRoomsRef, realmSlug),
from: realmChatRoomsProvider,
name: r'realmChatRoomsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$realmChatRoomsHash,
dependencies: RealmChatRoomsFamily._dependencies,
allTransitiveDependencies:
RealmChatRoomsFamily._allTransitiveDependencies,
realmSlug: realmSlug,
);
RealmChatRoomsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.realmSlug,
}) : super.internal();
final String realmSlug;
@override
Override overrideWith(
FutureOr<List<SnChatRoom>> Function(RealmChatRoomsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: RealmChatRoomsProvider._internal(
(ref) => create(ref as RealmChatRoomsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
realmSlug: realmSlug,
),
);
}
@override
AutoDisposeFutureProviderElement<List<SnChatRoom>> createElement() {
return _RealmChatRoomsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is RealmChatRoomsProvider && other.realmSlug == realmSlug;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, realmSlug.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin RealmChatRoomsRef on AutoDisposeFutureProviderRef<List<SnChatRoom>> {
/// The parameter `realmSlug` of this provider.
String get realmSlug;
}
class _RealmChatRoomsProviderElement
extends AutoDisposeFutureProviderElement<List<SnChatRoom>>
with RealmChatRoomsRef {
_RealmChatRoomsProviderElement(super.provider);
@override
String get realmSlug => (origin as RealmChatRoomsProvider).realmSlug;
}
String _$realmMemberListNotifierHash() => String _$realmMemberListNotifierHash() =>
r'b2e3eefc62a597f45df9470b2058fdda62f8853f'; r'b2e3eefc62a597f45df9470b2058fdda62f8853f';

View File

@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:croppy/croppy.dart' show CropAspectRatio; import 'package:croppy/croppy.dart' show CropAspectRatio;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -11,7 +11,6 @@ import 'package:island/models/file.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
@ -33,7 +32,6 @@ Future<List<SnRealm>> realmsJoined(Ref ref) async {
return resp.data.map((e) => SnRealm.fromJson(e)).cast<SnRealm>().toList(); return resp.data.map((e) => SnRealm.fromJson(e)).cast<SnRealm>().toList();
} }
@RoutePage()
class RealmListScreen extends HookConsumerWidget { class RealmListScreen extends HookConsumerWidget {
const RealmListScreen({super.key}); const RealmListScreen({super.key});
@ -48,6 +46,10 @@ class RealmListScreen extends HookConsumerWidget {
appBar: AppBar( appBar: AppBar(
title: const Text('realms').tr(), title: const Text('realms').tr(),
actions: [ actions: [
IconButton(
icon: const Icon(Symbols.travel_explore),
onPressed: () => context.push('/discovery/realms'),
),
IconButton( IconButton(
icon: Badge( icon: Badge(
label: Text( label: Text(
@ -68,7 +70,7 @@ class RealmListScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: (_) => _RealmInviteSheet(), builder: (_) => const _RealmInviteSheet(),
); );
}, },
), ),
@ -76,10 +78,10 @@ class RealmListScreen extends HookConsumerWidget {
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: Key("realms-page-fab"), heroTag: const Key("realms-page-fab"),
child: const Icon(Symbols.add), child: const Icon(Symbols.add),
onPressed: () { onPressed: () {
context.router.push(NewRealmRoute()).then((value) { context.push('/realms/new').then((value) {
if (value != null) { if (value != null) {
ref.invalidate(realmsJoinedProvider); ref.invalidate(realmsJoinedProvider);
} }
@ -106,11 +108,9 @@ class RealmListScreen extends HookConsumerWidget {
title: Text(value[item].name), title: Text(value[item].name),
subtitle: Text(value[item].description), subtitle: Text(value[item].description),
onTap: () { onTap: () {
context.router.push( context.push('/realms/${value[item].slug}');
RealmDetailRoute(slug: value[item].slug),
);
}, },
contentPadding: EdgeInsets.only( contentPadding: const EdgeInsets.only(
left: 16, left: 16,
right: 14, right: 14,
top: 8, top: 8,
@ -143,7 +143,6 @@ Future<SnRealm?> realm(Ref ref, String? identifier) async {
return SnRealm.fromJson(resp.data); return SnRealm.fromJson(resp.data);
} }
@RoutePage()
class NewRealmScreen extends StatelessWidget { class NewRealmScreen extends StatelessWidget {
const NewRealmScreen({super.key}); const NewRealmScreen({super.key});
@ -153,10 +152,9 @@ class NewRealmScreen extends StatelessWidget {
} }
} }
@RoutePage()
class EditRealmScreen extends HookConsumerWidget { class EditRealmScreen extends HookConsumerWidget {
final String? slug; final String? slug;
const EditRealmScreen({super.key, @PathParam('slug') this.slug}); const EditRealmScreen({super.key, this.slug});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -164,6 +162,8 @@ class EditRealmScreen extends HookConsumerWidget {
final picture = useState<SnCloudFile?>(null); final picture = useState<SnCloudFile?>(null);
final background = useState<SnCloudFile?>(null); final background = useState<SnCloudFile?>(null);
final isPublic = useState(true);
final isCommunity = useState(false);
final slugController = useTextEditingController(); final slugController = useTextEditingController();
final nameController = useTextEditingController(); final nameController = useTextEditingController();
@ -180,6 +180,8 @@ class EditRealmScreen extends HookConsumerWidget {
slugController.text = realm.value!.slug; slugController.text = realm.value!.slug;
nameController.text = realm.value!.name; nameController.text = realm.value!.name;
descriptionController.text = realm.value!.description; descriptionController.text = realm.value!.description;
isPublic.value = realm.value!.isPublic;
isCommunity.value = realm.value!.isCommunity;
} }
return null; return null;
}, [realm]); }, [realm]);
@ -200,9 +202,9 @@ class EditRealmScreen extends HookConsumerWidget {
image: result, image: result,
allowedAspectRatios: [ allowedAspectRatios: [
if (position == 'background') if (position == 'background')
CropAspectRatio(height: 7, width: 16) const CropAspectRatio(height: 7, width: 16)
else else
CropAspectRatio(height: 1, width: 1), const CropAspectRatio(height: 1, width: 1),
], ],
); );
if (result == null) { if (result == null) {
@ -258,11 +260,13 @@ class EditRealmScreen extends HookConsumerWidget {
'description': descriptionController.text, 'description': descriptionController.text,
'background_id': background.value?.id, 'background_id': background.value?.id,
'picture_id': picture.value?.id, 'picture_id': picture.value?.id,
'is_public': isPublic.value,
'is_community': isCommunity.value,
}, },
options: Options(method: slug == null ? 'POST' : 'PATCH'), options: Options(method: slug == null ? 'POST' : 'PATCH'),
); );
if (context.mounted) { if (context.mounted) {
context.maybePop(SnRealm.fromJson(resp.data)); context.pop(SnRealm.fromJson(resp.data));
} }
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
@ -290,9 +294,9 @@ class EditRealmScreen extends HookConsumerWidget {
child: child:
background.value != null background.value != null
? CloudFileWidget( ? CloudFileWidget(
item: background.value!, item: background.value!,
fit: BoxFit.cover, fit: BoxFit.cover,
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
onTap: () { onTap: () {
@ -320,7 +324,6 @@ class EditRealmScreen extends HookConsumerWidget {
key: formKey, key: formKey,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: [ children: [
TextFormField( TextFormField(
controller: slugController, controller: slugController,
@ -331,12 +334,14 @@ class EditRealmScreen extends HookConsumerWidget {
onTapOutside: onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
const SizedBox(height: 16),
TextFormField( TextFormField(
controller: nameController, controller: nameController,
decoration: InputDecoration(labelText: 'name'.tr()), decoration: InputDecoration(labelText: 'name'.tr()),
onTapOutside: onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
const SizedBox(height: 16),
TextFormField( TextFormField(
controller: descriptionController, controller: descriptionController,
decoration: InputDecoration(labelText: 'description'.tr()), decoration: InputDecoration(labelText: 'description'.tr()),
@ -345,6 +350,20 @@ class EditRealmScreen extends HookConsumerWidget {
onTapOutside: onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
const SizedBox(height: 16),
CheckboxListTile(
title: const Text('isPublic').tr(),
subtitle: const Text('isPublicHint').tr(),
value: isPublic.value,
onChanged: (value) => isPublic.value = value ?? false,
),
CheckboxListTile(
title: const Text('isCommunity').tr(),
subtitle: const Text('isCommunityHint').tr(),
value: isCommunity.value,
onChanged: (value) => isCommunity.value = value ?? false,
),
const SizedBox(height: 16),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: TextButton.icon( child: TextButton.icon(
@ -416,47 +435,47 @@ class _RealmInviteSheet extends HookConsumerWidget {
(items) => (items) =>
items.isEmpty items.isEmpty
? Center( ? Center(
child: child:
Text( Text(
'invitesEmpty', 'invitesEmpty',
textAlign: TextAlign.center, textAlign: TextAlign.center,
).tr(), ).tr(),
) )
: ListView.builder( : ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemCount: items.length, itemCount: items.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final invite = items[index]; final invite = items[index];
return ListTile( return ListTile(
leading: ProfilePictureWidget( leading: ProfilePictureWidget(
fileId: invite.realm!.picture?.id, fileId: invite.realm!.picture?.id,
fallbackIcon: Symbols.group, fallbackIcon: Symbols.group,
), ),
title: Text(invite.realm!.name), title: Text(invite.realm!.name),
subtitle: subtitle:
Text( Text(
invite.role >= 100 invite.role >= 100
? 'permissionOwner' ? 'permissionOwner'
: invite.role >= 50 : invite.role >= 50
? 'permissionModerator' ? 'permissionModerator'
: 'permissionMember', : 'permissionMember',
).tr(), ).tr(),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( IconButton(
icon: const Icon(Symbols.check), icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite), onPressed: () => acceptInvite(invite),
), ),
IconButton( IconButton(
icon: const Icon(Symbols.close), icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite), onPressed: () => declineInvite(invite),
), ),
], ],
), ),
); );
}, },
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error:
(error, _) => ResponseErrorWidget( (error, _) => ResponseErrorWidget(
@ -466,4 +485,4 @@ class _RealmInviteSheet extends HookConsumerWidget {
), ),
); );
} }
} }

View File

@ -1,11 +1,11 @@
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -21,7 +21,6 @@ import 'package:path_provider/path_provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
@RoutePage()
class SettingsScreen extends HookConsumerWidget { class SettingsScreen extends HookConsumerWidget {
const SettingsScreen({super.key}); const SettingsScreen({super.key});
@ -590,7 +589,7 @@ class SettingsScreen extends HookConsumerWidget {
if (isDesktop && if (isDesktop &&
event is KeyDownEvent && event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) { event.logicalKey == LogicalKeyboardKey.escape) {
context.router.pop(); context.pop();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
return KeyEventResult.ignored; return KeyEventResult.ignored;

View File

@ -1,10 +1,9 @@
import 'dart:developer';
import 'dart:ui'; import 'dart:ui';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/route.gr.dart'; import 'package:go_router/go_router.dart';
import 'package:island/screens/notification.dart'; import 'package:island/screens/notification.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/navigation/conditional_bottom_nav.dart'; import 'package:island/widgets/navigation/conditional_bottom_nav.dart';
@ -12,42 +11,22 @@ import 'package:material_symbols_icons/symbols.dart';
final currentRouteProvider = StateProvider<String?>((ref) => null); final currentRouteProvider = StateProvider<String?>((ref) => null);
class TabNavigationObserver extends AutoRouterObserver {
Function(String?) onChange;
TabNavigationObserver({required this.onChange});
@override
void didPush(Route route, Route? previousRoute) {
log('pushed ${previousRoute?.settings.name} -> ${route.settings.name}');
if (route is DialogRoute) return;
final name = route.settings.name;
if (name == null) return;
if (name.contains('Shell')) return;
Future(() {
onChange(name);
});
}
@override
void didPop(Route route, Route? previousRoute) {
log('popped ${route.settings.name} -> ${previousRoute?.settings.name}');
if (previousRoute is DialogRoute) return;
final name = previousRoute?.settings.name;
if (name == null) return;
if (name.contains('Shell')) return;
Future(() {
onChange(name);
});
}
}
@RoutePage()
class TabsScreen extends HookConsumerWidget { class TabsScreen extends HookConsumerWidget {
const TabsScreen({super.key}); final Widget? child;
const TabsScreen({super.key, this.child});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final useHorizontalLayout = isWideScreen(context); // final useHorizontalLayout = isWideScreen(context);
final currentLocation = GoRouterState.of(context).uri.toString();
// Update the current route provider whenever the location changes
useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(currentRouteProvider.notifier).state = currentLocation;
});
return null;
}, [currentLocation]);
final notificationUnreadCount = ref.watch( final notificationUnreadCount = ref.watch(
notificationUnreadCountNotifierProvider, notificationUnreadCountNotifierProvider,
@ -73,85 +52,89 @@ class TabsScreen extends HookConsumerWidget {
), ),
]; ];
final routes = <PageRouteInfo>[ final routes = [
ExploreShellRoute(), '/',
ChatShellRoute(), '/chat',
RealmListRoute(), '/realms',
AccountShellRoute(), '/account',
]; ];
return AutoTabsRouter.tabBar( int getCurrentIndex() {
routes: routes, if (currentLocation.startsWith('/chat')) return 1;
scrollDirection: useHorizontalLayout ? Axis.vertical : Axis.horizontal, if (currentLocation.startsWith('/realms')) return 2;
physics: const NeverScrollableScrollPhysics(), if (currentLocation.startsWith('/account')) return 3;
builder: (context, child, _) { return 0; // Default to explore
final tabsRouter = AutoTabsRouter.of(context); }
if (isWideScreen(context)) { void onDestinationSelected(int index) {
return Row( context.go(routes[index]);
children: [ }
NavigationRail(
destinations:
destinations
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
),
)
.toList(),
selectedIndex: tabsRouter.activeIndex,
onDestinationSelected: tabsRouter.setActiveIndex,
),
const VerticalDivider(width: 1),
Expanded(child: child),
],
);
}
return Stack( final currentIndex = getCurrentIndex();
children: [
Positioned.fill(child: child), if (isWideScreen(context)) {
Positioned( return Row(
left: 0, children: [
right: 0, NavigationRail(
bottom: 0, destinations:
child: ConditionalBottomNav( destinations
child: ClipRRect( .map(
child: BackdropFilter( (e) => NavigationRailDestination(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), icon: e.icon,
child: Container( label: Text(e.label),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surface.withOpacity(0.8),
), ),
child: MediaQuery.removePadding( )
context: context, .toList(),
removeTop: true, selectedIndex: currentIndex,
child: NavigationBar( onDestinationSelected: onDestinationSelected,
backgroundColor: Colors.transparent, ),
shadowColor: Colors.transparent, const VerticalDivider(width: 1),
overlayColor: const WidgetStatePropertyAll( Expanded(child: child ?? const SizedBox.shrink()),
Colors.transparent, ],
), );
surfaceTintColor: Colors.transparent, }
height: 56,
labelBehavior: return Stack(
NavigationDestinationLabelBehavior.alwaysHide, children: [
selectedIndex: tabsRouter.activeIndex, Positioned.fill(child: child ?? const SizedBox.shrink()),
onDestinationSelected: tabsRouter.setActiveIndex, Positioned(
destinations: destinations, left: 0,
), right: 0,
bottom: 0,
child: ConditionalBottomNav(
child: ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surface.withOpacity(0.8),
),
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: NavigationBar(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
overlayColor: const WidgetStatePropertyAll(
Colors.transparent,
), ),
surfaceTintColor: Colors.transparent,
height: 56,
labelBehavior:
NavigationDestinationLabelBehavior.alwaysHide,
selectedIndex: currentIndex,
onDestinationSelected: onDestinationSelected,
destinations: destinations,
), ),
), ),
), ),
), ),
), ),
], ),
); ),
}, ],
); );
} }
} }

View File

@ -1,4 +1,3 @@
import 'package:auto_route/annotations.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -72,7 +71,6 @@ class TransactionListNotifier extends _$TransactionListNotifier
} }
} }
@RoutePage()
class WalletScreen extends HookConsumerWidget { class WalletScreen extends HookConsumerWidget {
const WalletScreen({super.key}); const WalletScreen({super.key});

View File

@ -185,7 +185,6 @@ Completer<SnCloudFile?> _processUpload(
onProgress: (double progress, Duration estimate) { onProgress: (double progress, Duration estimate) {
onProgress?.call(progress, estimate); onProgress?.call(progress, estimate);
}, },
measureUploadSpeed: true,
) )
.catchError(completer.completeError); .catchError(completer.completeError);

View File

@ -7,7 +7,9 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:island/main.dart'; import 'package:island/main.dart';
import 'package:island/route.dart';
import 'package:island/models/user.dart'; import 'package:island/models/user.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/widgets/app_notification.dart'; import 'package:island/widgets/app_notification.dart';
@ -30,7 +32,9 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
var uri = notification.meta['action_uri'] as String; var uri = notification.meta['action_uri'] as String;
if (uri.startsWith('/')) { if (uri.startsWith('/')) {
// In-app routes // In-app routes
appRouter.pushPath(notification.meta['action_uri']); rootNavigatorKey.currentContext?.push(
notification.meta['action_uri'],
);
} else { } else {
// External URLs // External URLs
launchUrlString(uri); launchUrlString(uri);
@ -44,8 +48,14 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 16, left: 16,
right: 16, right: 16,
// ignore: use_build_context_synchronously top:
top: MediaQuery.of(context).padding.top + 24, (!kIsWeb &&
(Platform.isMacOS ||
Platform.isWindows ||
Platform.isLinux))
? 24
// ignore: use_build_context_synchronously
: MediaQuery.of(context).padding.top + 8,
bottom: 16, bottom: 16,
), ),
); );

View File

@ -1,8 +1,8 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_popup_card/flutter_popup_card.dart'; import 'package:flutter_popup_card/flutter_popup_card.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -105,7 +105,7 @@ class AccountProfileCard extends HookConsumerWidget {
FilledButton.tonalIcon( FilledButton.tonalIcon(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
context.router.pushPath('/account/${data.name}'); context.push('/account/${data.name}');
}, },
icon: const Icon(Symbols.launch), icon: const Icon(Symbols.launch),
label: Text('accountProfileView').tr(), label: Text('accountProfileView').tr(),

View File

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart'; import 'package:island/models/activity.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@ -66,7 +66,7 @@ class FortuneGraphWidget extends HookConsumerWidget {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
onPressed: () { onPressed: () {
context.router.pushNamed( context.pushNamed(
'/account/$eventCalanderUser/calendar', '/account/$eventCalanderUser/calendar',
); );
}, },

View File

@ -1,16 +1,14 @@
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/route.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -18,8 +16,7 @@ import 'package:styled_widget/styled_widget.dart';
class WindowScaffold extends HookConsumerWidget { class WindowScaffold extends HookConsumerWidget {
final Widget child; final Widget child;
final AppRouter router; const WindowScaffold({super.key, required this.child});
const WindowScaffold({super.key, required this.child, required this.router});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -238,7 +235,7 @@ class PageBackButton extends StatelessWidget {
return IconButton( return IconButton(
onPressed: () { onPressed: () {
onWillPop?.call(); onWillPop?.call();
context.router.maybePop(); context.pop();
}, },
icon: Icon( icon: Icon(
color: color, color: color,

View File

@ -1,15 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/services/notify.dart'; import 'package:island/services/notify.dart';
import 'package:island/services/sharing_intent.dart'; import 'package:island/services/sharing_intent.dart';
import 'package:island/widgets/tour/tour.dart';
@RoutePage()
class AppWrapper extends HookConsumerWidget { class AppWrapper extends HookConsumerWidget {
const AppWrapper({super.key}); final Widget child;
const AppWrapper({super.key, required this.child});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -26,6 +25,6 @@ class AppWrapper extends HookConsumerWidget {
}; };
}, const []); }, const []);
return AutoRouter(); return TourTriggerWidget(child: child);
} }
} }

View File

@ -1,12 +1,11 @@
import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/chat.dart'; import 'package:island/models/chat.dart';
import 'package:island/pods/call.dart'; import 'package:island/pods/call.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -45,7 +44,7 @@ class AudioCallButton extends HookConsumerWidget {
try { try {
await apiClient.post('/chat/realtime/$roomId'); await apiClient.post('/chat/realtime/$roomId');
if (context.mounted) { if (context.mounted) {
context.router.push(CallRoute(roomId: roomId)); context.push('/chat/call/$roomId');
} }
} catch (e) { } catch (e) {
showErrorAlert(e); showErrorAlert(e);
@ -97,7 +96,7 @@ class AudioCallButton extends HookConsumerWidget {
tooltip: 'Join Ongoing Call', tooltip: 'Join Ongoing Call',
onPressed: () { onPressed: () {
if (context.mounted) { if (context.mounted) {
context.router.push(CallRoute(roomId: roomId)); context.push('/chat/$roomId/call');
} }
}, },
); );

View File

@ -1,10 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/call.dart'; import 'package:island/pods/call.dart';
import 'package:island/route.gr.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/chat/call_participant_tile.dart'; import 'package:island/widgets/chat/call_participant_tile.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
@ -361,7 +360,7 @@ class CallOverlayBar extends HookConsumerWidget {
).padding(all: 16), ).padding(all: 16),
), ),
onTap: () { onTap: () {
context.router.push(CallRoute(roomId: callNotifier.roomId!)); context.push('/chat/call/callNotifier.roomId!');
}, },
); );
} }

View File

@ -233,16 +233,27 @@ class MessageItem extends HookConsumerWidget {
if (remoteMessage.meta['embeds'] != null) if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>) ...((remoteMessage.meta['embeds'] as List<dynamic>)
.where((embed) => embed['Type'] == 'link') .where((embed) => embed['Type'] == 'link')
.map((embed) => SnEmbedLink.fromJson(embed as Map<String, dynamic>)) .map(
.map((link) => LayoutBuilder( (embed) => SnEmbedLink.fromJson(
builder: (context, constraints) { embed as Map<String, dynamic>,
return EmbedLinkWidget( ),
link: link, )
maxWidth: math.min(constraints.maxWidth, 480), .map(
margin: const EdgeInsets.symmetric(vertical: 4), (link) => LayoutBuilder(
); builder: (context, constraints) {
}, return EmbedLinkWidget(
)) link: link,
maxWidth: math.min(
constraints.maxWidth,
480,
),
margin: const EdgeInsets.symmetric(
vertical: 4,
),
);
},
),
)
.toList()), .toList()),
if (progress != null && progress!.isNotEmpty) if (progress != null && progress!.isNotEmpty)
Column( Column(
@ -482,7 +493,11 @@ class _MessageItemContent extends StatelessWidget {
); );
case 'text': case 'text':
default: default:
return MarkdownTextContent(content: item.content!, isSelectable: true); return MarkdownTextContent(
content: item.content!,
isSelectable: true,
linesMargin: EdgeInsets.zero,
);
} }
} }

View File

@ -1,15 +1,14 @@
import 'dart:convert'; import 'dart:convert';
import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart'; import 'package:island/models/activity.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/auth/captcha.dart'; import 'package:island/screens/auth/captcha.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
@ -137,7 +136,7 @@ class CheckInWidget extends HookConsumerWidget {
if (todayResult.valueOrNull == null) { if (todayResult.valueOrNull == null) {
checkIn(); checkIn();
} else { } else {
context.router.push(EventCalanderRoute(name: 'me')); context.push('/account/me/calendar');
} }
}, },
icon: AnimatedSwitcher( icon: AnimatedSwitcher(

View File

@ -125,6 +125,7 @@ class CloudFileList extends HookConsumerWidget {
if (!disableZoomIn) { if (!disableZoomIn) {
context.pushTransparentRoute( context.pushTransparentRoute(
CloudFileZoomIn(item: files[i], heroTag: heroTags[i]), CloudFileZoomIn(item: files[i], heroTag: heroTags[i]),
rootNavigator: true,
); );
} }
}, },
@ -186,13 +187,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
Future<void> saveToGallery() async { Future<void> saveToGallery() async {
try { try {
// Show loading indicator // Show loading indicator
final scaffold = ScaffoldMessenger.of(context); showSnackBar('Saving image to gallery...');
scaffold.showSnackBar(
const SnackBar(
content: Text('Saving image to gallery...'),
duration: Duration(seconds: 1),
),
);
// Get the image URL // Get the image URL
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
@ -209,12 +204,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
await Gal.putImage(filePath, album: 'Solar Network'); await Gal.putImage(filePath, album: 'Solar Network');
// Show success message // Show success message
scaffold.showSnackBar( showSnackBar('Image saved to gallery');
const SnackBar(
content: Text('Image saved to gallery'),
duration: Duration(seconds: 2),
),
);
} catch (e) { } catch (e) {
showErrorAlert(e); showErrorAlert(e);
} }

View File

@ -1,6 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_highlight/themes/a11y-dark.dart'; import 'package:flutter_highlight/themes/a11y-dark.dart';
import 'package:flutter_highlight/themes/a11y-light.dart'; import 'package:flutter_highlight/themes/a11y-light.dart';
@ -74,7 +74,7 @@ class MarkdownTextContent extends HookConsumerWidget {
final url = Uri.tryParse(href); final url = Uri.tryParse(href);
if (url != null) { if (url != null) {
if (url.scheme == 'solian') { if (url.scheme == 'solian') {
context.router.pushPath( context.push(
['', url.host, ...url.pathSegments].join('/'), ['', url.host, ...url.pathSegments].join('/'),
); );
return; return;

View File

@ -1,26 +1,26 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/tabs.dart';
class ConditionalBottomNav extends HookConsumerWidget { class ConditionalBottomNav extends HookConsumerWidget {
final Widget child; final Widget child;
const ConditionalBottomNav({super.key, required this.child}); const ConditionalBottomNav({super.key, required this.child});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final currentRouteName = ref.watch(currentRouteProvider); final currentLocation = GoRouterState.of(context).uri.toString();
const mainTabRoutes = { // Force rebuild when route changes
ExploreRoute.name, useEffect(() {
ChatListRoute.name, // This effect will run whenever currentLocation changes
RealmListRoute.name, return null;
AccountRoute.name, }, [currentLocation]);
};
debugPrint(currentRouteName); // Use the same route logic as TabsScreen for consistency
final shouldShowBottomNav = mainTabRoutes.contains(currentRouteName); const mainTabRoutes = ['/', '/chat', '/realms', '/account'];
final shouldShowBottomNav = mainTabRoutes.contains(currentLocation);
return shouldShowBottomNav ? child : const SizedBox.shrink(); return shouldShowBottomNav ? child : const SizedBox.shrink();
} }

View File

@ -4,12 +4,107 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:textfield_tags/textfield_tags.dart';
/// A reusable widget for tag input fields with chip display
class ChipTagInputField extends StatelessWidget {
final InputFieldValues inputFieldValues;
final String labelText;
final String hintText;
const ChipTagInputField({
super.key,
required this.inputFieldValues,
required this.labelText,
required this.hintText,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: inputFieldValues.textEditingController,
focusNode: inputFieldValues.focusNode,
decoration: InputDecoration(
label: Text(labelText).tr(),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.all(16),
hintText: inputFieldValues.tags.isNotEmpty ? '' : hintText.tr(),
errorText: inputFieldValues.error,
prefixIconConstraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8,
),
prefixIcon:
inputFieldValues.tags.isNotEmpty
? SingleChildScrollView(
controller: inputFieldValues.tagScrollController,
scrollDirection: Axis.vertical,
child: Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8, left: 8),
child: Wrap(
runSpacing: 4.0,
spacing: 4.0,
children:
inputFieldValues.tags.map<Widget>((dynamic tag) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(20.0),
),
color: Theme.of(context).colorScheme.primary,
),
margin: const EdgeInsets.only(left: 5),
padding: const EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 5.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
child: Text(
'#$tag',
style: TextStyle(
color:
Theme.of(
context,
).colorScheme.onPrimary,
),
),
),
const Gap(4),
InkWell(
child: const Icon(
Icons.cancel,
size: 14.0,
color: Color.fromARGB(255, 233, 233, 233),
),
onTap: () {
inputFieldValues.onTagRemoved(tag);
},
),
],
),
);
}).toList(),
),
),
)
: null,
),
onChanged: inputFieldValues.onTagChanged,
onSubmitted: inputFieldValues.onTagSubmitted,
);
}
}
class ComposeSettingsSheet extends HookWidget { class ComposeSettingsSheet extends HookWidget {
final TextEditingController titleController; final TextEditingController titleController;
final TextEditingController descriptionController; final TextEditingController descriptionController;
final ValueNotifier<int> visibility; final ValueNotifier<int> visibility;
final VoidCallback? onVisibilityChanged; final VoidCallback? onVisibilityChanged;
final StringTagController tagsController;
final StringTagController categoriesController;
const ComposeSettingsSheet({ const ComposeSettingsSheet({
super.key, super.key,
@ -17,6 +112,8 @@ class ComposeSettingsSheet extends HookWidget {
required this.descriptionController, required this.descriptionController,
required this.visibility, required this.visibility,
this.onVisibilityChanged, this.onVisibilityChanged,
required this.tagsController,
required this.categoriesController,
}); });
@override @override
@ -117,6 +214,7 @@ class ComposeSettingsSheet extends HookWidget {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: [ children: [
// Title field // Title field
TextField( TextField(
@ -133,7 +231,6 @@ class ComposeSettingsSheet extends HookWidget {
onTapOutside: onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(16),
// Description field // Description field
TextField( TextField(
@ -151,7 +248,45 @@ class ComposeSettingsSheet extends HookWidget {
onTapOutside: onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(16),
// Tags field
TextFieldTags(
textfieldTagsController: tagsController,
textSeparators: const [' ', ','],
letterCase: LetterCase.normal,
validator: (String tag) {
if (tag.isEmpty) {
return 'No, cannot be empty';
}
return null;
},
inputFieldBuilder: (context, inputFieldValues) {
return ChipTagInputField(
inputFieldValues: inputFieldValues,
labelText: 'tags',
hintText: 'tagsHint',
);
},
),
// Categories field
TextFieldTags(
textfieldTagsController: categoriesController,
textSeparators: const [' ', ','],
letterCase: LetterCase.small,
validator: (String tag) {
if (tag.isEmpty) return 'No, cannot be empty';
if (tag.contains(' ')) return 'Tags should be URL-safe';
return null;
},
inputFieldBuilder: (context, inputFieldValues) {
return ChipTagInputField(
inputFieldValues: inputFieldValues,
labelText: 'categories',
hintText: 'categoriesHint',
);
},
),
// Visibility setting // Visibility setting
Container( Container(

View File

@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
@ -16,34 +17,42 @@ import 'package:pasteboard/pasteboard.dart';
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:textfield_tags/textfield_tags.dart';
class ComposeState { class ComposeState {
final ValueNotifier<List<UniversalFile>> attachments;
final TextEditingController titleController; final TextEditingController titleController;
final TextEditingController descriptionController; final TextEditingController descriptionController;
final TextEditingController contentController; final TextEditingController contentController;
final ValueNotifier<int> visibility; final ValueNotifier<int> visibility;
final ValueNotifier<bool> submitting; final ValueNotifier<List<UniversalFile>> attachments;
final ValueNotifier<Map<int, double>> attachmentProgress; final ValueNotifier<Map<int, double>> attachmentProgress;
final ValueNotifier<SnPublisher?> currentPublisher; final ValueNotifier<SnPublisher?> currentPublisher;
final ValueNotifier<bool> submitting;
StringTagController tagsController;
StringTagController categoriesController;
final String draftId; final String draftId;
int postType;
Timer? _autoSaveTimer; Timer? _autoSaveTimer;
ComposeState({ ComposeState({
required this.attachments,
required this.titleController, required this.titleController,
required this.descriptionController, required this.descriptionController,
required this.contentController, required this.contentController,
required this.visibility, required this.visibility,
required this.submitting, required this.attachments,
required this.attachmentProgress, required this.attachmentProgress,
required this.currentPublisher, required this.currentPublisher,
required this.submitting,
required this.tagsController,
required this.categoriesController,
required this.draftId, required this.draftId,
this.postType = 0,
}); });
void startAutoSave(WidgetRef ref, {int postType = 0}) { void startAutoSave(WidgetRef ref) {
_autoSaveTimer?.cancel(); _autoSaveTimer?.cancel();
_autoSaveTimer = Timer.periodic(const Duration(seconds: 3), (_) { _autoSaveTimer = Timer.periodic(const Duration(seconds: 3), (_) {
ComposeLogic.saveDraftWithoutUpload(ref, this, postType: postType); ComposeLogic.saveDraftWithoutUpload(ref, this);
}); });
} }
@ -59,9 +68,15 @@ class ComposeLogic {
SnPost? forwardedPost, SnPost? forwardedPost,
SnPost? repliedPost, SnPost? repliedPost,
String? draftId, String? draftId,
int postType = 0,
}) { }) {
final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString(); final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString();
final tagsController = StringTagController();
final categoriesController = StringTagController();
originalPost?.tags.forEach((x) => tagsController.addTag(x.slug));
originalPost?.categories.forEach(
(x) => categoriesController.addTag(x.slug),
);
return ComposeState( return ComposeState(
attachments: ValueNotifier<List<UniversalFile>>( attachments: ValueNotifier<List<UniversalFile>>(
originalPost?.attachments originalPost?.attachments
@ -86,17 +101,32 @@ class ComposeLogic {
contentController: TextEditingController( contentController: TextEditingController(
text: text:
originalPost?.content ?? originalPost?.content ??
(forwardedPost != null ? '> ${forwardedPost.content}\n\n' : null), (forwardedPost != null
? '''> ${forwardedPost.content}
'''
: null),
), ),
visibility: ValueNotifier<int>(originalPost?.visibility ?? 0), visibility: ValueNotifier<int>(originalPost?.visibility ?? 0),
submitting: ValueNotifier<bool>(false), submitting: ValueNotifier<bool>(false),
attachmentProgress: ValueNotifier<Map<int, double>>({}), attachmentProgress: ValueNotifier<Map<int, double>>({}),
currentPublisher: ValueNotifier<SnPublisher?>(null), currentPublisher: ValueNotifier<SnPublisher?>(null),
tagsController: tagsController,
categoriesController: categoriesController,
draftId: id, draftId: id,
postType: postType,
); );
} }
static ComposeState createStateFromDraft(SnPost draft) { static ComposeState createStateFromDraft(SnPost draft, {int postType = 0}) {
final tagsController = StringTagController();
final categoriesController = StringTagController();
for (var x in draft.tags) {
tagsController.addTag(x.slug);
}
for (var x in draft.categories) {
categoriesController.addTag(x.slug);
}
return ComposeState( return ComposeState(
attachments: ValueNotifier<List<UniversalFile>>( attachments: ValueNotifier<List<UniversalFile>>(
draft.attachments.map((e) => UniversalFile.fromAttachment(e)).toList(), draft.attachments.map((e) => UniversalFile.fromAttachment(e)).toList(),
@ -108,15 +138,14 @@ class ComposeLogic {
submitting: ValueNotifier<bool>(false), submitting: ValueNotifier<bool>(false),
attachmentProgress: ValueNotifier<Map<int, double>>({}), attachmentProgress: ValueNotifier<Map<int, double>>({}),
currentPublisher: ValueNotifier<SnPublisher?>(null), currentPublisher: ValueNotifier<SnPublisher?>(null),
tagsController: tagsController,
categoriesController: categoriesController,
draftId: draft.id, draftId: draft.id,
postType: postType,
); );
} }
static Future<void> saveDraft( static Future<void> saveDraft(WidgetRef ref, ComposeState state) async {
WidgetRef ref,
ComposeState state, {
int postType = 0,
}) async {
final hasContent = final hasContent =
state.titleController.text.trim().isNotEmpty || state.titleController.text.trim().isNotEmpty ||
state.descriptionController.text.trim().isNotEmpty || state.descriptionController.text.trim().isNotEmpty ||
@ -148,7 +177,7 @@ class ComposeLogic {
baseUrl: baseUrl, baseUrl: baseUrl,
filename: filename:
attachment.data.name ?? attachment.data.name ??
(postType == 1 ? 'Article media' : 'Post media'), (state.postType == 1 ? 'Article media' : 'Post media'),
mimetype: mimetype:
attachment.data.mimeType ?? attachment.data.mimeType ??
ComposeLogic.getMimeTypeFromFileType(attachment.type), ComposeLogic.getMimeTypeFromFileType(attachment.type),
@ -175,7 +204,7 @@ class ComposeLogic {
publishedAt: DateTime.now(), publishedAt: DateTime.now(),
visibility: state.visibility.value, visibility: state.visibility.value,
content: state.contentController.text, content: state.contentController.text,
type: postType, type: state.postType,
meta: null, meta: null,
viewsUnique: 0, viewsUnique: 0,
viewsTotal: 0, viewsTotal: 0,
@ -225,9 +254,8 @@ class ComposeLogic {
static Future<void> saveDraftWithoutUpload( static Future<void> saveDraftWithoutUpload(
WidgetRef ref, WidgetRef ref,
ComposeState state, { ComposeState state,
int postType = 0, ) async {
}) async {
final hasContent = final hasContent =
state.titleController.text.trim().isNotEmpty || state.titleController.text.trim().isNotEmpty ||
state.descriptionController.text.trim().isNotEmpty || state.descriptionController.text.trim().isNotEmpty ||
@ -252,7 +280,7 @@ class ComposeLogic {
publishedAt: DateTime.now(), publishedAt: DateTime.now(),
visibility: state.visibility.value, visibility: state.visibility.value,
content: state.contentController.text, content: state.contentController.text,
type: postType, type: state.postType,
meta: null, meta: null,
viewsUnique: 0, viewsUnique: 0,
viewsTotal: 0, viewsTotal: 0,
@ -306,54 +334,7 @@ class ComposeLogic {
BuildContext context, BuildContext context,
) async { ) async {
try { try {
final draft = SnPost( await saveDraft(ref, state);
id: state.draftId,
title: state.titleController.text,
description: state.descriptionController.text,
language: null,
editedAt: null,
publishedAt: DateTime.now(),
visibility: state.visibility.value,
content: state.contentController.text,
type: 0,
meta: null,
viewsUnique: 0,
viewsTotal: 0,
upvotes: 0,
downvotes: 0,
repliesCount: 0,
threadedPostId: null,
threadedPost: null,
repliedPostId: null,
repliedPost: null,
forwardedPostId: null,
forwardedPost: null,
attachments: [], // TODO: Handle attachments
publisher: SnPublisher(
id: '',
type: 0,
name: '',
nick: '',
picture: null,
background: null,
account: null,
accountId: null,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
realmId: null,
verification: null,
),
reactions: [],
tags: [],
categories: [],
collections: [],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
);
await ref.read(composeStorageNotifierProvider.notifier).saveDraft(draft);
if (context.mounted) { if (context.mounted) {
showSnackBar('draftSaved'.tr()); showSnackBar('draftSaved'.tr());
@ -508,7 +489,6 @@ class ComposeLogic {
SnPost? originalPost, SnPost? originalPost,
SnPost? repliedPost, SnPost? repliedPost,
SnPost? forwardedPost, SnPost? forwardedPost,
int? postType, // 0 for regular post, 1 for article
}) async { }) async {
if (state.submitting.value) return; if (state.submitting.value) return;
@ -554,9 +534,11 @@ class ComposeLogic {
.where((e) => e.isOnCloud) .where((e) => e.isOnCloud)
.map((e) => e.data.id) .map((e) => e.data.id)
.toList(), .toList(),
if (postType != null) 'type': postType, 'type': state.postType,
if (repliedPost != null) 'replied_post_id': repliedPost.id, if (repliedPost != null) 'replied_post_id': repliedPost.id,
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id, if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
'tags': state.tagsController.getTags,
'categories': state.categoriesController.getTags,
}; };
// Send request // Send request
@ -570,7 +552,7 @@ class ComposeLogic {
); );
// Delete draft after successful submission // Delete draft after successful submission
if (postType == 1) { if (state.postType == 1) {
// Delete article draft // Delete article draft
await ref await ref
.read(composeStorageNotifierProvider.notifier) .read(composeStorageNotifierProvider.notifier)
@ -613,7 +595,6 @@ class ComposeLogic {
SnPost? originalPost, SnPost? originalPost,
SnPost? repliedPost, SnPost? repliedPost,
SnPost? forwardedPost, SnPost? forwardedPost,
int? postType,
}) { }) {
if (event is! RawKeyDownEvent) return; if (event is! RawKeyDownEvent) return;
@ -634,7 +615,6 @@ class ComposeLogic {
originalPost: originalPost, originalPost: originalPost,
repliedPost: repliedPost, repliedPost: repliedPost,
forwardedPost: forwardedPost, forwardedPost: forwardedPost,
postType: postType,
); );
} }
} }
@ -649,5 +629,7 @@ class ComposeLogic {
state.submitting.dispose(); state.submitting.dispose();
state.attachmentProgress.dispose(); state.attachmentProgress.dispose();
state.currentPublisher.dispose(); state.currentPublisher.dispose();
state.tagsController.dispose();
state.categoriesController.dispose();
} }
} }

View File

@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/services/compose_storage_db.dart'; import 'package:island/services/compose_storage_db.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
class DraftManagerSheet extends HookConsumerWidget { class DraftManagerSheet extends HookConsumerWidget {
@ -43,9 +44,9 @@ class DraftManagerSheet extends HookConsumerWidget {
], ],
); );
return Scaffold( return SheetScaffold(
appBar: AppBar(title: Text('drafts'.tr())), titleText: 'drafts'.tr(),
body: child:
isLoading.value isLoading.value
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: Column( : Column(

View File

@ -1,6 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@ -11,7 +11,6 @@ import 'package:island/models/post.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
import 'package:island/widgets/account/account_name.dart'; import 'package:island/widgets/account/account_name.dart';
@ -72,7 +71,7 @@ class PostItem extends HookConsumerWidget {
title: 'edit'.tr(), title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit), image: MenuImage.icon(Symbols.edit),
callback: () { callback: () {
context.router.push(PostEditRoute(id: item.id)).then((value) { context.push('/posts/${item.id}/edit').then((value) {
if (value != null) { if (value != null) {
onRefresh?.call(); onRefresh?.call();
} }
@ -117,14 +116,14 @@ class PostItem extends HookConsumerWidget {
title: 'reply'.tr(), title: 'reply'.tr(),
image: MenuImage.icon(Symbols.reply), image: MenuImage.icon(Symbols.reply),
callback: () { callback: () {
context.router.push(PostComposeRoute(repliedPost: item)); context.push('/posts/compose', extra: {'repliedPost': item});
}, },
), ),
MenuAction( MenuAction(
title: 'forward'.tr(), title: 'forward'.tr(),
image: MenuImage.icon(Symbols.forward), image: MenuImage.icon(Symbols.forward),
callback: () { callback: () {
context.router.push(PostComposeRoute(forwardedPost: item)); context.push('/posts/compose', extra: {'forwardedPost': item});
}, },
), ),
MenuSeparator(), MenuSeparator(),
@ -168,9 +167,7 @@ class PostItem extends HookConsumerWidget {
GestureDetector( GestureDetector(
child: ProfilePictureWidget(file: item.publisher.picture), child: ProfilePictureWidget(file: item.publisher.picture),
onTap: () { onTap: () {
context.router.push( context.push('/publishers/${item.publisher.name}');
PublisherProfileRoute(name: item.publisher.name),
);
}, },
), ),
Expanded( Expanded(
@ -245,6 +242,57 @@ class PostItem extends HookConsumerWidget {
? EdgeInsets.only(bottom: 8) ? EdgeInsets.only(bottom: 8)
: null, : null,
), ),
// Render tags and categories if they exist
if (item.tags.isNotEmpty ||
item.categories.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.tags.isNotEmpty)
Wrap(
children: [
for (final tag in item.tags)
InkWell(
child: Row(
spacing: 4,
children: [
const Icon(
Symbols.label,
size: 13,
),
Text(
tag.name ?? '#${tag.slug}',
).fontSize(13),
],
),
onTap: () {},
),
],
),
if (item.categories.isNotEmpty)
Wrap(
children: [
for (final category in item.categories)
InkWell(
child: Row(
spacing: 4,
children: [
const Icon(
Symbols.category,
size: 13,
),
Text(
category.name ??
'#${category.slug}',
).fontSize(13),
],
),
onTap: () {},
),
],
),
],
),
// Show truncation hint if post is truncated // Show truncation hint if post is truncated
if (item.isTruncated && !isFullPost) if (item.isTruncated && !isFullPost)
_PostTruncateHint().padding( _PostTruncateHint().padding(
@ -286,7 +334,7 @@ class PostItem extends HookConsumerWidget {
), ),
onTap: () { onTap: () {
if (isOpenable) { if (isOpenable) {
context.router.push(PostDetailRoute(id: item.id)); context.push('/posts/${item.id}');
} }
}, },
), ),
@ -314,6 +362,7 @@ class PostItem extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
useRootNavigator: true,
builder: (context) => PostRepliesSheet(post: item), builder: (context) => PostRepliesSheet(post: item),
); );
} }
@ -487,9 +536,7 @@ Widget _buildReferencePost(BuildContext context, SnPost item) {
), ),
], ],
), ),
).gestures( ).gestures(onTap: () => context.push('/posts/referencePost.id'));
onTap: () => context.router.push(PostDetailRoute(id: referencePost.id)),
);
} }
class PostReactionList extends HookConsumerWidget { class PostReactionList extends HookConsumerWidget {

View File

@ -1,11 +1,10 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_file_collection.dart'; import 'package:island/widgets/content/cloud_file_collection.dart';
@ -46,7 +45,7 @@ class PostItemCreator extends HookConsumerWidget {
title: 'edit'.tr(), title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit), image: MenuImage.icon(Symbols.edit),
callback: () { callback: () {
context.router.push(PostEditRoute(id: item.id)).then((value) { context.push('/posts/${item.id}/edit').then((value) {
if (value != null) { if (value != null) {
onRefresh?.call(); onRefresh?.call();
} }
@ -81,7 +80,7 @@ class PostItemCreator extends HookConsumerWidget {
image: MenuImage.icon(Symbols.link), image: MenuImage.icon(Symbols.link),
callback: () { callback: () {
// Copy post link to clipboard // Copy post link to clipboard
context.router.push(PostDetailRoute(id: item.id)); context.push('/posts/${item.id}');
}, },
), ),
], ],
@ -95,7 +94,7 @@ class PostItemCreator extends HookConsumerWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
onTap: () { onTap: () {
if (isOpenable) { if (isOpenable) {
context.router.pushPath('/posts/${item.id}'); context.push('/posts/${item.id}');
} }
}, },
child: Padding( child: Padding(

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart'; import 'package:island/screens/creators/publishers.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';

View File

@ -1,11 +1,10 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/creators/publishers.dart'; import 'package:island/screens/creators/publishers.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@ -44,8 +43,7 @@ class PublisherModal extends HookConsumerWidget {
const Gap(12), const Gap(12),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
context.router context.push('/creators/publishers/new')
.push(NewPublisherRoute())
.then((value) { .then((value) {
if (value != null) { if (value != null) {
ref.invalidate( ref.invalidate(

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/publisher.dart';
import 'package:island/widgets/content/cloud_files.dart';
class PublisherCard extends ConsumerWidget {
final SnPublisher publisher;
final double? maxWidth;
const PublisherCard({super.key, required this.publisher, this.maxWidth});
@override
Widget build(BuildContext context, WidgetRef ref) {
Widget imageWidget;
if (publisher.picture != null) {
imageWidget = CloudImageWidget(
file: publisher.background,
fit: BoxFit.cover,
);
} else {
imageWidget = ColoredBox(
color: Theme.of(context).colorScheme.secondaryContainer,
);
}
Widget card = Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
context.push('/publishers/${publisher.name}');
},
child: AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
fit: StackFit.expand,
children: [
imageWidget,
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
],
),
),
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.5),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: ProfilePictureWidget(
file: publisher.picture,
radius: 12,
),
),
const Gap(2),
Text(
publisher.nick,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
],
),
),
),
);
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: card,
);
}
}

View File

@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/realm.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
class RealmCard extends ConsumerWidget {
final SnRealm realm;
final double? maxWidth;
const RealmCard({super.key, required this.realm, this.maxWidth});
@override
Widget build(BuildContext context, WidgetRef ref) {
Widget imageWidget;
if (realm.picture != null) {
imageWidget =
imageWidget = CloudImageWidget(
file: realm.background,
fit: BoxFit.cover,
);
} else {
imageWidget = ColoredBox(
color: Theme.of(context).colorScheme.secondaryContainer,
);
}
Widget card = Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
context.push('/realms/${realm.slug}');
},
child: AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
fit: StackFit.expand,
children: [
imageWidget,
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
],
),
),
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.5),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: ProfilePictureWidget(
file: realm.picture,
fallbackIcon: Symbols.group,
radius: 12,
),
),
const Gap(2),
Text(
realm.name,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
],
),
),
),
);
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: card,
);
}
}

View File

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/realm/realm_card.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
part 'realm_list.g.dart';
@riverpod
class RealmListNotifier extends _$RealmListNotifier
with CursorPagingNotifierMixin<SnRealm> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnRealm>> build(String? query) {
return fetch(cursor: null, query: query);
}
@override
Future<CursorPagingData<SnRealm>> fetch({
required String? cursor,
String? query,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final queryParams = {
'offset': offset,
'take': _pageSize,
if (query != null && query.isNotEmpty) 'query': query,
};
final response = await client.get(
'/discovery/realms',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final realms = data.map((json) => SnRealm.fromJson(json)).toList();
final hasMore = offset + realms.length < total;
final nextCursor = hasMore ? (offset + realms.length).toString() : null;
return CursorPagingData(
items: realms,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
class SliverRealmList extends HookConsumerWidget {
const SliverRealmList({super.key, this.query});
final String? query;
@override
Widget build(BuildContext context, WidgetRef ref) {
return PagingHelperSliverView(
provider: realmListNotifierProvider(query),
futureRefreshable: realmListNotifierProvider(query).future,
notifierRefreshable: realmListNotifierProvider(query).notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.separated(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final realm = data.items[index];
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: RealmCard(realm: realm),
);
},
separatorBuilder: (_, _) => const Gap(8),
),
);
}
}

View File

@ -0,0 +1,179 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'realm_list.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$realmListNotifierHash() => r'02dee373a5609a5617b04ffec395d09dea7ae070';
/// 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 _$RealmListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnRealm>> {
late final String? query;
FutureOr<CursorPagingData<SnRealm>> build(String? query);
}
/// See also [RealmListNotifier].
@ProviderFor(RealmListNotifier)
const realmListNotifierProvider = RealmListNotifierFamily();
/// See also [RealmListNotifier].
class RealmListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnRealm>>> {
/// See also [RealmListNotifier].
const RealmListNotifierFamily();
/// See also [RealmListNotifier].
RealmListNotifierProvider call(String? query) {
return RealmListNotifierProvider(query);
}
@override
RealmListNotifierProvider getProviderOverride(
covariant RealmListNotifierProvider provider,
) {
return call(provider.query);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'realmListNotifierProvider';
}
/// See also [RealmListNotifier].
class RealmListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
RealmListNotifier,
CursorPagingData<SnRealm>
> {
/// See also [RealmListNotifier].
RealmListNotifierProvider(String? query)
: this._internal(
() => RealmListNotifier()..query = query,
from: realmListNotifierProvider,
name: r'realmListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$realmListNotifierHash,
dependencies: RealmListNotifierFamily._dependencies,
allTransitiveDependencies:
RealmListNotifierFamily._allTransitiveDependencies,
query: query,
);
RealmListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.query,
}) : super.internal();
final String? query;
@override
FutureOr<CursorPagingData<SnRealm>> runNotifierBuild(
covariant RealmListNotifier notifier,
) {
return notifier.build(query);
}
@override
Override overrideWith(RealmListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: RealmListNotifierProvider._internal(
() => create()..query = query,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
query: query,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
RealmListNotifier,
CursorPagingData<SnRealm>
>
createElement() {
return _RealmListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is RealmListNotifierProvider && other.query == query;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, query.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin RealmListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnRealm>> {
/// The parameter `query` of this provider.
String? get query;
}
class _RealmListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
RealmListNotifier,
CursorPagingData<SnRealm>
>
with RealmListNotifierRef {
_RealmListNotifierProviderElement(super.provider);
@override
String? get query => (origin as RealmListNotifierProvider).query;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/realm.dart';
import 'package:island/widgets/content/cloud_files.dart';
class RealmTile extends HookConsumerWidget {
final SnRealm realm;
const RealmTile({super.key, required this.realm});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ListTile(
leading: ProfilePictureWidget(file: realm.picture),
title: Text(realm.name),
subtitle: Text(realm.description),
onTap: () => context.push('/realms/${realm.slug}'),
);
}
}

View File

@ -1,19 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:auto_route/auto_route.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/posts/compose.dart'; import 'package:island/screens/posts/compose.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/link_preview.dart'; import 'package:island/pods/link_preview.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:mime/mime.dart';
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@ -150,9 +149,9 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
case ShareContentType.file: case ShareContentType.file:
if (widget.content.files != null) { if (widget.content.files != null) {
// Convert XFiles to UniversalFiles // Convert XFiles to UniversalFiles
for (final xFile in widget.content.files!) { for (final file in widget.content.files!) {
final file = File(xFile.path); var mimeType = file.mimeType;
final mimeType = xFile.mimeType; mimeType ??= lookupMimeType(file.path);
UniversalFileType fileType; UniversalFileType fileType;
if (mimeType?.startsWith('image/') == true) { if (mimeType?.startsWith('image/') == true) {
@ -179,7 +178,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
// Navigate to compose screen // Navigate to compose screen
if (mounted) { if (mounted) {
context.router.push(PostComposeRoute(initialState: initialState)); context.push('/posts/compose', extra: initialState);
Navigator.of(context).pop(); // Close the share sheet Navigator.of(context).pop(); // Close the share sheet
} }
} catch (e) { } catch (e) {
@ -193,7 +192,6 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
final apiClient = ref.read(apiClientProvider); final apiClient = ref.read(apiClientProvider);
final userInfo = ref.read(userInfoProvider.notifier);
final serverUrl = ref.read(serverUrlProvider); final serverUrl = ref.read(serverUrlProvider);
String content = _messageController.text.trim(); String content = _messageController.text.trim();
@ -218,7 +216,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
case ShareContentType.file: case ShareContentType.file:
// Upload files to cloud storage // Upload files to cloud storage
if (widget.content.files?.isNotEmpty == true) { if (widget.content.files?.isNotEmpty == true) {
final token = await userInfo.getAccessToken(); final token = ref.watch(tokenProvider)?.token;
if (token == null) { if (token == null) {
throw Exception('Authentication required'); throw Exception('Authentication required');
} }
@ -325,7 +323,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
// Navigate to chat if requested // Navigate to chat if requested
if (shouldNavigate == true && mounted) { if (shouldNavigate == true && mounted) {
context.router.pushPath('/chat/$chatRoom'); context.push('/chat/${chatRoom.id}');
} }
} }
} catch (e) { } catch (e) {
@ -405,132 +403,153 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
heightFactor: 0.75, heightFactor: 0.75,
child: Column( child: Column(
children: [ children: [
// Content preview // Share options with keyboard avoidance
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'contentToShare'.tr(),
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
_ContentPreview(content: widget.content),
],
),
),
// Share options
Expanded( Expanded(
child: SingleChildScrollView( child: AnimatedPadding(
child: Column( duration: const Duration(milliseconds: 300),
crossAxisAlignment: CrossAxisAlignment.start, padding: EdgeInsets.only(
children: [ bottom: MediaQuery.of(context).viewInsets.bottom,
// Quick actions row (horizontally scrollable) ),
Padding( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ // Content preview
Text( Container(
'quickActions'.tr(), margin: const EdgeInsets.all(16),
style: Theme.of( padding: const EdgeInsets.all(16),
context, decoration: BoxDecoration(
).textTheme.titleSmall?.copyWith( color:
color: Theme.of(
Theme.of(context).colorScheme.onSurfaceVariant, context,
).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'contentToShare'.tr(),
style: Theme.of(
context,
).textTheme.labelMedium?.copyWith(
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
), ),
), const SizedBox(height: 8),
const SizedBox(height: 12), _ContentPreview(content: widget.content),
SizedBox( ],
height: 80, ),
child: ListView( ),
scrollDirection: Axis.horizontal, // Quick actions row (horizontally scrollable)
children: [ Padding(
_CompactShareOption( padding: const EdgeInsets.symmetric(horizontal: 16),
icon: Symbols.post_add, child: Column(
title: 'post'.tr(), crossAxisAlignment: CrossAxisAlignment.start,
onTap: _isLoading ? null : _shareToPost, children: [
), Text(
const SizedBox(width: 12), 'quickActions'.tr(),
_CompactShareOption( style: Theme.of(
icon: Symbols.content_copy, context,
title: 'copy'.tr(), ).textTheme.titleSmall?.copyWith(
onTap: _isLoading ? null : _copyToClipboard, color:
), Theme.of(
if (widget.toSystem) ...<Widget>[ context,
).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
SizedBox(
height: 80,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
_CompactShareOption(
icon: Symbols.post_add,
title: 'post'.tr(),
onTap: _isLoading ? null : _shareToPost,
),
const SizedBox(width: 12), const SizedBox(width: 12),
_CompactShareOption( _CompactShareOption(
icon: Symbols.share, icon: Symbols.content_copy,
title: 'share'.tr(), title: 'copy'.tr(),
onTap: _isLoading ? null : _shareToSystem, onTap: _isLoading ? null : _copyToClipboard,
), ),
if (widget.toSystem) ...<Widget>[
const SizedBox(width: 12),
_CompactShareOption(
icon: Symbols.share,
title: 'share'.tr(),
onTap: _isLoading ? null : _shareToSystem,
),
],
], ],
],
),
),
],
),
),
const SizedBox(height: 24),
// Chat section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'sendToChat'.tr(),
style: Theme.of(
context,
).textTheme.titleSmall?.copyWith(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
// Additional message input
Container(
margin: const EdgeInsets.only(bottom: 16),
child: TextField(
controller: _messageController,
decoration: InputDecoration(
hintText: 'addAdditionalMessage'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
), ),
maxLines: 3,
minLines: 1,
enabled: !_isLoading,
), ),
), ],
),
_ChatRoomsList(
onChatSelected:
_isLoading ? null : _shareToSpecificChat,
),
],
), ),
),
const SizedBox(height: 16), const SizedBox(height: 24),
],
// Chat section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'sendToChat'.tr(),
style: Theme.of(
context,
).textTheme.titleSmall?.copyWith(
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
// Additional message input
Container(
margin: const EdgeInsets.only(bottom: 16),
child: TextField(
controller: _messageController,
decoration: InputDecoration(
hintText: 'addAdditionalMessage'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
maxLines: 3,
minLines: 1,
enabled: !_isLoading,
),
),
_ChatRoomsList(
onChatSelected:
_isLoading ? null : _shareToSpecificChat,
),
],
),
),
const SizedBox(height: 16),
],
),
), ),
), ),
), ),
@ -830,9 +849,7 @@ class _TextPreview extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
constraints: const BoxConstraints(maxHeight: kPreviewMaxHeight), constraints: const BoxConstraints(maxHeight: kPreviewMaxHeight),
child: SingleChildScrollView( child: Text(text, style: Theme.of(context).textTheme.bodyMedium),
child: Text(text, style: Theme.of(context).textTheme.bodyMedium),
),
); );
} }
} }
@ -1001,13 +1018,11 @@ class _LinkPreview extends ConsumerWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Expanded( Expanded(
child: SingleChildScrollView( child: SelectableText(
child: SelectableText( link,
link, style: Theme.of(context).textTheme.bodyMedium?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.primary, decoration: TextDecoration.underline,
decoration: TextDecoration.underline,
),
), ),
), ),
), ),
@ -1236,6 +1251,7 @@ void showShareSheet({
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
useRootNavigator: true,
builder: builder:
(context) => ShareSheet( (context) => ShareSheet(
content: content, content: content,

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