Compare commits

..

4 Commits

24 changed files with 846 additions and 283 deletions

View File

@ -89,31 +89,14 @@
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.", "authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
"authFactorPin": "Pin Code", "authFactorPin": "Pin Code",
"authFactorPinDescription": "It consists of 6 digits. It cannot be used to log in. When performing some dangerous operations, the system will ask you to enter this PIN for confirmation.", "authFactorPinDescription": "It consists of 6 digits. It cannot be used to log in. When performing some dangerous operations, the system will ask you to enter this PIN for confirmation.",
"realms": "Realms",
"createRealm": "Create a Realm",
"createRealmHint": "Meet friends with same interests, build communities, and more.",
"editRealm": "Edit Realm",
"deleteRealm": "Delete Realm",
"deleteRealmHint": "Are you sure to delete this realm? This will also deleted all the channels, publishers, and posts under this realm.",
"explore": "Explore", "explore": "Explore",
"exploreFilterSubscriptions": "Subscriptions", "exploreFilterSubscriptions": "Subscriptions",
"exploreFilterFriends": "Friends", "exploreFilterFriends": "Friends",
"discoverCommunities": "Discover Communities", "discover": "Discover",
"account": "Account", "account": "Account",
"name": "Name", "name": "Name",
"slug": "Slug", "slug": "Slug",
"slugHint": "The slug will be used in the URL to access this resource, it should be unique and URL safe.", "slugHint": "The slug will be used in the URL to access this resource, it should be unique and URL safe.",
"createChatRoom": "Create a Room",
"editChatRoom": "Edit Room",
"deleteChatRoom": "Delete Room",
"deleteChatRoomHint": "Are you sure to delete this room? This action cannot be undone.",
"chat": "Chat",
"chatTabAll": "All",
"chatTabDirect": "Direct Messages",
"chatTabGroup": "Group Chats",
"chatMessageHint": "Message in {}",
"chatDirectMessageHint": "Message to {}",
"directMessage": "Direct Message",
"loading": "Loading...", "loading": "Loading...",
"descriptionNone": "No description yet.", "descriptionNone": "No description yet.",
"invites": "Invites", "invites": "Invites",
@ -248,7 +231,6 @@
"uploadingProgress": "Uploading {} of {}", "uploadingProgress": "Uploading {} of {}",
"uploadAll": "Upload All", "uploadAll": "Upload All",
"stickerCopyPlaceholder": "Copy Placeholder", "stickerCopyPlaceholder": "Copy Placeholder",
"realmSelection": "Select a Realm",
"individual": "Individual", "individual": "Individual",
"firstPostBadgeName": "First Post", "firstPostBadgeName": "First Post",
"firstPostBadgeDescription": "Created your first post on Solar Network", "firstPostBadgeDescription": "Created your first post on Solar Network",
@ -304,10 +286,6 @@
"levelingProgressExperience": "{} EXP", "levelingProgressExperience": "{} EXP",
"levelingProgressLevel": "Level {}", "levelingProgressLevel": "Level {}",
"fileUploadingProgress": "Uploading file #{}: {}%", "fileUploadingProgress": "Uploading file #{}: {}%",
"removeChatMember": "Remove Chat Room Member",
"removeChatMemberHint": "Are you sure to remove this member from the room?",
"removeRealmMember": "Remove Realm Member",
"removeRealmMemberHint": "Are you sure to remove this member from the realm?",
"memberRole": "Member Role", "memberRole": "Member Role",
"memberRoleHint": "Greater number has higher permission.", "memberRoleHint": "Greater number has higher permission.",
"memberRoleEdit": "Edit role for @{}", "memberRoleEdit": "Edit role for @{}",
@ -315,10 +293,6 @@
"openLinkConfirmDescription": "You're going to leave the Solar Network and open the link ({}) in your browser. It is not related to Solar Network. Beware of phishing and scams.", "openLinkConfirmDescription": "You're going to leave the Solar Network and open the link ({}) in your browser. It is not related to Solar Network. Beware of phishing and scams.",
"brokenLink": "Unable open link {}... It might be broken or missing uri parts...", "brokenLink": "Unable open link {}... It might be broken or missing uri parts...",
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copy to clipboard",
"leaveChatRoom": "Leave Chat Room",
"leaveChatRoomHint": "Are you sure to leave this chat room?",
"leaveRealm": "Leave Realm",
"leaveRealmHint": "Are you sure to leave this realm?",
"walletNotFound": "Wallet not found", "walletNotFound": "Wallet not found",
"walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.", "walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.",
"walletCreate": "Create a Wallet", "walletCreate": "Create a Wallet",
@ -330,12 +304,6 @@
"settingsBackgroundImageClear": "Clear Background Image", "settingsBackgroundImageClear": "Clear Background Image",
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image", "settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
"messageNone": "No content to display", "messageNone": "No content to display",
"unreadMessages": {
"one": "{} unread message",
"other": "{} unread messages"
},
"chatBreakNone": "None",
"settingsRealmCompactView": "Compact Realm View",
"settingsMixedFeed": "Mixed Feed", "settingsMixedFeed": "Mixed Feed",
"settingsAutoTranslate": "Auto Translate", "settingsAutoTranslate": "Auto Translate",
"settingsHideBottomNav": "Hide Bottom Navigation", "settingsHideBottomNav": "Hide Bottom Navigation",
@ -378,7 +346,6 @@
"postVisibilityUnlisted": "Unlisted", "postVisibilityUnlisted": "Unlisted",
"postVisibilityPrivate": "Private", "postVisibilityPrivate": "Private",
"postTruncated": "Content truncated, tap to view full post", "postTruncated": "Content truncated, tap to view full post",
"copyMessage": "Copy Message",
"authFactor": "Authentication Factor", "authFactor": "Authentication Factor",
"authFactorDelete": "Delete the Factor", "authFactorDelete": "Delete the Factor",
"authFactorDeleteHint": "Are you sure you want to delete this authentication factor? This action cannot be undone.", "authFactorDeleteHint": "Are you sure you want to delete this authentication factor? This action cannot be undone.",
@ -411,10 +378,6 @@
"authDeviceLabelHint": "Enter a name for this device", "authDeviceLabelHint": "Enter a name for this device",
"authDeviceSwipeEditHint": "Swipe left to edit label", "authDeviceSwipeEditHint": "Swipe left to edit label",
"authDeviceSwipeLogoutHint": "Swipe right to logout device", "authDeviceSwipeLogoutHint": "Swipe right to logout device",
"typingHint": {
"one": "{} is typing...",
"other": "{} are typing..."
},
"settingsAppearance": "Appearance", "settingsAppearance": "Appearance",
"settingsServer": "Server", "settingsServer": "Server",
"settingsBehavior": "Behavior", "settingsBehavior": "Behavior",
@ -476,21 +439,6 @@
"contactMethodSetPrimary": "Set as Primary", "contactMethodSetPrimary": "Set as Primary",
"contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications", "contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications",
"contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.", "contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.",
"chatNotifyLevel": "Notify Level",
"chatNotifyLevelDescription": "Decide how many notifications you will receive.",
"chatNotifyLevelAll": "All",
"chatNotifyLevelMention": "Mentions",
"chatNotifyLevelNone": "None",
"chatNotifyLevelUpdated": "The notify level has been updated to {}.",
"chatBreak": "Take a Break",
"chatBreakDescription": "Set a time, before that time, your notification level will be metions only, to take a break of the current topic they're talking about.",
"chatBreakClear": "Clear the break time",
"chatBreakHour": "{} break",
"chatBreakDay": "{} day break",
"chatBreakSet": "Break set for {}",
"chatBreakCleared": "Chat break has been cleared.",
"chatBreakCustom": "Custom duration",
"chatBreakEnterMinutes": "Enter minutes",
"firstName": "First Name", "firstName": "First Name",
"middleName": "Middle Name", "middleName": "Middle Name",
"lastName": "Last Name", "lastName": "Last Name",
@ -572,29 +520,17 @@
"quickActions": "Quick Actions", "quickActions": "Quick Actions",
"post": "Post", "post": "Post",
"copy": "Copy", "copy": "Copy",
"sendToChat": "Send to Chat",
"failedToShareToPost": "Failed to share to post: {}", "failedToShareToPost": "Failed to share to post: {}",
"shareToChatComingSoon": "Share to chat functionality coming soon", "shareToChatComingSoon": "Share to chat functionality coming soon",
"failedToShareToChat": "Failed to share to chat: {}",
"shareToSpecificChatComingSoon": "Share to {} coming soon",
"directChat": "Direct Chat",
"systemShareComingSoon": "System share functionality coming soon", "systemShareComingSoon": "System share functionality coming soon",
"failedToShareToSystem": "Failed to share to system: {}", "failedToShareToSystem": "Failed to share to system: {}",
"failedToCopy": "Failed to copy: {}", "failedToCopy": "Failed to copy: {}",
"noChatRoomsAvailable": "No chat rooms available",
"failedToLoadChats": "Failed to load chats",
"contentToShare": "Content to share:", "contentToShare": "Content to share:",
"unknownChat": "Unknown Chat",
"addAdditionalMessage": "Add additional message...",
"uploadingFiles": "Uploading files...", "uploadingFiles": "Uploading files...",
"sharedSuccessfully": "Shared successfully!",
"shareSuccess": "Shared successfully!", "shareSuccess": "Shared successfully!",
"shareToSpecificChatSuccess": "Shared to {} successfully!",
"wouldYouLikeToGoToChat": "Would you like to go to the chat?", "wouldYouLikeToGoToChat": "Would you like to go to the chat?",
"no": "No", "no": "No",
"yes": "Yes", "yes": "Yes",
"navigateToChat": "Navigate to Chat",
"wouldYouLikeToNavigateToChat": "Would you like to navigate to the chat?",
"abuseReport": "Report", "abuseReport": "Report",
"abuseReportTitle": "Report Content", "abuseReportTitle": "Report Content",
"abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.", "abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.",

View File

@ -29,6 +29,12 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface.
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
log('Handling a background message: ${message.messageId}');
}
void main() async { void main() async {
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
@ -43,6 +49,7 @@ void main() async {
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
log("[SplashScreen] Firebase is ready!"); log("[SplashScreen] Firebase is ready!");
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
@ -151,17 +158,30 @@ class IslandApp extends HookConsumerWidget {
} }
useEffect(() { useEffect(() {
Future(() async { // When the app is opened from a terminated state.
RemoteMessage? initialMessage = FirebaseMessaging.instance.getInitialMessage().then((message) {
await FirebaseMessaging.instance.getInitialMessage(); if (message != null) {
if (initialMessage != null) { handleMessage(message);
handleMessage(initialMessage);
} }
FirebaseMessaging.onMessageOpenedApp.listen(handleMessage);
}); });
return null; // When the app is in the background and opened.
final onMessageOpenedAppSubscription = FirebaseMessaging
.onMessageOpenedApp
.listen(handleMessage);
// When the app is in the foreground.
final onMessageSubscription = FirebaseMessaging.onMessage.listen((
message,
) {
log('Foreground message received: ${message.messageId}');
handleMessage(message);
});
return () {
onMessageOpenedAppSubscription.cancel();
onMessageSubscription.cancel();
};
}, []); }, []);
useEffect(() { useEffect(() {
@ -204,9 +224,8 @@ class IslandApp extends HookConsumerWidget {
initialEntries: [ initialEntries: [
OverlayEntry( OverlayEntry(
builder: builder:
(_) => WindowScaffold( (_) =>
child: child ?? const SizedBox.shrink(), WindowScaffold(child: child ?? const SizedBox.shrink()),
),
), ),
], ],
); );

View File

@ -14,6 +14,7 @@ sealed class SnChatRoom with _$SnChatRoom {
required String? description, required String? description,
required int type, required int type,
required bool isPublic, required bool isPublic,
required bool isCommunity,
required SnCloudFile? picture, required SnCloudFile? picture,
required SnCloudFile? background, required SnCloudFile? background,
required String? realmId, required String? realmId,

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$SnChatRoom { mixin _$SnChatRoom {
String get id; String? get name; String? get description; int get type; bool get isPublic; SnCloudFile? get picture; SnCloudFile? get background; String? get realmId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List<SnChatMember>? get members; String get id; String? get name; String? get description; int get type; bool get isPublic; bool get isCommunity; SnCloudFile? get picture; SnCloudFile? get background; String? get realmId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List<SnChatMember>? get members;
/// Create a copy of SnChatRoom /// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -29,16 +29,16 @@ $SnChatRoomCopyWith<SnChatRoom> get copyWith => _$SnChatRoomCopyWithImpl<SnChatR
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other.members, members)); return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other.members, members));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,picture,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(members)); int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,isCommunity,picture,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(members));
@override @override
String toString() { String toString() {
return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, picture: $picture, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)'; return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, isCommunity: $isCommunity, picture: $picture, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)';
} }
@ -49,7 +49,7 @@ abstract mixin class $SnChatRoomCopyWith<$Res> {
factory $SnChatRoomCopyWith(SnChatRoom value, $Res Function(SnChatRoom) _then) = _$SnChatRoomCopyWithImpl; factory $SnChatRoomCopyWith(SnChatRoom value, $Res Function(SnChatRoom) _then) = _$SnChatRoomCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String id, String? name, String? description, int type, bool isPublic, SnCloudFile? picture, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members
}); });
@ -66,13 +66,14 @@ class _$SnChatRoomCopyWithImpl<$Res>
/// Create a copy of SnChatRoom /// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? isCommunity = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable as int,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable
as bool,isCommunity: null == isCommunity ? _self.isCommunity : isCommunity // ignore: cast_nullable_to_non_nullable
as bool,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable as bool,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable as SnCloudFile?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
@ -128,7 +129,7 @@ $SnRealmCopyWith<$Res>? get realm {
@JsonSerializable() @JsonSerializable()
class _SnChatRoom implements SnChatRoom { class _SnChatRoom implements SnChatRoom {
const _SnChatRoom({required this.id, required this.name, required this.description, required this.type, required this.isPublic, required this.picture, required this.background, required this.realmId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt, required final List<SnChatMember>? members}): _members = members; const _SnChatRoom({required this.id, required this.name, required this.description, required this.type, required this.isPublic, required this.isCommunity, required this.picture, required this.background, required this.realmId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt, required final List<SnChatMember>? members}): _members = members;
factory _SnChatRoom.fromJson(Map<String, dynamic> json) => _$SnChatRoomFromJson(json); factory _SnChatRoom.fromJson(Map<String, dynamic> json) => _$SnChatRoomFromJson(json);
@override final String id; @override final String id;
@ -136,6 +137,7 @@ class _SnChatRoom implements SnChatRoom {
@override final String? description; @override final String? description;
@override final int type; @override final int type;
@override final bool isPublic; @override final bool isPublic;
@override final bool isCommunity;
@override final SnCloudFile? picture; @override final SnCloudFile? picture;
@override final SnCloudFile? background; @override final SnCloudFile? background;
@override final String? realmId; @override final String? realmId;
@ -166,16 +168,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._members, _members)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._members, _members));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,picture,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_members)); int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,isCommunity,picture,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_members));
@override @override
String toString() { String toString() {
return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, picture: $picture, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)'; return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, isCommunity: $isCommunity, picture: $picture, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)';
} }
@ -186,7 +188,7 @@ abstract mixin class _$SnChatRoomCopyWith<$Res> implements $SnChatRoomCopyWith<$
factory _$SnChatRoomCopyWith(_SnChatRoom value, $Res Function(_SnChatRoom) _then) = __$SnChatRoomCopyWithImpl; factory _$SnChatRoomCopyWith(_SnChatRoom value, $Res Function(_SnChatRoom) _then) = __$SnChatRoomCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String id, String? name, String? description, int type, bool isPublic, SnCloudFile? picture, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members
}); });
@ -203,13 +205,14 @@ class __$SnChatRoomCopyWithImpl<$Res>
/// Create a copy of SnChatRoom /// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? isCommunity = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) {
return _then(_SnChatRoom( return _then(_SnChatRoom(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable as int,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable
as bool,isCommunity: null == isCommunity ? _self.isCommunity : isCommunity // ignore: cast_nullable_to_non_nullable
as bool,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable as bool,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable as SnCloudFile?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable

View File

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

View File

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

View File

@ -117,13 +117,13 @@ $SnCloudFileCopyWith<$Res>? get background {
@JsonSerializable() @JsonSerializable()
class _SnRealm implements SnRealm { class _SnRealm implements SnRealm {
const _SnRealm({required this.id, required this.slug, required this.name, required this.description, required this.verifiedAs, required this.verifiedAt, required this.isCommunity, required this.isPublic, required this.picture, required this.background, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}); const _SnRealm({required this.id, required this.slug, required this.name, this.description = '', required this.verifiedAs, required this.verifiedAt, required this.isCommunity, required this.isPublic, required this.picture, required this.background, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnRealm.fromJson(Map<String, dynamic> json) => _$SnRealmFromJson(json); factory _SnRealm.fromJson(Map<String, dynamic> json) => _$SnRealmFromJson(json);
@override final String id; @override final String id;
@override final String slug; @override final String slug;
@override final String name; @override final String name;
@override final String description; @override@JsonKey() final String description;
@override final String? verifiedAs; @override final String? verifiedAs;
@override final DateTime? verifiedAt; @override final DateTime? verifiedAt;
@override final bool isCommunity; @override final bool isCommunity;

View File

@ -10,7 +10,7 @@ _SnRealm _$SnRealmFromJson(Map<String, dynamic> json) => _SnRealm(
id: json['id'] as String, id: json['id'] as String,
slug: json['slug'] as String, slug: json['slug'] as String,
name: json['name'] as String, name: json['name'] as String,
description: json['description'] as String, description: json['description'] as String? ?? '',
verifiedAs: json['verified_as'] as String?, verifiedAs: json['verified_as'] as String?,
verifiedAt: verifiedAt:
json['verified_at'] == null json['verified_at'] == null

View File

@ -31,6 +31,7 @@ import 'package:island/screens/settings.dart';
import 'package:island/screens/realm/realms.dart'; import 'package:island/screens/realm/realms.dart';
import 'package:island/screens/realm/detail.dart'; import 'package:island/screens/realm/detail.dart';
import 'package:island/screens/account/event_calendar.dart'; import 'package:island/screens/account/event_calendar.dart';
import 'package:island/screens/discovery/realms.dart';
// Shell route keys for nested navigation // Shell route keys for nested navigation
final rootNavigatorKey = GlobalKey<NavigatorState>(); final rootNavigatorKey = GlobalKey<NavigatorState>();
@ -105,10 +106,7 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (context, state) { builder: (context, state) {
final name = state.pathParameters['name']!; final name = state.pathParameters['name']!;
final packId = state.pathParameters['packId']!; final packId = state.pathParameters['packId']!;
return EditStickerPacksScreen( return EditStickerPacksScreen(pubName: name, packId: packId);
pubName: name,
packId: packId,
);
}, },
), ),
GoRoute( GoRoute(
@ -190,6 +188,10 @@ final routerProvider = Provider<GoRouter>((ref) {
return PublisherProfileScreen(name: name); return PublisherProfileScreen(name: name);
}, },
), ),
GoRoute(
path: 'discovery/realms',
builder: (context, state) => const DiscoveryRealmsScreen(),
),
], ],
), ),
@ -198,6 +200,10 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/chat', path: '/chat',
builder: (context, state) => const ChatListScreen(), builder: (context, state) => const ChatListScreen(),
routes: [ routes: [
GoRoute(
path: 'new',
builder: (context, state) => const NewChatScreen(),
),
GoRoute( GoRoute(
path: ':id', path: ':id',
builder: (context, state) { builder: (context, state) {
@ -205,10 +211,6 @@ final routerProvider = Provider<GoRouter>((ref) {
return ChatRoomScreen(id: id); return ChatRoomScreen(id: id);
}, },
), ),
GoRoute(
path: 'new',
builder: (context, state) => const NewChatScreen(),
),
GoRoute( GoRoute(
path: ':id/edit', path: ':id/edit',
builder: (context, state) { builder: (context, state) {

View File

@ -200,7 +200,7 @@ class AccountScreen extends HookConsumerWidget {
], ],
), ),
onTap: () { onTap: () {
context.push('/notification'); context.push('/account/notifications');
}, },
), ),
ListTile( ListTile(

View File

@ -215,6 +215,7 @@ class RelationshipScreen extends HookConsumerWidget {
Future<void> addFriend() async { Future<void> addFriend() async {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
context: context, context: context,
useRootNavigator: true,
builder: (context) => AccountPickerSheet(), builder: (context) => AccountPickerSheet(),
); );
if (result == null) return; if (result == null) return;

View File

@ -186,7 +186,7 @@ class ChatShellScreen extends HookConsumerWidget {
child: Row( child: Row(
children: [ children: [
Flexible(flex: 2, child: ChatListScreen(isAside: true)), Flexible(flex: 2, child: ChatListScreen(isAside: true)),
VerticalDivider(width: 1), const VerticalDivider(width: 1),
Flexible(flex: 4, child: child), Flexible(flex: 4, child: child),
], ],
), ),
@ -227,7 +227,8 @@ class ChatListScreen extends HookConsumerWidget {
Future<void> createDirectMessage() async { Future<void> createDirectMessage() async {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
context: context, context: context,
builder: (context) => AccountPickerSheet(), useRootNavigator: true,
builder: (context) => const AccountPickerSheet(),
); );
if (result == null) return; if (result == null) return;
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
@ -242,7 +243,7 @@ class ChatListScreen extends HookConsumerWidget {
return AppScaffold( return AppScaffold(
extendBody: false, // Prevent conflicts with tabs navigation extendBody: false, // Prevent conflicts with tabs navigation
appBar: AppBar( appBar: AppBar(
title: Text('chat').tr(), title: const Text('chat').tr(),
bottom: TabBar( bottom: TabBar(
controller: tabController, controller: tabController,
tabs: [ tabs: [
@ -296,7 +297,7 @@ class ChatListScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: (context) => _ChatInvitesSheet(), builder: (context) => const _ChatInvitesSheet(),
); );
}, },
), ),
@ -307,13 +308,14 @@ class ChatListScreen extends HookConsumerWidget {
onPressed: () { onPressed: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
useRootNavigator: true,
builder: builder:
(context) => Column( (context) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
ListTile( ListTile(
title: Text('createChatRoom').tr(), title: const Text('createChatRoom').tr(),
leading: const Icon(Symbols.add), leading: const Icon(Symbols.add),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
@ -325,7 +327,7 @@ class ChatListScreen extends HookConsumerWidget {
}, },
), ),
ListTile( ListTile(
title: Text('createDirectMessage').tr(), title: const Text('createDirectMessage').tr(),
leading: const Icon(Symbols.person), leading: const Icon(Symbols.person),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
@ -450,7 +452,7 @@ class NewChatScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return EditChatScreen(); return const EditChatScreen();
} }
} }
@ -468,6 +470,8 @@ class EditChatScreen extends HookConsumerWidget {
final descriptionController = useTextEditingController(); final descriptionController = useTextEditingController();
final picture = useState<SnCloudFile?>(null); final picture = useState<SnCloudFile?>(null);
final background = useState<SnCloudFile?>(null); final background = useState<SnCloudFile?>(null);
final isPublic = useState(true);
final isCommunity = useState(false);
final chat = ref.watch(chatroomProvider(id)); final chat = ref.watch(chatroomProvider(id));
@ -480,12 +484,14 @@ class EditChatScreen extends HookConsumerWidget {
descriptionController.text = chat.value!.description ?? ''; descriptionController.text = chat.value!.description ?? '';
picture.value = chat.value!.picture; picture.value = chat.value!.picture;
background.value = chat.value!.background; background.value = chat.value!.background;
isPublic.value = chat.value!.isPublic;
isCommunity.value = chat.value!.isCommunity;
currentRealm.value = joinedRealms.value?.firstWhereOrNull( currentRealm.value = joinedRealms.value?.firstWhereOrNull(
(realm) => realm.id == chat.value!.realmId, (realm) => realm.id == chat.value!.realmId,
); );
} }
return; return;
}, [chat]); }, [chat, joinedRealms]);
void setPicture(String position) async { void setPicture(String position) async {
showLoadingModal(context); showLoadingModal(context);
@ -503,9 +509,9 @@ class EditChatScreen extends HookConsumerWidget {
image: result, image: result,
allowedAspectRatios: [ allowedAspectRatios: [
if (position == 'background') if (position == 'background')
CropAspectRatio(height: 7, width: 16) const CropAspectRatio(height: 7, width: 16)
else else
CropAspectRatio(height: 1, width: 1), const CropAspectRatio(height: 1, width: 1),
], ],
); );
if (result == null) { if (result == null) {
@ -562,6 +568,8 @@ class EditChatScreen extends HookConsumerWidget {
'background_id': background.value?.id, 'background_id': background.value?.id,
'picture_id': picture.value?.id, 'picture_id': picture.value?.id,
'realm_id': currentRealm.value?.id, 'realm_id': currentRealm.value?.id,
'is_public': isPublic.value,
'is_community': isCommunity.value,
}, },
options: Options(method: id == null ? 'POST' : 'PATCH'), options: Options(method: id == null ? 'POST' : 'PATCH'),
); );
@ -654,6 +662,19 @@ class EditChatScreen extends HookConsumerWidget {
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
CheckboxListTile(
title: const Text('isPublic').tr(),
subtitle: const Text('isPublicHint').tr(),
value: isPublic.value,
onChanged: (value) => isPublic.value = value ?? false,
),
CheckboxListTile(
title: const Text('isCommunity').tr(),
subtitle: const Text('isCommunityHint').tr(),
value: isCommunity.value,
onChanged: (value) => isCommunity.value = value ?? false,
),
const SizedBox(height: 16),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: TextButton.icon( child: TextButton.icon(
@ -754,7 +775,7 @@ class _ChatInvitesSheet extends HookConsumerWidget {
), ),
if (invite.chatRoom!.type == 1) if (invite.chatRoom!.type == 1)
Badge( Badge(
label: Text('directMessage').tr(), label: const Text('directMessage').tr(),
backgroundColor: backgroundColor:
Theme.of(context).colorScheme.primary, Theme.of(context).colorScheme.primary,
textColor: textColor:

View File

@ -295,6 +295,20 @@ class ChatRoomScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final chatRoom = ref.watch(chatroomProvider(id)); final chatRoom = ref.watch(chatroomProvider(id));
final chatIdentity = ref.watch(chatroomIdentityProvider(id)); final chatIdentity = ref.watch(chatroomIdentityProvider(id));
if (chatIdentity.isLoading || chatRoom.isLoading) {
return AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: CircularProgressIndicator().center(),
);
} else if (chatIdentity.value == null) {
// Identity was not found, user was not joined
return AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: Center(child: Text('You are not a member of this chat room')),
);
}
final messages = ref.watch(messagesNotifierProvider(id)); final messages = ref.watch(messagesNotifierProvider(id));
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier); final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
final ws = ref.watch(websocketProvider); final ws = ref.watch(websocketProvider);

View File

@ -584,8 +584,8 @@ class _ChatMemberListSheet extends HookConsumerWidget {
Future<void> invitePerson() async { Future<void> invitePerson() async {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
isScrollControlled: true,
context: context, context: context,
useRootNavigator: true,
builder: (context) => const AccountPickerSheet(), builder: (context) => const AccountPickerSheet(),
); );
if (result == null) return; if (result == null) return;

View File

@ -0,0 +1,24 @@
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/widgets/app_scaffold.dart';
import 'package:island/widgets/realm/realm_list.dart';
class DiscoveryRealmsScreen extends HookConsumerWidget {
const DiscoveryRealmsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return AppScaffold(
appBar: AppBar(title: Text('discoverRealms'.tr())),
body: CustomScrollView(
slivers: [
SliverGap(16),
SliverRealmList(),
SliverGap(MediaQuery.of(context).padding.bottom + 16),
],
),
);
}
}

View File

@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart'; import 'package:island/models/activity.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
@ -17,8 +18,8 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/realm/realm_card.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:island/models/realm.dart';
part 'explore.g.dart'; part 'explore.g.dart';
@ -206,7 +207,7 @@ class _DiscoveryActivityItem extends StatelessWidget {
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final realm = items[index]; final realm = items[index];
return _RealmCard(realm: realm); return RealmCard(realm: realm);
}, },
), ),
), ),
@ -215,86 +216,6 @@ class _DiscoveryActivityItem extends StatelessWidget {
} }
} }
class _RealmCard extends ConsumerWidget {
final SnRealm realm;
const _RealmCard({required this.realm});
@override
Widget build(BuildContext context, WidgetRef ref) {
final client = ref.watch(apiClientProvider);
Widget imageWidget;
if (realm.picture != null) {
final imageUrl = '${client.options.baseUrl}/files/${realm.picture!.id}';
imageWidget = Image.network(
imageUrl,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
);
} else {
imageWidget = Container(
color: Theme.of(context).colorScheme.secondaryContainer,
child: Center(
child: Icon(
Symbols.photo_camera,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
);
}
return Card(
clipBehavior: Clip.antiAlias,
margin: const EdgeInsets.only(left: 16, bottom: 8, top: 8),
child: InkWell(
onTap: () {
context.push('/realms/${realm.slug}');
},
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 280),
child: AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
children: [
imageWidget,
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
],
),
),
padding: const EdgeInsets.all(8),
child: Text(
realm.name,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
),
),
),
);
}
}
class _ActivityListView extends HookConsumerWidget { class _ActivityListView extends HookConsumerWidget {
final CursorPagingData<SnActivity> data; final CursorPagingData<SnActivity> data;
final int widgetCount; final int widgetCount;

View File

@ -1,12 +1,17 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:island/screens/chat/chat.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:island/models/chat.dart';
import 'package:island/services/color.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/config.dart';
import 'package:island/screens/realm/realms.dart'; import 'package:island/screens/realm/realms.dart';
import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
@ -19,11 +24,40 @@ import 'package:styled_widget/styled_widget.dart';
part 'detail.g.dart'; part 'detail.g.dart';
@riverpod
Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async {
final realm = await ref.watch(realmProvider(realmSlug).future);
if (realm?.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider(
CloudImageWidget.provider(
fileId: realm!.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
final dominantColor = palette.dominantColor?.color;
if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
}
@riverpod @riverpod
Future<SnRealmMember?> realmIdentity(Ref ref, String realmSlug) async { Future<SnRealmMember?> realmIdentity(Ref ref, String realmSlug) async {
try {
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get('/realms/$realmSlug/members/me'); final response = await apiClient.get('/realms/$realmSlug/members/me');
return SnRealmMember.fromJson(response.data); return SnRealmMember.fromJson(response.data);
} catch (err) {
if (err is DioException && err.response?.statusCode == 404) {
return null; // No identity found, user is not a member
}
rethrow;
}
}
@riverpod
Future<List<SnChatRoom>> realmChatRooms(Ref ref, String realmSlug) async {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get('/realms/$realmSlug/chat');
return (response.data as List).map((e) => SnChatRoom.fromJson(e)).toList();
} }
class RealmDetailScreen extends HookConsumerWidget { class RealmDetailScreen extends HookConsumerWidget {
@ -34,9 +68,10 @@ class RealmDetailScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final realmState = ref.watch(realmProvider(slug)); final realmState = ref.watch(realmProvider(slug));
final appbarColor = ref.watch(realmAppbarForegroundColorProvider(slug));
const iconShadow = Shadow( final iconShadow = Shadow(
color: Colors.black54, color: appbarColor.value?.invert ?? Colors.black54,
blurRadius: 5.0, blurRadius: 5.0,
offset: Offset(1.0, 1.0), offset: Offset(1.0, 1.0),
); );
@ -51,7 +86,11 @@ class RealmDetailScreen extends HookConsumerWidget {
SliverAppBar( SliverAppBar(
expandedHeight: 180, expandedHeight: 180,
pinned: true, pinned: true,
leading: PageBackButton(shadows: [iconShadow]), foregroundColor: appbarColor.value,
leading: PageBackButton(
color: appbarColor.value,
shadows: [iconShadow],
),
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: background:
realm!.background?.id != null realm!.background?.id != null
@ -63,14 +102,16 @@ class RealmDetailScreen extends HookConsumerWidget {
title: Text( title: Text(
realm.name, realm.name,
style: TextStyle( style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor, color:
appbarColor.value ??
Theme.of(context).appBarTheme.foregroundColor,
shadows: [iconShadow], shadows: [iconShadow],
), ),
), ),
), ),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.people, shadows: [iconShadow]), icon: Icon(Icons.people, shadows: [iconShadow]),
onPressed: () { onPressed: () {
showModalBottomSheet( showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
@ -86,19 +127,98 @@ class RealmDetailScreen extends HookConsumerWidget {
], ],
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: ref
padding: const EdgeInsets.all(16.0), .watch(realmIdentityProvider(slug))
child: Column( .when(
crossAxisAlignment: CrossAxisAlignment.start, loading: () => const SizedBox.shrink(),
error: (_, _) => const SizedBox.shrink(),
data:
(identity) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ExpansionTile(
title: const Text('description').tr(),
initiallyExpanded: identity == null,
tilePadding: EdgeInsets.symmetric(
horizontal: 20,
),
expandedCrossAxisAlignment:
CrossAxisAlignment.stretch,
children: [ children: [
Text( Text(
realm.description, realm.description,
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
).padding(
horizontal: 20,
bottom: 16,
top: 8,
), ),
], ],
), ),
if (identity == null && realm.isPublic)
FilledButton.tonalIcon(
onPressed: () async {
try {
final apiClient = ref.read(
apiClientProvider,
);
await apiClient.post(
'/realms/$slug/members/me',
);
ref.invalidate(
realmIdentityProvider(slug),
);
ref.invalidate(realmsJoinedProvider);
showSnackBar('joinRealmSuccess'.tr());
} catch (err) {
showErrorAlert(err);
}
},
icon: const Icon(Symbols.add),
label: const Text('joinRealm').tr(),
).padding(horizontal: 16, vertical: 8)
else
const SizedBox.shrink(),
],
), ),
), ),
),
const SliverToBoxAdapter(child: Divider(height: 1)),
Consumer(
builder: (context, ref, _) {
final chatRooms = ref.watch(realmChatRoomsProvider(slug));
return chatRooms.when(
loading:
() => const SliverToBoxAdapter(
child: Center(child: CircularProgressIndicator()),
),
error:
(error, _) => SliverToBoxAdapter(
child: Center(child: Text('Error: $error')),
),
data: (rooms) {
if (rooms.isEmpty) {
return const SliverToBoxAdapter(
child: SizedBox.shrink(),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate((
context,
index,
) {
return ChatRoomListTile(
room: rooms[index],
onTap: () {
context.push('/chat/${rooms[index].id}');
},
);
}, childCount: rooms.length),
);
},
);
},
),
], ],
), ),
), ),
@ -114,8 +234,8 @@ class _RealmActionMenu extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final realmIdentityAsync = ref.watch(realmIdentityProvider(realmSlug)); final realmIdentity = ref.watch(realmIdentityProvider(realmSlug));
final isModerator = realmIdentityAsync.when( final isModerator = realmIdentity.when(
data: (identity) => (identity?.role ?? 0) >= 50, data: (identity) => (identity?.role ?? 0) >= 50,
loading: () => false, loading: () => false,
error: (_, _) => false, error: (_, _) => false,
@ -141,7 +261,7 @@ class _RealmActionMenu extends HookConsumerWidget {
], ],
), ),
), ),
realmIdentityAsync.when( realmIdentity.when(
data: data:
(identity) => (identity) =>
(identity?.role ?? 0) >= 100 (identity?.role ?? 0) >= 100

View File

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

View File

@ -46,6 +46,10 @@ class RealmListScreen extends HookConsumerWidget {
appBar: AppBar( appBar: AppBar(
title: const Text('realms').tr(), title: const Text('realms').tr(),
actions: [ actions: [
IconButton(
icon: const Icon(Symbols.travel_explore),
onPressed: () => context.push('/discovery/realms'),
),
IconButton( IconButton(
icon: Badge( icon: Badge(
label: Text( label: Text(
@ -66,7 +70,7 @@ class RealmListScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: (_) => _RealmInviteSheet(), builder: (_) => const _RealmInviteSheet(),
); );
}, },
), ),
@ -74,7 +78,7 @@ class RealmListScreen extends HookConsumerWidget {
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: Key("realms-page-fab"), heroTag: const Key("realms-page-fab"),
child: const Icon(Symbols.add), child: const Icon(Symbols.add),
onPressed: () { onPressed: () {
context.push('/realms/new').then((value) { context.push('/realms/new').then((value) {
@ -106,7 +110,7 @@ class RealmListScreen extends HookConsumerWidget {
onTap: () { onTap: () {
context.push('/realms/${value[item].slug}'); context.push('/realms/${value[item].slug}');
}, },
contentPadding: EdgeInsets.only( contentPadding: const EdgeInsets.only(
left: 16, left: 16,
right: 14, right: 14,
top: 8, top: 8,
@ -158,6 +162,8 @@ class EditRealmScreen extends HookConsumerWidget {
final picture = useState<SnCloudFile?>(null); final picture = useState<SnCloudFile?>(null);
final background = useState<SnCloudFile?>(null); final background = useState<SnCloudFile?>(null);
final isPublic = useState(true);
final isCommunity = useState(false);
final slugController = useTextEditingController(); final slugController = useTextEditingController();
final nameController = useTextEditingController(); final nameController = useTextEditingController();
@ -174,6 +180,8 @@ class EditRealmScreen extends HookConsumerWidget {
slugController.text = realm.value!.slug; slugController.text = realm.value!.slug;
nameController.text = realm.value!.name; nameController.text = realm.value!.name;
descriptionController.text = realm.value!.description; descriptionController.text = realm.value!.description;
isPublic.value = realm.value!.isPublic;
isCommunity.value = realm.value!.isCommunity;
} }
return null; return null;
}, [realm]); }, [realm]);
@ -194,9 +202,9 @@ class EditRealmScreen extends HookConsumerWidget {
image: result, image: result,
allowedAspectRatios: [ allowedAspectRatios: [
if (position == 'background') if (position == 'background')
CropAspectRatio(height: 7, width: 16) const CropAspectRatio(height: 7, width: 16)
else else
CropAspectRatio(height: 1, width: 1), const CropAspectRatio(height: 1, width: 1),
], ],
); );
if (result == null) { if (result == null) {
@ -252,6 +260,8 @@ class EditRealmScreen extends HookConsumerWidget {
'description': descriptionController.text, 'description': descriptionController.text,
'background_id': background.value?.id, 'background_id': background.value?.id,
'picture_id': picture.value?.id, 'picture_id': picture.value?.id,
'is_public': isPublic.value,
'is_community': isCommunity.value,
}, },
options: Options(method: slug == null ? 'POST' : 'PATCH'), options: Options(method: slug == null ? 'POST' : 'PATCH'),
); );
@ -314,7 +324,6 @@ class EditRealmScreen extends HookConsumerWidget {
key: formKey, key: formKey,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: [ children: [
TextFormField( TextFormField(
controller: slugController, controller: slugController,
@ -325,12 +334,14 @@ class EditRealmScreen extends HookConsumerWidget {
onTapOutside: onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
const SizedBox(height: 16),
TextFormField( TextFormField(
controller: nameController, controller: nameController,
decoration: InputDecoration(labelText: 'name'.tr()), decoration: InputDecoration(labelText: 'name'.tr()),
onTapOutside: onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
const SizedBox(height: 16),
TextFormField( TextFormField(
controller: descriptionController, controller: descriptionController,
decoration: InputDecoration(labelText: 'description'.tr()), decoration: InputDecoration(labelText: 'description'.tr()),
@ -339,6 +350,20 @@ class EditRealmScreen extends HookConsumerWidget {
onTapOutside: onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
const SizedBox(height: 16),
CheckboxListTile(
title: const Text('isPublic').tr(),
subtitle: const Text('isPublicHint').tr(),
value: isPublic.value,
onChanged: (value) => isPublic.value = value ?? false,
),
CheckboxListTile(
title: const Text('isCommunity').tr(),
subtitle: const Text('isCommunityHint').tr(),
value: isCommunity.value,
onChanged: (value) => isCommunity.value = value ?? false,
),
const SizedBox(height: 16),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: TextButton.icon( child: TextButton.icon(

View File

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

View File

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart';
import 'package:material_symbols_icons/symbols.dart';
class RealmCard extends ConsumerWidget {
final SnRealm realm;
const RealmCard({super.key, required this.realm});
@override
Widget build(BuildContext context, WidgetRef ref) {
final client = ref.watch(apiClientProvider);
Widget imageWidget;
if (realm.picture != null) {
final imageUrl = '${client.options.baseUrl}/files/${realm.picture!.id}';
imageWidget = Image.network(
imageUrl,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
);
} else {
imageWidget = Container(
color: Theme.of(context).colorScheme.secondaryContainer,
child: Center(
child: Icon(
Symbols.photo_camera,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
);
}
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
context.push('/realms/${realm.slug}');
},
child: AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
children: [
imageWidget,
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
],
),
),
padding: const EdgeInsets.all(8),
child: Text(
realm.name,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
),
),
);
}
}

View File

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

View File

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

View File

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