Compare commits

..

63 Commits

Author SHA1 Message Date
LittleSheep
c5cbf0af37 ⬆️ Upgrade android native project 2025-08-10 13:51:19 +08:00
LittleSheep
1a31e22450 🐛 Fix stickers pack unable to create 2025-08-10 13:23:15 +08:00
LittleSheep
8e0c0c6054 🚀 Launch 3.1.0+122 2025-08-10 04:16:58 +08:00
LittleSheep
f3d1183076 🐛 Fix android update 2025-08-10 04:16:53 +08:00
LittleSheep
a9f7f0cce0 🐛 Fix bugs, ah bugs ha ha, bugs 2025-08-10 04:04:31 +08:00
LittleSheep
f2943f8411 🐛 Fix iOS notify delegate wrong path 2025-08-10 03:30:57 +08:00
LittleSheep
808e7dcffa Option to boost github assets download 2025-08-10 03:25:57 +08:00
LittleSheep
9bed4fa6fb Android install update 2025-08-10 03:21:34 +08:00
LittleSheep
e6255a340b 🐛 Fix background image didn't apply in certain page 2025-08-10 02:59:28 +08:00
LittleSheep
78bf319fb7 💄 Adjust stickers styles 2025-08-10 02:59:11 +08:00
LittleSheep
36a966d582 🐛 Fix profile link parsing 2025-08-10 02:55:00 +08:00
LittleSheep
f72b268d36 💄 Optimize profile page 2025-08-10 02:29:46 +08:00
LittleSheep
44ef31034e 👽 Update the profile links 2025-08-10 02:17:06 +08:00
LittleSheep
229dc2186f 💄 Optimize markdown rendering 2025-08-10 01:53:14 +08:00
LittleSheep
a2f9a1efb4 Optimize post quick reply 2025-08-10 01:45:02 +08:00
LittleSheep
823e3c5de6 🔀 Merge pull request #160 from Texas0295/v3
[FIX] linux_firebase_guard: skip FirebaseMessaging calls on Linux
2025-08-10 01:11:29 +08:00
Texas0295
faac7bac35 🐛 linux: guard FirebaseMessaging calls when Firebase is not initialized 2025-08-10 00:40:49 +08:00
LittleSheep
1fac1bfe02 🐛 Fix post item and payment 2025-08-10 00:26:32 +08:00
LittleSheep
9394b1d9c8 🐛 Serval bug fixes 2025-08-09 23:24:21 +08:00
LittleSheep
43dd13bac4 🐛 Fix status update issue 2025-08-09 22:55:46 +08:00
LittleSheep
65bc372103 🐛 Fix iOS NSE wrong avatar path 2025-08-09 22:55:35 +08:00
LittleSheep
6558854a7a 🚀 Launch 3.1.0+120 2025-08-09 13:37:35 +08:00
LittleSheep
892035ab27 🐛 Somehow fix production video player issue 2025-08-09 13:35:54 +08:00
LittleSheep
87ae8d2ff4 🚀 Launch 3.1.0+119 2025-08-09 01:44:18 +08:00
LittleSheep
15c2dbaa0d 🐛 Fix developer 2025-08-09 01:41:51 +08:00
LittleSheep
6b3338b885 🚀 Launch 3.1.0+118 2025-08-09 00:48:50 +08:00
LittleSheep
bb00b1bc6a Debug options 2025-08-09 00:44:37 +08:00
LittleSheep
5e1a15ada2 Show profile links 2025-08-09 00:27:43 +08:00
LittleSheep
9bdf8ba346 🐛 Fix bugs 2025-08-09 00:21:27 +08:00
LittleSheep
204c087f29 Edit profile links 2025-08-09 00:15:35 +08:00
LittleSheep
1def3e1895 💄 Optimize profile page 2025-08-08 23:46:40 +08:00
LittleSheep
550c74e544 🐛 Fix unable to subscribe 2025-08-08 23:23:16 +08:00
LittleSheep
a39565f012 Post category details 2025-08-08 23:20:22 +08:00
LittleSheep
aa9755e6a7 💄 Optimized post category and tag 2025-08-08 22:19:17 +08:00
LittleSheep
b25e8d661a Show post tags and categories 2025-08-08 21:18:25 +08:00
LittleSheep
4b253ac3ec ⚗️ Testing out the firebase options on linux 2025-08-08 19:47:51 +08:00
LittleSheep
5d1b875d3c 🐛 Disable firebase on linux to prevent errors 2025-08-08 19:41:48 +08:00
LittleSheep
e2e103fa67 Post categories selection 2025-08-08 17:57:47 +08:00
LittleSheep
43c90da4e3 🐛 Fixes attachments 2025-08-08 15:04:50 +08:00
LittleSheep
fa210dd98f 💄 Post featured optimization 2025-08-08 14:44:59 +08:00
LittleSheep
43d9ca92bf Featured post 2025-08-08 03:28:16 +08:00
LittleSheep
5e592c143f ♻️ Rebuild the dupe device 2025-08-08 03:16:16 +08:00
LittleSheep
0c59816f26 👽 Update developer's path 2025-08-08 03:11:38 +08:00
LittleSheep
19c2457895 Network status, poll submit feedback 2025-08-08 02:56:44 +08:00
LittleSheep
af8d87857e Update service 2025-08-07 14:29:20 +08:00
LittleSheep
d05f63a36a 🐛 Bug fixes 2025-08-07 13:29:33 +08:00
LittleSheep
e2dc520012 Post item show more info 2025-08-07 12:17:37 +08:00
LittleSheep
cff9c15e31 🐛 Slicence post list disposed 2025-08-07 12:07:33 +08:00
LittleSheep
f00135c4bf 🐛 Fix room input 2025-08-07 12:04:47 +08:00
LittleSheep
30b8a6c30f 🐛 Fix status overflow 2025-08-07 11:56:37 +08:00
LittleSheep
b9c4ee31b1 Adjust picker size 2025-08-07 11:55:16 +08:00
LittleSheep
87870af866 Sticker picker basis 2025-08-07 03:22:56 +08:00
LittleSheep
b83cb0fb0b ♻️ Replace the snackbar usage 2025-08-07 03:03:15 +08:00
LittleSheep
7fd1fe34e5 Stickers marketplace 2025-08-07 03:00:29 +08:00
LittleSheep
1c18330891 🐛 Fixes poll feedback 2025-08-07 02:17:03 +08:00
LittleSheep
d320879ad0 Poll feedback 2025-08-07 02:01:15 +08:00
LittleSheep
950150e119 🐛 Fix large screen have no post category filter 2025-08-06 21:42:23 +08:00
LittleSheep
3c4a9767e1 ♻️ Move the title, description out of the settings sheet 2025-08-06 20:55:13 +08:00
LittleSheep
5df2445f3f 📝 Update user agreement 2025-08-06 20:30:59 +08:00
LittleSheep
56543d7b4c Publisher page category filter 2025-08-06 20:17:07 +08:00
LittleSheep
4c6fea1242 💄 Optimize cloud video 2025-08-06 18:49:04 +08:00
LittleSheep
fff43de9e3 🐛 Dozens of bug fixes 2025-08-06 18:15:13 +08:00
LittleSheep
b31a915544 🐛 Bug fixes 2025-08-06 14:57:42 +08:00
107 changed files with 5578 additions and 1501 deletions

View File

@@ -51,6 +51,12 @@ android {
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
@@ -58,7 +64,7 @@ android {
dependencies {
implementation("com.google.android.material:material:1.12.0")
implementation("com.github.bumptech.glide:glide:4.16.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:okhttp:5.1.0")
}
flutter {

View File

@@ -12,7 +12,12 @@
"package_name": "dev.solsynth.solian"
}
},
"oauth_client": [],
"oauth_client": [
{
"client_id": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDvFNudXYs29uDtcCv6pFR8h5tXBs90FYk"
@@ -20,7 +25,20 @@
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
"other_platform_oauth_client": [
{
"client_id": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "dev.solsynth.solian",
"app_store_id": "6499032345"
}
}
]
}
}
}

5
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,5 @@
# JNI Zero initialization (required for WebRTC native method registration)
-keep class livekit.org.jni_zero.JniInit {
# Keep the init method un-obfuscated for native code callback
private static java.lang.Object[] init();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip

View File

@@ -18,11 +18,11 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.10.1" apply false
id("com.android.application") version "8.12.0" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
id("org.jetbrains.kotlin.android") version("2.2.0") apply false
}
include(":app")

View File

@@ -144,6 +144,7 @@
"other": "{} attachments"
},
"edited": "Edited",
"editedAt": "Edited at {}",
"addVideo": "Add video",
"addPhoto": "Add photo",
"addAudio": "Add audio",
@@ -705,6 +706,7 @@
"copyToClipboardTooltip": "Copy to clipboard",
"postForwardingTo": "Forwarding to",
"postReplyingTo": "Replying to",
"postReplyPlaceholder": "Post your reply",
"postEditing": "You are editing an existing post",
"postArticle": "Article",
"aboutDeviceName": "Device Name",
@@ -761,5 +763,31 @@
"publisher": "Publisher",
"publisherHint": "Enter the publisher name",
"publisherCannotBeEmpty": "Publisher cannot be empty",
"operationFailed": "Operation failed: {}"
"operationFailed": "Operation failed: {}",
"stickerMarketplace": "Sticker Marketplace",
"stickerPackAdded": "Sticker pack added to your collection",
"stickerPackRemoved": "Sticker pack removed from your collection",
"addPack": "Add Pack",
"removePack": "Remove Pack",
"browseAndAddStickers": "Browse and add sticker packs",
"stickerPack": "Sticker Pack",
"postCategoryTechnology": "Technology",
"postCategoryTravel": "Travel",
"postCategoryFood": "Food",
"postCategoryHealth": "Health",
"postCategoryScience": "Science",
"postCategorySports": "Sports",
"postCategoryFinance": "Finance",
"postCategoryLife": "Life",
"postCategoryArt": "Art",
"postCategoryStudy": "Study",
"postCategoryGaming": "Gaming",
"postCategoryProgramming": "Programming",
"postCategoryMusic": "Music",
"links": "Links",
"addLink": "Add link",
"linkKey": "Link Name",
"linkValue": "URL",
"debugOptions": "Debug Options",
"joinedAt": "Joined at {}"
}

View File

@@ -120,6 +120,7 @@
"other": "{}个附件"
},
"edited": "已编辑",
"editedAt": "编辑于 {}",
"addVideo": "添加视频",
"addPhoto": "添加照片",
"addFile": "添加文件",

View File

@@ -1 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:android:a8d3f7995b0b8e86f4188b","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:ios:727229d368cc47e1f4188b","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:ios:727229d368cc47e1f4188b","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"solian-0x001","configurations":{"android":"1:961776991058:android:a8d3f7995b0b8e86f4188b","ios":"1:961776991058:ios:727229d368cc47e1f4188b","macos":"1:961776991058:ios:727229d368cc47e1f4188b","web":"1:961776991058:web:b91d12f2892a5609f4188b","windows":"1:961776991058:web:3a912c0eb14028e5f4188b"}}}}}}
{"flutter":{"platforms":{"android":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:android:a8d3f7995b0b8e86f4188b","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:ios:727229d368cc47e1f4188b","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:ios:727229d368cc47e1f4188b","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"solian-0x001","configurations":{"android":"1:961776991058:android:a8d3f7995b0b8e86f4188b","ios":"1:961776991058:ios:727229d368cc47e1f4188b","macos":"1:961776991058:ios:727229d368cc47e1f4188b","web":"1:961776991058:web:3a912c0eb14028e5f4188b","windows":"1:961776991058:web:3a912c0eb14028e5f4188b"}}}}}}

View File

@@ -73,6 +73,8 @@ PODS:
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- Flutter (1.0.0)
- flutter_app_update (0.0.1):
- Flutter
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
@@ -178,25 +180,25 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.50.3):
- sqlite3/common (= 3.50.3)
- sqlite3/common (3.50.3)
- sqlite3/dbstatvtab (3.50.3):
- sqlite3 (3.50.4):
- sqlite3/common (= 3.50.4)
- sqlite3/common (3.50.4)
- sqlite3/dbstatvtab (3.50.4):
- sqlite3/common
- sqlite3/fts5 (3.50.3):
- sqlite3/fts5 (3.50.4):
- sqlite3/common
- sqlite3/math (3.50.3):
- sqlite3/math (3.50.4):
- sqlite3/common
- sqlite3/perf-threadsafe (3.50.3):
- sqlite3/perf-threadsafe (3.50.4):
- sqlite3/common
- sqlite3/rtree (3.50.3):
- sqlite3/rtree (3.50.4):
- sqlite3/common
- sqlite3/session (3.50.3):
- sqlite3/session (3.50.4):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.50.3)
- sqlite3 (~> 3.50.4)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
@@ -223,6 +225,7 @@ DEPENDENCIES:
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- 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`)
@@ -293,6 +296,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
:path: Flutter
flutter_app_update:
:path: ".symlinks/plugins/flutter_app_update/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_keyboard_visibility:
@@ -372,6 +377,7 @@ SPEC CHECKSUMS:
FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988
FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
@@ -406,8 +412,8 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81
sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d

View File

@@ -2,6 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
<key>ANDROID_CLIENT_ID</key>
<string>961776991058-r4iv9qoio57ul7utbfpgfrda2etvtch8.apps.googleusercontent.com</string>
<key>API_KEY</key>
<string>AIzaSyCzQIyiYKoYHTpGXhN-IjgMML8z797WVD8</string>
<key>GCM_SENDER_ID</key>

View File

@@ -34,7 +34,7 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
}
let serverUrl = UserDefaults.standard.getServerUrl()
let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages"
let url = "\(serverUrl)/sphere/chat/\(metadata["room_id"] ?? "")/messages"
let parameters: [String: Any?] = [
"content": textResponse.userText,

View File

@@ -8,7 +8,7 @@
import Foundation
func getAttachmentUrl(for identifier: String) -> String {
let serverBaseUrl = "https://nt.solian.app"
let serverBaseUrl = "https://api.solian.app"
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/files/\(identifier)"
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)"
}

View File

@@ -29,10 +29,7 @@ class DefaultFirebaseOptions {
case TargetPlatform.windows:
return windows;
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
return windows;
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
@@ -41,13 +38,13 @@ class DefaultFirebaseOptions {
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyBKfIQpTouj5rXnlzkEieSlbAzepm4mgJE',
appId: '1:961776991058:web:b91d12f2892a5609f4188b',
apiKey: 'AIzaSyCfgOdlcr7h8x8j0WKx_S2wXnGkOopq320',
appId: '1:961776991058:web:3a912c0eb14028e5f4188b',
messagingSenderId: '961776991058',
projectId: 'solian-0x001',
authDomain: 'solian-0x001.firebaseapp.com',
storageBucket: 'solian-0x001.firebasestorage.app',
measurementId: 'G-XY3HHKG0PE',
measurementId: 'G-JD1YEG9D6F',
);
static const FirebaseOptions android = FirebaseOptions(
@@ -64,6 +61,10 @@ class DefaultFirebaseOptions {
messagingSenderId: '961776991058',
projectId: 'solian-0x001',
storageBucket: 'solian-0x001.firebasestorage.app',
androidClientId:
'961776991058-r4iv9qoio57ul7utbfpgfrda2etvtch8.apps.googleusercontent.com',
iosClientId:
'961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com',
iosBundleId: 'dev.solsynth.solian',
);
@@ -73,6 +74,10 @@ class DefaultFirebaseOptions {
messagingSenderId: '961776991058',
projectId: 'solian-0x001',
storageBucket: 'solian-0x001.firebasestorage.app',
androidClientId:
'961776991058-r4iv9qoio57ul7utbfpgfrda2etvtch8.apps.googleusercontent.com',
iosClientId:
'961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com',
iosBundleId: 'dev.solsynth.solian',
);
@@ -85,5 +90,4 @@ class DefaultFirebaseOptions {
storageBucket: 'solian-0x001.firebasestorage.app',
measurementId: 'G-JD1YEG9D6F',
);
}
}

View File

@@ -53,10 +53,16 @@ void main() async {
try {
await langdetect.initLangDetect();
await EasyLocalization.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
if (kIsWeb || !Platform.isLinux) {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler,
);
}
log("[SplashScreen] Firebase is ready!");
} catch (err) {
showErrorAlert(err);
@@ -165,6 +171,9 @@ class IslandApp extends HookConsumerWidget {
}
useEffect(() {
if (!kIsWeb && Platform.isLinux) {
return null;
}
const channel = MethodChannel('dev.solsynth.solian/notifications');
Future<void> handleInitialLink() async {

View File

@@ -1,14 +1,26 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/publisher.dart';
part 'developer.freezed.dart';
part 'developer.g.dart';
@freezed
sealed class SnDeveloper with _$SnDeveloper {
const factory SnDeveloper({
required String id,
required String publisherId,
SnPublisher? publisher,
}) = _SnDeveloper;
factory SnDeveloper.fromJson(Map<String, dynamic> json) =>
_$SnDeveloperFromJson(json);
}
@freezed
sealed class DeveloperStats with _$DeveloperStats {
const factory DeveloperStats({
@Default(0) int totalCustomApps,
}) = _DeveloperStats;
const factory DeveloperStats({@Default(0) int totalCustomApps}) =
_DeveloperStats;
factory DeveloperStats.fromJson(Map<String, dynamic> json) =>
_$DeveloperStatsFromJson(json);
}
}

View File

@@ -12,6 +12,293 @@ part of 'developer.dart';
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnDeveloper {
String get id; String get publisherId; SnPublisher? get publisher;
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnDeveloperCopyWith<SnDeveloper> get copyWith => _$SnDeveloperCopyWithImpl<SnDeveloper>(this as SnDeveloper, _$identity);
/// Serializes this SnDeveloper to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnDeveloper&&(identical(other.id, id) || other.id == id)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,publisherId,publisher);
@override
String toString() {
return 'SnDeveloper(id: $id, publisherId: $publisherId, publisher: $publisher)';
}
}
/// @nodoc
abstract mixin class $SnDeveloperCopyWith<$Res> {
factory $SnDeveloperCopyWith(SnDeveloper value, $Res Function(SnDeveloper) _then) = _$SnDeveloperCopyWithImpl;
@useResult
$Res call({
String id, String publisherId, SnPublisher? publisher
});
$SnPublisherCopyWith<$Res>? get publisher;
}
/// @nodoc
class _$SnDeveloperCopyWithImpl<$Res>
implements $SnDeveloperCopyWith<$Res> {
_$SnDeveloperCopyWithImpl(this._self, this._then);
final SnDeveloper _self;
final $Res Function(SnDeveloper) _then;
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? publisherId = null,Object? publisher = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,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?,
));
}
/// Create a copy of SnDeveloper
/// 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));
});
}
}
/// Adds pattern-matching-related methods to [SnDeveloper].
extension SnDeveloperPatterns on SnDeveloper {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnDeveloper value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnDeveloper() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnDeveloper value) $default,){
final _that = this;
switch (_that) {
case _SnDeveloper():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnDeveloper value)? $default,){
final _that = this;
switch (_that) {
case _SnDeveloper() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String publisherId, SnPublisher? publisher)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnDeveloper() when $default != null:
return $default(_that.id,_that.publisherId,_that.publisher);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String publisherId, SnPublisher? publisher) $default,) {final _that = this;
switch (_that) {
case _SnDeveloper():
return $default(_that.id,_that.publisherId,_that.publisher);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String publisherId, SnPublisher? publisher)? $default,) {final _that = this;
switch (_that) {
case _SnDeveloper() when $default != null:
return $default(_that.id,_that.publisherId,_that.publisher);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnDeveloper implements SnDeveloper {
const _SnDeveloper({required this.id, required this.publisherId, this.publisher});
factory _SnDeveloper.fromJson(Map<String, dynamic> json) => _$SnDeveloperFromJson(json);
@override final String id;
@override final String publisherId;
@override final SnPublisher? publisher;
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnDeveloperCopyWith<_SnDeveloper> get copyWith => __$SnDeveloperCopyWithImpl<_SnDeveloper>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnDeveloperToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnDeveloper&&(identical(other.id, id) || other.id == id)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,publisherId,publisher);
@override
String toString() {
return 'SnDeveloper(id: $id, publisherId: $publisherId, publisher: $publisher)';
}
}
/// @nodoc
abstract mixin class _$SnDeveloperCopyWith<$Res> implements $SnDeveloperCopyWith<$Res> {
factory _$SnDeveloperCopyWith(_SnDeveloper value, $Res Function(_SnDeveloper) _then) = __$SnDeveloperCopyWithImpl;
@override @useResult
$Res call({
String id, String publisherId, SnPublisher? publisher
});
@override $SnPublisherCopyWith<$Res>? get publisher;
}
/// @nodoc
class __$SnDeveloperCopyWithImpl<$Res>
implements _$SnDeveloperCopyWith<$Res> {
__$SnDeveloperCopyWithImpl(this._self, this._then);
final _SnDeveloper _self;
final $Res Function(_SnDeveloper) _then;
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? publisherId = null,Object? publisher = freezed,}) {
return _then(_SnDeveloper(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,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?,
));
}
/// Create a copy of SnDeveloper
/// 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));
});
}
}
/// @nodoc
mixin _$DeveloperStats {

View File

@@ -6,6 +6,22 @@ part of 'developer.dart';
// JsonSerializableGenerator
// **************************************************************************
_SnDeveloper _$SnDeveloperFromJson(Map<String, dynamic> json) => _SnDeveloper(
id: json['id'] as String,
publisherId: json['publisher_id'] as String,
publisher:
json['publisher'] == null
? null
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
);
Map<String, dynamic> _$SnDeveloperToJson(_SnDeveloper instance) =>
<String, dynamic>{
'id': instance.id,
'publisher_id': instance.publisherId,
'publisher': instance.publisher?.toJson(),
};
_DeveloperStats _$DeveloperStatsFromJson(Map<String, dynamic> json) =>
_DeveloperStats(
totalCustomApps: (json['total_custom_apps'] as num?)?.toInt() ?? 0,

View File

@@ -3,25 +3,6 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'embed.freezed.dart';
part 'embed.g.dart';
@freezed
sealed class SnEmbedLink with _$SnEmbedLink {
const factory SnEmbedLink({
@JsonKey(name: 'Type') required String type,
@JsonKey(name: 'Url') required String url,
@JsonKey(name: 'Title') required String title,
@JsonKey(name: 'Description') required String? description,
@JsonKey(name: 'ImageUrl') required String? imageUrl,
@JsonKey(name: 'FaviconUrl') @Default("") String faviconUrl,
@JsonKey(name: 'SiteName') @Default("") String siteName,
@JsonKey(name: 'ContentType') required String? contentType,
@JsonKey(name: 'Author') required String? author,
@JsonKey(name: 'PublishedDate') required DateTime? publishedDate,
}) = _SnEmbedLink;
factory SnEmbedLink.fromJson(Map<String, dynamic> json) =>
_$SnEmbedLinkFromJson(json);
}
@freezed
sealed class SnScrappedLink with _$SnScrappedLink {
const factory SnScrappedLink({

View File

@@ -12,290 +12,6 @@ part of 'embed.dart';
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnEmbedLink {
@JsonKey(name: 'Type') String get type;@JsonKey(name: 'Url') String get url;@JsonKey(name: 'Title') String get title;@JsonKey(name: 'Description') String? get description;@JsonKey(name: 'ImageUrl') String? get imageUrl;@JsonKey(name: 'FaviconUrl') String get faviconUrl;@JsonKey(name: 'SiteName') String get siteName;@JsonKey(name: 'ContentType') String? get contentType;@JsonKey(name: 'Author') String? get author;@JsonKey(name: 'PublishedDate') DateTime? get publishedDate;
/// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnEmbedLinkCopyWith<SnEmbedLink> get copyWith => _$SnEmbedLinkCopyWithImpl<SnEmbedLink>(this as SnEmbedLink, _$identity);
/// Serializes this SnEmbedLink to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnEmbedLink&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.faviconUrl, faviconUrl) || other.faviconUrl == faviconUrl)&&(identical(other.siteName, siteName) || other.siteName == siteName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.author, author) || other.author == author)&&(identical(other.publishedDate, publishedDate) || other.publishedDate == publishedDate));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,author,publishedDate);
@override
String toString() {
return 'SnEmbedLink(type: $type, url: $url, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl, siteName: $siteName, contentType: $contentType, author: $author, publishedDate: $publishedDate)';
}
}
/// @nodoc
abstract mixin class $SnEmbedLinkCopyWith<$Res> {
factory $SnEmbedLinkCopyWith(SnEmbedLink value, $Res Function(SnEmbedLink) _then) = _$SnEmbedLinkCopyWithImpl;
@useResult
$Res call({
@JsonKey(name: 'Type') String type,@JsonKey(name: 'Url') String url,@JsonKey(name: 'Title') String title,@JsonKey(name: 'Description') String? description,@JsonKey(name: 'ImageUrl') String? imageUrl,@JsonKey(name: 'FaviconUrl') String faviconUrl,@JsonKey(name: 'SiteName') String siteName,@JsonKey(name: 'ContentType') String? contentType,@JsonKey(name: 'Author') String? author,@JsonKey(name: 'PublishedDate') DateTime? publishedDate
});
}
/// @nodoc
class _$SnEmbedLinkCopyWithImpl<$Res>
implements $SnEmbedLinkCopyWith<$Res> {
_$SnEmbedLinkCopyWithImpl(this._self, this._then);
final SnEmbedLink _self;
final $Res Function(SnEmbedLink) _then;
/// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
return _then(_self.copyWith(
type: null == type ? _self.type : type // 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?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// Adds pattern-matching-related methods to [SnEmbedLink].
extension SnEmbedLinkPatterns on SnEmbedLink {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnEmbedLink value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnEmbedLink() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnEmbedLink value) $default,){
final _that = this;
switch (_that) {
case _SnEmbedLink():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnEmbedLink value)? $default,){
final _that = this;
switch (_that) {
case _SnEmbedLink() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'Type') String type, @JsonKey(name: 'Url') String url, @JsonKey(name: 'Title') String title, @JsonKey(name: 'Description') String? description, @JsonKey(name: 'ImageUrl') String? imageUrl, @JsonKey(name: 'FaviconUrl') String faviconUrl, @JsonKey(name: 'SiteName') String siteName, @JsonKey(name: 'ContentType') String? contentType, @JsonKey(name: 'Author') String? author, @JsonKey(name: 'PublishedDate') DateTime? publishedDate)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnEmbedLink() when $default != null:
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'Type') String type, @JsonKey(name: 'Url') String url, @JsonKey(name: 'Title') String title, @JsonKey(name: 'Description') String? description, @JsonKey(name: 'ImageUrl') String? imageUrl, @JsonKey(name: 'FaviconUrl') String faviconUrl, @JsonKey(name: 'SiteName') String siteName, @JsonKey(name: 'ContentType') String? contentType, @JsonKey(name: 'Author') String? author, @JsonKey(name: 'PublishedDate') DateTime? publishedDate) $default,) {final _that = this;
switch (_that) {
case _SnEmbedLink():
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'Type') String type, @JsonKey(name: 'Url') String url, @JsonKey(name: 'Title') String title, @JsonKey(name: 'Description') String? description, @JsonKey(name: 'ImageUrl') String? imageUrl, @JsonKey(name: 'FaviconUrl') String faviconUrl, @JsonKey(name: 'SiteName') String siteName, @JsonKey(name: 'ContentType') String? contentType, @JsonKey(name: 'Author') String? author, @JsonKey(name: 'PublishedDate') DateTime? publishedDate)? $default,) {final _that = this;
switch (_that) {
case _SnEmbedLink() when $default != null:
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnEmbedLink implements SnEmbedLink {
const _SnEmbedLink({@JsonKey(name: 'Type') required this.type, @JsonKey(name: 'Url') required this.url, @JsonKey(name: 'Title') required this.title, @JsonKey(name: 'Description') required this.description, @JsonKey(name: 'ImageUrl') required this.imageUrl, @JsonKey(name: 'FaviconUrl') this.faviconUrl = "", @JsonKey(name: 'SiteName') this.siteName = "", @JsonKey(name: 'ContentType') required this.contentType, @JsonKey(name: 'Author') required this.author, @JsonKey(name: 'PublishedDate') required this.publishedDate});
factory _SnEmbedLink.fromJson(Map<String, dynamic> json) => _$SnEmbedLinkFromJson(json);
@override@JsonKey(name: 'Type') final String type;
@override@JsonKey(name: 'Url') final String url;
@override@JsonKey(name: 'Title') final String title;
@override@JsonKey(name: 'Description') final String? description;
@override@JsonKey(name: 'ImageUrl') final String? imageUrl;
@override@JsonKey(name: 'FaviconUrl') final String faviconUrl;
@override@JsonKey(name: 'SiteName') final String siteName;
@override@JsonKey(name: 'ContentType') final String? contentType;
@override@JsonKey(name: 'Author') final String? author;
@override@JsonKey(name: 'PublishedDate') final DateTime? publishedDate;
/// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnEmbedLinkCopyWith<_SnEmbedLink> get copyWith => __$SnEmbedLinkCopyWithImpl<_SnEmbedLink>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnEmbedLinkToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnEmbedLink&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.faviconUrl, faviconUrl) || other.faviconUrl == faviconUrl)&&(identical(other.siteName, siteName) || other.siteName == siteName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.author, author) || other.author == author)&&(identical(other.publishedDate, publishedDate) || other.publishedDate == publishedDate));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,author,publishedDate);
@override
String toString() {
return 'SnEmbedLink(type: $type, url: $url, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl, siteName: $siteName, contentType: $contentType, author: $author, publishedDate: $publishedDate)';
}
}
/// @nodoc
abstract mixin class _$SnEmbedLinkCopyWith<$Res> implements $SnEmbedLinkCopyWith<$Res> {
factory _$SnEmbedLinkCopyWith(_SnEmbedLink value, $Res Function(_SnEmbedLink) _then) = __$SnEmbedLinkCopyWithImpl;
@override @useResult
$Res call({
@JsonKey(name: 'Type') String type,@JsonKey(name: 'Url') String url,@JsonKey(name: 'Title') String title,@JsonKey(name: 'Description') String? description,@JsonKey(name: 'ImageUrl') String? imageUrl,@JsonKey(name: 'FaviconUrl') String faviconUrl,@JsonKey(name: 'SiteName') String siteName,@JsonKey(name: 'ContentType') String? contentType,@JsonKey(name: 'Author') String? author,@JsonKey(name: 'PublishedDate') DateTime? publishedDate
});
}
/// @nodoc
class __$SnEmbedLinkCopyWithImpl<$Res>
implements _$SnEmbedLinkCopyWith<$Res> {
__$SnEmbedLinkCopyWithImpl(this._self, this._then);
final _SnEmbedLink _self;
final $Res Function(_SnEmbedLink) _then;
/// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
return _then(_SnEmbedLink(
type: null == type ? _self.type : type // 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?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
mixin _$SnScrappedLink {

View File

@@ -6,36 +6,6 @@ part of 'embed.dart';
// JsonSerializableGenerator
// **************************************************************************
_SnEmbedLink _$SnEmbedLinkFromJson(Map<String, dynamic> json) => _SnEmbedLink(
type: json['Type'] as String,
url: json['Url'] as String,
title: json['Title'] as String,
description: json['Description'] as String?,
imageUrl: json['ImageUrl'] as String?,
faviconUrl: json['FaviconUrl'] as String? ?? "",
siteName: json['SiteName'] as String? ?? "",
contentType: json['ContentType'] as String?,
author: json['Author'] as String?,
publishedDate:
json['PublishedDate'] == null
? null
: DateTime.parse(json['PublishedDate'] as String),
);
Map<String, dynamic> _$SnEmbedLinkToJson(_SnEmbedLink instance) =>
<String, dynamic>{
'Type': instance.type,
'Url': instance.url,
'Title': instance.title,
'Description': instance.description,
'ImageUrl': instance.imageUrl,
'FaviconUrl': instance.faviconUrl,
'SiteName': instance.siteName,
'ContentType': instance.contentType,
'Author': instance.author,
'PublishedDate': instance.publishedDate?.toIso8601String(),
};
_SnScrappedLink _$SnScrappedLinkFromJson(Map<String, dynamic> json) =>
_SnScrappedLink(
type: json['type'] as String,

View File

@@ -90,3 +90,19 @@ enum SnPollQuestionType {
@JsonValue(4)
freeText,
}
@freezed
sealed class SnPollAnswer with _$SnPollAnswer {
const factory SnPollAnswer({
required String id,
required Map<String, dynamic> answer,
required String accountId,
required String pollId,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnPollAnswer;
factory SnPollAnswer.fromJson(Map<String, dynamic> json) =>
_$SnPollAnswerFromJson(json);
}

View File

@@ -1181,6 +1181,287 @@ as int,
}
}
/// @nodoc
mixin _$SnPollAnswer {
String get id; Map<String, dynamic> get answer; String get accountId; String get pollId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnPollAnswer
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnPollAnswerCopyWith<SnPollAnswer> get copyWith => _$SnPollAnswerCopyWithImpl<SnPollAnswer>(this as SnPollAnswer, _$identity);
/// Serializes this SnPollAnswer to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPollAnswer&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other.answer, answer)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.pollId, pollId) || other.pollId == pollId)&&(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,const DeepCollectionEquality().hash(answer),accountId,pollId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnPollAnswer(id: $id, answer: $answer, accountId: $accountId, pollId: $pollId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnPollAnswerCopyWith<$Res> {
factory $SnPollAnswerCopyWith(SnPollAnswer value, $Res Function(SnPollAnswer) _then) = _$SnPollAnswerCopyWithImpl;
@useResult
$Res call({
String id, Map<String, dynamic> answer, String accountId, String pollId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnPollAnswerCopyWithImpl<$Res>
implements $SnPollAnswerCopyWith<$Res> {
_$SnPollAnswerCopyWithImpl(this._self, this._then);
final SnPollAnswer _self;
final $Res Function(SnPollAnswer) _then;
/// Create a copy of SnPollAnswer
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? answer = null,Object? accountId = null,Object? pollId = 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,answer: null == answer ? _self.answer : answer // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,pollId: null == pollId ? _self.pollId : pollId // 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?,
));
}
}
/// Adds pattern-matching-related methods to [SnPollAnswer].
extension SnPollAnswerPatterns on SnPollAnswer {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPollAnswer value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnPollAnswer() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPollAnswer value) $default,){
final _that = this;
switch (_that) {
case _SnPollAnswer():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPollAnswer value)? $default,){
final _that = this;
switch (_that) {
case _SnPollAnswer() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, Map<String, dynamic> answer, String accountId, String pollId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPollAnswer() when $default != null:
return $default(_that.id,_that.answer,_that.accountId,_that.pollId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, Map<String, dynamic> answer, String accountId, String pollId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnPollAnswer():
return $default(_that.id,_that.answer,_that.accountId,_that.pollId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, Map<String, dynamic> answer, String accountId, String pollId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnPollAnswer() when $default != null:
return $default(_that.id,_that.answer,_that.accountId,_that.pollId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnPollAnswer implements SnPollAnswer {
const _SnPollAnswer({required this.id, required final Map<String, dynamic> answer, required this.accountId, required this.pollId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _answer = answer;
factory _SnPollAnswer.fromJson(Map<String, dynamic> json) => _$SnPollAnswerFromJson(json);
@override final String id;
final Map<String, dynamic> _answer;
@override Map<String, dynamic> get answer {
if (_answer is EqualUnmodifiableMapView) return _answer;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_answer);
}
@override final String accountId;
@override final String pollId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnPollAnswer
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnPollAnswerCopyWith<_SnPollAnswer> get copyWith => __$SnPollAnswerCopyWithImpl<_SnPollAnswer>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnPollAnswerToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPollAnswer&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other._answer, _answer)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.pollId, pollId) || other.pollId == pollId)&&(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,const DeepCollectionEquality().hash(_answer),accountId,pollId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnPollAnswer(id: $id, answer: $answer, accountId: $accountId, pollId: $pollId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnPollAnswerCopyWith<$Res> implements $SnPollAnswerCopyWith<$Res> {
factory _$SnPollAnswerCopyWith(_SnPollAnswer value, $Res Function(_SnPollAnswer) _then) = __$SnPollAnswerCopyWithImpl;
@override @useResult
$Res call({
String id, Map<String, dynamic> answer, String accountId, String pollId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnPollAnswerCopyWithImpl<$Res>
implements _$SnPollAnswerCopyWith<$Res> {
__$SnPollAnswerCopyWithImpl(this._self, this._then);
final _SnPollAnswer _self;
final $Res Function(_SnPollAnswer) _then;
/// Create a copy of SnPollAnswer
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? answer = null,Object? accountId = null,Object? pollId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnPollAnswer(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,answer: null == answer ? _self._answer : answer // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,pollId: null == pollId ? _self.pollId : pollId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
// dart format on

View File

@@ -131,3 +131,28 @@ Map<String, dynamic> _$SnPollOptionToJson(_SnPollOption instance) =>
'description': instance.description,
'order': instance.order,
};
_SnPollAnswer _$SnPollAnswerFromJson(Map<String, dynamic> json) =>
_SnPollAnswer(
id: json['id'] as String,
answer: json['answer'] as Map<String, dynamic>,
accountId: json['account_id'] as String,
pollId: json['poll_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> _$SnPollAnswerToJson(_SnPollAnswer instance) =>
<String, dynamic>{
'id': instance.id,
'answer': instance.answer,
'account_id': instance.accountId,
'poll_id': instance.pollId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@@ -36,8 +36,8 @@ sealed class SnPost with _$SnPost {
@Default({}) Map<String, int> reactionsCount,
@Default({}) Map<String, bool> reactionsMade,
@Default([]) List<dynamic> reactions,
@Default([]) List<PostTag> tags,
@Default([]) List<PostCategory> categories,
@Default([]) List<SnPostTag> tags,
@Default([]) List<SnPostCategory> categories,
@Default([]) List<dynamic> collections,
@Default(null) DateTime? createdAt,
@Default(null) DateTime? updatedAt,

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnPost {
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<PostTag> get tags; List<PostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated;
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<SnPostTag> get tags; List<SnPostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated;
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -48,7 +48,7 @@ abstract mixin class $SnPostCopyWith<$Res> {
factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl;
@useResult
$Res call({
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
});
@@ -94,8 +94,8 @@ as SnPublisher,reactionsCount: null == reactionsCount ? _self.reactionsCount : r
as Map<String, int>,reactionsMade: null == reactionsMade ? _self.reactionsMade : reactionsMade // ignore: cast_nullable_to_non_nullable
as Map<String, bool>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable
as List<dynamic>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
as List<PostTag>,categories: null == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable
as List<PostCategory>,collections: null == collections ? _self.collections : collections // ignore: cast_nullable_to_non_nullable
as List<SnPostTag>,categories: null == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable
as List<SnPostCategory>,collections: null == collections ? _self.collections : collections // ignore: cast_nullable_to_non_nullable
as List<dynamic>,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
@@ -227,7 +227,7 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPost() when $default != null:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
@@ -248,7 +248,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated) $default,) {final _that = this;
switch (_that) {
case _SnPost():
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);}
@@ -265,7 +265,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,) {final _that = this;
switch (_that) {
case _SnPost() when $default != null:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
@@ -280,7 +280,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
@JsonSerializable()
class _SnPost implements SnPost {
const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.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 Map<String, bool> reactionsMade = 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,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.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 Map<String, bool> reactionsMade = const {}, final List<dynamic> reactions = const [], final List<SnPostTag> tags = const [], final List<SnPostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json);
@override final String id;
@@ -341,15 +341,15 @@ class _SnPost implements SnPost {
return EqualUnmodifiableListView(_reactions);
}
final List<PostTag> _tags;
@override@JsonKey() List<PostTag> get tags {
final List<SnPostTag> _tags;
@override@JsonKey() List<SnPostTag> get tags {
if (_tags is EqualUnmodifiableListView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_tags);
}
final List<PostCategory> _categories;
@override@JsonKey() List<PostCategory> get categories {
final List<SnPostCategory> _categories;
@override@JsonKey() List<SnPostCategory> get categories {
if (_categories is EqualUnmodifiableListView) return _categories;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_categories);
@@ -400,7 +400,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl;
@override @useResult
$Res call({
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
});
@@ -446,8 +446,8 @@ as SnPublisher,reactionsCount: null == reactionsCount ? _self._reactionsCount :
as Map<String, int>,reactionsMade: null == reactionsMade ? _self._reactionsMade : reactionsMade // ignore: cast_nullable_to_non_nullable
as Map<String, bool>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable
as List<dynamic>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
as List<PostTag>,categories: null == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable
as List<PostCategory>,collections: null == collections ? _self._collections : collections // ignore: cast_nullable_to_non_nullable
as List<SnPostTag>,categories: null == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable
as List<SnPostCategory>,collections: null == collections ? _self._collections : collections // ignore: cast_nullable_to_non_nullable
as List<dynamic>,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable

View File

@@ -62,12 +62,12 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
reactions: json['reactions'] as List<dynamic>? ?? const [],
tags:
(json['tags'] as List<dynamic>?)
?.map((e) => PostTag.fromJson(e as Map<String, dynamic>))
?.map((e) => SnPostTag.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
categories:
(json['categories'] as List<dynamic>?)
?.map((e) => PostCategory.fromJson(e as Map<String, dynamic>))
?.map((e) => SnPostCategory.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
collections: json['collections'] as List<dynamic>? ?? const [],

View File

@@ -1,19 +1,30 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/post.dart';
import 'package:island/services/text.dart';
part 'post_category.freezed.dart';
part 'post_category.g.dart';
@freezed
sealed class PostCategory with _$PostCategory {
const factory PostCategory({
sealed class SnPostCategory with _$SnPostCategory {
const SnPostCategory._();
const factory SnPostCategory({
required String id,
required String slug,
String? name,
@Default([]) List<SnPost> posts,
}) = _PostCategory;
}) = _SnPostCategory;
factory PostCategory.fromJson(Map<String, dynamic> json) =>
_$PostCategoryFromJson(json);
factory SnPostCategory.fromJson(Map<String, dynamic> json) =>
_$SnPostCategoryFromJson(json);
String get categoryDisplayTitle {
final capitalizedSlug = slug.capitalizeEachWord();
if ('postCategory$capitalizedSlug'.trExists()) {
return 'postCategory$capitalizedSlug'.tr();
}
return name ?? slug;
}
}

View File

@@ -13,22 +13,22 @@ part of 'post_category.dart';
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$PostCategory {
mixin _$SnPostCategory {
String get id; String get slug; String? get name; List<SnPost> get posts;
/// Create a copy of PostCategory
/// Create a copy of SnPostCategory
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$PostCategoryCopyWith<PostCategory> get copyWith => _$PostCategoryCopyWithImpl<PostCategory>(this as PostCategory, _$identity);
$SnPostCategoryCopyWith<SnPostCategory> get copyWith => _$SnPostCategoryCopyWithImpl<SnPostCategory>(this as SnPostCategory, _$identity);
/// Serializes this PostCategory to a JSON map.
/// Serializes this SnPostCategory to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is PostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -37,15 +37,15 @@ int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEqu
@override
String toString() {
return 'PostCategory(id: $id, slug: $slug, name: $name, posts: $posts)';
return 'SnPostCategory(id: $id, slug: $slug, name: $name, posts: $posts)';
}
}
/// @nodoc
abstract mixin class $PostCategoryCopyWith<$Res> {
factory $PostCategoryCopyWith(PostCategory value, $Res Function(PostCategory) _then) = _$PostCategoryCopyWithImpl;
abstract mixin class $SnPostCategoryCopyWith<$Res> {
factory $SnPostCategoryCopyWith(SnPostCategory value, $Res Function(SnPostCategory) _then) = _$SnPostCategoryCopyWithImpl;
@useResult
$Res call({
String id, String slug, String? name, List<SnPost> posts
@@ -56,14 +56,14 @@ $Res call({
}
/// @nodoc
class _$PostCategoryCopyWithImpl<$Res>
implements $PostCategoryCopyWith<$Res> {
_$PostCategoryCopyWithImpl(this._self, this._then);
class _$SnPostCategoryCopyWithImpl<$Res>
implements $SnPostCategoryCopyWith<$Res> {
_$SnPostCategoryCopyWithImpl(this._self, this._then);
final PostCategory _self;
final $Res Function(PostCategory) _then;
final SnPostCategory _self;
final $Res Function(SnPostCategory) _then;
/// Create a copy of PostCategory
/// Create a copy of SnPostCategory
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) {
return _then(_self.copyWith(
@@ -78,8 +78,8 @@ as List<SnPost>,
}
/// Adds pattern-matching-related methods to [PostCategory].
extension PostCategoryPatterns on PostCategory {
/// Adds pattern-matching-related methods to [SnPostCategory].
extension SnPostCategoryPatterns on SnPostCategory {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
@@ -92,10 +92,10 @@ extension PostCategoryPatterns on PostCategory {
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PostCategory value)? $default,{required TResult orElse(),}){
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPostCategory value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _PostCategory() when $default != null:
case _SnPostCategory() when $default != null:
return $default(_that);case _:
return orElse();
@@ -114,10 +114,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PostCategory value) $default,){
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPostCategory value) $default,){
final _that = this;
switch (_that) {
case _PostCategory():
case _SnPostCategory():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
@@ -132,10 +132,10 @@ return $default(_that);}
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PostCategory value)? $default,){
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPostCategory value)? $default,){
final _that = this;
switch (_that) {
case _PostCategory() when $default != null:
case _SnPostCategory() when $default != null:
return $default(_that);case _:
return null;
@@ -155,7 +155,7 @@ return $default(_that);case _:
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _PostCategory() when $default != null:
case _SnPostCategory() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
return orElse();
@@ -176,7 +176,7 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts) $default,) {final _that = this;
switch (_that) {
case _PostCategory():
case _SnPostCategory():
return $default(_that.id,_that.slug,_that.name,_that.posts);}
}
/// A variant of `when` that fallback to returning `null`
@@ -193,7 +193,7 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);}
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String? name, List<SnPost> posts)? $default,) {final _that = this;
switch (_that) {
case _PostCategory() when $default != null:
case _SnPostCategory() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
return null;
@@ -205,9 +205,9 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
/// @nodoc
@JsonSerializable()
class _PostCategory implements PostCategory {
const _PostCategory({required this.id, required this.slug, this.name, final List<SnPost> posts = const []}): _posts = posts;
factory _PostCategory.fromJson(Map<String, dynamic> json) => _$PostCategoryFromJson(json);
class _SnPostCategory extends SnPostCategory {
const _SnPostCategory({required this.id, required this.slug, this.name, final List<SnPost> posts = const []}): _posts = posts,super._();
factory _SnPostCategory.fromJson(Map<String, dynamic> json) => _$SnPostCategoryFromJson(json);
@override final String id;
@override final String slug;
@@ -220,20 +220,20 @@ class _PostCategory implements PostCategory {
}
/// Create a copy of PostCategory
/// Create a copy of SnPostCategory
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$PostCategoryCopyWith<_PostCategory> get copyWith => __$PostCategoryCopyWithImpl<_PostCategory>(this, _$identity);
_$SnPostCategoryCopyWith<_SnPostCategory> get copyWith => __$SnPostCategoryCopyWithImpl<_SnPostCategory>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$PostCategoryToJson(this, );
return _$SnPostCategoryToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -242,15 +242,15 @@ int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEqu
@override
String toString() {
return 'PostCategory(id: $id, slug: $slug, name: $name, posts: $posts)';
return 'SnPostCategory(id: $id, slug: $slug, name: $name, posts: $posts)';
}
}
/// @nodoc
abstract mixin class _$PostCategoryCopyWith<$Res> implements $PostCategoryCopyWith<$Res> {
factory _$PostCategoryCopyWith(_PostCategory value, $Res Function(_PostCategory) _then) = __$PostCategoryCopyWithImpl;
abstract mixin class _$SnPostCategoryCopyWith<$Res> implements $SnPostCategoryCopyWith<$Res> {
factory _$SnPostCategoryCopyWith(_SnPostCategory value, $Res Function(_SnPostCategory) _then) = __$SnPostCategoryCopyWithImpl;
@override @useResult
$Res call({
String id, String slug, String? name, List<SnPost> posts
@@ -261,17 +261,17 @@ $Res call({
}
/// @nodoc
class __$PostCategoryCopyWithImpl<$Res>
implements _$PostCategoryCopyWith<$Res> {
__$PostCategoryCopyWithImpl(this._self, this._then);
class __$SnPostCategoryCopyWithImpl<$Res>
implements _$SnPostCategoryCopyWith<$Res> {
__$SnPostCategoryCopyWithImpl(this._self, this._then);
final _PostCategory _self;
final $Res Function(_PostCategory) _then;
final _SnPostCategory _self;
final $Res Function(_SnPostCategory) _then;
/// Create a copy of PostCategory
/// Create a copy of SnPostCategory
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) {
return _then(_PostCategory(
return _then(_SnPostCategory(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable

View File

@@ -6,8 +6,8 @@ part of 'post_category.dart';
// JsonSerializableGenerator
// **************************************************************************
_PostCategory _$PostCategoryFromJson(Map<String, dynamic> json) =>
_PostCategory(
_SnPostCategory _$SnPostCategoryFromJson(Map<String, dynamic> json) =>
_SnPostCategory(
id: json['id'] as String,
slug: json['slug'] as String,
name: json['name'] as String?,
@@ -18,7 +18,7 @@ _PostCategory _$PostCategoryFromJson(Map<String, dynamic> json) =>
const [],
);
Map<String, dynamic> _$PostCategoryToJson(_PostCategory instance) =>
Map<String, dynamic> _$SnPostCategoryToJson(_SnPostCategory instance) =>
<String, dynamic>{
'id': instance.id,
'slug': instance.slug,

View File

@@ -1,4 +1,3 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/post.dart';
@@ -6,14 +5,14 @@ part 'post_tag.freezed.dart';
part 'post_tag.g.dart';
@freezed
sealed class PostTag with _$PostTag {
const factory PostTag({
sealed class SnPostTag with _$SnPostTag {
const factory SnPostTag({
required String id,
required String slug,
String? name,
@Default([]) List<SnPost> posts,
}) = _PostTag;
}) = _SnPostTag;
factory PostTag.fromJson(Map<String, dynamic> json) =>
_$PostTagFromJson(json);
factory SnPostTag.fromJson(Map<String, dynamic> json) =>
_$SnPostTagFromJson(json);
}

View File

@@ -13,22 +13,22 @@ part of 'post_tag.dart';
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$PostTag {
mixin _$SnPostTag {
String get id; String get slug; String? get name; List<SnPost> get posts;
/// Create a copy of PostTag
/// Create a copy of SnPostTag
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$PostTagCopyWith<PostTag> get copyWith => _$PostTagCopyWithImpl<PostTag>(this as PostTag, _$identity);
$SnPostTagCopyWith<SnPostTag> get copyWith => _$SnPostTagCopyWithImpl<SnPostTag>(this as SnPostTag, _$identity);
/// Serializes this PostTag to a JSON map.
/// Serializes this SnPostTag to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is PostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -37,15 +37,15 @@ int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEqu
@override
String toString() {
return 'PostTag(id: $id, slug: $slug, name: $name, posts: $posts)';
return 'SnPostTag(id: $id, slug: $slug, name: $name, posts: $posts)';
}
}
/// @nodoc
abstract mixin class $PostTagCopyWith<$Res> {
factory $PostTagCopyWith(PostTag value, $Res Function(PostTag) _then) = _$PostTagCopyWithImpl;
abstract mixin class $SnPostTagCopyWith<$Res> {
factory $SnPostTagCopyWith(SnPostTag value, $Res Function(SnPostTag) _then) = _$SnPostTagCopyWithImpl;
@useResult
$Res call({
String id, String slug, String? name, List<SnPost> posts
@@ -56,14 +56,14 @@ $Res call({
}
/// @nodoc
class _$PostTagCopyWithImpl<$Res>
implements $PostTagCopyWith<$Res> {
_$PostTagCopyWithImpl(this._self, this._then);
class _$SnPostTagCopyWithImpl<$Res>
implements $SnPostTagCopyWith<$Res> {
_$SnPostTagCopyWithImpl(this._self, this._then);
final PostTag _self;
final $Res Function(PostTag) _then;
final SnPostTag _self;
final $Res Function(SnPostTag) _then;
/// Create a copy of PostTag
/// Create a copy of SnPostTag
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) {
return _then(_self.copyWith(
@@ -78,8 +78,8 @@ as List<SnPost>,
}
/// Adds pattern-matching-related methods to [PostTag].
extension PostTagPatterns on PostTag {
/// Adds pattern-matching-related methods to [SnPostTag].
extension SnPostTagPatterns on SnPostTag {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
@@ -92,10 +92,10 @@ extension PostTagPatterns on PostTag {
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PostTag value)? $default,{required TResult orElse(),}){
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPostTag value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _PostTag() when $default != null:
case _SnPostTag() when $default != null:
return $default(_that);case _:
return orElse();
@@ -114,10 +114,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PostTag value) $default,){
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPostTag value) $default,){
final _that = this;
switch (_that) {
case _PostTag():
case _SnPostTag():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
@@ -132,10 +132,10 @@ return $default(_that);}
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PostTag value)? $default,){
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPostTag value)? $default,){
final _that = this;
switch (_that) {
case _PostTag() when $default != null:
case _SnPostTag() when $default != null:
return $default(_that);case _:
return null;
@@ -155,7 +155,7 @@ return $default(_that);case _:
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _PostTag() when $default != null:
case _SnPostTag() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
return orElse();
@@ -176,7 +176,7 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts) $default,) {final _that = this;
switch (_that) {
case _PostTag():
case _SnPostTag():
return $default(_that.id,_that.slug,_that.name,_that.posts);}
}
/// A variant of `when` that fallback to returning `null`
@@ -193,7 +193,7 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);}
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String? name, List<SnPost> posts)? $default,) {final _that = this;
switch (_that) {
case _PostTag() when $default != null:
case _SnPostTag() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
return null;
@@ -205,9 +205,9 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
/// @nodoc
@JsonSerializable()
class _PostTag implements PostTag {
const _PostTag({required this.id, required this.slug, this.name, final List<SnPost> posts = const []}): _posts = posts;
factory _PostTag.fromJson(Map<String, dynamic> json) => _$PostTagFromJson(json);
class _SnPostTag implements SnPostTag {
const _SnPostTag({required this.id, required this.slug, this.name, final List<SnPost> posts = const []}): _posts = posts;
factory _SnPostTag.fromJson(Map<String, dynamic> json) => _$SnPostTagFromJson(json);
@override final String id;
@override final String slug;
@@ -220,20 +220,20 @@ class _PostTag implements PostTag {
}
/// Create a copy of PostTag
/// Create a copy of SnPostTag
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$PostTagCopyWith<_PostTag> get copyWith => __$PostTagCopyWithImpl<_PostTag>(this, _$identity);
_$SnPostTagCopyWith<_SnPostTag> get copyWith => __$SnPostTagCopyWithImpl<_SnPostTag>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$PostTagToJson(this, );
return _$SnPostTagToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -242,15 +242,15 @@ int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEqu
@override
String toString() {
return 'PostTag(id: $id, slug: $slug, name: $name, posts: $posts)';
return 'SnPostTag(id: $id, slug: $slug, name: $name, posts: $posts)';
}
}
/// @nodoc
abstract mixin class _$PostTagCopyWith<$Res> implements $PostTagCopyWith<$Res> {
factory _$PostTagCopyWith(_PostTag value, $Res Function(_PostTag) _then) = __$PostTagCopyWithImpl;
abstract mixin class _$SnPostTagCopyWith<$Res> implements $SnPostTagCopyWith<$Res> {
factory _$SnPostTagCopyWith(_SnPostTag value, $Res Function(_SnPostTag) _then) = __$SnPostTagCopyWithImpl;
@override @useResult
$Res call({
String id, String slug, String? name, List<SnPost> posts
@@ -261,17 +261,17 @@ $Res call({
}
/// @nodoc
class __$PostTagCopyWithImpl<$Res>
implements _$PostTagCopyWith<$Res> {
__$PostTagCopyWithImpl(this._self, this._then);
class __$SnPostTagCopyWithImpl<$Res>
implements _$SnPostTagCopyWith<$Res> {
__$SnPostTagCopyWithImpl(this._self, this._then);
final _PostTag _self;
final $Res Function(_PostTag) _then;
final _SnPostTag _self;
final $Res Function(_SnPostTag) _then;
/// Create a copy of PostTag
/// Create a copy of SnPostTag
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) {
return _then(_PostTag(
return _then(_SnPostTag(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable

View File

@@ -6,7 +6,7 @@ part of 'post_tag.dart';
// JsonSerializableGenerator
// **************************************************************************
_PostTag _$PostTagFromJson(Map<String, dynamic> json) => _PostTag(
_SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) => _SnPostTag(
id: json['id'] as String,
slug: json['slug'] as String,
name: json['name'] as String?,
@@ -17,9 +17,10 @@ _PostTag _$PostTagFromJson(Map<String, dynamic> json) => _PostTag(
const [],
);
Map<String, dynamic> _$PostTagToJson(_PostTag instance) => <String, dynamic>{
'id': instance.id,
'slug': instance.slug,
'name': instance.name,
'posts': instance.posts.map((e) => e.toJson()).toList(),
};
Map<String, dynamic> _$SnPostTagToJson(_SnPostTag instance) =>
<String, dynamic>{
'id': instance.id,
'slug': instance.slug,
'name': instance.name,
'posts': instance.posts.map((e) => e.toJson()).toList(),
};

View File

@@ -35,6 +35,7 @@ sealed class SnStickerPack with _$SnStickerPack {
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
@Default([]) List<SnSticker> stickers,
}) = _SnStickerPack;
factory SnStickerPack.fromJson(Map<String, dynamic> json) =>

View File

@@ -338,7 +338,7 @@ $SnStickerPackCopyWith<$Res>? get pack {
/// @nodoc
mixin _$SnStickerPack {
String get id; String get name; String get description; String get prefix; String get publisherId; SnPublisher? get publisher; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String get name; String get description; String get prefix; String get publisherId; SnPublisher? get publisher; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List<SnSticker> get stickers;
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -351,16 +351,16 @@ $SnStickerPackCopyWith<SnStickerPack> get copyWith => _$SnStickerPackCopyWithImp
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other.stickers, stickers));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(stickers));
@override
String toString() {
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stickers: $stickers)';
}
@@ -371,7 +371,7 @@ abstract mixin class $SnStickerPackCopyWith<$Res> {
factory $SnStickerPackCopyWith(SnStickerPack value, $Res Function(SnStickerPack) _then) = _$SnStickerPackCopyWithImpl;
@useResult
$Res call({
String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers
});
@@ -388,7 +388,7 @@ class _$SnStickerPackCopyWithImpl<$Res>
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? stickers = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@@ -399,7 +399,8 @@ as String,publisher: freezed == publisher ? _self.publisher : publisher // ignor
as SnPublisher?,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?,
as DateTime?,stickers: null == stickers ? _self.stickers : stickers // ignore: cast_nullable_to_non_nullable
as List<SnSticker>,
));
}
/// Create a copy of SnStickerPack
@@ -493,10 +494,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnStickerPack() when $default != null:
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);case _:
return orElse();
}
@@ -514,10 +515,10 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers) $default,) {final _that = this;
switch (_that) {
case _SnStickerPack():
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);}
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -531,10 +532,10 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers)? $default,) {final _that = this;
switch (_that) {
case _SnStickerPack() when $default != null:
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);case _:
return null;
}
@@ -546,7 +547,7 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish
@JsonSerializable()
class _SnStickerPack implements SnStickerPack {
const _SnStickerPack({required this.id, required this.name, required this.description, required this.prefix, required this.publisherId, required this.publisher, required this.createdAt, required this.updatedAt, required this.deletedAt});
const _SnStickerPack({required this.id, required this.name, required this.description, required this.prefix, required this.publisherId, required this.publisher, required this.createdAt, required this.updatedAt, required this.deletedAt, final List<SnSticker> stickers = const []}): _stickers = stickers;
factory _SnStickerPack.fromJson(Map<String, dynamic> json) => _$SnStickerPackFromJson(json);
@override final String id;
@@ -558,6 +559,13 @@ class _SnStickerPack implements SnStickerPack {
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
final List<SnSticker> _stickers;
@override@JsonKey() List<SnSticker> get stickers {
if (_stickers is EqualUnmodifiableListView) return _stickers;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_stickers);
}
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@@ -572,16 +580,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._stickers, _stickers));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_stickers));
@override
String toString() {
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stickers: $stickers)';
}
@@ -592,7 +600,7 @@ abstract mixin class _$SnStickerPackCopyWith<$Res> implements $SnStickerPackCopy
factory _$SnStickerPackCopyWith(_SnStickerPack value, $Res Function(_SnStickerPack) _then) = __$SnStickerPackCopyWithImpl;
@override @useResult
$Res call({
String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers
});
@@ -609,7 +617,7 @@ class __$SnStickerPackCopyWithImpl<$Res>
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? stickers = null,}) {
return _then(_SnStickerPack(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@@ -620,7 +628,8 @@ as String,publisher: freezed == publisher ? _self.publisher : publisher // ignor
as SnPublisher?,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?,
as DateTime?,stickers: null == stickers ? _self._stickers : stickers // ignore: cast_nullable_to_non_nullable
as List<SnSticker>,
));
}

View File

@@ -54,6 +54,11 @@ _SnStickerPack _$SnStickerPackFromJson(Map<String, dynamic> json) =>
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
stickers:
(json['stickers'] as List<dynamic>?)
?.map((e) => SnSticker.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
);
Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) =>
@@ -67,4 +72,5 @@ Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) =>
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'stickers': instance.stickers.map((e) => e.toJson()).toList(),
};

View File

@@ -25,6 +25,32 @@ sealed class SnAccount with _$SnAccount {
_$SnAccountFromJson(json);
}
@freezed
sealed class ProfileLink with _$ProfileLink {
const factory ProfileLink({required String name, required String url}) =
_ProfileLink;
factory ProfileLink.fromJson(Map<String, dynamic> json) =>
_$ProfileLinkFromJson(json);
}
class ProfileLinkConverter
implements JsonConverter<List<ProfileLink>, dynamic> {
const ProfileLinkConverter();
@override
List<ProfileLink> fromJson(dynamic json) {
return json is List<dynamic>
? json.map((e) => ProfileLink.fromJson(e)).cast<ProfileLink>().toList()
: <ProfileLink>[];
}
@override
List<dynamic> toJson(List<ProfileLink> object) {
return object.map((e) => e.toJson()).toList();
}
}
@freezed
sealed class SnAccountProfile with _$SnAccountProfile {
const factory SnAccountProfile({
@@ -38,6 +64,7 @@ sealed class SnAccountProfile with _$SnAccountProfile {
@Default('') String location,
@Default('') String timeZone,
DateTime? birthday,
@ProfileLinkConverter() @Default([]) List<ProfileLink> links,
DateTime? lastSeenAt,
SnAccountBadge? activeBadge,
required int experience,

View File

@@ -347,10 +347,270 @@ $SnWalletSubscriptionRefCopyWith<$Res>? get perkSubscription {
}
/// @nodoc
mixin _$ProfileLink {
String get name; String get url;
/// Create a copy of ProfileLink
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ProfileLinkCopyWith<ProfileLink> get copyWith => _$ProfileLinkCopyWithImpl<ProfileLink>(this as ProfileLink, _$identity);
/// Serializes this ProfileLink to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProfileLink&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,url);
@override
String toString() {
return 'ProfileLink(name: $name, url: $url)';
}
}
/// @nodoc
abstract mixin class $ProfileLinkCopyWith<$Res> {
factory $ProfileLinkCopyWith(ProfileLink value, $Res Function(ProfileLink) _then) = _$ProfileLinkCopyWithImpl;
@useResult
$Res call({
String name, String url
});
}
/// @nodoc
class _$ProfileLinkCopyWithImpl<$Res>
implements $ProfileLinkCopyWith<$Res> {
_$ProfileLinkCopyWithImpl(this._self, this._then);
final ProfileLink _self;
final $Res Function(ProfileLink) _then;
/// Create a copy of ProfileLink
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? url = null,}) {
return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [ProfileLink].
extension ProfileLinkPatterns on ProfileLink {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ProfileLink value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ProfileLink() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ProfileLink value) $default,){
final _that = this;
switch (_that) {
case _ProfileLink():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ProfileLink value)? $default,){
final _that = this;
switch (_that) {
case _ProfileLink() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String url)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ProfileLink() when $default != null:
return $default(_that.name,_that.url);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String url) $default,) {final _that = this;
switch (_that) {
case _ProfileLink():
return $default(_that.name,_that.url);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String url)? $default,) {final _that = this;
switch (_that) {
case _ProfileLink() when $default != null:
return $default(_that.name,_that.url);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ProfileLink implements ProfileLink {
const _ProfileLink({required this.name, required this.url});
factory _ProfileLink.fromJson(Map<String, dynamic> json) => _$ProfileLinkFromJson(json);
@override final String name;
@override final String url;
/// Create a copy of ProfileLink
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ProfileLinkCopyWith<_ProfileLink> get copyWith => __$ProfileLinkCopyWithImpl<_ProfileLink>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ProfileLinkToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProfileLink&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,url);
@override
String toString() {
return 'ProfileLink(name: $name, url: $url)';
}
}
/// @nodoc
abstract mixin class _$ProfileLinkCopyWith<$Res> implements $ProfileLinkCopyWith<$Res> {
factory _$ProfileLinkCopyWith(_ProfileLink value, $Res Function(_ProfileLink) _then) = __$ProfileLinkCopyWithImpl;
@override @useResult
$Res call({
String name, String url
});
}
/// @nodoc
class __$ProfileLinkCopyWithImpl<$Res>
implements _$ProfileLinkCopyWith<$Res> {
__$ProfileLinkCopyWithImpl(this._self, this._then);
final _ProfileLink _self;
final $Res Function(_ProfileLink) _then;
/// Create a copy of ProfileLink
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? url = null,}) {
return _then(_ProfileLink(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
mixin _$SnAccountProfile {
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -363,16 +623,16 @@ $SnAccountProfileCopyWith<SnAccountProfile> get copyWith => _$SnAccountProfileCo
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -383,7 +643,7 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> {
factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
@useResult
$Res call({
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -400,7 +660,7 @@ class _$SnAccountProfileCopyWithImpl<$Res>
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
@@ -412,7 +672,8 @@ as String,pronouns: null == pronouns ? _self.pronouns : pronouns // ignore: cast
as String,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
as DateTime?,links: null == links ? _self.links : links // ignore: cast_nullable_to_non_nullable
as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
@@ -553,10 +814,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
@@ -574,10 +835,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnAccountProfile():
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -591,10 +852,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
@@ -606,7 +867,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
@JsonSerializable()
class _SnAccountProfile implements SnAccountProfile {
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt});
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
@override final String id;
@@ -619,6 +880,13 @@ class _SnAccountProfile implements SnAccountProfile {
@override@JsonKey() final String location;
@override@JsonKey() final String timeZone;
@override final DateTime? birthday;
final List<ProfileLink> _links;
@override@JsonKey()@ProfileLinkConverter() List<ProfileLink> get links {
if (_links is EqualUnmodifiableListView) return _links;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_links);
}
@override final DateTime? lastSeenAt;
@override final SnAccountBadge? activeBadge;
@override final int experience;
@@ -644,16 +912,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -664,7 +932,7 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi
factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
@override @useResult
$Res call({
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -681,7 +949,7 @@ class __$SnAccountProfileCopyWithImpl<$Res>
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAccountProfile(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
@@ -693,7 +961,8 @@ as String,pronouns: null == pronouns ? _self.pronouns : pronouns // ignore: cast
as String,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
as DateTime?,links: null == links ? _self._links : links // ignore: cast_nullable_to_non_nullable
as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable

View File

@@ -47,6 +47,12 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_ProfileLink _$ProfileLinkFromJson(Map<String, dynamic> json) =>
_ProfileLink(name: json['name'] as String, url: json['url'] as String);
Map<String, dynamic> _$ProfileLinkToJson(_ProfileLink instance) =>
<String, dynamic>{'name': instance.name, 'url': instance.url};
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
_SnAccountProfile(
id: json['id'] as String,
@@ -62,6 +68,10 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
json['birthday'] == null
? null
: DateTime.parse(json['birthday'] as String),
links:
json['links'] == null
? const []
: const ProfileLinkConverter().fromJson(json['links']),
lastSeenAt:
json['last_seen_at'] == null
? null
@@ -111,6 +121,7 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
'location': instance.location,
'time_zone': instance.timeZone,
'birthday': instance.birthday?.toIso8601String(),
'links': const ProfileLinkConverter().toJson(instance.links),
'last_seen_at': instance.lastSeenAt?.toIso8601String(),
'active_badge': instance.activeBadge?.toJson(),
'experience': instance.experience,

View File

@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -18,6 +17,8 @@ sealed class WebSocketState with _$WebSocketState {
const factory WebSocketState.connected() = _Connected;
const factory WebSocketState.connecting() = _Connecting;
const factory WebSocketState.disconnected() = _Disconnected;
const factory WebSocketState.serverDown() = _ServerDown;
const factory WebSocketState.duplicateDevice() = _DuplicateDevice;
const factory WebSocketState.error(String message) = _Error;
}
@@ -49,7 +50,7 @@ class WebSocketService {
Timer? _heartbeatTimer;
DateTime? _heartbeatAt;
Duration? _heartbeatDelay;
Duration? heartbeatDelay;
Stream<WebSocketPacket> get dataStream => _streamController.stream;
Stream<WebSocketState> get statusStream => _statusStreamController.stream;
@@ -81,15 +82,20 @@ class WebSocketService {
final dataStr =
data is Uint8List ? utf8.decode(data) : data.toString();
final packet = WebSocketPacket.fromJson(jsonDecode(dataStr));
if (packet.type == 'error.dupe') {
_statusStreamController.sink.add(WebSocketState.duplicateDevice());
_channel!.sink.close();
return;
}
_streamController.sink.add(packet);
log(
"[WebSocket] Received packet: ${packet.type} ${packet.errorMessage}",
);
if (packet.type == 'pong' && _heartbeatAt != null) {
var now = DateTime.now();
_heartbeatDelay = now.difference(_heartbeatAt!);
heartbeatDelay = now.difference(_heartbeatAt!);
log(
"[WebSocket] Server respond last heartbeat for ${_heartbeatDelay!.inMilliseconds} ms",
"[WebSocket] Server respond last heartbeat for ${heartbeatDelay!.inMilliseconds} ms",
);
}
},

View File

@@ -61,13 +61,15 @@ extension WebSocketStatePatterns on WebSocketState {
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( _Connected value)? connected,TResult Function( _Connecting value)? connecting,TResult Function( _Disconnected value)? disconnected,TResult Function( _Error value)? error,required TResult orElse(),}){
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( _Connected value)? connected,TResult Function( _Connecting value)? connecting,TResult Function( _Disconnected value)? disconnected,TResult Function( _ServerDown value)? serverDown,TResult Function( _DuplicateDevice value)? duplicateDevice,TResult Function( _Error value)? error,required TResult orElse(),}){
final _that = this;
switch (_that) {
case _Connected() when connected != null:
return connected(_that);case _Connecting() when connecting != null:
return connecting(_that);case _Disconnected() when disconnected != null:
return disconnected(_that);case _Error() when error != null:
return disconnected(_that);case _ServerDown() when serverDown != null:
return serverDown(_that);case _DuplicateDevice() when duplicateDevice != null:
return duplicateDevice(_that);case _Error() when error != null:
return error(_that);case _:
return orElse();
@@ -86,13 +88,15 @@ return error(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( _Connected value) connected,required TResult Function( _Connecting value) connecting,required TResult Function( _Disconnected value) disconnected,required TResult Function( _Error value) error,}){
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( _Connected value) connected,required TResult Function( _Connecting value) connecting,required TResult Function( _Disconnected value) disconnected,required TResult Function( _ServerDown value) serverDown,required TResult Function( _DuplicateDevice value) duplicateDevice,required TResult Function( _Error value) error,}){
final _that = this;
switch (_that) {
case _Connected():
return connected(_that);case _Connecting():
return connecting(_that);case _Disconnected():
return disconnected(_that);case _Error():
return disconnected(_that);case _ServerDown():
return serverDown(_that);case _DuplicateDevice():
return duplicateDevice(_that);case _Error():
return error(_that);}
}
/// A variant of `map` that fallback to returning `null`.
@@ -107,13 +111,15 @@ return error(_that);}
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( _Connected value)? connected,TResult? Function( _Connecting value)? connecting,TResult? Function( _Disconnected value)? disconnected,TResult? Function( _Error value)? error,}){
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( _Connected value)? connected,TResult? Function( _Connecting value)? connecting,TResult? Function( _Disconnected value)? disconnected,TResult? Function( _ServerDown value)? serverDown,TResult? Function( _DuplicateDevice value)? duplicateDevice,TResult? Function( _Error value)? error,}){
final _that = this;
switch (_that) {
case _Connected() when connected != null:
return connected(_that);case _Connecting() when connecting != null:
return connecting(_that);case _Disconnected() when disconnected != null:
return disconnected(_that);case _Error() when error != null:
return disconnected(_that);case _ServerDown() when serverDown != null:
return serverDown(_that);case _DuplicateDevice() when duplicateDevice != null:
return duplicateDevice(_that);case _Error() when error != null:
return error(_that);case _:
return null;
@@ -131,12 +137,14 @@ return error(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function()? connected,TResult Function()? connecting,TResult Function()? disconnected,TResult Function( String message)? error,required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function()? connected,TResult Function()? connecting,TResult Function()? disconnected,TResult Function()? serverDown,TResult Function()? duplicateDevice,TResult Function( String message)? error,required TResult orElse(),}) {final _that = this;
switch (_that) {
case _Connected() when connected != null:
return connected();case _Connecting() when connecting != null:
return connecting();case _Disconnected() when disconnected != null:
return disconnected();case _Error() when error != null:
return disconnected();case _ServerDown() when serverDown != null:
return serverDown();case _DuplicateDevice() when duplicateDevice != null:
return duplicateDevice();case _Error() when error != null:
return error(_that.message);case _:
return orElse();
@@ -155,12 +163,14 @@ return error(_that.message);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function() connected,required TResult Function() connecting,required TResult Function() disconnected,required TResult Function( String message) error,}) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function() connected,required TResult Function() connecting,required TResult Function() disconnected,required TResult Function() serverDown,required TResult Function() duplicateDevice,required TResult Function( String message) error,}) {final _that = this;
switch (_that) {
case _Connected():
return connected();case _Connecting():
return connecting();case _Disconnected():
return disconnected();case _Error():
return disconnected();case _ServerDown():
return serverDown();case _DuplicateDevice():
return duplicateDevice();case _Error():
return error(_that.message);}
}
/// A variant of `when` that fallback to returning `null`
@@ -175,12 +185,14 @@ return error(_that.message);}
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function()? connected,TResult? Function()? connecting,TResult? Function()? disconnected,TResult? Function( String message)? error,}) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function()? connected,TResult? Function()? connecting,TResult? Function()? disconnected,TResult? Function()? serverDown,TResult? Function()? duplicateDevice,TResult? Function( String message)? error,}) {final _that = this;
switch (_that) {
case _Connected() when connected != null:
return connected();case _Connecting() when connecting != null:
return connecting();case _Disconnected() when disconnected != null:
return disconnected();case _Error() when error != null:
return disconnected();case _ServerDown() when serverDown != null:
return serverDown();case _DuplicateDevice() when duplicateDevice != null:
return duplicateDevice();case _Error() when error != null:
return error(_that.message);case _:
return null;
@@ -303,6 +315,82 @@ String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
/// @nodoc
class _ServerDown with DiagnosticableTreeMixin implements WebSocketState {
const _ServerDown();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'WebSocketState.serverDown'))
;
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerDown);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'WebSocketState.serverDown()';
}
}
/// @nodoc
class _DuplicateDevice with DiagnosticableTreeMixin implements WebSocketState {
const _DuplicateDevice();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'WebSocketState.duplicateDevice'))
;
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DuplicateDevice);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'WebSocketState.duplicateDevice()';
}
}
/// @nodoc

View File

@@ -7,6 +7,7 @@ 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_category_detail.dart';
import 'package:island/screens/posts/post_search.dart';
import 'package:island/widgets/app_wrapper.dart';
import 'package:island/screens/tabs.dart';
@@ -28,6 +29,8 @@ import 'package:island/screens/creators/hub.dart';
import 'package:island/screens/creators/posts/post_manage_list.dart';
import 'package:island/screens/creators/stickers/stickers.dart';
import 'package:island/screens/creators/stickers/pack_detail.dart';
import 'package:island/screens/stickers/marketplace.dart';
import 'package:island/screens/stickers/pack_detail.dart';
import 'package:island/screens/creators/poll/poll_list.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
@@ -320,15 +323,6 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const AboutScreen(),
),
GoRoute(
name: 'reportDetail',
path: '/safety/reports/me/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return AbuseReportDetailScreen(reportId: id);
},
),
// Main tabs with TabsScreen shell
ShellRoute(
navigatorKey: _tabsShellKey,
@@ -355,6 +349,25 @@ final routerProvider = Provider<GoRouter>((ref) {
return PostDetailScreen(id: id);
},
),
GoRoute(
name: 'postCategoryDetail',
path: '/posts/categories/:slug',
builder: (context, state) {
final slug = state.pathParameters['slug']!;
return PostCategoryDetailScreen(slug: slug, isCategory: true);
},
),
GoRoute(
name: 'postTagDetail',
path: '/posts/tags/:slug',
builder: (context, state) {
final slug = state.pathParameters['slug']!;
return PostCategoryDetailScreen(
slug: slug,
isCategory: false,
);
},
),
GoRoute(
name: 'publisherProfile',
path: '/publishers/:name',
@@ -451,6 +464,23 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/account',
builder: (context, state) => const AccountScreen(),
),
// Sticker marketplace (user-facing, no publisher)
GoRoute(
name: 'stickerMarketplace',
path: '/stickers',
builder:
(context, state) => const MarketplaceStickersScreen(),
routes: [
GoRoute(
name: 'stickerPackDetail',
path: ':packId',
builder: (context, state) {
final packId = state.pathParameters['packId']!;
return MarketplaceStickerPackDetailScreen(id: packId);
},
),
],
),
GoRoute(
name: 'notifications',
path: '/account/notifications',
@@ -486,6 +516,14 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/safety/reports/me',
builder: (context, state) => const AbuseReportListScreen(),
),
GoRoute(
name: 'reportDetail',
path: '/safety/reports/me/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return AbuseReportDetailScreen(reportId: id);
},
),
],
),

View File

@@ -7,10 +7,12 @@ import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/services/udid.native.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/services/update_service.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:url_launcher/url_launcher_string.dart';
@@ -100,196 +102,226 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
? const Center(child: CircularProgressIndicator())
: _errorMessage != null
? Center(child: Text(_errorMessage!))
: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 24),
// App Icon and Name
CircleAvatar(
radius: 50,
backgroundColor: theme.colorScheme.primary.withOpacity(
0.1,
),
child: Image.asset(
'assets/icons/icon.png',
width: 56,
height: 56,
),
),
const SizedBox(height: 16),
Text(
_packageInfo.appName,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'aboutScreenVersionInfo'.tr(
args: [_packageInfo.version, _packageInfo.buildNumber],
),
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.textTheme.bodySmall?.color,
),
),
const SizedBox(height: 32),
// App Info Card
_buildSection(
context,
title: 'aboutScreenAppInfoSectionTitle'.tr(),
: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 540),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildInfoItem(
context,
icon: Symbols.info,
label: 'aboutScreenPackageNameLabel'.tr(),
value: _packageInfo.packageName,
),
_buildInfoItem(
context,
icon: Symbols.update,
label: 'aboutScreenVersionLabel'.tr(),
value: _packageInfo.version,
),
_buildInfoItem(
context,
icon: Symbols.build,
label: 'aboutScreenBuildNumberLabel'.tr(),
value: _packageInfo.buildNumber,
),
],
),
if (_deviceInfo != null) const SizedBox(height: 16),
if (_deviceInfo != null)
_buildSection(
context,
title: 'Device Information',
children: [
_buildInfoItem(
context,
icon: Symbols.label,
label: 'aboutDeviceName'.tr(),
value: _deviceInfo?.data['name'],
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,
),
_buildInfoItem(
context,
icon: Symbols.fingerprint,
label: 'aboutDeviceIdentifier'.tr(),
value: _deviceUdid ?? 'N/A',
copyable: true,
),
const SizedBox(height: 16),
Text(
_packageInfo.appName,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
],
),
const SizedBox(height: 16),
// Links Card
_buildSection(
context,
title: 'aboutScreenLinksSectionTitle'.tr(),
children: [
_buildListTile(
context,
icon: Symbols.privacy_tip,
title: 'aboutScreenPrivacyPolicyTitle'.tr(),
onTap:
() => _launchURL(
'https://solsynth.dev/terms/privacy-policy',
),
),
_buildListTile(
context,
icon: Symbols.description,
title: 'aboutScreenTermsOfServiceTitle'.tr(),
onTap:
() => _launchURL(
'https://solsynth.dev/terms/basic-law',
),
),
_buildListTile(
context,
icon: Symbols.code,
title: 'aboutScreenOpenSourceLicensesTitle'.tr(),
onTap: () {
showLicensePage(
context: context,
applicationName: _packageInfo.appName,
applicationVersion:
'Version ${_packageInfo.version}',
);
},
),
],
),
const SizedBox(height: 16),
// Developer Info
_buildSection(
context,
title: 'aboutScreenDeveloperSectionTitle'.tr(),
children: [
_buildListTile(
context,
icon: Symbols.email,
title: 'aboutScreenContactUsTitle'.tr(),
subtitle: 'lily@solsynth.dev',
onTap: () => _launchURL('mailto:lily@solsynth.dev'),
),
_buildListTile(
context,
icon: Symbols.copyright,
title: 'aboutScreenLicenseTitle'.tr(),
subtitle: 'aboutScreenLicenseContent'.tr(
args: [DateTime.now().year.toString()],
Text(
'aboutScreenVersionInfo'.tr(
args: [
_packageInfo.version,
_packageInfo.buildNumber,
],
),
onTap:
() => _launchURL(
'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt',
),
),
if (kIsWeb || !(Platform.isMacOS || Platform.isIOS))
_buildListTile(
context,
icon: Symbols.favorite,
title: 'donate'.tr(),
subtitle: 'donateDescription'.tr(),
onTap: () {
launchUrlString(
'https://afdian.com/@littlesheep',
);
},
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.textTheme.bodySmall?.color,
),
],
),
),
const SizedBox(height: 32),
const SizedBox(height: 32),
// Copyright
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text(
'aboutScreenCopyright'.tr(
args: [DateTime.now().year.toString()],
// App Info Card
_buildSection(
context,
title: 'aboutScreenAppInfoSectionTitle'.tr(),
children: [
_buildInfoItem(
context,
icon: Symbols.info,
label: 'aboutScreenPackageNameLabel'.tr(),
value: _packageInfo.packageName,
),
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
const Gap(1),
Text(
'aboutScreenMadeWith'.tr(),
textAlign: TextAlign.center,
).fontSize(10).opacity(0.8),
],
),
),
_buildInfoItem(
context,
icon: Symbols.update,
label: 'aboutScreenVersionLabel'.tr(),
value: _packageInfo.version,
),
_buildInfoItem(
context,
icon: Symbols.build,
label: 'aboutScreenBuildNumberLabel'.tr(),
value: _packageInfo.buildNumber,
),
],
),
Gap(MediaQuery.of(context).padding.bottom + 16),
],
if (_deviceInfo != null) const SizedBox(height: 16),
if (_deviceInfo != null)
_buildSection(
context,
title: 'Device Information',
children: [
_buildInfoItem(
context,
icon: Symbols.label,
label: 'aboutDeviceName'.tr(),
value: _deviceInfo?.data['name'],
),
_buildInfoItem(
context,
icon: Symbols.fingerprint,
label: 'aboutDeviceIdentifier'.tr(),
value: _deviceUdid ?? 'N/A',
copyable: true,
),
],
),
const SizedBox(height: 16),
// Links Card
_buildSection(
context,
title: 'aboutScreenLinksSectionTitle'.tr(),
children: [
_buildListTile(
context,
icon: Symbols.system_update,
title: 'Check for updates',
onTap: () async {
// Fetch latest release and show the unified sheet
final svc = UpdateService();
// Reuse service fetch + compare to decide content
showLoadingModal(context);
final release = await svc.fetchLatestRelease();
if (!context.mounted) return;
hideLoadingModal(context);
if (release != null) {
await svc.showUpdateSheet(context, release);
} else {
showInfoAlert(
'Currently cannot get update from the GitHub.',
'Unable to check for updates',
);
}
},
),
_buildListTile(
context,
icon: Symbols.privacy_tip,
title: 'aboutScreenPrivacyPolicyTitle'.tr(),
onTap:
() => _launchURL(
'https://solsynth.dev/terms/privacy-policy',
),
),
_buildListTile(
context,
icon: Symbols.description,
title: 'aboutScreenTermsOfServiceTitle'.tr(),
onTap:
() => _launchURL(
'https://solsynth.dev/terms/user-agreement',
),
),
_buildListTile(
context,
icon: Symbols.code,
title: 'aboutScreenOpenSourceLicensesTitle'.tr(),
onTap: () {
showLicensePage(
context: context,
applicationName: _packageInfo.appName,
applicationVersion:
'Version ${_packageInfo.version}',
);
},
),
],
),
const SizedBox(height: 16),
// Developer Info
_buildSection(
context,
title: 'aboutScreenDeveloperSectionTitle'.tr(),
children: [
_buildListTile(
context,
icon: Symbols.email,
title: 'aboutScreenContactUsTitle'.tr(),
subtitle: 'lily@solsynth.dev',
onTap:
() => _launchURL('mailto:lily@solsynth.dev'),
),
_buildListTile(
context,
icon: Symbols.copyright,
title: 'aboutScreenLicenseTitle'.tr(),
subtitle: 'aboutScreenLicenseContent'.tr(
args: [DateTime.now().year.toString()],
),
onTap:
() => _launchURL(
'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt',
),
),
if (kIsWeb || !(Platform.isMacOS || Platform.isIOS))
_buildListTile(
context,
icon: Symbols.favorite,
title: 'donate'.tr(),
subtitle: 'donateDescription'.tr(),
onTap: () {
launchUrlString(
'https://afdian.com/@littlesheep',
);
},
),
],
),
const SizedBox(height: 32),
// Copyright
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text(
'aboutScreenCopyright'.tr(
args: [DateTime.now().year.toString()],
),
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
const Gap(1),
Text(
'aboutScreenMadeWith'.tr(),
textAlign: TextAlign.center,
).fontSize(10).opacity(0.8),
],
),
),
Gap(MediaQuery.of(context).padding.bottom + 16),
],
),
),
),
),
);

View File

@@ -1,12 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/message.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/screens/notification.dart';
import 'package:island/services/responsive.dart';
@@ -15,6 +11,7 @@ import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/account/leveling_progress.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/debug_sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -189,7 +186,6 @@ class AccountScreen extends HookConsumerWidget {
),
],
).padding(horizontal: 8),
const Gap(8),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.notifications),
@@ -228,6 +224,16 @@ class AccountScreen extends HookConsumerWidget {
context.pushNamed('relationships');
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.emoji_emotions),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('stickers').tr(),
onTap: () {
context.pushNamed('stickerMarketplace');
},
),
ListTile(
minTileHeight: 48,
title: Text('abuseReports').tr(),
@@ -267,30 +273,6 @@ class AccountScreen extends HookConsumerWidget {
context.pushNamed('accountSettings');
},
),
if (kDebugMode) const Divider(height: 1).padding(vertical: 8),
if (kDebugMode)
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.copy_all),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('Copy access token'),
onTap: () async {
final tk = ref.watch(tokenProvider);
Clipboard.setData(ClipboardData(text: tk!.token));
},
),
if (kDebugMode)
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.delete),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('Reset database'),
onTap: () async {
resetDatabase(ref);
},
),
const Divider(height: 1).padding(vertical: 8),
ListTile(
minTileHeight: 48,
@@ -302,6 +284,19 @@ class AccountScreen extends HookConsumerWidget {
context.pushNamed('about');
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.bug_report),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('debugOptions').tr(),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => DebugSheet(),
);
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.logout),

View File

@@ -3,9 +3,11 @@ 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:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
@@ -94,6 +96,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
final usernameController = useTextEditingController(text: user.value!.name);
final nicknameController = useTextEditingController(text: user.value!.nick);
final language = useState(user.value!.language);
final links = useState<List<ProfileLink>>(user.value!.profile.links);
void updateBasicInfo() async {
if (!formKeyBasicInfo.currentState!.validate()) return;
@@ -165,6 +168,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
'location': locationController.text,
'time_zone': timeZoneController.text,
'birthday': birthday.value?.toUtc().toIso8601String(),
'links': links.value,
},
);
final userNotifier = ref.read(userInfoProvider.notifier);
@@ -558,6 +562,73 @@ class UpdateProfileScreen extends HookConsumerWidget {
),
),
),
Text('links').tr().bold().fontSize(18).padding(top: 16),
Column(
spacing: 8,
children: [
for (var i = 0; i < links.value.length; i++)
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: TextFormField(
initialValue: links.value[i].name,
decoration: InputDecoration(
labelText: 'linkKey'.tr(),
isDense: true,
),
onChanged: (value) {
links.value[i] = links.value[i].copyWith(
name: value,
);
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
),
const Gap(8),
Expanded(
child: TextFormField(
initialValue: links.value[i].url,
decoration: InputDecoration(
labelText: 'linkValue'.tr(),
isDense: true,
),
onChanged: (value) {
links.value[i] = links.value[i].copyWith(
url: value,
);
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
),
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
links.value = List.from(links.value)
..removeAt(i);
},
),
],
),
Align(
alignment: Alignment.centerRight,
child: FilledButton.icon(
onPressed: () {
links.value = List.from(links.value)
..add(ProfileLink(name: '', url: ''));
},
label: Text('addLink').tr(),
icon: const Icon(Symbols.add),
).padding(top: 8),
),
],
),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(

View File

@@ -1,5 +1,6 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart';
@@ -13,6 +14,7 @@ import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/color.dart';
import 'package:island/services/responsive.dart';
import 'package:island/services/text.dart';
import 'package:island/services/time.dart';
import 'package:island/services/timezone/native.dart';
import 'package:island/widgets/account/account_name.dart';
@@ -30,6 +32,7 @@ import 'package:palette_generator/palette_generator.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:share_plus/share_plus.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
part 'profile.g.dart';
@@ -194,6 +197,15 @@ class AccountProfileScreen extends HookConsumerWidget {
List<Widget> buildSubcolumn(SnAccount data) {
return [
Row(
spacing: 6,
children: [
const Icon(Symbols.join, size: 17, fill: 1),
Text(
'joinedAt'.tr(args: [data.createdAt.formatCustom('yyyy-MM-dd')]),
),
],
),
if (data.profile.birthday != null)
Row(
spacing: 6,
@@ -320,7 +332,7 @@ class AccountProfileScreen extends HookConsumerWidget {
spacing: 2,
children: buildSubcolumn(data),
),
if (data.profile.timeZone.isNotEmpty)
if (data.profile.timeZone.isNotEmpty && !kIsWeb)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -350,6 +362,32 @@ class AccountProfileScreen extends HookConsumerWidget {
).padding(horizontal: 24, vertical: 16),
);
Widget accountProfileLinks(SnAccount data) => Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4),
for (final link in data.profile.links)
ListTile(
title: Text(link.name.capitalizeEachWord()),
subtitle: Text(link.url),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Symbols.chevron_right),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
onTap: () {
if (!link.url.startsWith('http') && !link.url.contains('://')) {
launchUrlString('https://${link.url}');
} else {
launchUrlString(link.url);
}
},
),
],
),
);
Widget accountAction(SnAccount data) => Card(
child: Column(
children: [
@@ -452,7 +490,7 @@ class AccountProfileScreen extends HookConsumerWidget {
],
),
],
).padding(horizontal: 16, vertical: 8),
).padding(horizontal: 16, vertical: 12),
);
return account.when(
@@ -509,9 +547,11 @@ class AccountProfileScreen extends HookConsumerWidget {
SliverToBoxAdapter(child: accountBasicInfo(data)),
if (data.badges.isNotEmpty)
SliverToBoxAdapter(
child: BadgeList(
badges: data.badges,
).padding(horizontal: 24, bottom: 24),
child: Card(
child: BadgeList(
badges: data.badges,
).padding(horizontal: 26, vertical: 20),
).padding(left: 2, right: 4),
),
SliverToBoxAdapter(
child: Column(
@@ -521,9 +561,10 @@ class AccountProfileScreen extends HookConsumerWidget {
level: data.profile.level,
experience: data.profile.experience,
progress: data.profile.levelingProgress,
),
).padding(left: 2, right: 4),
if (data.profile.verification != null)
Card(
margin: EdgeInsets.zero,
child: VerificationStatusCard(
mark: data.profile.verification!,
),
@@ -534,6 +575,10 @@ class AccountProfileScreen extends HookConsumerWidget {
SliverToBoxAdapter(
child: accountProfileBio(data).padding(top: 4),
),
if (data.profile.links.isNotEmpty)
SliverToBoxAdapter(
child: accountProfileLinks(data),
),
SliverToBoxAdapter(
child: accountProfileDetail(data),
),
@@ -604,9 +649,11 @@ class AccountProfileScreen extends HookConsumerWidget {
SliverToBoxAdapter(child: accountBasicInfo(data)),
if (data.badges.isNotEmpty)
SliverToBoxAdapter(
child: BadgeList(
badges: data.badges,
).padding(horizontal: 24, bottom: 24),
child: Card(
child: BadgeList(
badges: data.badges,
).padding(horizontal: 26, vertical: 20),
).padding(horizontal: 4),
),
SliverToBoxAdapter(
child: Column(
@@ -628,6 +675,12 @@ class AccountProfileScreen extends HookConsumerWidget {
SliverToBoxAdapter(
child: accountProfileBio(data).padding(horizontal: 4),
),
if (data.profile.links.isNotEmpty)
SliverToBoxAdapter(
child: accountProfileLinks(
data,
).padding(horizontal: 4),
),
SliverToBoxAdapter(
child: accountProfileDetail(
data,

View File

@@ -35,6 +35,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'chat.dart';
import 'package:island/widgets/chat/call_button.dart';
import 'package:island/widgets/stickers/picker.dart';
part 'room.g.dart';
@@ -338,7 +339,7 @@ class ChatRoomScreen extends HookConsumerWidget {
}
await apiClient.post(
'/chat/${chatRoom.value!.id}/members/me',
'/sphere/chat/${chatRoom.value!.id}/members/me',
);
ref.invalidate(chatroomIdentityProvider(id));
} catch (err) {
@@ -928,7 +929,7 @@ class ChatRoomScreen extends HookConsumerWidget {
if (attachment.isOnCloud) {
final client = ref.watch(apiClientProvider);
await client.delete(
'/files/${attachment.data.id}',
'/drive/files/${attachment.data.id}',
);
}
final clone = List.of(attachments.value);
@@ -1066,15 +1067,19 @@ class _ChatInput extends HookConsumerWidget {
scrollDirection: Axis.horizontal,
itemCount: attachments.length,
itemBuilder: (context, idx) {
return AttachmentPreview(
item: attachments[idx],
onRequestUpload: () => onUploadAttachment(idx),
onDelete: () => onDeleteAttachment(idx),
onUpdate: (value) {
attachments[idx] = value;
onAttachmentsChanged(attachments);
},
onMove: (delta) => onMoveAttachment(idx, delta),
return SizedBox(
height: 280,
width: 280,
child: AttachmentPreview(
item: attachments[idx],
onRequestUpload: () => onUploadAttachment(idx),
onDelete: () => onDeleteAttachment(idx),
onUpdate: (value) {
attachments[idx] = value;
onAttachmentsChanged(attachments);
},
onMove: (delta) => onMoveAttachment(idx, delta),
),
);
},
separatorBuilder: (_, _) => const Gap(8),
@@ -1129,31 +1134,76 @@ class _ChatInput extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
child: Row(
children: [
PopupMenuButton(
icon: const Icon(Symbols.photo_library),
itemBuilder:
(context) => [
PopupMenuItem(
onTap: () => onPickFile(true),
child: Row(
spacing: 12,
children: [
const Icon(Symbols.photo),
Text('addPhoto').tr(),
],
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'stickers'.tr(),
icon: const Icon(Symbols.emoji_symbols),
onPressed: () {
final size = MediaQuery.of(context).size;
showStickerPickerPopover(
context,
Offset(
20,
size.height -
480 -
MediaQuery.of(context).padding.bottom,
),
),
PopupMenuItem(
onTap: () => onPickFile(false),
child: Row(
spacing: 12,
children: [
const Icon(Symbols.video_call),
Text('addVideo').tr(),
],
),
),
],
onPick: (placeholder) {
// Insert placeholder at current cursor position
final text = messageController.text;
final selection = messageController.selection;
final start =
selection.start >= 0
? selection.start
: text.length;
final end =
selection.end >= 0
? selection.end
: text.length;
final newText = text.replaceRange(
start,
end,
placeholder,
);
messageController.value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(
offset: start + placeholder.length,
),
);
},
);
},
),
PopupMenuButton(
icon: const Icon(Symbols.photo_library),
itemBuilder:
(context) => [
PopupMenuItem(
onTap: () => onPickFile(true),
child: Row(
spacing: 12,
children: [
const Icon(Symbols.photo),
Text('addPhoto').tr(),
],
),
),
PopupMenuItem(
onTap: () => onPickFile(false),
child: Row(
spacing: 12,
children: [
const Icon(Symbols.video_call),
Text('addVideo').tr(),
],
),
),
],
),
],
),
Expanded(
child: RawKeyboardListener(

View File

@@ -4,6 +4,8 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/poll.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/poll/poll_feedback.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
@@ -69,7 +71,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
return AppScaffold(
appBar: AppBar(title: const Text('Polls')),
floatingActionButton: FloatingActionButton(
onPressed: () => _createPoll(context),
@@ -164,10 +166,13 @@ class _CreatorPollItem extends StatelessWidget {
],
),
onTap: () {
// Open editor for edit
// Navigator push by path to keep consistency with rest of app:
// Note: pub name string may be required in route; when absent, route may need query or pick later.
// For safety, just do nothing if no publisher in list item.
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder:
(context) => PollFeedbackSheet(pollId: poll.id, poll: poll),
);
},
),
);

View File

@@ -58,7 +58,7 @@ class StickerPackDetailScreen extends HookConsumerWidget {
try {
showLoadingModal(context);
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete('/stickers/$id/content/${sticker.id}');
await apiClient.delete('/sphere/stickers/$id/content/${sticker.id}');
ref.invalidate(stickerPackContentProvider(id));
} catch (err) {
showErrorAlert(err);
@@ -297,7 +297,7 @@ class _StickerPackActionMenu extends HookConsumerWidget {
).then((confirm) {
if (confirm) {
final client = ref.watch(apiClientProvider);
client.delete('/stickers/$packId');
client.delete('/sphere/stickers/$packId');
ref.invalidate(stickerPacksNotifierProvider);
if (context.mounted) context.pop(true);
}
@@ -325,7 +325,7 @@ Future<SnSticker?> stickerPackSticker(
if (query == null) return null;
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get(
'/stickers/${query.packId}/content/${query.id}',
'/sphere/stickers/${query.packId}/content/${query.id}',
);
if (resp.data == null) return null;
return SnSticker.fromJson(resp.data);
@@ -379,8 +379,8 @@ class EditStickersScreen extends HookConsumerWidget {
try {
final resp = await apiClient.request(
id == null
? '/stickers/$packId/content'
: '/stickers/$packId/content/$id',
? '/sphere/stickers/$packId/content'
: '/sphere/stickers/$packId/content/$id',
data: {'slug': slugController.text, 'image_id': imageController.text},
options: Options(method: id == null ? 'POST' : 'PATCH'),
);

View File

@@ -151,7 +151,7 @@ class _StickerPackContentProviderElement
}
String _$stickerPackStickerHash() =>
r'36f524c047e632236d5597aaaa8678ed86599602';
r'5c553666b3a63530bdebae4b7cd52f303c5ab3a0';
/// See also [stickerPackSticker].
@ProviderFor(stickerPackSticker)

View File

@@ -31,7 +31,7 @@ class StickersScreen extends HookConsumerWidget {
context
.pushNamed(
'creatorStickerPackNew',
queryParameters: {'name': pubName},
pathParameters: {'name': pubName},
)
.then((value) {
if (value != null) {
@@ -187,10 +187,8 @@ class EditStickerPacksScreen extends HookConsumerWidget {
'description': descriptionController.text,
'prefix': prefixController.text,
},
options: Options(
method: packId == null ? 'POST' : 'PATCH',
headers: {'X-Pub': pubName},
),
queryParameters: {'pub': pubName},
options: Options(method: packId == null ? 'POST' : 'PATCH'),
);
if (!context.mounted) return;
context.pop(SnStickerPack.fromJson(resp.data));

View File

@@ -114,10 +114,11 @@ class WebFeedEditScreen extends HookConsumerWidget {
return feedAsync.when(
loading:
() =>
const Scaffold(body: Center(child: CircularProgressIndicator())),
() => const AppScaffold(
body: Center(child: CircularProgressIndicator()),
),
error:
(error, stack) => Scaffold(
(error, stack) => AppScaffold(
appBar: AppBar(title: const Text('Error')),
body: Center(child: Text('Error: $error')),
),

View File

@@ -18,7 +18,7 @@ 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');
final resp = await client.get('/develop/developers/$publisherName/apps');
return resp.data.map((e) => CustomApp.fromJson(e)).cast<CustomApp>().toList();
}
@@ -37,7 +37,10 @@ class CustomAppsScreen extends HookConsumerWidget {
IconButton(
icon: const Icon(Symbols.add),
onPressed: () {
context.pushNamed('developerAppNew', pathParameters: {'name': publisherName});
context.pushNamed(
'developerAppNew',
pathParameters: {'name': publisherName},
);
},
),
],
@@ -121,7 +124,13 @@ class CustomAppsScreen extends HookConsumerWidget {
],
onSelected: (value) {
if (value == 'edit') {
context.pushNamed('developerAppEdit', pathParameters: {'name': publisherName, 'id': app.id});
context.pushNamed(
'developerAppEdit',
pathParameters: {
'name': publisherName,
'id': app.id,
},
);
} else if (value == 'delete') {
showConfirmAlert(
'deleteCustomAppHint'.tr(),
@@ -130,7 +139,7 @@ class CustomAppsScreen extends HookConsumerWidget {
if (confirm) {
final client = ref.read(apiClientProvider);
client.delete(
'/developers/$publisherName/apps/${app.id}',
'/develop/developers/$publisherName/apps/${app.id}',
);
ref.invalidate(
customAppsProvider(publisherName),

View File

@@ -6,7 +6,7 @@ part of 'apps.dart';
// RiverpodGenerator
// **************************************************************************
String _$customAppsHash() => r'1dec11573b9d987c3adbdf4732b3781a6f40172a';
String _$customAppsHash() => r'c6ac78060eb51a2b208a749a81ecbe0a9c608ce1';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -24,7 +24,7 @@ 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');
final resp = await client.get('/develop/developers/$publisherName/apps/$id');
return CustomApp.fromJson(resp.data);
}
@@ -282,9 +282,15 @@ class EditAppScreen extends HookConsumerWidget {
: null,
};
if (isNew) {
await client.post('/developers/$publisherName/apps', data: data);
await client.post(
'/develop/developers/$publisherName/apps',
data: data,
);
} else {
await client.patch('/developers/$publisherName/apps/$id', data: data);
await client.patch(
'/develop/developers/$publisherName/apps/$id',
data: data,
);
}
ref.invalidate(customAppsProvider(publisherName));
if (context.mounted) {

View File

@@ -6,7 +6,7 @@ part of 'edit_app.dart';
// RiverpodGenerator
// **************************************************************************
String _$customAppHash() => r'aa4d1fb803c47a99cbacf6d91481f4fce3fda457';
String _$customAppHash() => r'42ad937b8439c793e3c5c35568bb5fa4da017df3';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -25,17 +25,17 @@ part 'hub.g.dart';
Future<DeveloperStats?> developerStats(Ref ref, String? uname) async {
if (uname == null) return null;
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/sphere/developers/$uname/stats');
final resp = await apiClient.get('/develop/developers/$uname/stats');
return DeveloperStats.fromJson(resp.data);
}
@riverpod
Future<List<SnPublisher>> developers(Ref ref) async {
Future<List<SnDeveloper>> developers(Ref ref) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/sphere/developers');
final resp = await client.get('/develop/developers');
return resp.data
.map((e) => SnPublisher.fromJson(e))
.cast<SnPublisher>()
.map((e) => SnDeveloper.fromJson(e))
.cast<SnDeveloper>()
.toList();
}
@@ -74,25 +74,25 @@ class DeveloperHubScreen extends HookConsumerWidget {
}
final developers = ref.watch(developersProvider);
final currentDeveloper = useState<SnPublisher?>(
final currentDeveloper = useState<SnDeveloper?>(
developers.value?.firstOrNull,
);
final List<DropdownMenuItem<SnPublisher>> developersMenu = developers.when(
final List<DropdownMenuItem<SnDeveloper>> developersMenu = developers.when(
data:
(data) =>
data
.map(
(item) => DropdownMenuItem<SnPublisher>(
(item) => DropdownMenuItem<SnDeveloper>(
value: item,
child: ListTile(
minTileHeight: 48,
leading: ProfilePictureWidget(
radius: 16,
fileId: item.picture?.id,
fileId: item.publisher?.picture?.id,
),
title: Text(item.nick),
subtitle: Text('@${item.name}'),
title: Text(item.publisher!.nick),
subtitle: Text('@${item.publisher!.name}'),
trailing:
currentDeveloper.value?.id == item.id
? const Icon(Icons.check)
@@ -107,7 +107,7 @@ class DeveloperHubScreen extends HookConsumerWidget {
);
final developerStats = ref.watch(
developerStatsProvider(currentDeveloper.value?.name),
developerStatsProvider(currentDeveloper.value?.publisher?.name),
);
return AppScaffold(
@@ -117,7 +117,7 @@ class DeveloperHubScreen extends HookConsumerWidget {
title: Text('developerHub').tr(),
actions: [
DropdownButtonHideUnderline(
child: DropdownButton2<SnPublisher>(
child: DropdownButton2<SnDeveloper>(
alignment: Alignment.centerRight,
value: currentDeveloper.value,
hint: CircleAvatar(
@@ -139,7 +139,7 @@ class DeveloperHubScreen extends HookConsumerWidget {
...developersMenu.map(
(e) => ProfilePictureWidget(
radius: 16,
fileId: e.value?.picture?.id,
fileId: e.value?.publisher?.picture?.id,
).center().padding(right: 8),
),
];
@@ -193,10 +193,12 @@ class DeveloperHubScreen extends HookConsumerWidget {
...(developers.value?.map(
(developer) => ListTile(
leading: ProfilePictureWidget(
file: developer.picture,
file: developer.publisher?.picture,
),
title: Text(developer.publisher!.nick),
subtitle: Text(
'@${developer.publisher!.name}',
),
title: Text(developer.nick),
subtitle: Text('@${developer.name}'),
onTap: () {
currentDeveloper.value = developer;
},
@@ -243,7 +245,8 @@ class DeveloperHubScreen extends HookConsumerWidget {
context.pushNamed(
'developerApps',
pathParameters: {
'name': currentDeveloper.value!.name,
'name':
currentDeveloper.value!.publisher!.name,
},
);
},
@@ -257,7 +260,9 @@ class DeveloperHubScreen extends HookConsumerWidget {
error: err,
onRetry: () {
ref.invalidate(
developerStatsProvider(currentDeveloper.value?.name),
developerStatsProvider(
currentDeveloper.value?.publisher!.name,
),
);
},
),
@@ -336,7 +341,7 @@ class _DeveloperEnrollmentSheet extends HookConsumerWidget {
Future<void> enroll(SnPublisher publisher) async {
try {
final client = ref.read(apiClientProvider);
await client.post('/sphere/developers/${publisher.name}/enroll');
await client.post('/develop/developers/${publisher.name}/enroll');
if (context.mounted) {
Navigator.pop(context, true);
}
@@ -354,7 +359,7 @@ class _DeveloperEnrollmentSheet extends HookConsumerWidget {
? Center(
child:
Text(
'noPublishersToEnroll',
'noDevelopersToEnroll',
textAlign: TextAlign.center,
).tr(),
)

View File

@@ -6,7 +6,7 @@ part of 'hub.dart';
// RiverpodGenerator
// **************************************************************************
String _$developerStatsHash() => r'baa708f3586e8987e221cc8ab825d759658c0f55';
String _$developerStatsHash() => r'45546f29ec7cd1a9c3a4e0f4e39275e78bf34755';
/// Copied from Dart SDK
class _SystemHash {
@@ -149,12 +149,12 @@ class _DeveloperStatsProviderElement
String? get uname => (origin as DeveloperStatsProvider).uname;
}
String _$developersHash() => r'f11335fdf553c661110281edeec70ef89c64727d';
String _$developersHash() => r'252341098617ac398ce133994453f318dd3edbd2';
/// See also [developers].
@ProviderFor(developers)
final developersProvider =
AutoDisposeFutureProvider<List<SnPublisher>>.internal(
AutoDisposeFutureProvider<List<SnDeveloper>>.internal(
developers,
name: r'developersProvider',
debugGetCreateSourceHash:
@@ -167,6 +167,6 @@ final developersProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnPublisher>>;
typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnDeveloper>>;
// 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

@@ -12,11 +12,11 @@ import 'package:island/models/webfeed.dart';
import 'package:island/pods/event_calendar.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/event_calendar.dart';
import 'package:island/widgets/account/fortune_graph.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/models/post.dart';
import 'package:island/widgets/check_in.dart';
import 'package:island/widgets/post/post_featured.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/screens/tabs.dart';
import 'package:material_symbols_icons/symbols.dart';
@@ -70,15 +70,6 @@ class ExploreScreen extends HookConsumerWidget {
final events = ref.watch(eventCalendarProvider(query.value));
final selectedDay = useState(now);
void onMonthChanged(int year, int month) {
query.value = EventCalendarQuery(
uname: query.value.uname,
year: year,
month: month,
);
}
// Function to handle day selection for synchronizing between widgets
void onDaySelected(DateTime day) {
selectedDay.value = day;
@@ -218,21 +209,16 @@ class ExploreScreen extends HookConsumerWidget {
right: 12,
top: 16,
),
onChecked: () {
ref.invalidate(
eventCalendarProvider(query.value),
);
},
),
Card(
margin: EdgeInsets.only(left: 8, right: 12, top: 8),
child: Column(
children: [
// Use the reusable EventCalendarWidget
EventCalendarWidget(
events: events,
initialDate: now,
showEventDetails: true,
onMonthChanged: onMonthChanged,
onDaySelected: onDaySelected,
),
],
),
PostFeaturedList().padding(
left: 8,
right: 12,
top: 8,
),
FortuneGraphWidget(
margin: EdgeInsets.only(left: 8, right: 12, top: 8),
@@ -403,6 +389,10 @@ class _ActivityListView extends HookConsumerWidget {
margin: EdgeInsets.only(left: 8, right: 8, bottom: 4),
),
),
if (!contentOnly)
SliverToBoxAdapter(
child: PostFeaturedList().padding(horizontal: 8, bottom: 4, top: 4),
),
SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {

View File

@@ -9,6 +9,7 @@ import 'package:gap/gap.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/models/poll.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:uuid/uuid.dart';
class PollEditorState {
@@ -413,7 +414,7 @@ class PollEditorScreen extends ConsumerWidget {
});
}
return Scaffold(
return AppScaffold(
appBar: AppBar(
title: Text(model.id == null ? 'Create Poll' : 'Edit Poll'),
actions: [
@@ -428,175 +429,175 @@ class PollEditorScreen extends ConsumerWidget {
const Gap(8),
],
),
body: SafeArea(
child: Form(
key: ValueKey(model.id),
child: ListView(
padding: const EdgeInsets.all(16),
children: [
TextFormField(
initialValue: model.title ?? '',
decoration: const InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
),
textInputAction: TextInputAction.next,
maxLength: 256,
onChanged: notifier.setTitle,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
validator: (v) {
if (v == null || v.trim().isEmpty) {
return 'Title is required';
}
return null;
},
),
const Gap(12),
TextFormField(
initialValue: model.description ?? '',
decoration: const InputDecoration(
labelText: 'Description',
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
),
maxLines: 3,
maxLength: 4096,
onChanged: notifier.setDescription,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
_EndDatePicker(
value: model.endedAt,
onChanged: notifier.setEndedAt,
),
const Gap(24),
Row(
body: Column(
children: [
Expanded(
child: Form(
key: ValueKey(model.id),
child: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
'Questions',
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
MenuAnchor(
builder: (context, controller, child) {
return FilledButton.icon(
onPressed: () {
controller.isOpen
? controller.close()
: controller.open();
},
icon: const Icon(Icons.add),
label: const Text('Add question'),
);
TextFormField(
initialValue: model.title ?? '',
decoration: const InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
),
textInputAction: TextInputAction.next,
maxLength: 256,
onChanged: notifier.setTitle,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
validator: (v) {
if (v == null || v.trim().isEmpty) {
return 'Title is required';
}
return null;
},
menuChildren:
SnPollQuestionType.values
.map(
(t) => MenuItemButton(
leadingIcon: Icon(_iconForType(t)),
onPressed: () => notifier.addQuestion(t),
child: Text(_labelForType(t)),
),
)
.toList(),
),
const Gap(12),
TextFormField(
initialValue: model.description ?? '',
decoration: const InputDecoration(
labelText: 'Description',
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
),
maxLines: 3,
maxLength: 4096,
onChanged: notifier.setDescription,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
_EndDatePicker(
value: model.endedAt,
onChanged: notifier.setEndedAt,
),
const Gap(24),
Row(
children: [
Text(
'Questions',
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
MenuAnchor(
builder: (context, controller, child) {
return FilledButton.icon(
onPressed: () {
controller.isOpen
? controller.close()
: controller.open();
},
icon: const Icon(Icons.add),
label: const Text('Add question'),
);
},
menuChildren:
SnPollQuestionType.values
.map(
(t) => MenuItemButton(
leadingIcon: Icon(_iconForType(t)),
onPressed: () => notifier.addQuestion(t),
child: Text(_labelForType(t)),
),
)
.toList(),
),
],
),
const Gap(8),
if (model.questions.isEmpty)
_EmptyState(
title: 'No questions yet',
subtitle:
'Use "Add question" to start building your poll.',
)
else
ReorderableListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: model.questions.length,
onReorder: (oldIndex, newIndex) {
// Convert to stepwise moves using provided functions
if (newIndex > oldIndex) newIndex -= 1;
final steps = newIndex - oldIndex;
if (steps == 0) return;
if (steps > 0) {
for (int i = 0; i < steps; i++) {
notifier.moveQuestionDown(oldIndex + i);
}
} else {
for (int i = 0; i > steps; i--) {
notifier.moveQuestionUp(oldIndex + i);
}
}
},
buildDefaultDragHandles: false,
itemBuilder: (context, index) {
final q = model.questions[index];
return Card(
key: ValueKey('q_$index'),
margin: const EdgeInsets.symmetric(vertical: 8),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
_QuestionHeader(
index: index,
question: q,
onMoveUp:
index > 0
? () => notifier.moveQuestionUp(index)
: null,
onMoveDown:
index < model.questions.length - 1
? () => notifier.moveQuestionDown(index)
: null,
onDelete: () => notifier.removeQuestion(index),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.all(16),
child: _QuestionEditor(
index: index,
question: q,
),
),
],
),
);
},
),
const Gap(96),
],
),
const Gap(8),
if (model.questions.isEmpty)
_EmptyState(
title: 'No questions yet',
subtitle: 'Use "Add question" to start building your poll.',
)
else
ReorderableListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: model.questions.length,
onReorder: (oldIndex, newIndex) {
// Convert to stepwise moves using provided functions
if (newIndex > oldIndex) newIndex -= 1;
final steps = newIndex - oldIndex;
if (steps == 0) return;
if (steps > 0) {
for (int i = 0; i < steps; i++) {
notifier.moveQuestionDown(oldIndex + i);
}
} else {
for (int i = 0; i > steps; i--) {
notifier.moveQuestionUp(oldIndex + i);
}
}
},
buildDefaultDragHandles: false,
itemBuilder: (context, index) {
final q = model.questions[index];
return Card(
key: ValueKey('q_$index'),
margin: const EdgeInsets.symmetric(vertical: 8),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
_QuestionHeader(
index: index,
question: q,
onMoveUp:
index > 0
? () => notifier.moveQuestionUp(index)
: null,
onMoveDown:
index < model.questions.length - 1
? () => notifier.moveQuestionDown(index)
: null,
onDelete: () => notifier.removeQuestion(index),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.all(16),
child: _QuestionEditor(index: index, question: q),
),
],
),
);
},
),
const Gap(96),
),
),
Row(
children: [
OutlinedButton.icon(
onPressed: () {
Navigator.of(context).maybePop();
},
icon: const Icon(Icons.close),
label: const Text('Cancel'),
),
const Spacer(),
FilledButton.icon(
onPressed: () {
_submitPoll(context, ref);
},
icon: const Icon(Icons.cloud_upload_outlined),
label: Text(model.id == null ? 'Create' : 'Update'),
),
],
),
),
),
bottomNavigationBar: Padding(
padding: EdgeInsets.fromLTRB(
16,
8,
16,
16 + MediaQuery.of(context).padding.bottom,
),
child: Row(
children: [
OutlinedButton.icon(
onPressed: () {
Navigator.of(context).maybePop();
},
icon: const Icon(Icons.close),
label: const Text('Cancel'),
),
const Spacer(),
FilledButton.icon(
onPressed: () {
_submitPoll(context, ref);
},
icon: const Icon(Icons.cloud_upload_outlined),
label: Text(model.id == null ? 'Create' : 'Update'),
),
],
),
],
),
);
}

View File

@@ -205,17 +205,7 @@ class PostComposeScreen extends HookConsumerWidget {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => ComposeSettingsSheet(
titleController: state.titleController,
descriptionController: state.descriptionController,
visibility: state.visibility,
tagsController: state.tagsController,
categoriesController: state.categoriesController,
onVisibilityChanged: () {
// Trigger rebuild if needed
},
),
builder: (context) => ComposeSettingsSheet(state: state),
);
}
@@ -369,31 +359,73 @@ class PostComposeScreen extends HookConsumerWidget {
// Post content form
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Content field with borderless design
RawKeyboardListener(
focusNode: FocusNode(),
onKey:
(event) => ComposeLogic.handleKeyPress(
event,
state,
ref,
context,
originalPost: originalPost,
repliedPost: repliedPost,
forwardedPost: forwardedPost,
child: KeyboardListener(
focusNode: FocusNode(),
onKeyEvent:
(event) => ComposeLogic.handleKeyPress(
event,
state,
ref,
context,
originalPost: originalPost,
repliedPost: repliedPost,
forwardedPost: forwardedPost,
),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: state.titleController,
decoration: InputDecoration(
hintText: 'postTitle'.tr(),
border: InputBorder.none,
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: TextField(
),
style: theme.textTheme.titleMedium,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
TextField(
controller: state.descriptionController,
decoration: InputDecoration(
hintText: 'postDescription'.tr(),
border: InputBorder.none,
isCollapsed: true,
contentPadding: const EdgeInsets.fromLTRB(
8,
4,
8,
12,
),
),
style: theme.textTheme.bodyMedium,
minLines: 1,
maxLines: 3,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
// Content field with borderless design
TextField(
controller: state.contentController,
style: theme.textTheme.bodyMedium,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'postContent'.tr(),
contentPadding: const EdgeInsets.all(8),
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
),
maxLines: null,
onTapOutside:
@@ -401,23 +433,23 @@ class PostComposeScreen extends HookConsumerWidget {
FocusManager.instance.primaryFocus
?.unfocus(),
),
),
const Gap(8),
const Gap(8),
// Attachments preview
if (state.attachments.value.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
final isWide = isWideScreen(context);
return isWide
? buildWideAttachmentGrid()
: buildNarrowAttachmentList();
},
)
else
const SizedBox.shrink(),
],
// Attachments preview
if (state.attachments.value.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
final isWide = isWideScreen(context);
return isWide
? buildWideAttachmentGrid()
: buildNarrowAttachmentList();
},
)
else
const SizedBox.shrink(),
],
),
),
),
),

View File

@@ -138,17 +138,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => ComposeSettingsSheet(
titleController: state.titleController,
descriptionController: state.descriptionController,
visibility: state.visibility,
tagsController: state.tagsController,
categoriesController: state.categoriesController,
onVisibilityChanged: () {
// Trigger rebuild if needed
},
),
builder: (context) => ComposeSettingsSheet(state: state),
);
}
@@ -242,10 +232,39 @@ class ArticleComposeScreen extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: state.titleController,
decoration: InputDecoration(
hintText: 'postTitle'.tr(),
border: InputBorder.none,
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
),
style: theme.textTheme.titleMedium,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
TextField(
controller: state.descriptionController,
decoration: InputDecoration(
hintText: 'postDescription'.tr(),
border: InputBorder.none,
isCollapsed: true,
contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
),
style: theme.textTheme.bodyMedium,
minLines: 1,
maxLines: 3,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Expanded(
child: RawKeyboardListener(
child: KeyboardListener(
focusNode: FocusNode(),
onKey:
onKeyEvent:
(event) => _handleKeyPress(
event,
state,
@@ -454,7 +473,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
flex: showPreview.value ? 1 : 2,
child: buildEditorPane(),
),
const VerticalDivider(),
if (showPreview.value) const VerticalDivider(),
if (showPreview.value)
Expanded(child: buildPreviewPane()),
],
@@ -475,7 +494,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
// Helper method to handle keyboard shortcuts
void _handleKeyPress(
RawKeyEvent event,
KeyEvent event,
ComposeState state,
WidgetRef ref,
BuildContext context, {
@@ -485,7 +504,9 @@ class ArticleComposeScreen extends HookConsumerWidget {
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
final isSave = event.logicalKey == LogicalKeyboardKey.keyS;
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
final isModifierPressed =
HardwareKeyboard.instance.isMetaPressed ||
HardwareKeyboard.instance.isControlPressed;
final isSubmit = event.logicalKey == LogicalKeyboardKey.enter;
if (isPaste && isModifierPressed) {

View File

@@ -0,0 +1,107 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_list.dart';
import 'package:island/widgets/response.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'post_category_detail.g.dart';
@riverpod
Future<SnPostCategory> postCategory(Ref ref, String slug) async {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/sphere/posts/categories/$slug');
return SnPostCategory.fromJson(resp.data);
}
@riverpod
Future<SnPostTag> postTag(Ref ref, String slug) async {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/sphere/posts/tags/$slug');
return SnPostTag.fromJson(resp.data);
}
class PostCategoryDetailScreen extends HookConsumerWidget {
final String slug;
final bool isCategory;
const PostCategoryDetailScreen({
super.key,
required this.slug,
required this.isCategory,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final postCategory =
isCategory ? ref.watch(postCategoryProvider(slug)) : null;
final postTag = isCategory ? null : ref.watch(postTagProvider(slug));
final postFilterTitle =
isCategory
? postCategory?.value?.categoryDisplayTitle ?? 'loading'
: postTag?.value?.name ?? postTag?.value?.slug ?? 'loading';
return AppScaffold(
isNoBackground: false,
appBar: AppBar(title: Text(postFilterTitle).tr()),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isCategory)
postCategory!.when(
data:
(category) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(category.categoryDisplayTitle).bold().fontSize(15),
Text('A category'),
],
).padding(horizontal: 24, vertical: 16),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(postCategoryProvider(slug)),
),
loading: () => ResponseLoadingWidget(),
)
else
postTag!.when(
data:
(tag) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(tag.name ?? '#${tag.slug}').bold().fontSize(15),
Text('A tag'),
],
).padding(horizontal: 24, vertical: 16),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(postTagProvider(slug)),
),
loading: () => ResponseLoadingWidget(),
),
const Divider(height: 1),
Expanded(
child: CustomScrollView(
slivers: [
const SliverGap(4),
SliverPostList(
categories: isCategory ? [slug] : null,
tags: isCategory ? null : [slug],
),
SliverGap(MediaQuery.of(context).padding.bottom + 8),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,270 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'post_category_detail.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$postCategoryHash() => r'0df2de729ba96819ee37377314615abef0c99547';
/// 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 [postCategory].
@ProviderFor(postCategory)
const postCategoryProvider = PostCategoryFamily();
/// See also [postCategory].
class PostCategoryFamily extends Family<AsyncValue<SnPostCategory>> {
/// See also [postCategory].
const PostCategoryFamily();
/// See also [postCategory].
PostCategoryProvider call(String slug) {
return PostCategoryProvider(slug);
}
@override
PostCategoryProvider getProviderOverride(
covariant PostCategoryProvider provider,
) {
return call(provider.slug);
}
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'postCategoryProvider';
}
/// See also [postCategory].
class PostCategoryProvider extends AutoDisposeFutureProvider<SnPostCategory> {
/// See also [postCategory].
PostCategoryProvider(String slug)
: this._internal(
(ref) => postCategory(ref as PostCategoryRef, slug),
from: postCategoryProvider,
name: r'postCategoryProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$postCategoryHash,
dependencies: PostCategoryFamily._dependencies,
allTransitiveDependencies:
PostCategoryFamily._allTransitiveDependencies,
slug: slug,
);
PostCategoryProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.slug,
}) : super.internal();
final String slug;
@override
Override overrideWith(
FutureOr<SnPostCategory> Function(PostCategoryRef provider) create,
) {
return ProviderOverride(
origin: this,
override: PostCategoryProvider._internal(
(ref) => create(ref as PostCategoryRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
slug: slug,
),
);
}
@override
AutoDisposeFutureProviderElement<SnPostCategory> createElement() {
return _PostCategoryProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PostCategoryProvider && other.slug == slug;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, slug.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PostCategoryRef on AutoDisposeFutureProviderRef<SnPostCategory> {
/// The parameter `slug` of this provider.
String get slug;
}
class _PostCategoryProviderElement
extends AutoDisposeFutureProviderElement<SnPostCategory>
with PostCategoryRef {
_PostCategoryProviderElement(super.provider);
@override
String get slug => (origin as PostCategoryProvider).slug;
}
String _$postTagHash() => r'e050fdf9af81a843a9abd9cf979dd2672e0a2b93';
/// See also [postTag].
@ProviderFor(postTag)
const postTagProvider = PostTagFamily();
/// See also [postTag].
class PostTagFamily extends Family<AsyncValue<SnPostTag>> {
/// See also [postTag].
const PostTagFamily();
/// See also [postTag].
PostTagProvider call(String slug) {
return PostTagProvider(slug);
}
@override
PostTagProvider getProviderOverride(covariant PostTagProvider provider) {
return call(provider.slug);
}
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'postTagProvider';
}
/// See also [postTag].
class PostTagProvider extends AutoDisposeFutureProvider<SnPostTag> {
/// See also [postTag].
PostTagProvider(String slug)
: this._internal(
(ref) => postTag(ref as PostTagRef, slug),
from: postTagProvider,
name: r'postTagProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$postTagHash,
dependencies: PostTagFamily._dependencies,
allTransitiveDependencies: PostTagFamily._allTransitiveDependencies,
slug: slug,
);
PostTagProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.slug,
}) : super.internal();
final String slug;
@override
Override overrideWith(
FutureOr<SnPostTag> Function(PostTagRef provider) create,
) {
return ProviderOverride(
origin: this,
override: PostTagProvider._internal(
(ref) => create(ref as PostTagRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
slug: slug,
),
);
}
@override
AutoDisposeFutureProviderElement<SnPostTag> createElement() {
return _PostTagProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PostTagProvider && other.slug == slug;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, slug.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PostTagRef on AutoDisposeFutureProviderRef<SnPostTag> {
/// The parameter `slug` of this provider.
String get slug;
}
class _PostTagProviderElement
extends AutoDisposeFutureProviderElement<SnPostTag>
with PostTagRef {
_PostTagProviderElement(super.provider);
@override
String get slug => (origin as PostTagProvider).slug;
}
// 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

@@ -92,6 +92,7 @@ class PostDetailScreen extends HookConsumerWidget {
right: 0,
child: Material(
elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainer,
child: postState
.when(
data:
@@ -107,8 +108,8 @@ class PostDetailScreen extends HookConsumerWidget {
error: (_, _) => const SizedBox.shrink(),
)
.padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
top: 16,
bottom: MediaQuery.of(context).padding.bottom + 8,
top: 8,
horizontal: 16,
),
),

View File

@@ -87,13 +87,22 @@ class PublisherProfileScreen extends HookConsumerWidget {
publisherAppbarForcegroundColorProvider(name),
);
final categoryTabController = useTabController(initialLength: 3);
final categoryTab = useState(0);
categoryTabController.addListener(() {
categoryTab.value = categoryTabController.index;
});
final subscribing = useState(false);
Future<void> subscribe() async {
final apiClient = ref.watch(apiClientProvider);
subscribing.value = true;
try {
await apiClient.post("/publishers/$name/subscribe", data: {'tier': 0});
await apiClient.post(
"/sphere/publishers/$name/subscribe",
data: {'tier': 0},
);
ref.invalidate(publisherSubscriptionStatusProvider(name));
HapticFeedback.heavyImpact();
} catch (err) {
@@ -107,7 +116,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
final apiClient = ref.watch(apiClientProvider);
subscribing.value = true;
try {
await apiClient.post("/publishers/$name/unsubscribe");
await apiClient.post("/sphere/publishers/$name/unsubscribe");
ref.invalidate(publisherSubscriptionStatusProvider(name));
HapticFeedback.heavyImpact();
} catch (err) {
@@ -268,6 +277,16 @@ class PublisherProfileScreen extends HookConsumerWidget {
).padding(horizontal: 20, vertical: 16),
);
Widget publisherCategoryTabWidget() => Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: TabBar(
controller: categoryTabController,
dividerColor: Colors.transparent,
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
tabs: [Tab(text: 'All'), Tab(text: 'Posts'), Tab(text: 'Articles')],
),
);
return publisher.when(
data:
(data) => AppScaffold(
@@ -321,7 +340,18 @@ class PublisherProfileScreen extends HookConsumerWidget {
child: CustomScrollView(
slivers: [
SliverGap(16),
SliverPostList(pubName: name),
SliverToBoxAdapter(
child: publisherCategoryTabWidget(),
),
SliverPostList(
key: ValueKey(categoryTab.value),
pubName: name,
type: switch (categoryTab.value) {
1 => 0,
2 => 1,
_ => null,
},
),
SliverGap(
MediaQuery.of(context).padding.bottom + 16,
),
@@ -334,9 +364,9 @@ class PublisherProfileScreen extends HookConsumerWidget {
alignment: Alignment.topLeft,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
publisherBasisWidget(data),
publisherBasisWidget(data).padding(bottom: 8),
publisherBadgesWidget(data),
publisherVerificationWidget(data),
publisherBioWidget(data),
@@ -398,7 +428,16 @@ class PublisherProfileScreen extends HookConsumerWidget {
child: publisherVerificationWidget(data),
),
SliverToBoxAdapter(child: publisherBioWidget(data)),
SliverPostList(pubName: name),
SliverToBoxAdapter(child: publisherCategoryTabWidget()),
SliverPostList(
key: ValueKey(categoryTab.value),
pubName: name,
type: switch (categoryTab.value) {
1 => 0,
2 => 1,
_ => null,
},
),
SliverGap(MediaQuery.of(context).padding.bottom + 16),
],
),

View File

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

View File

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

View File

@@ -0,0 +1,230 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/sticker.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/stickers/stickers.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'pack_detail.g.dart'; // generated by riverpod_annotation build_runner
/// Marketplace version of sticker pack detail page (no publisher dependency).
/// Shows all stickers in the pack and provides a button to add the sticker.
/// API interactions are intentionally left blank per request.
@riverpod
Future<List<SnSticker>> marketplaceStickerPackContent(
Ref ref, {
required String packId,
}) async {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/sphere/stickers/$packId/content');
return (resp.data as List).map((e) => SnSticker.fromJson(e)).toList();
}
@riverpod
Future<bool> marketplaceStickerPackOwnership(
Ref ref, {
required String packId,
}) async {
final api = ref.watch(apiClientProvider);
try {
await api.get('/sphere/stickers/$packId/own');
// If not 404, consider owned
return true;
} on Object catch (e) {
// Dio error handling agnostic: treat 404 as not-owned, rethrow others
final msg = e.toString();
if (msg.contains('404')) return false;
rethrow;
}
}
class MarketplaceStickerPackDetailScreen extends HookConsumerWidget {
final String id;
const MarketplaceStickerPackDetailScreen({super.key, required this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Pack metadata provider exists globally in creators file; reuse it.
final pack = ref.watch(stickerPackProvider(id));
final packContent = ref.watch(
marketplaceStickerPackContentProvider(packId: id),
);
final owned = ref.watch(
marketplaceStickerPackOwnershipProvider(packId: id),
);
// Add entire pack to user's collection
Future<void> addPackToMyCollection() async {
final apiClient = ref.watch(apiClientProvider);
await apiClient.post('/sphere/stickers/$id/own');
HapticFeedback.selectionClick();
ref.invalidate(marketplaceStickerPackOwnershipProvider(packId: id));
if (!context.mounted) return;
showSnackBar('stickerPackAdded'.tr());
}
// Remove ownership of the pack
Future<void> removePackFromMyCollection() async {
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete('/sphere/stickers/$id/own');
HapticFeedback.selectionClick();
ref.invalidate(marketplaceStickerPackOwnershipProvider(packId: id));
if (!context.mounted) return;
showSnackBar('stickerPackRemoved'.tr());
}
return AppScaffold(
appBar: AppBar(title: Text(pack.value?.name ?? 'loading'.tr())),
body: pack.when(
data: (p) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Pack meta
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(p?.description ?? ''),
Row(
spacing: 4,
children: [
const Icon(Symbols.folder, size: 16),
Text(
'${packContent.value?.length ?? 0}/24',
style: GoogleFonts.robotoMono(),
),
],
).opacity(0.85),
Row(
spacing: 4,
children: [
const Icon(Symbols.sell, size: 16),
Text(p?.prefix ?? '', style: GoogleFonts.robotoMono()),
],
).opacity(0.85),
Row(
spacing: 4,
children: [
const Icon(Symbols.tag, size: 16),
SelectableText(
p?.id ?? id,
style: GoogleFonts.robotoMono(),
),
],
).opacity(0.85),
],
).padding(horizontal: 24, vertical: 24),
const Divider(height: 1),
// Stickers grid
Expanded(
child: packContent.when(
data:
(stickers) => RefreshIndicator(
onRefresh:
() => ref.refresh(
marketplaceStickerPackContentProvider(
packId: id,
).future,
),
child: GridView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 20,
),
gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 96,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
),
itemCount: stickers.length,
itemBuilder: (context, index) {
final sticker = stickers[index];
return Tooltip(
message: ':${p?.prefix ?? ''}${sticker.slug}:',
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: Container(
decoration: BoxDecoration(
color:
Theme.of(
context,
).colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: AspectRatio(
aspectRatio: 1,
child: CloudImageWidget(
fileId: sticker.imageId,
fit: BoxFit.contain,
),
),
),
),
);
},
),
),
error:
(err, _) =>
Text(
'Error: $err',
).textAlignment(TextAlign.center).center(),
loading: () => const CircularProgressIndicator().center(),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: owned.when(
data:
(isOwned) => FilledButton.icon(
onPressed:
isOwned
? removePackFromMyCollection
: addPackToMyCollection,
icon: Icon(
isOwned ? Symbols.remove_circle : Symbols.add_circle,
),
label: Text(
isOwned ? 'removePack'.tr() : 'addPack'.tr(),
),
),
loading:
() => const SizedBox(
height: 32,
width: 32,
child: CircularProgressIndicator(strokeWidth: 2),
),
error:
(_, _) => OutlinedButton.icon(
onPressed: addPackToMyCollection,
icon: const Icon(Symbols.add_circle),
label: Text('addPack').tr(),
),
),
),
Gap(MediaQuery.of(context).padding.bottom),
],
);
},
error:
(err, _) =>
Text('Error: $err').textAlignment(TextAlign.center).center(),
loading: () => const CircularProgressIndicator().center(),
),
);
}
}

View File

@@ -0,0 +1,317 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'pack_detail.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$marketplaceStickerPackContentHash() =>
r'886f8305c978dbea6e5d990a7d555048ac704a5d';
/// 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));
}
}
/// Marketplace version of sticker pack detail page (no publisher dependency).
/// Shows all stickers in the pack and provides a button to add the sticker.
/// API interactions are intentionally left blank per request.
///
/// Copied from [marketplaceStickerPackContent].
@ProviderFor(marketplaceStickerPackContent)
const marketplaceStickerPackContentProvider =
MarketplaceStickerPackContentFamily();
/// Marketplace version of sticker pack detail page (no publisher dependency).
/// Shows all stickers in the pack and provides a button to add the sticker.
/// API interactions are intentionally left blank per request.
///
/// Copied from [marketplaceStickerPackContent].
class MarketplaceStickerPackContentFamily
extends Family<AsyncValue<List<SnSticker>>> {
/// Marketplace version of sticker pack detail page (no publisher dependency).
/// Shows all stickers in the pack and provides a button to add the sticker.
/// API interactions are intentionally left blank per request.
///
/// Copied from [marketplaceStickerPackContent].
const MarketplaceStickerPackContentFamily();
/// Marketplace version of sticker pack detail page (no publisher dependency).
/// Shows all stickers in the pack and provides a button to add the sticker.
/// API interactions are intentionally left blank per request.
///
/// Copied from [marketplaceStickerPackContent].
MarketplaceStickerPackContentProvider call({required String packId}) {
return MarketplaceStickerPackContentProvider(packId: packId);
}
@override
MarketplaceStickerPackContentProvider getProviderOverride(
covariant MarketplaceStickerPackContentProvider provider,
) {
return call(packId: provider.packId);
}
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'marketplaceStickerPackContentProvider';
}
/// Marketplace version of sticker pack detail page (no publisher dependency).
/// Shows all stickers in the pack and provides a button to add the sticker.
/// API interactions are intentionally left blank per request.
///
/// Copied from [marketplaceStickerPackContent].
class MarketplaceStickerPackContentProvider
extends AutoDisposeFutureProvider<List<SnSticker>> {
/// Marketplace version of sticker pack detail page (no publisher dependency).
/// Shows all stickers in the pack and provides a button to add the sticker.
/// API interactions are intentionally left blank per request.
///
/// Copied from [marketplaceStickerPackContent].
MarketplaceStickerPackContentProvider({required String packId})
: this._internal(
(ref) => marketplaceStickerPackContent(
ref as MarketplaceStickerPackContentRef,
packId: packId,
),
from: marketplaceStickerPackContentProvider,
name: r'marketplaceStickerPackContentProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$marketplaceStickerPackContentHash,
dependencies: MarketplaceStickerPackContentFamily._dependencies,
allTransitiveDependencies:
MarketplaceStickerPackContentFamily._allTransitiveDependencies,
packId: packId,
);
MarketplaceStickerPackContentProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.packId,
}) : super.internal();
final String packId;
@override
Override overrideWith(
FutureOr<List<SnSticker>> Function(
MarketplaceStickerPackContentRef provider,
)
create,
) {
return ProviderOverride(
origin: this,
override: MarketplaceStickerPackContentProvider._internal(
(ref) => create(ref as MarketplaceStickerPackContentRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
packId: packId,
),
);
}
@override
AutoDisposeFutureProviderElement<List<SnSticker>> createElement() {
return _MarketplaceStickerPackContentProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is MarketplaceStickerPackContentProvider &&
other.packId == packId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, packId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin MarketplaceStickerPackContentRef
on AutoDisposeFutureProviderRef<List<SnSticker>> {
/// The parameter `packId` of this provider.
String get packId;
}
class _MarketplaceStickerPackContentProviderElement
extends AutoDisposeFutureProviderElement<List<SnSticker>>
with MarketplaceStickerPackContentRef {
_MarketplaceStickerPackContentProviderElement(super.provider);
@override
String get packId => (origin as MarketplaceStickerPackContentProvider).packId;
}
String _$marketplaceStickerPackOwnershipHash() =>
r'e5dd301c309fac958729d13d984ce7a77edbe7e6';
/// See also [marketplaceStickerPackOwnership].
@ProviderFor(marketplaceStickerPackOwnership)
const marketplaceStickerPackOwnershipProvider =
MarketplaceStickerPackOwnershipFamily();
/// See also [marketplaceStickerPackOwnership].
class MarketplaceStickerPackOwnershipFamily extends Family<AsyncValue<bool>> {
/// See also [marketplaceStickerPackOwnership].
const MarketplaceStickerPackOwnershipFamily();
/// See also [marketplaceStickerPackOwnership].
MarketplaceStickerPackOwnershipProvider call({required String packId}) {
return MarketplaceStickerPackOwnershipProvider(packId: packId);
}
@override
MarketplaceStickerPackOwnershipProvider getProviderOverride(
covariant MarketplaceStickerPackOwnershipProvider provider,
) {
return call(packId: provider.packId);
}
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'marketplaceStickerPackOwnershipProvider';
}
/// See also [marketplaceStickerPackOwnership].
class MarketplaceStickerPackOwnershipProvider
extends AutoDisposeFutureProvider<bool> {
/// See also [marketplaceStickerPackOwnership].
MarketplaceStickerPackOwnershipProvider({required String packId})
: this._internal(
(ref) => marketplaceStickerPackOwnership(
ref as MarketplaceStickerPackOwnershipRef,
packId: packId,
),
from: marketplaceStickerPackOwnershipProvider,
name: r'marketplaceStickerPackOwnershipProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$marketplaceStickerPackOwnershipHash,
dependencies: MarketplaceStickerPackOwnershipFamily._dependencies,
allTransitiveDependencies:
MarketplaceStickerPackOwnershipFamily._allTransitiveDependencies,
packId: packId,
);
MarketplaceStickerPackOwnershipProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.packId,
}) : super.internal();
final String packId;
@override
Override overrideWith(
FutureOr<bool> Function(MarketplaceStickerPackOwnershipRef provider) create,
) {
return ProviderOverride(
origin: this,
override: MarketplaceStickerPackOwnershipProvider._internal(
(ref) => create(ref as MarketplaceStickerPackOwnershipRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
packId: packId,
),
);
}
@override
AutoDisposeFutureProviderElement<bool> createElement() {
return _MarketplaceStickerPackOwnershipProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is MarketplaceStickerPackOwnershipProvider &&
other.packId == packId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, packId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin MarketplaceStickerPackOwnershipRef on AutoDisposeFutureProviderRef<bool> {
/// The parameter `packId` of this provider.
String get packId;
}
class _MarketplaceStickerPackOwnershipProviderElement
extends AutoDisposeFutureProviderElement<bool>
with MarketplaceStickerPackOwnershipRef {
_MarketplaceStickerPackOwnershipProviderElement(super.provider);
@override
String get packId =>
(origin as MarketplaceStickerPackOwnershipProvider).packId;
}
// 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,395 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_update/azhon_app_update.dart';
import 'package:flutter_app_update/update_model.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:collection/collection.dart'; // Added for firstWhereOrNull
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:island/widgets/content/sheet.dart';
/// Data model for a GitHub release we care about
class GithubReleaseInfo {
final String tagName;
final String name;
final String body;
final String htmlUrl;
final DateTime createdAt;
final List<GithubReleaseAsset> assets;
const GithubReleaseInfo({
required this.tagName,
required this.name,
required this.body,
required this.htmlUrl,
required this.createdAt,
this.assets = const [],
});
}
/// Data model for a GitHub release asset
class GithubReleaseAsset {
final String name;
final String browserDownloadUrl;
const GithubReleaseAsset({
required this.name,
required this.browserDownloadUrl,
});
factory GithubReleaseAsset.fromJson(Map<String, dynamic> json) {
return GithubReleaseAsset(
name: json['name'] as String,
browserDownloadUrl: json['browser_download_url'] as String,
);
}
}
/// Parses version and build number from "x.y.z+build"
class _ParsedVersion implements Comparable<_ParsedVersion> {
final int major;
final int minor;
final int patch;
final int build;
const _ParsedVersion(this.major, this.minor, this.patch, this.build);
static _ParsedVersion? tryParse(String input) {
// Expect format like 0.0.0+00 (build after '+'). Allow missing build as 0.
final partsPlus = input.split('+');
final core = partsPlus[0].trim();
final buildStr = partsPlus.length > 1 ? partsPlus[1].trim() : '0';
final coreParts = core.split('.');
if (coreParts.length != 3) return null;
final major = int.tryParse(coreParts[0]) ?? 0;
final minor = int.tryParse(coreParts[1]) ?? 0;
final patch = int.tryParse(coreParts[2]) ?? 0;
final build = int.tryParse(buildStr) ?? 0;
return _ParsedVersion(major, minor, patch, build);
}
@override
int compareTo(_ParsedVersion other) {
if (major != other.major) return major.compareTo(other.major);
if (minor != other.minor) return minor.compareTo(other.minor);
if (patch != other.patch) return patch.compareTo(other.patch);
return build.compareTo(other.build);
}
@override
String toString() => '$major.$minor.$patch+$build';
}
class UpdateService {
UpdateService({Dio? dio, this.useProxy = false})
: _dio =
dio ??
Dio(
BaseOptions(
headers: {
// Identify the app to GitHub; avoids some rate-limits and adds clarity
'Accept': 'application/vnd.github+json',
'User-Agent': 'solian-update-checker',
},
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
),
);
final Dio _dio;
final bool useProxy;
static const _proxyBaseUrl = 'https://ghfast.top/';
static const _releasesLatestApi =
'https://api.github.com/repos/solsynth/solian/releases/latest';
/// Checks GitHub for the latest release and compares against the current app version.
/// If update is available, shows a bottom sheet with changelog and an action to open release page.
Future<void> checkForUpdates(BuildContext context) async {
log('[Update] Checking for updates...');
try {
final release = await fetchLatestRelease();
if (release == null) {
log('[Update] No latest release found or could not fetch.');
return;
}
log('[Update] Fetched latest release: ${release.tagName}');
final info = await PackageInfo.fromPlatform();
final localVersionStr = '${info.version}+${info.buildNumber}';
log('[Update] Local app version: $localVersionStr');
final latest = _ParsedVersion.tryParse(release.tagName);
final local = _ParsedVersion.tryParse(localVersionStr);
if (latest == null || local == null) {
log(
'[Update] Failed to parse versions. Latest: ${release.tagName}, Local: $localVersionStr',
);
// If parsing fails, do nothing silently
return;
}
log('[Update] Parsed versions. Latest: $latest, Local: $local');
final needsUpdate = latest.compareTo(local) > 0;
if (!needsUpdate) {
log('[Update] App is up to date. No update needed.');
return;
}
log('[Update] Update available! Latest: $latest, Local: $local');
if (!context.mounted) {
log('[Update] Context not mounted, cannot show update sheet.');
return;
}
// Delay to ensure UI is ready (if called at startup)
await Future.delayed(const Duration(milliseconds: 100));
if (context.mounted) {
await showUpdateSheet(context, release);
log('[Update] Update sheet shown.');
}
} catch (e) {
log('[Update] Error checking for updates: $e');
// Ignore errors (network, api, etc.)
return;
}
}
/// Manually show the update sheet with a provided release.
/// Useful for About page or testing.
Future<void> showUpdateSheet(
BuildContext context,
GithubReleaseInfo release,
) async {
if (!context.mounted) return;
await showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (ctx) {
String? androidUpdateUrl;
if (Platform.isAndroid) {
androidUpdateUrl = _getAndroidUpdateUrl(release.assets);
}
return _UpdateSheet(
release: release,
onOpen: () async {
final uri = Uri.parse(release.htmlUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
},
androidUpdateUrl: androidUpdateUrl,
useProxy: useProxy, // Pass the useProxy flag
);
},
);
}
String? _getAndroidUpdateUrl(List<GithubReleaseAsset> assets) {
final arm64 = assets.firstWhereOrNull(
(asset) => asset.name == 'app-arm64-v8a-release.apk',
);
final armeabi = assets.firstWhereOrNull(
(asset) => asset.name == 'app-armeabi-v7a-release.apk',
);
final x86_64 = assets.firstWhereOrNull(
(asset) => asset.name == 'app-x86_64-release.apk',
);
// Prioritize arm64, then armeabi, then x86_64
if (arm64 != null) {
return arm64.browserDownloadUrl;
} else if (armeabi != null) {
return armeabi.browserDownloadUrl;
} else if (x86_64 != null) {
return x86_64.browserDownloadUrl;
}
return null;
}
/// Fetch the latest release info from GitHub.
/// Public so other screens (e.g., About) can manually trigger update checks.
Future<GithubReleaseInfo?> fetchLatestRelease() async {
final apiEndpoint =
useProxy
? '$_proxyBaseUrl${Uri.encodeComponent(_releasesLatestApi)}'
: _releasesLatestApi;
log(
'[Update] Fetching latest release from GitHub API: $apiEndpoint (Proxy: $useProxy)',
);
final resp = await _dio.get(apiEndpoint);
if (resp.statusCode != 200) {
log(
'[Update] Failed to fetch latest release. Status code: ${resp.statusCode}',
);
return null;
}
final data = resp.data as Map<String, dynamic>;
log('[Update] Successfully fetched release data.');
final tagName = (data['tag_name'] ?? '').toString();
final name = (data['name'] ?? tagName).toString();
final body = (data['body'] ?? '').toString();
final htmlUrl = (data['html_url'] ?? '').toString();
final createdAtStr = (data['created_at'] ?? '').toString();
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
final assetsData =
(data['assets'] as List<dynamic>?)
?.map((e) => GithubReleaseAsset.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
if (tagName.isEmpty || htmlUrl.isEmpty) {
log(
'[Update] Missing tag_name or html_url in release data. TagName: "$tagName", HtmlUrl: "$htmlUrl"',
);
return null;
}
log('[Update] Returning GithubReleaseInfo for tag: $tagName');
return GithubReleaseInfo(
tagName: tagName,
name: name,
body: body,
htmlUrl: htmlUrl,
createdAt: createdAt,
assets: assetsData,
);
}
}
class _UpdateSheet extends StatefulWidget {
const _UpdateSheet({
required this.release,
required this.onOpen,
this.androidUpdateUrl,
this.useProxy = false,
});
final String? androidUpdateUrl;
final bool useProxy;
final GithubReleaseInfo release;
final VoidCallback onOpen;
@override
State<_UpdateSheet> createState() => _UpdateSheetState();
}
class _UpdateSheetState extends State<_UpdateSheet> {
late bool _useProxy;
@override
void initState() {
super.initState();
_useProxy = widget.useProxy;
}
Future<void> _installUpdate(String url) async {
final downloadUrl =
_useProxy ? 'https://ghfast.top/${Uri.encodeComponent(url)}' : url;
UpdateModel model = UpdateModel(
downloadUrl,
"solian-update-${widget.release.tagName}.apk",
"launcher_icon",
'https://apps.apple.com/us/app/solian/id6499032345',
);
AzhonAppUpdate.update(model);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SheetScaffold(
titleText: 'Update available',
child: Padding(
padding: EdgeInsets.only(
bottom: 16 + MediaQuery.of(context).padding.bottom,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.release.name,
style: theme.textTheme.titleMedium,
).bold(),
Text(widget.release.tagName).fontSize(12),
],
).padding(vertical: 16, horizontal: 16),
const Divider(height: 1),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: MarkdownTextContent(
content:
widget.release.body.isEmpty
? 'No changelog provided.'
: widget.release.body,
),
),
),
if (!kIsWeb && Platform.isAndroid)
SwitchListTile(
title: const Text('Use GitHub Proxy for Download'),
value: _useProxy,
onChanged: (value) {
setState(() {
_useProxy = value;
});
},
).padding(horizontal: 8),
Column(
children: [
Row(
spacing: 8,
children: [
if (!kIsWeb &&
Platform.isAndroid &&
widget.androidUpdateUrl != null)
Expanded(
child: FilledButton.icon(
onPressed: () {
log(widget.androidUpdateUrl!);
_installUpdate(widget.androidUpdateUrl!);
},
icon: const Icon(Symbols.update),
label: const Text('Install update'),
),
),
Expanded(
child: FilledButton.icon(
onPressed: widget.onOpen,
icon: const Icon(Icons.open_in_new),
label: const Text('Open release page'),
),
),
],
),
],
).padding(horizontal: 16),
],
),
),
);
}
}

30
lib/utils/mapping.dart Normal file
View File

@@ -0,0 +1,30 @@
String _upperCamelToLowerSnake(String input) {
final regex = RegExp(r'(?<=[a-z0-9])([A-Z])');
return input
.replaceAllMapped(regex, (match) => '_${match.group(0)}')
.toLowerCase();
}
Map<String, dynamic> convertMapKeysToSnakeCase(Map<String, dynamic> input) {
final result = <String, dynamic>{};
input.forEach((key, value) {
final newKey = _upperCamelToLowerSnake(key);
if (value is Map<String, dynamic>) {
result[newKey] = convertMapKeysToSnakeCase(value);
} else if (value is List) {
result[newKey] =
value.map((item) {
if (item is Map<String, dynamic>) {
return convertMapKeysToSnakeCase(item);
}
return item;
}).toList();
} else {
result[newKey] = value;
}
});
return result;
}

View File

@@ -130,9 +130,22 @@ class AccountStatusWidget extends HookConsumerWidget {
size: 16,
).padding(right: 4),
if (status.value?.isCustomized ?? false)
Text(status.value?.label ?? 'unknown'.tr())
Flexible(
child: Text(
status.value?.label ?? 'unknown'.tr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
else
Text((status.value?.label ?? 'offline').toLowerCase()).tr(),
Flexible(
child:
Text(
(status.value?.label ?? 'offline').toLowerCase(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
).tr(),
),
if (!(status.value?.isOnline ?? false) &&
account.value?.profile.lastSeenAt != null)
Flexible(

View File

@@ -55,7 +55,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget {
'attitude': attitude.value,
'is_invisible': isInvisible.value,
'is_not_disturb': isNotDisturb.value,
'cleared_at': clearedAt.value?.toIso8601String(),
'cleared_at': clearedAt.value?.toUtc().toIso8601String(),
if (labelController.text.isNotEmpty) 'label': labelController.text,
},
options: Options(method: initialStatus == null ? 'POST' : 'PATCH'),

View File

@@ -331,7 +331,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
final user = ref.watch(userInfoProvider);
final websocketState = ref.watch(websocketStateProvider);
final indicatorHeight =
MediaQuery.of(context).padding.top + (isDesktop ? 27.5 : 60);
MediaQuery.of(context).padding.top + (isDesktop ? 27.5 : 25);
Color indicatorColor;
String indicatorText;
@@ -343,7 +343,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
indicatorColor = Colors.teal;
indicatorText = 'connectionReconnecting';
} else {
indicatorColor = Colors.orange;
indicatorColor = Colors.red;
indicatorText = 'connectionDisconnected';
}

View File

@@ -2,8 +2,11 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/services/notify.dart';
import 'package:island/services/sharing_intent.dart';
import 'package:island/services/update_service.dart';
import 'package:island/widgets/content/network_status_sheet.dart';
import 'package:island/widgets/tour/tour.dart';
class AppWrapper extends HookConsumerWidget {
@@ -19,12 +22,34 @@ class AppWrapper extends HookConsumerWidget {
});
final sharingService = SharingIntentService();
sharingService.initialize(context);
UpdateService().checkForUpdates(context);
return () {
sharingService.dispose();
ntySubs?.cancel();
};
}, const []);
final wsNotifier = ref.watch(websocketStateProvider.notifier);
final websocketState = ref.watch(websocketStateProvider);
final networkStateShowing = useState(false);
if (websocketState == WebSocketState.duplicateDevice()) {
if (!networkStateShowing.value) {
WidgetsBinding.instance.addPostFrameCallback((_) {
networkStateShowing.value = true;
showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: false,
builder:
(context) =>
NetworkStatusSheet(onReconnect: () => wsNotifier.connect()),
).then((_) => networkStateShowing.value = false);
});
}
}
return TourTriggerWidget(child: child);
}
}

View File

@@ -14,6 +14,7 @@ import 'package:island/models/embed.dart';
import 'package:island/pods/call.dart';
import 'package:island/pods/translate.dart';
import 'package:island/screens/chat/room.dart';
import 'package:island/utils/mapping.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/app_scaffold.dart';
@@ -292,12 +293,11 @@ class MessageItem extends HookConsumerWidget {
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.where((embed) => embed['Type'] == 'link')
.map(
(embed) => SnEmbedLink.fromJson(
embed as Map<String, dynamic>,
),
(embed) => convertMapKeysToSnakeCase(embed),
)
.where((embed) => embed['type'] == 'link')
.map((embed) => SnScrappedLink.fromJson(embed))
.map(
(link) => LayoutBuilder(
builder: (context, constraints) {

View File

@@ -36,7 +36,8 @@ Future<SnCheckInResult?> checkInResultToday(Ref ref) async {
class CheckInWidget extends HookConsumerWidget {
final EdgeInsets? margin;
const CheckInWidget({super.key, this.margin});
final VoidCallback? onChecked;
const CheckInWidget({super.key, this.margin, this.onChecked});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -52,6 +53,7 @@ class CheckInWidget extends HookConsumerWidget {
ref.invalidate(checkInResultTodayProvider);
final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.fetchUser();
onChecked?.call();
} catch (err) {
if (err is DioException) {
if (err.response?.statusCode == 423 && context.mounted) {

View File

@@ -272,8 +272,96 @@ class AttachmentPreview extends HookConsumerWidget {
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: Column(
child: Stack(
children: [
AspectRatio(
aspectRatio: ratio,
child: Stack(
fit: StackFit.expand,
children: [
Builder(
key: ValueKey(item.hashCode),
builder: (context) {
if (item.isOnCloud) {
return CloudFileWidget(item: item.data);
} else if (item.data is XFile) {
final file = item.data as XFile;
if (file.path.isEmpty) {
return FutureBuilder<Uint8List>(
future: file.readAsBytes(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Image.memory(snapshot.data!);
}
return const Center(
child: CircularProgressIndicator(),
);
},
);
}
switch (item.type) {
case UniversalFileType.image:
return kIsWeb
? Image.network(file.path)
: Image.file(File(file.path));
default:
return Column(
children: [
const Icon(Symbols.document_scanner),
Text(file.name),
],
);
}
} else if (item is List<int> || item is Uint8List) {
switch (item.type) {
case UniversalFileType.image:
return Image.memory(item.data);
default:
return Column(
children: [const Icon(Symbols.document_scanner)],
);
}
}
return Placeholder();
},
),
if (progress != null)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.3),
padding: EdgeInsets.symmetric(
horizontal: 40,
vertical: 16,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (progress != null)
Text(
'${progress!.toStringAsFixed(2)}%',
style: TextStyle(color: Colors.white),
)
else
Text(
'uploading'.tr(),
style: TextStyle(color: Colors.white),
),
Gap(6),
Center(
child: LinearProgressIndicator(
value:
progress != null ? progress! / 100.0 : null,
),
),
],
),
),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -397,94 +485,6 @@ class AttachmentPreview extends HookConsumerWidget {
),
],
).padding(horizontal: 12, vertical: 8),
AspectRatio(
aspectRatio: ratio,
child: Stack(
fit: StackFit.expand,
children: [
Builder(
key: ValueKey(item.hashCode),
builder: (context) {
if (item.isOnCloud) {
return CloudFileWidget(item: item.data);
} else if (item.data is XFile) {
final file = item.data as XFile;
if (file.path.isEmpty) {
return FutureBuilder<Uint8List>(
future: file.readAsBytes(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Image.memory(snapshot.data!);
}
return const Center(
child: CircularProgressIndicator(),
);
},
);
}
switch (item.type) {
case UniversalFileType.image:
return kIsWeb
? Image.network(file.path)
: Image.file(File(file.path));
default:
return Column(
children: [
const Icon(Symbols.document_scanner),
Text(file.name),
],
);
}
} else if (item is List<int> || item is Uint8List) {
switch (item.type) {
case UniversalFileType.image:
return Image.memory(item.data);
default:
return Column(
children: [const Icon(Symbols.document_scanner)],
);
}
}
return Placeholder();
},
),
if (progress != null)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.3),
padding: EdgeInsets.symmetric(
horizontal: 40,
vertical: 16,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (progress != null)
Text(
'${progress!.toStringAsFixed(2)}%',
style: TextStyle(color: Colors.white),
)
else
Text(
'uploading'.tr(),
style: TextStyle(color: Colors.white),
),
Gap(6),
Center(
child: LinearProgressIndicator(
value:
progress != null ? progress! / 100.0 : null,
),
),
],
),
),
),
],
),
),
],
),
),

View File

@@ -1,5 +1,4 @@
import 'dart:math' as math;
import 'dart:ui';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
@@ -103,6 +102,7 @@ class CloudVideoWidget extends HookConsumerWidget {
Symbols.play_arrow,
fill: 1,
size: 32,
color: Colors.white,
shadows: [
BoxShadow(
color: Colors.black54,
@@ -114,6 +114,26 @@ class CloudVideoWidget extends HookConsumerWidget {
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: IgnorePointer(
child: Container(
height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surface.withOpacity(0.85),
Colors.transparent,
],
),
),
),
),
),
Positioned(
bottom: 0,
left: 0,
@@ -122,7 +142,7 @@ class CloudVideoWidget extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
Wrap(
spacing: 8,
children: [
if (item.fileMeta?['duration'] != null)
@@ -133,6 +153,7 @@ class CloudVideoWidget extends HookConsumerWidget {
.toInt(),
).formatDuration(),
style: TextStyle(
color: Colors.white,
shadows: [
BoxShadow(
color: Colors.black54,
@@ -147,6 +168,7 @@ class CloudVideoWidget extends HookConsumerWidget {
Text(
'${int.parse(item.fileMeta?['bit_rate'] as String) ~/ 1000} Kbps',
style: TextStyle(
color: Colors.white,
shadows: [
BoxShadow(
color: Colors.black54,
@@ -161,7 +183,10 @@ class CloudVideoWidget extends HookConsumerWidget {
),
Text(
item.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
shadows: [
BoxShadow(
@@ -174,8 +199,8 @@ class CloudVideoWidget extends HookConsumerWidget {
),
),
],
),
).padding(horizontal: 16, bottom: 12),
).padding(horizontal: 16, bottom: 12),
),
],
),
onTap: () {

View File

@@ -6,7 +6,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:url_launcher/url_launcher.dart';
class EmbedLinkWidget extends StatelessWidget {
final SnEmbedLink link;
final SnScrappedLink link;
final double? maxWidth;
final EdgeInsetsGeometry? margin;
@@ -116,7 +116,8 @@ class EmbedLinkWidget extends StatelessWidget {
],
// Description
if (link.description != null && link.description!.isNotEmpty) ...[
if (link.description != null &&
link.description!.isNotEmpty) ...[
Text(
link.description!,
style: theme.textTheme.bodyMedium?.copyWith(
@@ -191,7 +192,7 @@ class EmbedLinkWidget extends StatelessWidget {
try {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays == 0) {
return 'Today';
} else if (difference.inDays == 1) {

View File

@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_highlight/themes/a11y-dark.dart';
import 'package:flutter_highlight/themes/a11y-light.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
@@ -71,7 +72,22 @@ class MarkdownTextContent extends HookConsumerWidget {
textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!,
),
HrConfig(height: 1, color: Theme.of(context).dividerColor),
PreConfig(theme: isDark ? a11yDarkTheme : a11yLightTheme),
PreConfig(
theme: isDark ? a11yDarkTheme : a11yLightTheme,
textStyle: GoogleFonts.robotoMono(fontSize: 14),
styleNotMatched: GoogleFonts.robotoMono(fontSize: 14),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.all(Radius.circular(8.0)),
),
),
TableConfig(
wrapper:
(child) => SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: child,
),
),
LinkConfig(
style:
linkStyle ??
@@ -145,6 +161,8 @@ class MarkdownTextContent extends HookConsumerWidget {
);
case 'stickers':
final size = doesEnlargeSticker ? 96.0 : 24.0;
final stickerUri =
'$baseUrl/sphere/stickers/lookup/${uri.pathSegments[0]}/open';
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
@@ -155,11 +173,10 @@ class MarkdownTextContent extends HookConsumerWidget {
),
),
child: UniversalImage(
uri:
'$baseUrl/sphere/stickers/lookup/${uri.pathSegments[0]}/open',
uri: stickerUri,
width: size,
height: size,
fit: BoxFit.cover,
fit: BoxFit.contain,
noCacheOptimization: true,
),
),

View File

@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/websocket.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:island/widgets/content/sheet.dart';
class NetworkStatusSheet extends HookConsumerWidget {
final VoidCallback onReconnect;
const NetworkStatusSheet({super.key, required this.onReconnect});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ws = ref.watch(websocketProvider);
final wsState = ref.watch(websocketStateProvider);
return SheetScaffold(
titleText:
wsState == WebSocketState.connected()
? 'Connection Status'
: 'Connection Issue',
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
wsState.when(
connected:
() => Text(
'Connected to server',
style: Theme.of(context).textTheme.bodyLarge,
),
connecting:
() => Text(
'Connecting to server...',
style: Theme.of(context).textTheme.bodyLarge,
),
disconnected:
() => Text(
'Disconnected from server',
style: Theme.of(context).textTheme.bodyLarge,
),
serverDown:
() => Text(
'The server is not available right now... Please try again later...',
style: Theme.of(context).textTheme.bodyLarge,
),
duplicateDevice:
() => Text(
'Another device has connected with the same account.',
style: Theme.of(context).textTheme.bodyLarge,
),
error:
(message) => Text(
'Connection error: $message',
style: Theme.of(context).textTheme.bodyLarge,
),
),
const SizedBox(height: 16),
if (ws.heartbeatDelay != null)
Text(
'Last heartbeat: ${ws.heartbeatDelay!.inMilliseconds}ms',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 24),
Center(
child: FilledButton.icon(
icon: const Icon(Symbols.wifi),
label: const Text('Reconnect'),
onPressed: () {
onReconnect();
Navigator.pop(context);
},
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/message.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/widgets/content/network_status_sheet.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
class DebugSheet extends HookConsumerWidget {
const DebugSheet({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final wsNotifier = ref.watch(websocketStateProvider.notifier);
return SheetScaffold(
titleText: 'Debug',
child: Column(
children: [
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.wifi),
trailing: const Icon(Symbols.chevron_right),
title: Text('Connection Status'),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => NetworkStatusSheet(
onReconnect: () => wsNotifier.connect(),
),
);
},
),
const Divider(height: 1),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.copy_all),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('Copy access token'),
onTap: () async {
final tk = ref.watch(tokenProvider);
Clipboard.setData(ClipboardData(text: tk!.token));
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.delete),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('Reset database'),
onTap: () async {
resetDatabase(ref);
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.clear),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('Clear cache'),
onTap: () async {
DefaultCacheManager().emptyCache();
},
),
],
),
);
}
}

View File

@@ -248,7 +248,7 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> {
try {
final client = ref.read(apiClientProvider);
final response = await client.post(
'/orders/${widget.order.id}/pay',
'/id/orders/${widget.order.id}/pay',
data: {'pin_code': pin},
);

View File

@@ -0,0 +1,236 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/poll.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/content/sheet.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 'poll_feedback.g.dart';
@riverpod
class PollFeedbackNotifier extends _$PollFeedbackNotifier
with CursorPagingNotifierMixin<SnPollAnswer> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnPollAnswer>> build(String id) {
// immediately load first page
return fetch(cursor: null);
}
@override
Future<CursorPagingData<SnPollAnswer>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final queryParams = {'offset': offset, 'take': _pageSize};
final response = await client.get(
'/sphere/polls/$id/feedback',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final items = data.map((json) => SnPollAnswer.fromJson(json)).toList();
final hasMore = offset + items.length < total;
final nextCursor = hasMore ? (offset + items.length).toString() : null;
return CursorPagingData(
items: items,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
class PollFeedbackSheet extends HookConsumerWidget {
final String pollId;
final String? title;
final SnPoll poll;
final Map<String, dynamic>? stats; // stats object similar to PollSubmit
const PollFeedbackSheet({
super.key,
required this.pollId,
required this.poll,
this.title,
this.stats,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return SheetScaffold(
titleText: title ?? 'Poll feedback',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_PollHeader(poll: poll, stats: stats),
const Divider(height: 1),
Expanded(
child: PagingHelperView(
provider: pollFeedbackNotifierProvider(pollId),
futureRefreshable: pollFeedbackNotifierProvider(pollId).future,
notifierRefreshable:
pollFeedbackNotifierProvider(pollId).notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
// Provided by PagingHelperView to indicate end/loading
return endItemView;
}
final answer = data.items[index];
return _PollAnswerTile(answer: answer, poll: poll);
},
separatorBuilder:
(context, index) =>
const Divider(height: 1).padding(vertical: 4),
),
),
),
],
),
);
}
}
class _PollHeader extends StatelessWidget {
const _PollHeader({required this.poll, this.stats});
final SnPoll poll;
final Map<String, dynamic>? stats;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (poll.title != null)
Text(poll.title!, style: theme.textTheme.titleLarge),
if (poll.description != null)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
poll.description!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7),
),
),
),
],
).padding(horizontal: 20, vertical: 16);
}
}
class _PollAnswerTile extends StatelessWidget {
final SnPollAnswer answer;
final SnPoll poll;
const _PollAnswerTile({required this.answer, required this.poll});
String _formatPerQuestionAnswer(
SnPollQuestion q,
Map<String, dynamic> ansMap,
) {
switch (q.type) {
case SnPollQuestionType.singleChoice:
final val = ansMap[q.id];
if (val is String) {
final opt = q.options?.firstWhere(
(o) => o.id == val,
orElse: () => SnPollOption(id: val, label: '#$val', order: 0),
);
return opt?.label ?? '#$val';
}
return '';
case SnPollQuestionType.multipleChoice:
final val = ansMap[q.id];
if (val is List) {
final ids = val.whereType<String>().toList();
if (ids.isEmpty) return '';
final labels =
ids.map((id) {
final opt = q.options?.firstWhere(
(o) => o.id == id,
orElse: () => SnPollOption(id: id, label: '#$id', order: 0),
);
return opt?.label ?? '#$id';
}).toList();
return labels.join(', ');
}
return '';
case SnPollQuestionType.yesNo:
final val = ansMap[q.id];
if (val is bool) {
return val ? 'Yes' : 'No';
}
return '';
case SnPollQuestionType.rating:
final val = ansMap[q.id];
if (val is int) return val.toString();
if (val is num) return val.toString();
return '';
case SnPollQuestionType.freeText:
final val = ansMap[q.id];
if (val is String && val.trim().isNotEmpty) return val;
return '';
}
}
@override
Widget build(BuildContext context) {
// Submit date/time (title)
final submitText = answer.createdAt.formatSystem();
// Compose content from poll questions if provided, otherwise fallback to joined key-values
String content;
if (poll.questions.isNotEmpty) {
final questions = [...poll.questions]
..sort((a, b) => a.order.compareTo(b.order));
final buffer = StringBuffer();
for (final q in questions) {
final formatted = _formatPerQuestionAnswer(q, answer.answer);
buffer.writeln('${q.title}: $formatted');
}
content = buffer.toString().trimRight();
} else {
// Fallback formatting without poll context. We still want to show the question title
// instead of the raw question id key if we can derive it from the answer map itself.
// Since we don't have poll metadata here, we cannot resolve the title; therefore we
// will show only values line-by-line without exposing the raw id.
if (answer.answer.isEmpty) {
content = '';
} else {
final parts = <String>[];
answer.answer.forEach((key, value) {
var question = poll.questions.firstWhere((q) => q.id == key);
if (value is List) {
parts.add('${question.title}: ${value.join(', ')}');
} else {
parts.add('${question.title}: $value');
}
});
content = parts.join('\n');
}
}
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
isThreeLine: true,
leading: const CircleAvatar(
radius: 16,
child: Icon(Icons.how_to_vote, size: 16),
),
title: Text(submitText),
subtitle: Text(content),
trailing: null,
);
}
}

View File

@@ -0,0 +1,180 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'poll_feedback.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$pollFeedbackNotifierHash() =>
r'1bf3925b5b751cfd1a9abafb75274f1e95e7f27e';
/// 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 _$PollFeedbackNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPollAnswer>> {
late final String id;
FutureOr<CursorPagingData<SnPollAnswer>> build(String id);
}
/// See also [PollFeedbackNotifier].
@ProviderFor(PollFeedbackNotifier)
const pollFeedbackNotifierProvider = PollFeedbackNotifierFamily();
/// See also [PollFeedbackNotifier].
class PollFeedbackNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnPollAnswer>>> {
/// See also [PollFeedbackNotifier].
const PollFeedbackNotifierFamily();
/// See also [PollFeedbackNotifier].
PollFeedbackNotifierProvider call(String id) {
return PollFeedbackNotifierProvider(id);
}
@override
PollFeedbackNotifierProvider getProviderOverride(
covariant PollFeedbackNotifierProvider provider,
) {
return call(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'pollFeedbackNotifierProvider';
}
/// See also [PollFeedbackNotifier].
class PollFeedbackNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
PollFeedbackNotifier,
CursorPagingData<SnPollAnswer>
> {
/// See also [PollFeedbackNotifier].
PollFeedbackNotifierProvider(String id)
: this._internal(
() => PollFeedbackNotifier()..id = id,
from: pollFeedbackNotifierProvider,
name: r'pollFeedbackNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$pollFeedbackNotifierHash,
dependencies: PollFeedbackNotifierFamily._dependencies,
allTransitiveDependencies:
PollFeedbackNotifierFamily._allTransitiveDependencies,
id: id,
);
PollFeedbackNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.id,
}) : super.internal();
final String id;
@override
FutureOr<CursorPagingData<SnPollAnswer>> runNotifierBuild(
covariant PollFeedbackNotifier notifier,
) {
return notifier.build(id);
}
@override
Override overrideWith(PollFeedbackNotifier Function() create) {
return ProviderOverride(
origin: this,
override: PollFeedbackNotifierProvider._internal(
() => create()..id = id,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
id: id,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
PollFeedbackNotifier,
CursorPagingData<SnPollAnswer>
>
createElement() {
return _PollFeedbackNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PollFeedbackNotifierProvider && other.id == id;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.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 PollFeedbackNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPollAnswer>> {
/// The parameter `id` of this provider.
String get id;
}
class _PollFeedbackNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
PollFeedbackNotifier,
CursorPagingData<SnPollAnswer>
>
with PollFeedbackNotifierRef {
_PollFeedbackNotifierProviderElement(super.provider);
@override
String get id => (origin as PollFeedbackNotifierProvider).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

@@ -1,23 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/poll.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
/// A poll answering widget that shows one question at a time and collects answers.
///
/// Usage:
/// PollSubmit(
/// poll: poll,
/// onSubmit: (answers) {
/// // answers is Map<String, dynamic>: questionId -> answer
/// // answer types by question:
/// // - singleChoice: String optionId
/// // - multipleChoice: List<String> optionIds
/// // - yesNo: bool
/// // - rating: int (1..5)
/// // - freeText: String
/// },
/// )
class PollSubmit extends ConsumerStatefulWidget {
const PollSubmit({
super.key,
@@ -208,12 +195,11 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
// Only call onSubmit after server accepts
widget.onSubmit(Map<String, dynamic>.unmodifiable(_answers));
showSnackBar('Poll answer has been submitted.');
HapticFeedback.heavyImpact();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Failed to submit poll: $e')));
}
showErrorAlert(e);
} finally {
if (mounted) {
setState(() {

View File

@@ -189,8 +189,8 @@ class ComposePollSheet extends HookConsumerWidget {
Widget? _buildPollSubtitle(SnPoll poll) {
try {
final SnPoll dyn = poll;
final List<SnPollQuestion>? options = dyn.questions;
if (options == null || options.isEmpty) return null;
final List<SnPollQuestion> options = dyn.questions;
if (options.isEmpty) return null;
final preview = options.take(3).map((e) => e.title).join(' · ');
if (preview.trim().isEmpty) return null;
return Text(preview);

View File

@@ -1,11 +1,29 @@
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:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post_category.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/compose_shared.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:textfield_tags/textfield_tags.dart';
part 'compose_settings_sheet.g.dart';
@riverpod
Future<List<SnPostCategory>> postCategories(Ref ref) async {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/sphere/posts/categories');
return resp.data
.map((e) => SnPostCategory.fromJson(e))
.cast<SnPostCategory>()
.toList();
}
/// A reusable widget for tag input fields with chip display
class ChipTagInputField extends StatelessWidget {
final InputFieldValues inputFieldValues;
@@ -98,31 +116,20 @@ class ChipTagInputField extends StatelessWidget {
}
}
class ComposeSettingsSheet extends HookWidget {
final TextEditingController titleController;
final TextEditingController descriptionController;
final ValueNotifier<int> visibility;
final VoidCallback? onVisibilityChanged;
final StringTagController tagsController;
final StringTagController categoriesController;
class ComposeSettingsSheet extends HookConsumerWidget {
final ComposeState state;
const ComposeSettingsSheet({
super.key,
required this.titleController,
required this.descriptionController,
required this.visibility,
this.onVisibilityChanged,
required this.tagsController,
required this.categoriesController,
});
const ComposeSettingsSheet({super.key, required this.state});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
// Listen to visibility changes to trigger rebuilds
final currentVisibility = useValueListenable(visibility);
final currentVisibility = useValueListenable(state.visibility);
final currentCategories = useValueListenable(state.categories);
final postCategories = ref.watch(postCategoriesProvider);
IconData getVisibilityIcon(int visibilityValue) {
switch (visibilityValue) {
@@ -160,11 +167,10 @@ class ComposeSettingsSheet extends HookWidget {
leading: Icon(icon),
title: Text(textKey.tr()),
onTap: () {
visibility.value = value;
onVisibilityChanged?.call();
state.visibility.value = value;
Navigator.pop(context);
},
selected: visibility.value == value,
selected: state.visibility.value == value,
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
);
}
@@ -210,48 +216,16 @@ class ComposeSettingsSheet extends HookWidget {
return SheetScaffold(
titleText: 'postSettings'.tr(),
heightFactor: 0.6,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: [
// Title field
TextField(
controller: titleController,
decoration: InputDecoration(
labelText: 'postTitle'.tr(),
hintText: 'postTitle'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.all(16),
),
style: theme.textTheme.titleMedium,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
// Description field
TextField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'postDescription'.tr(),
hintText: 'postDescription'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.all(16),
),
style: theme.textTheme.bodyMedium,
maxLines: 3,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
// Tags field
TextFieldTags(
textfieldTagsController: tagsController,
textfieldTagsController: state.tagsController,
textSeparators: const [' ', ','],
letterCase: LetterCase.normal,
validator: (String tag) {
@@ -270,22 +244,105 @@ class ComposeSettingsSheet extends HookWidget {
),
// Categories field
TextFieldTags(
textfieldTagsController: categoriesController,
textSeparators: const [' ', ','],
letterCase: LetterCase.small,
validator: (String tag) {
if (tag.isEmpty) return 'No, cannot be empty';
if (tag.contains(' ')) return 'Tags should be URL-safe';
return null;
},
inputFieldBuilder: (context, inputFieldValues) {
return ChipTagInputField(
inputFieldValues: inputFieldValues,
labelText: 'categories',
hintText: 'categoriesHint',
);
// FIXME: Sometimes the entire dropdown crashes: 'package:flutter/src/rendering/stack.dart': Failed assertion: line 799 pos 12: 'firstChild == null || child != null': is not true.
DropdownButtonFormField2<SnPostCategory>(
isExpanded: true,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(vertical: 9),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
hint: Text('categories'.tr(), style: TextStyle(fontSize: 15)),
items:
(postCategories.value ?? <SnPostCategory>[]).map((item) {
return DropdownMenuItem(
value: item,
enabled: false,
child: StatefulBuilder(
builder: (context, menuSetState) {
final isSelected = state.categories.value.contains(
item,
);
return InkWell(
onTap: () {
isSelected
? state.categories.value =
state.categories.value
.where((e) => e != item)
.toList()
: state.categories.value = [
...state.categories.value,
item,
];
menuSetState(() {});
},
child: Container(
height: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: Row(
children: [
if (isSelected)
const Icon(Icons.check_box_outlined)
else
const Icon(Icons.check_box_outline_blank),
const SizedBox(width: 16),
Expanded(
child: Text(
item.categoryDisplayTitle,
style: const TextStyle(fontSize: 14),
),
),
],
),
),
);
},
),
);
}).toList(),
value: currentCategories.isEmpty ? null : currentCategories.last,
onChanged: (_) {},
selectedItemBuilder: (context) {
return currentCategories.map((item) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (final category in currentCategories)
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Theme.of(context).colorScheme.primary,
),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
margin: const EdgeInsets.only(right: 4),
child: Text(
category.categoryDisplayTitle,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontSize: 13,
),
),
),
],
),
);
}).toList();
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.only(left: 16, right: 8),
height: 40,
),
menuItemStyleData: const MenuItemStyleData(
height: 40,
padding: EdgeInsets.zero,
),
),
// Visibility setting

View File

@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'compose_settings_sheet.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$postCategoriesHash() => r'24337fe806d088b6468a350f62d5a5d40232a73c';
/// See also [postCategories].
@ProviderFor(postCategories)
final postCategoriesProvider =
AutoDisposeFutureProvider<List<SnPostCategory>>.internal(
postCategories,
name: r'postCategoriesProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$postCategoriesHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef PostCategoriesRef = AutoDisposeFutureProviderRef<List<SnPostCategory>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -7,6 +7,7 @@ import 'package: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/post_category.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
@@ -30,8 +31,8 @@ class ComposeState {
final ValueNotifier<Map<int, double>> attachmentProgress;
final ValueNotifier<SnPublisher?> currentPublisher;
final ValueNotifier<bool> submitting;
final ValueNotifier<List<SnPostCategory>> categories;
StringTagController tagsController;
StringTagController categoriesController;
final String draftId;
int postType;
// Linked poll id for this compose session (nullable)
@@ -48,7 +49,7 @@ class ComposeState {
required this.currentPublisher,
required this.submitting,
required this.tagsController,
required this.categoriesController,
required this.categories,
required this.draftId,
this.postType = 0,
String? pollId,
@@ -80,11 +81,7 @@ class ComposeLogic {
}) {
final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString();
final tagsController = StringTagController();
final categoriesController = StringTagController();
originalPost?.tags.forEach((x) => tagsController.addTag(x.slug));
originalPost?.categories.forEach(
(x) => categoriesController.addTag(x.slug),
);
return ComposeState(
attachments: ValueNotifier<List<UniversalFile>>(
originalPost?.attachments
@@ -112,7 +109,9 @@ class ComposeLogic {
attachmentProgress: ValueNotifier<Map<int, double>>({}),
currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher),
tagsController: tagsController,
categoriesController: categoriesController,
categories: ValueNotifier<List<SnPostCategory>>(
originalPost?.categories ?? [],
),
draftId: id,
postType: postType,
// initialize without poll by default
@@ -141,7 +140,7 @@ class ComposeLogic {
attachmentProgress: ValueNotifier<Map<int, double>>({}),
currentPublisher: ValueNotifier<SnPublisher?>(null),
tagsController: tagsController,
categoriesController: categoriesController,
categories: ValueNotifier<List<SnPostCategory>>([]),
draftId: draft.id,
postType: postType,
pollId: null,
@@ -640,7 +639,7 @@ class ComposeLogic {
if (repliedPost != null) 'replied_post_id': repliedPost.id,
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
'tags': state.tagsController.getTags,
'categories': state.categoriesController.getTags,
'categories': state.categories.value.map((e) => e.slug).toList(),
if (state.pollId.value != null) 'poll_id': state.pollId.value,
};
@@ -689,7 +688,7 @@ class ComposeLogic {
}
static void handleKeyPress(
RawKeyEvent event,
KeyEvent event,
ComposeState state,
WidgetRef ref,
BuildContext context, {
@@ -697,11 +696,13 @@ class ComposeLogic {
SnPost? repliedPost,
SnPost? forwardedPost,
}) {
if (event is! RawKeyDownEvent) return;
if (event is! KeyDownEvent) return;
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
final isSave = event.logicalKey == LogicalKeyboardKey.keyS;
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
final isModifierPressed =
HardwareKeyboard.instance.isMetaPressed ||
HardwareKeyboard.instance.isControlPressed;
final isSubmit = event.logicalKey == LogicalKeyboardKey.enter;
if (isPaste && isModifierPressed) {
@@ -731,7 +732,7 @@ class ComposeLogic {
state.attachmentProgress.dispose();
state.currentPublisher.dispose();
state.tagsController.dispose();
state.categoriesController.dispose();
state.categories.dispose();
state.pollId.dispose();
}
}

View File

@@ -63,6 +63,7 @@ class ComposeToolbar extends HookConsumerWidget {
return Material(
elevation: 4,
color: Theme.of(context).colorScheme.surfaceContainerLow,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),

View File

@@ -279,18 +279,14 @@ class _DraftItem extends StatelessWidget {
String _parseVisibility(int visibility) {
switch (visibility) {
case 0:
return 'public'.tr();
case 1:
return 'unlisted'.tr();
return 'postVisibilityFriends';
case 2:
return 'friends'.tr();
return 'postVisibilityUnlisted';
case 3:
return 'selected'.tr();
case 4:
return 'private'.tr();
return 'postVisibilityPrivate';
default:
return 'unknown'.tr();
return 'postVisibilityPublic';
}
}
}

View File

@@ -0,0 +1,105 @@
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/pods/network.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:styled_widget/styled_widget.dart';
part 'post_featured.g.dart';
@riverpod
Future<List<SnPost>> featuredPosts(Ref ref) async {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/sphere/posts/featured');
return resp.data.map((e) => SnPost.fromJson(e)).cast<SnPost>().toList();
}
class PostFeaturedList extends HookConsumerWidget {
const PostFeaturedList({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final featuredPostsAsync = ref.watch(featuredPostsProvider);
final pageViewController = usePageController();
final pageViewCurrent = useState(0);
useEffect(() {
pageViewController.addListener(() {
pageViewCurrent.value = pageViewController.page?.round() ?? 0;
});
return null;
}, [pageViewController]);
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Card(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
margin: EdgeInsets.zero,
child: Column(
children: [
Row(
spacing: 8,
children: [
const Icon(Symbols.highlight),
Text('Highlight Posts'),
Spacer(),
IconButton(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(),
onPressed: () {
pageViewController.animateToPage(
pageViewCurrent.value - 1,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
},
icon: const Icon(Symbols.arrow_left),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(),
onPressed: () {
pageViewController.animateToPage(
pageViewCurrent.value + 1,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
},
icon: const Icon(Symbols.arrow_right),
),
],
).padding(horizontal: 16, vertical: 8),
featuredPostsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
data: (posts) {
return SizedBox(
height: 320,
child: PageView.builder(
controller: pageViewController,
scrollDirection: Axis.horizontal,
itemCount: posts.length,
itemBuilder: (context, index) {
return SingleChildScrollView(
child: PostActionableItem(
item: posts[index],
borderRadius: 8,
),
);
},
),
);
},
),
],
),
),
);
}
}

View File

@@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'post_featured.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$featuredPostsHash() => r'4b7fffb02eac72f5861b02af1b1e5da36b571698';
/// See also [featuredPosts].
@ProviderFor(featuredPosts)
final featuredPostsProvider = AutoDisposeFutureProvider<List<SnPost>>.internal(
featuredPosts,
name: r'featuredPostsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$featuredPostsHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef FeaturedPostsRef = AutoDisposeFutureProviderRef<List<SnPost>>;
// 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

@@ -16,6 +16,7 @@ import 'package:island/pods/userinfo.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/services/responsive.dart';
import 'package:island/services/time.dart';
import 'package:island/utils/mapping.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
@@ -272,7 +273,7 @@ class PostItem extends HookConsumerWidget {
: item.reactionsCount.entries
.sortedBy((e) => e.value)
.map((e) => e.key)
.first;
.last;
final postLanguage =
item.content != null
@@ -314,6 +315,19 @@ class PostItem extends HookConsumerWidget {
}
}
String parseVisibility(int visibility) {
switch (visibility) {
case 1:
return 'postVisibilityFriends';
case 2:
return 'postVisibilityUnlisted';
case 3:
return 'postVisibilityPrivate';
default:
return 'postVisibilityPublic';
}
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -349,13 +363,27 @@ class PostItem extends HookConsumerWidget {
Text('@${item.publisher.name}').fontSize(11),
],
),
Text(
isFullPost
? (item.publishedAt ?? item.createdAt)!.formatSystem()
: (item.publishedAt ?? item.createdAt)!.formatRelative(
context,
),
).fontSize(10),
Row(
spacing: 6,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
isFullPost
? (item.publishedAt ?? item.createdAt)!
.formatSystem()
: (item.publishedAt ?? item.createdAt)!
.formatRelative(context),
).fontSize(10),
if (item.editedAt != null)
Text(
'editedAt'.tr(args: [item.editedAt!.formatSystem()]),
).fontSize(10),
if (item.visibility != 0)
Text(
parseVisibility(item.visibility).tr(),
).fontSize(10),
],
),
],
),
),
@@ -452,7 +480,9 @@ class PostItem extends HookConsumerWidget {
],
),
)
else if (item.content?.isNotEmpty ?? false)
else if ((item.content?.isNotEmpty ?? false) ||
(item.title?.isNotEmpty ?? false) ||
(item.description?.isNotEmpty ?? false))
Padding(
padding: EdgeInsets.only(
left: renderingPadding.horizontal,
@@ -535,7 +565,7 @@ class PostItem extends HookConsumerWidget {
right: renderingPadding.horizontal,
),
),
if (item.attachments.isNotEmpty)
if (item.attachments.isNotEmpty && item.type != 1)
CloudFileList(
files: item.attachments,
padding: EdgeInsets.symmetric(
@@ -543,37 +573,95 @@ class PostItem extends HookConsumerWidget {
vertical: 4,
),
),
if (item.tags.isNotEmpty)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 2,
children: [
if (item.tags.isNotEmpty)
Wrap(
runAlignment: WrapAlignment.center,
spacing: 8,
children: [
const Icon(Symbols.label, size: 16).padding(top: 2),
for (final tag
in isFullPost ? item.tags : item.tags.take(3))
InkWell(
child: Text('#${tag.name ?? tag.slug}'),
onTap: () {
GoRouter.of(context).pushNamed(
'postTagDetail',
pathParameters: {'slug': tag.slug},
);
},
),
if (!isFullPost && item.tags.length > 3)
Text('+${item.tags.length - 3}').opacity(0.6),
],
),
if (item.categories.isNotEmpty)
Wrap(
runAlignment: WrapAlignment.center,
spacing: 8,
children: [
const Icon(Symbols.category, size: 16).padding(top: 2),
for (final category
in isFullPost
? item.categories
: item.categories.take(2))
InkWell(
child: Text(category.categoryDisplayTitle),
onTap: () {
GoRouter.of(context).pushNamed(
'postCategoryDetail',
pathParameters: {'slug': category.slug},
);
},
),
if (!isFullPost && item.categories.length > 2)
Text('+${item.categories.length - 2}').opacity(0.6),
],
),
],
).padding(horizontal: renderingPadding.horizontal + 4, top: 4),
if (item.meta?['embeds'] != null)
...((item.meta!['embeds'] as List<dynamic>).map(
(embedData) => switch (embedData['type']) {
'link' => EmbedLinkWidget(
link: SnEmbedLink.fromJson(embedData as Map<String, dynamic>),
maxWidth: math.min(
MediaQuery.of(context).size.width,
kWideScreenWidth,
),
margin: EdgeInsets.only(
top: 4,
bottom: 4,
left: renderingPadding.horizontal,
right: renderingPadding.horizontal,
),
),
'poll' => Card(
margin: EdgeInsets.symmetric(
horizontal: renderingPadding.horizontal,
vertical: 8,
),
child: PollSubmit(
initialAnswers: embedData['poll']?['user_answer']?['answer'],
stats: embedData['poll']?['stats'],
poll: SnPollWithStats.fromJson(embedData['poll']),
onSubmit: (_) {},
).padding(horizontal: 16, vertical: 12),
),
_ => Text('Unable show embed: ${embedData['type']}'),
},
)),
...((item.meta!['embeds'] as List<dynamic>)
.map((embedData) => convertMapKeysToSnakeCase(embedData))
.map(
(embedData) => switch (embedData['type']) {
'link' => EmbedLinkWidget(
link: SnScrappedLink.fromJson(embedData),
maxWidth: math.min(
MediaQuery.of(context).size.width,
kWideScreenWidth,
),
margin: EdgeInsets.only(
top: 4,
bottom: 4,
left: renderingPadding.horizontal,
right: renderingPadding.horizontal,
),
),
'poll' => Card(
margin: EdgeInsets.symmetric(
horizontal: renderingPadding.horizontal,
vertical: 8,
),
child:
embedData['poll'] == null
? Text('Poll was not loaded...')
: PollSubmit(
initialAnswers:
embedData['poll']?['user_answer']?['answer'],
stats: embedData['poll']?['stats'],
poll: SnPollWithStats.fromJson(embedData['poll']),
onSubmit: (_) {},
).padding(horizontal: 16, vertical: 12),
),
_ => Text('Unable show embed: ${embedData['type']}'),
},
)),
if (isShowReference)
_buildReferencePost(context, item, renderingPadding),
if (item.repliesCount > 0 && isEmbedReply)
@@ -770,7 +858,7 @@ class PostReplyPreview extends HookConsumerWidget {
final posts = useState<List<SnPost>>([]);
final loading = useState(false);
Future<void> fetchMoreReplies({int pageSize = 1}) async {
Future<void> fetchMoreReplies({int pageSize = 3}) async {
final client = ref.read(apiClientProvider);
loading.value = true;
@@ -779,10 +867,14 @@ class PostReplyPreview extends HookConsumerWidget {
'/sphere/posts/${parent.id}/replies',
queryParameters: {'offset': posts.value.length, 'take': pageSize},
);
posts.value = [
...posts.value,
...response.data.map((e) => SnPost.fromJson(e)),
];
try {
posts.value = [
...posts.value,
...response.data.map((e) => SnPost.fromJson(e)),
];
} catch (_) {
// ignore disposed
}
} catch (err) {
showErrorAlert(err);
} finally {
@@ -877,38 +969,40 @@ class PostReplyPreview extends HookConsumerWidget {
),
],
)
: featuredReply!.when(
: (featuredReply!).map(
data:
(value) => Row(
(data) => Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 8,
children: [
ProfilePictureWidget(
file: value?.publisher.picture,
file: data.value?.publisher.picture,
radius: 12,
).padding(top: 4),
if (value?.content?.isNotEmpty ?? false)
if (data.value?.content?.isNotEmpty ?? false)
Expanded(
child: MarkdownTextContent(content: value!.content!),
child: MarkdownTextContent(
content: data.value!.content!,
),
)
else
Expanded(
child: Text(
'postHasAttachments',
).plural(value?.attachments.length ?? 0),
).plural(data.value?.attachments.length ?? 0),
),
],
),
error:
(error, _) => Row(
(e) => Row(
spacing: 8,
children: [
const Icon(Symbols.close, size: 18),
Text(error.toString()),
Text(e.error.toString()),
],
),
loading:
() => Row(
(_) => Row(
spacing: 8,
children: [
SizedBox(
@@ -939,7 +1033,6 @@ class PostReplyPreview extends HookConsumerWidget {
children: [
Text('repliesCount')
.plural(parent.repliesCount)
.tr()
.fontSize(15)
.bold()
.padding(horizontal: 5),

View File

@@ -15,7 +15,12 @@ class PostListNotifier extends _$PostListNotifier
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnPost>> build(String? pubName) {
Future<CursorPagingData<SnPost>> build(
String? pubName, {
int? type,
List<String>? categories,
List<String>? tags,
}) {
return fetch(cursor: null);
}
@@ -28,6 +33,9 @@ class PostListNotifier extends _$PostListNotifier
'offset': offset,
'take': _pageSize,
if (pubName != null) 'pub': pubName,
if (type != null) 'type': type,
if (tags != null) 'tags': tags,
if (categories != null) 'categories': categories,
};
final response = await client.get(
@@ -60,6 +68,9 @@ enum PostItemType {
class SliverPostList extends HookConsumerWidget {
final String? pubName;
final int? type;
final List<String>? categories;
final List<String>? tags;
final PostItemType itemType;
final Color? backgroundColor;
final EdgeInsets? padding;
@@ -70,6 +81,9 @@ class SliverPostList extends HookConsumerWidget {
const SliverPostList({
super.key,
this.pubName,
this.type,
this.categories,
this.tags,
this.itemType = PostItemType.regular,
this.backgroundColor,
this.padding,
@@ -81,9 +95,26 @@ class SliverPostList extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return PagingHelperSliverView(
provider: postListNotifierProvider(pubName),
futureRefreshable: postListNotifierProvider(pubName).future,
notifierRefreshable: postListNotifierProvider(pubName).notifier,
provider: postListNotifierProvider(
pubName,
type: type,
categories: categories,
tags: tags,
),
futureRefreshable:
postListNotifierProvider(
pubName,
type: type,
categories: categories,
tags: tags,
).future,
notifierRefreshable:
postListNotifierProvider(
pubName,
type: type,
categories: categories,
tags: tags,
).notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder(
itemCount: widgetCount,

View File

@@ -6,7 +6,7 @@ part of 'post_list.dart';
// RiverpodGenerator
// **************************************************************************
String _$postListNotifierHash() => r'2e4fb36123d3f97ac1edf9945043251d4eb519a2';
String _$postListNotifierHash() => r'2ca4f3cfbbcd04f3cc32e7f7bd511a5811042829';
/// Copied from Dart SDK
class _SystemHash {
@@ -32,8 +32,16 @@ class _SystemHash {
abstract class _$PostListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPost>> {
late final String? pubName;
late final int? type;
late final List<String>? categories;
late final List<String>? tags;
FutureOr<CursorPagingData<SnPost>> build(String? pubName);
FutureOr<CursorPagingData<SnPost>> build(
String? pubName, {
int? type,
List<String>? categories,
List<String>? tags,
});
}
/// See also [PostListNotifier].
@@ -47,15 +55,30 @@ class PostListNotifierFamily
const PostListNotifierFamily();
/// See also [PostListNotifier].
PostListNotifierProvider call(String? pubName) {
return PostListNotifierProvider(pubName);
PostListNotifierProvider call(
String? pubName, {
int? type,
List<String>? categories,
List<String>? tags,
}) {
return PostListNotifierProvider(
pubName,
type: type,
categories: categories,
tags: tags,
);
}
@override
PostListNotifierProvider getProviderOverride(
covariant PostListNotifierProvider provider,
) {
return call(provider.pubName);
return call(
provider.pubName,
type: provider.type,
categories: provider.categories,
tags: provider.tags,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@@ -81,20 +104,32 @@ class PostListNotifierProvider
CursorPagingData<SnPost>
> {
/// See also [PostListNotifier].
PostListNotifierProvider(String? pubName)
: this._internal(
() => PostListNotifier()..pubName = pubName,
from: postListNotifierProvider,
name: r'postListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$postListNotifierHash,
dependencies: PostListNotifierFamily._dependencies,
allTransitiveDependencies:
PostListNotifierFamily._allTransitiveDependencies,
pubName: pubName,
);
PostListNotifierProvider(
String? pubName, {
int? type,
List<String>? categories,
List<String>? tags,
}) : this._internal(
() =>
PostListNotifier()
..pubName = pubName
..type = type
..categories = categories
..tags = tags,
from: postListNotifierProvider,
name: r'postListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$postListNotifierHash,
dependencies: PostListNotifierFamily._dependencies,
allTransitiveDependencies:
PostListNotifierFamily._allTransitiveDependencies,
pubName: pubName,
type: type,
categories: categories,
tags: tags,
);
PostListNotifierProvider._internal(
super._createNotifier, {
@@ -104,15 +139,26 @@ class PostListNotifierProvider
required super.debugGetCreateSourceHash,
required super.from,
required this.pubName,
required this.type,
required this.categories,
required this.tags,
}) : super.internal();
final String? pubName;
final int? type;
final List<String>? categories;
final List<String>? tags;
@override
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
covariant PostListNotifier notifier,
) {
return notifier.build(pubName);
return notifier.build(
pubName,
type: type,
categories: categories,
tags: tags,
);
}
@override
@@ -120,13 +166,21 @@ class PostListNotifierProvider
return ProviderOverride(
origin: this,
override: PostListNotifierProvider._internal(
() => create()..pubName = pubName,
() =>
create()
..pubName = pubName
..type = type
..categories = categories
..tags = tags,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
pubName: pubName,
type: type,
categories: categories,
tags: tags,
),
);
}
@@ -142,13 +196,20 @@ class PostListNotifierProvider
@override
bool operator ==(Object other) {
return other is PostListNotifierProvider && other.pubName == pubName;
return other is PostListNotifierProvider &&
other.pubName == pubName &&
other.type == type &&
other.categories == categories &&
other.tags == tags;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, pubName.hashCode);
hash = _SystemHash.combine(hash, type.hashCode);
hash = _SystemHash.combine(hash, categories.hashCode);
hash = _SystemHash.combine(hash, tags.hashCode);
return _SystemHash.finish(hash);
}
@@ -160,6 +221,15 @@ mixin PostListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPost>> {
/// The parameter `pubName` of this provider.
String? get pubName;
/// The parameter `type` of this provider.
int? get type;
/// The parameter `categories` of this provider.
List<String>? get categories;
/// The parameter `tags` of this provider.
List<String>? get tags;
}
class _PostListNotifierProviderElement
@@ -173,6 +243,13 @@ class _PostListNotifierProviderElement
@override
String? get pubName => (origin as PostListNotifierProvider).pubName;
@override
int? get type => (origin as PostListNotifierProvider).type;
@override
List<String>? get categories =>
(origin as PostListNotifierProvider).categories;
@override
List<String>? get tags => (origin as PostListNotifierProvider).tags;
}
// ignore_for_file: type=lint

View File

@@ -1,11 +1,13 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package: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/screens/posts/compose.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/publishers_modal.dart';
@@ -14,8 +16,14 @@ import 'package:styled_widget/styled_widget.dart';
class PostQuickReply extends HookConsumerWidget {
final SnPost parent;
final Function? onPosted;
const PostQuickReply({super.key, required this.parent, this.onPosted});
final VoidCallback? onPosted;
final VoidCallback? onLaunch;
const PostQuickReply({
super.key,
required this.parent,
this.onPosted,
this.onLaunch,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -48,7 +56,7 @@ class PostQuickReply extends HookConsumerWidget {
'content': contentController.text,
'replied_post_id': parent.id,
},
options: Options(headers: {'X-Pub': currentPublisher.value?.name}),
queryParameters: {'pub': currentPublisher.value?.name},
);
contentController.clear();
onPosted?.call();
@@ -83,9 +91,10 @@ class PostQuickReply extends HookConsumerWidget {
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: 'Post your reply',
border: const OutlineInputBorder(),
hintText: 'postReplyPlaceholder'.tr(),
border: InputBorder.none,
isDense: true,
isCollapsed: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
@@ -97,6 +106,26 @@ class PostQuickReply extends HookConsumerWidget {
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
IconButton(
onPressed: () {
onLaunch?.call();
GoRouter.of(context)
.pushNamed(
'postCompose',
extra: PostComposeInitialState(
content: contentController.text,
replyingTo: parent,
),
)
.then((value) {
if (value != null) onPosted?.call();
});
},
icon: const Icon(Symbols.launch, size: 20),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
@@ -110,6 +139,7 @@ class PostQuickReply extends HookConsumerWidget {
: Icon(Symbols.send, size: 20),
color: Theme.of(context).colorScheme.primary,
onPressed: submitting.value ? null : performAction,
constraints: const BoxConstraints(),
),
],
),

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