Compare commits

...

34 Commits

Author SHA1 Message Date
e1286c797f Audio player 2025-08-02 14:54:56 +08:00
bec037622f Audio recorder 2025-08-02 14:06:58 +08:00
a0d8c1a9b3 🐛 Fixes call 2025-08-02 01:53:02 +08:00
26135d2116 🐛 Fix embed 2025-08-01 23:54:31 +08:00
71b67fd22d 🐛 Fixes of videos and more 2025-08-01 23:21:43 +08:00
855072dfea 🐛 Fix refresh failed after delete data 2025-08-01 21:46:43 +08:00
b39e2e2d64 💄 Video player optimized 2025-08-01 20:36:39 +08:00
84b1d6a346 💄 Optimization 2025-08-01 18:03:54 +08:00
28335dd548 💄 Optimize profile page 2025-08-01 16:29:23 +08:00
7253e2d3ef 🐛 Fix fast scroll explore cause error 2025-08-01 13:03:05 +08:00
4d489425fa Comment threading
👽 Fix chat notify level
2025-08-01 13:01:38 +08:00
890a8a44cf Adjustable zoom in image quaility 2025-08-01 11:37:56 +08:00
8e3583f57a 💄 Optimize post 2025-08-01 11:30:38 +08:00
d0ff14659f 🚀 Launch 3.1.0+116 2025-08-01 01:36:31 +08:00
1f7caaeaac 💄 Optimize call 2025-08-01 01:03:01 +08:00
9f9f42071a 💄 Fixes of call and optimizations 2025-08-01 00:12:18 +08:00
6bd6e994cb Refreshed article post editor 2025-07-31 22:39:27 +08:00
02e68d76ee 💄 Optimized creator mode post item 2025-07-31 22:31:22 +08:00
d04b06089c 💄 Optimized reaction sheet 2025-07-31 22:27:25 +08:00
9be6fea2e0 ⬆️ Upgrade deps in order to fix firebase breaking changes 2025-07-31 22:04:19 +08:00
6b1214a06f 🐛 Fix the goddamn AI code 2025-07-31 21:44:14 +08:00
4597373ac9 Message translate 2025-07-31 21:16:38 +08:00
047c8d93aa Translate infra and post translate 2025-07-31 21:05:29 +08:00
715f95ca22 🐛 Bug fixes 2025-07-31 16:48:50 +08:00
ba709012d7 💄 Optimize article compose 2025-07-31 02:32:03 +08:00
fd186f8391 💄 Optimize post compose and more 2025-07-31 02:01:18 +08:00
262d36cd2d 🐛 Fix bugs 2025-07-31 01:20:11 +08:00
f320855348 💄 Optimize explore page styles 2025-07-31 01:17:20 +08:00
ed90152462 🐛 Fix message with link will not able to load 2025-07-30 23:04:59 +08:00
6e5c5f1690 💄 Optimized post item 2025-07-30 22:36:27 +08:00
7c92dee097 Edited the post item styles 2025-07-30 22:16:36 +08:00
e4bb031138 💄 Optimize file viewer 2025-07-30 00:29:26 +08:00
97226ae96b Websocket hearbeat 2025-07-29 18:13:26 +08:00
d8cd33e79a Add file from ID
🐛 Fix explore unauthorized render error on large screen
2025-07-28 02:01:36 +08:00
93 changed files with 5115 additions and 2367 deletions

View File

@@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
@@ -89,6 +90,13 @@
</intent-filter>
</activity>
<!-- Livekit Screenshare -->
<service
android:name="de.julianassmann.flutter_background.IsolateHolderService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="mediaProjection" />
<!-- Sign in with Apple -->
<activity
android:name="com.aboutyou.dart_packages.sign_in_with_apple.SignInWithAppleCallback"

View File

@@ -146,7 +146,12 @@
"edited": "Edited",
"addVideo": "Add video",
"addPhoto": "Add photo",
"addVoice": "Add your voice",
"addFile": "Add file",
"recordAudio": "Record Audio",
"linkAttachment": "Link Attachment",
"fileIdCannotBeEmpty": "File ID cannot be empty",
"failedToFetchFile": "Failed to fetch file: {}",
"createDirectMessage": "Send new DM",
"gotoDirectMessage": "Go to DM",
"react": "React",
@@ -352,6 +357,8 @@
"postTitle": "Title",
"postDescription": "Description",
"call": "Call",
"callLeave": "Leave",
"callEnd": "End this call",
"done": "Done",
"loginResetPasswordSent": "Password reset link sent, please check your email inbox.",
"accountDeletion": "Delete Account",
@@ -622,8 +629,8 @@
"chatJoin": "Join the Chat",
"realmJoin": "Join the Realm",
"realmJoinSuccess": "Successfully joined the realm.",
"discoverRealms": "Discover Realms",
"discoverPublishers": "Discover Publishers",
"discoverRealms": "Discover realms",
"discoverPublishers": "Discover publishers",
"search": "Search",
"publisherMembers": "Collaborators",
"developerHub": "Developer Hub",
@@ -702,5 +709,29 @@
"aboutDeviceName": "Device Name",
"aboutDeviceIdentifier": "Device Identifier",
"donate": "Donate",
"donateDescription": "Support us to continue developing the Solar Network and keep the server up and running."
}
"donateDescription": "Support us to continue developing the Solar Network and keep the server up and running.",
"fileId": "File ID",
"fileIdHint": "The file ID is the ID you get after upload the file via the Solar Network Drive.",
"translate": "Translate",
"translating": "Translating",
"translated": "Translated",
"reactionThumbUp": "Thumbs Up",
"reactionThumbDown": "Thumbs Down",
"reactionJustOkay": "Just Okay",
"reactionCry": "Cry",
"reactionConfuse": "Confused",
"reactionClap": "Clap",
"reactionLaugh": "Laugh",
"reactionAngry": "Angry",
"reactionParty": "Party",
"reactionPray": "Pray",
"reactionHeart": "Heart",
"selectMicrophone": "Select Microphone",
"selectCamera": "Select Camera",
"switchedTo": "Switched to {}",
"connecting": "Connecting",
"reconnecting": "Reconnecting",
"disconnected": "Disconnected",
"connected": "Connected",
"repliesLoadMore": "Load more replies"
}

View File

@@ -123,6 +123,10 @@
"addVideo": "添加视频",
"addPhoto": "添加照片",
"addFile": "添加文件",
"addAttachmentById": "通过 ID 添加附件",
"enterFileId": "输入文件 ID",
"fileIdCannotBeEmpty": "文件 ID 不能为空",
"failedToFetchFile": "获取文件失败: {}",
"createDirectMessage": "创建新私人消息",
"gotoDirectMessage": "前往私信",
"react": "反应",

View File

@@ -123,6 +123,10 @@
"addVideo": "新增影片",
"addPhoto": "新增照片",
"addFile": "新增檔案",
"addAttachmentById": "透過 ID 新增附件",
"enterFileId": "輸入檔案 ID",
"fileIdCannotBeEmpty": "檔案 ID 不能為空",
"failedToFetchFile": "無法取得檔案: {}",
"createDirectMessage": "建立新私人訊息",
"gotoDirectMessage": "Go to DM",
"react": "反應",

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '13.0'
platform :ios, '15.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -40,33 +40,33 @@ PODS:
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/CoreOnly (11.15.0):
- FirebaseCore (~> 11.15.0)
- Firebase/Messaging (11.15.0):
- Firebase/CoreOnly (12.0.0):
- FirebaseCore (~> 12.0.0)
- Firebase/Messaging (12.0.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.15.0)
- firebase_core (3.15.2):
- Firebase/CoreOnly (= 11.15.0)
- FirebaseMessaging (~> 12.0.0)
- firebase_core (4.0.0):
- Firebase/CoreOnly (= 12.0.0)
- Flutter
- firebase_messaging (15.2.10):
- Firebase/Messaging (= 11.15.0)
- firebase_messaging (16.0.0):
- Firebase/Messaging (= 12.0.0)
- firebase_core
- Flutter
- FirebaseCore (11.15.0):
- FirebaseCoreInternal (~> 11.15.0)
- FirebaseCore (12.0.0):
- FirebaseCoreInternal (~> 12.0.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (11.15.0):
- FirebaseCoreInternal (12.0.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (12.0.0):
- FirebaseCore (~> 12.0.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- FirebaseMessaging (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
@@ -93,9 +93,9 @@ PODS:
- flutter_udid (0.0.1):
- Flutter
- SAMKeychain
- flutter_webrtc (0.14.0):
- flutter_webrtc (1.0.0):
- Flutter
- WebRTC-SDK (= 125.6422.07)
- WebRTC-SDK (= 137.7151.02)
- gal (1.0.0):
- Flutter
- FlutterMacOS
@@ -131,10 +131,10 @@ PODS:
- irondash_engine_context (0.0.1):
- Flutter
- Kingfisher (8.5.0)
- livekit_client (2.4.9):
- livekit_client (2.5.0):
- Flutter
- flutter_webrtc
- WebRTC-SDK (= 125.6422.07)
- WebRTC-SDK (= 137.7151.02)
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
@@ -191,6 +191,8 @@ PODS:
- sqlite3/common
- sqlite3/rtree (3.50.3):
- sqlite3/common
- sqlite3/session (3.50.3):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
@@ -200,6 +202,7 @@ PODS:
- sqlite3/math
- sqlite3/perf-threadsafe
- sqlite3/rtree
- sqlite3/session
- super_native_extensions (0.0.1):
- Flutter
- SwiftyGif (5.4.5)
@@ -209,7 +212,7 @@ PODS:
- Flutter
- wakelock_plus (0.0.1):
- Flutter
- WebRTC-SDK (125.6422.07)
- WebRTC-SDK (137.7151.02)
DEPENDENCIES:
- Alamofire
@@ -361,13 +364,13 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_core: 995454a784ff288be5689b796deb9e9fa3601818
firebase_messaging: f4a41dd102ac18b840eba3f39d67e77922d3f707
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09
Firebase: 800d487043c0557d9faed71477a38d9aafb08a41
firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4
firebase_messaging: d17feef781edc84ebefe62624fb384358ad96361
FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a
FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55
FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988
FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
@@ -376,14 +379,14 @@ SPEC CHECKSUMS:
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117
flutter_webrtc: 6f7da106613d52ade777d5b4875a43f48c28b457
gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c
livekit_client: 3f79d79233a5bd13d5b541732624ef959d7c538e
livekit_client: e3b79b99405428aac439b6b76a254cd9a11dbbfb
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
@@ -404,14 +407,14 @@ SPEC CHECKSUMS:
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81
sqlite3_flutter_libs: ce0522d143cee6ef5e16587acfce8f476316e005
sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
WebRTC-SDK: dff00a3892bc570b6014e046297782084071657e
WebRTC-SDK: d20de357dcbf7c9696b124b39f3ff62125107e4b
PODFILE CHECKSUM: f6df17c2a0cbd7af89692fd3877231eaea40230f
PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f
COCOAPODS: 1.16.2

View File

@@ -3,13 +3,15 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; };
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
73CDD6812DEC00480059D95D /* SolianNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
73D4264B2DEB815D006C0AAE /* NotifyDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D4264A2DEB815D006C0AAE /* NotifyDelegate.swift */; };
@@ -32,6 +34,13 @@
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
73ACDFC12E3D0E6100B63535 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 73ACDFAA2E3D0E6100B63535;
remoteInfo = SolianBroadcastExtension;
};
73C305D62E0BE878009035B9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
@@ -55,6 +64,7 @@
dstPath = "";
dstSubfolderSpec = 13;
files = (
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */,
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */,
73CDD6812DEC00480059D95D /* SolianNotificationService.appex in Embed Foundation Extensions */,
);
@@ -91,6 +101,9 @@
3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
73ACDFB82E3D0E6100B63535 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianNotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
73D4264A2DEB815D006C0AAE /* NotifyDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyDelegate.swift; sourceTree = "<group>"; };
@@ -117,6 +130,13 @@
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
73ACDFCA2E3D0E6100B63535 /* Exceptions for "SolianBroadcastExtension" folder in "SolianBroadcastExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */;
};
73C305DC2E0BE878009035B9 /* Exceptions for "SolianShareExtension" folder in "SolianShareExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
@@ -150,6 +170,14 @@
path = Services;
sourceTree = "<group>";
};
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
73ACDFCA2E3D0E6100B63535 /* Exceptions for "SolianBroadcastExtension" folder in "SolianBroadcastExtension" target */,
);
path = SolianBroadcastExtension;
sourceTree = "<group>";
};
73C305CF2E0BE878009035B9 /* SolianShareExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
@@ -177,6 +205,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
73ACDFA82E3D0E6100B63535 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
73C305CB2E0BE878009035B9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -220,6 +256,8 @@
AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */,
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */,
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */,
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */,
73ACDFB82E3D0E6100B63535 /* UIKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -264,6 +302,7 @@
97C146F01CF9000F007C117D /* Runner */,
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
91E124CE95BCB4DCD890160D /* Pods */,
@@ -279,6 +318,7 @@
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */,
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */,
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -323,6 +363,28 @@
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */;
buildPhases = (
73ACDFA72E3D0E6100B63535 /* Sources */,
73ACDFA82E3D0E6100B63535 /* Frameworks */,
73ACDFA92E3D0E6100B63535 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */,
);
name = SolianBroadcastExtension;
packageProductDependencies = (
);
productName = SolianBroadcastExtension;
productReference = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
73C305CD2E0BE878009035B9 /* SolianShareExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 73C305DD2E0BE878009035B9 /* Build configuration list for PBXNativeTarget "SolianShareExtension" */;
@@ -385,6 +447,7 @@
dependencies = (
73CDD6802DEC00480059D95D /* PBXTargetDependency */,
73C305D72E0BE878009035B9 /* PBXTargetDependency */,
73ACDFC22E3D0E6100B63535 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
73268D272DEB012A0076E970 /* Services */,
@@ -409,6 +472,9 @@
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
73ACDFAA2E3D0E6100B63535 = {
CreatedOnToolsVersion = 16.4;
};
73C305CD2E0BE878009035B9 = {
CreatedOnToolsVersion = 16.4;
};
@@ -438,6 +504,7 @@
331C8080294A63A400263BE5 /* RunnerTests */,
73CDD6792DEC00480059D95D /* SolianNotificationService */,
73C305CD2E0BE878009035B9 /* SolianShareExtension */,
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */,
);
};
/* End PBXProject section */
@@ -450,6 +517,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
73ACDFA92E3D0E6100B63535 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
73C305CC2E0BE878009035B9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -525,10 +599,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -586,10 +664,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -643,6 +725,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
73ACDFA72E3D0E6100B63535 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
73C305CA2E0BE878009035B9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -675,6 +764,11 @@
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
73ACDFC22E3D0E6100B63535 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */;
targetProxy = 73ACDFC12E3D0E6100B63535 /* PBXContainerItemProxy */;
};
73C305D72E0BE878009035B9 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 73C305CD2E0BE878009035B9 /* SolianShareExtension */;
@@ -773,7 +867,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -836,6 +930,123 @@
};
name = Profile;
};
73ACDFC42E3D0E6100B63535 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = SolianBroadcastExtension/SolianBroadcastExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SolianBroadcastExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianBroadcastExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
73ACDFC52E3D0E6100B63535 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = SolianBroadcastExtension/SolianBroadcastExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SolianBroadcastExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianBroadcastExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
73ACDFC62E3D0E6100B63535 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = SolianBroadcastExtension/SolianBroadcastExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SolianBroadcastExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianBroadcastExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
73C305D92E0BE878009035B9 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */;
@@ -1204,7 +1415,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1232,7 +1443,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1258,6 +1469,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
73ACDFC42E3D0E6100B63535 /* Debug */,
73ACDFC52E3D0E6100B63535 /* Release */,
73ACDFC62E3D0E6100B63535 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
73C305DD2E0BE878009035B9 /* Build configuration list for PBXNativeTarget "SolianShareExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@@ -0,0 +1,37 @@
//
// Atomic.swift
// Broadcast Extension
//
// Created by Maksym Shcheglov.
// https://www.onswiftwings.com/posts/atomic-property-wrapper/
//
import Foundation
@propertyWrapper
struct Atomic<Value> {
private var value: Value
private let lock = NSLock()
init(wrappedValue value: Value) {
self.value = value
}
var wrappedValue: Value {
get { load() }
set { store(newValue: newValue) }
}
func load() -> Value {
lock.lock()
defer { lock.unlock() }
return value
}
mutating func store(newValue: Value) {
lock.lock()
defer { lock.unlock() }
value = newValue
}
}

View File

@@ -0,0 +1,29 @@
//
// DarwinNotificationCenter.swift
// Broadcast Extension
//
// Created by Alex-Dan Bumbu on 23/03/2021.
// Copyright © 2021 8x8, Inc. All rights reserved.
//
import Foundation
enum DarwinNotification: String {
case broadcastStarted = "iOS_BroadcastStarted"
case broadcastStopped = "iOS_BroadcastStopped"
}
class DarwinNotificationCenter {
static let shared = DarwinNotificationCenter()
private let notificationCenter: CFNotificationCenter
init() {
notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
}
func postNotification(_ name: DarwinNotification) {
CFNotificationCenterPostNotification(notificationCenter, CFNotificationName(rawValue: name.rawValue as CFString), nil, nil, true)
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.broadcast-services-upload</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).SampleHandler</string>
<key>RPBroadcastProcessMode</key>
<string>RPBroadcastProcessModeSampleBuffer</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,103 @@
//
// SampleHandler.swift
// Broadcast Extension
//
// Created by Alex-Dan Bumbu on 04.06.2021.
//
import ReplayKit
import OSLog
let broadcastLogger = OSLog(subsystem: "dev.solsynth.solian", category: "Broadcast")
private enum Constants {
// the App Group ID value that the app and the broadcast extension targets are setup with. It differs for each app.
static let appGroupIdentifier = "group.solsynth.solian"
}
class SampleHandler: RPBroadcastSampleHandler {
private var clientConnection: SocketConnection?
private var uploader: SampleUploader?
private var frameCount: Int = 0
var socketFilePath: String {
let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupIdentifier)
return sharedContainer?.appendingPathComponent("rtc_SSFD").path ?? ""
}
override init() {
super.init()
if let connection = SocketConnection(filePath: socketFilePath) {
clientConnection = connection
setupConnection()
uploader = SampleUploader(connection: connection)
}
os_log(.debug, log: broadcastLogger, "%{public}s", socketFilePath)
}
override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) {
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
frameCount = 0
DarwinNotificationCenter.shared.postNotification(.broadcastStarted)
openConnection()
}
override func broadcastPaused() {
// User has requested to pause the broadcast. Samples will stop being delivered.
}
override func broadcastResumed() {
// User has requested to resume the broadcast. Samples delivery will resume.
}
override func broadcastFinished() {
// User has requested to finish the broadcast.
DarwinNotificationCenter.shared.postNotification(.broadcastStopped)
clientConnection?.close()
}
override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
switch sampleBufferType {
case RPSampleBufferType.video:
uploader?.send(sample: sampleBuffer)
default:
break
}
}
}
private extension SampleHandler {
func setupConnection() {
clientConnection?.didClose = { [weak self] error in
os_log(.debug, log: broadcastLogger, "client connection did close \(String(describing: error))")
if let error = error {
self?.finishBroadcastWithError(error)
} else {
// the displayed failure message is more user friendly when using NSError instead of Error
let JMScreenSharingStopped = 10001
let customError = NSError(domain: RPRecordingErrorDomain, code: JMScreenSharingStopped, userInfo: [NSLocalizedDescriptionKey: "Screen sharing stopped"])
self?.finishBroadcastWithError(customError)
}
}
}
func openConnection() {
let queue = DispatchQueue(label: "broadcast.connectTimer")
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: .milliseconds(100), leeway: .milliseconds(500))
timer.setEventHandler { [weak self] in
guard self?.clientConnection?.open() == true else {
return
}
timer.cancel()
}
timer.resume()
}
}

View File

@@ -0,0 +1,147 @@
//
// SampleUploader.swift
// Broadcast Extension
//
// Created by Alex-Dan Bumbu on 22/03/2021.
// Copyright © 2021 8x8, Inc. All rights reserved.
//
import Foundation
import ReplayKit
import OSLog
private enum Constants {
static let bufferMaxLength = 10240
}
class SampleUploader {
private static var imageContext = CIContext(options: nil)
@Atomic private var isReady = false
private var connection: SocketConnection
private var dataToSend: Data?
private var byteIndex = 0
private let serialQueue: DispatchQueue
init(connection: SocketConnection) {
self.connection = connection
self.serialQueue = DispatchQueue(label: "org.jitsi.meet.broadcast.sampleUploader")
setupConnection()
}
@discardableResult func send(sample buffer: CMSampleBuffer) -> Bool {
guard isReady else {
return false
}
isReady = false
dataToSend = prepare(sample: buffer)
byteIndex = 0
serialQueue.async { [weak self] in
self?.sendDataChunk()
}
return true
}
}
private extension SampleUploader {
func setupConnection() {
connection.didOpen = { [weak self] in
self?.isReady = true
}
connection.streamHasSpaceAvailable = { [weak self] in
self?.serialQueue.async {
if let success = self?.sendDataChunk() {
self?.isReady = !success
}
}
}
}
@discardableResult func sendDataChunk() -> Bool {
guard let dataToSend = dataToSend else {
return false
}
var bytesLeft = dataToSend.count - byteIndex
var length = bytesLeft > Constants.bufferMaxLength ? Constants.bufferMaxLength : bytesLeft
length = dataToSend[byteIndex..<(byteIndex + length)].withUnsafeBytes {
guard let ptr = $0.bindMemory(to: UInt8.self).baseAddress else {
return 0
}
return connection.writeToStream(buffer: ptr, maxLength: length)
}
if length > 0 {
byteIndex += length
bytesLeft -= length
if bytesLeft == 0 {
self.dataToSend = nil
byteIndex = 0
}
} else {
os_log(.debug, log: broadcastLogger, "writeBufferToStream failure")
}
return true
}
func prepare(sample buffer: CMSampleBuffer) -> Data? {
guard let imageBuffer = CMSampleBufferGetImageBuffer(buffer) else {
os_log(.debug, log: broadcastLogger, "image buffer not available")
return nil
}
CVPixelBufferLockBaseAddress(imageBuffer, .readOnly)
let scaleFactor = 1.0
let width = CVPixelBufferGetWidth(imageBuffer)/Int(scaleFactor)
let height = CVPixelBufferGetHeight(imageBuffer)/Int(scaleFactor)
let orientation = CMGetAttachment(buffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil)?.uintValue ?? 0
let scaleTransform = CGAffineTransform(scaleX: CGFloat(1.0/scaleFactor), y: CGFloat(1.0/scaleFactor))
let bufferData = self.jpegData(from: imageBuffer, scale: scaleTransform)
CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly)
guard let messageData = bufferData else {
os_log(.debug, log: broadcastLogger, "corrupted image buffer")
return nil
}
let httpResponse = CFHTTPMessageCreateResponse(nil, 200, nil, kCFHTTPVersion1_1).takeRetainedValue()
CFHTTPMessageSetHeaderFieldValue(httpResponse, "Content-Length" as CFString, String(messageData.count) as CFString)
CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Width" as CFString, String(width) as CFString)
CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Height" as CFString, String(height) as CFString)
CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Orientation" as CFString, String(orientation) as CFString)
CFHTTPMessageSetBody(httpResponse, messageData as CFData)
let serializedMessage = CFHTTPMessageCopySerializedMessage(httpResponse)?.takeRetainedValue() as Data?
return serializedMessage
}
func jpegData(from buffer: CVPixelBuffer, scale scaleTransform: CGAffineTransform) -> Data? {
let image = CIImage(cvPixelBuffer: buffer).transformed(by: scaleTransform)
guard let colorSpace = image.colorSpace else {
return nil
}
let options: [CIImageRepresentationOption: Float] = [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 1.0]
return SampleUploader.imageContext.jpegRepresentation(of: image, colorSpace: colorSpace, options: options)
}
}

View File

@@ -0,0 +1,199 @@
//
// SocketConnection.swift
// Broadcast Extension
//
// Created by Alex-Dan Bumbu on 22/03/2021.
// Copyright © 2021 Atlassian Inc. All rights reserved.
//
import Foundation
import OSLog
class SocketConnection: NSObject {
var didOpen: (() -> Void)?
var didClose: ((Error?) -> Void)?
var streamHasSpaceAvailable: (() -> Void)?
private let filePath: String
private var socketHandle: Int32 = -1
private var address: sockaddr_un?
private var inputStream: InputStream?
private var outputStream: OutputStream?
private var networkQueue: DispatchQueue?
private var shouldKeepRunning = false
init?(filePath path: String) {
filePath = path
socketHandle = Darwin.socket(AF_UNIX, SOCK_STREAM, 0)
guard socketHandle != -1 else {
os_log(.debug, log: broadcastLogger, "failure: create socket")
return nil
}
}
func open() -> Bool {
os_log(.debug, log: broadcastLogger, "open socket connection")
guard FileManager.default.fileExists(atPath: filePath) else {
os_log(.debug, log: broadcastLogger, "failure: socket file missing")
return false
}
guard setupAddress() == true else {
return false
}
guard connectSocket() == true else {
return false
}
setupStreams()
inputStream?.open()
outputStream?.open()
return true
}
func close() {
unscheduleStreams()
inputStream?.delegate = nil
outputStream?.delegate = nil
inputStream?.close()
outputStream?.close()
inputStream = nil
outputStream = nil
}
func writeToStream(buffer: UnsafePointer<UInt8>, maxLength length: Int) -> Int {
outputStream?.write(buffer, maxLength: length) ?? 0
}
}
extension SocketConnection: StreamDelegate {
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
switch eventCode {
case .openCompleted:
os_log(.debug, log: broadcastLogger, "client stream open completed")
if aStream == outputStream {
didOpen?()
}
case .hasBytesAvailable:
if aStream == inputStream {
var buffer: UInt8 = 0
let numberOfBytesRead = inputStream?.read(&buffer, maxLength: 1)
if numberOfBytesRead == 0 && aStream.streamStatus == .atEnd {
os_log(.debug, log: broadcastLogger, "server socket closed")
close()
notifyDidClose(error: nil)
}
}
case .hasSpaceAvailable:
if aStream == outputStream {
streamHasSpaceAvailable?()
}
case .errorOccurred:
os_log(.debug, log: broadcastLogger, "client stream error occured: \(String(describing: aStream.streamError))")
close()
notifyDidClose(error: aStream.streamError)
default:
break
}
}
}
private extension SocketConnection {
func setupAddress() -> Bool {
var addr = sockaddr_un()
guard filePath.count < MemoryLayout.size(ofValue: addr.sun_path) else {
os_log(.debug, log: broadcastLogger, "failure: fd path is too long")
return false
}
_ = withUnsafeMutablePointer(to: &addr.sun_path.0) { ptr in
filePath.withCString {
strncpy(ptr, $0, filePath.count)
}
}
address = addr
return true
}
func connectSocket() -> Bool {
guard var addr = address else {
return false
}
let status = withUnsafePointer(to: &addr) { ptr in
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) {
Darwin.connect(socketHandle, $0, socklen_t(MemoryLayout<sockaddr_un>.size))
}
}
guard status == noErr else {
os_log(.debug, log: broadcastLogger, "failure: \(status)")
return false
}
return true
}
func setupStreams() {
var readStream: Unmanaged<CFReadStream>?
var writeStream: Unmanaged<CFWriteStream>?
CFStreamCreatePairWithSocket(kCFAllocatorDefault, socketHandle, &readStream, &writeStream)
inputStream = readStream?.takeRetainedValue()
inputStream?.delegate = self
inputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String))
outputStream = writeStream?.takeRetainedValue()
outputStream?.delegate = self
outputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String))
scheduleStreams()
}
func scheduleStreams() {
shouldKeepRunning = true
networkQueue = DispatchQueue.global(qos: .userInitiated)
networkQueue?.async { [weak self] in
self?.inputStream?.schedule(in: .current, forMode: .common)
self?.outputStream?.schedule(in: .current, forMode: .common)
RunLoop.current.run()
var isRunning = false
repeat {
isRunning = self?.shouldKeepRunning ?? false && RunLoop.current.run(mode: .default, before: .distantFuture)
} while (isRunning)
}
}
func unscheduleStreams() {
networkQueue?.sync { [weak self] in
self?.inputStream?.remove(from: .current, forMode: .common)
self?.outputStream?.remove(from: .current, forMode: .common)
}
shouldKeepRunning = false
}
func notifyDidClose(error: Error?) {
if didClose != nil {
didClose?(error)
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.solsynth.solian</string>
</array>
</dict>
</plist>

View File

@@ -49,8 +49,7 @@ class AppDatabase extends _$AppDatabase {
}
Future<int> updateMessage(ChatMessagesCompanion message) {
return (update(chatMessages)
..where((m) => m.id.equals(message.id.value))).write(message);
return into(chatMessages).insert(message, mode: InsertMode.insertOrReplace);
}
Future<int> updateMessageStatus(String id, MessageStatus status) {

View File

@@ -20,7 +20,6 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/route.dart';
import 'package:island/services/notify.dart';
import 'package:island/services/timezone.dart';
import 'package:island/widgets/alert.dart';
@@ -30,6 +29,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect;
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
@@ -51,6 +51,7 @@ void main() async {
}
try {
await langdetect.initLangDetect();
await EasyLocalization.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,

View File

@@ -162,8 +162,6 @@ sealed class CallParticipant with _$CallParticipant {
required String identity,
required String name,
required DateTime joinedAt,
required String? accountId,
required SnChatMember? profile,
}) = _CallParticipant;
factory CallParticipant.fromJson(Map<String, dynamic> json) =>

View File

@@ -2498,7 +2498,7 @@ as List<CallParticipant>,
/// @nodoc
mixin _$CallParticipant {
String get identity; String get name; DateTime get joinedAt; String? get accountId; SnChatMember? get profile;
String get identity; String get name; DateTime get joinedAt;
/// Create a copy of CallParticipant
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -2511,16 +2511,16 @@ $CallParticipantCopyWith<CallParticipant> get copyWith => _$CallParticipantCopyW
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.profile, profile) || other.profile == profile));
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,identity,name,joinedAt,accountId,profile);
int get hashCode => Object.hash(runtimeType,identity,name,joinedAt);
@override
String toString() {
return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt, accountId: $accountId, profile: $profile)';
return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt)';
}
@@ -2531,11 +2531,11 @@ abstract mixin class $CallParticipantCopyWith<$Res> {
factory $CallParticipantCopyWith(CallParticipant value, $Res Function(CallParticipant) _then) = _$CallParticipantCopyWithImpl;
@useResult
$Res call({
String identity, String name, DateTime joinedAt, String? accountId, SnChatMember? profile
String identity, String name, DateTime joinedAt
});
$SnChatMemberCopyWith<$Res>? get profile;
}
/// @nodoc
@@ -2548,29 +2548,15 @@ class _$CallParticipantCopyWithImpl<$Res>
/// Create a copy of CallParticipant
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,Object? accountId = freezed,Object? profile = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,}) {
return _then(_self.copyWith(
identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
as DateTime,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,profile: freezed == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable
as SnChatMember?,
as DateTime,
));
}
/// Create a copy of CallParticipant
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnChatMemberCopyWith<$Res>? get profile {
if (_self.profile == null) {
return null;
}
return $SnChatMemberCopyWith<$Res>(_self.profile!, (value) {
return _then(_self.copyWith(profile: value));
});
}
}
@@ -2649,10 +2635,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String identity, String name, DateTime joinedAt, String? accountId, SnChatMember? profile)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String identity, String name, DateTime joinedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _CallParticipant() when $default != null:
return $default(_that.identity,_that.name,_that.joinedAt,_that.accountId,_that.profile);case _:
return $default(_that.identity,_that.name,_that.joinedAt);case _:
return orElse();
}
@@ -2670,10 +2656,10 @@ return $default(_that.identity,_that.name,_that.joinedAt,_that.accountId,_that.p
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String identity, String name, DateTime joinedAt, String? accountId, SnChatMember? profile) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String identity, String name, DateTime joinedAt) $default,) {final _that = this;
switch (_that) {
case _CallParticipant():
return $default(_that.identity,_that.name,_that.joinedAt,_that.accountId,_that.profile);}
return $default(_that.identity,_that.name,_that.joinedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -2687,10 +2673,10 @@ return $default(_that.identity,_that.name,_that.joinedAt,_that.accountId,_that.p
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String identity, String name, DateTime joinedAt, String? accountId, SnChatMember? profile)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String identity, String name, DateTime joinedAt)? $default,) {final _that = this;
switch (_that) {
case _CallParticipant() when $default != null:
return $default(_that.identity,_that.name,_that.joinedAt,_that.accountId,_that.profile);case _:
return $default(_that.identity,_that.name,_that.joinedAt);case _:
return null;
}
@@ -2702,14 +2688,12 @@ return $default(_that.identity,_that.name,_that.joinedAt,_that.accountId,_that.p
@JsonSerializable()
class _CallParticipant implements CallParticipant {
const _CallParticipant({required this.identity, required this.name, required this.joinedAt, required this.accountId, required this.profile});
const _CallParticipant({required this.identity, required this.name, required this.joinedAt});
factory _CallParticipant.fromJson(Map<String, dynamic> json) => _$CallParticipantFromJson(json);
@override final String identity;
@override final String name;
@override final DateTime joinedAt;
@override final String? accountId;
@override final SnChatMember? profile;
/// Create a copy of CallParticipant
/// with the given fields replaced by the non-null parameter values.
@@ -2724,16 +2708,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.profile, profile) || other.profile == profile));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,identity,name,joinedAt,accountId,profile);
int get hashCode => Object.hash(runtimeType,identity,name,joinedAt);
@override
String toString() {
return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt, accountId: $accountId, profile: $profile)';
return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt)';
}
@@ -2744,11 +2728,11 @@ abstract mixin class _$CallParticipantCopyWith<$Res> implements $CallParticipant
factory _$CallParticipantCopyWith(_CallParticipant value, $Res Function(_CallParticipant) _then) = __$CallParticipantCopyWithImpl;
@override @useResult
$Res call({
String identity, String name, DateTime joinedAt, String? accountId, SnChatMember? profile
String identity, String name, DateTime joinedAt
});
@override $SnChatMemberCopyWith<$Res>? get profile;
}
/// @nodoc
@@ -2761,30 +2745,16 @@ class __$CallParticipantCopyWithImpl<$Res>
/// Create a copy of CallParticipant
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,Object? accountId = freezed,Object? profile = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,}) {
return _then(_CallParticipant(
identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
as DateTime,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,profile: freezed == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable
as SnChatMember?,
as DateTime,
));
}
/// Create a copy of CallParticipant
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnChatMemberCopyWith<$Res>? get profile {
if (_self.profile == null) {
return null;
}
return $SnChatMemberCopyWith<$Res>(_self.profile!, (value) {
return _then(_self.copyWith(profile: value));
});
}
}

View File

@@ -285,11 +285,6 @@ _CallParticipant _$CallParticipantFromJson(Map<String, dynamic> json) =>
identity: json['identity'] as String,
name: json['name'] as String,
joinedAt: DateTime.parse(json['joined_at'] as String),
accountId: json['account_id'] as String?,
profile:
json['profile'] == null
? null
: SnChatMember.fromJson(json['profile'] as Map<String, dynamic>),
);
Map<String, dynamic> _$CallParticipantToJson(_CallParticipant instance) =>
@@ -297,8 +292,6 @@ Map<String, dynamic> _$CallParticipantToJson(_CallParticipant instance) =>
'identity': instance.identity,
'name': instance.name,
'joined_at': instance.joinedAt.toIso8601String(),
'account_id': instance.accountId,
'profile': instance.profile?.toJson(),
};
_SnRealtimeCall _$SnRealtimeCallFromJson(Map<String, dynamic> json) =>

View File

@@ -11,8 +11,8 @@ sealed class SnEmbedLink with _$SnEmbedLink {
@JsonKey(name: 'Title') required String title,
@JsonKey(name: 'Description') required String? description,
@JsonKey(name: 'ImageUrl') required String? imageUrl,
@JsonKey(name: 'FaviconUrl') required String faviconUrl,
@JsonKey(name: 'SiteName') required String siteName,
@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,

View File

@@ -212,7 +212,7 @@ return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUr
@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') required this.faviconUrl, @JsonKey(name: 'SiteName') required this.siteName, @JsonKey(name: 'ContentType') required this.contentType, @JsonKey(name: 'Author') required this.author, @JsonKey(name: 'PublishedDate') required this.publishedDate});
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;

View File

@@ -12,8 +12,8 @@ _SnEmbedLink _$SnEmbedLinkFromJson(Map<String, dynamic> json) => _SnEmbedLink(
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,
faviconUrl: json['FaviconUrl'] as String? ?? "",
siteName: json['SiteName'] as String? ?? "",
contentType: json['ContentType'] as String?,
author: json['Author'] as String?,
publishedDate:

View File

@@ -34,6 +34,7 @@ sealed class SnPost with _$SnPost {
@Default([]) List<SnCloudFile> attachments,
required SnPublisher publisher,
@Default({}) Map<String, int> reactionsCount,
@Default({}) Map<String, bool> reactionsMade,
@Default([]) List<dynamic> reactions,
@Default([]) List<PostTag> tags,
@Default([]) List<PostCategory> categories,
@@ -77,6 +78,13 @@ sealed class SnSubscriptionStatus with _$SnSubscriptionStatus {
sealed class ReactInfo with _$ReactInfo {
const factory ReactInfo({required String icon, required int attitude}) =
_ReactInfo;
static String getTranslationKey(String templateKey) {
final parts = templateKey.split('_');
final camelCase =
parts.map((p) => p[0].toUpperCase() + p.substring(1)).join();
return 'reaction$camelCase';
}
}
const Map<String, ReactInfo> kReactionTemplates = {

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; 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<PostTag> get tags; List<PostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated;
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $SnPostCopyWith<SnPost> get copyWith => _$SnPostCopyWithImpl<SnPost>(this as SnP
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactionsMade, reactionsMade)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]);
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactionsMade),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]);
@override
String toString() {
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
}
@@ -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, 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<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
});
@@ -65,7 +65,7 @@ class _$SnPostCopyWithImpl<$Res>
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
@@ -91,7 +91,8 @@ as String?,forwardedPost: freezed == forwardedPost ? _self.forwardedPost : forwa
as SnPost?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable
as List<SnCloudFile>,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as SnPublisher,reactionsCount: null == reactionsCount ? _self.reactionsCount : reactionsCount // ignore: cast_nullable_to_non_nullable
as Map<String, int>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable
as Map<String, int>,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
@@ -226,10 +227,10 @@ 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, 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<PostTag> tags, List<PostCategory> 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.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
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 _:
return orElse();
}
@@ -247,10 +248,10 @@ 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, 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<PostTag> tags, List<PostCategory> 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.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);}
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);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -264,10 +265,10 @@ 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, 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<PostTag> tags, List<PostCategory> 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.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
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 _:
return null;
}
@@ -279,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 List<dynamic> reactions = const [], final List<PostTag> tags = const [], final List<PostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.type = 0, final Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, final List<SnCloudFile> attachments = const [], required this.publisher, final Map<String, int> reactionsCount = const {}, final 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;
factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json);
@override final String id;
@@ -326,6 +327,13 @@ class _SnPost implements SnPost {
return EqualUnmodifiableMapView(_reactionsCount);
}
final Map<String, bool> _reactionsMade;
@override@JsonKey() Map<String, bool> get reactionsMade {
if (_reactionsMade is EqualUnmodifiableMapView) return _reactionsMade;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_reactionsMade);
}
final List<dynamic> _reactions;
@override@JsonKey() List<dynamic> get reactions {
if (_reactions is EqualUnmodifiableListView) return _reactions;
@@ -372,16 +380,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactionsMade, _reactionsMade)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]);
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactionsMade),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]);
@override
String toString() {
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
}
@@ -392,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, 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<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
});
@@ -409,7 +417,7 @@ class __$SnPostCopyWithImpl<$Res>
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
return _then(_SnPost(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
@@ -435,7 +443,8 @@ as String?,forwardedPost: freezed == forwardedPost ? _self.forwardedPost : forwa
as SnPost?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable
as List<SnCloudFile>,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as SnPublisher,reactionsCount: null == reactionsCount ? _self._reactionsCount : reactionsCount // ignore: cast_nullable_to_non_nullable
as Map<String, int>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable
as Map<String, int>,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

View File

@@ -54,6 +54,11 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
(k, e) => MapEntry(k, (e as num).toInt()),
) ??
const {},
reactionsMade:
(json['reactions_made'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as bool),
) ??
const {},
reactions: json['reactions'] as List<dynamic>? ?? const [],
tags:
(json['tags'] as List<dynamic>?)
@@ -106,6 +111,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'attachments': instance.attachments.map((e) => e.toJson()).toList(),
'publisher': instance.publisher.toJson(),
'reactions_count': instance.reactionsCount,
'reactions_made': instance.reactionsMade,
'reactions': instance.reactions,
'tags': instance.tags.map((e) => e.toJson()).toList(),
'categories': instance.categories.map((e) => e.toJson()).toList(),

View File

@@ -1,13 +1,14 @@
import 'package:island/pods/userinfo.dart';
import 'package:island/screens/chat/chat.dart';
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:island/widgets/chat/call_button.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:island/pods/network.dart';
import 'package:island/models/chat.dart';
import 'package:island/pods/websocket.dart';
part 'call.g.dart';
part 'call.freezed.dart';
@@ -27,6 +28,7 @@ sealed class CallState with _$CallState {
required bool isMicrophoneEnabled,
required bool isCameraEnabled,
required bool isScreenSharing,
required bool isSpeakerphone,
@Default(Duration(seconds: 0)) Duration duration,
String? error,
}) = _CallState;
@@ -42,7 +44,8 @@ sealed class CallParticipantLive with _$CallParticipantLive {
}) = _CallParticipantLive;
bool get isSpeaking => remoteParticipant.isSpeaking;
bool get isMuted => remoteParticipant.isMuted;
bool get isMuted =>
remoteParticipant.isMuted || !remoteParticipant.isMicrophoneEnabled();
bool get isScreenSharing => remoteParticipant.isScreenShareEnabled();
bool get isScreenSharingWithAudio =>
remoteParticipant.isScreenShareAudioEnabled();
@@ -57,13 +60,14 @@ class CallNotifier extends _$CallNotifier {
LocalParticipant? _localParticipant;
List<CallParticipantLive> _participants = [];
final Map<String, CallParticipant> _participantInfoByIdentity = {};
StreamSubscription? _wsSubscription;
EventsListener? _roomListener;
List<CallParticipantLive> get participants =>
List.unmodifiable(_participants);
LocalParticipant? get localParticipant => _localParticipant;
Map<String, double> participantsVolumes = {};
Timer? _durationTimer;
Room? get room => _room;
@@ -71,36 +75,15 @@ class CallNotifier extends _$CallNotifier {
@override
CallState build() {
// Subscribe to websocket updates
_subscribeToParticipantsUpdate();
return const CallState(
isConnected: false,
isMicrophoneEnabled: true,
isCameraEnabled: false,
isScreenSharing: false,
isSpeakerphone: true,
);
}
void _subscribeToParticipantsUpdate() {
// Only subscribe once
if (_wsSubscription != null) return;
final ws = ref.read(websocketProvider);
_wsSubscription = ws.dataStream.listen((packet) {
if (packet.type == 'call.participants.update' && packet.data != null) {
final participantsData = packet.data!["participants"];
if (participantsData is List) {
final parsed =
participantsData
.map(
(e) =>
CallParticipant.fromJson(Map<String, dynamic>.from(e)),
)
.toList();
_updateLiveParticipants(parsed);
}
}
});
}
void _initRoomListeners() {
if (_room == null) return;
_roomListener?.dispose();
@@ -143,8 +126,6 @@ class CallNotifier extends _$CallNotifier {
identity: remote.identity,
name: remote.identity,
joinedAt: DateTime.now(),
accountId: null,
profile: null,
);
return CallParticipantLive(
participant: match,
@@ -169,16 +150,12 @@ class CallNotifier extends _$CallNotifier {
if (idx != -1) return participants[idx];
}
final userInfo = ref.read(userInfoProvider);
final roomIdentity = ref.read(chatroomIdentityProvider(_roomId));
// Otherwise, use info from the identity map or fallback to minimal
return _participantInfoByIdentity[_localParticipant!.identity] ??
CallParticipant(
identity: _localParticipant!.identity,
name: _localParticipant!.identity,
joinedAt: DateTime.now(),
accountId: userInfo.value?.id,
profile: roomIdentity.value,
);
}
@@ -205,6 +182,7 @@ class CallNotifier extends _$CallNotifier {
remoteParticipant: _localParticipant!,
),
);
state = state.copyWith();
}
// Add remote participants
_participants.addAll(
@@ -233,7 +211,13 @@ class CallNotifier extends _$CallNotifier {
Future<void> joinRoom(String roomId) async {
if (_roomId == roomId && _room != null) {
log('[Call] Call skipped. Already has data');
return;
} else if (_room != null) {
if (!_room!.isDisposed &&
_room!.connectionState != ConnectionState.disconnected) {
throw Exception('Call already connected');
}
}
_roomId = roomId;
if (_room != null) {
@@ -264,7 +248,8 @@ class CallNotifier extends _$CallNotifier {
duration: Duration(
milliseconds:
(DateTime.now().millisecondsSinceEpoch -
(ongoingCall?.createdAt.millisecondsSinceEpoch ?? 0)),
(ongoingCall?.createdAt.millisecondsSinceEpoch ??
DateTime.now().millisecondsSinceEpoch)),
),
);
});
@@ -286,6 +271,10 @@ class CallNotifier extends _$CallNotifier {
_initRoomListeners();
_updateLiveParticipants(participants);
if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
Hardware.instance.setSpeakerphoneOn(true);
}
// Listen for connection updates
_room!.addListener(() {
state = state.copyWith(
@@ -318,6 +307,7 @@ class CallNotifier extends _$CallNotifier {
stopOnMute: autostop,
);
}
state = state.copyWith();
}
}
@@ -326,6 +316,7 @@ class CallNotifier extends _$CallNotifier {
final target = !_localParticipant!.isCameraEnabled();
state = state.copyWith(isCameraEnabled: target);
await _localParticipant!.setCameraEnabled(target);
state = state.copyWith();
}
}
@@ -334,9 +325,16 @@ class CallNotifier extends _$CallNotifier {
final target = !_localParticipant!.isScreenShareEnabled();
state = state.copyWith(isScreenSharing: target);
await _localParticipant!.setScreenShareEnabled(target);
state = state.copyWith();
}
}
Future<void> toggleSpeakerphone() async {
state = state.copyWith(isSpeakerphone: !state.isSpeakerphone);
await Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone);
state = state.copyWith();
}
Future<void> disconnect() async {
if (_room != null) {
await _room!.disconnect();
@@ -349,11 +347,39 @@ class CallNotifier extends _$CallNotifier {
}
}
void setParticipantVolume(CallParticipantLive live, double volume) {
if (participantsVolumes[live.remoteParticipant.sid] == null) {
participantsVolumes[live.remoteParticipant.sid] = 1;
}
Helper.setVolume(
volume,
live
.remoteParticipant
.audioTrackPublications
.first
.track!
.mediaStreamTrack,
);
participantsVolumes[live.remoteParticipant.sid] = volume;
}
double getParticipantVolume(CallParticipantLive live) {
return participantsVolumes[live.remoteParticipant.sid] ?? 1;
}
void dispose() {
_wsSubscription?.cancel();
state = state.copyWith(
error: null,
isConnected: false,
isMicrophoneEnabled: false,
isCameraEnabled: false,
isScreenSharing: false,
);
_roomListener?.dispose();
_room?.removeListener(_onRoomChange);
_room?.dispose();
_durationTimer?.cancel();
_roomId = null;
participantsVolumes = {};
}
}

View File

@@ -12,9 +12,9 @@ part of 'call.dart';
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$CallState {
mixin _$CallState implements DiagnosticableTreeMixin {
bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; Duration get duration; String? get error;
bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; bool get isSpeakerphone; Duration get duration; String? get error;
/// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -22,19 +22,25 @@ mixin _$CallState {
$CallStateCopyWith<CallState> get copyWith => _$CallStateCopyWithImpl<CallState>(this as CallState, _$identity);
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'CallState'))
..add(DiagnosticsProperty('isConnected', isConnected))..add(DiagnosticsProperty('isMicrophoneEnabled', isMicrophoneEnabled))..add(DiagnosticsProperty('isCameraEnabled', isCameraEnabled))..add(DiagnosticsProperty('isScreenSharing', isScreenSharing))..add(DiagnosticsProperty('isSpeakerphone', isSpeakerphone))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('error', error));
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error));
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.isSpeakerphone, isSpeakerphone) || other.isSpeakerphone == isSpeakerphone)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error));
}
@override
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,duration,error);
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,error);
@override
String toString() {
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)';
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, error: $error)';
}
@@ -45,7 +51,7 @@ abstract mixin class $CallStateCopyWith<$Res> {
factory $CallStateCopyWith(CallState value, $Res Function(CallState) _then) = _$CallStateCopyWithImpl;
@useResult
$Res call({
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error
});
@@ -62,12 +68,13 @@ class _$CallStateCopyWithImpl<$Res>
/// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? duration = null,Object? error = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? isSpeakerphone = null,Object? duration = null,Object? error = freezed,}) {
return _then(_self.copyWith(
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable
as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable
as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable
as bool,isSpeakerphone: null == isSpeakerphone ? _self.isSpeakerphone : isSpeakerphone // ignore: cast_nullable_to_non_nullable
as bool,duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
as Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as String?,
@@ -152,10 +159,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _CallState() when $default != null:
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.duration,_that.error);case _:
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.error);case _:
return orElse();
}
@@ -173,10 +180,10 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error) $default,) {final _that = this;
switch (_that) {
case _CallState():
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.duration,_that.error);}
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.error);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -190,10 +197,10 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error)? $default,) {final _that = this;
switch (_that) {
case _CallState() when $default != null:
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.duration,_that.error);case _:
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.error);case _:
return null;
}
@@ -204,14 +211,15 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable
/// @nodoc
class _CallState implements CallState {
const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, this.duration = const Duration(seconds: 0), this.error});
class _CallState with DiagnosticableTreeMixin implements CallState {
const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, required this.isSpeakerphone, this.duration = const Duration(seconds: 0), this.error});
@override final bool isConnected;
@override final bool isMicrophoneEnabled;
@override final bool isCameraEnabled;
@override final bool isScreenSharing;
@override final bool isSpeakerphone;
@override@JsonKey() final Duration duration;
@override final String? error;
@@ -222,19 +230,25 @@ class _CallState implements CallState {
_$CallStateCopyWith<_CallState> get copyWith => __$CallStateCopyWithImpl<_CallState>(this, _$identity);
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'CallState'))
..add(DiagnosticsProperty('isConnected', isConnected))..add(DiagnosticsProperty('isMicrophoneEnabled', isMicrophoneEnabled))..add(DiagnosticsProperty('isCameraEnabled', isCameraEnabled))..add(DiagnosticsProperty('isScreenSharing', isScreenSharing))..add(DiagnosticsProperty('isSpeakerphone', isSpeakerphone))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('error', error));
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.isSpeakerphone, isSpeakerphone) || other.isSpeakerphone == isSpeakerphone)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error));
}
@override
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,duration,error);
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,error);
@override
String toString() {
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)';
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, error: $error)';
}
@@ -245,7 +259,7 @@ abstract mixin class _$CallStateCopyWith<$Res> implements $CallStateCopyWith<$Re
factory _$CallStateCopyWith(_CallState value, $Res Function(_CallState) _then) = __$CallStateCopyWithImpl;
@override @useResult
$Res call({
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error
});
@@ -262,12 +276,13 @@ class __$CallStateCopyWithImpl<$Res>
/// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? duration = null,Object? error = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? isSpeakerphone = null,Object? duration = null,Object? error = freezed,}) {
return _then(_CallState(
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable
as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable
as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable
as bool,isSpeakerphone: null == isSpeakerphone ? _self.isSpeakerphone : isSpeakerphone // ignore: cast_nullable_to_non_nullable
as bool,duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
as Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as String?,
@@ -278,7 +293,7 @@ as String?,
}
/// @nodoc
mixin _$CallParticipantLive {
mixin _$CallParticipantLive implements DiagnosticableTreeMixin {
CallParticipant get participant; Participant get remoteParticipant;
/// Create a copy of CallParticipantLive
@@ -288,6 +303,12 @@ mixin _$CallParticipantLive {
$CallParticipantLiveCopyWith<CallParticipantLive> get copyWith => _$CallParticipantLiveCopyWithImpl<CallParticipantLive>(this as CallParticipantLive, _$identity);
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'CallParticipantLive'))
..add(DiagnosticsProperty('participant', participant))..add(DiagnosticsProperty('remoteParticipant', remoteParticipant));
}
@override
bool operator ==(Object other) {
@@ -299,7 +320,7 @@ bool operator ==(Object other) {
int get hashCode => Object.hash(runtimeType,participant,remoteParticipant);
@override
String toString() {
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)';
}
@@ -475,7 +496,7 @@ return $default(_that.participant,_that.remoteParticipant);case _:
/// @nodoc
class _CallParticipantLive extends CallParticipantLive {
class _CallParticipantLive extends CallParticipantLive with DiagnosticableTreeMixin {
const _CallParticipantLive({required this.participant, required this.remoteParticipant}): super._();
@@ -489,6 +510,12 @@ class _CallParticipantLive extends CallParticipantLive {
_$CallParticipantLiveCopyWith<_CallParticipantLive> get copyWith => __$CallParticipantLiveCopyWithImpl<_CallParticipantLive>(this, _$identity);
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'CallParticipantLive'))
..add(DiagnosticsProperty('participant', participant))..add(DiagnosticsProperty('remoteParticipant', remoteParticipant));
}
@override
bool operator ==(Object other) {
@@ -500,7 +527,7 @@ bool operator ==(Object other) {
int get hashCode => Object.hash(runtimeType,participant,remoteParticipant);
@override
String toString() {
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)';
}

View File

@@ -6,7 +6,7 @@ part of 'call.dart';
// RiverpodGenerator
// **************************************************************************
String _$callNotifierHash() => r'107174cd6cfab6bfafe44f8c4a72a67bcb93217b';
String _$callNotifierHash() => r'333a1cd566a339644c83932e15dae03f1c5cc24b';
/// See also [CallNotifier].
@ProviderFor(CallNotifier)

38
lib/pods/translate.dart Normal file
View File

@@ -0,0 +1,38 @@
import 'dart:convert';
import 'dart:developer';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/network.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect;
part 'translate.freezed.dart';
part 'translate.g.dart';
@freezed
sealed class TranslateQuery with _$TranslateQuery {
const factory TranslateQuery({required String text, required String lang}) =
_TranslateQuery;
}
@riverpod
Future<String> translateString(Ref ref, TranslateQuery query) async {
final client = ref.watch(apiClientProvider);
final response = await client.post(
'/sphere/translate',
queryParameters: {'to': query.lang},
data: jsonEncode(query.text),
);
return response.data as String;
}
@riverpod
String? detectStringLanguage(Ref ref, String text) {
try {
return langdetect.detectLangs(text).firstOrNull?.lang;
} catch (err) {
log('[Language] Unable to detect text\'s language: $text');
return null;
}
}

View File

@@ -0,0 +1,268 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'translate.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$TranslateQuery {
String get text; String get lang;
/// Create a copy of TranslateQuery
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$TranslateQueryCopyWith<TranslateQuery> get copyWith => _$TranslateQueryCopyWithImpl<TranslateQuery>(this as TranslateQuery, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is TranslateQuery&&(identical(other.text, text) || other.text == text)&&(identical(other.lang, lang) || other.lang == lang));
}
@override
int get hashCode => Object.hash(runtimeType,text,lang);
@override
String toString() {
return 'TranslateQuery(text: $text, lang: $lang)';
}
}
/// @nodoc
abstract mixin class $TranslateQueryCopyWith<$Res> {
factory $TranslateQueryCopyWith(TranslateQuery value, $Res Function(TranslateQuery) _then) = _$TranslateQueryCopyWithImpl;
@useResult
$Res call({
String text, String lang
});
}
/// @nodoc
class _$TranslateQueryCopyWithImpl<$Res>
implements $TranslateQueryCopyWith<$Res> {
_$TranslateQueryCopyWithImpl(this._self, this._then);
final TranslateQuery _self;
final $Res Function(TranslateQuery) _then;
/// Create a copy of TranslateQuery
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? text = null,Object? lang = null,}) {
return _then(_self.copyWith(
text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable
as String,lang: null == lang ? _self.lang : lang // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [TranslateQuery].
extension TranslateQueryPatterns on TranslateQuery {
/// 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( _TranslateQuery value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _TranslateQuery() 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( _TranslateQuery value) $default,){
final _that = this;
switch (_that) {
case _TranslateQuery():
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( _TranslateQuery value)? $default,){
final _that = this;
switch (_that) {
case _TranslateQuery() 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 text, String lang)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _TranslateQuery() when $default != null:
return $default(_that.text,_that.lang);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 text, String lang) $default,) {final _that = this;
switch (_that) {
case _TranslateQuery():
return $default(_that.text,_that.lang);}
}
/// 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 text, String lang)? $default,) {final _that = this;
switch (_that) {
case _TranslateQuery() when $default != null:
return $default(_that.text,_that.lang);case _:
return null;
}
}
}
/// @nodoc
class _TranslateQuery implements TranslateQuery {
const _TranslateQuery({required this.text, required this.lang});
@override final String text;
@override final String lang;
/// Create a copy of TranslateQuery
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$TranslateQueryCopyWith<_TranslateQuery> get copyWith => __$TranslateQueryCopyWithImpl<_TranslateQuery>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TranslateQuery&&(identical(other.text, text) || other.text == text)&&(identical(other.lang, lang) || other.lang == lang));
}
@override
int get hashCode => Object.hash(runtimeType,text,lang);
@override
String toString() {
return 'TranslateQuery(text: $text, lang: $lang)';
}
}
/// @nodoc
abstract mixin class _$TranslateQueryCopyWith<$Res> implements $TranslateQueryCopyWith<$Res> {
factory _$TranslateQueryCopyWith(_TranslateQuery value, $Res Function(_TranslateQuery) _then) = __$TranslateQueryCopyWithImpl;
@override @useResult
$Res call({
String text, String lang
});
}
/// @nodoc
class __$TranslateQueryCopyWithImpl<$Res>
implements _$TranslateQueryCopyWith<$Res> {
__$TranslateQueryCopyWithImpl(this._self, this._then);
final _TranslateQuery _self;
final $Res Function(_TranslateQuery) _then;
/// Create a copy of TranslateQuery
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? text = null,Object? lang = null,}) {
return _then(_TranslateQuery(
text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable
as String,lang: null == lang ? _self.lang : lang // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on

274
lib/pods/translate.g.dart Normal file
View File

@@ -0,0 +1,274 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'translate.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$translateStringHash() => r'51d638cf07cbf3ffa9469298f5bd9c667bc0ccb7';
/// 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 [translateString].
@ProviderFor(translateString)
const translateStringProvider = TranslateStringFamily();
/// See also [translateString].
class TranslateStringFamily extends Family<AsyncValue<String>> {
/// See also [translateString].
const TranslateStringFamily();
/// See also [translateString].
TranslateStringProvider call(TranslateQuery query) {
return TranslateStringProvider(query);
}
@override
TranslateStringProvider getProviderOverride(
covariant TranslateStringProvider provider,
) {
return call(provider.query);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'translateStringProvider';
}
/// See also [translateString].
class TranslateStringProvider extends AutoDisposeFutureProvider<String> {
/// See also [translateString].
TranslateStringProvider(TranslateQuery query)
: this._internal(
(ref) => translateString(ref as TranslateStringRef, query),
from: translateStringProvider,
name: r'translateStringProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$translateStringHash,
dependencies: TranslateStringFamily._dependencies,
allTransitiveDependencies:
TranslateStringFamily._allTransitiveDependencies,
query: query,
);
TranslateStringProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.query,
}) : super.internal();
final TranslateQuery query;
@override
Override overrideWith(
FutureOr<String> Function(TranslateStringRef provider) create,
) {
return ProviderOverride(
origin: this,
override: TranslateStringProvider._internal(
(ref) => create(ref as TranslateStringRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
query: query,
),
);
}
@override
AutoDisposeFutureProviderElement<String> createElement() {
return _TranslateStringProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is TranslateStringProvider && other.query == query;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, query.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin TranslateStringRef on AutoDisposeFutureProviderRef<String> {
/// The parameter `query` of this provider.
TranslateQuery get query;
}
class _TranslateStringProviderElement
extends AutoDisposeFutureProviderElement<String>
with TranslateStringRef {
_TranslateStringProviderElement(super.provider);
@override
TranslateQuery get query => (origin as TranslateStringProvider).query;
}
String _$detectStringLanguageHash() =>
r'697b68464b3d00927cc43ccc1ba8ba93f2a470ed';
/// See also [detectStringLanguage].
@ProviderFor(detectStringLanguage)
const detectStringLanguageProvider = DetectStringLanguageFamily();
/// See also [detectStringLanguage].
class DetectStringLanguageFamily extends Family<String?> {
/// See also [detectStringLanguage].
const DetectStringLanguageFamily();
/// See also [detectStringLanguage].
DetectStringLanguageProvider call(String text) {
return DetectStringLanguageProvider(text);
}
@override
DetectStringLanguageProvider getProviderOverride(
covariant DetectStringLanguageProvider provider,
) {
return call(provider.text);
}
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'detectStringLanguageProvider';
}
/// See also [detectStringLanguage].
class DetectStringLanguageProvider extends AutoDisposeProvider<String?> {
/// See also [detectStringLanguage].
DetectStringLanguageProvider(String text)
: this._internal(
(ref) => detectStringLanguage(ref as DetectStringLanguageRef, text),
from: detectStringLanguageProvider,
name: r'detectStringLanguageProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$detectStringLanguageHash,
dependencies: DetectStringLanguageFamily._dependencies,
allTransitiveDependencies:
DetectStringLanguageFamily._allTransitiveDependencies,
text: text,
);
DetectStringLanguageProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.text,
}) : super.internal();
final String text;
@override
Override overrideWith(
String? Function(DetectStringLanguageRef provider) create,
) {
return ProviderOverride(
origin: this,
override: DetectStringLanguageProvider._internal(
(ref) => create(ref as DetectStringLanguageRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
text: text,
),
);
}
@override
AutoDisposeProviderElement<String?> createElement() {
return _DetectStringLanguageProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is DetectStringLanguageProvider && other.text == text;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, text.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin DetectStringLanguageRef on AutoDisposeProviderRef<String?> {
/// The parameter `text` of this provider.
String get text;
}
class _DetectStringLanguageProviderElement
extends AutoDisposeProviderElement<String?>
with DetectStringLanguageRef {
_DetectStringLanguageProviderElement(super.provider);
@override
String get text => (origin as DetectStringLanguageProvider).text;
}
// 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

@@ -46,6 +46,10 @@ class WebSocketService {
final StreamController<WebSocketState> _statusStreamController =
StreamController<WebSocketState>.broadcast();
Timer? _reconnectTimer;
Timer? _heartbeatTimer;
DateTime? _heartbeatAt;
Duration? _heartbeatDelay;
Stream<WebSocketPacket> get dataStream => _streamController.stream;
Stream<WebSocketState> get statusStream => _statusStreamController.stream;
@@ -71,6 +75,7 @@ class WebSocketService {
}
await _channel!.ready;
_statusStreamController.sink.add(WebSocketState.connected());
_scheduleHeartbeat();
_channel!.stream.listen(
(data) {
final dataStr =
@@ -80,6 +85,13 @@ class WebSocketService {
log(
"[WebSocket] Received packet: ${packet.type} ${packet.errorMessage}",
);
if (packet.type == 'pong' && _heartbeatAt != null) {
var now = DateTime.now();
_heartbeatDelay = now.difference(_heartbeatAt!);
log(
"[WebSocket] Server respond last heartbeat for ${_heartbeatDelay!.inMilliseconds} ms",
);
}
},
onDone: () {
log('[WebSocket] Connection closed, attempting to reconnect...');
@@ -108,6 +120,19 @@ class WebSocketService {
});
}
void _scheduleHeartbeat() {
_heartbeatTimer?.cancel();
_heartbeatTimer = Timer.periodic(const Duration(seconds: 60), (_) {
_beatTheHeart();
});
}
void _beatTheHeart() {
_heartbeatAt = DateTime.now();
log('[WebSocket] We\'re beating the heart! $_heartbeatAt');
sendMessage(jsonEncode(WebSocketPacket(type: 'ping', data: null)));
}
WebSocketChannel? get ws => _channel;
void sendMessage(String message) {

View File

@@ -287,12 +287,6 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const AboutScreen(),
),
GoRoute(
name: 'reportList',
path: '/safety/reports/me',
builder: (context, state) => const AbuseReportListScreen(),
),
GoRoute(
name: 'reportDetail',
path: '/safety/reports/me/:id',
@@ -439,14 +433,6 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/account/relationships',
builder: (context, state) => const RelationshipScreen(),
),
GoRoute(
name: 'accountProfile',
path: '/account/:name',
builder: (context, state) {
final name = state.pathParameters['name']!;
return AccountProfileScreen(name: name);
},
),
GoRoute(
name: 'profileUpdate',
path: '/account/me/update',
@@ -462,8 +448,22 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/account/me/settings',
builder: (context, state) => const AccountSettingsScreen(),
),
GoRoute(
name: 'reportList',
path: '/safety/reports/me',
builder: (context, state) => const AbuseReportListScreen(),
),
],
),
GoRoute(
name: 'accountProfile',
path: '/account/:name',
builder: (context, state) {
final name = state.pathParameters['name']!;
return AccountProfileScreen(name: name);
},
),
],
),
],

View File

@@ -93,6 +93,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
final theme = Theme.of(context);
return AppScaffold(
isNoBackground: false,
appBar: AppBar(title: Text('about'.tr()), elevation: 0),
body:
_isLoading

View File

@@ -64,7 +64,7 @@ class AccountScreen extends HookConsumerWidget {
}
return AppScaffold(
noBackground: isWide,
isNoBackground: isWide,
appBar: AppBar(backgroundColor: Colors.transparent, toolbarHeight: 0),
body: SingleChildScrollView(
padding: getTabbedPadding(context),
@@ -231,7 +231,7 @@ class AccountScreen extends HookConsumerWidget {
ListTile(
minTileHeight: 48,
title: Text('abuseReports').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.gavel),
trailing: const Icon(Symbols.chevron_right),
onTap: () => context.pushNamed('reportList'),

View File

@@ -46,7 +46,7 @@ class EventCalanderScreen extends HookConsumerWidget {
}
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: Text('eventCalander').tr(),

View File

@@ -7,7 +7,7 @@ part of 'leveling.dart';
// **************************************************************************
String _$accountStellarSubscriptionHash() =>
r'37fb821460e3ac50b5cf777c933b6779f732daee';
r'80abcdefb3868775fd8fe3c980215713efff5948';
/// See also [accountStellarSubscription].
@ProviderFor(accountStellarSubscription)

View File

@@ -12,6 +12,7 @@ import 'package:island/pods/event_calendar.dart';
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/time.dart';
import 'package:island/services/timezone/native.dart';
import 'package:island/widgets/account/account_name.dart';
@@ -248,294 +249,367 @@ class AccountProfileScreen extends HookConsumerWidget {
final user = ref.watch(userInfoProvider);
Widget accountBasicInfo(SnAccount data) => Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(file: data.profile.picture, radius: 32),
const Gap(20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
AccountName(account: data, style: TextStyle(fontSize: 20)),
const Gap(6),
Text('@${data.name}').fontSize(14).opacity(0.85),
],
),
AccountStatusWidget(uname: name, padding: EdgeInsets.zero),
],
),
),
],
),
);
Widget accountProfileDetail(SnAccount data) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 24,
children: [
if (buildSubcolumn(data).isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 2,
children: buildSubcolumn(data),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('bio').tr().bold(),
Text(
data.profile.bio.isEmpty
? 'descriptionNone'.tr()
: data.profile.bio,
),
],
),
if (data.profile.timeZone.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('timeZone').tr().bold(),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
spacing: 6,
children: [
Text(data.profile.timeZone),
Text(
getTzInfo(
data.profile.timeZone,
).$2.formatCustomGlobal('HH:mm'),
),
Text(
getTzInfo(data.profile.timeZone).$1.formatOffsetLocal(),
).fontSize(11),
Text(
'UTC${getTzInfo(data.profile.timeZone).$1.formatOffset()}',
).fontSize(11).opacity(0.75),
],
),
],
),
],
).padding(horizontal: 24);
Widget accountAction(SnAccount data) => Card(
child: Column(
children: [
Row(
spacing: 8,
children: [
if (accountRelationship.value == null ||
accountRelationship.value!.status > -100)
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.onSecondary,
),
),
onPressed: relationshipAction,
label:
Text(
accountRelationship.value == null
? 'addFriendShort'
: 'added',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.person_add)
: const Icon(Symbols.person_check),
),
),
if (accountRelationship.value == null ||
accountRelationship.value!.status <= -100)
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.onSecondary,
),
),
onPressed: blockAction,
label:
Text(
accountRelationship.value == null
? 'blockUser'
: 'unblockUser',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.block)
: const Icon(Symbols.person_cancel),
),
),
],
).padding(horizontal: 16),
Row(
spacing: 8,
children: [
Expanded(
child: FilledButton.icon(
onPressed: directMessageAction,
icon: const Icon(Symbols.message),
label:
Text(
accountChat.value == null
? 'createDirectMessage'
: 'gotoDirectMessage',
maxLines: 1,
).tr(),
),
),
IconButton.filled(
onPressed: () {
showAbuseReportSheet(
context,
resourceIdentifier: 'account/${data.id}',
);
},
icon: Icon(
Symbols.flag,
color: Theme.of(context).colorScheme.onError,
),
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
Theme.of(context).colorScheme.error,
),
),
),
],
),
],
).padding(horizontal: 16, vertical: 8),
);
return account.when(
data:
(data) => AppScaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
foregroundColor: appbarColor.value,
expandedHeight: 180,
pinned: true,
leading: PageBackButton(
color: appbarColor.value,
shadows: [appbarShadow],
),
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
data.profile.background?.id != null
? CloudImageWidget(
file: data.profile.background,
)
: Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
isNoBackground: false,
appBar:
isWideScreen(context)
? AppBar(
foregroundColor: appbarColor.value,
leading: PageBackButton(
color: appbarColor.value,
shadows: [appbarShadow],
),
FlexibleSpaceBar(
title: Text(
data.nick,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(context).appBarTheme.foregroundColor,
shadows: [appbarShadow],
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
data.profile.background?.id != null
? CloudImageWidget(
file: data.profile.background,
)
: Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
),
FlexibleSpaceBar(
title: Text(
data.nick,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(
context,
).appBarTheme.foregroundColor,
shadows: [appbarShadow],
),
),
),
],
),
)
: null,
body:
isWideScreen(context)
? Row(
children: [
Flexible(
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(child: accountBasicInfo(data)),
if (data.badges.isNotEmpty)
SliverToBoxAdapter(
child: BadgeList(
badges: data.badges,
).padding(horizontal: 24, bottom: 24),
),
SliverToBoxAdapter(
child: Column(
spacing: 12,
children: [
LevelingProgressCard(
level: data.profile.level,
experience: data.profile.experience,
progress: data.profile.levelingProgress,
),
if (data.profile.verification != null)
VerificationStatusCard(
mark: data.profile.verification!,
),
],
).padding(horizontal: 20),
),
],
),
),
),
],
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(
file: data.profile.picture,
radius: 32,
),
const Gap(20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
AccountName(
account: data,
style: TextStyle(fontSize: 20),
),
const Gap(6),
Text(
'@${data.name}',
).fontSize(14).opacity(0.85),
],
Flexible(
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: accountProfileDetail(data),
),
AccountStatusWidget(
uname: name,
padding: EdgeInsets.zero,
if (user.value != null)
SliverToBoxAdapter(child: accountAction(data)),
SliverToBoxAdapter(
child: Card(
child: FortuneGraphWidget(
events: accountEvents,
eventCalanderUser: data.name,
),
).padding(all: 8),
),
],
),
),
],
),
),
),
if (data.badges.isNotEmpty)
SliverToBoxAdapter(
child: BadgeList(
badges: data.badges,
).padding(horizontal: 24, bottom: 24),
),
SliverToBoxAdapter(
child: Column(
spacing: 12,
children: [
LevelingProgressCard(
level: data.profile.level,
experience: data.profile.experience,
progress: data.profile.levelingProgress,
),
if (data.profile.verification != null)
VerificationStatusCard(
mark: data.profile.verification!,
),
],
).padding(horizontal: 20),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(vertical: 24),
),
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 24,
children: [
if (buildSubcolumn(data).isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 2,
children: buildSubcolumn(data),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('bio').tr().bold(),
Text(
data.profile.bio.isEmpty
? 'descriptionNone'.tr()
: data.profile.bio,
)
: CustomScrollView(
slivers: [
SliverAppBar(
foregroundColor: appbarColor.value,
expandedHeight: 180,
pinned: true,
leading: PageBackButton(
color: appbarColor.value,
shadows: [appbarShadow],
),
],
),
if (data.profile.timeZone.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('timeZone').tr().bold(),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
spacing: 6,
children: [
Text(data.profile.timeZone),
Text(
getTzInfo(
data.profile.timeZone,
).$2.formatCustomGlobal('HH:mm'),
),
Text(
getTzInfo(
data.profile.timeZone,
).$1.formatOffsetLocal(),
).fontSize(11),
Text(
'UTC${getTzInfo(data.profile.timeZone).$1.formatOffset()}',
).fontSize(11).opacity(0.75),
],
),
],
),
],
).padding(horizontal: 24),
),
if (user.value != null)
SliverToBoxAdapter(
child: const Divider(
height: 1,
).padding(top: 24, bottom: 12),
),
if (user.value != null)
SliverToBoxAdapter(
child: Row(
spacing: 8,
children: [
if (accountRelationship.value == null ||
accountRelationship.value!.status > -100)
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(
context,
).colorScheme.onSecondary,
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
data.profile.background?.id != null
? CloudImageWidget(
file: data.profile.background,
)
: Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
),
FlexibleSpaceBar(
title: Text(
data.nick,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(
context,
).appBarTheme.foregroundColor,
shadows: [appbarShadow],
),
),
),
onPressed: relationshipAction,
label:
Text(
accountRelationship.value == null
? 'addFriendShort'
: 'added',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.person_add)
: const Icon(Symbols.person_check),
),
],
),
if (accountRelationship.value == null ||
accountRelationship.value!.status <= -100)
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(
context,
).colorScheme.onSecondary,
),
),
SliverToBoxAdapter(child: accountBasicInfo(data)),
if (data.badges.isNotEmpty)
SliverToBoxAdapter(
child: BadgeList(
badges: data.badges,
).padding(horizontal: 24, bottom: 24),
),
SliverToBoxAdapter(
child: Column(
spacing: 12,
children: [
LevelingProgressCard(
level: data.profile.level,
experience: data.profile.experience,
progress: data.profile.levelingProgress,
),
onPressed: blockAction,
label:
Text(
accountRelationship.value == null
? 'blockUser'
: 'unblockUser',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.block)
: const Icon(Symbols.person_cancel),
),
),
if (data.profile.verification != null)
VerificationStatusCard(
mark: data.profile.verification!,
),
],
).padding(horizontal: 20),
),
SliverToBoxAdapter(child: accountProfileDetail(data)),
if (user.value != null)
SliverToBoxAdapter(child: accountAction(data)),
SliverToBoxAdapter(
child: Column(
children: [
FortuneGraphWidget(
events: accountEvents,
eventCalanderUser: data.name,
),
],
).padding(all: 8),
),
],
).padding(horizontal: 16),
),
SliverToBoxAdapter(
child: Row(
spacing: 8,
children: [
Expanded(
child: FilledButton.icon(
onPressed: directMessageAction,
icon: const Icon(Symbols.message),
label:
Text(
accountChat.value == null
? 'createDirectMessage'
: 'gotoDirectMessage',
maxLines: 1,
).tr(),
),
),
IconButton.filled(
onPressed: () {
showAbuseReportSheet(
context,
resourceIdentifier: 'account/${data.id}',
);
},
icon: Icon(
Symbols.flag,
color: Theme.of(context).colorScheme.onError,
),
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
Theme.of(context).colorScheme.error,
),
),
),
],
).padding(horizontal: 16, top: 4),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(top: 12),
),
SliverToBoxAdapter(
child: Column(
children: [
FortuneGraphWidget(
events: accountEvents,
eventCalanderUser: data.name,
),
],
).padding(all: 8),
),
],
),
),
),
error:
(error, stackTrace) => AppScaffold(

View File

@@ -73,7 +73,7 @@ class CreateAccountScreen extends HookConsumerWidget {
}
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: Text('createAccount').tr(),

View File

@@ -55,7 +55,7 @@ class LoginScreen extends HookConsumerWidget {
final factorPicked = useState<SnAuthFactor?>(null);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: Text('login').tr(),

View File

@@ -1,14 +1,16 @@
import 'dart:developer';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide ConnectionState;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/call.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/call_button.dart';
import 'package:island/widgets/chat/call_overlay.dart';
import 'package:island/widgets/chat/call_participant_tile.dart';
import 'package:island/widgets/alert.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -21,17 +23,39 @@ class CallScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final ongoingCall = ref.watch(ongoingCallProvider(roomId));
final callState = ref.watch(callNotifierProvider);
final callNotifier = ref.read(callNotifierProvider.notifier);
final callNotifier = ref.watch(callNotifierProvider.notifier);
useEffect(() {
callNotifier.joinRoom(roomId);
log('[Call] Joining the call...');
callNotifier.joinRoom(roomId).catchError((_) {
showConfirmAlert(
'Seems there already has a call connected, do you want override it?',
'Call already connected',
).then((value) {
if (value != true) return;
log('[Call] Joining the call... with overrides');
callNotifier.disconnect();
callNotifier.dispose();
callNotifier.joinRoom(roomId);
});
});
return null;
}, []);
final viewMode = useState<String>('grid');
final allAudioOnly = callNotifier.participants.every(
(p) =>
!(p.hasVideo &&
p.remoteParticipant.trackPublications.values.any(
(pub) =>
pub.track != null &&
pub.kind == TrackType.VIDEO &&
!pub.muted &&
!pub.isDisposed,
)),
);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: PageBackButton(),
title: Column(
@@ -44,45 +68,55 @@ class CallScreen extends HookConsumerWidget {
Text(
callState.isConnected
? formatDuration(callState.duration)
: 'Connecting',
: (switch (callNotifier.room?.connectionState) {
ConnectionState.connected => 'connected',
ConnectionState.connecting => 'connecting',
ConnectionState.reconnecting => 'reconnecting',
_ => 'disconnected',
}).tr(),
style: const TextStyle(fontSize: 14),
),
],
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Icon(Symbols.grid_view),
tooltip: 'Grid View',
onPressed: () => viewMode.value = 'grid',
color:
viewMode.value == 'grid'
? Theme.of(context).colorScheme.primary
: null,
if (!allAudioOnly)
SingleChildScrollView(
child: Row(
spacing: 4,
children: [
for (final live in callNotifier.participants)
SpeakingRippleAvatar(live: live, size: 30),
const Gap(8),
],
),
IconButton(
icon: Icon(Symbols.view_agenda),
tooltip: 'Stage View',
onPressed: () => viewMode.value = 'stage',
color:
viewMode.value == 'stage'
? Theme.of(context).colorScheme.primary
: null,
),
],
),
const Gap(8),
),
],
),
body:
callState.error != null
? Center(
child: Text(
callState.error!,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 320),
child: Column(
children: [
const Icon(Symbols.error_outline, size: 48),
const Gap(4),
Text(
callState.error!,
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF757575)),
),
const Gap(8),
TextButton(
onPressed: () {
callNotifier.disconnect();
callNotifier.dispose();
callNotifier.joinRoom(roomId);
},
child: Text('retry').tr(),
),
],
),
),
)
: Column(
@@ -100,17 +134,8 @@ class CallScreen extends HookConsumerWidget {
child: Text('No participants in call'),
);
}
final participants = callNotifier.participants;
final allAudioOnly = participants.every(
(p) =>
!(p.hasVideo &&
p.remoteParticipant.trackPublications.values
.any(
(pub) =>
pub.track != null &&
pub.kind == TrackType.VIDEO,
)),
);
if (allAudioOnly) {
// Audio-only: show avatars in a compact row
return Center(
@@ -123,138 +148,41 @@ class CallScreen extends HookConsumerWidget {
runSpacing: 8,
children: [
for (final live in participants)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
),
child: SpeakingRippleAvatar(
isSpeaking: live.isSpeaking,
audioLevel:
live.remoteParticipant.audioLevel,
pictureId:
live
.participant
.profile
?.account
.profile
.picture
?.id,
size: 72,
),
),
SpeakingRippleAvatar(
live: live,
size: 72,
).padding(horizontal: 4),
],
),
),
);
}
if (viewMode.value == 'stage') {
// Stage view: show main speaker(s) large, others in row
final mainSpeakers =
participants
.where(
(p) => p
.remoteParticipant
.trackPublications
.values
.any(
(pub) =>
pub.track != null &&
pub.kind == TrackType.VIDEO,
),
)
.toList();
if (mainSpeakers.isEmpty && participants.isNotEmpty) {
mainSpeakers.add(participants.first);
}
final others =
participants
.where((p) => !mainSpeakers.contains(p))
.toList();
return Column(
children: [
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (final speaker in mainSpeakers)
Expanded(
child:
AspectRatio(
aspectRatio: 16 / 9,
child: Card(
margin: EdgeInsets.zero,
child: ClipRRect(
borderRadius:
BorderRadius.circular(8),
child: Column(
children: [
CallParticipantTile(
live: speaker,
),
],
),
),
),
).center(),
// Stage view: show main speaker(s) large, others in row
final mainSpeakers =
participants
.where(
(p) => p
.remoteParticipant
.trackPublications
.values
.any(
(pub) =>
pub.track != null &&
pub.kind == TrackType.VIDEO,
),
],
).padding(horizontal: 12),
),
if (others.isNotEmpty)
SizedBox(
height: 100,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
for (final other in others)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
),
child: CallParticipantTile(
live: other,
),
),
],
),
),
],
);
)
.toList();
if (mainSpeakers.isEmpty && participants.isNotEmpty) {
mainSpeakers.add(participants.first);
}
// Default: grid view
return GridView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount:
isWidestScreen(context)
? 4
: isWiderScreen(context)
? 3
: 2,
childAspectRatio: 16 / 9,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
return Column(
children: [
for (final speaker in mainSpeakers)
Expanded(
child: CallParticipantTile(live: speaker),
),
itemCount: participants.length,
itemBuilder: (context, idx) {
final live = participants[idx];
return AspectRatio(
aspectRatio: 16 / 9,
child: Card(
margin: EdgeInsets.zero,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Column(
children: [CallParticipantTile(live: live)],
),
),
),
).center();
},
],
);
},
),

View File

@@ -21,7 +21,6 @@ import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/call_overlay.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/realms/selection_dropdown.dart';
@@ -346,91 +345,79 @@ class ChatListScreen extends HookConsumerWidget {
child: const Icon(Symbols.add),
),
floatingActionButtonLocation: TabbedFabLocation(context),
body: Stack(
body: Column(
children: [
Column(
children: [
Consumer(
builder: (context, ref, _) {
final summaryState = ref.watch(chatSummaryProvider);
return summaryState.maybeWhen(
loading:
() => const LinearProgressIndicator(
minHeight: 2,
borderRadius: BorderRadius.zero,
),
orElse: () => const SizedBox.shrink(),
);
},
),
Expanded(
child: chats.when(
data:
(items) => RefreshIndicator(
onRefresh:
() => Future.sync(() {
ref.invalidate(chatroomsJoinedProvider);
}),
child: ListView.builder(
padding: getTabbedPadding(
context,
bottom: callState.isConnected ? 96 : null,
),
itemCount:
items
.where(
(item) =>
selectedTab.value == 0 ||
(selectedTab.value == 1 &&
item.type == 1) ||
(selectedTab.value == 2 &&
item.type != 1),
)
.length,
itemBuilder: (context, index) {
final filteredItems =
items
.where(
(item) =>
selectedTab.value == 0 ||
(selectedTab.value == 1 &&
item.type == 1) ||
(selectedTab.value == 2 &&
item.type != 1),
)
.toList();
final item = filteredItems[index];
return ChatRoomListTile(
room: item,
isDirect: item.type == 1,
onTap: () {
context.pushNamed(
'chatRoom',
pathParameters: {'id': item.id},
);
},
Consumer(
builder: (context, ref, _) {
final summaryState = ref.watch(chatSummaryProvider);
return summaryState.maybeWhen(
loading:
() => const LinearProgressIndicator(
minHeight: 2,
borderRadius: BorderRadius.zero,
),
orElse: () => const SizedBox.shrink(),
);
},
),
Expanded(
child: chats.when(
data:
(items) => RefreshIndicator(
onRefresh:
() => Future.sync(() {
ref.invalidate(chatroomsJoinedProvider);
}),
child: ListView.builder(
padding: getTabbedPadding(
context,
bottom: callState.isConnected ? 96 : null,
),
itemCount:
items
.where(
(item) =>
selectedTab.value == 0 ||
(selectedTab.value == 1 &&
item.type == 1) ||
(selectedTab.value == 2 && item.type != 1),
)
.length,
itemBuilder: (context, index) {
final filteredItems =
items
.where(
(item) =>
selectedTab.value == 0 ||
(selectedTab.value == 1 &&
item.type == 1) ||
(selectedTab.value == 2 &&
item.type != 1),
)
.toList();
final item = filteredItems[index];
return ChatRoomListTile(
room: item,
isDirect: item.type == 1,
onTap: () {
context.pushNamed(
'chatRoom',
pathParameters: {'id': item.id},
);
},
),
),
loading:
() => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () {
ref.invalidate(chatroomsJoinedProvider);
},
),
),
),
],
),
Positioned(
left: 0,
right: 0,
bottom: getTabbedPadding(context).bottom + 8,
child: const CallOverlayBar().padding(horizontal: 16, vertical: 12),
);
},
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () {
ref.invalidate(chatroomsJoinedProvider);
},
),
),
),
],
),

View File

@@ -41,7 +41,7 @@ class ChatDetailScreen extends HookConsumerWidget {
try {
final client = ref.watch(apiClientProvider);
await client.patch(
'/chat/$id/members/me/notify',
'/sphere/chat/$id/members/me/notify',
data: {'notify_level': level},
);
ref.invalidate(chatroomIdentityProvider(id));
@@ -59,7 +59,7 @@ class ChatDetailScreen extends HookConsumerWidget {
try {
final client = ref.watch(apiClientProvider);
await client.patch(
'/chat/$id/members/me/notify',
'/sphere/chat/$id/members/me/notify',
data: {'break_until': until.toUtc().toIso8601String()},
);
ref.invalidate(chatroomProvider(id));
@@ -421,10 +421,10 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
showConfirmAlert(
'deleteChatRoomHint'.tr(),
'deleteChatRoom'.tr(),
).then((confirm) {
).then((confirm) async {
if (confirm) {
final client = ref.watch(apiClientProvider);
client.delete('/sphere/chat/$id');
await client.delete('/sphere/chat/$id');
ref.invalidate(chatroomsJoinedProvider);
if (context.mounted) {
context.pop();
@@ -454,10 +454,10 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
showConfirmAlert(
'leaveChatRoomHint'.tr(),
'leaveChatRoom'.tr(),
).then((confirm) {
).then((confirm) async {
if (confirm) {
final client = ref.watch(apiClientProvider);
client.delete('/sphere/chat/$id/members/me');
await client.delete('/sphere/chat/$id/members/me');
ref.invalidate(chatroomsJoinedProvider);
if (context.mounted) {
context.pop();

View File

@@ -114,9 +114,9 @@ class CreatorHubShellScreen extends StatelessWidget {
isRoot: true,
child: Row(
children: [
SizedBox(width: 360, child: const CreatorHubScreen(isAside: true)),
Flexible(flex: 2, child: const CreatorHubScreen(isAside: true)),
const VerticalDivider(width: 1),
Expanded(child: child),
Flexible(flex: 3, child: child),
],
),
);

View File

@@ -26,7 +26,7 @@ part 'pack_detail.freezed.dart';
@riverpod
Future<List<SnSticker>> stickerPackContent(Ref ref, String packId) async {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/stickers/$packId/content');
final resp = await apiClient.get('/sphere/stickers/$packId/content');
return resp.data
.map<SnSticker>((e) => SnSticker.fromJson(e))
.cast<SnSticker>()
@@ -74,13 +74,16 @@ class StickerPackDetailScreen extends HookConsumerWidget {
IconButton(
icon: const Icon(Symbols.add_circle),
onPressed: () {
context.pushNamed('creatorStickerNew', pathParameters: {'packId': id}).then((
value,
) {
if (value != null) {
ref.invalidate(stickerPackContentProvider(id));
}
});
context
.pushNamed(
'creatorStickerNew',
pathParameters: {'name': pubName, 'packId': id},
)
.then((value) {
if (value != null) {
ref.invalidate(stickerPackContentProvider(id));
}
});
},
),
_StickerPackActionMenu(
@@ -173,9 +176,13 @@ class StickerPackDetailScreen extends HookConsumerWidget {
title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit),
callback: () {
context.pushNamed(
context
.pushNamed(
'creatorStickerEdit',
pathParameters: {'packId': id, 'id': sticker.id},
pathParameters: {
'packId': id,
'id': sticker.id,
},
)
.then((value) {
if (value != null) {
@@ -259,9 +266,7 @@ class _StickerPackActionMenu extends HookConsumerWidget {
(context) => [
PopupMenuItem(
onTap: () {
context.push(
'/creators/$pubName/stickers/$packId/edit',
);
context.push('/creators/$pubName/stickers/$packId/edit');
},
child: Row(
children: [

View File

@@ -7,7 +7,7 @@ part of 'pack_detail.dart';
// **************************************************************************
String _$stickerPackContentHash() =>
r'78de848fba1f341f217f8ae4b9eef2d8afa67964';
r'42d74f51022e67e35cb601c2f30f4f02e1f2be9d';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -31,7 +31,7 @@ class StickersScreen extends HookConsumerWidget {
context
.pushNamed(
'creatorStickerPackNew',
queryParameters: {'pubName': pubName},
queryParameters: {'name': pubName},
)
.then((value) {
if (value != null) {
@@ -76,7 +76,7 @@ class SliverStickerPacksList extends HookConsumerWidget {
onTap: () {
context.pushNamed(
'creatorStickerPackDetail',
pathParameters: {'pubName': pubName, 'packId': sticker.id},
pathParameters: {'name': pubName, 'packId': sticker.id},
);
},
);

View File

@@ -51,12 +51,9 @@ class DeveloperHubShellScreen extends StatelessWidget {
isRoot: true,
child: Row(
children: [
SizedBox(
width: 360,
child: const DeveloperHubScreen(isAside: true),
),
Flexible(flex: 2, child: const DeveloperHubScreen(isAside: true)),
const VerticalDivider(width: 1),
Expanded(child: child),
Flexible(flex: 3, child: child),
],
),
);
@@ -114,7 +111,7 @@ class DeveloperHubScreen extends HookConsumerWidget {
);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: !isWide ? const PageBackButton() : null,
title: Text('developerHub').tr(),

View File

@@ -17,7 +17,7 @@ class DiscoveryRealmsScreen extends HookConsumerWidget {
final currentQuery = useState<String?>(null);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(title: Text('discoverRealms'.tr())),
body: Stack(
children: [

View File

@@ -84,8 +84,10 @@ class ExploreScreen extends HookConsumerWidget {
selectedDay.value = day;
}
final user = ref.watch(userInfoProvider);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
toolbarHeight: 0,
bottom: PreferredSize(
@@ -167,67 +169,100 @@ class ExploreScreen extends HookConsumerWidget {
),
),
),
floatingActionButton: FloatingActionButton(
heroTag: Key("explore-page-fab"),
onPressed: () {
context.pushNamed('postCompose').then((value) {
if (value != null) {
activitiesNotifier.forceRefresh();
}
});
floatingActionButton: InkWell(
onLongPress: () {
context.pushNamed('postCompose', queryParameters: {'type': '1'}).then(
(value) {
if (value != null) {
activitiesNotifier.forceRefresh();
}
},
);
},
child: const Icon(Symbols.edit),
child: FloatingActionButton(
heroTag: Key("explore-page-fab"),
onPressed: () {
context.pushNamed('postCompose').then((value) {
if (value != null) {
activitiesNotifier.forceRefresh();
}
});
},
child: const Icon(Symbols.edit),
),
),
floatingActionButtonLocation: TabbedFabLocation(context),
body: Builder(
builder: (context) {
final isWider = isWiderScreen(context);
final bodyView = TabBarView(
controller: tabController,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildActivityList(context, ref, null),
_buildActivityList(context, ref, 'subscriptions'),
_buildActivityList(context, ref, 'friends'),
],
final bodyView = _buildActivityList(
context,
ref,
currentFilter.value,
);
if (isWider) {
return Row(
children: [
Flexible(flex: 3, child: bodyView),
const VerticalDivider(width: 1),
Flexible(
flex: 2,
child: SingleChildScrollView(
child: Column(
children: [
CheckInWidget(),
Card(
margin: EdgeInsets.only(left: 16, right: 16, top: 8),
child: Column(
children: [
// Use the reusable EventCalendarWidget
EventCalendarWidget(
events: events,
initialDate: now,
showEventDetails: true,
onMonthChanged: onMonthChanged,
onDaySelected: onDaySelected,
),
],
Flexible(flex: 3, child: bodyView.padding(left: 8)),
if (user.value != null)
Flexible(
flex: 2,
child: SingleChildScrollView(
child: Column(
children: [
CheckInWidget(
margin: EdgeInsets.only(
left: 8,
right: 12,
top: 16,
),
),
),
FortuneGraphWidget(
events: events,
constrainWidth: true,
onPointSelected: onDaySelected,
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,
),
],
),
),
FortuneGraphWidget(
margin: EdgeInsets.only(left: 8, right: 12, top: 8),
events: events,
constrainWidth: true,
onPointSelected: onDaySelected,
),
],
),
),
)
else
Flexible(
flex: 2,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Welcome to\nthe Solar Network',
style: Theme.of(context).textTheme.titleLarge,
).bold(),
const Gap(2),
Text(
'Login to explore more!',
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
).padding(horizontal: 36, vertical: 16),
),
),
],
);
}
@@ -280,56 +315,62 @@ class _DiscoveryActivityItem extends StatelessWidget {
final items = data['items'] as List;
final type = items.firstOrNull?['type'] ?? 'unknown';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.explore, size: 19),
const Gap(8),
Text(
(switch (type) {
'realm' => 'discoverRealms',
'publisher' => 'discoverPublishers',
'article' => 'discoverWebArticles',
_ => 'unknown',
}).tr(),
style: Theme.of(context).textTheme.titleMedium,
).padding(top: 1),
],
).padding(horizontal: 20, top: 8, bottom: 4),
SizedBox(
height: 180,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: items.length,
padding: const EdgeInsets.symmetric(horizontal: 8),
itemBuilder: (context, index) {
final item = items[index];
switch (type) {
case 'realm':
return RealmCard(
realm: SnRealm.fromJson(item['data']),
maxWidth: 280,
);
case 'publisher':
return PublisherCard(
publisher: SnPublisher.fromJson(item['data']),
maxWidth: 280,
);
case 'article':
return WebArticleCard(
article: SnWebArticle.fromJson(item['data']),
maxWidth: 280,
);
default:
return Placeholder();
}
},
),
).padding(bottom: 4),
],
return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.explore, size: 19),
const Gap(8),
Text(
(switch (type) {
'realm' => 'discoverRealms',
'publisher' => 'discoverPublishers',
'article' => 'discoverWebArticles',
_ => 'unknown',
}).tr(),
style: Theme.of(context).textTheme.titleMedium,
).padding(top: 1),
],
).padding(horizontal: 20, top: 8, bottom: 4),
SizedBox(
height: 180,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200),
child: CarouselView.weighted(
flexWeights:
isWideScreen(context) ? <int>[3, 2, 1] : <int>[4, 1],
consumeMaxWeight: false,
enableSplash: false,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
children: [
for (final item in items)
switch (type) {
'realm' => RealmCard(
realm: SnRealm.fromJson(item['data']),
maxWidth: 280,
),
'publisher' => PublisherCard(
publisher: SnPublisher.fromJson(item['data']),
maxWidth: 280,
),
'article' => WebArticleCard(
article: SnWebArticle.fromJson(item['data']),
maxWidth: 280,
),
_ => Placeholder(),
},
],
),
),
).padding(bottom: 8, horizontal: 8),
],
),
);
}
}
@@ -355,8 +396,13 @@ class _ActivityListView extends HookConsumerWidget {
return CustomScrollView(
slivers: [
SliverGap(12),
if (user.value != null && !contentOnly)
SliverToBoxAdapter(child: CheckInWidget()),
SliverToBoxAdapter(
child: CheckInWidget(
margin: EdgeInsets.only(left: 8, right: 8, bottom: 4),
),
),
SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
@@ -373,19 +419,9 @@ class _ActivityListView extends HookConsumerWidget {
switch (item.type) {
case 'posts.new':
case 'posts.new.replies':
final isReply = item.type == 'posts.new.replies';
itemWidget = PostItem(
backgroundColor:
isWideScreen(context) ? Colors.transparent : null,
itemWidget = PostActionableItem(
borderRadius: 8,
item: SnPost.fromJson(item.data!),
padding:
isReply
? const EdgeInsets.only(
left: 16,
right: 16,
bottom: 16,
)
: null,
onRefresh: () {
activitiesNotifier.forceRefresh();
},
@@ -396,21 +432,10 @@ class _ActivityListView extends HookConsumerWidget {
);
},
);
if (isReply) {
itemWidget = Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const Icon(Symbols.reply),
const Gap(8),
Text('Replying your post'),
],
).padding(horizontal: 20, vertical: 8),
itemWidget,
],
);
}
itemWidget = Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: itemWidget,
);
break;
case 'discovery':
itemWidget = _DiscoveryActivityItem(data: item.data!);
@@ -419,7 +444,7 @@ class _ActivityListView extends HookConsumerWidget {
itemWidget = const Placeholder();
}
return Column(children: [itemWidget, const Divider(height: 1)]);
return itemWidget;
},
),
SliverGap(getTabbedPadding(context).bottom),

View File

@@ -18,7 +18,8 @@ import 'package:island/widgets/post/publishers_modal.dart';
import 'package:island/screens/posts/post_detail.dart';
import 'package:island/widgets/post/compose_settings_sheet.dart';
import 'package:island/services/compose_storage_db.dart';
import 'package:island/widgets/post/draft_manager.dart';
// DraftManagerSheet is now imported through compose_toolbar.dart
import 'package:island/widgets/post/compose_toolbar.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -52,13 +53,13 @@ class PostEditScreen extends HookConsumerWidget {
data: (post) => PostComposeScreen(originalPost: post),
loading:
() => AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(leading: const PageBackButton()),
body: const Center(child: CircularProgressIndicator()),
),
error:
(e, _) => AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(leading: const PageBackButton()),
body: Text('Error: $e', textAlign: TextAlign.center),
),
@@ -92,7 +93,6 @@ class PostComposeScreen extends HookConsumerWidget {
// Otherwise, continue with regular post compose
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
// When editing, preserve the original replied/forwarded post references
final effectiveRepliedPost = repliedPost ?? originalPost?.repliedPost;
@@ -287,43 +287,10 @@ class PostComposeScreen extends HookConsumerWidget {
}
},
child: AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
actions: [
if (originalPost == null) // Only show drafts for new posts
IconButton(
icon: const Icon(Symbols.draft),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => DraftManagerSheet(
onDraftSelected: (draftId) {
final draft =
ref.read(
composeStorageNotifierProvider,
)[draftId];
if (draft != null) {
state.titleController.text = draft.title ?? '';
state.descriptionController.text =
draft.description ?? '';
state.contentController.text =
draft.content ?? '';
state.visibility.value = draft.visibility;
}
},
),
);
},
tooltip: 'drafts'.tr(),
),
IconButton(
icon: const Icon(Symbols.save),
onPressed: () => ComposeLogic.saveDraft(ref, state),
tooltip: 'saveDraft'.tr(),
),
IconButton(
icon: const Icon(Symbols.settings),
onPressed: showSettingsSheet,
@@ -455,27 +422,7 @@ class PostComposeScreen extends HookConsumerWidget {
),
// Bottom toolbar
Material(
elevation: 4,
child: Row(
children: [
IconButton(
onPressed: () => ComposeLogic.pickPhotoMedia(ref, state),
icon: const Icon(Symbols.add_a_photo),
color: colorScheme.primary,
),
IconButton(
onPressed: () => ComposeLogic.pickVideoMedia(ref, state),
icon: const Icon(Symbols.videocam),
color: colorScheme.primary,
),
],
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
horizontal: 16,
top: 8,
),
),
ComposeToolbar(state: state, originalPost: originalPost),
],
),
),
@@ -650,7 +597,7 @@ class PostComposeScreen extends HookConsumerWidget {
child: SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.all(16),
child: PostItem(item: post, isOpenable: false),
child: PostItem(item: post),
),
),
],

View File

@@ -19,8 +19,8 @@ import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/post/compose_shared.dart';
import 'package:island/widgets/post/compose_settings_sheet.dart';
import 'package:island/services/compose_storage_db.dart';
import 'package:island/widgets/post/compose_toolbar.dart';
import 'package:island/widgets/post/publishers_modal.dart';
import 'package:island/widgets/post/draft_manager.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -153,6 +153,57 @@ class ArticleComposeScreen extends HookConsumerWidget {
}
Widget buildPreviewPane() {
final widgetItem = SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 8),
child: ValueListenableBuilder<TextEditingValue>(
valueListenable: state.titleController,
builder: (context, titleValue, _) {
return ValueListenableBuilder<TextEditingValue>(
valueListenable: state.descriptionController,
builder: (context, descriptionValue, _) {
return ValueListenableBuilder<TextEditingValue>(
valueListenable: state.contentController,
builder: (context, contentValue, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (titleValue.text.isNotEmpty) ...[
Text(
titleValue.text,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Gap(16),
],
if (descriptionValue.text.isNotEmpty) ...[
Text(
descriptionValue.text,
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurface.withOpacity(0.7),
),
),
const Gap(16),
],
if (contentValue.text.isNotEmpty)
MarkdownTextContent(
content: contentValue.text,
textStyle: theme.textTheme.bodyMedium,
),
],
);
},
);
},
);
},
),
);
if (isWideScreen(context)) {
return Align(alignment: Alignment.topLeft, child: widgetItem);
}
return Container(
decoration: BoxDecoration(
border: Border.all(color: colorScheme.outline.withOpacity(0.3)),
@@ -178,210 +229,119 @@ class ArticleComposeScreen extends HookConsumerWidget {
],
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: ValueListenableBuilder<TextEditingValue>(
valueListenable: state.titleController,
builder: (context, titleValue, _) {
return ValueListenableBuilder<TextEditingValue>(
valueListenable: state.descriptionController,
builder: (context, descriptionValue, _) {
return ValueListenableBuilder<TextEditingValue>(
valueListenable: state.contentController,
builder: (context, contentValue, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (titleValue.text.isNotEmpty) ...[
Text(
titleValue.text,
style: theme.textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.bold),
),
const Gap(16),
],
if (descriptionValue.text.isNotEmpty) ...[
Text(
descriptionValue.text,
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurface.withOpacity(
0.7,
),
),
),
const Gap(16),
],
if (contentValue.text.isNotEmpty)
MarkdownTextContent(
content: contentValue.text,
textStyle: theme.textTheme.bodyMedium,
),
],
);
},
);
},
);
},
),
),
),
Expanded(child: widgetItem),
],
),
);
}
Widget buildEditorPane() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Publisher row
Card(
margin: EdgeInsets.only(top: 8),
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
GestureDetector(
child: ProfilePictureWidget(
fileId: state.currentPublisher.value?.picture?.id,
radius: 20,
fallbackIcon:
state.currentPublisher.value == null
? Symbols.question_mark
: null,
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey:
(event) => _handleKeyPress(
event,
state,
ref,
context,
originalPost: originalPost,
),
child: TextField(
controller: state.contentController,
style: theme.textTheme.bodyMedium,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'postContent'.tr(),
contentPadding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 8,
),
),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => const PublisherModal(),
).then((value) {
if (value != null) {
state.currentPublisher.value = value;
}
});
},
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(16),
if (state.currentPublisher.value == null)
Text(
'postPublisherUnselected'.tr(),
style: theme.textTheme.bodyMedium,
)
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(state.currentPublisher.value!.nick).bold(),
Text(
'@${state.currentPublisher.value!.name}',
).fontSize(12),
],
),
],
),
),
),
// Content field with keyboard listener
Expanded(
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey:
(event) => _handleKeyPress(
event,
state,
ref,
context,
originalPost: originalPost,
),
child: TextField(
controller: state.contentController,
style: theme.textTheme.bodyMedium,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'postContent'.tr(),
contentPadding: const EdgeInsets.all(8),
),
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
),
// Attachments preview
ValueListenableBuilder<List<UniversalFile>>(
valueListenable: state.attachments,
builder: (context, attachments, _) {
if (attachments.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Gap(16),
Text(
'articleAttachmentHint'.tr(),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
).padding(bottom: 8),
ValueListenableBuilder<Map<int, double>>(
valueListenable: state.attachmentProgress,
builder: (context, progressMap, _) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (var idx = 0; idx < attachments.length; idx++)
SizedBox(
width: 280,
height: 280,
child: AttachmentPreview(
item: attachments[idx],
progress: progressMap[idx],
onRequestUpload:
() => ComposeLogic.uploadAttachment(
ref,
state,
idx,
),
onDelete:
() => ComposeLogic.deleteAttachment(
ref,
state,
idx,
),
onMove: (delta) {
state
.attachments
.value = ComposeLogic.moveAttachment(
state.attachments.value,
idx,
delta,
);
},
onInsert:
() => ComposeLogic.insertAttachment(
ref,
state,
idx,
),
),
),
],
);
},
),
],
);
},
// Attachments preview
ValueListenableBuilder<List<UniversalFile>>(
valueListenable: state.attachments,
builder: (context, attachments, _) {
if (attachments.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Gap(16),
Text(
'articleAttachmentHint'.tr(),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
).padding(bottom: 8),
ValueListenableBuilder<Map<int, double>>(
valueListenable: state.attachmentProgress,
builder: (context, progressMap, _) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (var idx = 0; idx < attachments.length; idx++)
SizedBox(
width: 280,
height: 280,
child: AttachmentPreview(
item: attachments[idx],
progress: progressMap[idx],
onRequestUpload:
() => ComposeLogic.uploadAttachment(
ref,
state,
idx,
),
onDelete:
() => ComposeLogic.deleteAttachment(
ref,
state,
idx,
),
onMove: (delta) {
state
.attachments
.value = ComposeLogic.moveAttachment(
state.attachments.value,
idx,
delta,
);
},
onInsert:
() => ComposeLogic.insertAttachment(
ref,
state,
idx,
),
),
),
],
);
},
),
],
);
},
),
],
),
],
),
);
}
@@ -392,7 +352,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
}
},
child: AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: ValueListenableBuilder<TextEditingValue>(
@@ -406,38 +366,26 @@ class ArticleComposeScreen extends HookConsumerWidget {
actions: [
// Info banner for article compose
const SizedBox.shrink(),
if (originalPost == null) // Only show drafts for new articles
IconButton(
icon: const Icon(Symbols.draft),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => DraftManagerSheet(
onDraftSelected: (draftId) {
final draft =
ref.read(
composeStorageNotifierProvider,
)[draftId];
if (draft != null) {
state.titleController.text = draft.title ?? '';
state.descriptionController.text =
draft.description ?? '';
state.contentController.text =
draft.content ?? '';
state.visibility.value = draft.visibility;
}
},
),
);
},
tooltip: 'drafts'.tr(),
),
IconButton(
icon: const Icon(Symbols.save),
onPressed: () => ComposeLogic.saveDraft(ref, state),
tooltip: 'saveDraft'.tr(),
icon: ProfilePictureWidget(
fileId: state.currentPublisher.value?.picture?.id,
radius: 12,
fallbackIcon:
state.currentPublisher.value == null
? Symbols.question_mark
: null,
),
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => const PublisherModal(),
).then((value) {
if (value != null) {
state.currentPublisher.value = value;
}
});
},
),
IconButton(
icon: const Icon(Symbols.settings),
@@ -499,6 +447,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
flex: showPreview.value ? 1 : 2,
child: buildEditorPane(),
),
const VerticalDivider(),
if (showPreview.value)
Expanded(child: buildPreviewPane()),
],
@@ -510,27 +459,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
),
// Bottom toolbar
Material(
elevation: 4,
child: Row(
children: [
IconButton(
onPressed: () => ComposeLogic.pickPhotoMedia(ref, state),
icon: const Icon(Symbols.add_a_photo),
color: colorScheme.primary,
),
IconButton(
onPressed: () => ComposeLogic.pickVideoMedia(ref, state),
icon: const Icon(Symbols.videocam),
color: colorScheme.primary,
),
],
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
horizontal: 16,
top: 8,
),
),
ComposeToolbar(state: state, originalPost: originalPost),
],
),
),

View File

@@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_quick_reply.dart';
@@ -54,10 +53,8 @@ class PostDetailScreen extends HookConsumerWidget {
final postState = ref.watch(postStateProvider(id));
final user = ref.watch(userInfoProvider);
final isWide = isWideScreen(context);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(title: const Text('Post')),
body: postState.when(
data: (post) {
@@ -67,13 +64,13 @@ class PostDetailScreen extends HookConsumerWidget {
CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Column(
children: [
PostItem(
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 600),
child: PostItem(
item: post!,
isOpenable: false,
isFullPost: true,
backgroundColor: isWide ? Colors.transparent : null,
isEmbedReply: false,
onUpdate: (newItem) {
// Update the local state with the new post data
ref
@@ -81,11 +78,10 @@ class PostDetailScreen extends HookConsumerWidget {
.updatePost(newItem);
},
),
const Divider(height: 1),
],
),
),
),
PostRepliesList(postId: id),
PostRepliesList(postId: id, maxWidth: 600),
SliverGap(MediaQuery.of(context).padding.bottom + 80),
],
),

View File

@@ -5,6 +5,7 @@ import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/response.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
final postSearchNotifierProvider = StateNotifierProvider.autoDispose<
@@ -55,7 +56,7 @@ class PostSearchNotifier
'query': _currentQuery,
'offset': offset,
'take': _pageSize,
'useVector': true,
'useVector': false,
},
);
@@ -109,7 +110,7 @@ class _PostSearchScreenState extends ConsumerState<PostSearchScreen> {
@override
Widget build(BuildContext context) {
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
title: TextField(
controller: _searchController,
@@ -141,6 +142,7 @@ class _PostSearchScreenState extends ConsumerState<PostSearchScreen> {
}
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.items.length + (data.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= data.items.length) {
@@ -151,14 +153,27 @@ class _PostSearchScreenState extends ConsumerState<PostSearchScreen> {
}
final post = data.items[index];
return Column(
children: [PostItem(item: post), const Divider(height: 1)],
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 600),
child: Card(
margin: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
child: PostActionableItem(item: post, borderRadius: 8),
),
),
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(postSearchNotifierProvider),
),
);
},
),

View File

@@ -11,6 +11,7 @@ import 'package:island/models/user.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/color.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/badge.dart';
import 'package:island/widgets/account/status.dart';
@@ -121,210 +122,280 @@ class PublisherProfileScreen extends HookConsumerWidget {
offset: Offset(1.0, 1.0),
);
Widget publisherBasisWidget(SnPublisher data) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20,
children: [
GestureDetector(
child: Badge(
isLabelVisible: data.type == 0,
padding: EdgeInsets.all(4),
label: Icon(
Symbols.launch,
size: 16,
color: Theme.of(context).colorScheme.onPrimary,
),
backgroundColor: Theme.of(context).colorScheme.primary,
offset: Offset(0, 48),
child: ProfilePictureWidget(file: data.picture, radius: 32),
),
onTap: () {
Navigator.pop(context, true);
if (data.account?.name != null) {
context.pushNamed(
'accountProfile',
pathParameters: {'name': data.account!.name},
);
}
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
spacing: 6,
children: [
Text(data.nick).fontSize(20),
if (data.verification != null)
VerificationMark(mark: data.verification!),
Expanded(
child: Text(
'@${data.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).fontSize(14).opacity(0.85),
),
],
),
if (data.type == 0 && data.account != null)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 6,
children: [
Icon(
data.type == 0 ? Symbols.person : Symbols.workspaces,
fill: 1,
size: 17,
),
Text(
'publisherBelongsTo'.tr(args: ['@${data.account!.name}']),
).fontSize(14),
],
).opacity(0.85).padding(bottom: 6),
if (data.type == 0 && data.account != null)
AccountStatusWidget(
uname: data.account!.name,
padding: EdgeInsets.zero,
),
subStatus
.when(
data:
(status) => FilledButton.icon(
onPressed:
subscribing.value
? null
: (status.isSubscribed
? unsubscribe
: subscribe),
icon: Icon(
status.isSubscribed
? Symbols.remove_circle
: Symbols.add_circle,
),
label:
Text(
status.isSubscribed
? 'unsubscribe'
: 'subscribe',
).tr(),
style: ButtonStyle(
visualDensity: VisualDensity(vertical: -2),
),
),
error: (_, _) => const SizedBox(),
loading:
() => const SizedBox(
height: 36,
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
),
)
.padding(top: 8),
],
),
),
],
).padding(horizontal: 24, top: 24);
Widget publisherVerificationWidget(SnPublisher data) => Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
children: [
if (badges.value?.isNotEmpty ?? false)
BadgeList(badges: badges.value!).padding(top: 16),
if (data.verification != null)
VerificationStatusCard(mark: data.verification!),
],
),
).padding(top: 16);
Widget publisherDetailWidget(SnPublisher data) => Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('bio').tr().bold().padding(bottom: 2),
Text(data.bio.isEmpty ? 'descriptionNone'.tr() : data.bio),
],
).padding(horizontal: 20, vertical: 16),
);
return publisher.when(
data:
(data) => AppScaffold(
noBackground: false,
body: CustomScrollView(
slivers: [
SliverAppBar(
foregroundColor: appbarColor.value,
expandedHeight: 180,
pinned: true,
leading: PageBackButton(
color: appbarColor.value,
shadows: [appbarShadow],
),
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
data.background?.id != null
? CloudImageWidget(file: data.background)
: Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
isNoBackground: false,
appBar:
isWideScreen(context)
? AppBar(
foregroundColor: appbarColor.value,
leading: PageBackButton(
color: appbarColor.value,
shadows: [appbarShadow],
),
FlexibleSpaceBar(
title: Text(
data.nick,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(context).appBarTheme.foregroundColor,
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
data.background?.id != null
? CloudImageWidget(file: data.background)
: Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
),
FlexibleSpaceBar(
title: Text(
data.nick,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(
context,
).appBarTheme.foregroundColor,
shadows: [appbarShadow],
),
),
background:
Container(), // Empty container since background is handled by Stack
),
],
),
)
: null,
body:
isWideScreen(context)
? Row(
children: [
Flexible(
flex: 4,
child: CustomScrollView(
slivers: [
SliverGap(16),
SliverPostList(pubName: name),
SliverGap(
MediaQuery.of(context).padding.bottom + 16,
),
],
).padding(left: 8),
),
Flexible(
flex: 3,
child: Align(
alignment: Alignment.topLeft,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
publisherBasisWidget(data),
publisherVerificationWidget(data),
publisherDetailWidget(data),
],
),
),
),
),
],
)
: CustomScrollView(
slivers: [
SliverAppBar(
foregroundColor: appbarColor.value,
expandedHeight: 180,
pinned: true,
leading: PageBackButton(
color: appbarColor.value,
shadows: [appbarShadow],
),
),
background:
Container(), // Empty container since background is handled by Stack
),
],
),
),
SliverToBoxAdapter(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20,
children: [
GestureDetector(
child: Badge(
isLabelVisible: data.type == 0,
padding: EdgeInsets.all(4),
label: Icon(
Symbols.launch,
size: 16,
color: Theme.of(context).colorScheme.onPrimary,
),
backgroundColor:
Theme.of(context).colorScheme.primary,
offset: Offset(0, 48),
child: ProfilePictureWidget(
file: data.picture,
radius: 32,
),
),
onTap: () {
Navigator.pop(context, true);
if (data.account?.name != null) {
context.pushNamed(
'accountProfile',
pathParameters: {'name': data.account!.name},
);
}
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
spacing: 6,
children: [
Text(data.nick).fontSize(20),
if (data.verification != null)
VerificationMark(mark: data.verification!),
Text(
'@${data.name}',
).fontSize(14).opacity(0.85),
],
),
if (data.type == 0 && data.account != null)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 6,
children: [
Icon(
data.type == 0
? Symbols.person
: Symbols.workspaces,
fill: 1,
size: 17,
),
Text(
'publisherBelongsTo'.tr(
args: ['@${data.account!.name}'],
),
).fontSize(14),
],
).opacity(0.85).padding(bottom: 6),
if (data.type == 0 && data.account != null)
AccountStatusWidget(
uname: data.account!.name,
padding: EdgeInsets.zero,
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
data.background?.id != null
? CloudImageWidget(
file: data.background,
)
: Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
),
subStatus
.when(
data:
(status) => FilledButton.icon(
onPressed:
subscribing.value
? null
: (status.isSubscribed
? unsubscribe
: subscribe),
icon: Icon(
status.isSubscribed
? Symbols.remove_circle
: Symbols.add_circle,
),
label:
Text(
status.isSubscribed
? 'unsubscribe'
: 'subscribe',
).tr(),
style: ButtonStyle(
visualDensity: VisualDensity(
vertical: -2,
),
),
),
error: (_, _) => const SizedBox(),
loading:
() => const SizedBox(
height: 36,
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
),
)
.padding(top: 8),
],
FlexibleSpaceBar(
title: Text(
data.nick,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(
context,
).appBarTheme.foregroundColor,
shadows: [appbarShadow],
),
),
background:
Container(), // Empty container since background is handled by Stack
),
],
),
),
),
],
).padding(horizontal: 24, top: 24),
),
SliverToBoxAdapter(
child: Column(
children: [
if (badges.value?.isNotEmpty ?? false)
BadgeList(badges: badges.value!).padding(top: 16),
if (data.verification != null)
VerificationStatusCard(
mark: data.verification!,
).padding(top: 16),
],
).padding(horizontal: 24),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(vertical: 24),
),
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('bio').tr().bold(),
Text(
data.bio.isEmpty ? 'descriptionNone'.tr() : data.bio,
),
],
).padding(horizontal: 24),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(top: 24),
),
SliverPostList(pubName: name),
SliverGap(MediaQuery.of(context).padding.bottom + 16),
],
),
SliverToBoxAdapter(child: publisherBasisWidget(data)),
SliverToBoxAdapter(
child: publisherVerificationWidget(data),
),
SliverToBoxAdapter(child: publisherDetailWidget(data)),
SliverPostList(pubName: name),
SliverGap(MediaQuery.of(context).padding.bottom + 16),
],
),
),
error:
(error, stackTrace) => AppScaffold(
isNoBackground: false,
appBar: AppBar(leading: const PageBackButton()),
body: Center(child: Text(error.toString())),
),
loading:
() => AppScaffold(
isNoBackground: false,
appBar: AppBar(leading: const PageBackButton()),
body: Center(child: CircularProgressIndicator()),
),

View File

@@ -79,7 +79,7 @@ class RealmDetailScreen extends HookConsumerWidget {
);
return AppScaffold(
noBackground: false,
isNoBackground: false,
body: realmState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),
@@ -321,10 +321,10 @@ class _RealmActionMenu extends HookConsumerWidget {
showConfirmAlert(
'leaveRealmHint'.tr(),
'leaveRealm'.tr(),
).then((confirm) {
).then((confirm) async {
if (confirm) {
final client = ref.watch(apiClientProvider);
client.delete(
await client.delete(
'/sphere/realms/$realmSlug/members/me',
);
ref.invalidate(realmsJoinedProvider);
@@ -361,10 +361,12 @@ class _RealmActionMenu extends HookConsumerWidget {
showConfirmAlert(
'leaveRealmHint'.tr(),
'leaveRealm'.tr(),
).then((confirm) {
).then((confirm) async {
if (confirm) {
final client = ref.watch(apiClientProvider);
client.delete('/sphere/realms/$realmSlug/members/me');
await client.delete(
'/sphere/realms/$realmSlug/members/me',
);
ref.invalidate(realmsJoinedProvider);
if (context.mounted) {
context.pop(true);

View File

@@ -41,7 +41,7 @@ class RealmListScreen extends HookConsumerWidget {
final realmInvites = ref.watch(realmInvitesProvider);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
title: const Text('realms').tr(),
actions: [
@@ -279,7 +279,7 @@ class EditRealmScreen extends HookConsumerWidget {
}
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
title: Text(slug == null ? 'createRealm'.tr() : 'editRealm'.tr()),
leading: const PageBackButton(),

View File

@@ -552,7 +552,7 @@ class SettingsScreen extends HookConsumerWidget {
}
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
title: Text('settings').tr(),
actions:

View File

@@ -20,6 +20,33 @@ extension DurationFormatter on Duration {
return '${isNegative ? '-' : ''}$hours:$minutes:$seconds';
}
String formatShortDuration() {
final isNegative = inMicroseconds < 0;
final positiveDuration = isNegative ? -this : this;
final hours = positiveDuration.inHours;
final minutes = (positiveDuration.inMinutes % 60).toString().padLeft(
2,
'0',
);
final seconds = (positiveDuration.inSeconds % 60).toString().padLeft(
2,
'0',
);
final milliseconds = (positiveDuration.inMilliseconds % 1000)
.toString()
.padLeft(3, '0');
String result;
if (hours > 0) {
result =
'${isNegative ? '-' : ''}${hours.toString().padLeft(2, '0')}:$minutes:$seconds.$milliseconds';
} else {
result = '${isNegative ? '-' : ''}$minutes:$seconds.$milliseconds';
}
return result;
}
String formatOffset() {
final isNegative = inMicroseconds < 0;
final positiveDuration = isNegative ? -this : this;

View File

@@ -140,30 +140,27 @@ class VerificationStatusCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
mark.type == 4
? Symbols.play_circle
: mark.type == 0
? Symbols.build_circle
: Symbols.verified,
size: 32,
color: kVerificationMarkColors[mark.type],
fill: 1,
),
const Gap(8),
Text(mark.title ?? 'No title').bold(),
Text(mark.description ?? 'descriptionNone'.tr()),
const Gap(6),
Text(
'Verified by\n${mark.verifiedBy ?? 'No one verified it'}',
).fontSize(11).opacity(0.8),
],
).padding(horizontal: 24, vertical: 16),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
mark.type == 4
? Symbols.play_circle
: mark.type == 0
? Symbols.build_circle
: Symbols.verified,
size: 32,
color: kVerificationMarkColors[mark.type],
fill: 1,
),
const Gap(8),
Text(mark.title ?? 'No title').bold(),
Text(mark.description ?? 'descriptionNone'.tr()),
const Gap(6),
Text(
'Verified by\n${mark.verifiedBy ?? 'No one verified it'}',
).fontSize(11).opacity(0.8),
],
).padding(horizontal: 24, vertical: 16);
}
}

View File

@@ -167,6 +167,7 @@ Future<void> showAccountProfileCard(
offset: offset ?? Offset.zero,
context: context,
builder: (context) => AccountProfileCard(uname: uname),
alignment: Alignment.center,
dimBackground: true,
);
}

View File

@@ -26,6 +26,8 @@ class FortuneGraphWidget extends HookConsumerWidget {
final String? eventCalanderUser;
final EdgeInsets? margin;
const FortuneGraphWidget({
super.key,
required this.events,
@@ -34,6 +36,7 @@ class FortuneGraphWidget extends HookConsumerWidget {
this.height = 180,
this.onPointSelected,
this.eventCalanderUser,
this.margin,
});
@override
@@ -249,7 +252,7 @@ class FortuneGraphWidget extends HookConsumerWidget {
if (constrainWidth) {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Card(margin: EdgeInsets.all(16), child: content),
child: Card(margin: margin ?? EdgeInsets.all(16), child: content),
).center();
}

View File

@@ -86,6 +86,7 @@ class AccountStatusCreationWidget extends HookConsumerWidget {
onTap: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
builder:
(context) => AccountStatusCreationSheet(
initialStatus:

View File

@@ -49,7 +49,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget {
final user = ref.watch(userInfoProvider);
final apiClient = ref.read(apiClientProvider);
await apiClient.request(
'/accounts/me/statuses',
'/id/accounts/me/statuses',
data: {
'attitude': attitude.value,
'is_invisible': isInvisible.value,

View File

@@ -165,7 +165,7 @@ class AppScaffold extends StatelessWidget {
final AppBar? appBar;
final DrawerCallback? onDrawerChanged;
final DrawerCallback? onEndDrawerChanged;
final bool? noBackground;
final bool? isNoBackground;
final bool? extendBody;
const AppScaffold({
@@ -181,7 +181,7 @@ class AppScaffold extends StatelessWidget {
this.endDrawer,
this.onDrawerChanged,
this.onEndDrawerChanged,
this.noBackground,
this.isNoBackground,
this.extendBody,
});
@@ -190,7 +190,7 @@ class AppScaffold extends StatelessWidget {
final appBarHeight = appBar?.preferredSize.height ?? 0;
final safeTop = MediaQuery.of(context).padding.top;
final noBackground = this.noBackground ?? isWideScreen(context);
final noBackground = isNoBackground ?? isWideScreen(context);
final content = Column(
children: [

View File

@@ -4,9 +4,11 @@ import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/call.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/chat/call_participant_tile.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:livekit_client/livekit_client.dart';
@@ -20,8 +22,10 @@ class CallControlsBar extends HookConsumerWidget {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
child: Wrap(
alignment: WrapAlignment.center,
runSpacing: 16,
spacing: 16,
children: [
_buildCircularButtonWithDropdown(
context: context,
@@ -33,7 +37,6 @@ class CallControlsBar extends HookConsumerWidget {
hasDropdown: true,
deviceType: 'videoinput',
),
const Gap(16),
_buildCircularButton(
icon:
callState.isScreenSharing
@@ -42,7 +45,6 @@ class CallControlsBar extends HookConsumerWidget {
onPressed: () => callNotifier.toggleScreenShare(),
backgroundColor: const Color(0xFF424242),
),
const Gap(16),
_buildCircularButtonWithDropdown(
context: context,
ref: ref,
@@ -52,10 +54,62 @@ class CallControlsBar extends HookConsumerWidget {
hasDropdown: true,
deviceType: 'audioinput',
),
const Gap(16),
_buildCircularButton(
icon:
callState.isSpeakerphone
? Symbols.mobile_speaker
: Symbols.ear_sound,
onPressed: () => callNotifier.toggleSpeakerphone(),
backgroundColor: const Color(0xFF424242),
),
_buildCircularButton(
icon: Icons.call_end,
onPressed: () => callNotifier.disconnect(),
onPressed:
() => showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder:
(innerContext) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Symbols.logout, fill: 1),
title: Text('callLeave').tr(),
onTap: () {
callNotifier.disconnect();
Navigator.of(context).pop();
Navigator.of(innerContext).pop();
},
),
ListTile(
leading: const Icon(Symbols.call_end, fill: 1),
iconColor: Colors.red,
title: Text('callEnd').tr(),
onTap: () async {
callNotifier.disconnect();
final apiClient = ref.watch(apiClientProvider);
try {
showLoadingModal(context);
await apiClient.delete(
'/sphere/chat/realtime/${callNotifier.roomId}',
);
callNotifier.dispose();
if (context.mounted) {
Navigator.of(context).pop();
Navigator.of(innerContext).pop();
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
},
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
),
backgroundColor: const Color(0xFFE53E3E),
iconColor: Colors.white,
),
@@ -212,24 +266,14 @@ class CallControlsBar extends HookConsumerWidget {
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${'switchedTo'.tr()} ${device.label.isNotEmpty ? device.label : 'selectedDevice'.tr()}',
),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${'failedToSwitchDevice'.tr()}: $e'),
backgroundColor: Colors.red,
showSnackBar(
'switchedTo'.tr(
args: [device.label.isNotEmpty ? device.label : 'device'],
),
);
}
} catch (err) {
showErrorAlert(err);
}
}
}
@@ -279,7 +323,7 @@ class CallOverlayBar extends HookConsumerWidget {
child: Card(
margin: EdgeInsets.zero,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Row(
@@ -294,17 +338,7 @@ class CallOverlayBar extends HookConsumerWidget {
height: 40,
child:
SpeakingRippleAvatar(
isSpeaking: lastSpeaker.isSpeaking,
audioLevel:
lastSpeaker.remoteParticipant.audioLevel,
pictureId:
lastSpeaker
.participant
.profile
?.account
.profile
.picture
?.id,
live: lastSpeaker,
size: 36,
).center(),
);
@@ -314,10 +348,7 @@ class CallOverlayBar extends HookConsumerWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
lastSpeaker.participant.profile?.account.nick ??
'unknown'.tr(),
).bold(),
Text('@${lastSpeaker.participant.identity}').bold(),
Text(
formatDuration(callState.duration),
style: Theme.of(context).textTheme.bodySmall,
@@ -360,7 +391,10 @@ class CallOverlayBar extends HookConsumerWidget {
).padding(all: 16),
),
onTap: () {
context.pushNamed('chatCall', pathParameters: {'id': callNotifier.roomId!});
context.pushNamed(
'chatCall',
pathParameters: {'id': callNotifier.roomId!},
);
},
);
}

View File

@@ -0,0 +1,123 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_popup_card/flutter_popup_card.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/call.dart';
import 'package:island/widgets/account/account_nameplate.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:styled_widget/styled_widget.dart';
class CallParticipantCard extends HookConsumerWidget {
final CallParticipantLive live;
const CallParticipantCard({super.key, required this.live});
@override
Widget build(BuildContext context, WidgetRef ref) {
final width =
math.min(MediaQuery.of(context).size.width - 80, 360).toDouble();
final callNotifier = ref.watch(callNotifierProvider.notifier);
final volumeSliderValue = useState(callNotifier.getParticipantVolume(live));
return PopupCard(
elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
child: SizedBox(
width: width,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Column(
spacing: 4,
children: [
Row(
children: [
const Icon(Symbols.sound_detection_loud_sound, size: 16),
const Gap(8),
Expanded(
child: Slider(
value: volumeSliderValue.value,
onChanged: (value) {
volumeSliderValue.value = value;
},
onChangeEnd: (value) {
callNotifier.setParticipantVolume(live, value);
},
year2023: true,
padding: EdgeInsets.zero,
),
),
const Gap(8),
Text(
'${(volumeSliderValue.value * 100).toStringAsFixed(0)}%',
),
],
),
Row(
children: [
const Icon(Symbols.wifi, size: 16),
const Gap(8),
Text(switch (live.remoteParticipant.connectionQuality) {
ConnectionQuality.excellent => 'Excellent',
ConnectionQuality.good => 'Good',
ConnectionQuality.poor => 'Bad',
ConnectionQuality.lost => 'Lost',
_ => 'Connecting',
}),
],
),
],
).padding(horizontal: 20, top: 16),
AccountNameplate(
name: live.participant.identity,
isOutlined: false,
),
],
),
),
);
}
}
class CallParticipantGestureDetector extends StatelessWidget {
final CallParticipantLive participant;
final Widget child;
const CallParticipantGestureDetector({
super.key,
required this.participant,
required this.child,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
child: child,
onTapDown: (details) {
showCallParticipantCard(
context,
participant,
offset: details.localPosition,
);
},
);
}
}
Future<void> showCallParticipantCard(
BuildContext context,
CallParticipantLive participant, {
Offset? offset,
}) async {
await showPopupCard<void>(
offset: offset ?? Offset.zero,
context: context,
builder: (context) => CallParticipantCard(live: participant),
alignment: Alignment.center,
dimBackground: true,
);
}

View File

@@ -1,92 +1,127 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/call.dart';
import 'package:island/screens/account/profile.dart';
import 'package:island/widgets/chat/call_participant_card.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class SpeakingRippleAvatar extends StatelessWidget {
final bool isSpeaking;
final double audioLevel;
final String? pictureId;
class SpeakingRippleAvatar extends HookConsumerWidget {
final CallParticipantLive live;
final double size;
const SpeakingRippleAvatar({
super.key,
required this.isSpeaking,
required this.audioLevel,
required this.pictureId,
this.size = 96,
});
const SpeakingRippleAvatar({super.key, required this.live, this.size = 96});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final account = ref.watch(accountProvider(live.participant.identity));
final avatarRadius = size / 2;
final clampedLevel = audioLevel.clamp(0.0, 1.0);
final clampedLevel = live.remoteParticipant.audioLevel.clamp(0.0, 1.0);
final rippleRadius = avatarRadius + clampedLevel * (size * 0.333);
return TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: avatarRadius,
end: isSpeaking ? rippleRadius : avatarRadius,
),
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
builder: (context, animatedRadius, child) {
return Stack(
alignment: Alignment.center,
children: [
if (isSpeaking)
return SizedBox(
width: size + 8,
height: size + 8,
child: TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: avatarRadius,
end: live.remoteParticipant.isSpeaking ? rippleRadius : avatarRadius,
),
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
builder: (context, animatedRadius, child) {
return Stack(
alignment: Alignment.center,
children: [
if (live.remoteParticipant.isSpeaking)
Container(
width: animatedRadius * 2,
height: animatedRadius * 2,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.green.withOpacity(0.75 + 0.25 * clampedLevel),
),
),
Container(
width: animatedRadius * 2,
height: animatedRadius * 2,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.green.withOpacity(0.75 + 0.25 * clampedLevel),
width: size,
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(shape: BoxShape.circle),
child: account.when(
data:
(value) => CallParticipantGestureDetector(
participant: live,
child: ProfilePictureWidget(
file: value.profile.picture,
radius: size / 2,
),
),
error:
(_, _) => CircleAvatar(
radius: size / 2,
child: const Icon(Symbols.person_remove),
),
loading:
() => CircleAvatar(
radius: size / 2,
child: CircularProgressIndicator(),
),
),
),
Container(
width: size,
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(shape: BoxShape.circle),
child: ProfilePictureWidget(fileId: pictureId, radius: size / 2),
),
],
);
},
if (live.remoteParticipant.isMuted)
Positioned(
bottom: 4,
right: 4,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
child: const Icon(
Symbols.mic_off,
size: 14,
fill: 1,
).padding(left: 1.5, top: 1.5),
),
),
],
);
},
),
);
}
}
class CallParticipantTile extends StatelessWidget {
class CallParticipantTile extends HookConsumerWidget {
final CallParticipantLive live;
const CallParticipantTile({super.key, required this.live});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final hasVideo =
live.hasVideo &&
live.remoteParticipant.trackPublications.values
.where((pub) => pub.track != null && pub.kind == TrackType.VIDEO)
.isNotEmpty;
final audioLevel = live.remoteParticipant.audioLevel;
if (hasVideo) {
return Stack(
fit: StackFit.loose,
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: AspectRatio(
aspectRatio: 16 / 9,
child: VideoTrackRenderer(
live.remoteParticipant.trackPublications.values
.where((track) => track.kind == TrackType.VIDEO)
.first
.track
as VideoTrack,
renderMode: VideoRenderMode.platformView,
),
AspectRatio(
aspectRatio: 16 / 9,
child: VideoTrackRenderer(
live.remoteParticipant.trackPublications.values
.where((track) => track.kind == TrackType.VIDEO)
.first
.track
as VideoTrack,
renderMode: VideoRenderMode.platformView,
),
),
Positioned(
@@ -94,21 +129,26 @@ class CallParticipantTile extends StatelessWidget {
right: 8,
bottom: 8,
child: Text(
live.participant.profile?.account.nick ??
'${'unknown'.tr()}\'s video',
'@${live.participant.name}',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 14, color: Colors.white),
style: const TextStyle(
fontSize: 14,
color: Colors.white,
shadows: [
BoxShadow(
color: Colors.black54,
offset: Offset(1, 1),
spreadRadius: 8,
blurRadius: 8,
),
],
),
),
),
],
);
} else {
return SpeakingRippleAvatar(
isSpeaking: live.isSpeaking,
audioLevel: audioLevel,
pictureId: live.participant.profile?.account.profile.picture?.id,
size: 84,
);
return SpeakingRippleAvatar(size: 84, live: live);
}
}
}

View File

@@ -5,16 +5,19 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/database/message.dart';
import 'package:island/models/chat.dart';
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/widgets/account/account_name.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/alert.native.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/embed/link.dart';
@@ -67,6 +70,46 @@ class MessageItem extends HookConsumerWidget {
final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
final messageLanguage =
remoteMessage.content != null
? ref.watch(detectStringLanguageProvider(remoteMessage.content!))
: null;
final currentLanguage = context.locale.toString();
final translatableLanguage =
messageLanguage != null
? messageLanguage.substring(0, 2) != currentLanguage.substring(0, 2)
: false;
final translating = useState(false);
final translatedText = useState<String?>(null);
Future<void> translate() async {
if (translatedText.value != null) {
translatedText.value = null;
return;
}
if (translating.value) return;
if (remoteMessage.content == null) return;
translating.value = true;
try {
final text = await ref.watch(
translateStringProvider(
TranslateQuery(
text: remoteMessage.content!,
lang: currentLanguage.substring(0, 2),
),
).future,
);
translatedText.value = text;
} catch (err) {
showErrorAlert(err);
} finally {
translating.value = false;
}
}
return ContextMenuWidget(
menuProvider: (_) {
if (onAction == null) return Menu(children: []);
@@ -103,6 +146,18 @@ class MessageItem extends HookConsumerWidget {
onAction!.call(MessageItemAction.forward);
},
),
if (translatableLanguage) MenuSeparator(),
if (translatableLanguage)
MenuAction(
title:
translatedText.value == null
? 'translate'.tr()
: translating.value
? 'translating'.tr()
: 'translated'.tr(),
image: MenuImage.icon(Symbols.translate),
callback: translate,
),
if (isMobile) MenuSeparator(),
if (isMobile)
MenuAction(
@@ -221,14 +276,18 @@ class MessageItem extends HookConsumerWidget {
isReply: false,
).padding(vertical: 4),
if (_MessageItemContent.hasContent(remoteMessage))
_MessageItemContent(item: remoteMessage),
_MessageItemContent(
item: remoteMessage,
translatedText: translatedText.value,
),
if (remoteMessage.attachments.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
return CloudFileList(
files: remoteMessage.attachments,
maxWidth: constraints.maxWidth,
).padding(vertical: 4);
padding: EdgeInsets.symmetric(vertical: 4),
);
},
),
if (remoteMessage.meta['embeds'] != null)
@@ -481,7 +540,8 @@ class MessageQuoteWidget extends HookConsumerWidget {
class _MessageItemContent extends StatelessWidget {
final SnChatMessage item;
const _MessageItemContent({required this.item});
final String? translatedText;
const _MessageItemContent({required this.item, this.translatedText});
@override
Widget build(BuildContext context) {
@@ -494,10 +554,40 @@ class _MessageItemContent extends StatelessWidget {
);
case 'text':
default:
return MarkdownTextContent(
content: item.content!,
isSelectable: true,
linesMargin: EdgeInsets.zero,
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownTextContent(
content: item.content!,
isSelectable: true,
linesMargin: EdgeInsets.zero,
),
if (translatedText?.isNotEmpty ?? false)
...([
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: math.min(
280,
MediaQuery.of(context).size.width * 0.4,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('translated').tr().fontSize(11).opacity(0.75),
const Gap(8),
Flexible(child: Divider()),
],
).padding(vertical: 4),
),
MarkdownTextContent(
content: translatedText!,
isSelectable: true,
linesMargin: EdgeInsets.zero,
),
]),
],
);
}
}

View File

@@ -35,7 +35,8 @@ Future<SnCheckInResult?> checkInResultToday(Ref ref) async {
}
class CheckInWidget extends HookConsumerWidget {
const CheckInWidget({super.key});
final EdgeInsets? margin;
const CheckInWidget({super.key, this.margin});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -66,7 +67,8 @@ class CheckInWidget extends HookConsumerWidget {
}
return Card(
margin: EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
margin:
margin ?? EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 16,

View File

@@ -0,0 +1,146 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/time.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:media_kit/media_kit.dart';
import 'package:styled_widget/styled_widget.dart';
class UniversalAudio extends ConsumerStatefulWidget {
final String uri;
final bool autoplay;
const UniversalAudio({super.key, required this.uri, this.autoplay = false});
@override
ConsumerState<UniversalAudio> createState() => _UniversalAudioState();
}
class _UniversalAudioState extends ConsumerState<UniversalAudio> {
Player? _player;
Duration _duration = Duration(seconds: 1);
Duration _duartionBuffered = Duration(seconds: 1);
Duration _position = Duration(seconds: 0);
bool _sliderWorking = false;
Duration _sliderPosition = Duration(seconds: 0);
void _openAudio() async {
final url = widget.uri;
MediaKit.ensureInitialized();
_player = Player();
_player!.stream.position.listen((value) {
_position = value;
if (!_sliderWorking) _sliderPosition = _position;
setState(() {});
});
_player!.stream.buffer.listen((value) {
_duartionBuffered = value;
setState(() {});
});
_player!.stream.duration.listen((value) {
_duration = value;
setState(() {});
});
String? uri;
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
if (inCacheInfo == null) {
log('[MediaPlayer] Miss cache: $url');
final token = ref.watch(tokenProvider)?.token;
DefaultCacheManager().downloadFile(
url,
authHeaders: {'Authorization': 'AtField $token'},
);
uri = url;
} else {
uri = inCacheInfo.file.path;
log('[MediaPlayer] Hit cache: $url');
}
_player!.open(Media(uri), play: widget.autoplay);
}
@override
void initState() {
super.initState();
_openAudio();
}
@override
void dispose() {
super.dispose();
_player?.dispose();
}
@override
Widget build(BuildContext context) {
if (_player == null) {
return Center(child: CircularProgressIndicator());
}
return Card(
color: Theme.of(context).colorScheme.surfaceContainerLowest,
child: Row(
children: [
IconButton.filled(
onPressed: () {
_player!.playOrPause().then((_) {
if (mounted) setState(() {});
});
},
icon:
_player!.state.playing
? const Icon(Symbols.pause, fill: 1, color: Colors.white)
: const Icon(
Symbols.play_arrow,
fill: 1,
color: Colors.white,
),
),
const Gap(20),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Text(
'${_position.formatShortDuration()} / ${_duration.formatShortDuration()}',
),
],
),
Slider(
value: _sliderPosition.inMilliseconds.toDouble(),
secondaryTrackValue:
_duartionBuffered.inMilliseconds.toDouble(),
max: _duration.inMilliseconds.toDouble(),
onChangeStart: (_) {
_sliderWorking = true;
},
onChanged: (value) {
_sliderPosition = Duration(milliseconds: value.toInt());
setState(() {});
},
onChangeEnd: (value) {
_sliderPosition = Duration(milliseconds: value.toInt());
_sliderWorking = false;
_player!.seek(_sliderPosition);
},
year2023: true,
padding: EdgeInsets.zero,
),
],
),
),
],
).padding(horizontal: 24, vertical: 16),
);
}
}

View File

@@ -14,6 +14,7 @@ import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:path/path.dart' show extension;
import 'package:path_provider/path_provider.dart';
import 'package:photo_view/photo_view.dart';
@@ -27,14 +28,16 @@ class CloudFileList extends HookConsumerWidget {
final double? minWidth;
final bool disableZoomIn;
final bool disableConstraint;
final EdgeInsets? padding;
const CloudFileList({
super.key,
required this.files,
this.maxHeight = 360,
this.maxHeight = 560,
this.maxWidth = double.infinity,
this.minWidth,
this.disableZoomIn = false,
this.disableConstraint = false,
this.padding,
});
double calculateAspectRatio() {
@@ -60,42 +63,43 @@ class CloudFileList extends HookConsumerWidget {
if (files.isEmpty) return const SizedBox.shrink();
if (files.length == 1) {
final isImage = files.first.mimeType?.startsWith('image') ?? false;
return ConstrainedBox(
final isAudio = files.first.mimeType?.startsWith('audio') ?? false;
final widgetItem = ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: _CloudFileListEntry(
file: files.first,
heroTag: heroTags.first,
isImage: isImage,
disableZoomIn: disableZoomIn,
onTap: () {
if (!isImage) {
return;
}
if (!disableZoomIn) {
context.pushTransparentRoute(
CloudFileZoomIn(item: files.first, heroTag: heroTags.first),
rootNavigator: true,
);
}
},
),
);
return Container(
padding: padding,
constraints: BoxConstraints(
maxHeight: disableConstraint ? double.infinity : maxHeight,
minWidth: minWidth ?? 0,
maxWidth:
files.length == 1
? math.max(
math.min(520, MediaQuery.of(context).size.width * 0.85),
minWidth ?? 0,
)
: double.infinity,
maxWidth: files.length == 1 ? maxWidth : double.infinity,
),
child: AspectRatio(
aspectRatio: calculateAspectRatio(),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: _CloudFileListEntry(
file: files.first,
heroTag: heroTags.first,
isImage: isImage,
disableZoomIn: disableZoomIn,
onTap: () {
if (!isImage) {
return;
}
if (!disableZoomIn) {
context.pushTransparentRoute(
CloudFileZoomIn(item: files.first, heroTag: heroTags.first),
rootNavigator: true,
);
}
},
),
),
),
).padding(horizontal: 3);
height: isAudio ? 180 : null,
child:
isAudio
? widgetItem
: AspectRatio(
aspectRatio: calculateAspectRatio(),
child: widgetItem,
),
);
}
final allImages =
@@ -109,22 +113,37 @@ class CloudFileList extends HookConsumerWidget {
child: AspectRatio(
aspectRatio: calculateAspectRatio(),
child: CarouselView(
padding: padding,
itemSnapping: true,
itemExtent: math.min(
MediaQuery.of(context).size.width * 0.85,
maxWidth * 0.85,
),
itemSnapping: true,
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
children: [
for (var i = 0; i < files.length; i++)
_CloudFileListEntry(
file: files[i],
heroTag: heroTags[i],
isImage: files[i].mimeType?.startsWith('image') ?? false,
disableZoomIn: disableZoomIn,
fit: BoxFit.cover,
Stack(
children: [
_CloudFileListEntry(
file: files[i],
heroTag: heroTags[i],
isImage: files[i].mimeType?.startsWith('image') ?? false,
disableZoomIn: disableZoomIn,
),
Positioned(
bottom: 12,
left: 16,
child: Text('${i + 1}/${files.length}')
.textColor(Colors.white)
.textShadow(
color: Colors.black54,
offset: Offset(1, 1),
blurRadius: 3,
),
),
],
),
],
onTap: (i) {
@@ -150,28 +169,52 @@ class CloudFileList extends HookConsumerWidget {
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: files.length,
padding: EdgeInsets.symmetric(horizontal: 3),
padding: padding,
itemBuilder: (context, index) {
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: _CloudFileListEntry(
file: files[index],
heroTag: heroTags[index],
isImage: files[index].mimeType?.startsWith('image') ?? false,
disableZoomIn: disableZoomIn,
onTap: () {
if (!(files[index].mimeType?.startsWith('image') ?? false)) {
return;
}
if (!disableZoomIn) {
context.pushTransparentRoute(
CloudFileZoomIn(
item: files[index],
heroTag: heroTags[index],
),
);
}
},
return AspectRatio(
aspectRatio:
files[index].fileMeta?['ratio'] is num
? files[index].fileMeta!['ratio'].toDouble()
: 1.0,
child: Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: _CloudFileListEntry(
file: files[index],
heroTag: heroTags[index],
isImage:
files[index].mimeType?.startsWith('image') ?? false,
disableZoomIn: disableZoomIn,
onTap: () {
if (!(files[index].mimeType?.startsWith('image') ??
false)) {
return;
}
if (!disableZoomIn) {
context.pushTransparentRoute(
CloudFileZoomIn(
item: files[index],
heroTag: heroTags[index],
),
rootNavigator: true,
);
}
},
),
),
Positioned(
bottom: 12,
left: 16,
child: Text('${index + 1}/${files.length}')
.textColor(Colors.white)
.textShadow(
color: Colors.black54,
offset: Offset(1, 1),
blurRadius: 3,
),
),
],
),
);
},
@@ -193,6 +236,8 @@ class CloudFileZoomIn extends HookConsumerWidget {
final photoViewController = useMemoized(() => PhotoViewController(), []);
final rotation = useState(0);
final showOriginal = useState(false);
Future<void> saveToGallery() async {
try {
// Show loading indicator
@@ -206,7 +251,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
final filePath = '${tempDir.path}/${item.id}.${extension(item.name)}';
await client.download(
'/files/${item.id}',
'/drive/files/${item.id}',
filePath,
queryParameters: {'original': true},
);
@@ -356,7 +401,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
imageProvider: CloudImageWidget.provider(
fileId: item.id,
serverUrl: serverUrl,
original: true,
original: showOriginal.value,
),
// Apply rotation transformation
customSize: MediaQuery.of(context).size,
@@ -390,6 +435,23 @@ class CloudFileZoomIn extends HookConsumerWidget {
saveToGallery();
},
),
IconButton(
onPressed: () {
showOriginal.value = !showOriginal.value;
},
icon: Icon(
showOriginal.value ? Symbols.raw_on : Symbols.raw_off,
color: Colors.white,
size: 24,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
),
],
),
IconButton(
@@ -497,7 +559,6 @@ class _CloudFileListEntry extends StatelessWidget {
final bool isImage;
final bool disableZoomIn;
final VoidCallback? onTap;
final BoxFit fit;
const _CloudFileListEntry({
required this.file,
@@ -505,7 +566,6 @@ class _CloudFileListEntry extends StatelessWidget {
required this.isImage,
required this.disableZoomIn,
this.onTap,
this.fit = BoxFit.contain,
});
@override
@@ -528,10 +588,10 @@ class _CloudFileListEntry extends StatelessWidget {
item: file,
heroTag: heroTag,
noBlurhash: true,
fit: fit,
fit: BoxFit.contain,
)
else
CloudFileWidget(item: file, heroTag: heroTag, fit: fit),
CloudFileWidget(item: file, heroTag: heroTag, fit: BoxFit.contain),
],
);

View File

@@ -1,8 +1,13 @@
import 'dart:math' as math;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/content/audio.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -45,7 +50,15 @@ class CloudFileWidget extends ConsumerWidget {
),
"video" => AspectRatio(
aspectRatio: ratio,
child: UniversalVideo(uri: uri, aspectRatio: ratio),
child: CloudVideoWidget(item: item),
),
"audio" => Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: math.min(360, MediaQuery.of(context).size.width * 0.8),
),
child: UniversalAudio(uri: uri),
),
),
_ => Text('Unable render for ${item.mimeType}'),
};
@@ -58,6 +71,119 @@ class CloudFileWidget extends ConsumerWidget {
}
}
class CloudVideoWidget extends HookConsumerWidget {
final SnCloudFile item;
const CloudVideoWidget({super.key, required this.item});
@override
Widget build(BuildContext context, WidgetRef ref) {
final open = useState(false);
final serverUrl = ref.watch(serverUrlProvider);
final uri = '$serverUrl/drive/files/${item.id}';
var ratio =
item.fileMeta?['ratio'] is num
? item.fileMeta!['ratio'].toDouble()
: 1.0;
if (ratio == 0) ratio = 1.0;
if (open.value) {
return UniversalVideo(uri: uri, aspectRatio: ratio, autoplay: true);
}
return GestureDetector(
child: Stack(
children: [
UniversalImage(uri: '$uri?thumbnail=true'),
Positioned.fill(
child: Center(
child: const Icon(
Symbols.play_arrow,
fill: 1,
size: 32,
shadows: [
BoxShadow(
color: Colors.black54,
offset: Offset(1, 1),
spreadRadius: 8,
blurRadius: 8,
),
],
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
spacing: 8,
children: [
if (item.fileMeta?['duration'] != null)
Text(
Duration(
milliseconds:
((item.fileMeta?['duration'] as num) * 1000)
.toInt(),
).formatDuration(),
style: TextStyle(
shadows: [
BoxShadow(
color: Colors.black54,
offset: Offset(1, 1),
spreadRadius: 8,
blurRadius: 8,
),
],
),
),
if (item.fileMeta?['bit_rate'] != null)
Text(
'${int.parse(item.fileMeta?['bit_rate'] as String) ~/ 1000} Kbps',
style: TextStyle(
shadows: [
BoxShadow(
color: Colors.black54,
offset: Offset(1, 1),
spreadRadius: 8,
blurRadius: 8,
),
],
),
),
],
),
Text(
item.name,
style: TextStyle(
fontWeight: FontWeight.bold,
shadows: [
BoxShadow(
color: Colors.black54,
offset: Offset(1, 1),
spreadRadius: 8,
blurRadius: 8,
),
],
),
),
],
),
).padding(horizontal: 16, bottom: 12),
],
),
onTap: () {
open.value = true;
},
);
}
}
class CloudImageWidget extends ConsumerWidget {
final String? fileId;
final SnCloudFile? file;
@@ -92,7 +218,10 @@ class CloudImageWidget extends ConsumerWidget {
required String serverUrl,
bool original = false,
}) {
final uri = '$serverUrl/drive/files/$fileId?original=$original';
final uri =
original
? '$serverUrl/drive/files/$fileId?original=true'
: '$serverUrl/drive/files/$fileId';
return CachedNetworkImageProvider(uri);
}
}

View File

@@ -81,7 +81,10 @@ class MarkdownTextContent extends HookConsumerWidget {
if (url != null) {
if (url.scheme == 'solian') {
if (url.host == 'account') {
context.pushNamed('accountProfile', pathParameters: {'name': url.pathSegments[0]});
context.pushNamed(
'accountProfile',
pathParameters: {'name': url.pathSegments[0]},
);
}
return;
}
@@ -153,7 +156,7 @@ class MarkdownTextContent extends HookConsumerWidget {
),
child: UniversalImage(
uri:
'$baseUrl/stickers/lookup/${uri.pathSegments[0]}/open',
'$baseUrl/sphere/stickers/lookup/${uri.pathSegments[0]}/open',
width: size,
height: size,
fit: BoxFit.cover,

View File

@@ -11,10 +11,12 @@ import 'package:media_kit_video/media_kit_video.dart';
class UniversalVideo extends ConsumerStatefulWidget {
final String uri;
final double aspectRatio;
final bool autoplay;
const UniversalVideo({
super.key,
required this.uri,
this.aspectRatio = 16 / 9,
this.autoplay = false,
});
@override
@@ -47,7 +49,7 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
log('[MediaPlayer] Hit cache: $url');
}
_player!.open(Media(uri), play: false);
_player!.open(Media(uri), play: widget.autoplay);
}
@override

View File

@@ -4,10 +4,12 @@ import 'package:flutter/material.dart';
class UniversalVideo extends StatelessWidget {
final String uri;
final double aspectRatio;
final bool autoplay;
const UniversalVideo({
super.key,
required this.uri,
required this.aspectRatio,
this.autoplay = false,
});
@override

View File

@@ -0,0 +1,122 @@
import 'dart:async';
import 'dart:developer';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.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/services/time.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart' hide Amplitude;
import 'package:styled_widget/styled_widget.dart';
import 'package:uuid/uuid.dart';
import 'package:waveform_flutter/waveform_flutter.dart';
class ComposeRecorder extends HookConsumerWidget {
const ComposeRecorder({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final recording = useState(false);
final recordingStartAt = useState<DateTime?>(null);
final recordingDuration = useState<Duration>(Duration(seconds: 0));
StreamSubscription? originalAmplitude;
StreamController<Amplitude> amplitudeStream = StreamController();
var record = AudioRecorder();
final resultPath = useState<String?>(null);
Future<void> startRecord() async {
recording.value = true;
// Check and request permission if needed
final tempPath = !kIsWeb ? (await getTemporaryDirectory()).path : 'temp';
final uuid = const Uuid().v4().substring(0, 8);
if (!await record.hasPermission()) return;
const recordConfig = RecordConfig(
encoder: AudioEncoder.pcm16bits,
autoGain: true,
echoCancel: true,
noiseSuppress: true,
);
resultPath.value = '$tempPath/solar-network-record-$uuid.m4a';
await record.start(recordConfig, path: resultPath.value!);
recordingStartAt.value = DateTime.now();
originalAmplitude = record
.onAmplitudeChanged(const Duration(milliseconds: 100))
.listen((value) async {
amplitudeStream.add(
Amplitude(current: value.current, max: value.max),
);
recordingDuration.value = DateTime.now().difference(
recordingStartAt.value!,
);
});
}
useEffect(() {
return () {
// Called when widget is unmounted
log('[Recorder] Clean up!');
originalAmplitude?.cancel();
amplitudeStream.close();
record.dispose();
};
}, []);
Future<void> stopRecord() async {
recording.value = false;
await record.pause();
final newResult = await record.stop();
await record.cancel();
if (newResult != null) resultPath.value = newResult;
if (context.mounted) Navigator.of(context).pop(resultPath.value);
}
return SheetScaffold(
titleText: "recordAudio".tr(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Gap(32),
Text(
recordingDuration.value.formatShortDuration(),
).fontSize(20).bold().padding(bottom: 8),
SizedBox(
height: 120,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: Card(
color: Theme.of(context).colorScheme.surfaceContainer,
child: AnimatedWaveList(stream: amplitudeStream.stream),
),
),
),
),
const Gap(12),
IconButton.filled(
onPressed: recording.value ? stopRecord : startRecord,
iconSize: 32,
icon:
recording.value
? const Icon(Symbols.stop, fill: 1, color: Colors.white)
: const Icon(
Symbols.play_arrow,
fill: 1,
color: Colors.white,
),
),
],
),
);
}
}

View File

@@ -3,6 +3,7 @@ import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
@@ -13,12 +14,15 @@ import 'package:island/pods/network.dart';
import 'package:island/services/file.dart';
import 'package:island/services/compose_storage_db.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/compose_recorder.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:textfield_tags/textfield_tags.dart';
import 'dart:async';
import 'dart:developer';
import 'package:textfield_tags/textfield_tags.dart';
class ComposeState {
final TextEditingController titleController;
final TextEditingController descriptionController;
@@ -60,6 +64,9 @@ class ComposeState {
_autoSaveTimer?.cancel();
_autoSaveTimer = null;
}
bool get isEmpty =>
attachments.value.isEmpty && contentController.text.isEmpty;
}
class ComposeLogic {
@@ -392,6 +399,115 @@ class ComposeLogic {
];
}
static Future<void> recordAudioMedia(
WidgetRef ref,
ComposeState state,
BuildContext context,
) async {
final audioPath = await showModalBottomSheet<String?>(
context: context,
builder: (context) => ComposeRecorder(),
);
if (audioPath == null) return;
state.attachments.value = [
...state.attachments.value,
UniversalFile(
data: XFile(audioPath, mimeType: 'audio/m4a'),
type: UniversalFileType.audio,
),
];
}
static Future<void> linkAttachment(
WidgetRef ref,
ComposeState state,
BuildContext context,
) async {
final TextEditingController idController = TextEditingController();
String? errorMessage;
await showModalBottomSheet(
context: context,
builder: (BuildContext dialogContext) {
return StatefulBuilder(
builder: (context, setState) {
return SheetScaffold(
titleText: 'linkAttachment'.tr(),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: idController,
decoration: InputDecoration(
labelText: 'fileId'.tr(),
helperText: 'fileIdHint'.tr(),
helperMaxLines: 3,
errorText: errorMessage,
border: OutlineInputBorder(),
),
),
const Gap(16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
icon: const Icon(Symbols.add),
label: Text('add'.tr()),
onPressed: () async {
final fileId = idController.text.trim();
if (fileId.isEmpty) {
setState(() {
errorMessage = 'fileIdCannotBeEmpty'.tr();
});
return;
}
try {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/drive/files/$fileId/info',
);
final SnCloudFile cloudFile = SnCloudFile.fromJson(
response.data,
);
state.attachments.value = [
...state.attachments.value,
UniversalFile(
data: cloudFile,
type: switch (cloudFile.mimeType
?.split('/')
.firstOrNull) {
'image' => UniversalFileType.image,
'video' => UniversalFileType.video,
'audio' => UniversalFileType.audio,
_ => UniversalFileType.file,
},
),
];
if (context.mounted) {
Navigator.of(dialogContext).pop();
}
} catch (e) {
setState(() {
errorMessage = 'failedToFetchFile'.tr(
args: [e.toString()],
);
});
}
},
),
),
],
).padding(horizontal: 24, vertical: 24),
);
},
);
},
);
}
static Future<void> uploadAttachment(
WidgetRef ref,
ComposeState state,

View File

@@ -0,0 +1,117 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/services/compose_storage_db.dart';
import 'package:island/widgets/post/compose_shared.dart';
import 'package:island/widgets/post/draft_manager.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class ComposeToolbar extends HookConsumerWidget {
final ComposeState state;
final SnPost? originalPost;
const ComposeToolbar({super.key, required this.state, this.originalPost});
@override
Widget build(BuildContext context, WidgetRef ref) {
void pickPhotoMedia() {
ComposeLogic.pickPhotoMedia(ref, state);
}
void pickVideoMedia() {
ComposeLogic.pickVideoMedia(ref, state);
}
void addYourVoice() {
ComposeLogic.recordAudioMedia(ref, state, context);
}
void linkAttachment() {
ComposeLogic.linkAttachment(ref, state, context);
}
void saveDraft() {
ComposeLogic.saveDraft(ref, state);
}
void showDraftManager() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => DraftManagerSheet(
onDraftSelected: (draftId) {
final draft = ref.read(composeStorageNotifierProvider)[draftId];
if (draft != null) {
state.titleController.text = draft.title ?? '';
state.descriptionController.text = draft.description ?? '';
state.contentController.text = draft.content ?? '';
state.visibility.value = draft.visibility;
}
},
),
);
}
final colorScheme = Theme.of(context).colorScheme;
return Material(
elevation: 4,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Row(
children: [
IconButton(
onPressed: pickPhotoMedia,
tooltip: 'addPhoto'.tr(),
icon: const Icon(Symbols.add_a_photo),
color: colorScheme.primary,
),
IconButton(
onPressed: pickVideoMedia,
tooltip: 'addVideo'.tr(),
icon: const Icon(Symbols.videocam),
color: colorScheme.primary,
),
IconButton(
onPressed: addYourVoice,
tooltip: 'addYourVoice'.tr(),
icon: const Icon(Symbols.mic),
color: colorScheme.primary,
),
IconButton(
onPressed: linkAttachment,
icon: const Icon(Symbols.attach_file),
tooltip: 'linkAttachment'.tr(),
color: colorScheme.primary,
),
const Spacer(),
if (originalPost == null && state.isEmpty)
IconButton(
icon: const Icon(Symbols.draft),
color: colorScheme.primary,
onPressed: showDraftManager,
tooltip: 'drafts'.tr(),
)
else if (originalPost == null)
IconButton(
icon: const Icon(Symbols.save),
color: colorScheme.primary,
onPressed: saveDraft,
onLongPress: showDraftManager,
tooltip: 'saveDraft'.tr(),
),
],
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
horizontal: 16,
top: 8,
),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'post_item.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$postFeaturedReplyHash() => r'3f0ac0d51ad21f8754a63dd94109eb8ac4812293';
/// 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 [postFeaturedReply].
@ProviderFor(postFeaturedReply)
const postFeaturedReplyProvider = PostFeaturedReplyFamily();
/// See also [postFeaturedReply].
class PostFeaturedReplyFamily extends Family<AsyncValue<SnPost?>> {
/// See also [postFeaturedReply].
const PostFeaturedReplyFamily();
/// See also [postFeaturedReply].
PostFeaturedReplyProvider call(String id) {
return PostFeaturedReplyProvider(id);
}
@override
PostFeaturedReplyProvider getProviderOverride(
covariant PostFeaturedReplyProvider 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'postFeaturedReplyProvider';
}
/// See also [postFeaturedReply].
class PostFeaturedReplyProvider extends AutoDisposeFutureProvider<SnPost?> {
/// See also [postFeaturedReply].
PostFeaturedReplyProvider(String id)
: this._internal(
(ref) => postFeaturedReply(ref as PostFeaturedReplyRef, id),
from: postFeaturedReplyProvider,
name: r'postFeaturedReplyProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$postFeaturedReplyHash,
dependencies: PostFeaturedReplyFamily._dependencies,
allTransitiveDependencies:
PostFeaturedReplyFamily._allTransitiveDependencies,
id: id,
);
PostFeaturedReplyProvider._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
Override overrideWith(
FutureOr<SnPost?> Function(PostFeaturedReplyRef provider) create,
) {
return ProviderOverride(
origin: this,
override: PostFeaturedReplyProvider._internal(
(ref) => create(ref as PostFeaturedReplyRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
id: id,
),
);
}
@override
AutoDisposeFutureProviderElement<SnPost?> createElement() {
return _PostFeaturedReplyProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PostFeaturedReplyProvider && 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 PostFeaturedReplyRef on AutoDisposeFutureProviderRef<SnPost?> {
/// The parameter `id` of this provider.
String get id;
}
class _PostFeaturedReplyProviderElement
extends AutoDisposeFutureProviderElement<SnPost?>
with PostFeaturedReplyRef {
_PostFeaturedReplyProviderElement(super.provider);
@override
String get id => (origin as PostFeaturedReplyProvider).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

@@ -45,11 +45,13 @@ class PostItemCreator extends HookConsumerWidget {
title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit),
callback: () {
context.pushNamed('postEdit', pathParameters: {'id': item.id}).then((value) {
if (value != null) {
onRefresh?.call();
}
});
context
.pushNamed('postEdit', pathParameters: {'id': item.id})
.then((value) {
if (value != null) {
onRefresh?.call();
}
});
},
),
MenuAction(
@@ -80,7 +82,10 @@ class PostItemCreator extends HookConsumerWidget {
image: MenuImage.icon(Symbols.link),
callback: () {
// Copy post link to clipboard
context.pushNamed('postDetail', pathParameters: {'id': item.id});
context.pushNamed(
'postDetail',
pathParameters: {'id': item.id},
);
},
),
],
@@ -88,8 +93,6 @@ class PostItemCreator extends HookConsumerWidget {
},
child: Material(
color: backgroundColor ?? Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
elevation: 1,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
@@ -197,8 +200,8 @@ class PostItemCreator extends HookConsumerWidget {
CloudFileList(
files: item.attachments,
maxWidth: MediaQuery.of(context).size.width * 0.85,
minWidth: MediaQuery.of(context).size.width * 0.9,
).padding(top: 8),
padding: EdgeInsets.only(top: 8),
),
// Reference post indicator
if (item.repliedPost != null || item.forwardedPost != null)
@@ -211,7 +214,7 @@ class PostItemCreator extends HookConsumerWidget {
size: 16,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 4),
const Gap(4),
Text(
item.repliedPost != null
? 'repliedTo'.tr()
@@ -364,6 +367,7 @@ class PostItemCreator extends HookConsumerWidget {
PostReactionList(
parentId: item.id,
reactions: item.reactionsCount,
reactionsMade: item.reactionsMade,
padding: EdgeInsets.zero,
),
const Gap(16),

View File

@@ -94,9 +94,7 @@ class SliverPostList extends HookConsumerWidget {
final post = data.items[index];
return Column(
children: [_buildPostItem(post), const Divider(height: 1)],
);
return _buildPostItem(post);
},
),
);
@@ -105,16 +103,24 @@ class SliverPostList extends HookConsumerWidget {
Widget _buildPostItem(SnPost post) {
switch (itemType) {
case PostItemType.creator:
return PostItemCreator(
item: post,
backgroundColor: backgroundColor,
padding: padding,
isOpenable: isOpenable,
onRefresh: onRefresh,
onUpdate: onUpdate,
return Column(
children: [
PostItemCreator(
item: post,
backgroundColor: backgroundColor,
padding: padding,
isOpenable: isOpenable,
onRefresh: onRefresh,
onUpdate: onUpdate,
),
const Divider(),
],
);
case PostItemType.regular:
return PostItem(item: post);
return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: PostActionableItem(item: post, borderRadius: 8),
);
}
}
}

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
@@ -56,17 +55,17 @@ class PostRepliesNotifier extends _$PostRepliesNotifier
class PostRepliesList extends HookConsumerWidget {
final String postId;
final Color? backgroundColor;
final double? maxWidth;
final VoidCallback? onOpen;
const PostRepliesList({
super.key,
required this.postId,
this.backgroundColor,
this.maxWidth,
this.onOpen,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isWide = isWideScreen(context);
return PagingHelperSliverView(
provider: postRepliesNotifierProvider(postId),
futureRefreshable: postRepliesNotifierProvider(postId).future,
@@ -93,16 +92,24 @@ class PostRepliesList extends HookConsumerWidget {
return endItemView;
}
return Column(
children: [
PostItem(
item: data.items[index],
backgroundColor:
backgroundColor ?? (isWide ? Colors.transparent : null),
showReferencePost: false,
),
const Divider(height: 1),
],
final contentWidget = Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: PostActionableItem(
borderRadius: 8,
item: data.items[index],
isShowReference: false,
isEmbedOpenable: true,
onOpen: onOpen,
),
);
if (maxWidth == null) return contentWidget;
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth!),
child: contentWidget,
),
);
},
);

View File

@@ -27,7 +27,9 @@ class PostRepliesSheet extends HookConsumerWidget {
slivers: [
PostRepliesList(
postId: post.id.toString(),
backgroundColor: Colors.transparent,
onOpen: () {
Navigator.pop(context);
},
),
],
),

View File

@@ -27,9 +27,13 @@ class PublisherCard extends ConsumerWidget {
Widget card = Card(
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.zero,
child: InkWell(
onTap: () {
context.pushNamed('publisherProfile', pathParameters: {'name': publisher.name});
context.pushNamed(
'publisherProfile',
pathParameters: {'name': publisher.name},
);
},
child: AspectRatio(
aspectRatio: 16 / 7,

View File

@@ -29,9 +29,13 @@ class RealmCard extends ConsumerWidget {
Widget card = Card(
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.zero,
child: InkWell(
onTap: () {
context.pushNamed('realmDetail', pathParameters: {'slug': realm.slug});
context.pushNamed(
'realmDetail',
pathParameters: {'slug': realm.slug},
);
},
child: AspectRatio(
aspectRatio: 16 / 7,

View File

@@ -28,6 +28,7 @@ class WebArticleCard extends StatelessWidget {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Card(
margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => _onTap(context),

View File

@@ -14,6 +14,7 @@
#include <flutter_udid/flutter_udid_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <irondash_engine_context/irondash_engine_context_plugin.h>
#include <livekit_client/live_kit_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
#include <pasteboard/pasteboard_plugin.h>
@@ -48,6 +49,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin");
irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar);
g_autoptr(FlPluginRegistrar) livekit_client_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "LiveKitPlugin");
live_kit_plugin_register_with_registrar(livekit_client_registrar);
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);

View File

@@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_udid
flutter_webrtc
irondash_engine_context
livekit_client
media_kit_libs_linux
media_kit_video
pasteboard

View File

@@ -11,34 +11,34 @@ PODS:
- FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- Firebase/CoreOnly (11.15.0):
- FirebaseCore (~> 11.15.0)
- Firebase/Messaging (11.15.0):
- Firebase/CoreOnly (12.0.0):
- FirebaseCore (~> 12.0.0)
- Firebase/Messaging (12.0.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.15.0)
- firebase_core (3.15.2):
- Firebase/CoreOnly (~> 11.15.0)
- FirebaseMessaging (~> 12.0.0)
- firebase_core (4.0.0):
- Firebase/CoreOnly (~> 12.0.0)
- FlutterMacOS
- firebase_messaging (15.2.10):
- Firebase/CoreOnly (~> 11.15.0)
- Firebase/Messaging (~> 11.15.0)
- firebase_messaging (16.0.0):
- Firebase/CoreOnly (~> 12.0.0)
- Firebase/Messaging (~> 12.0.0)
- firebase_core
- FlutterMacOS
- FirebaseCore (11.15.0):
- FirebaseCoreInternal (~> 11.15.0)
- FirebaseCore (12.0.0):
- FirebaseCoreInternal (~> 12.0.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (11.15.0):
- FirebaseCoreInternal (12.0.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (12.0.0):
- FirebaseCore (~> 12.0.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- FirebaseMessaging (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
@@ -56,9 +56,9 @@ PODS:
- flutter_udid (0.0.1):
- FlutterMacOS
- SAMKeychain
- flutter_webrtc (0.14.0):
- flutter_webrtc (1.0.0):
- FlutterMacOS
- WebRTC-SDK (= 125.6422.07)
- WebRTC-SDK (= 137.7151.02)
- FlutterMacOS (1.0.0)
- gal (1.0.0):
- Flutter
@@ -92,10 +92,10 @@ PODS:
- GoogleUtilities/Privacy
- irondash_engine_context (0.0.1):
- FlutterMacOS
- livekit_client (2.4.9):
- livekit_client (2.5.0):
- flutter_webrtc
- FlutterMacOS
- WebRTC-SDK (= 125.6422.07)
- WebRTC-SDK (= 137.7151.02)
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
@@ -143,6 +143,8 @@ PODS:
- sqlite3/common
- sqlite3/rtree (3.50.3):
- sqlite3/common
- sqlite3/session (3.50.3):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
@@ -152,6 +154,7 @@ PODS:
- sqlite3/math
- sqlite3/perf-threadsafe
- sqlite3/rtree
- sqlite3/session
- super_native_extensions (0.0.1):
- FlutterMacOS
- url_launcher_macos (0.0.1):
@@ -160,7 +163,7 @@ PODS:
- FlutterMacOS
- wakelock_plus (0.0.1):
- FlutterMacOS
- WebRTC-SDK (125.6422.07)
- WebRTC-SDK (137.7151.02)
DEPENDENCIES:
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
@@ -291,25 +294,25 @@ SPEC CHECKSUMS:
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_core: 7667f880631ae8ad10e3d6567ab7582fe0682326
firebase_messaging: df39858bcbbcce792c9e4f1ca51b41123d6181fd
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09
Firebase: 800d487043c0557d9faed71477a38d9aafb08a41
firebase_core: eeea10f64026b68cd0bc3dee079ab4717e22909e
firebase_messaging: 5eefcd5bde556bfacdd9968e11c52f39032dfbe5
FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a
FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55
FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988
FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
flutter_timezone: d59eea86178cbd7943cd2431cc2eaa9850f935d8
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
flutter_webrtc: a7eeb54859e672228c28f4b48b1fb61561976ea3
flutter_webrtc: 0d70bd8782c19bde286dc52f766eebbea26de201
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
livekit_client: c9d9f41996f5cf22b9ba0e8483e6af4ca5094059
livekit_client: 0b0515e03858b86a7c14cc7fd6f772331f6ee84c
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
@@ -326,12 +329,12 @@ SPEC CHECKSUMS:
sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81
sqlite3_flutter_libs: ce0522d143cee6ef5e16587acfce8f476316e005
sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
WebRTC-SDK: dff00a3892bc570b6014e046297782084071657e
WebRTC-SDK: d20de357dcbf7c9696b124b39f3ff62125107e4b
PODFILE CHECKSUM: 346bfb2deb41d4a6ebd6f6799f92188bde2d246f

View File

@@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11
sha256: bb84ee51e527053dd8e25ecc9f97a6abfdc19130fb4d883e4e8585e23e7e6dd8
url: "https://pub.dev"
source: hosted
version: "1.3.59"
version: "1.3.60"
analyzer:
dependency: transitive
description:
@@ -421,10 +421,10 @@ packages:
dependency: transitive
description:
name: dart_webrtc
sha256: "5b76fd85ac95d6f5dee3e7d7de8d4b51bfbec1dc73804647c6aebb52d6297116"
sha256: a2ae542cdadc21359022adedc26138fa3487cc3b3547c24ff4f556681869e28c
url: "https://pub.dev"
source: hosted
version: "1.5.3+hotfix.2"
version: "1.5.3+hotfix.4"
dbus:
dependency: transitive
description:
@@ -477,10 +477,10 @@ packages:
dependency: "direct main"
description:
name: drift
sha256: dce2723fb0dd03563af21f305f8f96514c27f870efba934b4fe84d4fedb4eff7
sha256: "6aaea757f53bb035e8a3baedf3d1d53a79d6549a6c13d84f7546509da9372c7c"
url: "https://pub.dev"
source: hosted
version: "2.28.0"
version: "2.28.1"
drift_dev:
dependency: "direct dev"
description:
@@ -509,10 +509,10 @@ packages:
dependency: "direct main"
description:
name: easy_localization
sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12"
sha256: "2ccdf9db8fe4d9c5a75c122e6275674508fd0f0d49c827354967b8afcc56bbed"
url: "https://pub.dev"
source: hosted
version: "3.0.7+1"
version: "3.0.8"
easy_logger:
dependency: transitive
description:
@@ -573,10 +573,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: ef9908739bdd9c476353d6adff72e88fd00c625f5b959ae23f7567bd5137db0a
sha256: "13ba4e627ef24503a465d1d61b32596ce10eb6b8903678d362a528f9939b4aa8"
url: "https://pub.dev"
source: hosted
version: "10.2.0"
version: "10.2.1"
file_selector_linux:
dependency: transitive
description:
@@ -613,10 +613,10 @@ packages:
dependency: "direct main"
description:
name: firebase_core
sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5"
sha256: "6b343e6f7b72a4f32d7ce8df8c9a28d8f54b4ac20d7c6500f3e8b3969afca457"
url: "https://pub.dev"
source: hosted
version: "3.15.2"
version: "4.0.0"
firebase_core_platform_interface:
dependency: transitive
description:
@@ -629,34 +629,34 @@ packages:
dependency: transitive
description:
name: firebase_core_web
sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37"
sha256: "5d28b14dd32282fb7ce2b22b897362453755b6b8541d491127dc72b755bb7b16"
url: "https://pub.dev"
source: hosted
version: "2.24.1"
version: "3.0.0"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc"
sha256: "10272b553a49c13a6cedfd00121047157521f82a5d3f2a1706b9dd28342cc482"
url: "https://pub.dev"
source: hosted
version: "15.2.10"
version: "16.0.0"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754"
sha256: b846a305feb3f74ee3f0aace447f65a4696bc6550bc828ecf5a84a1b77473d16
url: "https://pub.dev"
source: hosted
version: "4.6.10"
version: "4.7.0"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390"
sha256: "28714749880f7242c5fb3b1ee6c66b41f61453f02ae348b43c82957df80b87ae"
url: "https://pub.dev"
source: hosted
version: "3.10.10"
version: "4.0.0"
fixnum:
dependency: transitive
description:
@@ -830,6 +830,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_langdetect:
dependency: "direct main"
description:
name: flutter_langdetect
sha256: "93bd865c7d5723eac614744abb32234ee4f593505a293bc17ef097bd55fbdf38"
url: "https://pub.dev"
source: hosted
version: "0.0.2"
flutter_launcher_icons:
dependency: "direct dev"
description:
@@ -1017,10 +1025,10 @@ packages:
dependency: "direct main"
description:
name: flutter_webrtc
sha256: "792aa1e5838a719f29ae52c0773dbb5dd781fc33b1bf87c321b274e55ab51ad1"
sha256: "69095ba39b83da3de48286dfc0769aa8e9f10491f70058dc8d8ecc960ef7a260"
url: "https://pub.dev"
source: hosted
version: "0.14.2"
version: "1.0.0"
font_awesome_flutter:
dependency: transitive
description:
@@ -1089,10 +1097,10 @@ packages:
dependency: "direct main"
description:
name: google_fonts
sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82
sha256: df9763500dadba0155373e9cb44e202ce21bd9ed5de6bdbd05c5854e86839cb8
url: "https://pub.dev"
source: hosted
version: "6.2.1"
version: "6.3.0"
graphs:
dependency: transitive
description:
@@ -1337,10 +1345,10 @@ packages:
dependency: "direct main"
description:
name: livekit_client
sha256: "5d182f40cc9aafce60a9acf936bad8bc69010b5cbf0a949f6f27dc4390f2fcce"
sha256: b3db2d8afa8d1dbe4fd8dfc965fc9d661cb51a8d864ad199919575ce919a40fb
url: "https://pub.dev"
source: hosted
version: "2.4.9"
version: "2.5.0+hotfix.1"
local_auth:
dependency: "direct main"
description:
@@ -1381,6 +1389,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.11"
logger:
dependency: transitive
description:
name: logger
sha256: "7ad7215c15420a102ec687bb320a7312afd449bac63bfb1c60d9787c27b9767f"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
logging:
dependency: transitive
description:
@@ -1433,10 +1449,10 @@ packages:
dependency: "direct main"
description:
name: material_symbols_icons
sha256: "7c50901b39d1ad645ee25d920aed008061e1fd541a897b4ebf2c01d966dbf16b"
sha256: ef20d86fb34c2b59eb7553c4d795bb8a7ec8c890c53ffd3148c64f7adc46ae50
url: "https://pub.dev"
source: hosted
version: "4.2815.1"
version: "4.2858.1"
media_kit:
dependency: "direct main"
description:
@@ -2166,10 +2182,10 @@ packages:
dependency: transitive
description:
name: sqflite_common
sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b"
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
url: "https://pub.dev"
source: hosted
version: "2.5.5"
version: "2.5.6"
sqflite_darwin:
dependency: transitive
description:
@@ -2190,18 +2206,18 @@ packages:
dependency: transitive
description:
name: sqlite3
sha256: "608b56d594e4c8498c972c8f1507209f9fd74939971b948ddbbfbfd1c9cb3c15"
sha256: dd806fff004a0aeb01e208b858dbc649bc72104670d425a81a6dd17698535f6e
url: "https://pub.dev"
source: hosted
version: "2.7.7"
version: "2.8.0"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: "60464aa06f3f6f6fba9abd7564e315526c1fee6d6a77d6ee52a1f7f48a9107f6"
sha256: fd996da5515a73aacd0a04ae7063db5fe8df42670d974df4c3ee538c652eef2e
url: "https://pub.dev"
source: hosted
version: "0.5.37"
version: "0.5.38"
sqlparser:
dependency: transitive
description:
@@ -2556,6 +2572,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
waveform_flutter:
dependency: "direct main"
description:
name: waveform_flutter
sha256: "08c9e98d4cf119428d8b3c083ed42c11c468623eaffdf30420ae38e36662922a"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
web:
dependency: "direct main"
description:

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 3.1.0+115
version: 3.1.0+116
environment:
sdk: ^3.7.2
@@ -46,65 +46,65 @@ dependencies:
path_provider: ^2.1.5
dio: ^5.8.0+1
very_good_infinite_list: ^0.9.0
freezed_annotation: ^3.0.0
freezed_annotation: ^3.1.0
json_annotation: ^4.9.0
flutter_markdown_latex: ^0.3.4
markdown: ^7.3.0
flutter_highlight: ^0.7.0
uuid: ^4.5.1
url_launcher: ^6.3.1
google_fonts: ^6.2.1
url_launcher: ^6.3.2
google_fonts: ^6.3.0
gap: ^3.0.1
cached_network_image: ^3.4.1
web: ^1.1.1
flutter_blurhash: ^0.9.0
flutter_blurhash: ^0.9.1
media_kit: ^1.2.0
media_kit_video: ^1.3.0
media_kit_libs_video: ^1.0.6
flutter_cache_manager: ^3.4.1
flutter_platform_alert: ^0.8.0
email_validator: ^3.0.0
easy_localization: ^3.0.7+1
easy_localization: ^3.0.8
flutter_inappwebview: ^6.1.5
animations: ^2.0.11
package_info_plus: ^8.3.0
device_info_plus: ^11.4.0
device_info_plus: ^11.5.0
tus_client_dart:
git: https://github.com/LittleSheep2Code/tus_client.git
cross_file: ^0.3.4+2
image_picker: ^1.1.2
file_picker: ^10.1.7
file_picker: ^10.2.1
riverpod_annotation: ^2.6.1
image_picker_platform_interface: ^2.10.1
image_picker_android: ^0.8.12+23
super_context_menu: ^0.9.0-dev.6
image_picker_android: ^0.8.12+24
super_context_menu: ^0.9.1
modal_bottom_sheet: ^3.0.0
firebase_messaging: ^15.2.5
firebase_messaging: ^16.0.0
flutter_udid: ^4.0.0
firebase_core: ^3.13.0
firebase_core: ^4.0.0
web_socket_channel: ^3.0.3
material_symbols_icons: ^4.2815.0
drift: ^2.26.0
drift_flutter: ^0.2.4
material_symbols_icons: ^4.2858.1
drift: ^2.28.1
drift_flutter: ^0.2.5
path: ^1.9.1
collection: ^1.19.1
markdown_editor_plus: ^0.2.15
croppy: ^1.3.6
table_calendar: ^3.1.3
table_calendar: ^3.2.0
relative_time: ^5.0.0
dropdown_button2: ^2.3.9
riverpod_paging_utils: ^0.8.0
riverpod_paging_utils: ^0.8.1
crypto: ^3.0.6
avatar_stack: ^3.0.0
markdown_widget: ^2.3.2+8
visibility_detector: ^0.4.0+2
flutter_native_splash: ^2.4.6
photo_view: ^0.15.0
gal: ^2.3.1
gal: ^2.3.2
dismissible_page: ^1.0.2
super_sliver_list: ^0.4.1
flutter_webrtc: ^0.14.1
livekit_client: ^2.4.7
flutter_webrtc: ^1.0.0
livekit_client: ^2.5.0+hotfix.1
pasteboard: ^0.4.0
flutter_colorpicker: ^1.1.0
record: ^6.0.0
@@ -116,7 +116,7 @@ dependencies:
flutter_timezone: ^4.1.1
fl_chart: ^1.0.0
sign_in_with_apple: ^7.0.1
flutter_svg: ^2.1.0
flutter_svg: ^2.2.0
native_exif: ^0.6.2
local_auth: ^2.3.0
flutter_secure_storage: ^9.2.4
@@ -131,6 +131,8 @@ dependencies:
mime: ^2.0.0
html2md: ^1.3.2
flutter_typeahead: ^5.2.0
flutter_langdetect: ^0.0.2
waveform_flutter: ^1.2.0
dev_dependencies:
flutter_test:
@@ -142,15 +144,15 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
auto_route_generator: ^10.0.1
build_runner: ^2.4.15
freezed: ^3.0.6
auto_route_generator: ^10.1.0
build_runner: ^2.5.4
freezed: ^3.1.0
json_serializable: ^6.9.5
riverpod_generator: ^2.6.5
custom_lint: ^0.7.5
custom_lint: ^0.7.6
riverpod_lint: ^2.6.5
drift_dev: ^2.26.0
flutter_launcher_icons: ^0.14.3
drift_dev: ^2.28.0
flutter_launcher_icons: ^0.14.4
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec