From f266968644d1e34345f4ec2cb1d8328286f2edc2 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 5 May 2025 20:59:52 +0800 Subject: [PATCH] :sparkles: WebSocket connection indicator --- assets/i18n/en-US.json | 5 +- lib/pods/websocket.dart | 22 ++ lib/route.gr.dart | 337 ++++++++++++++----------- lib/screens/account/profile.dart | 93 +++++++ lib/screens/account/profile.g.dart | 149 +++++++++++ lib/screens/auth/create_account.dart | 1 - lib/screens/chat/chat.dart | 1 - lib/screens/chat/room_detail.dart | 3 +- lib/screens/explore.dart | 35 ++- lib/screens/posts/compose.dart | 1 - lib/screens/realm/detail.dart | 1 - lib/widgets/app_scaffold.dart | 143 +++++++---- lib/widgets/post/post_quick_reply.dart | 1 - 13 files changed, 580 insertions(+), 212 deletions(-) create mode 100644 lib/screens/account/profile.dart create mode 100644 lib/screens/account/profile.g.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index f22e9c7..6c9dbba 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -103,5 +103,8 @@ }, "reactionPositive": "Postive", "reactionNegative": "Negative", - "reactionNeutral": "Neutral" + "reactionNeutral": "Neutral", + "connectionConnected": "Connected", + "connectionDisconnected": "Disconnected", + "connectionReconnecting": "Reconnecting" } diff --git a/lib/pods/websocket.dart b/lib/pods/websocket.dart index f3f172c..bcd4ccc 100644 --- a/lib/pods/websocket.dart +++ b/lib/pods/websocket.dart @@ -41,11 +41,14 @@ class WebSocketService { WebSocketChannel? _channel; final StreamController _streamController = StreamController.broadcast(); + final StreamController _statusStreamController = + StreamController.broadcast(); String? _lastUrl; String? _lastAtk; Timer? _reconnectTimer; Stream get dataStream => _streamController.stream; + Stream get statusStream => _statusStreamController.stream; Future connect(String url, String atk) async { _lastUrl = url; @@ -57,6 +60,7 @@ class WebSocketService { headers: {'Authorization': 'Bearer $atk'}, ); await _channel!.ready; + _statusStreamController.sink.add(WebSocketState.connected()); _channel!.stream.listen( (data) { final dataStr = @@ -68,10 +72,14 @@ class WebSocketService { onDone: () { log('[WebSocket] Connection closed, attempting to reconnect...'); _scheduleReconnect(); + _statusStreamController.sink.add(WebSocketState.disconnected()); }, onError: (error) { log('[WebSocket] Error occurred: $error, attempting to reconnect...'); _scheduleReconnect(); + _statusStreamController.sink.add( + WebSocketState.error(error.toString()), + ); }, ); } catch (err) { @@ -84,6 +92,7 @@ class WebSocketService { _reconnectTimer?.cancel(); _reconnectTimer = Timer(const Duration(milliseconds: 500), () { if (_lastUrl != null && _lastAtk != null) { + _statusStreamController.sink.add(WebSocketState.connecting()); connect(_lastUrl!, _lastAtk!); } }); @@ -110,6 +119,7 @@ final websocketStateProvider = class WebSocketStateNotifier extends StateNotifier { final Ref ref; + Timer? _reconnectTimer; WebSocketStateNotifier(this.ref) : super(const WebSocketState.disconnected()); @@ -132,11 +142,22 @@ class WebSocketStateNotifier extends StateNotifier { } await service.connect('$baseUrl/ws'.replaceFirst('http', 'ws'), atk); state = const WebSocketState.connected(); + service.statusStream.listen((event) { + state = event; + }); } catch (err) { state = WebSocketState.error('Failed to connect: $err'); + _scheduleReconnect(); } } + void _scheduleReconnect() { + _reconnectTimer?.cancel(); + _reconnectTimer = Timer(const Duration(milliseconds: 500), () { + connect(); + }); + } + void sendMessage(String message) { final service = ref.read(websocketProvider); service.sendMessage(message); @@ -145,6 +166,7 @@ class WebSocketStateNotifier extends StateNotifier { void close() { final service = ref.read(websocketProvider); service.close(); + _reconnectTimer?.cancel(); state = const WebSocketState.disconnected(); } } diff --git a/lib/route.gr.dart b/lib/route.gr.dart index 40ccf8c..f79ebb2 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -9,48 +9,91 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i16; -import 'package:flutter/material.dart' as _i17; -import 'package:island/models/post.dart' as _i18; -import 'package:island/screens/account.dart' as _i1; -import 'package:island/screens/account/me.dart' as _i10; -import 'package:island/screens/account/me/publishers.dart' as _i6; -import 'package:island/screens/account/me/update.dart' as _i15; -import 'package:island/screens/auth/create_account.dart' as _i5; -import 'package:island/screens/auth/login.dart' as _i9; -import 'package:island/screens/auth/tabs.dart' as _i14; -import 'package:island/screens/chat/chat.dart' as _i3; -import 'package:island/screens/chat/room.dart' as _i4; -import 'package:island/screens/chat/room_detail.dart' as _i2; -import 'package:island/screens/explore.dart' as _i8; -import 'package:island/screens/posts/compose.dart' as _i11; -import 'package:island/screens/posts/detail.dart' as _i12; -import 'package:island/screens/realm/detail.dart' as _i13; -import 'package:island/screens/realm/realms.dart' as _i7; +import 'package:auto_route/auto_route.dart' as _i17; +import 'package:flutter/material.dart' as _i18; +import 'package:island/models/post.dart' as _i19; +import 'package:island/screens/account.dart' as _i2; +import 'package:island/screens/account/me.dart' as _i11; +import 'package:island/screens/account/me/publishers.dart' as _i7; +import 'package:island/screens/account/me/update.dart' as _i16; +import 'package:island/screens/account/profile.dart' as _i1; +import 'package:island/screens/auth/create_account.dart' as _i6; +import 'package:island/screens/auth/login.dart' as _i10; +import 'package:island/screens/auth/tabs.dart' as _i15; +import 'package:island/screens/chat/chat.dart' as _i4; +import 'package:island/screens/chat/room.dart' as _i5; +import 'package:island/screens/chat/room_detail.dart' as _i3; +import 'package:island/screens/explore.dart' as _i9; +import 'package:island/screens/posts/compose.dart' as _i12; +import 'package:island/screens/posts/detail.dart' as _i13; +import 'package:island/screens/realm/detail.dart' as _i14; +import 'package:island/screens/realm/realms.dart' as _i8; /// generated route for -/// [_i1.AccountScreen] -class AccountRoute extends _i16.PageRouteInfo { - const AccountRoute({List<_i16.PageRouteInfo>? children}) +/// [_i1.AccountProfileScreen] +class AccountProfileRoute extends _i17.PageRouteInfo { + AccountProfileRoute({ + _i18.Key? key, + required String name, + List<_i17.PageRouteInfo>? children, + }) : super( + AccountProfileRoute.name, + args: AccountProfileRouteArgs(key: key, name: name), + rawPathParams: {'name': name}, + initialChildren: children, + ); + + static const String name = 'AccountProfileRoute'; + + static _i17.PageInfo page = _i17.PageInfo( + name, + builder: (data) { + final pathParams = data.inheritedPathParams; + final args = data.argsAs( + orElse: + () => AccountProfileRouteArgs(name: pathParams.getString('name')), + ); + return _i1.AccountProfileScreen(key: args.key, name: args.name); + }, + ); +} + +class AccountProfileRouteArgs { + const AccountProfileRouteArgs({this.key, required this.name}); + + final _i18.Key? key; + + final String name; + + @override + String toString() { + return 'AccountProfileRouteArgs{key: $key, name: $name}'; + } +} + +/// generated route for +/// [_i2.AccountScreen] +class AccountRoute extends _i17.PageRouteInfo { + const AccountRoute({List<_i17.PageRouteInfo>? children}) : super(AccountRoute.name, initialChildren: children); static const String name = 'AccountRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i1.AccountScreen(); + return const _i2.AccountScreen(); }, ); } /// generated route for -/// [_i2.ChatDetailScreen] -class ChatDetailRoute extends _i16.PageRouteInfo { +/// [_i3.ChatDetailScreen] +class ChatDetailRoute extends _i17.PageRouteInfo { ChatDetailRoute({ - _i17.Key? key, + _i18.Key? key, required int id, - List<_i16.PageRouteInfo>? children, + List<_i17.PageRouteInfo>? children, }) : super( ChatDetailRoute.name, args: ChatDetailRouteArgs(key: key, id: id), @@ -60,14 +103,14 @@ class ChatDetailRoute extends _i16.PageRouteInfo { static const String name = 'ChatDetailRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => ChatDetailRouteArgs(id: pathParams.getInt('id')), ); - return _i2.ChatDetailScreen(key: args.key, id: args.id); + return _i3.ChatDetailScreen(key: args.key, id: args.id); }, ); } @@ -75,7 +118,7 @@ class ChatDetailRoute extends _i16.PageRouteInfo { class ChatDetailRouteArgs { const ChatDetailRouteArgs({this.key, required this.id}); - final _i17.Key? key; + final _i18.Key? key; final int id; @@ -86,28 +129,28 @@ class ChatDetailRouteArgs { } /// generated route for -/// [_i3.ChatListScreen] -class ChatListRoute extends _i16.PageRouteInfo { - const ChatListRoute({List<_i16.PageRouteInfo>? children}) +/// [_i4.ChatListScreen] +class ChatListRoute extends _i17.PageRouteInfo { + const ChatListRoute({List<_i17.PageRouteInfo>? children}) : super(ChatListRoute.name, initialChildren: children); static const String name = 'ChatListRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i3.ChatListScreen(); + return const _i4.ChatListScreen(); }, ); } /// generated route for -/// [_i4.ChatRoomScreen] -class ChatRoomRoute extends _i16.PageRouteInfo { +/// [_i5.ChatRoomScreen] +class ChatRoomRoute extends _i17.PageRouteInfo { ChatRoomRoute({ - _i17.Key? key, + _i18.Key? key, required int id, - List<_i16.PageRouteInfo>? children, + List<_i17.PageRouteInfo>? children, }) : super( ChatRoomRoute.name, args: ChatRoomRouteArgs(key: key, id: id), @@ -117,14 +160,14 @@ class ChatRoomRoute extends _i16.PageRouteInfo { static const String name = 'ChatRoomRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => ChatRoomRouteArgs(id: pathParams.getInt('id')), ); - return _i4.ChatRoomScreen(key: args.key, id: args.id); + return _i5.ChatRoomScreen(key: args.key, id: args.id); }, ); } @@ -132,7 +175,7 @@ class ChatRoomRoute extends _i16.PageRouteInfo { class ChatRoomRouteArgs { const ChatRoomRouteArgs({this.key, required this.id}); - final _i17.Key? key; + final _i18.Key? key; final int id; @@ -143,25 +186,25 @@ class ChatRoomRouteArgs { } /// generated route for -/// [_i5.CreateAccountScreen] -class CreateAccountRoute extends _i16.PageRouteInfo { - const CreateAccountRoute({List<_i16.PageRouteInfo>? children}) +/// [_i6.CreateAccountScreen] +class CreateAccountRoute extends _i17.PageRouteInfo { + const CreateAccountRoute({List<_i17.PageRouteInfo>? children}) : super(CreateAccountRoute.name, initialChildren: children); static const String name = 'CreateAccountRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i5.CreateAccountScreen(); + return const _i6.CreateAccountScreen(); }, ); } /// generated route for -/// [_i3.EditChatScreen] -class EditChatRoute extends _i16.PageRouteInfo { - EditChatRoute({_i17.Key? key, int? id, List<_i16.PageRouteInfo>? children}) +/// [_i4.EditChatScreen] +class EditChatRoute extends _i17.PageRouteInfo { + EditChatRoute({_i18.Key? key, int? id, List<_i17.PageRouteInfo>? children}) : super( EditChatRoute.name, args: EditChatRouteArgs(key: key, id: id), @@ -171,14 +214,14 @@ class EditChatRoute extends _i16.PageRouteInfo { static const String name = 'EditChatRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => EditChatRouteArgs(id: pathParams.optInt('id')), ); - return _i3.EditChatScreen(key: args.key, id: args.id); + return _i4.EditChatScreen(key: args.key, id: args.id); }, ); } @@ -186,7 +229,7 @@ class EditChatRoute extends _i16.PageRouteInfo { class EditChatRouteArgs { const EditChatRouteArgs({this.key, this.id}); - final _i17.Key? key; + final _i18.Key? key; final int? id; @@ -197,12 +240,12 @@ class EditChatRouteArgs { } /// generated route for -/// [_i6.EditPublisherScreen] -class EditPublisherRoute extends _i16.PageRouteInfo { +/// [_i7.EditPublisherScreen] +class EditPublisherRoute extends _i17.PageRouteInfo { EditPublisherRoute({ - _i17.Key? key, + _i18.Key? key, String? name, - List<_i16.PageRouteInfo>? children, + List<_i17.PageRouteInfo>? children, }) : super( EditPublisherRoute.name, args: EditPublisherRouteArgs(key: key, name: name), @@ -212,14 +255,14 @@ class EditPublisherRoute extends _i16.PageRouteInfo { static const String name = 'EditPublisherRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => EditPublisherRouteArgs(name: pathParams.optString('id')), ); - return _i6.EditPublisherScreen(key: args.key, name: args.name); + return _i7.EditPublisherScreen(key: args.key, name: args.name); }, ); } @@ -227,7 +270,7 @@ class EditPublisherRoute extends _i16.PageRouteInfo { class EditPublisherRouteArgs { const EditPublisherRouteArgs({this.key, this.name}); - final _i17.Key? key; + final _i18.Key? key; final String? name; @@ -238,12 +281,12 @@ class EditPublisherRouteArgs { } /// generated route for -/// [_i7.EditRealmScreen] -class EditRealmRoute extends _i16.PageRouteInfo { +/// [_i8.EditRealmScreen] +class EditRealmRoute extends _i17.PageRouteInfo { EditRealmRoute({ - _i17.Key? key, + _i18.Key? key, String? slug, - List<_i16.PageRouteInfo>? children, + List<_i17.PageRouteInfo>? children, }) : super( EditRealmRoute.name, args: EditRealmRouteArgs(key: key, slug: slug), @@ -253,14 +296,14 @@ class EditRealmRoute extends _i16.PageRouteInfo { static const String name = 'EditRealmRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => EditRealmRouteArgs(slug: pathParams.optString('slug')), ); - return _i7.EditRealmScreen(key: args.key, slug: args.slug); + return _i8.EditRealmScreen(key: args.key, slug: args.slug); }, ); } @@ -268,7 +311,7 @@ class EditRealmRoute extends _i16.PageRouteInfo { class EditRealmRouteArgs { const EditRealmRouteArgs({this.key, this.slug}); - final _i17.Key? key; + final _i18.Key? key; final String? slug; @@ -279,124 +322,124 @@ class EditRealmRouteArgs { } /// generated route for -/// [_i8.ExploreScreen] -class ExploreRoute extends _i16.PageRouteInfo { - const ExploreRoute({List<_i16.PageRouteInfo>? children}) +/// [_i9.ExploreScreen] +class ExploreRoute extends _i17.PageRouteInfo { + const ExploreRoute({List<_i17.PageRouteInfo>? children}) : super(ExploreRoute.name, initialChildren: children); static const String name = 'ExploreRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i8.ExploreScreen(); + return const _i9.ExploreScreen(); }, ); } /// generated route for -/// [_i9.LoginScreen] -class LoginRoute extends _i16.PageRouteInfo { - const LoginRoute({List<_i16.PageRouteInfo>? children}) +/// [_i10.LoginScreen] +class LoginRoute extends _i17.PageRouteInfo { + const LoginRoute({List<_i17.PageRouteInfo>? children}) : super(LoginRoute.name, initialChildren: children); static const String name = 'LoginRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i9.LoginScreen(); + return const _i10.LoginScreen(); }, ); } /// generated route for -/// [_i6.ManagedPublisherScreen] -class ManagedPublisherRoute extends _i16.PageRouteInfo { - const ManagedPublisherRoute({List<_i16.PageRouteInfo>? children}) +/// [_i7.ManagedPublisherScreen] +class ManagedPublisherRoute extends _i17.PageRouteInfo { + const ManagedPublisherRoute({List<_i17.PageRouteInfo>? children}) : super(ManagedPublisherRoute.name, initialChildren: children); static const String name = 'ManagedPublisherRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i6.ManagedPublisherScreen(); + return const _i7.ManagedPublisherScreen(); }, ); } /// generated route for -/// [_i10.MyselfProfileScreen] -class MyselfProfileRoute extends _i16.PageRouteInfo { - const MyselfProfileRoute({List<_i16.PageRouteInfo>? children}) +/// [_i11.MyselfProfileScreen] +class MyselfProfileRoute extends _i17.PageRouteInfo { + const MyselfProfileRoute({List<_i17.PageRouteInfo>? children}) : super(MyselfProfileRoute.name, initialChildren: children); static const String name = 'MyselfProfileRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i10.MyselfProfileScreen(); + return const _i11.MyselfProfileScreen(); }, ); } /// generated route for -/// [_i3.NewChatScreen] -class NewChatRoute extends _i16.PageRouteInfo { - const NewChatRoute({List<_i16.PageRouteInfo>? children}) +/// [_i4.NewChatScreen] +class NewChatRoute extends _i17.PageRouteInfo { + const NewChatRoute({List<_i17.PageRouteInfo>? children}) : super(NewChatRoute.name, initialChildren: children); static const String name = 'NewChatRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i3.NewChatScreen(); + return const _i4.NewChatScreen(); }, ); } /// generated route for -/// [_i6.NewPublisherScreen] -class NewPublisherRoute extends _i16.PageRouteInfo { - const NewPublisherRoute({List<_i16.PageRouteInfo>? children}) +/// [_i7.NewPublisherScreen] +class NewPublisherRoute extends _i17.PageRouteInfo { + const NewPublisherRoute({List<_i17.PageRouteInfo>? children}) : super(NewPublisherRoute.name, initialChildren: children); static const String name = 'NewPublisherRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i6.NewPublisherScreen(); + return const _i7.NewPublisherScreen(); }, ); } /// generated route for -/// [_i7.NewRealmScreen] -class NewRealmRoute extends _i16.PageRouteInfo { - const NewRealmRoute({List<_i16.PageRouteInfo>? children}) +/// [_i8.NewRealmScreen] +class NewRealmRoute extends _i17.PageRouteInfo { + const NewRealmRoute({List<_i17.PageRouteInfo>? children}) : super(NewRealmRoute.name, initialChildren: children); static const String name = 'NewRealmRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i7.NewRealmScreen(); + return const _i8.NewRealmScreen(); }, ); } /// generated route for -/// [_i11.PostComposeScreen] -class PostComposeRoute extends _i16.PageRouteInfo { +/// [_i12.PostComposeScreen] +class PostComposeRoute extends _i17.PageRouteInfo { PostComposeRoute({ - _i17.Key? key, - _i18.SnPost? originalPost, - List<_i16.PageRouteInfo>? children, + _i18.Key? key, + _i19.SnPost? originalPost, + List<_i17.PageRouteInfo>? children, }) : super( PostComposeRoute.name, args: PostComposeRouteArgs(key: key, originalPost: originalPost), @@ -405,13 +448,13 @@ class PostComposeRoute extends _i16.PageRouteInfo { static const String name = 'PostComposeRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final args = data.argsAs( orElse: () => const PostComposeRouteArgs(), ); - return _i11.PostComposeScreen( + return _i12.PostComposeScreen( key: args.key, originalPost: args.originalPost, ); @@ -422,9 +465,9 @@ class PostComposeRoute extends _i16.PageRouteInfo { class PostComposeRouteArgs { const PostComposeRouteArgs({this.key, this.originalPost}); - final _i17.Key? key; + final _i18.Key? key; - final _i18.SnPost? originalPost; + final _i19.SnPost? originalPost; @override String toString() { @@ -433,12 +476,12 @@ class PostComposeRouteArgs { } /// generated route for -/// [_i12.PostDetailScreen] -class PostDetailRoute extends _i16.PageRouteInfo { +/// [_i13.PostDetailScreen] +class PostDetailRoute extends _i17.PageRouteInfo { PostDetailRoute({ - _i17.Key? key, + _i18.Key? key, required int id, - List<_i16.PageRouteInfo>? children, + List<_i17.PageRouteInfo>? children, }) : super( PostDetailRoute.name, args: PostDetailRouteArgs(key: key, id: id), @@ -448,14 +491,14 @@ class PostDetailRoute extends _i16.PageRouteInfo { static const String name = 'PostDetailRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => PostDetailRouteArgs(id: pathParams.getInt('id')), ); - return _i12.PostDetailScreen(key: args.key, id: args.id); + return _i13.PostDetailScreen(key: args.key, id: args.id); }, ); } @@ -463,7 +506,7 @@ class PostDetailRoute extends _i16.PageRouteInfo { class PostDetailRouteArgs { const PostDetailRouteArgs({this.key, required this.id}); - final _i17.Key? key; + final _i18.Key? key; final int id; @@ -474,12 +517,12 @@ class PostDetailRouteArgs { } /// generated route for -/// [_i11.PostEditScreen] -class PostEditRoute extends _i16.PageRouteInfo { +/// [_i12.PostEditScreen] +class PostEditRoute extends _i17.PageRouteInfo { PostEditRoute({ - _i17.Key? key, + _i18.Key? key, required int id, - List<_i16.PageRouteInfo>? children, + List<_i17.PageRouteInfo>? children, }) : super( PostEditRoute.name, args: PostEditRouteArgs(key: key, id: id), @@ -489,14 +532,14 @@ class PostEditRoute extends _i16.PageRouteInfo { static const String name = 'PostEditRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => PostEditRouteArgs(id: pathParams.getInt('id')), ); - return _i11.PostEditScreen(key: args.key, id: args.id); + return _i12.PostEditScreen(key: args.key, id: args.id); }, ); } @@ -504,7 +547,7 @@ class PostEditRoute extends _i16.PageRouteInfo { class PostEditRouteArgs { const PostEditRouteArgs({this.key, required this.id}); - final _i17.Key? key; + final _i18.Key? key; final int id; @@ -515,12 +558,12 @@ class PostEditRouteArgs { } /// generated route for -/// [_i13.RealmDetailScreen] -class RealmDetailRoute extends _i16.PageRouteInfo { +/// [_i14.RealmDetailScreen] +class RealmDetailRoute extends _i17.PageRouteInfo { RealmDetailRoute({ - _i17.Key? key, + _i18.Key? key, required String slug, - List<_i16.PageRouteInfo>? children, + List<_i17.PageRouteInfo>? children, }) : super( RealmDetailRoute.name, args: RealmDetailRouteArgs(key: key, slug: slug), @@ -530,14 +573,14 @@ class RealmDetailRoute extends _i16.PageRouteInfo { static const String name = 'RealmDetailRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => RealmDetailRouteArgs(slug: pathParams.getString('slug')), ); - return _i13.RealmDetailScreen(key: args.key, slug: args.slug); + return _i14.RealmDetailScreen(key: args.key, slug: args.slug); }, ); } @@ -545,7 +588,7 @@ class RealmDetailRoute extends _i16.PageRouteInfo { class RealmDetailRouteArgs { const RealmDetailRouteArgs({this.key, required this.slug}); - final _i17.Key? key; + final _i18.Key? key; final String slug; @@ -556,49 +599,49 @@ class RealmDetailRouteArgs { } /// generated route for -/// [_i7.RealmListScreen] -class RealmListRoute extends _i16.PageRouteInfo { - const RealmListRoute({List<_i16.PageRouteInfo>? children}) +/// [_i8.RealmListScreen] +class RealmListRoute extends _i17.PageRouteInfo { + const RealmListRoute({List<_i17.PageRouteInfo>? children}) : super(RealmListRoute.name, initialChildren: children); static const String name = 'RealmListRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i7.RealmListScreen(); + return const _i8.RealmListScreen(); }, ); } /// generated route for -/// [_i14.TabsScreen] -class TabsRoute extends _i16.PageRouteInfo { - const TabsRoute({List<_i16.PageRouteInfo>? children}) +/// [_i15.TabsScreen] +class TabsRoute extends _i17.PageRouteInfo { + const TabsRoute({List<_i17.PageRouteInfo>? children}) : super(TabsRoute.name, initialChildren: children); static const String name = 'TabsRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i14.TabsScreen(); + return const _i15.TabsScreen(); }, ); } /// generated route for -/// [_i15.UpdateProfileScreen] -class UpdateProfileRoute extends _i16.PageRouteInfo { - const UpdateProfileRoute({List<_i16.PageRouteInfo>? children}) +/// [_i16.UpdateProfileScreen] +class UpdateProfileRoute extends _i17.PageRouteInfo { + const UpdateProfileRoute({List<_i17.PageRouteInfo>? children}) : super(UpdateProfileRoute.name, initialChildren: children); static const String name = 'UpdateProfileRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i15.UpdateProfileScreen(); + return const _i16.UpdateProfileScreen(); }, ); } diff --git a/lib/screens/account/profile.dart b/lib/screens/account/profile.dart new file mode 100644 index 0000000..c2c2bbf --- /dev/null +++ b/lib/screens/account/profile.dart @@ -0,0 +1,93 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/user.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/content/cloud_files.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'profile.g.dart'; + +@riverpod +Future account(Ref ref, String uname) async { + final apiClient = ref.watch(apiClientProvider); + final resp = await apiClient.get("/accounts/$uname"); + return SnAccount.fromJson(resp.data); +} + +@RoutePage() +class AccountProfileScreen extends HookConsumerWidget { + final String name; + const AccountProfileScreen({ + super.key, + @PathParam("name") required this.name, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final accountAsync = ref.watch(accountProvider(name)); + return accountAsync.when( + data: + (data) => AppScaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 180, + pinned: true, + flexibleSpace: FlexibleSpaceBar( + background: + data.profile.backgroundId != null + ? CloudImageWidget( + fileId: data.profile.backgroundId!, + ) + : Container( + color: + Theme.of(context).appBarTheme.backgroundColor, + ), + title: Text( + data.name, + style: TextStyle( + color: Theme.of(context).appBarTheme.foregroundColor, + shadows: [ + Shadow( + color: Colors.black54, + blurRadius: 5.0, + offset: Offset(1.0, 1.0), + ), + ], + ), + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + data.profile.bio ?? '', + style: const TextStyle(fontSize: 16), + ), + ], + ), + ), + ), + ], + ), + ), + error: + (error, stackTrace) => AppScaffold( + appBar: AppBar(leading: const PageBackButton()), + body: Center(child: Text(error.toString())), + ), + loading: + () => AppScaffold( + appBar: AppBar(leading: const PageBackButton()), + body: Center(child: CircularProgressIndicator()), + ), + ); + } +} diff --git a/lib/screens/account/profile.g.dart b/lib/screens/account/profile.g.dart new file mode 100644 index 0000000..9ba0820 --- /dev/null +++ b/lib/screens/account/profile.g.dart @@ -0,0 +1,149 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'profile.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$accountHash() => r'39003ef3250181b9290e0562329c7801d4841941'; + +/// 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 [account]. +@ProviderFor(account) +const accountProvider = AccountFamily(); + +/// See also [account]. +class AccountFamily extends Family> { + /// See also [account]. + const AccountFamily(); + + /// See also [account]. + AccountProvider call(String uname) { + return AccountProvider(uname); + } + + @override + AccountProvider getProviderOverride(covariant AccountProvider provider) { + return call(provider.uname); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'accountProvider'; +} + +/// See also [account]. +class AccountProvider extends AutoDisposeFutureProvider { + /// See also [account]. + AccountProvider(String uname) + : this._internal( + (ref) => account(ref as AccountRef, uname), + from: accountProvider, + name: r'accountProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$accountHash, + dependencies: AccountFamily._dependencies, + allTransitiveDependencies: AccountFamily._allTransitiveDependencies, + uname: uname, + ); + + AccountProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.uname, + }) : super.internal(); + + final String uname; + + @override + Override overrideWith( + FutureOr Function(AccountRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: AccountProvider._internal( + (ref) => create(ref as AccountRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + uname: uname, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _AccountProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is AccountProvider && other.uname == uname; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, uname.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin AccountRef on AutoDisposeFutureProviderRef { + /// The parameter `uname` of this provider. + String get uname; +} + +class _AccountProviderElement + extends AutoDisposeFutureProviderElement + with AccountRef { + _AccountProviderElement(super.provider); + + @override + String get uname => (origin as AccountProvider).uname; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/screens/auth/create_account.dart b/lib/screens/auth/create_account.dart index 89c11dd..6c27710 100644 --- a/lib/screens/auth/create_account.dart +++ b/lib/screens/auth/create_account.dart @@ -10,7 +10,6 @@ import 'package:island/route.gr.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:url_launcher/url_launcher_string.dart'; diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 6dd0386..7426164 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -18,7 +18,6 @@ import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:styled_widget/styled_widget.dart'; diff --git a/lib/screens/chat/room_detail.dart b/lib/screens/chat/room_detail.dart index 6631503..96f441d 100644 --- a/lib/screens/chat/room_detail.dart +++ b/lib/screens/chat/room_detail.dart @@ -15,7 +15,6 @@ import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:styled_widget/styled_widget.dart'; part 'room_detail.freezed.dart'; @@ -42,7 +41,7 @@ class ChatDetailScreen extends HookConsumerWidget { offset: Offset(1.0, 1.0), ); - return Scaffold( + return AppScaffold( body: roomState.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) => Center(child: Text('Error: $error')), diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 4e2ccc1..efe46cc 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -85,27 +85,34 @@ class _PostListController extends StateNotifier> { if (isLoading || hasReachedMax) return; isLoading = true; - final response = await _dio.get( - '/posts', - queryParameters: {'offset': offset, 'take': take}, - ); + try { + final response = await _dio.get( + '/posts', + queryParameters: {'offset': offset, 'take': take}, + ); - final List fetched = - (response.data as List) - .map((e) => SnPost.fromJson(e as Map)) - .toList(); + final List fetched = + (response.data as List) + .map((e) => SnPost.fromJson(e as Map)) + .toList(); - final headerTotal = int.tryParse(response.headers['x-total']?.first ?? ''); - if (headerTotal != null) total = headerTotal; + final headerTotal = int.tryParse(response.headers['x-total']?.first ?? ''); + if (headerTotal != null) total = headerTotal; - state = [...state, ...fetched]; - offset += fetched.length; - if (state.length >= total) hasReachedMax = true; + if (!mounted) return; // Check if the notifier is still mounted - isLoading = false; + state = [...state, ...fetched]; + offset += fetched.length; + if (state.length >= total) hasReachedMax = true; + } finally { + if (mounted) { + isLoading = false; + } + } } void updateOne(int index, SnPost post) { + if (!mounted) return; // Check if the notifier is still mounted final updatedPosts = [...state]; updatedPosts[index] = post; state = updatedPosts; diff --git a/lib/screens/posts/compose.dart b/lib/screens/posts/compose.dart index d828504..e7e18da 100644 --- a/lib/screens/posts/compose.dart +++ b/lib/screens/posts/compose.dart @@ -23,7 +23,6 @@ import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/post/publishers_modal.dart'; import 'package:markdown_editor_plus/widgets/markdown_auto_preview.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:styled_widget/styled_widget.dart'; @RoutePage() diff --git a/lib/screens/realm/detail.dart b/lib/screens/realm/detail.dart index 1f3b725..7840117 100644 --- a/lib/screens/realm/detail.dart +++ b/lib/screens/realm/detail.dart @@ -14,7 +14,6 @@ import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:styled_widget/styled_widget.dart'; diff --git a/lib/widgets/app_scaffold.dart b/lib/widgets/app_scaffold.dart index 9c04ae9..21078d6 100644 --- a/lib/widgets/app_scaffold.dart +++ b/lib/widgets/app_scaffold.dart @@ -2,23 +2,24 @@ import 'dart:io'; import 'package:auto_route/auto_route.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/pods/websocket.dart'; import 'package:island/route.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:path_provider/path_provider.dart'; import 'package:responsive_framework/responsive_framework.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:styled_widget/styled_widget.dart'; -class WindowScaffold extends StatelessWidget { +class WindowScaffold extends HookConsumerWidget { final Widget child; final AppRouter router; const WindowScaffold({super.key, required this.child, required this.router}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; @@ -31,56 +32,65 @@ class WindowScaffold extends StatelessWidget { ); return Material( - child: Column( + child: Stack( + fit: StackFit.expand, children: [ - WindowTitleBarBox( - child: Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context).dividerColor, - width: 1 / devicePixelRatio, + Column( + children: [ + WindowTitleBarBox( + child: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 1 / devicePixelRatio, + ), + ), + ), + child: MoveWindow( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: + Platform.isMacOS + ? MainAxisAlignment.center + : MainAxisAlignment.start, + children: [ + Expanded( + child: Text( + 'Solar Network', + textAlign: + Platform.isMacOS + ? TextAlign.center + : TextAlign.start, + ).padding(horizontal: 12, vertical: 5), + ), + if (!Platform.isMacOS) + MinimizeWindowButton(colors: windowButtonColor), + if (!Platform.isMacOS) + MaximizeWindowButton(colors: windowButtonColor), + if (!Platform.isMacOS) + CloseWindowButton( + colors: windowButtonColor, + onPressed: () => appWindow.hide(), + ), + ], + ), ), ), ), - child: MoveWindow( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: - Platform.isMacOS - ? MainAxisAlignment.center - : MainAxisAlignment.start, - children: [ - Expanded( - child: Text( - 'Solar Network', - textAlign: - Platform.isMacOS - ? TextAlign.center - : TextAlign.start, - ).padding(horizontal: 12, vertical: 5), - ), - if (!Platform.isMacOS) - MinimizeWindowButton(colors: windowButtonColor), - if (!Platform.isMacOS) - MaximizeWindowButton(colors: windowButtonColor), - if (!Platform.isMacOS) - CloseWindowButton( - colors: windowButtonColor, - onPressed: () => appWindow.hide(), - ), - ], - ), - ), - ), + Expanded(child: child), + ], ), - Expanded(child: child), + _WebSocketIndicator(), ], ), ); } - return child; + return Stack( + fit: StackFit.expand, + children: [child, _WebSocketIndicator()], + ); } } @@ -242,3 +252,50 @@ class AppBackground extends ConsumerWidget { return Material(color: Colors.transparent, child: child); } } + +class _WebSocketIndicator extends HookConsumerWidget { + const _WebSocketIndicator(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final websocketState = ref.watch(websocketStateProvider); + final indicatorHeight = MediaQuery.of(context).padding.top + 60; + + Color indicatorColor; + String indicatorText; + + if (websocketState == WebSocketState.connected()) { + indicatorColor = Colors.green; + indicatorText = 'connectionConnected'; + } else if (websocketState == WebSocketState.connecting()) { + indicatorColor = Colors.teal; + indicatorText = 'connectionReconnecting'; + } else { + indicatorColor = Colors.orange; + indicatorText = 'connectionDisconnected'; + } + + return AnimatedPositioned( + duration: Duration(milliseconds: 1850), + top: websocketState == WebSocketState.connected() ? -indicatorHeight : 0, + curve: Curves.fastLinearToSlowEaseIn, + left: 0, + right: 0, + height: indicatorHeight, + child: Material( + elevation: 4, + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + color: indicatorColor, + child: Center( + child: + Text( + indicatorText, + style: TextStyle(color: Colors.white, fontSize: 16), + ).tr(), + ).padding(top: MediaQuery.of(context).padding.top), + ), + ), + ); + } +} diff --git a/lib/widgets/post/post_quick_reply.dart b/lib/widgets/post/post_quick_reply.dart index c7fbf2f..e05c82c 100644 --- a/lib/widgets/post/post_quick_reply.dart +++ b/lib/widgets/post/post_quick_reply.dart @@ -9,7 +9,6 @@ import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/post/publishers_modal.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:styled_widget/styled_widget.dart'; class PostQuickReply extends HookConsumerWidget {