Compare commits

...

20 Commits

Author SHA1 Message Date
007acedf29 🚀 Launch 3.0.0+110 2025-07-02 02:23:46 +08:00
8e903ec6c1 About page 2025-07-02 02:20:31 +08:00
b55e56c3c4 Web articles list 2025-07-02 01:11:25 +08:00
6f9de431b1 Search post 2025-07-02 00:40:35 +08:00
a8efd26262 ⬆️ Upgrade dependencies 2025-07-01 22:54:58 +08:00
e367fc3f5c Web articles detail page & explore feed 2025-07-01 13:19:14 +08:00
8a1af120ea Discover article 2025-07-01 01:34:34 +08:00
f03f0181f8 EXIF viewer 2025-07-01 00:14:12 +08:00
6c7d42c31a 🐛 Bug fixes 2025-06-30 23:55:44 +08:00
d6c829c26a Web feed 2025-06-30 23:33:14 +08:00
666a2dfbf5 Feature flags 2025-06-30 01:11:31 +08:00
fd979c3a35 Custom apps 2025-06-30 00:22:59 +08:00
847fc6e864 💄 Optimized custom app display 2025-06-29 23:34:56 +08:00
356b7bf01a Developer app basis 2025-06-29 23:00:51 +08:00
450d5ebc81 Developer portal basis 2025-06-29 19:36:37 +08:00
f04285848f 🐛 Fix upload file in share sheet 2025-06-29 18:03:18 +08:00
c4becb0a05 Publisher members 2025-06-28 18:57:32 +08:00
d22619396b 🐛 Fix linux builds 2025-06-28 11:45:50 +08:00
fe8640a6db 🚀 Launch 3.0.0+109 2025-06-28 02:48:15 +08:00
ff475d43dd 🐛 Dozen of hot fixes 2025-06-28 02:40:50 +08:00
86 changed files with 8414 additions and 728 deletions

View File

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

View File

@ -98,7 +98,7 @@
<receiver
android:name=".receiver.ReplyReceiver"
android:enabled="true"
android:exported="false" />
android:exported="true" />
<service
android:name=".service.MessagingService"

View File

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

View File

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

View File

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

View File

@ -89,14 +89,32 @@
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
"authFactorPin": "Pin Code",
"authFactorPinDescription": "It consists of 6 digits. It cannot be used to log in. When performing some dangerous operations, the system will ask you to enter this PIN for confirmation.",
"realms": "Realms",
"createRealm": "Create a Realm",
"createRealmHint": "Meet friends with same interests, build communities, and more.",
"editRealm": "Edit Realm",
"deleteRealm": "Delete Realm",
"deleteRealmHint": "Are you sure to delete this realm? This will also deleted all the channels, publishers, and posts under this realm.",
"explore": "Explore",
"exploreFilterSubscriptions": "Subscriptions",
"exploreFilterFriends": "Friends",
"discover": "Discover",
"joinRealm": "Join Realm",
"account": "Account",
"name": "Name",
"slug": "Slug",
"slugHint": "The slug will be used in the URL to access this resource, it should be unique and URL safe.",
"createChatRoom": "Create a Room",
"editChatRoom": "Edit Room",
"deleteChatRoom": "Delete Room",
"deleteChatRoomHint": "Are you sure to delete this room? This action cannot be undone.",
"chat": "Chat",
"chatTabAll": "All",
"chatTabDirect": "Direct Messages",
"chatTabGroup": "Group Chats",
"chatMessageHint": "Message in {}",
"chatDirectMessageHint": "Message to {}",
"directMessage": "Direct Message",
"loading": "Loading...",
"descriptionNone": "No description yet.",
"invites": "Invites",
@ -231,6 +249,7 @@
"uploadingProgress": "Uploading {} of {}",
"uploadAll": "Upload All",
"stickerCopyPlaceholder": "Copy Placeholder",
"realmSelection": "Select a Realm",
"individual": "Individual",
"firstPostBadgeName": "First Post",
"firstPostBadgeDescription": "Created your first post on Solar Network",
@ -286,6 +305,12 @@
"levelingProgressExperience": "{} EXP",
"levelingProgressLevel": "Level {}",
"fileUploadingProgress": "Uploading file #{}: {}%",
"removeChatMember": "Remove Chat Room Member",
"removeChatMemberHint": "Are you sure to remove this member from the room?",
"removeRealmMember": "Remove Realm Member",
"removeRealmMemberHint": "Are you sure to remove this member from the realm?",
"removePublisherMember": "Remove Publisher Member",
"removePublisherMemberHint": "Are you sure to remove this member from the publisher?",
"memberRole": "Member Role",
"memberRoleHint": "Greater number has higher permission.",
"memberRoleEdit": "Edit role for @{}",
@ -293,6 +318,10 @@
"openLinkConfirmDescription": "You're going to leave the Solar Network and open the link ({}) in your browser. It is not related to Solar Network. Beware of phishing and scams.",
"brokenLink": "Unable open link {}... It might be broken or missing uri parts...",
"copyToClipboard": "Copy to clipboard",
"leaveChatRoom": "Leave Chat Room",
"leaveChatRoomHint": "Are you sure to leave this chat room?",
"leaveRealm": "Leave Realm",
"leaveRealmHint": "Are you sure to leave this realm?",
"walletNotFound": "Wallet not found",
"walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.",
"walletCreate": "Create a Wallet",
@ -304,6 +333,12 @@
"settingsBackgroundImageClear": "Clear Background Image",
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
"messageNone": "No content to display",
"unreadMessages": {
"one": "{} unread message",
"other": "{} unread messages"
},
"chatBreakNone": "None",
"settingsRealmCompactView": "Compact Realm View",
"settingsMixedFeed": "Mixed Feed",
"settingsAutoTranslate": "Auto Translate",
"settingsHideBottomNav": "Hide Bottom Navigation",
@ -346,6 +381,7 @@
"postVisibilityUnlisted": "Unlisted",
"postVisibilityPrivate": "Private",
"postTruncated": "Content truncated, tap to view full post",
"copyMessage": "Copy Message",
"authFactor": "Authentication Factor",
"authFactorDelete": "Delete the Factor",
"authFactorDeleteHint": "Are you sure you want to delete this authentication factor? This action cannot be undone.",
@ -373,6 +409,10 @@
"lastActiveAt": "Last active at {}",
"authDeviceLogout": "Logout",
"authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.",
"typingHint": {
"one": "{} is typing...",
"other": "{} are typing..."
},
"authDeviceEditLabel": "Edit Label",
"authDeviceLabelTitle": "Edit Device Label",
"authDeviceLabelHint": "Enter a name for this device",
@ -439,6 +479,21 @@
"contactMethodSetPrimary": "Set as Primary",
"contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications",
"contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.",
"chatNotifyLevel": "Notify Level",
"chatNotifyLevelDescription": "Decide how many notifications you will receive.",
"chatNotifyLevelAll": "All",
"chatNotifyLevelMention": "Mentions",
"chatNotifyLevelNone": "None",
"chatNotifyLevelUpdated": "The notify level has been updated to {}.",
"chatBreak": "Take a Break",
"chatBreakDescription": "Set a time, before that time, your notification level will be metions only, to take a break of the current topic they're talking about.",
"chatBreakClear": "Clear the break time",
"chatBreakHour": "{} break",
"chatBreakDay": "{} day break",
"chatBreakSet": "Break set for {}",
"chatBreakCleared": "Chat break has been cleared.",
"chatBreakCustom": "Custom duration",
"chatBreakEnterMinutes": "Enter minutes",
"firstName": "First Name",
"middleName": "Middle Name",
"lastName": "Last Name",
@ -520,17 +575,29 @@
"quickActions": "Quick Actions",
"post": "Post",
"copy": "Copy",
"sendToChat": "Send to Chat",
"failedToShareToPost": "Failed to share to post: {}",
"shareToChatComingSoon": "Share to chat functionality coming soon",
"failedToShareToChat": "Failed to share to chat: {}",
"shareToSpecificChatComingSoon": "Share to {} coming soon",
"directChat": "Direct Chat",
"systemShareComingSoon": "System share functionality coming soon",
"failedToShareToSystem": "Failed to share to system: {}",
"failedToCopy": "Failed to copy: {}",
"noChatRoomsAvailable": "No chat rooms available",
"failedToLoadChats": "Failed to load chats",
"contentToShare": "Content to share:",
"unknownChat": "Unknown Chat",
"addAdditionalMessage": "Add additional message...",
"uploadingFiles": "Uploading files...",
"sharedSuccessfully": "Shared successfully!",
"shareSuccess": "Shared successfully!",
"shareToSpecificChatSuccess": "Shared to {} successfully!",
"wouldYouLikeToGoToChat": "Would you like to go to the chat?",
"no": "No",
"yes": "Yes",
"navigateToChat": "Navigate to Chat",
"wouldYouLikeToNavigateToChat": "Would you like to navigate to the chat?",
"abuseReport": "Report",
"abuseReportTitle": "Report Content",
"abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.",
@ -564,5 +631,52 @@
"realmJoinSuccess": "Successfully joined the realm.",
"discoverRealms": "Discover Realms",
"discoverPublishers": "Discover Publishers",
"search": "Search"
"search": "Search",
"publisherMembers": "Collaborators",
"developerHub": "Developer Hub",
"developerHubUnselectedHint": "Select a developer to see stats or enroll a new one.",
"enrollDeveloper": "Enroll as a Developer",
"enrollDeveloperHint": "Enroll one of your publishers to become a developer.",
"noPublishersToEnroll": "You don't have any publishers that can be enrolled as a developer.",
"totalCustomApps": "Total Custom Apps",
"customApps": "Custom Apps",
"noCustomApps": "No custom apps yet.",
"createCustomApp": "Create Custom App",
"editCustomApp": "Edit Custom App",
"deleteCustomApp": "Delete Custom App",
"deleteCustomAppHint": "Are you sure you want to delete this custom app? This action cannot be undone.",
"publicRealm": "Public Realm",
"publicRealmDescription": "Anyone can preview the content of this realm.",
"communityRealm": "Community Realm",
"communityRealmDescription": "Anyone can join this realm and participate in discussions. And will show in the discover page & feed.",
"publicChat": "Public Chat",
"publicChatDescription": "Anyone can preview the content of this chat. Including unjoined bots.",
"communityChat": "Community Chat",
"communityChatDescription": "Anyone can join this chat and participate in discussions.",
"appLinks": "App Links",
"homePageUrl": "Home Page URL",
"privacyPolicyUrl": "Privacy Policy URL",
"termsOfServiceUrl": "Terms of Service URL",
"oauthConfig": "OAuth Configuration",
"clientUri": "Client URI",
"redirectUris": "Redirect URIs",
"addRedirectUri": "Add Redirect URI",
"allowedScopes": "Allowed Scopes",
"requirePkce": "Require PKCE",
"allowOfflineAccess": "Allow Offline Access",
"redirectUri": "Redirect URI",
"redirectUriHint": "The redirect URI is used for OAuth authentication. When the app goes to production, we will validate the redirect URI is match your configuration to reject invalid requests.",
"uriRequired": "The URI is required.",
"uriInvalid": "The URI is invalid.",
"add": "Add",
"addScope": "Add Scope",
"scope": "Scope",
"publisherFeatures": "Features",
"publisherFeatureDevelop": "Developer Program",
"publisherFeatureDevelopDescription": "Unlock development abilities for your publisher, including custom apps, API keys, and more.",
"publisherFeatureDevelopHint": "Currently, this feature is under active development, you need send a request to unlock this feature.",
"learnMore": "Learn More",
"discoverWebArticles": "Articles from external sites",
"webArticlesStand": "Article Stand",
"about": "About"
}

View File

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

View File

@ -7,6 +7,7 @@ import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker_android/image_picker_android.dart';
@ -158,6 +159,28 @@ class IslandApp extends HookConsumerWidget {
}
useEffect(() {
const channel = MethodChannel('dev.solsynth.solian/notifications');
Future<void> handleInitialLink() async {
final String? link = await channel.invokeMethod('initialLink');
if (link != null) {
final router = ref.read(routerProvider);
router.go(link);
}
}
if (!kIsWeb && Platform.isAndroid) {
handleInitialLink();
}
channel.setMethodCallHandler((call) async {
if (call.method == 'newLink') {
final String link = call.arguments;
final router = ref.read(routerProvider);
router.go(link);
}
});
// When the app is opened from a terminated state.
FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

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

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

View File

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

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

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

View File

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

View File

@ -0,0 +1 @@

View File

@ -11,11 +11,6 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
UserInfoNotifier(this._ref) : super(const AsyncValue.data(null));
Future<String?> getAccessToken() async {
final prefs = _ref.read(sharedPreferencesProvider);
return prefs.getString(kTokenPairStoreKey);
}
Future<void> fetchUser() async {
try {
final client = _ref.read(apiClientProvider);

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

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

View File

@ -1,10 +1,17 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/screens/about.dart';
import 'package:island/screens/developers/apps.dart';
import 'package:island/screens/developers/edit_app.dart';
import 'package:island/screens/developers/new_app.dart';
import 'package:island/screens/developers/hub.dart';
import 'package:island/screens/discovery/articles.dart';
import 'package:island/screens/posts/post_search.dart';
import 'package:island/widgets/app_wrapper.dart';
import 'package:island/screens/tabs.dart';
import 'package:island/screens/explore.dart';
import 'package:island/screens/article_detail_screen.dart';
import 'package:island/screens/account.dart';
import 'package:island/screens/notification.dart';
import 'package:island/screens/wallet.dart';
@ -18,18 +25,20 @@ import 'package:island/screens/chat/room.dart';
import 'package:island/screens/chat/room_detail.dart';
import 'package:island/screens/chat/call.dart';
import 'package:island/screens/creators/hub.dart';
import 'package:island/screens/creators/posts/list.dart';
import 'package:island/screens/creators/posts/post_manage_list.dart';
import 'package:island/screens/creators/stickers/stickers.dart';
import 'package:island/screens/creators/stickers/pack_detail.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
import 'package:island/screens/creators/webfeed/webfeed_edit.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/screens/posts/detail.dart';
import 'package:island/screens/posts/post_detail.dart';
import 'package:island/screens/posts/pub_profile.dart';
import 'package:island/screens/auth/login.dart';
import 'package:island/screens/auth/create_account.dart';
import 'package:island/screens/settings.dart';
import 'package:island/screens/realm/realms.dart';
import 'package:island/screens/realm/detail.dart';
import 'package:island/screens/realm/realm_detail.dart';
import 'package:island/screens/account/event_calendar.dart';
import 'package:island/screens/discovery/realms.dart';
@ -79,33 +88,64 @@ final routerProvider = Provider<GoRouter>((ref) {
return EventCalanderScreen(name: name);
},
),
GoRoute(
path: '/creators',
builder: (context, state) => const CreatorHubScreen(),
ShellRoute(
builder:
(context, state, child) => CreatorHubShellScreen(child: child),
routes: [
GoRoute(
path: ':name/posts',
path: '/creators',
builder: (context, state) => const CreatorHubScreen(),
),
// Web Feed Routes
GoRoute(
path: '/creators/:name/feeds',
builder: (context, state) {
final name = state.pathParameters['name']!;
return WebFeedListScreen(pubName: name);
},
routes: [
GoRoute(
path: 'new',
builder: (context, state) {
return WebFeedNewScreen(
pubName: state.pathParameters['name']!,
);
},
),
GoRoute(
path: ':feedId',
builder: (context, state) {
return WebFeedEditScreen(
pubName: state.pathParameters['name']!,
feedId: state.pathParameters['feedId'],
);
},
),
],
),
GoRoute(
path: '/creators/:name/posts',
builder: (context, state) {
final name = state.pathParameters['name']!;
return CreatorPostListScreen(pubName: name);
},
),
GoRoute(
path: ':name/stickers',
path: '/creators/:name/stickers',
builder: (context, state) {
final name = state.pathParameters['name']!;
return StickersScreen(pubName: name);
},
),
GoRoute(
path: ':name/stickers/new',
path: '/creators/:name/stickers/new',
builder: (context, state) {
final name = state.pathParameters['name']!;
return NewStickerPacksScreen(pubName: name);
},
),
GoRoute(
path: ':name/stickers/:packId/edit',
path: '/creators/:name/stickers/:packId/edit',
builder: (context, state) {
final name = state.pathParameters['name']!;
final packId = state.pathParameters['packId']!;
@ -113,7 +153,7 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
GoRoute(
path: ':name/stickers/:packId',
path: '/creators/:name/stickers/:packId',
builder: (context, state) {
final name = state.pathParameters['name']!;
final packId = state.pathParameters['packId']!;
@ -121,14 +161,14 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
GoRoute(
path: ':name/stickers/:packId/new',
path: '/creators/:name/stickers/:packId/new',
builder: (context, state) {
final packId = state.pathParameters['packId']!;
return NewStickersScreen(packId: packId);
},
),
GoRoute(
path: ':name/stickers/:packId/:id/edit',
path: '/creators/:name/stickers/:packId/:id/edit',
builder: (context, state) {
final packId = state.pathParameters['packId']!;
final id = state.pathParameters['id']!;
@ -136,11 +176,11 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
GoRoute(
path: 'new',
path: '/creators/new',
builder: (context, state) => const NewPublisherScreen(),
),
GoRoute(
path: ':name/edit',
path: '/creators/:name/edit',
builder: (context, state) {
final name = state.pathParameters['name']!;
return EditPublisherScreen(name: name);
@ -148,6 +188,52 @@ final routerProvider = Provider<GoRouter>((ref) {
),
],
),
ShellRoute(
builder:
(context, state, child) =>
DeveloperHubShellScreen(child: child),
routes: [
GoRoute(
path: '/developers',
builder: (context, state) => const DeveloperHubScreen(),
),
GoRoute(
path: '/developers/:name/apps',
builder:
(context, state) => CustomAppsScreen(
publisherName: state.pathParameters['name']!,
),
),
GoRoute(
path: '/developers/:name/apps/new',
builder:
(context, state) => NewCustomAppScreen(
publisherName: state.pathParameters['name']!,
),
),
GoRoute(
path: '/developers/:name/apps/:id',
builder:
(context, state) => EditAppScreen(
publisherName: state.pathParameters['name']!,
id: state.pathParameters['id']!,
),
),
],
),
// Web articles
GoRoute(
path: '/feeds/articles',
builder: (context, state) => const ArticlesScreen(),
),
GoRoute(
path: '/feeds/articles/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ArticleDetailScreen(articleId: id);
},
),
// Auth routes
GoRoute(
@ -164,6 +250,10 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/settings',
builder: (context, state) => const SettingsScreen(),
),
GoRoute(
path: '/about',
builder: (context, state) => const AboutScreen(),
),
// Main tabs with TabsScreen shell
ShellRoute(
@ -173,56 +263,68 @@ final routerProvider = Provider<GoRouter>((ref) {
},
routes: [
// Explore tab
GoRoute(
path: '/',
builder: (context, state) => const ExploreScreen(),
ShellRoute(
builder:
(context, state, child) => ExploreShellScreen(child: child),
routes: [
GoRoute(
path: 'posts/:id',
path: '/',
builder: (context, state) => const ExploreScreen(),
),
GoRoute(
path: '/posts/search',
builder: (context, state) => const PostSearchScreen(),
),
GoRoute(
path: '/posts/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return PostDetailScreen(id: id);
},
),
GoRoute(
path: 'publishers/:name',
path: '/publishers/:name',
builder: (context, state) {
final name = state.pathParameters['name']!;
return PublisherProfileScreen(name: name);
},
),
GoRoute(
path: 'discovery/realms',
path: '/discovery/realms',
builder: (context, state) => const DiscoveryRealmsScreen(),
),
],
),
// Chat tab
GoRoute(
path: '/chat',
builder: (context, state) => const ChatListScreen(),
ShellRoute(
builder:
(context, state, child) => ChatShellScreen(child: child),
routes: [
GoRoute(
path: 'new',
path: '/chat',
builder: (context, state) => const ChatListScreen(),
),
GoRoute(
path: '/chat/new',
builder: (context, state) => const NewChatScreen(),
),
GoRoute(
path: ':id',
path: '/chat/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ChatRoomScreen(id: id);
},
),
GoRoute(
path: ':id/edit',
path: '/chat/:id/edit',
builder: (context, state) {
final id = state.pathParameters['id']!;
return EditChatScreen(id: id);
},
),
GoRoute(
path: ':id/detail',
path: '/chat/:id/detail',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ChatDetailScreen(id: id);
@ -258,39 +360,43 @@ final routerProvider = Provider<GoRouter>((ref) {
),
// Account tab
GoRoute(
path: '/account',
builder: (context, state) => const AccountScreen(),
ShellRoute(
builder:
(context, state, child) => AccountShellScreen(child: child),
routes: [
GoRoute(
path: 'notifications',
path: '/account',
builder: (context, state) => const AccountScreen(),
),
GoRoute(
path: '/account/notifications',
builder: (context, state) => const NotificationScreen(),
),
GoRoute(
path: 'wallet',
path: '/account/wallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: 'relationships',
path: '/account/relationships',
builder: (context, state) => const RelationshipScreen(),
),
GoRoute(
path: ':name',
path: '/account/:name',
builder: (context, state) {
final name = state.pathParameters['name']!;
return AccountProfileScreen(name: name);
},
),
GoRoute(
path: 'me/update',
path: '/account/me/update',
builder: (context, state) => const UpdateProfileScreen(),
),
GoRoute(
path: 'me/leveling',
path: '/account/me/leveling',
builder: (context, state) => const LevelingScreen(),
),
GoRoute(
path: 'settings',
path: '/account/settings',
builder: (context, state) => const AccountSettingsScreen(),
),
],

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

@ -0,0 +1,300 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutScreen extends StatefulWidget {
const AboutScreen({super.key});
@override
State<AboutScreen> createState() => _AboutScreenState();
}
class _AboutScreenState extends State<AboutScreen> {
PackageInfo _packageInfo = PackageInfo(
appName: 'Island',
packageName: 'com.example.island',
version: '1.0.0',
buildNumber: '1',
);
bool _isLoading = true;
String? _errorMessage;
@override
void initState() {
super.initState();
_initPackageInfo();
}
Future<void> _initPackageInfo() async {
try {
final info = await PackageInfo.fromPlatform();
if (mounted) {
setState(() {
_packageInfo = info;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = 'Failed to load package info: $e';
_isLoading = false;
});
}
}
}
Future<void> _launchURL(String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('About'), elevation: 0),
body:
_isLoading
? const Center(child: CircularProgressIndicator())
: _errorMessage != null
? Center(child: Text(_errorMessage!))
: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 24),
// App Icon and Name
CircleAvatar(
radius: 50,
backgroundColor: theme.colorScheme.primary.withOpacity(
0.1,
),
child: Image.asset(
'assets/icons/icon.png',
width: 56,
height: 56,
),
),
const SizedBox(height: 16),
Text(
_packageInfo.appName,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'Version ${_packageInfo.version} (${_packageInfo.buildNumber})',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.textTheme.bodySmall?.color,
),
),
const SizedBox(height: 32),
// App Info Card
_buildSection(
context,
title: 'App Information',
children: [
_buildInfoItem(
context,
icon: Icons.info_outline,
label: 'Package Name',
value: _packageInfo.packageName,
),
_buildInfoItem(
context,
icon: Icons.update,
label: 'Version',
value: _packageInfo.version,
),
_buildInfoItem(
context,
icon: Icons.build,
label: 'Build Number',
value: _packageInfo.buildNumber,
),
],
),
const SizedBox(height: 16),
// Links Card
_buildSection(
context,
title: 'Links',
children: [
_buildListTile(
context,
icon: Icons.privacy_tip_outlined,
title: 'Privacy Policy',
onTap:
() => _launchURL(
'https://solsynth.dev/terms/privacy-policy',
),
),
_buildListTile(
context,
icon: Icons.description_outlined,
title: 'Terms of Service',
onTap:
() => _launchURL(
'https://example.com/terms/basic-law',
),
),
_buildListTile(
context,
icon: Icons.code,
title: 'Open Source Licenses',
onTap: () {
showLicensePage(
context: context,
applicationName: _packageInfo.appName,
applicationVersion:
'Version ${_packageInfo.version}',
);
},
),
],
),
const SizedBox(height: 16),
// Developer Info
_buildSection(
context,
title: 'Developer',
children: [
_buildListTile(
context,
icon: Icons.email_outlined,
title: 'Contact Us',
subtitle: 'lily@solsynth.dev',
onTap: () => _launchURL('mailto:lily@solsynth.dev'),
),
_buildListTile(
context,
icon: Icons.copyright,
title: 'License',
subtitle:
'Copyright reserved © ${DateTime.now().year} Solsynth\nGNU Affero General Public License v3.0',
onTap:
() => _launchURL(
'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt',
),
),
],
),
const SizedBox(height: 32),
// Copyright
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'© ${DateTime.now().year} ${_packageInfo.appName}. All rights reserved.',
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
),
],
),
),
);
}
Widget _buildSection(
BuildContext context, {
required String title,
required List<Widget> children,
}) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
title,
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
const Divider(height: 1),
...children,
],
),
);
}
Widget _buildInfoItem(
BuildContext context, {
required IconData icon,
required String label,
required String value,
}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(icon, size: 20, color: Theme.of(context).hintColor),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: Theme.of(context).textTheme.bodySmall),
const SizedBox(height: 2),
SelectableText(
value,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
if (value.startsWith('http') || value.contains('@'))
IconButton(
icon: const Icon(Icons.copy, size: 16),
onPressed: () {
Clipboard.setData(ClipboardData(text: value));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Copied to clipboard')),
);
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
tooltip: 'Copy to clipboard',
),
],
),
);
}
Widget _buildListTile(
BuildContext context, {
required IconData icon,
required String title,
String? subtitle,
required VoidCallback onTap,
}) {
return Column(
children: [
ListTile(
leading: Icon(icon),
title: Text(title),
subtitle: subtitle != null ? Text(subtitle) : null,
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
minLeadingWidth: 24,
),
],
);
}
}

View File

@ -143,7 +143,7 @@ class AccountScreen extends HookConsumerWidget {
progress: user.value!.profile.levelingProgress,
),
onTap: () {
context.push('/account/leveling');
context.push('/account/me/leveling');
},
).padding(horizontal: 12),
Row(
@ -178,7 +178,9 @@ class AccountScreen extends HookConsumerWidget {
Text('developerPortalDescription').tr(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {},
onTap: () {
context.push('/developers');
},
),
).height(140),
),
@ -210,7 +212,7 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('wallet').tr(),
onTap: () {
context.push('/wallet');
context.push('/account/wallet');
},
),
ListTile(
@ -279,6 +281,16 @@ class AccountScreen extends HookConsumerWidget {
},
),
const Divider(height: 1).padding(vertical: 8),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.info),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('about').tr(),
onTap: () {
context.push('/about');
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.logout),

View File

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

View File

@ -53,17 +53,21 @@ Future<List<SnAccountBadge>> accountBadges(Ref ref, String uname) async {
@riverpod
Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
final account = await ref.watch(accountProvider(uname).future);
if (account.profile.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider(
CloudImageWidget.provider(
fileId: account.profile.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
final dominantColor = palette.dominantColor?.color;
if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
try {
final account = await ref.watch(accountProvider(uname).future);
if (account.profile.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider(
CloudImageWidget.provider(
fileId: account.profile.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
final dominantColor = palette.dominantColor?.color;
if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
} catch (_) {
return null;
}
}
@riverpod

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/custom_app.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'apps.g.dart';
@riverpod
Future<List<CustomApp>> customApps(Ref ref, String publisherName) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/developers/$publisherName/apps');
return resp.data.map((e) => CustomApp.fromJson(e)).cast<CustomApp>().toList();
}
class CustomAppsScreen extends HookConsumerWidget {
final String publisherName;
const CustomAppsScreen({super.key, required this.publisherName});
@override
Widget build(BuildContext context, WidgetRef ref) {
final apps = ref.watch(customAppsProvider(publisherName));
return AppScaffold(
appBar: AppBar(
title: Text('customApps').tr(),
actions: [
IconButton(
icon: const Icon(Symbols.add),
onPressed: () {
context.push('/developers/$publisherName/apps/new');
},
),
],
),
body: apps.when(
data: (data) {
if (data.isEmpty) {
return Center(child: Text('noCustomApps').tr());
}
return RefreshIndicator(
onRefresh:
() => ref.refresh(customAppsProvider(publisherName).future),
child: ListView.builder(
padding: EdgeInsets.only(top: 4),
itemCount: data.length,
itemBuilder: (context, index) {
final app = data[index];
return Card(
margin: const EdgeInsets.all(8.0),
child: Column(
children: [
SizedBox(
height: 150,
child: Stack(
fit: StackFit.expand,
children: [
if (app.background != null)
CloudFileWidget(
item: app.background!,
fit: BoxFit.cover,
).clipRRect(topLeft: 8, topRight: 8),
if (app.picture != null)
Positioned(
left: 16,
bottom: 16,
child: ProfilePictureWidget(
fileId: app.picture!.id,
radius: 40,
fallbackIcon: Symbols.apps,
),
),
],
),
),
ListTile(
title: Text(app.name),
subtitle: Text(
app.slug,
style: GoogleFonts.robotoMono(fontSize: 12),
),
contentPadding: EdgeInsets.only(left: 20, right: 12),
trailing: PopupMenuButton(
itemBuilder:
(context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Symbols.edit),
const SizedBox(width: 12),
Text('edit').tr(),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
const Icon(
Symbols.delete,
color: Colors.red,
),
const SizedBox(width: 12),
Text(
'delete',
style: TextStyle(color: Colors.red),
).tr(),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
context.push(
'/developers/$publisherName/apps/${app.id}',
);
} else if (value == 'delete') {
showConfirmAlert(
'deleteCustomAppHint'.tr(),
'deleteCustomApp'.tr(),
).then((confirm) {
if (confirm) {
final client = ref.read(apiClientProvider);
client.delete(
'/developers/$publisherName/apps/${app.id}',
);
ref.invalidate(
customAppsProvider(publisherName),
);
}
});
}
},
),
),
],
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(customAppsProvider(publisherName)),
),
),
);
}
}

View File

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

View File

@ -0,0 +1,558 @@
import 'package:croppy/croppy.dart' hide cropImage;
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/custom_app.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/developers/apps.dart';
import 'package:island/services/file.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/widgets/content/sheet.dart';
part 'edit_app.g.dart';
@riverpod
Future<CustomApp?> customApp(Ref ref, String publisherName, String id) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/developers/$publisherName/apps/$id');
return CustomApp.fromJson(resp.data);
}
class EditAppScreen extends HookConsumerWidget {
final String publisherName;
final String? id;
const EditAppScreen({super.key, required this.publisherName, this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null;
final app = isNew ? null : ref.watch(customAppProvider(publisherName, id!));
final formKey = useMemoized(() => GlobalKey<FormState>());
final submitting = useState(false);
final nameController = useTextEditingController();
final slugController = useTextEditingController();
final descriptionController = useTextEditingController();
final picture = useState<SnCloudFile?>(null);
final background = useState<SnCloudFile?>(null);
final enableLinks = useState(false); // Only for UI purposes
final homePageController = useTextEditingController();
final privacyPolicyController = useTextEditingController();
final termsController = useTextEditingController();
final oauthEnabled = useState(false);
final redirectUris = useState<List<String>>([]);
final postLogoutUris = useState<List<String>>([]);
final allowedScopes = useState<List<String>>([
'openid',
'profile',
'email',
]);
final allowedGrantTypes = useState<List<String>>([
'authorization_code',
'refresh_token',
]);
final requirePkce = useState(true);
final allowOfflineAccess = useState(false);
useEffect(() {
if (app?.value != null) {
nameController.text = app!.value!.name;
slugController.text = app.value!.slug;
descriptionController.text = app.value!.description ?? '';
picture.value = app.value!.picture;
background.value = app.value!.background;
homePageController.text = app.value!.links?.homePage ?? '';
privacyPolicyController.text = app.value!.links?.privacyPolicy ?? '';
termsController.text = app.value!.links?.termsOfService ?? '';
if (app.value!.oauthConfig != null) {
oauthEnabled.value = true;
redirectUris.value = app.value!.oauthConfig!.redirectUris;
postLogoutUris.value =
app.value!.oauthConfig!.postLogoutRedirectUris ?? [];
allowedScopes.value = app.value!.oauthConfig!.allowedScopes;
allowedGrantTypes.value = app.value!.oauthConfig!.allowedGrantTypes;
requirePkce.value = app.value!.oauthConfig!.requirePkce;
allowOfflineAccess.value = app.value!.oauthConfig!.allowOfflineAccess;
}
}
return null;
}, [app]);
void setPicture(String position) async {
showLoadingModal(context);
var result = await ref
.read(imagePickerProvider)
.pickImage(source: ImageSource.gallery);
if (result == null) {
if (context.mounted) hideLoadingModal(context);
return;
}
if (!context.mounted) return;
hideLoadingModal(context);
result = await cropImage(
context,
image: result,
allowedAspectRatios: [
if (position == 'background')
const CropAspectRatio(height: 7, width: 16)
else
const CropAspectRatio(height: 1, width: 1),
],
);
if (result == null) {
if (context.mounted) hideLoadingModal(context);
return;
}
if (!context.mounted) return;
showLoadingModal(context);
submitting.value = true;
try {
final baseUrl = ref.watch(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile =
await putMediaToCloud(
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
atk: token,
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
switch (position) {
case 'picture':
picture.value = cloudFile;
case 'background':
background.value = cloudFile;
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
submitting.value = false;
}
}
void showAddScopeDialog() {
final scopeController = TextEditingController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'addScope'.tr(),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: scopeController,
decoration: InputDecoration(labelText: 'scopeName'.tr()),
),
const SizedBox(height: 20),
FilledButton.tonalIcon(
onPressed: () {
if (scopeController.text.isNotEmpty) {
allowedScopes.value = [
...allowedScopes.value,
scopeController.text,
];
Navigator.pop(context);
}
},
icon: const Icon(Symbols.add),
label: Text('add').tr(),
),
],
),
),
),
);
}
void showAddRedirectUriDialog() {
final uriController = TextEditingController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'addRedirectUri'.tr(),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: uriController,
decoration: InputDecoration(
labelText: 'redirectUri'.tr(),
hintText: 'https://example.com/auth/callback',
helperText: 'redirectUriHint'.tr(),
helperMaxLines: 3,
),
keyboardType: TextInputType.url,
validator: (value) {
if (value == null || value.isEmpty) {
return 'uriRequired'.tr();
}
final uri = Uri.tryParse(value);
if (uri == null || !uri.hasAbsolutePath) {
return 'invalidUri'.tr();
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 20),
FilledButton.tonalIcon(
onPressed: () {
if (uriController.text.isNotEmpty) {
redirectUris.value = [
...redirectUris.value,
uriController.text,
];
Navigator.pop(context);
}
},
icon: const Icon(Symbols.add),
label: Text('add').tr(),
),
],
),
),
),
);
}
void performAction() async {
final client = ref.read(apiClientProvider);
final data = {
'name': nameController.text,
'slug': slugController.text,
'description': descriptionController.text,
'picture_id': picture.value?.id,
'background_id': background.value?.id,
'links': {
'home_page':
homePageController.text.isNotEmpty
? homePageController.text
: null,
'privacy_policy':
privacyPolicyController.text.isNotEmpty
? privacyPolicyController.text
: null,
'terms_of_service':
termsController.text.isNotEmpty ? termsController.text : null,
},
'oauth_config':
oauthEnabled.value
? {
'redirect_uris': redirectUris.value,
'post_logout_redirect_uris':
postLogoutUris.value.isNotEmpty
? postLogoutUris.value
: null,
'allowed_scopes': allowedScopes.value,
'allowed_grant_types': allowedGrantTypes.value,
'require_pkce': requirePkce.value,
'allow_offline_access': allowOfflineAccess.value,
}
: null,
};
if (isNew) {
await client.post('/developers/$publisherName/apps', data: data);
} else {
await client.patch('/developers/$publisherName/apps/$id', data: data);
}
ref.invalidate(customAppsProvider(publisherName));
if (context.mounted) {
Navigator.pop(context);
}
}
return AppScaffold(
appBar: AppBar(
title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
),
body:
app == null && !isNew
? const Center(child: CircularProgressIndicator())
: app?.hasError == true && !isNew
? ResponseErrorWidget(
error: app!.error,
onRetry:
() => ref.invalidate(customAppProvider(publisherName, id!)),
)
: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
GestureDetector(
child: Container(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: picture.value?.id,
radius: 40,
fallbackIcon: Symbols.apps,
),
onTap: () {
setPicture('picture');
},
),
),
],
),
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(labelText: 'name'.tr()),
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
),
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
alignLabelWithHint: true,
),
maxLines: 3,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
const SizedBox(height: 16),
ExpansionPanelList(
expansionCallback: (index, isExpanded) {
switch (index) {
case 0:
enableLinks.value = isExpanded;
break;
case 1:
oauthEnabled.value = isExpanded;
break;
}
},
children: [
ExpansionPanel(
headerBuilder:
(context, isExpanded) =>
ListTile(title: Text('appLinks').tr()),
body: Column(
spacing: 16,
children: [
TextFormField(
controller: homePageController,
decoration: InputDecoration(
labelText: 'homePageUrl'.tr(),
hintText: 'https://example.com',
),
keyboardType: TextInputType.url,
),
TextFormField(
controller: privacyPolicyController,
decoration: InputDecoration(
labelText: 'privacyPolicyUrl'.tr(),
hintText: 'https://example.com/privacy',
),
keyboardType: TextInputType.url,
),
TextFormField(
controller: termsController,
decoration: InputDecoration(
labelText: 'termsOfServiceUrl'.tr(),
hintText: 'https://example.com/terms',
),
keyboardType: TextInputType.url,
),
],
).padding(horizontal: 16, bottom: 24),
isExpanded: enableLinks.value,
),
ExpansionPanel(
headerBuilder:
(context, isExpanded) => ListTile(
title: Text('oauthConfig').tr(),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('redirectUris'.tr()),
Card(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: Column(
children: [
...redirectUris.value.map(
(uri) => ListTile(
title: Text(uri),
trailing: IconButton(
icon: const Icon(
Symbols.delete,
),
onPressed: () {
redirectUris.value =
redirectUris.value
.where(
(u) => u != uri,
)
.toList();
},
),
),
),
if (redirectUris.value.isNotEmpty)
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.add),
title: Text('addRedirectUri'.tr()),
onTap: showAddRedirectUriDialog,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(8),
),
),
],
),
),
const SizedBox(height: 16),
Text('allowedScopes'.tr()),
Card(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: Column(
children: [
...allowedScopes.value.map(
(scope) => ListTile(
title: Text(scope),
trailing: IconButton(
icon: const Icon(
Symbols.delete,
),
onPressed: () {
allowedScopes.value =
allowedScopes.value
.where(
(s) => s != scope,
)
.toList();
},
),
),
),
if (allowedScopes.value.isNotEmpty)
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.add),
title: Text('add').tr(),
onTap: showAddScopeDialog,
),
],
),
),
const SizedBox(height: 16),
SwitchListTile(
title: Text('requirePkce'.tr()),
value: requirePkce.value,
onChanged:
(value) => requirePkce.value = value,
),
SwitchListTile(
title: Text('allowOfflineAccess'.tr()),
value: allowOfflineAccess.value,
onChanged:
(value) =>
allowOfflineAccess.value = value,
),
],
).padding(horizontal: 16, bottom: 24),
isExpanded: oauthEnabled.value,
),
],
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed:
submitting.value ? null : performAction,
label: Text('saveChanges'.tr()),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
],
),
),
);
}
}

View File

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

View File

@ -0,0 +1,380 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/developer.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'hub.g.dart';
@riverpod
Future<DeveloperStats?> developerStats(Ref ref, String? uname) async {
if (uname == null) return null;
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/developers/$uname/stats');
return DeveloperStats.fromJson(resp.data);
}
@riverpod
Future<List<SnPublisher>> developers(Ref ref) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/developers');
return resp.data
.map((e) => SnPublisher.fromJson(e))
.cast<SnPublisher>()
.toList();
}
class DeveloperHubShellScreen extends StatelessWidget {
final Widget child;
const DeveloperHubShellScreen({super.key, required this.child});
@override
Widget build(BuildContext context) {
final isWide = isWideScreen(context);
if (isWide) {
return Row(
children: [
SizedBox(width: 360, child: const DeveloperHubScreen(isAside: true)),
const VerticalDivider(width: 1),
Expanded(child: child),
],
);
}
return child;
}
}
class DeveloperHubScreen extends HookConsumerWidget {
final bool isAside;
const DeveloperHubScreen({super.key, this.isAside = false});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isWide = isWideScreen(context);
if (isWide && !isAside) {
return Container(color: Theme.of(context).colorScheme.surface);
}
final developers = ref.watch(developersProvider);
final currentDeveloper = useState<SnPublisher?>(
developers.value?.firstOrNull,
);
final List<DropdownMenuItem<SnPublisher>> developersMenu = developers.when(
data:
(data) =>
data
.map(
(item) => DropdownMenuItem<SnPublisher>(
value: item,
child: ListTile(
minTileHeight: 48,
leading: ProfilePictureWidget(
radius: 16,
fileId: item.picture?.id,
),
title: Text(item.nick),
subtitle: Text('@${item.name}'),
trailing:
currentDeveloper.value?.id == item.id
? const Icon(Icons.check)
: null,
contentPadding: EdgeInsets.symmetric(horizontal: 8),
),
),
)
.toList(),
loading: () => [],
error: (_, _) => [],
);
final developerStats = ref.watch(
developerStatsProvider(currentDeveloper.value?.name),
);
return AppScaffold(
noBackground: false,
appBar: AppBar(
leading: !isWide ? const PageBackButton() : null,
title: Text('developerHub').tr(),
actions: [
DropdownButtonHideUnderline(
child: DropdownButton2<SnPublisher>(
alignment: Alignment.centerRight,
value: currentDeveloper.value,
hint: CircleAvatar(
radius: 16,
child: Icon(
Symbols.person,
color: Theme.of(
context,
).colorScheme.onSecondaryContainer.withOpacity(0.9),
fill: 1,
),
).center().padding(right: 8),
items: [...developersMenu],
onChanged: (value) {
currentDeveloper.value = value;
},
selectedItemBuilder: (context) {
return [
...developersMenu.map(
(e) => ProfilePictureWidget(
radius: 16,
fileId: e.value?.picture?.id,
).center().padding(right: 8),
),
];
},
buttonStyleData: ButtonStyleData(
height: 40,
padding: const EdgeInsets.only(left: 14, right: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
),
),
dropdownStyleData: DropdownStyleData(
width: 320,
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
),
),
menuItemStyleData: const MenuItemStyleData(
height: 64,
padding: EdgeInsets.only(left: 14, right: 14),
),
iconStyleData: IconStyleData(
icon: Icon(Icons.arrow_drop_down),
iconSize: 19,
iconEnabledColor:
Theme.of(context).appBarTheme.foregroundColor!,
iconDisabledColor:
Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
const Gap(8),
],
),
body: developerStats.when(
data:
(stats) => SingleChildScrollView(
child:
currentDeveloper.value == null
? Column(
children: [
const Gap(24),
const Icon(Symbols.info, size: 32).padding(bottom: 4),
Text(
'developerHubUnselectedHint',
textAlign: TextAlign.center,
).tr(),
const Gap(24),
const Divider(height: 1),
...(developers.value?.map(
(developer) => ListTile(
leading: ProfilePictureWidget(
file: developer.picture,
),
title: Text(developer.nick),
subtitle: Text('@${developer.name}'),
onTap: () {
currentDeveloper.value = developer;
},
),
) ??
[]),
ListTile(
leading: const CircleAvatar(
child: Icon(Symbols.add),
),
title: Text('enrollDeveloper').tr(),
subtitle: Text('enrollDeveloperHint').tr(),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(_) => const _DeveloperEnrollmentSheet(),
).then((value) {
if (value == true) {
ref.invalidate(developersProvider);
}
});
},
),
],
)
: Column(
children: [
if (stats != null)
_DeveloperStatsWidget(
stats: stats,
).padding(vertical: 12, horizontal: 12),
ListTile(
minTileHeight: 48,
title: Text('customApps').tr(),
trailing: Icon(Symbols.chevron_right),
leading: const Icon(Symbols.apps),
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
onTap: () {
context.push(
'/developers/${currentDeveloper.value!.name}/apps',
);
},
),
],
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry: () {
ref.invalidate(
developerStatsProvider(currentDeveloper.value?.name),
);
},
),
),
);
}
}
class _DeveloperStatsWidget extends StatelessWidget {
final DeveloperStats stats;
const _DeveloperStatsWidget({required this.stats});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
spacing: 8,
children: [
Row(
spacing: 8,
children: [
Expanded(
child: _buildStatsCard(
context,
stats.totalCustomApps.toString(),
'totalCustomApps',
),
),
],
),
],
),
);
}
Widget _buildStatsCard(
BuildContext context,
String statValue,
String statLabel,
) {
return Card(
margin: EdgeInsets.zero,
child: SizedBox(
height: 100,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
statValue,
style: Theme.of(context).textTheme.headlineMedium,
),
const Gap(4),
Text(
statLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
).tr(),
],
),
),
),
);
}
}
class _DeveloperEnrollmentSheet extends HookConsumerWidget {
const _DeveloperEnrollmentSheet();
@override
Widget build(BuildContext context, WidgetRef ref) {
final publishers = ref.watch(publishersManagedProvider);
Future<void> enroll(SnPublisher publisher) async {
try {
final client = ref.read(apiClientProvider);
await client.post('/developers/${publisher.name}/enroll');
if (context.mounted) {
Navigator.pop(context, true);
}
} catch (err) {
showErrorAlert(err);
}
}
return SheetScaffold(
titleText: 'enrollDeveloper'.tr(),
child: publishers.when(
data:
(items) =>
items.isEmpty
? Center(
child:
Text(
'noPublishersToEnroll',
textAlign: TextAlign.center,
).tr(),
)
: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final publisher = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: publisher.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
onTap: () => enroll(publisher),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(publishersManagedProvider),
),
),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/realm.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
@ -20,6 +22,7 @@ import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/realm/realm_card.dart';
import 'package:island/widgets/publisher/publisher_card.dart';
import 'package:island/widgets/web_article_card.dart';
import 'package:styled_widget/styled_widget.dart';
part 'explore.g.dart';
@ -90,37 +93,69 @@ class ExploreScreen extends HookConsumerWidget {
extendBody: false, // Prevent conflicts with tabs navigation
appBar: AppBar(
toolbarHeight: 0,
bottom: TabBar(
controller: tabController,
tabs: [
Tab(
child: Text(
'explore'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: Row(
children: [
Expanded(
child: TabBar(
controller: tabController,
tabAlignment: TabAlignment.start,
isScrollable: true,
tabs: [
Tab(
icon: Tooltip(
message: 'explore'.tr(),
child: Icon(
Symbols.explore,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
icon: Tooltip(
message: 'exploreFilterSubscriptions'.tr(),
child: Icon(
Symbols.subscriptions,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
icon: Tooltip(
message: 'exploreFilterFriends'.tr(),
child: Icon(
Symbols.people,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),
),
Tab(
child: Text(
'exploreFilterSubscriptions'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
Spacer(),
IconButton(
onPressed: () {
context.push('/feeds/articles');
},
icon: Icon(
Symbols.auto_stories,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
tooltip: 'webArticlesStand'.tr(),
),
),
Tab(
child: Text(
'exploreFilterFriends'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
IconButton(
onPressed: () {
context.push('/posts/search');
},
icon: Icon(
Symbols.search,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
tooltip: 'search'.tr(),
),
),
],
],
).padding(horizontal: 8),
),
),
floatingActionButton: FloatingActionButton(
@ -195,6 +230,7 @@ class _DiscoveryActivityItem extends StatelessWidget {
(switch (type) {
'realm' => 'discoverRealms',
'publisher' => 'discoverPublishers',
'article' => 'discoverWebArticles',
_ => 'unknown',
}).tr(),
style: Theme.of(context).textTheme.titleMedium,
@ -220,6 +256,11 @@ class _DiscoveryActivityItem extends StatelessWidget {
publisher: SnPublisher.fromJson(item['data']),
maxWidth: 280,
);
case 'article':
return WebArticleCard(
article: SnWebArticle.fromJson(item['data']),
maxWidth: 280,
);
default:
return Placeholder();
}
@ -341,7 +382,7 @@ class ActivityListNotifier extends _$ActivityListNotifier
if (cursor != null) 'cursor': cursor,
'take': take,
if (filter != null) 'filter': filter,
if (kDebugMode) 'debugInclude': 'realms,publishers',
if (kDebugMode) 'debugInclude': 'realms,publishers,articles',
};
final response = await client.get(

View File

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

View File

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

View File

@ -15,7 +15,7 @@ import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/compose_shared.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/publishers_modal.dart';
import 'package:island/screens/posts/detail.dart';
import 'package:island/screens/posts/post_detail.dart';
import 'package:island/widgets/post/compose_settings_sheet.dart';
import 'package:island/services/compose_storage_db.dart';
import 'package:island/widgets/post/draft_manager.dart';

View File

@ -12,7 +12,7 @@ import 'package:island/models/post.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/screens/posts/detail.dart';
import 'package:island/screens/posts/post_detail.dart';
import 'package:island/widgets/content/attachment_preview.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown.dart';

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';

View File

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

View File

@ -22,7 +22,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
part 'detail.g.dart';
part 'realm_detail.g.dart';
@riverpod
Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async {

View File

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

View File

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

View File

@ -341,6 +341,25 @@ class SettingsScreen extends HookConsumerWidget {
];
final behaviorSettings = [
ListTile(
minLeadingWidth: 48,
title: Text('creatorHub').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.rocket_launch),
trailing: const Icon(Symbols.chevron_right),
onTap: () => context.push('/creators'),
),
// Developer Hub
ListTile(
minLeadingWidth: 48,
title: Text('developerHub').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.hub),
trailing: const Icon(Symbols.chevron_right),
onTap: () => context.push('/developers'),
),
// Auto translate settings
ListTile(
minLeadingWidth: 48,

View File

@ -0,0 +1 @@

View File

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

View File

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

View File

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

View File

@ -74,9 +74,7 @@ class MarkdownTextContent extends HookConsumerWidget {
final url = Uri.tryParse(href);
if (url != null) {
if (url.scheme == 'solian') {
context.push(
['', url.host, ...url.pathSegments].join('/'),
);
context.push(['', url.host, ...url.pathSegments].join('/'));
return;
}
final whitelistDomains = ['solian.app', 'solsynth.dev'];
@ -143,17 +141,27 @@ class MarkdownTextContent extends HookConsumerWidget {
),
],
),
generator: MarkdownGenerator(
generators: [latexGenerator],
inlineSyntaxList: [
_UserNameCardInlineSyntax(),
_StickerInlineSyntax(),
LatexSyntax(isDark),
],
linesMargin: linesMargin ?? EdgeInsets.symmetric(vertical: 4),
generator: MarkdownTextContent.buildGenerator(
isDark: isDark,
linesMargin: linesMargin,
),
);
}
static MarkdownGenerator buildGenerator({
bool isDark = false,
EdgeInsets? linesMargin,
}) {
return MarkdownGenerator(
generators: [latexGenerator],
inlineSyntaxList: [
_UserNameCardInlineSyntax(),
_StickerInlineSyntax(),
LatexSyntax(isDark),
],
linesMargin: linesMargin ?? EdgeInsets.symmetric(vertical: 4),
);
}
}
class _UserNameCardInlineSyntax extends markdown.InlineSyntax {

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/file.dart';

View File

@ -362,6 +362,7 @@ class PostItem extends HookConsumerWidget {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => PostRepliesSheet(post: item),
);
}
@ -535,20 +536,20 @@ Widget _buildReferencePost(BuildContext context, SnPost item) {
),
],
),
).gestures(onTap: () => context.push('/posts/referencePost.id'));
).gestures(onTap: () => context.push('/posts/${referencePost.id}'));
}
class PostReactionList extends HookConsumerWidget {
final String parentId;
final Map<String, int> reactions;
final Function(String symbol, int attitude, int delta) onReact;
final Function(String symbol, int attitude, int delta)? onReact;
final EdgeInsets? padding;
const PostReactionList({
super.key,
required this.parentId,
required this.reactions,
this.padding,
required this.onReact,
this.onReact,
});
@override
@ -569,7 +570,7 @@ class PostReactionList extends HookConsumerWidget {
})
.then((resp) {
var isRemoving = resp.statusCode == 204;
onReact(symbol, attitude, isRemoving ? -1 : 1);
onReact?.call(symbol, attitude, isRemoving ? -1 : 1);
HapticFeedback.heavyImpact();
});
submitting.value = false;
@ -581,33 +582,34 @@ class PostReactionList extends HookConsumerWidget {
scrollDirection: Axis.horizontal,
padding: padding ?? EdgeInsets.zero,
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: ActionChip(
avatar: Icon(Symbols.add_reaction),
label: Text('react').tr(),
visualDensity: const VisualDensity(
horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity,
if (onReact != null)
Padding(
padding: const EdgeInsets.only(right: 8),
child: ActionChip(
avatar: Icon(Symbols.add_reaction),
label: Text('react').tr(),
visualDensity: const VisualDensity(
horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity,
),
onPressed:
submitting.value
? null
: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return _PostReactionSheet(
reactionsCount: reactions,
onReact: (symbol, attitude) {
reactPost(symbol, attitude);
},
);
},
);
},
),
onPressed:
submitting.value
? null
: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return _PostReactionSheet(
reactionsCount: reactions,
onReact: (symbol, attitude) {
reactPost(symbol, attitude);
},
);
},
);
},
),
),
for (final symbol in reactions.keys)
Padding(
padding: const EdgeInsets.only(right: 8),

View File

@ -365,11 +365,6 @@ class PostItemCreator extends HookConsumerWidget {
parentId: item.id,
reactions: item.reactionsCount,
padding: EdgeInsets.zero,
onReact: (symbol, attitude, delta) {
final reactionsCount = Map<String, int>.from(item.reactionsCount);
reactionsCount[symbol] = (reactionsCount[symbol] ?? 0) + delta;
onUpdate?.call(item.copyWith(reactionsCount: reactionsCount));
},
),
const Gap(16),
],

View File

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

View File

@ -2,7 +2,7 @@ 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/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/widgets/content/cloud_files.dart';
class PublisherCard extends ConsumerWidget {

View File

@ -11,7 +11,6 @@ import 'package:island/models/file.dart';
import 'package:island/pods/link_preview.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/file.dart';
import 'package:mime/mime.dart';
@ -193,7 +192,6 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
setState(() => _isLoading = true);
try {
final apiClient = ref.read(apiClientProvider);
final userInfo = ref.read(userInfoProvider.notifier);
final serverUrl = ref.read(serverUrlProvider);
String content = _messageController.text.trim();
@ -218,7 +216,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
case ShareContentType.file:
// Upload files to cloud storage
if (widget.content.files?.isNotEmpty == true) {
final token = await userInfo.getAccessToken();
final token = ref.watch(tokenProvider)?.token;
if (token == null) {
throw Exception('Authentication required');
}

View File

@ -0,0 +1,139 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/services/time.dart';
class WebArticleCard extends StatelessWidget {
final SnWebArticle article;
final double? maxWidth;
final bool showDetails;
const WebArticleCard({
super.key,
required this.article,
this.maxWidth,
this.showDetails = false,
});
void _onTap(BuildContext context) {
context.push('/feeds/articles/${article.id}');
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => _onTap(context),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
fit: StackFit.expand,
children: [
// Image or fallback
article.preview?.imageUrl != null
? CachedNetworkImage(
imageUrl: article.preview!.imageUrl!,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
)
: ColoredBox(
color: colorScheme.secondaryContainer,
child: const Center(
child: Icon(
Icons.article_outlined,
size: 48,
color: Colors.white,
),
),
),
// Gradient overlay
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.7),
],
),
),
),
// Title
Align(
alignment: Alignment.bottomLeft,
child: Container(
padding: const EdgeInsets.only(
left: 12,
right: 12,
bottom: 8,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
if (showDetails)
const SizedBox(height: 8)
else
Spacer(),
Text(
article.title,
style: theme.textTheme.titleSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
height: 1.3,
),
maxLines: showDetails ? 3 : 2,
overflow: TextOverflow.ellipsis,
),
if (showDetails &&
article.author?.isNotEmpty == true) ...[
const SizedBox(height: 4),
Text(
article.author!,
style: TextStyle(
fontSize: 10,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w500,
),
),
],
if (showDetails) const Spacer(),
if (showDetails && article.publishedAt != null) ...[
Text(
'${article.publishedAt!.formatSystem()} · ${article.publishedAt!.formatRelative(context)}',
style: const TextStyle(
fontSize: 9,
color: Colors.white70,
),
),
const SizedBox(height: 2),
],
Text(
article.feed?.title ?? 'Unknown Source',
style: const TextStyle(
fontSize: 9,
color: Colors.white70,
),
),
],
),
),
),
],
),
),
),
),
);
}
}

View File

@ -11,32 +11,32 @@ PODS:
- FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- Firebase/CoreOnly (11.13.0):
- FirebaseCore (~> 11.13.0)
- Firebase/Messaging (11.13.0):
- Firebase/CoreOnly (11.15.0):
- FirebaseCore (~> 11.15.0)
- Firebase/Messaging (11.15.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.13.0)
- firebase_core (3.14.0):
- Firebase/CoreOnly (~> 11.13.0)
- FirebaseMessaging (~> 11.15.0)
- firebase_core (3.15.0):
- Firebase/CoreOnly (~> 11.15.0)
- FlutterMacOS
- firebase_messaging (15.2.7):
- Firebase/CoreOnly (~> 11.13.0)
- Firebase/Messaging (~> 11.13.0)
- firebase_messaging (15.2.8):
- Firebase/CoreOnly (~> 11.15.0)
- Firebase/Messaging (~> 11.15.0)
- firebase_core
- FlutterMacOS
- FirebaseCore (11.13.0):
- FirebaseCoreInternal (~> 11.13.0)
- FirebaseCore (11.15.0):
- FirebaseCoreInternal (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (11.13.0):
- FirebaseCoreInternal (11.15.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (11.13.0):
- FirebaseCore (~> 11.13.0)
- FirebaseInstallations (11.15.0):
- FirebaseCore (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.13.0):
- FirebaseCore (~> 11.13.0)
- FirebaseMessaging (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
@ -92,7 +92,7 @@ PODS:
- GoogleUtilities/Privacy
- irondash_engine_context (0.0.1):
- FlutterMacOS
- livekit_client (2.4.8):
- livekit_client (2.4.9):
- flutter_webrtc
- FlutterMacOS
- WebRTC-SDK (= 125.6422.07)
@ -291,13 +291,13 @@ SPEC CHECKSUMS:
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
Firebase: 3435bc66b4d494c2f22c79fd3aae4c1db6662327
firebase_core: 1095fcf33161d99bc34aa10f7c0d89414a208d15
firebase_messaging: 6417056ffb85141607618ddfef9fec9f3caab3ea
FirebaseCore: c692c7f1c75305ab6aff2b367f25e11d73aa8bd0
FirebaseCoreInternal: 29d7b3af4aaf0b8f3ed20b568c13df399b06f68c
FirebaseInstallations: 0ee9074f2c1e86561ace168ee1470dc67aabaf02
FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_core: 177f51be1650b15d2d5b9f1abf48792619288070
firebase_messaging: 8748a5d4bb435993cffa7f5501292f3e914a23d7
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
@ -309,7 +309,7 @@ SPEC CHECKSUMS:
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
livekit_client: 6a35243df3da61750c98e266e02dedcf5d25c888
livekit_client: c9d9f41996f5cf22b9ba0e8483e6af4ca5094059
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: dda4fd7909a732a014239009aa52537b136f8ce568de23c212587097887e2307
sha256: "50e24b769bd1e725732f0aff18b806b8731c1fbcf4e8018ab98e7c4805a2a52f"
url: "https://pub.dev"
source: hosted
version: "1.3.56"
version: "1.3.57"
analyzer:
dependency: transitive
description:
@ -629,50 +629,50 @@ packages:
dependency: "direct main"
description:
name: firebase_core
sha256: "420d9111dcf095341f1ea8fdce926eef750cf7b9745d21f38000de780c94f608"
sha256: "5bba5924139e91d26446fd2601c18a6aa62c1161c768a989bb5e245dcdc20644"
url: "https://pub.dev"
source: hosted
version: "3.14.0"
version: "3.15.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf
sha256: "5d2ab45779d91af2aa0252dec9fe4ee1caa015d83377de255454dcaa1526a0e0"
url: "https://pub.dev"
source: hosted
version: "5.4.0"
version: "5.4.1"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: ddd72baa6f727e5b23f32d9af23d7d453d67946f380bd9c21daf474ee0f7326e
sha256: eb3afccfc452b2b2075acbe0c4b27de62dd596802b4e5e19869c1e926cbb20b3
url: "https://pub.dev"
source: hosted
version: "2.23.0"
version: "2.24.0"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "758461f67b96aa5ad27625aaae39882fd6d1961b1c7e005301f9a74b6336100b"
sha256: c6711cf2f455532b84a94022c7aaf85088849763af2f01b775ca79d82d10a01a
url: "https://pub.dev"
source: hosted
version: "15.2.7"
version: "15.2.8"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "614db1b0df0f53e541e41cc182b6d7ede5763c400f6ba232a5f8d0e1b5e5de32"
sha256: "1c9dacccb1aee1bf17ba519dda5563a16fdd2ec1e79b5f2e421cb4bf75a166f7"
url: "https://pub.dev"
source: hosted
version: "4.6.7"
version: "4.6.8"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: b5fbbcdd3e0e7f3fde72b0c119410f22737638fed5fc428b54bba06bc1455d81
sha256: "54317c26fa92f0d90a2017977ac791cb0504eca29fcf397f06adf727d4a7a2d5"
url: "https://pub.dev"
source: hosted
version: "3.10.7"
version: "3.10.8"
fixnum:
dependency: transitive
description:
@ -798,6 +798,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
flutter_keyboard_visibility:
dependency: transitive
description:
name: flutter_keyboard_visibility
sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_keyboard_visibility_linux:
dependency: transitive
description:
name: flutter_keyboard_visibility_linux
sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_keyboard_visibility_macos:
dependency: transitive
description:
name: flutter_keyboard_visibility_macos
sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_keyboard_visibility_platform_interface:
dependency: transitive
description:
name: flutter_keyboard_visibility_platform_interface
sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4
url: "https://pub.dev"
source: hosted
version: "2.0.0"
flutter_keyboard_visibility_web:
dependency: transitive
description:
name: flutter_keyboard_visibility_web
sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1
url: "https://pub.dev"
source: hosted
version: "2.0.0"
flutter_keyboard_visibility_windows:
dependency: transitive
description:
name: flutter_keyboard_visibility_windows
sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
@ -960,6 +1008,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.1"
flutter_typeahead:
dependency: "direct main"
description:
name: flutter_typeahead
sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d
url: "https://pub.dev"
source: hosted
version: "5.2.0"
flutter_udid:
dependency: "direct main"
description:
@ -977,10 +1033,10 @@ packages:
dependency: "direct main"
description:
name: flutter_webrtc
sha256: dd47ca103b5b6217771e6277882674276d9621bbf9eb23da3c03898b507844e3
sha256: "792aa1e5838a719f29ae52c0773dbb5dd781fc33b1bf87c321b274e55ab51ad1"
url: "https://pub.dev"
source: hosted
version: "0.14.1"
version: "0.14.2"
font_awesome_flutter:
dependency: transitive
description:
@ -1093,6 +1149,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.15.6"
html2md:
dependency: "direct main"
description:
name: html2md
sha256: "465cf8ffa1b510fe0e97941579bf5b22e2d575f2cecb500a9c0254efe33a8036"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
http:
dependency: transitive
description:
@ -1297,10 +1361,10 @@ packages:
dependency: "direct main"
description:
name: livekit_client
sha256: c270720a49b935591960c6f3296fd8f00c09b45a70cd64aef78cd0a8f8257913
sha256: "5d182f40cc9aafce60a9acf936bad8bc69010b5cbf0a949f6f27dc4390f2fcce"
url: "https://pub.dev"
source: hosted
version: "2.4.8"
version: "2.4.9"
local_auth:
dependency: "direct main"
description:
@ -1677,6 +1741,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointer_interceptor:
dependency: transitive
description:
name: pointer_interceptor
sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523"
url: "https://pub.dev"
source: hosted
version: "0.10.1+2"
pointer_interceptor_ios:
dependency: transitive
description:
name: pointer_interceptor_ios
sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917
url: "https://pub.dev"
source: hosted
version: "0.10.1"
pointer_interceptor_platform_interface:
dependency: transitive
description:
name: pointer_interceptor_platform_interface
sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506"
url: "https://pub.dev"
source: hosted
version: "0.10.0+1"
pointer_interceptor_web:
dependency: transitive
description:
name: pointer_interceptor_web
sha256: "460b600e71de6fcea2b3d5f662c92293c049c4319e27f0829310e5a953b3ee2a"
url: "https://pub.dev"
source: hosted
version: "0.10.3"
pool:
dependency: transitive
description:
@ -1689,10 +1785,10 @@ packages:
dependency: transitive
description:
name: posix
sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
url: "https://pub.dev"
source: hosted
version: "6.0.2"
version: "6.0.3"
protobuf:
dependency: transitive
description:
@ -1809,10 +1905,10 @@ packages:
dependency: transitive
description:
name: record_web
sha256: "024c81eb7f51468b1833a3eca8b461c7ca25c04899dba37abe580bb57afd32e4"
sha256: a12856d0b3dd03d336b4b10d7520a8b3e21649a06a8f95815318feaa8f07adbb
url: "https://pub.dev"
source: hosted
version: "1.1.8"
version: "1.1.9"
record_windows:
dependency: transitive
description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 3.0.0+108
version: 3.0.0+110
environment:
sdk: ^3.7.2
@ -37,7 +37,7 @@ dependencies:
flutter_hooks: ^0.21.2
hooks_riverpod: ^2.6.1
bitsdojo_window: ^0.1.6
go_router: ^15.2.4
go_router: ^15.1.3
styled_widget: ^0.4.1
shared_preferences: ^2.5.3
flutter_riverpod: ^2.6.1
@ -123,10 +123,12 @@ dependencies:
receive_sharing_intent: ^1.8.1
top_snackbar_flutter: ^3.3.0
textfield_tags:
git:
git:
url: https://github.com/lionelmennig/textfield_tags.git
ref: fixes/allow-controller-re-registration
mime: ^2.0.0
html2md: ^1.3.2
flutter_typeahead: ^5.2.0
dev_dependencies:
flutter_test: