Compare commits

...

18 Commits

Author SHA1 Message Date
360572d7d1 🚀 Launch 3.0.0+95 2025-05-28 02:02:17 +08:00
9c1a983466 Reactive notification unread count 2025-05-28 01:52:44 +08:00
bfa97dcd11 💄 Optimized compose page 2025-05-28 01:13:49 +08:00
aaa505e83e Accoun settings 2025-05-28 01:08:18 +08:00
552bdfa58f 🐛 Fix publisher page renders error when publisher name didn't match username 2025-05-27 23:03:56 +08:00
ebde4eeed5 Reset password 2025-05-27 22:41:27 +08:00
688f035f85 Better chat overlay 2025-05-27 02:35:08 +08:00
fd587270da 🚀 Launch 3.0.0+94 2025-05-26 02:00:15 +08:00
6b4189a93b :drunk: Add call overlay 2025-05-26 01:52:08 +08:00
fed8171b36 🐛 Fix call messages 2025-05-26 01:44:50 +08:00
f39a066f71 Realtime call 2025-05-26 01:42:59 +08:00
edf4ff1c5b 🧱 Realtime call infra 2025-05-25 17:40:52 +08:00
9abc61a310 🐛 Fix post listing with pubName not filtered correctly 2025-05-25 12:22:56 +08:00
205d52df46 Add flutter webrtc
⬇️ Downgrade analyzer & custom lint for fix flutter new version related bugs
2025-05-25 01:25:19 +08:00
a2053aa772 🐛 Fixes and optimzation on post
♻️ Replace freezed abstract classes with sealed
2025-05-24 22:38:07 +08:00
81103ba8b6 🗑️ Remove some settings 2025-05-24 22:21:33 +08:00
76faa4172f 🍱 Update product name 2025-05-24 17:35:50 +08:00
9866c22746 🔨 Update iOS/macOS building details 2025-05-24 03:10:31 +08:00
77 changed files with 5035 additions and 1313 deletions

View File

@ -278,5 +278,35 @@
"settingsHideBottomNav": "Hide Bottom Navigation",
"settingsSoundEffects": "Sound Effects",
"settingsAprilFoolFeatures": "April Fool Features",
"settingsEnterToSend": "Enter to Send"
"settingsEnterToSend": "Enter to Send",
"postTitle": "Title",
"postDescription": "Description",
"call": "Call",
"done": "Done",
"loginResetPasswordSent": "Password reset link sent, please check your email inbox.",
"accountDeletion": "Delete Account",
"accountDeletionHint": "Are you sure to delete your account? If you confirmed, we will send an confirmation email to your primary email address, you can continue the deletion process by follow the insturctions in the email.",
"accountDeletionSent": "Account deletion confirmation email sent, please check your email inbox.",
"accountSecurityTitle": "Security",
"accountPrivacyTitle": "Privacy",
"accountDangerZoneTitle": "Danger Zone",
"accountPassword": "Password",
"accountPasswordDescription": "Change your account password",
"accountPasswordChange": "Change Password",
"accountPasswordChangeSent": "Password reset link sent, please check your email inbox.",
"accountPasswordChangeDescription": "We will send an email to your primary email address to reset your password.",
"accountTwoFactor": "Two-Factor Authentication",
"accountTwoFactorDescription": "Add an extra layer of security to your account",
"accountTwoFactorSetup": "Set Up 2FA",
"accountTwoFactorSetupDescription": "Two-factor authentication adds an additional layer of security to your account by requiring more than just a password to sign in.",
"accountPrivacy": "Privacy Settings",
"accountPrivacyDescription": "Control who can see your profile and content",
"accountDataExport": "Export Your Data",
"accountDataExportDescription": "Download a copy of your data",
"accountDataExportConfirmation": "We'll prepare an export of your data which may take some time. You'll receive an email when it's ready to download.",
"accountDataExportConfirm": "Request Export",
"accountDataExportRequested": "Data export requested. You'll receive an email when it's ready.",
"accountDeletionDescription": "Permanently delete your account and all your data",
"accountSettingsHelp": "Account Settings Help",
"accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support."
}

View File

@ -41,5 +41,9 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
# Workaround for https://github.com/flutter/flutter/issues/64502
config.build_settings['ONLY_ACTIVE_ARCH'] = 'YES' # <= this line
end
end
end

View File

@ -1,4 +1,6 @@
PODS:
- connectivity_plus (0.0.1):
- Flutter
- croppy (0.0.1):
- Flutter
- device_info_plus (0.0.1):
@ -84,6 +86,9 @@ PODS:
- flutter_udid (0.0.1):
- Flutter
- SAMKeychain
- flutter_webrtc (0.14.0):
- Flutter
- WebRTC-SDK (= 125.6422.07)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
@ -116,6 +121,10 @@ PODS:
- irondash_engine_context (0.0.1):
- Flutter
- Kingfisher (8.3.2)
- livekit_client (2.4.7):
- Flutter
- flutter_webrtc
- WebRTC-SDK (= 125.6422.07)
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_video (0.0.1):
@ -173,8 +182,10 @@ PODS:
- Flutter
- wakelock_plus (0.0.1):
- Flutter
- WebRTC-SDK (125.6422.07)
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
@ -185,9 +196,11 @@ DEPENDENCIES:
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- Kingfisher (~> 8.0)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
@ -219,8 +232,11 @@ SPEC REPOS:
- SDWebImage
- sqlite3
- SwiftyGif
- WebRTC-SDK
EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
croppy:
:path: ".symlinks/plugins/croppy/ios"
device_info_plus:
@ -241,10 +257,14 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_platform_alert/ios"
flutter_udid:
:path: ".symlinks/plugins/flutter_udid/ios"
flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
irondash_engine_context:
:path: ".symlinks/plugins/irondash_engine_context/ios"
livekit_client:
:path: ".symlinks/plugins/livekit_client/ios"
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_video:
@ -269,6 +289,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
@ -286,11 +307,13 @@ SPEC CHECKSUMS:
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
Kingfisher: 0621d0ac0c78fecb19f6dc5303bde2b52abaf2f5
livekit_client: c30950bf36aa4c0244dd5551b1818cd15f90ba32
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
@ -309,7 +332,8 @@ SPEC CHECKSUMS:
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
WebRTC-SDK: dff00a3892bc570b6014e046297782084071657e
PODFILE CHECKSUM: 4c3fad73fbbc9053a824d880097bda7884240991
PODFILE CHECKSUM: 2608312fddeea6d787ca3aa478c756da8f4ca66d
COCOAPODS: 1.16.2

View File

@ -498,6 +498,7 @@
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -683,6 +684,7 @@
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -708,6 +710,7 @@
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
@ -54,6 +55,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@ -1,67 +1,68 @@
<?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>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Island</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>island</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>audio</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSCalendarsUsageDescription</key>
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
<key>NSCameraUsageDescription</key>
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
<key>UIStatusBarHidden</key>
<false/>
</dict>
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Solian</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>solian</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCalendarsUsageDescription</key>
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
<key>NSCameraUsageDescription</key>
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>audio</string>
<string>remote-notification</string>
<string>voip</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@ -85,4 +85,5 @@ class DefaultFirebaseOptions {
storageBucket: 'solian-0x001.firebasestorage.app',
measurementId: 'G-JD1YEG9D6F',
);
}
}

View File

@ -19,6 +19,7 @@ import 'package:island/pods/websocket.dart';
import 'package:island/route.dart';
import 'package:island/screens/auth/tabs.dart';
import 'package:island/services/notify.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:relative_time/relative_time.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -28,11 +29,21 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
void main() async {
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
log(
"[SplashScreen] Keeping the flash screen to loading other resources...",
);
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
}
await EasyLocalization.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
try {
await EasyLocalization.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
log("[SplashScreen] Firebase is ready!");
} catch (err) {
showErrorAlert(err);
}
final prefs = await SharedPreferences.getInstance();
@ -43,6 +54,7 @@ void main() async {
appWindow.size = initialSize;
appWindow.alignment = Alignment.center;
appWindow.show();
log("[SplashScreen] Desktop window is ready!");
});
}
@ -52,10 +64,12 @@ void main() async {
if (imagePickerImplementation is ImagePickerAndroid) {
imagePickerImplementation.useAndroidPhotoPicker = true;
}
log("[SplashScreen] Android image picker is ready!");
}
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
FlutterNativeSplash.remove();
log("[SplashScreen] Now hiding the splash screen...");
}
runApp(

View File

@ -5,7 +5,7 @@ part 'activity.freezed.dart';
part 'activity.g.dart';
@freezed
abstract class SnActivity with _$SnActivity {
sealed class SnActivity with _$SnActivity {
const factory SnActivity({
required String id,
required String type,
@ -24,7 +24,7 @@ abstract class SnActivity with _$SnActivity {
}
@freezed
abstract class SnCheckInResult with _$SnCheckInResult {
sealed class SnCheckInResult with _$SnCheckInResult {
const factory SnCheckInResult({
required String id,
required int level,
@ -41,7 +41,7 @@ abstract class SnCheckInResult with _$SnCheckInResult {
}
@freezed
abstract class SnFortuneTip with _$SnFortuneTip {
sealed class SnFortuneTip with _$SnFortuneTip {
const factory SnFortuneTip({
required bool isPositive,
required String title,
@ -53,7 +53,7 @@ abstract class SnFortuneTip with _$SnFortuneTip {
}
@freezed
abstract class SnEventCalendarEntry with _$SnEventCalendarEntry {
sealed class SnEventCalendarEntry with _$SnEventCalendarEntry {
const factory SnEventCalendarEntry({
required DateTime date,
required SnCheckInResult? checkInResult,

View File

@ -4,7 +4,7 @@ part 'auth.freezed.dart';
part 'auth.g.dart';
@freezed
abstract class AppTokenPair with _$AppTokenPair {
sealed class AppTokenPair with _$AppTokenPair {
const factory AppTokenPair({
required String accessToken,
required String refreshToken,
@ -15,7 +15,7 @@ abstract class AppTokenPair with _$AppTokenPair {
}
@freezed
abstract class SnAuthChallenge with _$SnAuthChallenge {
sealed class SnAuthChallenge with _$SnAuthChallenge {
const factory SnAuthChallenge({
required String id,
required DateTime expiredAt,
@ -38,7 +38,7 @@ abstract class SnAuthChallenge with _$SnAuthChallenge {
}
@freezed
abstract class SnAuthFactor with _$SnAuthFactor {
sealed class SnAuthFactor with _$SnAuthFactor {
const factory SnAuthFactor({
required String id,
required int type,

View File

@ -7,7 +7,7 @@ part 'chat.freezed.dart';
part 'chat.g.dart';
@freezed
abstract class SnChatRoom with _$SnChatRoom {
sealed class SnChatRoom with _$SnChatRoom {
const factory SnChatRoom({
required String id,
required String? name,
@ -31,12 +31,13 @@ abstract class SnChatRoom with _$SnChatRoom {
}
@freezed
abstract class SnChatMessage with _$SnChatMessage {
sealed class SnChatMessage with _$SnChatMessage {
const factory SnChatMessage({
required DateTime createdAt,
required DateTime updatedAt,
DateTime? deletedAt,
required String id,
@Default('text') String type,
String? content,
String? nonce,
@Default({}) Map<String, dynamic> meta,
@ -56,7 +57,7 @@ abstract class SnChatMessage with _$SnChatMessage {
}
@freezed
abstract class SnChatReaction with _$SnChatReaction {
sealed class SnChatReaction with _$SnChatReaction {
const factory SnChatReaction({
required DateTime createdAt,
required DateTime updatedAt,
@ -74,7 +75,7 @@ abstract class SnChatReaction with _$SnChatReaction {
}
@freezed
abstract class SnChatMember with _$SnChatMember {
sealed class SnChatMember with _$SnChatMember {
const factory SnChatMember({
required DateTime createdAt,
required DateTime updatedAt,
@ -96,7 +97,7 @@ abstract class SnChatMember with _$SnChatMember {
}
@freezed
abstract class SnChatSummary with _$SnChatSummary {
sealed class SnChatSummary with _$SnChatSummary {
const factory SnChatSummary({
required int unreadCount,
required SnChatMessage lastMessage,
@ -113,7 +114,7 @@ class MessageChangeAction {
}
@freezed
abstract class MessageChange with _$MessageChange {
sealed class MessageChange with _$MessageChange {
const factory MessageChange({
required String messageId,
required String action,
@ -126,7 +127,7 @@ abstract class MessageChange with _$MessageChange {
}
@freezed
abstract class MessageSyncResponse with _$MessageSyncResponse {
sealed class MessageSyncResponse with _$MessageSyncResponse {
const factory MessageSyncResponse({
@Default([]) List<MessageChange> changes,
required DateTime currentTimestamp,
@ -137,12 +138,52 @@ abstract class MessageSyncResponse with _$MessageSyncResponse {
}
@freezed
abstract class ChatRealtimeJoinResponse with _$ChatRealtimeJoinResponse {
sealed class ChatRealtimeJoinResponse with _$ChatRealtimeJoinResponse {
const factory ChatRealtimeJoinResponse({
required String provider,
required String endpoint,
required String token,
required Map<String, dynamic> config,
required String callId,
required String roomName,
required bool isAdmin,
required List<CallParticipant> participants,
}) = _ChatRealtimeJoinResponse;
factory ChatRealtimeJoinResponse.fromJson(Map<String, dynamic> json) =>
_$ChatRealtimeJoinResponseFromJson(json);
}
@freezed
sealed class CallParticipant with _$CallParticipant {
const factory CallParticipant({
required String identity,
required String name,
required DateTime joinedAt,
required String? accountId,
required SnChatMember? profile,
}) = _CallParticipant;
factory CallParticipant.fromJson(Map<String, dynamic> json) =>
_$CallParticipantFromJson(json);
}
@freezed
sealed class SnRealtimeCall with _$SnRealtimeCall {
const factory SnRealtimeCall({
required String id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required DateTime? endedAt,
required String senderId,
required SnChatMember sender,
required String roomId,
required SnChatRoom room,
required Map<String, dynamic> upstreamConfig,
String? providerName,
String? sessionId,
}) = _SnRealtimeCall;
factory SnRealtimeCall.fromJson(Map<String, dynamic> json) =>
_$SnRealtimeCallFromJson(json);
}

View File

@ -271,7 +271,7 @@ $SnRealmCopyWith<$Res>? get realm {
/// @nodoc
mixin _$SnChatMessage {
DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String? get content; String? get nonce; Map<String, dynamic> get meta; List<String> get membersMetioned; DateTime? get editedAt; List<SnCloudFile> get attachments; List<SnChatReaction> get reactions; String? get repliedMessageId; String? get forwardedMessageId; String get senderId; SnChatMember get sender; String get chatRoomId;
DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get type; String? get content; String? get nonce; Map<String, dynamic> get meta; List<String> get membersMetioned; DateTime? get editedAt; List<SnCloudFile> get attachments; List<SnChatReaction> get reactions; String? get repliedMessageId; String? get forwardedMessageId; String get senderId; SnChatMember get sender; String get chatRoomId;
/// Create a copy of SnChatMessage
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -284,16 +284,16 @@ $SnChatMessageCopyWith<SnChatMessage> get copyWith => _$SnChatMessageCopyWithImp
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other.meta, meta)&&const DeepCollectionEquality().equals(other.membersMetioned, membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other.meta, meta)&&const DeepCollectionEquality().equals(other.membersMetioned, membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,content,nonce,const DeepCollectionEquality().hash(meta),const DeepCollectionEquality().hash(membersMetioned),editedAt,const DeepCollectionEquality().hash(attachments),const DeepCollectionEquality().hash(reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(meta),const DeepCollectionEquality().hash(membersMetioned),editedAt,const DeepCollectionEquality().hash(attachments),const DeepCollectionEquality().hash(reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
@override
String toString() {
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
}
@ -304,7 +304,7 @@ abstract mixin class $SnChatMessageCopyWith<$Res> {
factory $SnChatMessageCopyWith(SnChatMessage value, $Res Function(SnChatMessage) _then) = _$SnChatMessageCopyWithImpl;
@useResult
$Res call({
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
});
@ -321,12 +321,13 @@ class _$SnChatMessageCopyWithImpl<$Res>
/// Create a copy of SnChatMessage
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
return _then(_self.copyWith(
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
as String?,meta: null == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
@ -359,13 +360,14 @@ $SnChatMemberCopyWith<$Res> get sender {
@JsonSerializable()
class _SnChatMessage implements SnChatMessage {
const _SnChatMessage({required this.createdAt, required this.updatedAt, this.deletedAt, required this.id, this.content, this.nonce, final Map<String, dynamic> meta = const {}, final List<String> membersMetioned = const [], this.editedAt, final List<SnCloudFile> attachments = const [], final List<SnChatReaction> reactions = const [], this.repliedMessageId, this.forwardedMessageId, required this.senderId, required this.sender, required this.chatRoomId}): _meta = meta,_membersMetioned = membersMetioned,_attachments = attachments,_reactions = reactions;
const _SnChatMessage({required this.createdAt, required this.updatedAt, this.deletedAt, required this.id, this.type = 'text', this.content, this.nonce, final Map<String, dynamic> meta = const {}, final List<String> membersMetioned = const [], this.editedAt, final List<SnCloudFile> attachments = const [], final List<SnChatReaction> reactions = const [], this.repliedMessageId, this.forwardedMessageId, required this.senderId, required this.sender, required this.chatRoomId}): _meta = meta,_membersMetioned = membersMetioned,_attachments = attachments,_reactions = reactions;
factory _SnChatMessage.fromJson(Map<String, dynamic> json) => _$SnChatMessageFromJson(json);
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
@override final String id;
@override@JsonKey() final String type;
@override final String? content;
@override final String? nonce;
final Map<String, dynamic> _meta;
@ -416,16 +418,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other._meta, _meta)&&const DeepCollectionEquality().equals(other._membersMetioned, _membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other._meta, _meta)&&const DeepCollectionEquality().equals(other._membersMetioned, _membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,content,nonce,const DeepCollectionEquality().hash(_meta),const DeepCollectionEquality().hash(_membersMetioned),editedAt,const DeepCollectionEquality().hash(_attachments),const DeepCollectionEquality().hash(_reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(_meta),const DeepCollectionEquality().hash(_membersMetioned),editedAt,const DeepCollectionEquality().hash(_attachments),const DeepCollectionEquality().hash(_reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
@override
String toString() {
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
}
@ -436,7 +438,7 @@ abstract mixin class _$SnChatMessageCopyWith<$Res> implements $SnChatMessageCopy
factory _$SnChatMessageCopyWith(_SnChatMessage value, $Res Function(_SnChatMessage) _then) = __$SnChatMessageCopyWithImpl;
@override @useResult
$Res call({
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
});
@ -453,12 +455,13 @@ class __$SnChatMessageCopyWithImpl<$Res>
/// Create a copy of SnChatMessage
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
return _then(_SnChatMessage(
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
as String?,meta: null == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
@ -1339,7 +1342,7 @@ as DateTime,
/// @nodoc
mixin _$ChatRealtimeJoinResponse {
String get token; Map<String, dynamic> get config;
String get provider; String get endpoint; String get token; String get callId; String get roomName; bool get isAdmin; List<CallParticipant> get participants;
/// Create a copy of ChatRealtimeJoinResponse
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -1352,16 +1355,16 @@ $ChatRealtimeJoinResponseCopyWith<ChatRealtimeJoinResponse> get copyWith => _$Ch
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChatRealtimeJoinResponse&&(identical(other.token, token) || other.token == token)&&const DeepCollectionEquality().equals(other.config, config));
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChatRealtimeJoinResponse&&(identical(other.provider, provider) || other.provider == provider)&&(identical(other.endpoint, endpoint) || other.endpoint == endpoint)&&(identical(other.token, token) || other.token == token)&&(identical(other.callId, callId) || other.callId == callId)&&(identical(other.roomName, roomName) || other.roomName == roomName)&&(identical(other.isAdmin, isAdmin) || other.isAdmin == isAdmin)&&const DeepCollectionEquality().equals(other.participants, participants));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,token,const DeepCollectionEquality().hash(config));
int get hashCode => Object.hash(runtimeType,provider,endpoint,token,callId,roomName,isAdmin,const DeepCollectionEquality().hash(participants));
@override
String toString() {
return 'ChatRealtimeJoinResponse(token: $token, config: $config)';
return 'ChatRealtimeJoinResponse(provider: $provider, endpoint: $endpoint, token: $token, callId: $callId, roomName: $roomName, isAdmin: $isAdmin, participants: $participants)';
}
@ -1372,7 +1375,7 @@ abstract mixin class $ChatRealtimeJoinResponseCopyWith<$Res> {
factory $ChatRealtimeJoinResponseCopyWith(ChatRealtimeJoinResponse value, $Res Function(ChatRealtimeJoinResponse) _then) = _$ChatRealtimeJoinResponseCopyWithImpl;
@useResult
$Res call({
String token, Map<String, dynamic> config
String provider, String endpoint, String token, String callId, String roomName, bool isAdmin, List<CallParticipant> participants
});
@ -1389,11 +1392,16 @@ class _$ChatRealtimeJoinResponseCopyWithImpl<$Res>
/// Create a copy of ChatRealtimeJoinResponse
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? token = null,Object? config = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? provider = null,Object? endpoint = null,Object? token = null,Object? callId = null,Object? roomName = null,Object? isAdmin = null,Object? participants = null,}) {
return _then(_self.copyWith(
token: null == token ? _self.token : token // ignore: cast_nullable_to_non_nullable
as String,config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
provider: null == provider ? _self.provider : provider // ignore: cast_nullable_to_non_nullable
as String,endpoint: null == endpoint ? _self.endpoint : endpoint // ignore: cast_nullable_to_non_nullable
as String,token: null == token ? _self.token : token // ignore: cast_nullable_to_non_nullable
as String,callId: null == callId ? _self.callId : callId // ignore: cast_nullable_to_non_nullable
as String,roomName: null == roomName ? _self.roomName : roomName // ignore: cast_nullable_to_non_nullable
as String,isAdmin: null == isAdmin ? _self.isAdmin : isAdmin // ignore: cast_nullable_to_non_nullable
as bool,participants: null == participants ? _self.participants : participants // ignore: cast_nullable_to_non_nullable
as List<CallParticipant>,
));
}
@ -1404,15 +1412,20 @@ as Map<String, dynamic>,
@JsonSerializable()
class _ChatRealtimeJoinResponse implements ChatRealtimeJoinResponse {
const _ChatRealtimeJoinResponse({required this.token, required final Map<String, dynamic> config}): _config = config;
const _ChatRealtimeJoinResponse({required this.provider, required this.endpoint, required this.token, required this.callId, required this.roomName, required this.isAdmin, required final List<CallParticipant> participants}): _participants = participants;
factory _ChatRealtimeJoinResponse.fromJson(Map<String, dynamic> json) => _$ChatRealtimeJoinResponseFromJson(json);
@override final String provider;
@override final String endpoint;
@override final String token;
final Map<String, dynamic> _config;
@override Map<String, dynamic> get config {
if (_config is EqualUnmodifiableMapView) return _config;
@override final String callId;
@override final String roomName;
@override final bool isAdmin;
final List<CallParticipant> _participants;
@override List<CallParticipant> get participants {
if (_participants is EqualUnmodifiableListView) return _participants;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_config);
return EqualUnmodifiableListView(_participants);
}
@ -1429,16 +1442,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatRealtimeJoinResponse&&(identical(other.token, token) || other.token == token)&&const DeepCollectionEquality().equals(other._config, _config));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatRealtimeJoinResponse&&(identical(other.provider, provider) || other.provider == provider)&&(identical(other.endpoint, endpoint) || other.endpoint == endpoint)&&(identical(other.token, token) || other.token == token)&&(identical(other.callId, callId) || other.callId == callId)&&(identical(other.roomName, roomName) || other.roomName == roomName)&&(identical(other.isAdmin, isAdmin) || other.isAdmin == isAdmin)&&const DeepCollectionEquality().equals(other._participants, _participants));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,token,const DeepCollectionEquality().hash(_config));
int get hashCode => Object.hash(runtimeType,provider,endpoint,token,callId,roomName,isAdmin,const DeepCollectionEquality().hash(_participants));
@override
String toString() {
return 'ChatRealtimeJoinResponse(token: $token, config: $config)';
return 'ChatRealtimeJoinResponse(provider: $provider, endpoint: $endpoint, token: $token, callId: $callId, roomName: $roomName, isAdmin: $isAdmin, participants: $participants)';
}
@ -1449,7 +1462,7 @@ abstract mixin class _$ChatRealtimeJoinResponseCopyWith<$Res> implements $ChatRe
factory _$ChatRealtimeJoinResponseCopyWith(_ChatRealtimeJoinResponse value, $Res Function(_ChatRealtimeJoinResponse) _then) = __$ChatRealtimeJoinResponseCopyWithImpl;
@override @useResult
$Res call({
String token, Map<String, dynamic> config
String provider, String endpoint, String token, String callId, String roomName, bool isAdmin, List<CallParticipant> participants
});
@ -1466,15 +1479,397 @@ class __$ChatRealtimeJoinResponseCopyWithImpl<$Res>
/// Create a copy of ChatRealtimeJoinResponse
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? token = null,Object? config = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? provider = null,Object? endpoint = null,Object? token = null,Object? callId = null,Object? roomName = null,Object? isAdmin = null,Object? participants = null,}) {
return _then(_ChatRealtimeJoinResponse(
token: null == token ? _self.token : token // ignore: cast_nullable_to_non_nullable
as String,config: null == config ? _self._config : config // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
provider: null == provider ? _self.provider : provider // ignore: cast_nullable_to_non_nullable
as String,endpoint: null == endpoint ? _self.endpoint : endpoint // ignore: cast_nullable_to_non_nullable
as String,token: null == token ? _self.token : token // ignore: cast_nullable_to_non_nullable
as String,callId: null == callId ? _self.callId : callId // ignore: cast_nullable_to_non_nullable
as String,roomName: null == roomName ? _self.roomName : roomName // ignore: cast_nullable_to_non_nullable
as String,isAdmin: null == isAdmin ? _self.isAdmin : isAdmin // ignore: cast_nullable_to_non_nullable
as bool,participants: null == participants ? _self._participants : participants // ignore: cast_nullable_to_non_nullable
as List<CallParticipant>,
));
}
}
/// @nodoc
mixin _$CallParticipant {
String get identity; String get name; DateTime get joinedAt; String? get accountId; SnChatMember? get profile;
/// Create a copy of CallParticipant
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$CallParticipantCopyWith<CallParticipant> get copyWith => _$CallParticipantCopyWithImpl<CallParticipant>(this as CallParticipant, _$identity);
/// Serializes this CallParticipant to a JSON map.
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));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,identity,name,joinedAt,accountId,profile);
@override
String toString() {
return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt, accountId: $accountId, profile: $profile)';
}
}
/// @nodoc
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
});
$SnChatMemberCopyWith<$Res>? get profile;
}
/// @nodoc
class _$CallParticipantCopyWithImpl<$Res>
implements $CallParticipantCopyWith<$Res> {
_$CallParticipantCopyWithImpl(this._self, this._then);
final CallParticipant _self;
final $Res Function(CallParticipant) _then;
/// 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,}) {
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?,
));
}
/// 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));
});
}
}
/// @nodoc
@JsonSerializable()
class _CallParticipant implements CallParticipant {
const _CallParticipant({required this.identity, required this.name, required this.joinedAt, required this.accountId, required this.profile});
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.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$CallParticipantCopyWith<_CallParticipant> get copyWith => __$CallParticipantCopyWithImpl<_CallParticipant>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$CallParticipantToJson(this, );
}
@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));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,identity,name,joinedAt,accountId,profile);
@override
String toString() {
return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt, accountId: $accountId, profile: $profile)';
}
}
/// @nodoc
abstract mixin class _$CallParticipantCopyWith<$Res> implements $CallParticipantCopyWith<$Res> {
factory _$CallParticipantCopyWith(_CallParticipant value, $Res Function(_CallParticipant) _then) = __$CallParticipantCopyWithImpl;
@override @useResult
$Res call({
String identity, String name, DateTime joinedAt, String? accountId, SnChatMember? profile
});
@override $SnChatMemberCopyWith<$Res>? get profile;
}
/// @nodoc
class __$CallParticipantCopyWithImpl<$Res>
implements _$CallParticipantCopyWith<$Res> {
__$CallParticipantCopyWithImpl(this._self, this._then);
final _CallParticipant _self;
final $Res Function(_CallParticipant) _then;
/// 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,}) {
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?,
));
}
/// 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));
});
}
}
/// @nodoc
mixin _$SnRealtimeCall {
String get id; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; DateTime? get endedAt; String get senderId; SnChatMember get sender; String get roomId; SnChatRoom get room; Map<String, dynamic> get upstreamConfig; String? get providerName; String? get sessionId;
/// Create a copy of SnRealtimeCall
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnRealtimeCallCopyWith<SnRealtimeCall> get copyWith => _$SnRealtimeCallCopyWithImpl<SnRealtimeCall>(this as SnRealtimeCall, _$identity);
/// Serializes this SnRealtimeCall to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnRealtimeCall&&(identical(other.id, id) || other.id == id)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.roomId, roomId) || other.roomId == roomId)&&(identical(other.room, room) || other.room == room)&&const DeepCollectionEquality().equals(other.upstreamConfig, upstreamConfig)&&(identical(other.providerName, providerName) || other.providerName == providerName)&&(identical(other.sessionId, sessionId) || other.sessionId == sessionId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,createdAt,updatedAt,deletedAt,endedAt,senderId,sender,roomId,room,const DeepCollectionEquality().hash(upstreamConfig),providerName,sessionId);
@override
String toString() {
return 'SnRealtimeCall(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, endedAt: $endedAt, senderId: $senderId, sender: $sender, roomId: $roomId, room: $room, upstreamConfig: $upstreamConfig, providerName: $providerName, sessionId: $sessionId)';
}
}
/// @nodoc
abstract mixin class $SnRealtimeCallCopyWith<$Res> {
factory $SnRealtimeCallCopyWith(SnRealtimeCall value, $Res Function(SnRealtimeCall) _then) = _$SnRealtimeCallCopyWithImpl;
@useResult
$Res call({
String id, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, DateTime? endedAt, String senderId, SnChatMember sender, String roomId, SnChatRoom room, Map<String, dynamic> upstreamConfig, String? providerName, String? sessionId
});
$SnChatMemberCopyWith<$Res> get sender;$SnChatRoomCopyWith<$Res> get room;
}
/// @nodoc
class _$SnRealtimeCallCopyWithImpl<$Res>
implements $SnRealtimeCallCopyWith<$Res> {
_$SnRealtimeCallCopyWithImpl(this._self, this._then);
final SnRealtimeCall _self;
final $Res Function(SnRealtimeCall) _then;
/// Create a copy of SnRealtimeCall
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? endedAt = freezed,Object? senderId = null,Object? sender = null,Object? roomId = null,Object? room = null,Object? upstreamConfig = null,Object? providerName = freezed,Object? sessionId = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,endedAt: freezed == endedAt ? _self.endedAt : endedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,senderId: null == senderId ? _self.senderId : senderId // ignore: cast_nullable_to_non_nullable
as String,sender: null == sender ? _self.sender : sender // ignore: cast_nullable_to_non_nullable
as SnChatMember,roomId: null == roomId ? _self.roomId : roomId // ignore: cast_nullable_to_non_nullable
as String,room: null == room ? _self.room : room // ignore: cast_nullable_to_non_nullable
as SnChatRoom,upstreamConfig: null == upstreamConfig ? _self.upstreamConfig : upstreamConfig // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,providerName: freezed == providerName ? _self.providerName : providerName // ignore: cast_nullable_to_non_nullable
as String?,sessionId: freezed == sessionId ? _self.sessionId : sessionId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
/// Create a copy of SnRealtimeCall
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnChatMemberCopyWith<$Res> get sender {
return $SnChatMemberCopyWith<$Res>(_self.sender, (value) {
return _then(_self.copyWith(sender: value));
});
}/// Create a copy of SnRealtimeCall
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnChatRoomCopyWith<$Res> get room {
return $SnChatRoomCopyWith<$Res>(_self.room, (value) {
return _then(_self.copyWith(room: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnRealtimeCall implements SnRealtimeCall {
const _SnRealtimeCall({required this.id, required this.createdAt, required this.updatedAt, required this.deletedAt, required this.endedAt, required this.senderId, required this.sender, required this.roomId, required this.room, required final Map<String, dynamic> upstreamConfig, this.providerName, this.sessionId}): _upstreamConfig = upstreamConfig;
factory _SnRealtimeCall.fromJson(Map<String, dynamic> json) => _$SnRealtimeCallFromJson(json);
@override final String id;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
@override final DateTime? endedAt;
@override final String senderId;
@override final SnChatMember sender;
@override final String roomId;
@override final SnChatRoom room;
final Map<String, dynamic> _upstreamConfig;
@override Map<String, dynamic> get upstreamConfig {
if (_upstreamConfig is EqualUnmodifiableMapView) return _upstreamConfig;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_upstreamConfig);
}
@override final String? providerName;
@override final String? sessionId;
/// Create a copy of SnRealtimeCall
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnRealtimeCallCopyWith<_SnRealtimeCall> get copyWith => __$SnRealtimeCallCopyWithImpl<_SnRealtimeCall>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnRealtimeCallToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnRealtimeCall&&(identical(other.id, id) || other.id == id)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.roomId, roomId) || other.roomId == roomId)&&(identical(other.room, room) || other.room == room)&&const DeepCollectionEquality().equals(other._upstreamConfig, _upstreamConfig)&&(identical(other.providerName, providerName) || other.providerName == providerName)&&(identical(other.sessionId, sessionId) || other.sessionId == sessionId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,createdAt,updatedAt,deletedAt,endedAt,senderId,sender,roomId,room,const DeepCollectionEquality().hash(_upstreamConfig),providerName,sessionId);
@override
String toString() {
return 'SnRealtimeCall(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, endedAt: $endedAt, senderId: $senderId, sender: $sender, roomId: $roomId, room: $room, upstreamConfig: $upstreamConfig, providerName: $providerName, sessionId: $sessionId)';
}
}
/// @nodoc
abstract mixin class _$SnRealtimeCallCopyWith<$Res> implements $SnRealtimeCallCopyWith<$Res> {
factory _$SnRealtimeCallCopyWith(_SnRealtimeCall value, $Res Function(_SnRealtimeCall) _then) = __$SnRealtimeCallCopyWithImpl;
@override @useResult
$Res call({
String id, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, DateTime? endedAt, String senderId, SnChatMember sender, String roomId, SnChatRoom room, Map<String, dynamic> upstreamConfig, String? providerName, String? sessionId
});
@override $SnChatMemberCopyWith<$Res> get sender;@override $SnChatRoomCopyWith<$Res> get room;
}
/// @nodoc
class __$SnRealtimeCallCopyWithImpl<$Res>
implements _$SnRealtimeCallCopyWith<$Res> {
__$SnRealtimeCallCopyWithImpl(this._self, this._then);
final _SnRealtimeCall _self;
final $Res Function(_SnRealtimeCall) _then;
/// Create a copy of SnRealtimeCall
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? endedAt = freezed,Object? senderId = null,Object? sender = null,Object? roomId = null,Object? room = null,Object? upstreamConfig = null,Object? providerName = freezed,Object? sessionId = freezed,}) {
return _then(_SnRealtimeCall(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,endedAt: freezed == endedAt ? _self.endedAt : endedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,senderId: null == senderId ? _self.senderId : senderId // ignore: cast_nullable_to_non_nullable
as String,sender: null == sender ? _self.sender : sender // ignore: cast_nullable_to_non_nullable
as SnChatMember,roomId: null == roomId ? _self.roomId : roomId // ignore: cast_nullable_to_non_nullable
as String,room: null == room ? _self.room : room // ignore: cast_nullable_to_non_nullable
as SnChatRoom,upstreamConfig: null == upstreamConfig ? _self._upstreamConfig : upstreamConfig // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,providerName: freezed == providerName ? _self.providerName : providerName // ignore: cast_nullable_to_non_nullable
as String?,sessionId: freezed == sessionId ? _self.sessionId : sessionId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
/// Create a copy of SnRealtimeCall
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnChatMemberCopyWith<$Res> get sender {
return $SnChatMemberCopyWith<$Res>(_self.sender, (value) {
return _then(_self.copyWith(sender: value));
});
}/// Create a copy of SnRealtimeCall
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnChatRoomCopyWith<$Res> get room {
return $SnChatRoomCopyWith<$Res>(_self.room, (value) {
return _then(_self.copyWith(room: value));
});
}
}
// dart format on

View File

@ -67,6 +67,7 @@ _SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) =>
? null
: DateTime.parse(json['deleted_at'] as String),
id: json['id'] as String,
type: json['type'] as String? ?? 'text',
content: json['content'] as String?,
nonce: json['nonce'] as String?,
meta: json['meta'] as Map<String, dynamic>? ?? const {},
@ -102,6 +103,7 @@ Map<String, dynamic> _$SnChatMessageToJson(_SnChatMessage instance) =>
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'id': instance.id,
'type': instance.type,
'content': instance.content,
'nonce': instance.nonce,
'meta': instance.meta,
@ -241,10 +243,85 @@ Map<String, dynamic> _$MessageSyncResponseToJson(
_ChatRealtimeJoinResponse _$ChatRealtimeJoinResponseFromJson(
Map<String, dynamic> json,
) => _ChatRealtimeJoinResponse(
provider: json['provider'] as String,
endpoint: json['endpoint'] as String,
token: json['token'] as String,
config: json['config'] as Map<String, dynamic>,
callId: json['call_id'] as String,
roomName: json['room_name'] as String,
isAdmin: json['is_admin'] as bool,
participants:
(json['participants'] as List<dynamic>)
.map((e) => CallParticipant.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$ChatRealtimeJoinResponseToJson(
_ChatRealtimeJoinResponse instance,
) => <String, dynamic>{'token': instance.token, 'config': instance.config};
) => <String, dynamic>{
'provider': instance.provider,
'endpoint': instance.endpoint,
'token': instance.token,
'call_id': instance.callId,
'room_name': instance.roomName,
'is_admin': instance.isAdmin,
'participants': instance.participants.map((e) => e.toJson()).toList(),
};
_CallParticipant _$CallParticipantFromJson(Map<String, dynamic> json) =>
_CallParticipant(
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) =>
<String, dynamic>{
'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) =>
_SnRealtimeCall(
id: json['id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
endedAt:
json['ended_at'] == null
? null
: DateTime.parse(json['ended_at'] as String),
senderId: json['sender_id'] as String,
sender: SnChatMember.fromJson(json['sender'] as Map<String, dynamic>),
roomId: json['room_id'] as String,
room: SnChatRoom.fromJson(json['room'] as Map<String, dynamic>),
upstreamConfig: json['upstream_config'] as Map<String, dynamic>,
providerName: json['provider_name'] as String?,
sessionId: json['session_id'] as String?,
);
Map<String, dynamic> _$SnRealtimeCallToJson(_SnRealtimeCall instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'ended_at': instance.endedAt?.toIso8601String(),
'sender_id': instance.senderId,
'sender': instance.sender.toJson(),
'room_id': instance.roomId,
'room': instance.room.toJson(),
'upstream_config': instance.upstreamConfig,
'provider_name': instance.providerName,
'session_id': instance.sessionId,
};

View File

@ -6,7 +6,7 @@ part 'file.g.dart';
enum UniversalFileType { image, video, audio, file }
@freezed
abstract class UniversalFile with _$UniversalFile {
sealed class UniversalFile with _$UniversalFile {
const UniversalFile._();
const factory UniversalFile({
@ -31,7 +31,7 @@ abstract class UniversalFile with _$UniversalFile {
}
@freezed
abstract class SnCloudFile with _$SnCloudFile {
sealed class SnCloudFile with _$SnCloudFile {
const factory SnCloudFile({
required String id,
required String name,

View File

@ -1,11 +1,12 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
import 'package:island/models/user.dart';
part 'post.freezed.dart';
part 'post.g.dart';
@freezed
abstract class SnPost with _$SnPost {
sealed class SnPost with _$SnPost {
const factory SnPost({
required String id,
required String? title,
@ -43,7 +44,7 @@ abstract class SnPost with _$SnPost {
}
@freezed
abstract class SnPublisher with _$SnPublisher {
sealed class SnPublisher with _$SnPublisher {
const factory SnPublisher({
required String id,
required int type,
@ -54,6 +55,7 @@ abstract class SnPublisher with _$SnPublisher {
required SnCloudFile? picture,
required String? backgroundId,
required SnCloudFile? background,
required SnAccount? account,
required String? accountId,
required DateTime createdAt,
required DateTime updatedAt,
@ -66,7 +68,7 @@ abstract class SnPublisher with _$SnPublisher {
}
@freezed
abstract class SnPublisherStats with _$SnPublisherStats {
sealed class SnPublisherStats with _$SnPublisherStats {
const factory SnPublisherStats({
required int postsCreated,
required int stickerPacksCreated,
@ -80,7 +82,7 @@ abstract class SnPublisherStats with _$SnPublisherStats {
}
@freezed
abstract class SnSubscriptionStatus with _$SnSubscriptionStatus {
sealed class SnSubscriptionStatus with _$SnSubscriptionStatus {
const factory SnSubscriptionStatus({
required bool isSubscribed,
required int publisherId,
@ -92,7 +94,7 @@ abstract class SnSubscriptionStatus with _$SnSubscriptionStatus {
}
@freezed
abstract class ReactInfo with _$ReactInfo {
sealed class ReactInfo with _$ReactInfo {
const factory ReactInfo({required String icon, required int attitude}) =
_ReactInfo;
}

View File

@ -370,7 +370,7 @@ $SnPublisherCopyWith<$Res> get publisher {
/// @nodoc
mixin _$SnPublisher {
String get id; int get type; String get name; String get nick; String get bio; String? get pictureId; SnCloudFile? get picture; String? get backgroundId; SnCloudFile? get background; String? get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String? get realmId;
String get id; int get type; String get name; String get nick; String get bio; String? get pictureId; SnCloudFile? get picture; String? get backgroundId; SnCloudFile? get background; SnAccount? get account; String? get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String? get realmId;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -383,16 +383,16 @@ $SnPublisherCopyWith<SnPublisher> get copyWith => _$SnPublisherCopyWithImpl<SnPu
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,pictureId,picture,backgroundId,background,accountId,createdAt,updatedAt,deletedAt,realmId);
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,pictureId,picture,backgroundId,background,account,accountId,createdAt,updatedAt,deletedAt,realmId);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId)';
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId)';
}
@ -403,11 +403,11 @@ abstract mixin class $SnPublisherCopyWith<$Res> {
factory $SnPublisherCopyWith(SnPublisher value, $Res Function(SnPublisher) _then) = _$SnPublisherCopyWithImpl;
@useResult
$Res call({
String id, int type, String name, String nick, String bio, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, String? accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? realmId
String id, int type, String name, String nick, String bio, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, SnAccount? account, String? accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? realmId
});
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnAccountCopyWith<$Res>? get account;
}
/// @nodoc
@ -420,7 +420,7 @@ class _$SnPublisherCopyWithImpl<$Res>
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? accountId = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
@ -431,7 +431,8 @@ as String,pictureId: freezed == pictureId ? _self.pictureId : pictureId // ignor
as String?,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,backgroundId: freezed == backgroundId ? _self.backgroundId : backgroundId // ignore: cast_nullable_to_non_nullable
as String?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
@ -463,6 +464,18 @@ $SnCloudFileCopyWith<$Res>? get background {
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
}
@ -471,7 +484,7 @@ $SnCloudFileCopyWith<$Res>? get background {
@JsonSerializable()
class _SnPublisher implements SnPublisher {
const _SnPublisher({required this.id, required this.type, required this.name, required this.nick, this.bio = '', required this.pictureId, required this.picture, required this.backgroundId, required this.background, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt, required this.realmId});
const _SnPublisher({required this.id, required this.type, required this.name, required this.nick, this.bio = '', required this.pictureId, required this.picture, required this.backgroundId, required this.background, required this.account, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt, required this.realmId});
factory _SnPublisher.fromJson(Map<String, dynamic> json) => _$SnPublisherFromJson(json);
@override final String id;
@ -483,6 +496,7 @@ class _SnPublisher implements SnPublisher {
@override final SnCloudFile? picture;
@override final String? backgroundId;
@override final SnCloudFile? background;
@override final SnAccount? account;
@override final String? accountId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@ -502,16 +516,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,pictureId,picture,backgroundId,background,accountId,createdAt,updatedAt,deletedAt,realmId);
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,pictureId,picture,backgroundId,background,account,accountId,createdAt,updatedAt,deletedAt,realmId);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId)';
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId)';
}
@ -522,11 +536,11 @@ abstract mixin class _$SnPublisherCopyWith<$Res> implements $SnPublisherCopyWith
factory _$SnPublisherCopyWith(_SnPublisher value, $Res Function(_SnPublisher) _then) = __$SnPublisherCopyWithImpl;
@override @useResult
$Res call({
String id, int type, String name, String nick, String bio, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, String? accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? realmId
String id, int type, String name, String nick, String bio, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, SnAccount? account, String? accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? realmId
});
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnAccountCopyWith<$Res>? get account;
}
/// @nodoc
@ -539,7 +553,7 @@ class __$SnPublisherCopyWithImpl<$Res>
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? accountId = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,}) {
return _then(_SnPublisher(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
@ -550,7 +564,8 @@ as String,pictureId: freezed == pictureId ? _self.pictureId : pictureId // ignor
as String?,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,backgroundId: freezed == backgroundId ? _self.backgroundId : backgroundId // ignore: cast_nullable_to_non_nullable
as String?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
@ -583,6 +598,18 @@ $SnCloudFileCopyWith<$Res>? get background {
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
}

View File

@ -110,6 +110,10 @@ _SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
json['background'] == null
? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
account:
json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
accountId: json['account_id'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
@ -131,6 +135,7 @@ Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
'picture': instance.picture?.toJson(),
'background_id': instance.backgroundId,
'background': instance.background?.toJson(),
'account': instance.account?.toJson(),
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),

View File

@ -6,7 +6,7 @@ part 'realm.freezed.dart';
part 'realm.g.dart';
@freezed
abstract class SnRealm with _$SnRealm {
sealed class SnRealm with _$SnRealm {
const factory SnRealm({
required String id,
required String slug,
@ -31,7 +31,7 @@ abstract class SnRealm with _$SnRealm {
}
@freezed
abstract class SnRealmMember with _$SnRealmMember {
sealed class SnRealmMember with _$SnRealmMember {
const factory SnRealmMember({
required String realmId,
required SnRealm? realm,

View File

@ -6,7 +6,7 @@ part 'relationship.freezed.dart';
part 'relationship.g.dart';
@freezed
abstract class SnRelationship with _$SnRelationship {
sealed class SnRelationship with _$SnRelationship {
const factory SnRelationship({
required DateTime? createdAt,
required DateTime? updatedAt,

View File

@ -6,7 +6,7 @@ part 'sticker.freezed.dart';
part 'sticker.g.dart';
@freezed
abstract class SnSticker with _$SnSticker {
sealed class SnSticker with _$SnSticker {
const factory SnSticker({
required String id,
required String slug,
@ -24,7 +24,7 @@ abstract class SnSticker with _$SnSticker {
}
@freezed
abstract class SnStickerPack with _$SnStickerPack {
sealed class SnStickerPack with _$SnStickerPack {
const factory SnStickerPack({
required String id,
required String name,

View File

@ -5,7 +5,7 @@ part 'user.freezed.dart';
part 'user.g.dart';
@freezed
abstract class SnAccount with _$SnAccount {
sealed class SnAccount with _$SnAccount {
const factory SnAccount({
required String id,
required String name,
@ -24,7 +24,7 @@ abstract class SnAccount with _$SnAccount {
}
@freezed
abstract class SnAccountProfile with _$SnAccountProfile {
sealed class SnAccountProfile with _$SnAccountProfile {
const factory SnAccountProfile({
required String id,
required String? firstName,
@ -48,7 +48,7 @@ abstract class SnAccountProfile with _$SnAccountProfile {
}
@freezed
abstract class SnAccountStatus with _$SnAccountStatus {
sealed class SnAccountStatus with _$SnAccountStatus {
const factory SnAccountStatus({
required String id,
required int attitude,
@ -69,7 +69,7 @@ abstract class SnAccountStatus with _$SnAccountStatus {
}
@freezed
abstract class SnAccountBadge with _$SnAccountBadge {
sealed class SnAccountBadge with _$SnAccountBadge {
const factory SnAccountBadge({
required String id,
required String type,
@ -88,7 +88,7 @@ abstract class SnAccountBadge with _$SnAccountBadge {
}
@freezed
abstract class SnNotification with _$SnNotification {
sealed class SnNotification with _$SnNotification {
const factory SnNotification({
required DateTime createdAt,
required DateTime updatedAt,

View File

@ -5,7 +5,7 @@ part 'wallet.freezed.dart';
part 'wallet.g.dart';
@freezed
abstract class SnWallet with _$SnWallet {
sealed class SnWallet with _$SnWallet {
const factory SnWallet({
required String id,
required List<SnWalletPocket> pockets,
@ -21,7 +21,7 @@ abstract class SnWallet with _$SnWallet {
}
@freezed
abstract class SnWalletPocket with _$SnWalletPocket {
sealed class SnWalletPocket with _$SnWalletPocket {
const factory SnWalletPocket({
required String id,
required String currency,
@ -37,7 +37,7 @@ abstract class SnWalletPocket with _$SnWalletPocket {
}
@freezed
abstract class SnTransaction with _$SnTransaction {
sealed class SnTransaction with _$SnTransaction {
const factory SnTransaction({
required String id,
required String currency,

352
lib/pods/call.dart Normal file
View File

@ -0,0 +1,352 @@
import 'package:island/pods/userinfo.dart';
import 'package:island/screens/chat/chat.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';
String formatDuration(Duration duration) {
String negativeSign = duration.isNegative ? '-' : '';
String twoDigits(int n) => n.toString().padLeft(2, "0");
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60).abs());
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60).abs());
return "$negativeSign${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds";
}
@freezed
sealed class CallState with _$CallState {
const factory CallState({
required bool isConnected,
required bool isMicrophoneEnabled,
required bool isCameraEnabled,
required bool isScreenSharing,
@Default(Duration(seconds: 0)) Duration duration,
String? error,
}) = _CallState;
}
@freezed
sealed class CallParticipantLive with _$CallParticipantLive {
const CallParticipantLive._();
const factory CallParticipantLive({
required CallParticipant participant,
required Participant remoteParticipant,
}) = _CallParticipantLive;
bool get isSpeaking => remoteParticipant.isSpeaking;
bool get isMuted => remoteParticipant.isMuted;
bool get isScreenSharing => remoteParticipant.isScreenShareEnabled();
bool get isScreenSharingWithAudio =>
remoteParticipant.isScreenShareAudioEnabled();
bool get hasVideo => remoteParticipant.hasVideo;
bool get hasAudio => remoteParticipant.hasAudio;
}
@riverpod
class CallNotifier extends _$CallNotifier {
Room? _room;
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;
Timer? _durationTimer;
@override
CallState build() {
// Subscribe to websocket updates
_subscribeToParticipantsUpdate();
return const CallState(
isConnected: false,
isMicrophoneEnabled: true,
isCameraEnabled: false,
isScreenSharing: false,
);
}
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();
_roomListener = _room!.createListener();
_room!.addListener(_onRoomChange);
_roomListener!
..on<ParticipantConnectedEvent>((e) {
_refreshLiveParticipants();
})
..on<RoomDisconnectedEvent>((e) {
_participants = [];
state = state.copyWith();
});
}
void _onRoomChange() {
_refreshLiveParticipants();
}
void _refreshLiveParticipants() {
if (_room == null) return;
final remoteParticipants = _room!.remoteParticipants;
_participants = [];
// Add local participant first if available
if (_localParticipant != null) {
final localInfo = _buildParticipant();
_participants.add(
CallParticipantLive(
participant: localInfo,
remoteParticipant: _localParticipant!,
),
);
}
// Add remote participants
_participants.addAll(
remoteParticipants.values.map((remote) {
final match =
_participantInfoByIdentity[remote.identity] ??
CallParticipant(
identity: remote.identity,
name: remote.identity,
joinedAt: DateTime.now(),
accountId: null,
profile: null,
);
return CallParticipantLive(
participant: match,
remoteParticipant: remote,
);
}),
);
state = state.copyWith();
}
/// Builds the CallParticipant object for the local participant.
/// Optionally, pass [participants] if you want to prioritize info from the latest list.
CallParticipant _buildParticipant({List<CallParticipant>? participants}) {
if (_localParticipant == null) {
throw StateError('No local participant available');
}
// Prefer info from the latest participants list if available
if (participants != null) {
final idx = participants.indexWhere(
(p) => p.identity == _localParticipant!.identity,
);
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,
);
}
void _updateLiveParticipants(List<CallParticipant> participants) {
// Update the info map for lookup
for (final p in participants) {
_participantInfoByIdentity[p.identity] = p;
}
if (_room == null) {
// Can't build live objects, just store empty
_participants = [];
state = state.copyWith();
return;
}
final remoteParticipants = _room!.remoteParticipants;
final remotes = remoteParticipants.values.toList();
_participants = [];
// Add local participant if present in the list
if (_localParticipant != null) {
final localInfo = _buildParticipant(participants: participants);
_participants.add(
CallParticipantLive(
participant: localInfo,
remoteParticipant: _localParticipant!,
),
);
}
// Add remote participants
_participants.addAll(
participants.map((p) {
RemoteParticipant? remote;
for (final r in remotes) {
if (r.identity == p.identity) {
remote = r;
break;
}
}
if (_localParticipant != null &&
p.identity == _localParticipant!.identity) {
return null; // Already added local
}
return remote != null
? CallParticipantLive(participant: p, remoteParticipant: remote)
: null;
}).whereType<CallParticipantLive>(),
);
state = state.copyWith();
}
String? _roomId;
String? get roomId => _roomId;
Future<void> joinRoom(String roomId) async {
_roomId = roomId;
if (_room != null) {
await _room!.disconnect();
await _room!.dispose();
_room = null;
_localParticipant = null;
_participants = [];
}
try {
final apiClient = ref.read(apiClientProvider);
final ongoingCall = await ref.read(ongoingCallProvider(roomId).future);
final response = await apiClient.get('/chat/realtime/$roomId/join');
if (response.statusCode == 200 && response.data != null) {
final data = response.data;
// Parse join response
final joinResponse = ChatRealtimeJoinResponse.fromJson(data);
final participants = joinResponse.participants;
final String endpoint = joinResponse.endpoint;
final String token = joinResponse.token;
// Setup duration timer
_durationTimer?.cancel();
_durationTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
state = state.copyWith(
duration: Duration(
milliseconds:
(DateTime.now().millisecondsSinceEpoch -
(ongoingCall?.createdAt.millisecondsSinceEpoch ?? 0)),
),
);
});
// Connect to LiveKit
_room = Room();
await _room!.connect(
endpoint,
token,
connectOptions: ConnectOptions(autoSubscribe: true),
roomOptions: RoomOptions(adaptiveStream: true, dynacast: true),
fastConnectOptions: FastConnectOptions(
microphone: TrackOption(enabled: true),
),
);
_localParticipant = _room!.localParticipant;
_initRoomListeners();
_updateLiveParticipants(participants);
// Listen for connection updates
_room!.addListener(() {
state = state.copyWith(
isConnected: _room!.connectionState == ConnectionState.connected,
isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(),
isCameraEnabled: _localParticipant!.isCameraEnabled(),
isScreenSharing: _localParticipant!.isScreenShareEnabled(),
);
});
state = state.copyWith(isConnected: true);
} else {
state = state.copyWith(error: 'Failed to join room');
}
} catch (e) {
state = state.copyWith(error: e.toString());
}
}
Future<void> toggleMicrophone() async {
if (_localParticipant != null) {
const autostop = true;
final target = !_localParticipant!.isMicrophoneEnabled();
state = state.copyWith(isMicrophoneEnabled: target);
if (target) {
await _localParticipant!.audioTrackPublications.firstOrNull?.unmute(
stopOnMute: autostop,
);
} else {
await _localParticipant!.audioTrackPublications.firstOrNull?.mute(
stopOnMute: autostop,
);
}
}
}
Future<void> toggleCamera() async {
if (_localParticipant != null) {
final target = !_localParticipant!.isCameraEnabled();
state = state.copyWith(isCameraEnabled: target);
await _localParticipant!.setCameraEnabled(target);
}
}
Future<void> toggleScreenShare() async {
if (_localParticipant != null) {
final target = !_localParticipant!.isScreenShareEnabled();
state = state.copyWith(isScreenSharing: target);
await _localParticipant!.setScreenShareEnabled(target);
}
}
Future<void> disconnect() async {
if (_room != null) {
await _room!.disconnect();
state = state.copyWith(
isConnected: false,
isMicrophoneEnabled: false,
isCameraEnabled: false,
isScreenSharing: false,
);
}
}
void dispose() {
_wsSubscription?.cancel();
_roomListener?.dispose();
_room?.removeListener(_onRoomChange);
_room?.dispose();
_durationTimer?.cancel();
}
}

305
lib/pods/call.freezed.dart Normal file
View File

@ -0,0 +1,305 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'call.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$CallState {
bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; 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)
@pragma('vm:prefer-inline')
$CallStateCopyWith<CallState> get copyWith => _$CallStateCopyWithImpl<CallState>(this as CallState, _$identity);
@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));
}
@override
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,duration,error);
@override
String toString() {
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)';
}
}
/// @nodoc
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
});
}
/// @nodoc
class _$CallStateCopyWithImpl<$Res>
implements $CallStateCopyWith<$Res> {
_$CallStateCopyWithImpl(this._self, this._then);
final CallState _self;
final $Res Function(CallState) _then;
/// 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,}) {
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,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?,
));
}
}
/// @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});
@override final bool isConnected;
@override final bool isMicrophoneEnabled;
@override final bool isCameraEnabled;
@override final bool isScreenSharing;
@override@JsonKey() final Duration duration;
@override final String? error;
/// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$CallStateCopyWith<_CallState> get copyWith => __$CallStateCopyWithImpl<_CallState>(this, _$identity);
@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));
}
@override
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,duration,error);
@override
String toString() {
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)';
}
}
/// @nodoc
abstract mixin class _$CallStateCopyWith<$Res> implements $CallStateCopyWith<$Res> {
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
});
}
/// @nodoc
class __$CallStateCopyWithImpl<$Res>
implements _$CallStateCopyWith<$Res> {
__$CallStateCopyWithImpl(this._self, this._then);
final _CallState _self;
final $Res Function(_CallState) _then;
/// 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,}) {
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,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?,
));
}
}
/// @nodoc
mixin _$CallParticipantLive {
CallParticipant get participant; Participant get remoteParticipant;
/// Create a copy of CallParticipantLive
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$CallParticipantLiveCopyWith<CallParticipantLive> get copyWith => _$CallParticipantLiveCopyWithImpl<CallParticipantLive>(this as CallParticipantLive, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipantLive&&(identical(other.participant, participant) || other.participant == participant)&&(identical(other.remoteParticipant, remoteParticipant) || other.remoteParticipant == remoteParticipant));
}
@override
int get hashCode => Object.hash(runtimeType,participant,remoteParticipant);
@override
String toString() {
return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)';
}
}
/// @nodoc
abstract mixin class $CallParticipantLiveCopyWith<$Res> {
factory $CallParticipantLiveCopyWith(CallParticipantLive value, $Res Function(CallParticipantLive) _then) = _$CallParticipantLiveCopyWithImpl;
@useResult
$Res call({
CallParticipant participant, Participant remoteParticipant
});
$CallParticipantCopyWith<$Res> get participant;
}
/// @nodoc
class _$CallParticipantLiveCopyWithImpl<$Res>
implements $CallParticipantLiveCopyWith<$Res> {
_$CallParticipantLiveCopyWithImpl(this._self, this._then);
final CallParticipantLive _self;
final $Res Function(CallParticipantLive) _then;
/// Create a copy of CallParticipantLive
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? participant = null,Object? remoteParticipant = null,}) {
return _then(_self.copyWith(
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
as Participant,
));
}
/// Create a copy of CallParticipantLive
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$CallParticipantCopyWith<$Res> get participant {
return $CallParticipantCopyWith<$Res>(_self.participant, (value) {
return _then(_self.copyWith(participant: value));
});
}
}
/// @nodoc
class _CallParticipantLive extends CallParticipantLive {
const _CallParticipantLive({required this.participant, required this.remoteParticipant}): super._();
@override final CallParticipant participant;
@override final Participant remoteParticipant;
/// Create a copy of CallParticipantLive
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$CallParticipantLiveCopyWith<_CallParticipantLive> get copyWith => __$CallParticipantLiveCopyWithImpl<_CallParticipantLive>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipantLive&&(identical(other.participant, participant) || other.participant == participant)&&(identical(other.remoteParticipant, remoteParticipant) || other.remoteParticipant == remoteParticipant));
}
@override
int get hashCode => Object.hash(runtimeType,participant,remoteParticipant);
@override
String toString() {
return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)';
}
}
/// @nodoc
abstract mixin class _$CallParticipantLiveCopyWith<$Res> implements $CallParticipantLiveCopyWith<$Res> {
factory _$CallParticipantLiveCopyWith(_CallParticipantLive value, $Res Function(_CallParticipantLive) _then) = __$CallParticipantLiveCopyWithImpl;
@override @useResult
$Res call({
CallParticipant participant, Participant remoteParticipant
});
@override $CallParticipantCopyWith<$Res> get participant;
}
/// @nodoc
class __$CallParticipantLiveCopyWithImpl<$Res>
implements _$CallParticipantLiveCopyWith<$Res> {
__$CallParticipantLiveCopyWithImpl(this._self, this._then);
final _CallParticipantLive _self;
final $Res Function(_CallParticipantLive) _then;
/// Create a copy of CallParticipantLive
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? participant = null,Object? remoteParticipant = null,}) {
return _then(_CallParticipantLive(
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
as Participant,
));
}
/// Create a copy of CallParticipantLive
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$CallParticipantCopyWith<$Res> get participant {
return $CallParticipantCopyWith<$Res>(_self.participant, (value) {
return _then(_self.copyWith(participant: value));
});
}
}
// dart format on

27
lib/pods/call.g.dart Normal file
View File

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'call.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$callNotifierHash() => r'2082a572b5cfb4bf929dc1ed492c52cd2735452e';
/// See also [CallNotifier].
@ProviderFor(CallNotifier)
final callNotifierProvider =
AutoDisposeNotifierProvider<CallNotifier, CallState>.internal(
CallNotifier.new,
name: r'callNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$callNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$CallNotifier = AutoDisposeNotifier<CallState>;
// 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

@ -51,12 +51,9 @@ final serverUrlProvider = Provider<String>((ref) {
});
@freezed
abstract class AppSettings with _$AppSettings {
sealed class AppSettings with _$AppSettings {
const factory AppSettings({
required bool realmCompactView,
required bool mixedFeed,
required bool autoTranslate,
required bool hideBottomNav,
required bool soundEffects,
required bool aprilFoolFeatures,
required bool enterToSend,
@ -69,36 +66,18 @@ class AppSettingsNotifier extends StateNotifier<AppSettings> {
AppSettingsNotifier(this.prefs)
: super(
AppSettings(
realmCompactView: prefs.getBool(kAppRealmCompactView) ?? false,
mixedFeed: prefs.getBool(kAppMixedFeed) ?? true,
autoTranslate: prefs.getBool(kAppAutoTranslate) ?? false,
hideBottomNav: prefs.getBool(kAppHideBottomNav) ?? false,
soundEffects: prefs.getBool(kAppSoundEffects) ?? true,
aprilFoolFeatures: prefs.getBool(kAppAprilFoolFeatures) ?? true,
enterToSend: prefs.getBool(kAppEnterToSend) ?? true,
),
);
void setRealmCompactView(bool value) {
prefs.setBool(kAppRealmCompactView, value);
state = state.copyWith(realmCompactView: value);
}
void setMixedFeed(bool value) {
prefs.setBool(kAppMixedFeed, value);
state = state.copyWith(mixedFeed: value);
}
void setAutoTranslate(bool value) {
prefs.setBool(kAppAutoTranslate, value);
state = state.copyWith(autoTranslate: value);
}
void setHideBottomNav(bool value) {
prefs.setBool(kAppHideBottomNav, value);
state = state.copyWith(hideBottomNav: value);
}
void setSoundEffects(bool value) {
prefs.setBool(kAppSoundEffects, value);
state = state.copyWith(soundEffects: value);

View File

@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AppSettings {
bool get realmCompactView; bool get mixedFeed; bool get autoTranslate; bool get hideBottomNav; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend;
bool get autoTranslate; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend;
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -26,16 +26,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.realmCompactView, realmCompactView) || other.realmCompactView == realmCompactView)&&(identical(other.mixedFeed, mixedFeed) || other.mixedFeed == mixedFeed)&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.hideBottomNav, hideBottomNav) || other.hideBottomNav == hideBottomNav)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend));
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend));
}
@override
int get hashCode => Object.hash(runtimeType,realmCompactView,mixedFeed,autoTranslate,hideBottomNav,soundEffects,aprilFoolFeatures,enterToSend);
int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend);
@override
String toString() {
return 'AppSettings(realmCompactView: $realmCompactView, mixedFeed: $mixedFeed, autoTranslate: $autoTranslate, hideBottomNav: $hideBottomNav, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend)';
return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend)';
}
@ -46,7 +46,7 @@ abstract mixin class $AppSettingsCopyWith<$Res> {
factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
@useResult
$Res call({
bool realmCompactView, bool mixedFeed, bool autoTranslate, bool hideBottomNav, bool soundEffects, bool aprilFoolFeatures, bool enterToSend
bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend
});
@ -63,12 +63,9 @@ class _$AppSettingsCopyWithImpl<$Res>
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? realmCompactView = null,Object? mixedFeed = null,Object? autoTranslate = null,Object? hideBottomNav = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,}) {
return _then(_self.copyWith(
realmCompactView: null == realmCompactView ? _self.realmCompactView : realmCompactView // ignore: cast_nullable_to_non_nullable
as bool,mixedFeed: null == mixedFeed ? _self.mixedFeed : mixedFeed // ignore: cast_nullable_to_non_nullable
as bool,autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
as bool,hideBottomNav: null == hideBottomNav ? _self.hideBottomNav : hideBottomNav // ignore: cast_nullable_to_non_nullable
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
as bool,soundEffects: null == soundEffects ? _self.soundEffects : soundEffects // ignore: cast_nullable_to_non_nullable
as bool,aprilFoolFeatures: null == aprilFoolFeatures ? _self.aprilFoolFeatures : aprilFoolFeatures // ignore: cast_nullable_to_non_nullable
as bool,enterToSend: null == enterToSend ? _self.enterToSend : enterToSend // ignore: cast_nullable_to_non_nullable
@ -83,13 +80,10 @@ as bool,
class _AppSettings implements AppSettings {
const _AppSettings({required this.realmCompactView, required this.mixedFeed, required this.autoTranslate, required this.hideBottomNav, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend});
const _AppSettings({required this.autoTranslate, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend});
@override final bool realmCompactView;
@override final bool mixedFeed;
@override final bool autoTranslate;
@override final bool hideBottomNav;
@override final bool soundEffects;
@override final bool aprilFoolFeatures;
@override final bool enterToSend;
@ -104,16 +98,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.realmCompactView, realmCompactView) || other.realmCompactView == realmCompactView)&&(identical(other.mixedFeed, mixedFeed) || other.mixedFeed == mixedFeed)&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.hideBottomNav, hideBottomNav) || other.hideBottomNav == hideBottomNav)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend));
}
@override
int get hashCode => Object.hash(runtimeType,realmCompactView,mixedFeed,autoTranslate,hideBottomNav,soundEffects,aprilFoolFeatures,enterToSend);
int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend);
@override
String toString() {
return 'AppSettings(realmCompactView: $realmCompactView, mixedFeed: $mixedFeed, autoTranslate: $autoTranslate, hideBottomNav: $hideBottomNav, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend)';
return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend)';
}
@ -124,7 +118,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith
factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
@override @useResult
$Res call({
bool realmCompactView, bool mixedFeed, bool autoTranslate, bool hideBottomNav, bool soundEffects, bool aprilFoolFeatures, bool enterToSend
bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend
});
@ -141,12 +135,9 @@ class __$AppSettingsCopyWithImpl<$Res>
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? realmCompactView = null,Object? mixedFeed = null,Object? autoTranslate = null,Object? hideBottomNav = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,}) {
return _then(_AppSettings(
realmCompactView: null == realmCompactView ? _self.realmCompactView : realmCompactView // ignore: cast_nullable_to_non_nullable
as bool,mixedFeed: null == mixedFeed ? _self.mixedFeed : mixedFeed // ignore: cast_nullable_to_non_nullable
as bool,autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
as bool,hideBottomNav: null == hideBottomNav ? _self.hideBottomNav : hideBottomNav // ignore: cast_nullable_to_non_nullable
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
as bool,soundEffects: null == soundEffects ? _self.soundEffects : soundEffects // ignore: cast_nullable_to_non_nullable
as bool,aprilFoolFeatures: null == aprilFoolFeatures ? _self.aprilFoolFeatures : aprilFoolFeatures // ignore: cast_nullable_to_non_nullable
as bool,enterToSend: null == enterToSend ? _self.enterToSend : enterToSend // ignore: cast_nullable_to_non_nullable

View File

@ -14,7 +14,7 @@ part 'websocket.freezed.dart';
part 'websocket.g.dart';
@freezed
abstract class WebSocketState with _$WebSocketState {
sealed class WebSocketState with _$WebSocketState {
const factory WebSocketState.connected() = _Connected;
const factory WebSocketState.connecting() = _Connecting;
const factory WebSocketState.disconnected() = _Disconnected;
@ -22,7 +22,7 @@ abstract class WebSocketState with _$WebSocketState {
}
@freezed
abstract class WebSocketPacket with _$WebSocketPacket {
sealed class WebSocketPacket with _$WebSocketPacket {
const factory WebSocketPacket({
required String type,
required Map<String, dynamic>? data,
@ -51,6 +51,7 @@ class WebSocketService {
Future<void> connect(Ref ref) async {
_ref = ref;
_statusStreamController.sink.add(WebSocketState.connecting());
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
@ -140,23 +141,10 @@ class WebSocketStateNotifier extends StateNotifier<WebSocketState> {
state = const WebSocketState.connecting();
try {
final service = ref.read(websocketProvider);
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) {
state = const WebSocketState.error('Unauthorized');
return;
}
await service.connect(ref);
state = const WebSocketState.connected();
service.statusStream.listen((event) {
state = event;
if (mounted) state = event;
});
} catch (err) {
state = WebSocketState.error('Failed to connect: $err');

View File

@ -8,15 +8,15 @@ class AppRouter extends RootStackRouter {
@override
List<AutoRoute> get routes => [
RedirectRoute(path: '/', redirectTo: '/explore'),
AutoRoute(
page: ExploreShellRoute.page,
path: '/explore',
path: '/',
children: [
AutoRoute(page: ExploreRoute.page, path: ''),
AutoRoute(page: PostComposeRoute.page, path: 'posts/compose'),
AutoRoute(page: PostDetailRoute.page, path: 'posts/:id'),
AutoRoute(page: PostEditRoute.page, path: 'posts/:id/edit'),
AutoRoute(page: PublisherProfileRoute.page, path: 'publishers/:name'),
],
),
AutoRoute(
@ -28,12 +28,11 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: WalletRoute.page, path: 'wallet'),
AutoRoute(page: RelationshipRoute.page, path: 'relationships'),
AutoRoute(page: AccountProfileRoute.page, path: ':name'),
AutoRoute(page: PublisherProfileRoute.page, path: ':name/calendar'),
AutoRoute(page: MyselfEventCalendarRoute.page, path: 'me/calendar'),
AutoRoute(page: UpdateProfileRoute.page, path: 'me/update'),
AutoRoute(page: AccountSettingsRoute.page, path: 'settings'),
],
),
AutoRoute(page: EventCalanderRoute.page, path: '/account/:name/calendar'),
AutoRoute(page: RealmListRoute.page, path: '/realms'),
AutoRoute(
page: ChatShellRoute.page,
@ -41,6 +40,7 @@ class AppRouter extends RootStackRouter {
children: [
AutoRoute(page: ChatListRoute.page, path: ''),
AutoRoute(page: ChatRoomRoute.page, path: ':id'),
AutoRoute(page: CallRoute.page, path: ':id/call'),
AutoRoute(page: NewChatRoute.page, path: 'new'),
AutoRoute(page: EditChatRoute.page, path: ':id/edit'),
AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'),

File diff suppressed because it is too large Load Diff

View File

@ -235,6 +235,16 @@ class AccountScreen extends HookConsumerWidget {
context.router.push(SettingsRoute());
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.manage_accounts),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('accountSettings').tr(),
onTap: () {
context.router.push(AccountSettingsRoute());
},
),
if (kDebugMode) const Divider(height: 1).padding(vertical: 8),
if (kDebugMode)
ListTile(
@ -284,7 +294,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: const Text('Account')),
appBar: AppBar(title: const Text('account').tr()),
body:
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),

View File

@ -7,7 +7,9 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/account/profile.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
@ -17,7 +19,7 @@ part 'event_calendar.g.dart';
part 'event_calendar.freezed.dart';
@freezed
abstract class EventCalendarQuery with _$EventCalendarQuery {
sealed class EventCalendarQuery with _$EventCalendarQuery {
const factory EventCalendarQuery({
required String? uname,
required int year,
@ -39,8 +41,9 @@ Future<List<SnEventCalendarEntry>> accountEventCalendar(
}
@RoutePage()
class MyselfEventCalendarScreen extends HookConsumerWidget {
const MyselfEventCalendarScreen({super.key});
class EventCalanderScreen extends HookConsumerWidget {
final String name;
const EventCalanderScreen({super.key, @PathParam("name") required this.name});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -49,150 +52,180 @@ class MyselfEventCalendarScreen extends HookConsumerWidget {
final selectedDay = useState(DateTime.now());
final user = ref.watch(accountProvider(name));
final events = ref.watch(
accountEventCalendarProvider(
EventCalendarQuery(
uname: 'me',
uname: name,
year: selectedYear.value,
month: selectedMonth.value,
),
),
);
final content = Column(
children: [
TableCalendar(
locale: EasyLocalization.of(context)!.locale.toString(),
firstDay: DateTime.now().add(Duration(days: -3650)),
lastDay: DateTime.now().add(Duration(days: 3650)),
focusedDay: DateTime.utc(
selectedYear.value,
selectedMonth.value,
DateTime.now().day,
),
calendarFormat: CalendarFormat.month,
selectedDayPredicate: (day) {
return isSameDay(selectedDay.value, day);
},
onDaySelected: (value, _) {
selectedDay.value = value;
},
onPageChanged: (focusedDay) {
selectedMonth.value = focusedDay.month;
selectedYear.value = focusedDay.year;
},
eventLoader: (day) {
return events.value
?.where((e) => isSameDay(e.date, day))
.expand((e) => [...e.statuses, e.checkInResult])
.where((e) => e != null)
.toList() ??
[];
},
calendarBuilders: CalendarBuilders(
dowBuilder: (context, day) {
final text = DateFormat.EEEEE().format(day);
return Center(child: Text(text));
},
markerBuilder: (context, day, events) {
var checkInResult =
events.whereType<SnCheckInResult>().firstOrNull;
if (checkInResult != null) {
return Positioned(
top: 32,
child: Text(
['大凶', '', '中平', '', '大吉'][checkInResult.level],
style: TextStyle(
fontSize: 9,
color:
isSameDay(selectedDay.value, day)
? Theme.of(context).colorScheme.onPrimaryContainer
: isSameDay(DateTime.now(), day)
? Theme.of(
context,
).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onSurface,
),
),
);
}
return null;
},
),
),
const Divider(height: 1).padding(top: 8),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Builder(
builder: (context) {
final event =
events.value
?.where((e) => isSameDay(e.date, selectedDay.value))
.firstOrNull;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(DateFormat.EEEE().format(selectedDay.value))
.fontSize(16)
.bold()
.textColor(
Theme.of(context).colorScheme.onSecondaryContainer,
),
Text(DateFormat.yMd().format(selectedDay.value))
.fontSize(12)
.textColor(
Theme.of(context).colorScheme.onSecondaryContainer,
),
const Gap(16),
if (event?.checkInResult != null)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'checkInResultLevel${event!.checkInResult!.level}',
).tr().fontSize(16).bold(),
for (final tip in event.checkInResult!.tips)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
Icon(
Symbols.circle,
size: 12,
fill: 1,
).padding(top: 4, right: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(tip.title).bold(),
Text(tip.content),
],
),
),
],
).padding(top: 8),
],
),
if (event?.checkInResult == null &&
(event?.statuses.isEmpty ?? true))
Text('eventCalanderEmpty').tr(),
],
).padding(vertical: 24, horizontal: 24);
},
),
),
if (name != 'me' && user.hasValue)
Container(
decoration: BoxDecoration(
border: Border.all(
width: 1 / MediaQuery.of(context).devicePixelRatio,
color: Theme.of(context).dividerColor,
),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
margin: EdgeInsets.all(16),
child: Card(
margin: EdgeInsets.zero,
elevation: 0,
color: Colors.transparent,
child: ListTile(
leading: ProfilePictureWidget(
fileId: user.value!.profile.pictureId,
),
title: Text(user.value!.nick).bold(),
subtitle: Text('@${user.value!.name}'),
),
),
),
],
);
return AppScaffold(
noBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: Text('eventCalander').tr(),
),
body: SingleChildScrollView(
child: Column(
children: [
TableCalendar(
locale: EasyLocalization.of(context)!.locale.toString(),
firstDay: DateTime.now().add(Duration(days: -3650)),
lastDay: DateTime.now().add(Duration(days: 3650)),
focusedDay: DateTime.utc(
selectedYear.value,
selectedMonth.value,
DateTime.now().day,
),
calendarFormat: CalendarFormat.month,
selectedDayPredicate: (day) {
return isSameDay(selectedDay.value, day);
},
onDaySelected: (value, _) {
selectedDay.value = value;
},
onPageChanged: (focusedDay) {
selectedMonth.value = focusedDay.month;
selectedYear.value = focusedDay.year;
},
eventLoader: (day) {
return events.value
?.where((e) => isSameDay(e.date, day))
.expand((e) => [...e.statuses, e.checkInResult])
.where((e) => e != null)
.toList() ??
[];
},
calendarBuilders: CalendarBuilders(
dowBuilder: (context, day) {
final text = DateFormat.EEEEE().format(day);
return Center(child: Text(text));
},
markerBuilder: (context, day, events) {
var checkInResult =
events.whereType<SnCheckInResult>().firstOrNull;
if (checkInResult != null) {
return Positioned(
top: 32,
child: Text(
['大凶', '', '中平', '', '大吉'][checkInResult.level],
style: TextStyle(
fontSize: 9,
color:
isSameDay(selectedDay.value, day)
? Theme.of(
context,
).colorScheme.onPrimaryContainer
: isSameDay(DateTime.now(), day)
? Theme.of(
context,
).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onSurface,
),
),
);
}
return null;
},
),
),
const Divider(height: 1).padding(top: 8),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Builder(
builder: (context) {
final event =
events.value
?.where((e) => isSameDay(e.date, selectedDay.value))
.firstOrNull;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(DateFormat.EEEE().format(selectedDay.value))
.fontSize(16)
.bold()
.textColor(
Theme.of(context).colorScheme.onSecondaryContainer,
),
Text(DateFormat.yMd().format(selectedDay.value))
.fontSize(12)
.textColor(
Theme.of(context).colorScheme.onSecondaryContainer,
),
const Gap(16),
if (event?.checkInResult != null)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'checkInResultLevel${event!.checkInResult!.level}',
).tr().fontSize(16).bold(),
for (final tip in event.checkInResult!.tips)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
Icon(
Symbols.circle,
size: 12,
fill: 1,
).padding(top: 4, right: 4),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(tip.title).bold(),
Text(tip.content),
],
),
),
],
).padding(top: 8),
],
),
if (event?.checkInResult == null &&
(event?.statuses.isEmpty ?? true))
Text('eventCalanderEmpty').tr(),
],
).padding(vertical: 24, horizontal: 24);
},
),
),
],
),
child:
MediaQuery.of(context).size.width > 480
? ConstrainedBox(
constraints: BoxConstraints(maxWidth: 480),
child: Card(margin: EdgeInsets.all(16), child: content),
).center()
: content,
),
);
}

View File

@ -1,8 +1,19 @@
import 'dart:io';
import 'package:auto_route/annotations.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/screens/auth/captcha.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@RoutePage()
class AccountSettingsScreen extends HookConsumerWidget {
@ -10,9 +21,274 @@ class AccountSettingsScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isDesktop =
!kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
final isWide = isWideScreen(context);
Future<void> requestAccountDeletion() async {
final confirm = await showConfirmAlert(
'accountDeletionHint'.tr(),
'accountDeletion'.tr(),
);
if (!confirm || !context.mounted) return;
try {
final client = ref.read(apiClientProvider);
await client.delete('/accounts/me');
if (context.mounted) {
showSnackBar(context, 'accountDeletionSent'.tr());
}
} catch (err) {
showErrorAlert(err);
}
}
Future<void> requestResetPassword() async {
final confirm = await showConfirmAlert(
'accountPasswordChangeDescription'.tr(),
'accountPassword'.tr(),
);
if (!confirm || !context.mounted) return;
final captchaTk = await Navigator.of(
context,
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
if (captchaTk == null) return;
try {
final userInfo = ref.read(userInfoProvider);
final client = ref.read(apiClientProvider);
await client.post(
'/accounts/recovery/password',
data: {'account': userInfo.value!.name, 'captcha_token': captchaTk},
);
if (context.mounted) {
showSnackBar(context, 'accountPasswordChangeSent'.tr());
}
} catch (err) {
showErrorAlert(err);
}
}
// Group settings into categories for better organization
final securitySettings = [
ListTile(
minLeadingWidth: 48,
title: Text('accountPassword').tr(),
subtitle: Text('accountPasswordDescription').tr().fontSize(12),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.password),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
requestResetPassword();
},
),
ListTile(
minLeadingWidth: 48,
title: Text('accountTwoFactor').tr(),
subtitle: Text('accountTwoFactorDescription').tr().fontSize(12),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.security),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
// Navigate to two-factor authentication settings
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('accountTwoFactor').tr(),
content: Text('accountTwoFactorSetupDescription').tr(),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close').tr(),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
// Add navigation to 2FA setup screen
},
child: Text('accountTwoFactorSetup').tr(),
),
],
),
);
},
),
];
final privacySettings = [
// ListTile(
// minLeadingWidth: 48,
// title: Text('accountPrivacy').tr(),
// subtitle: Text('accountPrivacyDescription').tr().fontSize(12),
// contentPadding: const EdgeInsets.only(left: 24, right: 17),
// leading: const Icon(Symbols.visibility),
// trailing: const Icon(Symbols.chevron_right),
// onTap: () {
// // Navigate to privacy settings
// },
// ),
ListTile(
minLeadingWidth: 48,
title: Text('accountDataExport').tr(),
subtitle: Text('accountDataExportDescription').tr().fontSize(12),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.download),
trailing: const Icon(Symbols.chevron_right),
onTap: () async {
final confirm = await showConfirmAlert(
'accountDataExportConfirmation'.tr(),
'accountDataExport'.tr(),
);
if (!confirm || !context.mounted) return;
// Add data export logic
showSnackBar(context, 'accountDataExportRequested'.tr());
},
),
];
final dangerZoneSettings = [
ListTile(
minLeadingWidth: 48,
title: Text('accountDeletion').tr(),
subtitle: Text('accountDeletionDescription').tr().fontSize(12),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.delete_forever, color: Colors.red),
trailing: const Icon(Symbols.chevron_right),
onTap: requestAccountDeletion,
),
];
// Create a responsive layout based on screen width
Widget buildSettingsList() {
if (isWide) {
// Two-column layout for wide screens
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'accountSecurityTitle',
children: securitySettings,
),
_SettingsSection(
title: 'accountPrivacyTitle',
children: privacySettings,
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'accountDangerZoneTitle',
children: dangerZoneSettings,
),
],
),
),
],
).padding(horizontal: 16);
} else {
// Single column layout for narrow screens
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'accountSecurityTitle',
children: securitySettings,
),
_SettingsSection(
title: 'accountPrivacyTitle',
children: privacySettings,
),
_SettingsSection(
title: 'accountDangerZoneTitle',
children: dangerZoneSettings,
),
],
);
}
}
return AppScaffold(
appBar: AppBar(title: Text('accountSettings').tr()),
body: SingleChildScrollView(child: Column(children: [])),
appBar: AppBar(
title: Text('accountSettings').tr(),
actions:
isDesktop
? [
IconButton(
icon: const Icon(Symbols.help_outline),
onPressed: () {
// Show help dialog
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('accountSettingsHelp').tr(),
content: Text('accountSettingsHelpContent').tr(),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close').tr(),
),
],
),
);
},
),
]
: null,
),
body: Focus(
autofocus: true,
onKeyEvent: (node, event) {
// Add keyboard shortcuts for desktop
if (isDesktop &&
event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) {
Navigator.of(context).pop();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16),
child: buildSettingsList(),
),
),
);
}
}
// Helper widget for displaying settings sections with titles
class _SettingsSection extends StatelessWidget {
final String title;
final List<Widget> children;
const _SettingsSection({required this.title, required this.children});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
child: Text(
title.tr(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
...children,
const SizedBox(height: 16),
],
);
}
}

View File

@ -5,6 +5,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/widgets/account/badge.dart';
import 'package:island/widgets/account/leveling_progress.dart';
import 'package:island/widgets/account/status.dart';
@ -17,6 +18,12 @@ part 'profile.g.dart';
@riverpod
Future<SnAccount> account(Ref ref, String uname) async {
if (uname == 'me') {
final userInfo = ref.watch(userInfoProvider);
if (userInfo.hasValue && userInfo.value != null) {
return userInfo.value!;
}
}
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get("/accounts/$uname");
return SnAccount.fromJson(resp.data);

View File

@ -6,7 +6,7 @@ part of 'profile.dart';
// RiverpodGenerator
// **************************************************************************
String _$accountHash() => r'39003ef3250181b9290e0562329c7801d4841941';
String _$accountHash() => r'd2b0579617e6264452d98f47f695a9cdf45b24ec';
/// Copied from Dart SDK
class _SystemHash {

View File

@ -96,8 +96,7 @@ class RelationshipListTile extends StatelessWidget {
relationship.status == 0 && relationship.relatedId == currentUserId;
final isWaiting =
relationship.status == 0 && relationship.accountId == currentUserId;
final isEstablished =
relationship.status >= 100 || relationship.status <= -100;
final isEstablished = relationship.status == 1 || relationship.status == 2;
return ListTile(
contentPadding: const EdgeInsets.only(left: 16, right: 12),

View File

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

View File

@ -19,6 +19,8 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'captcha.dart';
final Map<int, (String, String, IconData)> kFactorTypes = {
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
@ -41,6 +43,7 @@ class LoginScreen extends HookConsumerWidget {
final factors = useState<List<SnAuthFactor>>([]);
final factorPicked = useState<SnAuthFactor?>(null);
return AppScaffold(
noBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: Text('login').tr(),
@ -353,15 +356,18 @@ class _LoginLookupScreen extends HookConsumerWidget {
showErrorAlert('loginResetPasswordHint'.tr());
return;
}
final captchaTk = await Navigator.of(
context,
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
if (captchaTk == null) return;
isBusy.value = true;
try {
final client = ref.watch(apiClientProvider);
final lookupResp = await client.get('/users/lookup?probe=$uname');
await client.post(
'/users/me/password-reset',
data: {'user_id': lookupResp.data['id']},
'/accounts/recovery/password',
data: {'account': uname, 'captcha_token': captchaTk},
);
showInfoAlert('done'.tr(), 'signinResetPasswordSent'.tr());
showInfoAlert('loginResetPasswordSent'.tr(), 'done'.tr());
} catch (err) {
showErrorAlert(err);
} finally {

View File

@ -17,6 +17,7 @@ class TabNavigationObserver extends AutoRouterObserver {
@override
void didPush(Route route, Route? previousRoute) {
if (route is DialogRoute) return;
Future(() {
onChange(route.settings.name);
});
@ -24,6 +25,7 @@ class TabNavigationObserver extends AutoRouterObserver {
@override
void didPop(Route route, Route? previousRoute) {
if (route is DialogRoute) return;
Future(() {
onChange(previousRoute?.settings.name);
});

296
lib/screens/chat/call.dart Normal file
View File

@ -0,0 +1,296 @@
import 'package:auto_route/annotations.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
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:livekit_client/livekit_client.dart';
import 'package:styled_widget/styled_widget.dart';
@RoutePage()
class CallScreen extends HookConsumerWidget {
final String roomId;
const CallScreen({super.key, @PathParam('id') required this.roomId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ongoingCall = ref.watch(ongoingCallProvider(roomId));
final callState = ref.watch(callNotifierProvider);
final callNotifier = ref.read(callNotifierProvider.notifier);
useEffect(() {
callNotifier.joinRoom(roomId);
return null;
}, []);
final viewMode = useState<String>('grid');
return AppScaffold(
appBar: AppBar(
leading: PageBackButton(
onWillPop: () {
showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
content: const Text(
'Do you want to leave the call or leave it in background?',
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('In Background'),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
await callNotifier.disconnect();
callNotifier.dispose();
},
child: const Text('Leave'),
),
],
);
},
);
},
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
ongoingCall.value?.room.name ?? 'call'.tr(),
style: const TextStyle(fontSize: 16),
),
Text(
callState.isConnected
? formatDuration(callState.duration)
: 'Connecting',
style: const TextStyle(fontSize: 14),
),
],
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Icon(Icons.grid_view),
tooltip: 'Grid View',
onPressed: () => viewMode.value = 'grid',
color:
viewMode.value == 'grid'
? Theme.of(context).colorScheme.primary
: null,
),
IconButton(
icon: Icon(Icons.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),
),
)
: Column(
children: [
Expanded(
child: Builder(
builder: (context) {
if (!callState.isConnected) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (callNotifier.participants.isEmpty) {
return const Center(
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(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
alignment: WrapAlignment.center,
spacing: 8,
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
.pictureId,
size: 72,
),
),
],
),
),
);
}
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(),
),
],
).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,
),
),
],
),
),
],
);
}
// 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,
),
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();
},
);
},
),
),
CallControlsBar(),
Gap(MediaQuery.of(context).padding.bottom + 16),
],
),
);
}
}

View File

@ -11,6 +11,7 @@ import 'package:image_picker/image_picker.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/file.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/call.dart';
import 'package:island/pods/chat_summary.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
@ -21,6 +22,7 @@ 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/realms/selection_dropdown.dart';
import 'package:island/widgets/response.dart';
@ -147,12 +149,11 @@ class ChatRoomListTile extends HookConsumerWidget {
subtitle: buildSubtitle(),
onTap: () async {
// Clear unread count if there are unread messages
final summary = await ref.read(chatSummaryProvider.future);
if ((summary[room.id]?.unreadCount ?? 0) > 0) {
await ref
.read(chatSummaryProvider.notifier)
.clearUnreadCount(room.id);
}
ref.read(chatSummaryProvider.future).then((summary) {
if ((summary[room.id]?.unreadCount ?? 0) > 0) {
ref.read(chatSummaryProvider.notifier).clearUnreadCount(room.id);
}
});
onTap?.call();
},
);
@ -213,6 +214,8 @@ class ChatListScreen extends HookConsumerWidget {
0,
); // 0 for All, 1 for Direct Messages, 2 for Group Chats
final callState = ref.watch(callNotifierProvider);
useEffect(() {
tabController.addListener(() {
selectedTab.value = tabController.index;
@ -241,9 +244,33 @@ class ChatListScreen extends HookConsumerWidget {
bottom: TabBar(
controller: tabController,
tabs: [
Tab(text: 'chatTabAll'.tr()),
Tab(text: 'chatTabDirect'.tr()),
Tab(text: 'chatTabGroup'.tr()),
Tab(
child: Text(
'chatTabAll'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'chatTabDirect'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'chatTabGroup'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
actions: [
@ -310,57 +337,95 @@ class ChatListScreen extends HookConsumerWidget {
},
child: const Icon(Symbols.add),
),
body: chats.when(
data:
(items) => RefreshIndicator(
onRefresh:
() => Future.sync(() {
ref.invalidate(chatroomsJoinedProvider);
}),
child: ListView.builder(
padding: EdgeInsets.zero,
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: () {
if (context.router.topRoute.name == ChatRoomRoute.name) {
context.router.replace(ChatRoomRoute(id: item.id));
} else {
context.router.push(ChatRoomRoute(id: item.id));
}
},
body: Stack(
children: [
Column(
children: [
Consumer(
builder: (context, ref, _) {
final summaryState = ref.watch(chatSummaryProvider);
return summaryState.maybeWhen(
loading: () => const LinearProgressIndicator(),
orElse: () => const SizedBox.shrink(),
);
},
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () {
ref.invalidate(chatroomsJoinedProvider);
},
),
Expanded(
child: chats.when(
data:
(items) => RefreshIndicator(
onRefresh:
() => Future.sync(() {
ref.invalidate(chatroomsJoinedProvider);
}),
child: ListView.builder(
padding:
callState.isConnected
? EdgeInsets.only(bottom: 96)
: EdgeInsets.zero,
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: () {
if (context.router.topRoute.name ==
ChatRoomRoute.name) {
context.router.replace(
ChatRoomRoute(id: item.id),
);
} else {
context.router.push(
ChatRoomRoute(id: item.id),
);
}
},
);
},
),
),
loading:
() => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () {
ref.invalidate(chatroomsJoinedProvider);
},
),
),
),
],
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: const CallOverlayBar().padding(horizontal: 16, vertical: 12),
),
],
),
);
}

View File

@ -22,6 +22,7 @@ import 'package:island/screens/posts/compose.dart';
import 'package:island/services/responsive.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/chat/message_item.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
@ -33,6 +34,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:super_clipboard/super_clipboard.dart';
import 'chat.dart';
import 'package:island/widgets/chat/call_button.dart';
part 'room.g.dart';
@ -351,6 +353,10 @@ class ChatRoomScreen extends HookConsumerWidget {
if (message.chatRoomId != chatRoom.value?.id) return;
switch (pkt.type) {
case 'messages.new':
if (message.type.startsWith('call')) {
// Handle the ongoing call.
ref.invalidate(ongoingCallProvider(message.chatRoomId));
}
messagesNotifier.receiveMessage(message);
// Send read receipt for new message
sendReadReceipt();
@ -514,12 +520,7 @@ class ChatRoomScreen extends HookConsumerWidget {
),
),
actions: [
IconButton(
icon: const Icon(Symbols.video_call),
onPressed: () {
showInfoAlert('Oops', 'Not implemented yet...');
},
),
AudioCallButton(roomId: id),
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
@ -529,152 +530,166 @@ class ChatRoomScreen extends HookConsumerWidget {
const Gap(8),
],
),
body: Column(
body: Stack(
children: [
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: SuperListView.builder(
padding: EdgeInsets.symmetric(vertical: 16),
controller: scrollController,
reverse: true, // Show newest messages at the bottom
itemCount: messageList.length,
itemBuilder: (context, index) {
final message = messageList[index];
final nextMessage =
index < messageList.length - 1
? messageList[index + 1]
: null;
final isLastInGroup =
nextMessage == null ||
nextMessage.senderId != message.senderId ||
nextMessage.createdAt
.difference(message.createdAt)
.inMinutes
.abs() >
3;
Column(
children: [
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: SuperListView.builder(
padding: EdgeInsets.symmetric(vertical: 16),
controller: scrollController,
reverse:
true, // Show newest messages at the bottom
itemCount: messageList.length,
itemBuilder: (context, index) {
final message = messageList[index];
final nextMessage =
index < messageList.length - 1
? messageList[index + 1]
: null;
final isLastInGroup =
nextMessage == null ||
nextMessage.senderId !=
message.senderId ||
nextMessage.createdAt
.difference(message.createdAt)
.inMinutes
.abs() >
3;
return chatIdentity.when(
skipError: true,
data:
(identity) => MessageItem(
message: message,
isCurrentUser:
identity?.id == message.senderId,
onAction: (action) {
switch (action) {
case MessageItemAction.delete:
messagesNotifier.deleteMessage(
message.id,
);
case MessageItemAction.edit:
messageEditingTo.value =
message.toRemoteMessage();
messageController.text =
messageEditingTo
.value
?.content ??
'';
attachments.value =
messageEditingTo
.value!
.attachments
.map(
(e) =>
UniversalFile.fromAttachment(
e,
),
)
.toList();
case MessageItemAction.forward:
messageForwardingTo.value =
message.toRemoteMessage();
case MessageItemAction.reply:
messageReplyingTo.value =
message.toRemoteMessage();
}
},
progress:
attachmentProgress.value[message.id],
showAvatar: isLastInGroup,
),
loading:
() => MessageItem(
message: message,
isCurrentUser: false,
onAction: null,
progress: null,
showAvatar: false,
),
error: (_, __) => const SizedBox.shrink(),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
),
),
chatRoom.when(
data:
(room) => _ChatInput(
messageController: messageController,
chatRoom: room!,
onSend: sendMessage,
onClear: () {
if (messageEditingTo.value != null) {
attachments.value.clear();
messageController.clear();
}
messageEditingTo.value = null;
messageReplyingTo.value = null;
messageForwardingTo.value = null;
},
messageEditingTo: messageEditingTo.value,
messageReplyingTo: messageReplyingTo.value,
messageForwardingTo: messageForwardingTo.value,
onPickFile: (bool isPhoto) {
if (isPhoto) {
pickPhotoMedia();
} else {
pickVideoMedia();
}
},
attachments: attachments.value,
onUploadAttachment: (_) {
// not going to do anything, only upload when send the message
},
onDeleteAttachment: (index) async {
final attachment = attachments.value[index];
if (attachment.isOnCloud) {
final client = ref.watch(apiClientProvider);
await client.delete('/files/${attachment.data.id}');
}
final clone = List.of(attachments.value);
clone.removeAt(index);
attachments.value = clone;
},
onMoveAttachment: (idx, delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(idx + delta, clone.removeAt(idx));
attachments.value = clone;
},
onAttachmentsChanged: (newAttachments) {
attachments.value = newAttachments;
},
return chatIdentity.when(
skipError: true,
data:
(identity) => MessageItem(
message: message,
isCurrentUser:
identity?.id == message.senderId,
onAction: (action) {
switch (action) {
case MessageItemAction.delete:
messagesNotifier.deleteMessage(
message.id,
);
case MessageItemAction.edit:
messageEditingTo.value =
message.toRemoteMessage();
messageController.text =
messageEditingTo
.value
?.content ??
'';
attachments.value =
messageEditingTo
.value!
.attachments
.map(
(e) =>
UniversalFile.fromAttachment(
e,
),
)
.toList();
case MessageItemAction.forward:
messageForwardingTo.value =
message.toRemoteMessage();
case MessageItemAction.reply:
messageReplyingTo.value =
message.toRemoteMessage();
}
},
progress:
attachmentProgress.value[message
.id],
showAvatar: isLastInGroup,
),
loading:
() => MessageItem(
message: message,
isCurrentUser: false,
onAction: null,
progress: null,
showAvatar: false,
),
error: (_, __) => const SizedBox.shrink(),
);
},
),
loading:
() => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
),
error: (_, __) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),
chatRoom.when(
data:
(room) => _ChatInput(
messageController: messageController,
chatRoom: room!,
onSend: sendMessage,
onClear: () {
if (messageEditingTo.value != null) {
attachments.value.clear();
messageController.clear();
}
messageEditingTo.value = null;
messageReplyingTo.value = null;
messageForwardingTo.value = null;
},
messageEditingTo: messageEditingTo.value,
messageReplyingTo: messageReplyingTo.value,
messageForwardingTo: messageForwardingTo.value,
onPickFile: (bool isPhoto) {
if (isPhoto) {
pickPhotoMedia();
} else {
pickVideoMedia();
}
},
attachments: attachments.value,
onUploadAttachment: (_) {
// not going to do anything, only upload when send the message
},
onDeleteAttachment: (index) async {
final attachment = attachments.value[index];
if (attachment.isOnCloud) {
final client = ref.watch(apiClientProvider);
await client.delete('/files/${attachment.data.id}');
}
final clone = List.of(attachments.value);
clone.removeAt(index);
attachments.value = clone;
},
onMoveAttachment: (idx, delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(idx + delta, clone.removeAt(idx));
attachments.value = clone;
},
onAttachmentsChanged: (newAttachments) {
attachments.value = newAttachments;
},
),
error: (_, __) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),
],
),
Positioned(
left: 0,
right: 0,
top: 0,
child: CallOverlayBar().padding(horizontal: 8, top: 12),
),
],
),
@ -874,6 +889,7 @@ class _ChatInput extends ConsumerWidget {
onKey: (event) => _handleKeyPress(context, ref, event),
child: TextField(
controller: messageController,
onSubmitted: enterToSend ? (_) => onSend() : null,
inputFormatters: [
if (enterToSend)
TextInputFormatter.withFunction((oldValue, newValue) {

View File

@ -229,7 +229,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
}
@freezed
abstract class ChatRoomMemberState with _$ChatRoomMemberState {
sealed class ChatRoomMemberState with _$ChatRoomMemberState {
const factory ChatRoomMemberState({
required List<SnChatMember> members,
required bool isLoading,

View File

@ -310,7 +310,7 @@ class _StickerPackActionMenu extends HookConsumerWidget {
}
@freezed
abstract class StickerWithPackQuery with _$StickerWithPackQuery {
sealed class StickerWithPackQuery with _$StickerWithPackQuery {
const factory StickerWithPackQuery({
required String packId,
required String id,

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
@ -7,6 +8,7 @@ import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/markdown.dart';
@ -21,8 +23,18 @@ part 'notification.g.dart';
@riverpod
class NotificationUnreadCountNotifier
extends _$NotificationUnreadCountNotifier {
StreamSubscription<WebSocketPacket>? _subscription;
@override
Future<int> build() async {
// Subscribe to websocket events when this provider is built
_subscribeToWebSocket();
// Dispose the subscription when this provider is disposed
ref.onDispose(() {
_subscription?.cancel();
});
try {
final client = ref.read(apiClientProvider);
final response = await client.get('/notifications/count');
@ -32,9 +44,23 @@ class NotificationUnreadCountNotifier
}
}
void _subscribeToWebSocket() {
final webSocketService = ref.read(websocketProvider);
_subscription = webSocketService.dataStream.listen((packet) {
if (packet.type == 'notifications.new') {
_incrementCounter();
}
});
}
Future<void> _incrementCounter() async {
final current = await future;
state = AsyncData(current + 1);
}
Future<void> decrement(int count) async {
final current = await future;
state = AsyncData(math.min(current - count, 0));
state = AsyncData(math.max(current - count, 0));
}
}
@ -94,6 +120,7 @@ class NotificationScreen extends HookConsumerWidget {
notifierRefreshable: notificationListNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
@ -142,7 +169,7 @@ class NotificationScreen extends HookConsumerWidget {
],
),
trailing:
notification.viewedAt == null
notification.viewedAt != null
? null
: Container(
width: 12,

View File

@ -1,5 +1,4 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
@ -18,6 +17,7 @@ import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/posts/detail.dart';
import 'package:island/services/file.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
@ -261,7 +261,43 @@ class PostComposeScreen extends HookConsumerWidget {
return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title:
isWideScreen(context)
? Text(originalPost != null ? 'editPost'.tr() : 'newPost'.tr())
: null,
actions: [
if (isWideScreen(context))
Tooltip(
message: 'keyboard_shortcuts'.tr(),
child: IconButton(
icon: const Icon(Symbols.keyboard),
onPressed: () {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('keyboard_shortcuts'.tr()),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Ctrl/Cmd + Enter: ${'submit'.tr()}'),
Text('Ctrl/Cmd + V: ${'paste'.tr()}'),
Text('Ctrl/Cmd + I: ${'add_image'.tr()}'),
Text('Ctrl/Cmd + Shift + V: ${'add_video'.tr()}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('close'.tr()),
),
],
),
);
},
),
),
IconButton(
onPressed: submitting.value ? null : performAction,
icon:
@ -316,7 +352,7 @@ class PostComposeScreen extends HookConsumerWidget {
TextField(
controller: titleController,
decoration: InputDecoration.collapsed(
hintText: 'Title',
hintText: 'postTitle'.tr(),
),
style: TextStyle(fontSize: 16),
onTapOutside:
@ -326,7 +362,7 @@ class PostComposeScreen extends HookConsumerWidget {
TextField(
controller: descriptionController,
decoration: InputDecoration.collapsed(
hintText: 'Description',
hintText: 'postDescription'.tr(),
),
style: TextStyle(fontSize: 16),
onTapOutside:
@ -345,6 +381,7 @@ class PostComposeScreen extends HookConsumerWidget {
hintText: 'postPlaceholder'.tr(),
isDense: true,
),
maxLines: null,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
@ -352,34 +389,64 @@ class PostComposeScreen extends HookConsumerWidget {
),
),
const Gap(8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
for (
var idx = 0;
idx < attachments.value.length;
idx++
)
AttachmentPreview(
item: attachments.value[idx],
progress: attachmentProgress.value[idx],
onRequestUpload: () => uploadAttachment(idx),
onDelete: () => deleteAttachment(idx),
onMove: (delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(
idx + delta,
clone.removeAt(idx),
LayoutBuilder(
builder: (context, constraints) {
final isWide = isWideScreen(context);
return isWide
? Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (var idx = 0; idx < attachments.value.length; idx++)
SizedBox(
width: constraints.maxWidth / 2 - 4,
child: AttachmentPreview(
item: attachments.value[idx],
progress: attachmentProgress.value[idx],
onRequestUpload: () => uploadAttachment(idx),
onDelete: () => deleteAttachment(idx),
onMove: (delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(
idx + delta,
clone.removeAt(idx),
);
attachments.value = clone;
},
),
),
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
for (var idx = 0; idx < attachments.value.length; idx++)
AttachmentPreview(
item: attachments.value[idx],
progress: attachmentProgress.value[idx],
onRequestUpload: () => uploadAttachment(idx),
onDelete: () => deleteAttachment(idx),
onMove: (delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(
idx + delta,
clone.removeAt(idx),
);
attachments.value = clone;
},
),
],
);
attachments.value = clone;
},
),
],
},
),
],
),

View File

@ -64,7 +64,12 @@ class PostDetailScreen extends HookConsumerWidget {
child: Material(
elevation: 2,
color: Colors.transparent,
child: PostQuickReply(parent: post).padding(
child: PostQuickReply(
parent: post,
onPosted: () {
ref.invalidate(postRepliesNotifierProvider(id));
},
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
top: 16,
horizontal: 16,

View File

@ -29,9 +29,9 @@ Future<SnPublisher> publisher(Ref ref, String uname) async {
@riverpod
Future<List<SnAccountBadge>> publisherBadges(Ref ref, String pubName) async {
final pub = await ref.watch(publisherProvider(pubName).future);
if (pub.type != 0) return [];
if (pub.type != 0 || pub.account == null) return [];
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get("/accounts/${pub.name}/badges");
final resp = await apiClient.get("/accounts/${pub.account!.name}/badges");
return List<SnAccountBadge>.from(
resp.data.map((x) => SnAccountBadge.fromJson(x)),
);
@ -177,9 +177,9 @@ class PublisherProfileScreen extends HookConsumerWidget {
).fontSize(14).opacity(0.85),
],
),
if (data.type == 0)
if (data.type == 0 && data.account != null)
AccountStatusWidget(
uname: name,
uname: data.account!.name,
padding: EdgeInsets.zero,
),
],

View File

@ -6,10 +6,12 @@ import 'package:dropdown_button2/dropdown_button2.dart';
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:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
@ -27,6 +29,9 @@ class SettingsScreen extends HookConsumerWidget {
final prefs = ref.watch(sharedPreferencesProvider);
final controller = TextEditingController(text: serverUrl);
final settings = ref.watch(appSettingsProvider);
final isDesktop =
!kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
final isWide = isWideScreen(context);
final docBasepath = useState<String?>(null);
@ -37,240 +42,437 @@ class SettingsScreen extends HookConsumerWidget {
return null;
}, []);
return AppScaffold(
noBackground: false,
appBar: AppBar(title: const Text('Settings')),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
minLeadingWidth: 48,
title: Text('settingsDisplayLanguage').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.translate),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<Locale?>(
isExpanded: true,
items: [
...EasyLocalization.of(
context,
)!.supportedLocales.mapIndexed((idx, ele) {
return DropdownMenuItem<Locale?>(
value: ele,
child: Text(
'${ele.languageCode}-${ele.countryCode}',
).fontSize(14),
);
}),
DropdownMenuItem<Locale?>(
value: null,
child: Text('languageFollowSystem').tr().fontSize(14),
),
],
value: EasyLocalization.of(context)!.currentLocale,
onChanged: (Locale? value) {
if (value != null) {
EasyLocalization.of(context)!.setLocale(value);
} else {
EasyLocalization.of(context)!.resetLocale();
}
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
height: 40,
width: 160,
),
menuItemStyleData: const MenuItemStyleData(height: 40),
),
// Group settings into categories for better organization
final appearanceSettings = [
// Language settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsDisplayLanguage').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.translate),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<Locale?>(
isExpanded: true,
items: [
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((
idx,
ele,
) {
return DropdownMenuItem<Locale?>(
value: ele,
child: Text(
'${ele.languageCode}-${ele.countryCode}',
).fontSize(14),
);
}),
DropdownMenuItem<Locale?>(
value: null,
child: Text('languageFollowSystem').tr().fontSize(14),
),
],
value: EasyLocalization.of(context)!.currentLocale,
onChanged: (Locale? value) {
if (value != null) {
EasyLocalization.of(context)!.setLocale(value);
} else {
EasyLocalization.of(context)!.resetLocale();
}
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
height: 40,
width: 160,
),
ListTile(
isThreeLine: true,
minLeadingWidth: 48,
title: Text('settingsServerUrl').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.link),
subtitle: Padding(
padding: const EdgeInsets.only(top: 6),
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: kNetworkServerDefault,
suffixIcon: IconButton(
icon: const Icon(Symbols.restart_alt),
onPressed: () {
controller.text = kNetworkServerDefault;
prefs.setString(
kNetworkServerStoreKey,
kNetworkServerDefault,
);
ref.invalidate(serverUrlProvider);
showSnackBar(context, 'settingsApplied'.tr());
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
isDense: true,
),
onSubmitted: (value) {
if (value.isNotEmpty) {
prefs.setString(kNetworkServerStoreKey, value);
ref.invalidate(serverUrlProvider);
showSnackBar(context, 'settingsApplied'.tr());
}
},
menuItemStyleData: const MenuItemStyleData(height: 40),
),
),
),
// Background image settings (only for non-web platforms)
if (!kIsWeb && docBasepath.value != null)
ListTile(
minLeadingWidth: 48,
title: Text('settingsBackgroundImage').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.image),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isDesktop)
Tooltip(
message: 'settingsBackgroundImageTooltip'.tr(),
padding: EdgeInsets.only(left: 8),
child: const Icon(Symbols.info, size: 18),
),
const Icon(Symbols.chevron_right),
],
),
onTap: () async {
final imagePicker = ref.read(imagePickerProvider);
final image = await imagePicker.pickImage(
source: ImageSource.gallery,
);
if (image == null) return;
await File(
image.path,
).copy('${docBasepath.value}/$kAppBackgroundImagePath');
prefs.setBool(kAppBackgroundStoreKey, true);
ref.invalidate(backgroundImageFileProvider);
if (context.mounted) {
showSnackBar(context, 'settingsApplied'.tr());
}
},
),
// Clear background image option
if (!kIsWeb && docBasepath.value != null)
FutureBuilder<bool>(
future: File('${docBasepath.value}/app_background_image').exists(),
builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!) {
return const SizedBox.shrink();
}
return ListTile(
title: Text('settingsBackgroundImageClear').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.texture),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
File(
'${docBasepath.value}/$kAppBackgroundImagePath',
).deleteSync();
prefs.remove(kAppBackgroundStoreKey);
ref.invalidate(backgroundImageFileProvider);
if (context.mounted) {
showSnackBar(context, 'settingsApplied'.tr());
}
},
);
},
),
];
final serverSettings = [
// Server URL settings
ListTile(
isThreeLine: true,
minLeadingWidth: 48,
title: Text('settingsServerUrl').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.link),
subtitle: Padding(
padding: const EdgeInsets.only(top: 6),
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: kNetworkServerDefault,
suffixIcon: IconButton(
icon: const Icon(Symbols.restart_alt),
onPressed: () {
controller.text = kNetworkServerDefault;
prefs.setString(
kNetworkServerStoreKey,
kNetworkServerDefault,
);
ref.invalidate(serverUrlProvider);
showSnackBar(context, 'settingsApplied'.tr());
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
isDense: true,
),
if (!kIsWeb && docBasepath.value != null)
onSubmitted: (value) {
if (value.isNotEmpty) {
prefs.setString(kNetworkServerStoreKey, value);
ref.invalidate(serverUrlProvider);
showSnackBar(context, 'settingsApplied'.tr());
}
},
),
),
),
];
final behaviorSettings = [
// Auto translate settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsAutoTranslate').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.translate),
trailing: Switch(
value: settings.autoTranslate,
onChanged: (value) {
ref.read(appSettingsProvider.notifier).setAutoTranslate(value);
},
),
),
// Sound effects settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsSoundEffects').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.volume_up),
trailing: Switch(
value: settings.soundEffects,
onChanged: (value) {
ref.read(appSettingsProvider.notifier).setSoundEffects(value);
},
),
),
// April Fool features settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsAprilFoolFeatures').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.celebration),
trailing: Switch(
value: settings.aprilFoolFeatures,
onChanged: (value) {
ref.read(appSettingsProvider.notifier).setAprilFoolFeatures(value);
},
),
),
// Enter to send settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsEnterToSend').tr(),
subtitle:
isDesktop
? Text('settingsEnterToSendDesktopHint').tr().fontSize(12)
: null,
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.send),
trailing: Switch(
value: settings.enterToSend,
onChanged: (value) {
ref.read(appSettingsProvider.notifier).setEnterToSend(value);
},
),
),
];
// Desktop-specific settings
final desktopSettings =
!isDesktop
? <Widget>[]
: <Widget>[
ListTile(
minLeadingWidth: 48,
title: Text('settingsBackgroundImage').tr(),
title: Text('settingsKeyboardShortcuts').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.image),
leading: const Icon(Symbols.keyboard),
onTap: () {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('settingsKeyboardShortcuts').tr(),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_ShortcutRow(
shortcut: 'Ctrl+F',
description: 'Search',
),
_ShortcutRow(
shortcut: 'Ctrl+,',
description: 'Settings',
),
_ShortcutRow(
shortcut: 'Ctrl+N',
description: 'New Message',
),
_ShortcutRow(
shortcut: 'Esc',
description: 'Close Dialog',
),
// Add more shortcuts as needed
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close').tr(),
),
],
),
);
},
trailing: const Icon(Symbols.chevron_right),
onTap: () async {
final imagePicker = ref.read(imagePickerProvider);
final image = await imagePicker.pickImage(
source: ImageSource.gallery,
);
if (image == null) return;
),
];
await File(
image.path,
).copy('${docBasepath.value}/$kAppBackgroundImagePath');
prefs.setBool(kAppBackgroundStoreKey, true);
ref.invalidate(backgroundImageFileProvider);
if (context.mounted) {
showSnackBar(context, 'settingsApplied'.tr());
}
},
),
if (!kIsWeb && docBasepath.value != null)
FutureBuilder<bool>(
future:
File('${docBasepath.value}/app_background_image').exists(),
builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!) {
return const SizedBox.shrink();
}
return ListTile(
title: Text('settingsBackgroundImageClear').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.texture),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
File(
'${docBasepath.value}/$kAppBackgroundImagePath',
).deleteSync();
prefs.remove(kAppBackgroundStoreKey);
ref.invalidate(backgroundImageFileProvider);
if (context.mounted) {
showSnackBar(context, 'settingsApplied'.tr());
}
},
);
},
),
const Divider(),
ListTile(
minLeadingWidth: 48,
title: Text('settingsRealmCompactView').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.view_compact),
trailing: Switch(
value: settings.realmCompactView,
onChanged: (value) {
ref
.read(appSettingsProvider.notifier)
.setRealmCompactView(value);
},
// Create a responsive layout based on screen width
Widget buildSettingsList() {
if (isWide) {
// Two-column layout for wide screens
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'Appearance',
children: appearanceSettings,
),
_SettingsSection(title: 'Server', children: serverSettings),
],
),
),
ListTile(
minLeadingWidth: 48,
title: Text('settingsMixedFeed').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.merge),
trailing: Switch(
value: settings.mixedFeed,
onChanged: (value) {
ref.read(appSettingsProvider.notifier).setMixedFeed(value);
},
),
),
ListTile(
minLeadingWidth: 48,
title: Text('settingsAutoTranslate').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.translate),
trailing: Switch(
value: settings.autoTranslate,
onChanged: (value) {
ref
.read(appSettingsProvider.notifier)
.setAutoTranslate(value);
},
),
),
ListTile(
minLeadingWidth: 48,
title: Text('settingsHideBottomNav').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.navigation),
trailing: Switch(
value: settings.hideBottomNav,
onChanged: (value) {
ref
.read(appSettingsProvider.notifier)
.setHideBottomNav(value);
},
),
),
ListTile(
minLeadingWidth: 48,
title: Text('settingsSoundEffects').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.volume_up),
trailing: Switch(
value: settings.soundEffects,
onChanged: (value) {
ref.read(appSettingsProvider.notifier).setSoundEffects(value);
},
),
),
ListTile(
minLeadingWidth: 48,
title: Text('settingsAprilFoolFeatures').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.celebration),
trailing: Switch(
value: settings.aprilFoolFeatures,
onChanged: (value) {
ref
.read(appSettingsProvider.notifier)
.setAprilFoolFeatures(value);
},
),
),
ListTile(
minLeadingWidth: 48,
title: Text('settingsEnterToSend').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.send),
trailing: Switch(
value: settings.enterToSend,
onChanged: (value) {
ref.read(appSettingsProvider.notifier).setEnterToSend(value);
},
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'Behavior',
children: behaviorSettings,
),
if (desktopSettings.isNotEmpty)
_SettingsSection(
title: 'Desktop',
children: desktopSettings,
),
],
),
),
],
).padding(horizontal: 16);
} else {
// Single column layout for narrow screens
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(title: 'Appearance', children: appearanceSettings),
_SettingsSection(title: 'Server', children: serverSettings),
_SettingsSection(title: 'Behavior', children: behaviorSettings),
if (desktopSettings.isNotEmpty)
_SettingsSection(title: 'Desktop', children: desktopSettings),
],
);
}
}
return AppScaffold(
noBackground: false,
appBar: AppBar(
title: Text('Settings').tr(),
actions:
isDesktop
? [
IconButton(
icon: const Icon(Symbols.help_outline),
onPressed: () {
// Show help dialog
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('settingsHelp').tr(),
content: Text('settingsHelpContent').tr(),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close').tr(),
),
],
),
);
},
),
]
: null,
),
body: Focus(
autofocus: true,
onKeyEvent: (node, event) {
// Add keyboard shortcuts for desktop
if (isDesktop &&
event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) {
context.router.pop();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(vertical: 16),
child: buildSettingsList(),
),
),
);
}
}
// Helper widget for displaying settings sections with titles
class _SettingsSection extends StatelessWidget {
final String title;
final List<Widget> children;
const _SettingsSection({required this.title, required this.children});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
...children,
const SizedBox(height: 16),
],
);
}
}
// Helper widget for displaying keyboard shortcuts
class _ShortcutRow extends StatelessWidget {
final String shortcut;
final String description;
const _ShortcutRow({required this.shortcut, required this.description});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
),
),
child: Text(shortcut, style: TextStyle(fontFamily: 'monospace')),
),
SizedBox(width: 16),
Text(description),
],
),
);
}
}

View File

@ -16,7 +16,7 @@ const List<Tour> kAllTours = [
];
@freezed
abstract class Tour with _$Tour {
sealed class Tour with _$Tour {
const Tour._();
const factory Tour({required String id, required bool isStartup}) = _Tour;

View File

@ -150,12 +150,8 @@ class AppScaffold extends StatelessWidget {
noBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor,
body: SizedBox.expand(
child:
noBackground
? content
: AppBackground(isRoot: true, child: content),
),
body:
noBackground ? content : AppBackground(isRoot: true, child: content),
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet,
@ -172,12 +168,14 @@ class AppScaffold extends StatelessWidget {
class PageBackButton extends StatelessWidget {
final List<Shadow>? shadows;
const PageBackButton({super.key, this.shadows});
final VoidCallback? onWillPop;
const PageBackButton({super.key, this.shadows, this.onWillPop});
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: () {
onWillPop?.call();
context.router.maybePop();
},
icon: Icon(

View File

@ -0,0 +1,112 @@
import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/chat.dart';
import 'package:island/pods/call.dart';
import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart';
import 'package:island/widgets/alert.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'call_button.g.dart';
@riverpod
Future<SnRealtimeCall?> ongoingCall(Ref ref, String roomId) async {
try {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/chat/realtime/$roomId');
return SnRealtimeCall.fromJson(resp.data);
} catch (e) {
if (e is DioException && e.response?.statusCode == 404) {
return null;
}
showErrorAlert(e);
return null;
}
}
class AudioCallButton extends HookConsumerWidget {
final String roomId;
const AudioCallButton({super.key, required this.roomId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ongoingCall = ref.watch(ongoingCallProvider(roomId));
final callState = ref.watch(callNotifierProvider);
final callNotifier = ref.read(callNotifierProvider.notifier);
final isLoading = useState(false);
final apiClient = ref.watch(apiClientProvider);
Future<void> handleJoin() async {
isLoading.value = true;
try {
await apiClient.post('/chat/realtime/$roomId');
if (context.mounted) {
context.router.push(CallRoute(roomId: roomId));
}
} catch (e) {
showErrorAlert(e);
} finally {
isLoading.value = false;
}
}
Future<void> handleEnd() async {
isLoading.value = true;
try {
await apiClient.delete('/chat/realtime/$roomId');
callNotifier.dispose(); // Clean up call resources
} catch (e) {
showErrorAlert(e);
} finally {
isLoading.value = false;
}
}
if (isLoading.value) {
return IconButton(
icon: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Theme.of(context).appBarTheme.foregroundColor!,
padding: EdgeInsets.all(4),
),
),
onPressed: null,
);
}
if (callState.isConnected) {
// Show end call button if in call
return IconButton(
icon: const Icon(Icons.call_end),
tooltip: 'End Call',
onPressed: handleEnd,
);
}
if (ongoingCall.value != null) {
// There is an ongoing call, offer to join it directly
return IconButton(
icon: const Icon(Icons.call),
tooltip: 'Join Ongoing Call',
onPressed: () {
if (context.mounted) {
context.router.push(CallRoute(roomId: roomId));
}
},
);
}
// Show join/start call button
return IconButton(
icon: const Icon(Icons.call),
tooltip: 'Start/Join Call',
onPressed: handleJoin,
);
}
}

View File

@ -0,0 +1,151 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'call_button.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$ongoingCallHash() => r'd8a942e6695a7da702daeaa452464c16761ef6e7';
/// 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 [ongoingCall].
@ProviderFor(ongoingCall)
const ongoingCallProvider = OngoingCallFamily();
/// See also [ongoingCall].
class OngoingCallFamily extends Family<AsyncValue<SnRealtimeCall?>> {
/// See also [ongoingCall].
const OngoingCallFamily();
/// See also [ongoingCall].
OngoingCallProvider call(String roomId) {
return OngoingCallProvider(roomId);
}
@override
OngoingCallProvider getProviderOverride(
covariant OngoingCallProvider provider,
) {
return call(provider.roomId);
}
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'ongoingCallProvider';
}
/// See also [ongoingCall].
class OngoingCallProvider extends AutoDisposeFutureProvider<SnRealtimeCall?> {
/// See also [ongoingCall].
OngoingCallProvider(String roomId)
: this._internal(
(ref) => ongoingCall(ref as OngoingCallRef, roomId),
from: ongoingCallProvider,
name: r'ongoingCallProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$ongoingCallHash,
dependencies: OngoingCallFamily._dependencies,
allTransitiveDependencies: OngoingCallFamily._allTransitiveDependencies,
roomId: roomId,
);
OngoingCallProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.roomId,
}) : super.internal();
final String roomId;
@override
Override overrideWith(
FutureOr<SnRealtimeCall?> Function(OngoingCallRef provider) create,
) {
return ProviderOverride(
origin: this,
override: OngoingCallProvider._internal(
(ref) => create(ref as OngoingCallRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
roomId: roomId,
),
);
}
@override
AutoDisposeFutureProviderElement<SnRealtimeCall?> createElement() {
return _OngoingCallProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is OngoingCallProvider && other.roomId == roomId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, roomId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin OngoingCallRef on AutoDisposeFutureProviderRef<SnRealtimeCall?> {
/// The parameter `roomId` of this provider.
String get roomId;
}
class _OngoingCallProviderElement
extends AutoDisposeFutureProviderElement<SnRealtimeCall?>
with OngoingCallRef {
_OngoingCallProviderElement(super.provider);
@override
String get roomId => (origin as OngoingCallProvider).roomId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -0,0 +1,221 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/call.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart';
import 'package:island/widgets/chat/call_participant_tile.dart';
import 'package:styled_widget/styled_widget.dart';
class CallControlsBar extends HookConsumerWidget {
const CallControlsBar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final callState = ref.watch(callNotifierProvider);
final callNotifier = ref.read(callNotifierProvider.notifier);
final userInfo = ref.watch(userInfoProvider);
final actionButtonStyle = ButtonStyle(
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
);
return Card(
margin: const EdgeInsets.only(left: 12, right: 12, top: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Builder(
builder: (context) {
if (callNotifier.localParticipant == null) {
return CircularProgressIndicator().center();
}
return SizedBox(
width: 40,
height: 40,
child:
SpeakingRippleAvatar(
isSpeaking:
callNotifier.localParticipant!.isSpeaking,
audioLevel:
callNotifier.localParticipant!.audioLevel,
pictureId: userInfo.value?.profile.pictureId,
size: 36,
).center(),
);
},
),
],
),
),
IconButton(
icon: Icon(
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
),
onPressed: () {
callNotifier.toggleMicrophone();
},
style: actionButtonStyle,
),
IconButton(
icon: Icon(
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
),
onPressed: () {
callNotifier.toggleCamera();
},
style: actionButtonStyle,
),
IconButton(
icon: Icon(
callState.isScreenSharing
? Icons.stop_screen_share
: Icons.screen_share,
),
onPressed: () {
callNotifier.toggleScreenShare();
},
style: actionButtonStyle,
),
],
).padding(all: 16),
);
}
}
class CallOverlayBar extends HookConsumerWidget {
const CallOverlayBar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final callState = ref.watch(callNotifierProvider);
final callNotifier = ref.read(callNotifierProvider.notifier);
// Only show if connected and not on the call screen
if (!callState.isConnected) return const SizedBox.shrink();
final lastSpeaker =
callNotifier.participants
.where(
(element) => element.remoteParticipant.lastSpokeAt != null,
)
.isEmpty
? callNotifier.participants.first
: callNotifier.participants
.where(
(element) => element.remoteParticipant.lastSpokeAt != null,
)
.fold(
callNotifier.participants.first,
(value, element) =>
element.remoteParticipant.lastSpokeAt != null &&
(value.remoteParticipant.lastSpokeAt == null ||
element.remoteParticipant.lastSpokeAt!
.compareTo(
value
.remoteParticipant
.lastSpokeAt!,
) >
0)
? element
: value,
);
final actionButtonStyle = ButtonStyle(
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
);
return GestureDetector(
child: Card(
margin: EdgeInsets.zero,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Builder(
builder: (context) {
if (callNotifier.localParticipant == null) {
return CircularProgressIndicator().center();
}
return SizedBox(
width: 40,
height: 40,
child:
SpeakingRippleAvatar(
isSpeaking: lastSpeaker.isSpeaking,
audioLevel:
lastSpeaker.remoteParticipant.audioLevel,
pictureId:
lastSpeaker
.participant
.profile
?.account
.profile
.pictureId,
size: 36,
).center(),
);
},
),
const Gap(8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
lastSpeaker.participant.profile?.account.nick ??
'unknown'.tr(),
).bold(),
Text(
formatDuration(callState.duration),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
],
),
),
IconButton(
icon: Icon(
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
),
onPressed: () {
callNotifier.toggleMicrophone();
},
style: actionButtonStyle,
),
IconButton(
icon: Icon(
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
),
onPressed: () {
callNotifier.toggleCamera();
},
style: actionButtonStyle,
),
IconButton(
icon: Icon(
callState.isScreenSharing
? Icons.stop_screen_share
: Icons.screen_share,
),
onPressed: () {
callNotifier.toggleScreenShare();
},
style: actionButtonStyle,
),
],
).padding(all: 16),
),
onTap: () {
context.router.push(CallRoute(roomId: callNotifier.roomId!));
},
);
}
}

View File

@ -0,0 +1,114 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:island/pods/call.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:livekit_client/livekit_client.dart';
class SpeakingRippleAvatar extends StatelessWidget {
final bool isSpeaking;
final double audioLevel;
final String? pictureId;
final double size;
const SpeakingRippleAvatar({
super.key,
required this.isSpeaking,
required this.audioLevel,
required this.pictureId,
this.size = 96,
});
@override
Widget build(BuildContext context) {
final avatarRadius = size / 2;
final clampedLevel = 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)
Container(
width: animatedRadius * 2,
height: animatedRadius * 2,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.green.withOpacity(0.75 + 0.25 * clampedLevel),
),
),
Container(
width: size,
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(shape: BoxShape.circle),
child: ProfilePictureWidget(fileId: pictureId, radius: size / 2),
),
],
);
},
);
}
}
class CallParticipantTile extends StatelessWidget {
final CallParticipantLive live;
const CallParticipantTile({super.key, required this.live});
@override
Widget build(BuildContext context) {
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,
),
),
),
Positioned(
left: 8,
right: 8,
bottom: 8,
child: Text(
live.participant.profile?.account.nick ??
'${'unknown'.tr()}\'s video',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 14, color: Colors.white),
),
),
],
);
} else {
return SpeakingRippleAvatar(
isSpeaking: live.isSpeaking,
audioLevel: audioLevel,
pictureId: live.participant.profile?.account.profile.pictureId,
size: 84,
);
}
}
}

View File

@ -4,6 +4,7 @@ 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/pods/call.dart';
import 'package:island/screens/chat/room.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
@ -46,10 +47,6 @@ class MessageItem extends HookConsumerWidget {
isCurrentUser
? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.5)
: Theme.of(context).colorScheme.surfaceContainer;
final linkColor = Color.alphaBlend(
Theme.of(context).colorScheme.primary,
containerColor,
);
final hasBackground =
ref.watch(backgroundImageFileProvider).valueOrNull != null;
@ -196,16 +193,8 @@ class MessageItem extends HookConsumerWidget {
textColor: textColor,
isReply: false,
).padding(vertical: 4),
if (remoteMessage.content?.isNotEmpty ?? false)
MarkdownTextContent(
content: remoteMessage.content!,
isSelectable: true,
linkStyle: TextStyle(color: linkColor),
textStyle: TextStyle(
color: textColor,
fontSize: 14,
),
),
if (_MessageItemContent.hasContent(remoteMessage))
_MessageItemContent(item: remoteMessage),
if (remoteMessage.attachments.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
@ -360,7 +349,10 @@ class MessageQuoteWidget extends HookConsumerWidget {
: message.toRemoteMessage().forwardedMessageId!,
),
builder: (context, snapshot) {
if (snapshot.hasData) {
final remoteMessage =
snapshot.hasData ? snapshot.data!.toRemoteMessage() : null;
if (remoteMessage != null) {
return ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(8)),
child: Container(
@ -378,7 +370,7 @@ class MessageQuoteWidget extends HookConsumerWidget {
children: [
Icon(Symbols.reply, size: 16, color: textColor),
Text(
'Replying to ${snapshot.data!.toRemoteMessage().sender.account.nick}',
'Replying to ${remoteMessage.sender.account.nick}',
).textColor(textColor).bold(),
],
).padding(right: 8)
@ -389,16 +381,12 @@ class MessageQuoteWidget extends HookConsumerWidget {
children: [
Icon(Symbols.forward, size: 16, color: textColor),
Text(
'Forwarded from ${snapshot.data!.toRemoteMessage().sender.account.nick}',
'Forwarded from ${remoteMessage.sender.account.nick}',
).textColor(textColor).bold(),
],
).padding(right: 8),
if (snapshot.data!.toRemoteMessage().content?.isNotEmpty ??
false)
Text(
snapshot.data!.toRemoteMessage().content!,
style: TextStyle(color: textColor),
),
if (_MessageItemContent.hasContent(remoteMessage))
_MessageItemContent(item: remoteMessage),
],
),
),
@ -410,3 +398,54 @@ class MessageQuoteWidget extends HookConsumerWidget {
);
}
}
class _MessageItemContent extends StatelessWidget {
final SnChatMessage item;
const _MessageItemContent({required this.item});
@override
Widget build(BuildContext context) {
switch (item.type) {
case 'call.start':
case 'call.ended':
return _MessageContentCall(
isEnded: item.type == 'call.ended',
duration: item.meta['duration']?.toDouble(),
);
case 'text':
default:
return MarkdownTextContent(content: item.content!);
}
}
static bool hasContent(SnChatMessage item) {
return item.type != 'text' || (item.content?.isNotEmpty ?? false);
}
}
class _MessageContentCall extends StatelessWidget {
final bool isEnded;
final double? duration;
const _MessageContentCall({required this.isEnded, this.duration});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isEnded ? Symbols.call_end : Symbols.phone_in_talk,
size: 16,
color: Theme.of(context).colorScheme.primary,
),
Gap(4),
Text(
isEnded
? 'Call ended after ${formatDuration(Duration(seconds: duration!.toInt()))}'
: 'Call started',
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
],
);
}
}

View File

@ -42,11 +42,16 @@ class CheckInWidget extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final todayResult = ref.watch(checkInResultTodayProvider);
Future<void> checkIn() async {
Future<void> checkIn({String? captchatTk}) async {
final client = ref.read(apiClientProvider);
try {
await client.post('/accounts/me/check-in');
await client.post(
'/accounts/me/check-in',
data: captchatTk == null ? null : jsonEncode(captchatTk),
);
ref.invalidate(checkInResultTodayProvider);
final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.fetchUser();
} catch (err) {
if (err is DioException) {
if (err.response?.statusCode == 423 && context.mounted) {
@ -54,14 +59,7 @@ class CheckInWidget extends HookConsumerWidget {
context,
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
if (captchaTk == null) return;
await client.post(
'/accounts/me/check-in',
data: jsonEncode(captchaTk),
);
ref.invalidate(checkInResultTodayProvider);
final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.fetchUser();
return;
return await checkIn(captchatTk: captchaTk);
}
}
showErrorAlert(err);
@ -139,7 +137,7 @@ class CheckInWidget extends HookConsumerWidget {
if (todayResult.valueOrNull == null) {
checkIn();
} else {
context.router.push(MyselfEventCalendarRoute());
context.router.push(EventCalanderRoute(name: 'me'));
}
},
icon: AnimatedSwitcher(

View File

@ -14,12 +14,13 @@ class PostListNotifier extends _$PostListNotifier
with CursorPagingNotifierMixin<SnPost> {
static const int _pageSize = 20;
PostListNotifier({this.pubName});
final String? pubName;
String? pubName;
@override
Future<CursorPagingData<SnPost>> build() => fetch(cursor: null);
Future<CursorPagingData<SnPost>> build(String? pubName) {
this.pubName = pubName;
return fetch(cursor: null);
}
@override
Future<CursorPagingData<SnPost>> fetch({required String? cursor}) async {
@ -55,9 +56,9 @@ class SliverPostList extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return PagingHelperSliverView(
provider: postListNotifierProvider,
futureRefreshable: postListNotifierProvider.future,
notifierRefreshable: postListNotifierProvider.notifier,
provider: postListNotifierProvider(pubName),
futureRefreshable: postListNotifierProvider(pubName).future,
notifierRefreshable: postListNotifierProvider(pubName).notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder(
itemCount: widgetCount,

View File

@ -6,24 +6,174 @@ part of 'post_list.dart';
// RiverpodGenerator
// **************************************************************************
String _$postListNotifierHash() => r'88aab109f36c7489c336fea522bffafa10869253';
String _$postListNotifierHash() => r'6568b7a5afad71551009d9bc7af26afb4b07c9e5';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$PostListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPost>> {
late final String? pubName;
FutureOr<CursorPagingData<SnPost>> build(String? pubName);
}
/// See also [PostListNotifier].
@ProviderFor(PostListNotifier)
final postListNotifierProvider = AutoDisposeAsyncNotifierProvider<
PostListNotifier,
CursorPagingData<SnPost>
>.internal(
PostListNotifier.new,
name: r'postListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$postListNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
const postListNotifierProvider = PostListNotifierFamily();
/// See also [PostListNotifier].
class PostListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnPost>>> {
/// See also [PostListNotifier].
const PostListNotifierFamily();
/// See also [PostListNotifier].
PostListNotifierProvider call(String? pubName) {
return PostListNotifierProvider(pubName);
}
@override
PostListNotifierProvider getProviderOverride(
covariant PostListNotifierProvider provider,
) {
return call(provider.pubName);
}
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'postListNotifierProvider';
}
/// See also [PostListNotifier].
class PostListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
PostListNotifier,
CursorPagingData<SnPost>
> {
/// See also [PostListNotifier].
PostListNotifierProvider(String? pubName)
: this._internal(
() => PostListNotifier()..pubName = pubName,
from: postListNotifierProvider,
name: r'postListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$postListNotifierHash,
dependencies: PostListNotifierFamily._dependencies,
allTransitiveDependencies:
PostListNotifierFamily._allTransitiveDependencies,
pubName: pubName,
);
PostListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.pubName,
}) : super.internal();
final String? pubName;
@override
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
covariant PostListNotifier notifier,
) {
return notifier.build(pubName);
}
@override
Override overrideWith(PostListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: PostListNotifierProvider._internal(
() => create()..pubName = pubName,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
pubName: pubName,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
PostListNotifier,
CursorPagingData<SnPost>
>
createElement() {
return _PostListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PostListNotifierProvider && other.pubName == pubName;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, pubName.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PostListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPost>> {
/// The parameter `pubName` of this provider.
String? get pubName;
}
class _PostListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
PostListNotifier,
CursorPagingData<SnPost>
>
with PostListNotifierRef {
_PostListNotifierProviderElement(super.provider);
@override
String? get pubName => (origin as PostListNotifierProvider).pubName;
}
typedef _$PostListNotifier = AutoDisposeAsyncNotifier<CursorPagingData<SnPost>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,13 +1,59 @@
import 'package:dio/dio.dart';
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/content/paging_helper_ext.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/response.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
part 'post_replies.g.dart';
@riverpod
class PostRepliesNotifier extends _$PostRepliesNotifier
with CursorPagingNotifierMixin<SnPost> {
static const int _pageSize = 20;
PostRepliesNotifier();
String? _postId;
@override
Future<CursorPagingData<SnPost>> build(String postId) {
_postId = postId;
return fetch(cursor: null);
}
@override
Future<CursorPagingData<SnPost>> fetch({required String? cursor}) async {
if (_postId == null) {
throw StateError('PostRepliesNotifier must be initialized with postId');
}
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/posts/$_postId/replies',
queryParameters: {'offset': offset, 'take': _pageSize},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final posts = data.map((json) => SnPost.fromJson(json)).toList();
final hasMore = offset + posts.length < total;
final nextCursor = hasMore ? (offset + posts.length).toString() : null;
return CursorPagingData(
items: posts,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
class PostRepliesList extends HookConsumerWidget {
final String postId;
@ -15,96 +61,46 @@ class PostRepliesList extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final postAsync = ref.watch(postRepliesProvider(postId));
final isWide = isWideScreen(context);
return postAsync.when(
data:
(controller) => SliverInfiniteList(
itemCount: controller.posts.length,
isLoading: controller.isLoading,
hasReachedMax: controller.hasReachedMax,
onFetchData: controller.fetchMore,
itemBuilder: (context, index) {
final post = controller.posts[index];
return PostItem(
item: post,
backgroundColor: isWide ? Colors.transparent : null,
);
},
separatorBuilder: (_, __) => const Divider(height: 1),
emptyBuilder: (context) {
return SliverToBoxAdapter(
child: Column(
children: [
Text(
'No replies',
textAlign: TextAlign.center,
).fontSize(18).bold(),
Text('Why not start a discussion?'),
],
).padding(vertical: 16),
);
},
),
loading:
() => SliverFillRemaining(
child: const Center(child: CircularProgressIndicator()),
),
error:
(e, _) => SliverFillRemaining(
child: ResponseErrorWidget(
error: e,
onRetry: () {
ref.invalidate(postRepliesProvider(postId));
},
),
),
return PagingHelperSliverView(
provider: postRepliesNotifierProvider(postId),
futureRefreshable: postRepliesNotifierProvider(postId).future,
notifierRefreshable: postRepliesNotifierProvider(postId).notifier,
contentBuilder: (data, widgetCount, endItemView) {
if (data.items.isEmpty) {
return SliverToBoxAdapter(
child: Column(
children: [
Text(
'No replies',
textAlign: TextAlign.center,
).fontSize(18).bold(),
const Text('Why not start a discussion?'),
],
).padding(vertical: 16),
);
}
return SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
return Column(
children: [
PostItem(
item: data.items[index],
backgroundColor: isWide ? Colors.transparent : null,
),
const Divider(height: 1),
],
);
},
);
},
);
}
}
final postRepliesProvider =
FutureProviderFamily<_PostRepliesController, String>((ref, postId) async {
final client = ref.watch(apiClientProvider);
final controller = _PostRepliesController(client, postId);
await controller.fetchMore();
return controller;
});
class _PostRepliesController {
_PostRepliesController(this._dio, this.parentId);
final Dio _dio;
final String parentId;
final List<SnPost> posts = [];
bool isLoading = false;
bool hasReachedMax = false;
int offset = 0;
final int take = 20;
int total = 0;
Future<void> fetchMore() async {
if (isLoading || hasReachedMax) return;
isLoading = true;
final response = await _dio.get(
'/posts/$parentId/replies',
queryParameters: {'offset': offset, 'take': take},
);
final List<SnPost> fetched =
(response.data as List)
.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
.toList();
final headerTotal = int.tryParse(response.headers['x-total']?.first ?? '');
if (headerTotal != null) total = headerTotal;
posts.addAll(fetched);
offset += fetched.length;
if (posts.length >= total) hasReachedMax = true;
isLoading = false;
}
}

View File

@ -0,0 +1,180 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'post_replies.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$postRepliesNotifierHash() =>
r'49c178102ec0a4136974a0e9a8f090f511abd542';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$PostRepliesNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPost>> {
late final String postId;
FutureOr<CursorPagingData<SnPost>> build(String postId);
}
/// See also [PostRepliesNotifier].
@ProviderFor(PostRepliesNotifier)
const postRepliesNotifierProvider = PostRepliesNotifierFamily();
/// See also [PostRepliesNotifier].
class PostRepliesNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnPost>>> {
/// See also [PostRepliesNotifier].
const PostRepliesNotifierFamily();
/// See also [PostRepliesNotifier].
PostRepliesNotifierProvider call(String postId) {
return PostRepliesNotifierProvider(postId);
}
@override
PostRepliesNotifierProvider getProviderOverride(
covariant PostRepliesNotifierProvider provider,
) {
return call(provider.postId);
}
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'postRepliesNotifierProvider';
}
/// See also [PostRepliesNotifier].
class PostRepliesNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
PostRepliesNotifier,
CursorPagingData<SnPost>
> {
/// See also [PostRepliesNotifier].
PostRepliesNotifierProvider(String postId)
: this._internal(
() => PostRepliesNotifier()..postId = postId,
from: postRepliesNotifierProvider,
name: r'postRepliesNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$postRepliesNotifierHash,
dependencies: PostRepliesNotifierFamily._dependencies,
allTransitiveDependencies:
PostRepliesNotifierFamily._allTransitiveDependencies,
postId: postId,
);
PostRepliesNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.postId,
}) : super.internal();
final String postId;
@override
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
covariant PostRepliesNotifier notifier,
) {
return notifier.build(postId);
}
@override
Override overrideWith(PostRepliesNotifier Function() create) {
return ProviderOverride(
origin: this,
override: PostRepliesNotifierProvider._internal(
() => create()..postId = postId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
postId: postId,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
PostRepliesNotifier,
CursorPagingData<SnPost>
>
createElement() {
return _PostRepliesNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PostRepliesNotifierProvider && other.postId == postId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, postId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PostRepliesNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPost>> {
/// The parameter `postId` of this provider.
String get postId;
}
class _PostRepliesNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
PostRepliesNotifier,
CursorPagingData<SnPost>
>
with PostRepliesNotifierRef {
_PostRepliesNotifierProviderElement(super.provider);
@override
String get postId => (origin as PostRepliesNotifierProvider).postId;
}
// 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

@ -10,12 +10,14 @@
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
#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 <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <super_native_extensions/super_native_extensions_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <volume_controller/volume_controller_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar =
@ -30,6 +32,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_udid_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterUdidPlugin");
flutter_udid_plugin_register_with_registrar(flutter_udid_registrar);
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
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);
@ -48,4 +53,7 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) volume_controller_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin");
volume_controller_plugin_register_with_registrar(volume_controller_registrar);
}

View File

@ -7,12 +7,14 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_platform_alert
flutter_udid
flutter_webrtc
irondash_engine_context
media_kit_libs_linux
media_kit_video
sqlite3_flutter_libs
super_native_extensions
url_launcher_linux
volume_controller
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation
import bitsdojo_window_macos
import connectivity_plus
import device_info_plus
import file_picker
import file_selector_macos
@ -14,7 +15,9 @@ import firebase_messaging
import flutter_inappwebview_macos
import flutter_platform_alert
import flutter_udid
import flutter_webrtc
import irondash_engine_context
import livekit_client
import media_kit_libs_macos_video
import media_kit_video
import package_info_plus
@ -29,6 +32,7 @@ import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
@ -37,7 +41,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin"))
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin"))
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))

View File

@ -1,6 +1,8 @@
PODS:
- bitsdojo_window_macos (0.0.1):
- FlutterMacOS
- connectivity_plus (0.0.1):
- FlutterMacOS
- croppy (0.0.1):
- FlutterMacOS
- device_info_plus (0.0.1):
@ -50,6 +52,9 @@ PODS:
- flutter_udid (0.0.1):
- FlutterMacOS
- SAMKeychain
- flutter_webrtc (0.14.0):
- FlutterMacOS
- WebRTC-SDK (= 125.6422.07)
- FlutterMacOS (1.0.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
@ -80,6 +85,10 @@ PODS:
- GoogleUtilities/Privacy
- irondash_engine_context (0.0.1):
- FlutterMacOS
- livekit_client (2.4.7):
- flutter_webrtc
- FlutterMacOS
- WebRTC-SDK (= 125.6422.07)
- media_kit_libs_macos_video (1.0.4):
- FlutterMacOS
- media_kit_video (0.0.1):
@ -133,9 +142,11 @@ PODS:
- FlutterMacOS
- wakelock_plus (0.0.1):
- FlutterMacOS
- WebRTC-SDK (125.6422.07)
DEPENDENCIES:
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
@ -145,8 +156,10 @@ DEPENDENCIES:
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
- flutter_platform_alert (from `Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos`)
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`)
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
@ -173,10 +186,13 @@ SPEC REPOS:
- PromisesObjC
- SAMKeychain
- sqlite3
- WebRTC-SDK
EXTERNAL SOURCES:
bitsdojo_window_macos:
:path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
connectivity_plus:
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
croppy:
:path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
device_info_plus:
@ -195,10 +211,14 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos
flutter_udid:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
flutter_webrtc:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos
FlutterMacOS:
:path: Flutter/ephemeral
irondash_engine_context:
:path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos
livekit_client:
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
media_kit_libs_macos_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
media_kit_video:
@ -224,6 +244,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
croppy: d9bfc8c02f3cd1851f669a421df298a474b78f43
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
@ -238,10 +259,12 @@ SPEC CHECKSUMS:
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
flutter_webrtc: a7eeb54859e672228c28f4b48b1fb61561976ea3
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
livekit_client: c5fc13c397f17de76534280744ddd8c232915057
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
@ -258,6 +281,7 @@ SPEC CHECKSUMS:
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
WebRTC-SDK: dff00a3892bc570b6014e046297782084071657e
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009

View File

@ -263,7 +263,6 @@
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
@ -583,9 +582,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = W7HPZ53V6B;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
@ -716,9 +719,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = W7HPZ53V6B;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
@ -737,9 +744,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = W7HPZ53V6B;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",

View File

@ -8,6 +8,8 @@
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>

View File

@ -26,6 +26,8 @@
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.social-networking</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>

View File

@ -4,5 +4,17 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

View File

@ -18,7 +18,7 @@ packages:
source: hosted
version: "1.3.55"
analyzer:
dependency: transitive
dependency: "direct overridden"
description:
name: analyzer
sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
@ -26,13 +26,13 @@ packages:
source: hosted
version: "7.3.0"
analyzer_plugin:
dependency: transitive
dependency: "direct overridden"
description:
name: analyzer_plugin
sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4
sha256: "1d460d14e3c2ae36dc2b32cef847c4479198cf87704f63c3c3c8150ee50c3916"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
version: "0.12.0"
animations:
dependency: "direct main"
description:
@ -69,26 +69,26 @@ packages:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.12.0"
version: "2.13.0"
auto_route:
dependency: "direct main"
description:
name: auto_route
sha256: "89bc5d17d8c575399891194b8cd02b39f52a8512c730052f17ebe443cdcb9109"
sha256: eae18fcd3e3762eb6074a3560c0f411d1e36bd9f8d3eed9c15ed1c577e8d1815
url: "https://pub.dev"
source: hosted
version: "10.0.1"
version: "10.1.0"
auto_route_generator:
dependency: "direct dev"
description:
name: auto_route_generator
sha256: "8e622d26dc6be4bf496d47969e3e9ba555c3abcf2290da6abfa43cbd4f57fa52"
sha256: "9e3846fcbeacba5c362557328dd8c8fbc953b6a0cbc3395365e8d8f92eea29c4"
url: "https://pub.dev"
source: hosted
version: "10.0.1"
version: "10.1.0"
avatar_stack:
dependency: "direct main"
description:
@ -313,6 +313,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
connectivity_plus:
dependency: transitive
description:
name: connectivity_plus
sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
connectivity_plus_platform_interface:
dependency: transitive
description:
name: connectivity_plus_platform_interface
sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
convert:
dependency: transitive
description:
@ -386,7 +402,7 @@ packages:
source: hosted
version: "0.7.5"
custom_lint_visitor:
dependency: transitive
dependency: "direct overridden"
description:
name: custom_lint_visitor
sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8"
@ -401,6 +417,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0"
dart_webrtc:
dependency: transitive
description:
name: dart_webrtc
sha256: "5b76fd85ac95d6f5dee3e7d7de8d4b51bfbec1dc73804647c6aebb52d6297116"
url: "https://pub.dev"
source: hosted
version: "1.5.3+hotfix.2"
dbus:
dependency: transitive
description:
@ -525,10 +549,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.3.3"
ffi:
dependency: transitive
description:
@ -845,6 +869,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_webrtc:
dependency: "direct main"
description:
name: flutter_webrtc
sha256: dd47ca103b5b6217771e6277882674276d9621bbf9eb23da3c03898b507844e3
url: "https://pub.dev"
source: hosted
version: "0.14.1"
font_awesome_flutter:
dependency: transitive
description:
@ -1041,10 +1073,10 @@ packages:
dependency: transitive
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.19.0"
version: "0.20.2"
io:
dependency: transitive
description:
@ -1097,10 +1129,10 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.dev"
source: hosted
version: "10.0.8"
version: "10.0.9"
leak_tracker_flutter_testing:
dependency: transitive
description:
@ -1133,6 +1165,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.1"
livekit_client:
dependency: "direct main"
description:
name: livekit_client
sha256: dc8fe8295353a6f4c96915a49faece4ede2334d2b2bdf5ed4b985e07507bdf09
url: "https://pub.dev"
source: hosted
version: "2.4.7"
logging:
dependency: transitive
description:
@ -1185,10 +1225,10 @@ packages:
dependency: "direct main"
description:
name: material_symbols_icons
sha256: d45b6c36c3effa8cb51b1afb8698107d5ff1f88fa4631428f34a8a01abc295d7
sha256: "7c50901b39d1ad645ee25d920aed008061e1fd541a897b4ebf2c01d966dbf16b"
url: "https://pub.dev"
source: hosted
version: "4.2815.0"
version: "4.2815.1"
media_kit:
dependency: "direct main"
description:
@ -1269,6 +1309,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mime_type:
dependency: transitive
description:
name: mime_type
sha256: d652b613e84dac1af28030a9fba82c0999be05b98163f9e18a0849c6e63838bb
url: "https://pub.dev"
source: hosted
version: "1.0.1"
modal_bottom_sheet:
dependency: "direct main"
description:
@ -1285,6 +1333,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
nm:
dependency: transitive
description:
name: nm
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
octo_image:
dependency: transitive
description:
@ -1437,6 +1493,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.2"
protobuf:
dependency: transitive
description:
name: protobuf
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
provider:
dependency: transitive
description:
@ -1565,6 +1629,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.1"
sdp_transform:
dependency: transitive
description:
name: sdp_transform
sha256: "73e412a5279a5c2de74001535208e20fff88f225c9a4571af0f7146202755e45"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
shared_preferences:
dependency: "direct main"
description:
@ -1734,10 +1806,10 @@ packages:
dependency: transitive
description:
name: sqlite3
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
sha256: c0503c69b44d5714e6abbf4c1f51a3c3cc42b75ce785f44404765e4635481d38
url: "https://pub.dev"
source: hosted
version: "2.7.5"
version: "2.7.6"
sqlite3_flutter_libs:
dependency: transitive
description:
@ -1854,10 +1926,10 @@ packages:
dependency: "direct main"
description:
name: table_calendar
sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63
sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
version: "3.2.0"
term_glyph:
dependency: transitive
description:
@ -2023,10 +2095,10 @@ packages:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331"
url: "https://pub.dev"
source: hosted
version: "1.1.16"
version: "1.1.17"
vector_math:
dependency: transitive
description:
@ -2055,18 +2127,18 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "14.3.1"
version: "15.0.0"
volume_controller:
dependency: transitive
description:
name: volume_controller
sha256: e82fd689bb8e1fe8e64be3fa5946ff8699058f8cf9f4c1679acdba20cda7f5bd
sha256: d75039e69c0d90e7810bfd47e3eedf29ff8543ea7a10392792e81f9bded7edf5
url: "https://pub.dev"
source: hosted
version: "3.3.3"
version: "3.4.0"
wakelock_plus:
dependency: transitive
description:
@ -2115,6 +2187,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webrtc_interface:
dependency: transitive
description:
name: webrtc_interface
sha256: "86fe3afc81a08481dfb25cf14a5a94e27062ecef25544783f352c914e0bbc1ca"
url: "https://pub.dev"
source: hosted
version: "1.2.2+hotfix.2"
win32:
dependency: transitive
description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 3.0.0+93
version: 3.0.0+95
environment:
sdk: ^3.7.2
@ -101,6 +101,8 @@ dependencies:
dismissible_page: ^1.0.2
super_sliver_list: ^0.4.1
super_clipboard: ^0.9.0-dev.6
flutter_webrtc: ^0.14.1
livekit_client: ^2.4.7
dev_dependencies:
flutter_test:
@ -122,6 +124,13 @@ dev_dependencies:
drift_dev: ^2.26.0
flutter_launcher_icons: ^0.14.3
dependency_overrides:
# https://github.com/dart-lang/sdk/issues/60784#issuecomment-2904784415
analyzer: 7.3.0
analyzer_plugin: 0.12.0
# https://github.com/dart-lang/sdk/issues/60784#issuecomment-2906872272
custom_lint_visitor: 1.0.0+7.3.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View File

@ -1,6 +1,6 @@
{
"name": "island",
"short_name": "island",
"name": "Solian",
"short_name": "Solian",
"start_url": ".",
"display": "standalone",
"background_color": "#ffffff",

View File

@ -4,7 +4,7 @@ project(island LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "island")
set(BINARY_NAME "Solian")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.

View File

@ -7,12 +7,15 @@
#include "generated_plugin_registrant.h"
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <firebase_core/firebase_core_plugin_c_api.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
#include <flutter_udid/flutter_udid_plugin_c_api.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <irondash_engine_context/irondash_engine_context_plugin_c_api.h>
#include <livekit_client/live_kit_plugin.h>
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
#include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
@ -23,6 +26,8 @@
void RegisterPlugins(flutter::PluginRegistry* registry) {
BitsdojoWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FirebaseCorePluginCApiRegisterWithRegistrar(
@ -33,8 +38,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterPlatformAlertPlugin"));
FlutterUdidPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
FlutterWebRTCPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
IrondashEngineContextPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi"));
LiveKitPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LiveKitPlugin"));
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
MediaKitVideoPluginCApiRegisterWithRegistrar(

View File

@ -4,12 +4,15 @@
list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_windows
connectivity_plus
file_selector_windows
firebase_core
flutter_inappwebview_windows
flutter_platform_alert
flutter_udid
flutter_webrtc
irondash_engine_context
livekit_client
media_kit_libs_windows_video
media_kit_video
sqlite3_flutter_libs

View File

@ -90,12 +90,12 @@ BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "com.example" "\0"
VALUE "FileDescription", "island" "\0"
VALUE "FileDescription", "Solian" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "island" "\0"
VALUE "InternalName", "Solian" "\0"
VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0"
VALUE "OriginalFilename", "island.exe" "\0"
VALUE "ProductName", "island" "\0"
VALUE "OriginalFilename", "Solian.exe" "\0"
VALUE "ProductName", "Solian" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END

View File

@ -30,7 +30,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.Create(L"island", origin, size)) {
if (!window.Create(L"Solian", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);