WebSocket connection indicator

This commit is contained in:
LittleSheep 2025-05-05 20:59:52 +08:00
parent e4e562918c
commit f266968644
13 changed files with 580 additions and 212 deletions

View File

@ -103,5 +103,8 @@
},
"reactionPositive": "Postive",
"reactionNegative": "Negative",
"reactionNeutral": "Neutral"
"reactionNeutral": "Neutral",
"connectionConnected": "Connected",
"connectionDisconnected": "Disconnected",
"connectionReconnecting": "Reconnecting"
}

View File

@ -41,11 +41,14 @@ class WebSocketService {
WebSocketChannel? _channel;
final StreamController<WebSocketPacket> _streamController =
StreamController<WebSocketPacket>.broadcast();
final StreamController<WebSocketState> _statusStreamController =
StreamController<WebSocketState>.broadcast();
String? _lastUrl;
String? _lastAtk;
Timer? _reconnectTimer;
Stream<WebSocketPacket> get dataStream => _streamController.stream;
Stream<WebSocketState> get statusStream => _statusStreamController.stream;
Future<void> 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<WebSocketState> {
final Ref ref;
Timer? _reconnectTimer;
WebSocketStateNotifier(this.ref) : super(const WebSocketState.disconnected());
@ -132,11 +142,22 @@ class WebSocketStateNotifier extends StateNotifier<WebSocketState> {
}
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<WebSocketState> {
void close() {
final service = ref.read(websocketProvider);
service.close();
_reconnectTimer?.cancel();
state = const WebSocketState.disconnected();
}
}

View File

@ -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<void> {
const AccountRoute({List<_i16.PageRouteInfo>? children})
/// [_i1.AccountProfileScreen]
class AccountProfileRoute extends _i17.PageRouteInfo<AccountProfileRouteArgs> {
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<AccountProfileRouteArgs>(
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<void> {
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<ChatDetailRouteArgs> {
/// [_i3.ChatDetailScreen]
class ChatDetailRoute extends _i17.PageRouteInfo<ChatDetailRouteArgs> {
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<ChatDetailRouteArgs> {
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<ChatDetailRouteArgs>(
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<ChatDetailRouteArgs> {
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<void> {
const ChatListRoute({List<_i16.PageRouteInfo>? children})
/// [_i4.ChatListScreen]
class ChatListRoute extends _i17.PageRouteInfo<void> {
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<ChatRoomRouteArgs> {
/// [_i5.ChatRoomScreen]
class ChatRoomRoute extends _i17.PageRouteInfo<ChatRoomRouteArgs> {
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<ChatRoomRouteArgs> {
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<ChatRoomRouteArgs>(
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<ChatRoomRouteArgs> {
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<void> {
const CreateAccountRoute({List<_i16.PageRouteInfo>? children})
/// [_i6.CreateAccountScreen]
class CreateAccountRoute extends _i17.PageRouteInfo<void> {
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<EditChatRouteArgs> {
EditChatRoute({_i17.Key? key, int? id, List<_i16.PageRouteInfo>? children})
/// [_i4.EditChatScreen]
class EditChatRoute extends _i17.PageRouteInfo<EditChatRouteArgs> {
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<EditChatRouteArgs> {
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<EditChatRouteArgs>(
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<EditChatRouteArgs> {
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<EditPublisherRouteArgs> {
/// [_i7.EditPublisherScreen]
class EditPublisherRoute extends _i17.PageRouteInfo<EditPublisherRouteArgs> {
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<EditPublisherRouteArgs> {
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<EditPublisherRouteArgs>(
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<EditPublisherRouteArgs> {
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<EditRealmRouteArgs> {
/// [_i8.EditRealmScreen]
class EditRealmRoute extends _i17.PageRouteInfo<EditRealmRouteArgs> {
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<EditRealmRouteArgs> {
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<EditRealmRouteArgs>(
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<EditRealmRouteArgs> {
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<void> {
const ExploreRoute({List<_i16.PageRouteInfo>? children})
/// [_i9.ExploreScreen]
class ExploreRoute extends _i17.PageRouteInfo<void> {
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<void> {
const LoginRoute({List<_i16.PageRouteInfo>? children})
/// [_i10.LoginScreen]
class LoginRoute extends _i17.PageRouteInfo<void> {
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<void> {
const ManagedPublisherRoute({List<_i16.PageRouteInfo>? children})
/// [_i7.ManagedPublisherScreen]
class ManagedPublisherRoute extends _i17.PageRouteInfo<void> {
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<void> {
const MyselfProfileRoute({List<_i16.PageRouteInfo>? children})
/// [_i11.MyselfProfileScreen]
class MyselfProfileRoute extends _i17.PageRouteInfo<void> {
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<void> {
const NewChatRoute({List<_i16.PageRouteInfo>? children})
/// [_i4.NewChatScreen]
class NewChatRoute extends _i17.PageRouteInfo<void> {
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<void> {
const NewPublisherRoute({List<_i16.PageRouteInfo>? children})
/// [_i7.NewPublisherScreen]
class NewPublisherRoute extends _i17.PageRouteInfo<void> {
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<void> {
const NewRealmRoute({List<_i16.PageRouteInfo>? children})
/// [_i8.NewRealmScreen]
class NewRealmRoute extends _i17.PageRouteInfo<void> {
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<PostComposeRouteArgs> {
/// [_i12.PostComposeScreen]
class PostComposeRoute extends _i17.PageRouteInfo<PostComposeRouteArgs> {
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<PostComposeRouteArgs> {
static const String name = 'PostComposeRoute';
static _i16.PageInfo page = _i16.PageInfo(
static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
final args = data.argsAs<PostComposeRouteArgs>(
orElse: () => const PostComposeRouteArgs(),
);
return _i11.PostComposeScreen(
return _i12.PostComposeScreen(
key: args.key,
originalPost: args.originalPost,
);
@ -422,9 +465,9 @@ class PostComposeRoute extends _i16.PageRouteInfo<PostComposeRouteArgs> {
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<PostDetailRouteArgs> {
/// [_i13.PostDetailScreen]
class PostDetailRoute extends _i17.PageRouteInfo<PostDetailRouteArgs> {
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<PostDetailRouteArgs> {
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<PostDetailRouteArgs>(
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<PostDetailRouteArgs> {
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<PostEditRouteArgs> {
/// [_i12.PostEditScreen]
class PostEditRoute extends _i17.PageRouteInfo<PostEditRouteArgs> {
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<PostEditRouteArgs> {
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<PostEditRouteArgs>(
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<PostEditRouteArgs> {
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<RealmDetailRouteArgs> {
/// [_i14.RealmDetailScreen]
class RealmDetailRoute extends _i17.PageRouteInfo<RealmDetailRouteArgs> {
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<RealmDetailRouteArgs> {
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<RealmDetailRouteArgs>(
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<RealmDetailRouteArgs> {
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<void> {
const RealmListRoute({List<_i16.PageRouteInfo>? children})
/// [_i8.RealmListScreen]
class RealmListRoute extends _i17.PageRouteInfo<void> {
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<void> {
const TabsRoute({List<_i16.PageRouteInfo>? children})
/// [_i15.TabsScreen]
class TabsRoute extends _i17.PageRouteInfo<void> {
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<void> {
const UpdateProfileRoute({List<_i16.PageRouteInfo>? children})
/// [_i16.UpdateProfileScreen]
class UpdateProfileRoute extends _i17.PageRouteInfo<void> {
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();
},
);
}

View File

@ -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<SnAccount> 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()),
),
);
}
}

View File

@ -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<AsyncValue<SnAccount>> {
/// 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<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'accountProvider';
}
/// See also [account].
class AccountProvider extends AutoDisposeFutureProvider<SnAccount> {
/// 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<SnAccount> 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<SnAccount> 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<SnAccount> {
/// The parameter `uname` of this provider.
String get uname;
}
class _AccountProviderElement
extends AutoDisposeFutureProviderElement<SnAccount>
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

View File

@ -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';

View File

@ -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';

View File

@ -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')),

View File

@ -85,27 +85,34 @@ class _PostListController extends StateNotifier<List<SnPost>> {
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<SnPost> fetched =
(response.data as List)
.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
.toList();
final List<SnPost> fetched =
(response.data as List)
.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
.toList();
final headerTotal = int.tryParse(response.headers['x-total']?.first ?? '');
if (headerTotal != null) total = headerTotal;
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;

View File

@ -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()

View File

@ -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';

View File

@ -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),
),
),
);
}
}

View File

@ -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 {