🎨 Formatted all the code

This commit is contained in:
LittleSheep 2024-05-01 17:37:34 +08:00
parent 28c0094837
commit cd5cfedb2f
51 changed files with 938 additions and 540 deletions

View File

@ -21,8 +21,9 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file # `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint. # producing the lint.
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule lines_longer_than_80_chars: false
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule avoid_print: false # Uncomment to disable the `avoid_print` rule
prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@ -16,6 +16,8 @@
"confirmation": "Confirmation", "confirmation": "Confirmation",
"confirmCancel": "Not sure", "confirmCancel": "Not sure",
"confirmOkay": "OK", "confirmOkay": "OK",
"email": "Email Address",
"nickname": "Nickname",
"username": "Username", "username": "Username",
"password": "Password", "password": "Password",
"next": "Next", "next": "Next",

View File

@ -16,6 +16,8 @@
"confirmation": "确认", "confirmation": "确认",
"confirmCancel": "不太确定", "confirmCancel": "不太确定",
"confirmOkay": "确定", "confirmOkay": "确定",
"email": "邮箱地址",
"nickname": "显示名",
"username": "用户名", "username": "用户名",
"password": "密码", "password": "密码",
"next": "下一步", "next": "下一步",

View File

@ -25,28 +25,29 @@ class Call {
}); });
factory Call.fromJson(Map<String, dynamic> json) => Call( factory Call.fromJson(Map<String, dynamic> json) => Call(
id: json["id"], id: json["id"],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json["created_at"]),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json["updated_at"]),
deletedAt: json["deleted_at"], deletedAt: json["deleted_at"],
endedAt: json["ended_at"] != null ? DateTime.parse(json["ended_at"]) : null, endedAt:
externalId: json["external_id"], json["ended_at"] != null ? DateTime.parse(json["ended_at"]) : null,
founderId: json["founder_id"], externalId: json["external_id"],
channelId: json["channel_id"], founderId: json["founder_id"],
channel: Channel.fromJson(json["channel"]), channelId: json["channel_id"],
); channel: Channel.fromJson(json["channel"]),
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, "id": id,
"created_at": createdAt.toIso8601String(), "created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), "updated_at": updatedAt.toIso8601String(),
"deleted_at": deletedAt, "deleted_at": deletedAt,
"ended_at": endedAt?.toIso8601String(), "ended_at": endedAt?.toIso8601String(),
"external_id": externalId, "external_id": externalId,
"founder_id": founderId, "founder_id": founderId,
"channel_id": channelId, "channel_id": channelId,
"channel": channel.toJson(), "channel": channel.toJson(),
}; };
} }
enum ParticipantStatsType { enum ParticipantStatsType {
@ -60,9 +61,9 @@ enum ParticipantStatsType {
class ParticipantTrack { class ParticipantTrack {
ParticipantTrack( ParticipantTrack(
{required this.participant, {required this.participant,
required this.videoTrack, required this.videoTrack,
required this.isScreenShare}); required this.isScreenShare});
VideoTrack? videoTrack; VideoTrack? videoTrack;
Participant participant; Participant participant;
bool isScreenShare; bool isScreenShare;
} }

View File

@ -32,36 +32,36 @@ class Channel {
}); });
factory Channel.fromJson(Map<String, dynamic> json) => Channel( factory Channel.fromJson(Map<String, dynamic> json) => Channel(
id: json["id"], id: json["id"],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json["created_at"]),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json["updated_at"]),
deletedAt: json["deleted_at"], deletedAt: json["deleted_at"],
alias: json["alias"], alias: json["alias"],
name: json["name"], name: json["name"],
description: json["description"], description: json["description"],
members: json["members"], members: json["members"],
calls: json["calls"], calls: json["calls"],
type: json["type"], type: json["type"],
account: Account.fromJson(json["account"]), account: Account.fromJson(json["account"]),
accountId: json["account_id"], accountId: json["account_id"],
realmId: json["realm_id"], realmId: json["realm_id"],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, "id": id,
"created_at": createdAt.toIso8601String(), "created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), "updated_at": updatedAt.toIso8601String(),
"deleted_at": deletedAt, "deleted_at": deletedAt,
"alias": alias, "alias": alias,
"name": name, "name": name,
"description": description, "description": description,
"members": members, "members": members,
"calls": calls, "calls": calls,
"type": type, "type": type,
"account": account, "account": account,
"account_id": accountId, "account_id": accountId,
"realm_id": realmId, "realm_id": realmId,
}; };
} }
class ChannelMember { class ChannelMember {
@ -86,24 +86,24 @@ class ChannelMember {
}); });
factory ChannelMember.fromJson(Map<String, dynamic> json) => ChannelMember( factory ChannelMember.fromJson(Map<String, dynamic> json) => ChannelMember(
id: json["id"], id: json["id"],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json["created_at"]),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json["updated_at"]),
deletedAt: json["deleted_at"], deletedAt: json["deleted_at"],
channelId: json["channel_id"], channelId: json["channel_id"],
accountId: json["account_id"], accountId: json["account_id"],
account: Account.fromJson(json["account"]), account: Account.fromJson(json["account"]),
notify: json["notify"], notify: json["notify"],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, "id": id,
"created_at": createdAt.toIso8601String(), "created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), "updated_at": updatedAt.toIso8601String(),
"deleted_at": deletedAt, "deleted_at": deletedAt,
"channel_id": channelId, "channel_id": channelId,
"account_id": accountId, "account_id": accountId,
"account": account.toJson(), "account": account.toJson(),
"notify": notify, "notify": notify,
}; };
} }

View File

@ -26,30 +26,30 @@ class Friendship {
}); });
factory Friendship.fromJson(Map<String, dynamic> json) => Friendship( factory Friendship.fromJson(Map<String, dynamic> json) => Friendship(
id: json["id"], id: json["id"],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json["created_at"]),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json["updated_at"]),
deletedAt: json["deleted_at"], deletedAt: json["deleted_at"],
accountId: json["account_id"], accountId: json["account_id"],
relatedId: json["related_id"], relatedId: json["related_id"],
blockedBy: json["blocked_by"], blockedBy: json["blocked_by"],
account: Account.fromJson(json["account"]), account: Account.fromJson(json["account"]),
related: Account.fromJson(json["related"]), related: Account.fromJson(json["related"]),
status: json["status"], status: json["status"],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, "id": id,
"created_at": createdAt.toIso8601String(), "created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), "updated_at": updatedAt.toIso8601String(),
"deleted_at": deletedAt, "deleted_at": deletedAt,
"account_id": accountId, "account_id": accountId,
"related_id": relatedId, "related_id": relatedId,
"blocked_by": blockedBy, "blocked_by": blockedBy,
"account": account.toJson(), "account": account.toJson(),
"related": related.toJson(), "related": related.toJson(),
"status": status, "status": status,
}; };
Account getOtherside(int selfId) { Account getOtherside(int selfId) {
if (accountId != selfId) { if (accountId != selfId) {
@ -58,4 +58,4 @@ class Friendship {
return related; return related;
} }
} }
} }

View File

@ -43,11 +43,15 @@ class Message {
content: json["content"], content: json["content"],
metadata: json["metadata"], metadata: json["metadata"],
type: json["type"], type: json["type"],
attachments: List<Attachment>.from(json["attachments"]?.map((x) => Attachment.fromJson(x)) ?? List.empty()), attachments: List<Attachment>.from(
json["attachments"]?.map((x) => Attachment.fromJson(x)) ??
List.empty()),
channel: Channel.fromJson(json["channel"]), channel: Channel.fromJson(json["channel"]),
sender: Sender.fromJson(json["sender"]), sender: Sender.fromJson(json["sender"]),
replyId: json["reply_id"], replyId: json["reply_id"],
replyTo: json["reply_to"] != null ? Message.fromJson(json["reply_to"]) : null, replyTo: json["reply_to"] != null
? Message.fromJson(json["reply_to"])
: null,
channelId: json["channel_id"], channelId: json["channel_id"],
senderId: json["sender_id"], senderId: json["sender_id"],
); );
@ -60,7 +64,8 @@ class Message {
"content": content, "content": content,
"metadata": metadata, "metadata": metadata,
"type": type, "type": type,
"attachments": List<dynamic>.from(attachments?.map((x) => x.toJson()) ?? List.empty()), "attachments": List<dynamic>.from(
attachments?.map((x) => x.toJson()) ?? List.empty()),
"channel": channel?.toJson(), "channel": channel?.toJson(),
"sender": sender.toJson(), "sender": sender.toJson(),
"reply_id": replyId, "reply_id": replyId,

View File

@ -34,7 +34,9 @@ class Notification {
deletedAt: json["deleted_at"], deletedAt: json["deleted_at"],
subject: json["subject"], subject: json["subject"],
content: json["content"], content: json["content"],
links: json["links"] != null ? List<Link>.from(json["links"].map((x) => Link.fromJson(x))) : List.empty(), links: json["links"] != null
? List<Link>.from(json["links"].map((x) => Link.fromJson(x)))
: List.empty(),
isImportant: json["is_important"], isImportant: json["is_important"],
isRealtime: json["is_realtime"], isRealtime: json["is_realtime"],
readAt: json["read_at"], readAt: json["read_at"],
@ -49,7 +51,9 @@ class Notification {
"deleted_at": deletedAt, "deleted_at": deletedAt,
"subject": subject, "subject": subject,
"content": content, "content": content,
"links": links != null ? List<dynamic>.from(links!.map((x) => x.toJson())) : List.empty(), "links": links != null
? List<dynamic>.from(links!.map((x) => x.toJson()))
: List.empty(),
"is_important": isImportant, "is_important": isImportant,
"is_realtime": isRealtime, "is_realtime": isRealtime,
"read_at": readAt, "read_at": readAt,

View File

@ -10,14 +10,14 @@ class NetworkPackage {
}); });
factory NetworkPackage.fromJson(Map<String, dynamic> json) => NetworkPackage( factory NetworkPackage.fromJson(Map<String, dynamic> json) => NetworkPackage(
method: json["w"], method: json["w"],
message: json["m"], message: json["m"],
payload: json["p"], payload: json["p"],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"w": method, "w": method,
"m": message, "m": message,
"p": payload, "p": payload,
}; };
} }

View File

@ -8,7 +8,8 @@ import 'package:solian/utils/service_url.dart';
class AuthProvider extends ChangeNotifier { class AuthProvider extends ChangeNotifier {
AuthProvider(); AuthProvider();
final deviceEndpoint = getRequestUri('passport', '/api/notifications/subscribe'); final deviceEndpoint =
getRequestUri('passport', '/api/notifications/subscribe');
final tokenEndpoint = getRequestUri('passport', '/api/auth/token'); final tokenEndpoint = getRequestUri('passport', '/api/auth/token');
final userinfoEndpoint = getRequestUri('passport', '/api/users/me'); final userinfoEndpoint = getRequestUri('passport', '/api/users/me');
final redirectUrl = Uri.parse('solian://auth'); final redirectUrl = Uri.parse('solian://auth');
@ -29,8 +30,10 @@ class AuthProvider extends ChangeNotifier {
Future<bool> loadClient() async { Future<bool> loadClient() async {
if (await storage.containsKey(key: storageKey)) { if (await storage.containsKey(key: storageKey)) {
try { try {
final credentials = oauth2.Credentials.fromJson((await storage.read(key: storageKey))!); final credentials =
client = oauth2.Client(credentials, identifier: clientId, secret: clientSecret); oauth2.Credentials.fromJson((await storage.read(key: storageKey))!);
client = oauth2.Client(credentials,
identifier: clientId, secret: clientSecret);
await fetchProfiles(); await fetchProfiles();
return true; return true;
} catch (e) { } catch (e) {
@ -42,7 +45,8 @@ class AuthProvider extends ChangeNotifier {
} }
} }
Future<oauth2.Client> createClient(BuildContext context, String username, String password) async { Future<oauth2.Client> createClient(
BuildContext context, String username, String password) async {
if (await loadClient()) { if (await loadClient()) {
return client!; return client!;
} }
@ -68,15 +72,17 @@ class AuthProvider extends ChangeNotifier {
Future<void> refreshToken() async { Future<void> refreshToken() async {
if (client != null) { if (client != null) {
final credentials = final credentials = await client!.credentials.refresh(
await client!.credentials.refresh(identifier: clientId, secret: clientSecret, basicAuth: false); identifier: clientId, secret: clientSecret, basicAuth: false);
client = oauth2.Client(credentials, identifier: clientId, secret: clientSecret); client = oauth2.Client(credentials,
identifier: clientId, secret: clientSecret);
storage.write(key: storageKey, value: credentials.toJson()); storage.write(key: storageKey, value: credentials.toJson());
} }
notifyListeners(); notifyListeners();
} }
Future<void> signin(BuildContext context, String username, String password) async { Future<void> signin(
BuildContext context, String username, String password) async {
client = await createClient(context, username, password); client = await createClient(context, username, password);
storage.write(key: storageKey, value: client!.credentials.toJson()); storage.write(key: storageKey, value: client!.credentials.toJson());
@ -94,7 +100,10 @@ class AuthProvider extends ChangeNotifier {
if (client == null) { if (client == null) {
await loadClient(); await loadClient();
} }
if (lastRefreshedAt == null || DateTime.now().subtract(const Duration(minutes: 3)).isAfter(lastRefreshedAt!)) { if (lastRefreshedAt == null ||
DateTime.now()
.subtract(const Duration(minutes: 3))
.isAfter(lastRefreshedAt!)) {
await refreshToken(); await refreshToken();
lastRefreshedAt = DateTime.now(); lastRefreshedAt = DateTime.now();
} }

View File

@ -32,7 +32,9 @@ class ChatProvider extends ChangeNotifier {
scheme: ori.scheme.replaceFirst('http', 'ws'), scheme: ori.scheme.replaceFirst('http', 'ws'),
host: ori.host, host: ori.host,
path: ori.path, path: ori.path,
queryParameters: {'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)}, queryParameters: {
'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)
},
); );
final channel = WebSocketChannel.connect(uri); final channel = WebSocketChannel.connect(uri);
@ -41,7 +43,8 @@ class ChatProvider extends ChangeNotifier {
return channel; return channel;
} }
bool handleCall(Call call, Channel channel, {Function? onUpdate, Function? onDispose}) { bool handleCall(Call call, Channel channel,
{Function? onUpdate, Function? onDispose}) {
if (this.call != null) return false; if (this.call != null) return false;
this.call = ChatCallInstance( this.call = ChatCallInstance(
@ -106,7 +109,8 @@ class ChatCallInstance {
}); });
void init() { void init() {
subscription = Hardware.instance.onDeviceChange.stream.listen(revertDevices); subscription =
Hardware.instance.onDeviceChange.stream.listen(revertDevices);
room = Room(); room = Room();
listener = room.createListener(); listener = room.createListener();
Hardware.instance.enumerateDevices().then(revertDevices); Hardware.instance.enumerateDevices().then(revertDevices);
@ -114,7 +118,8 @@ class ChatCallInstance {
} }
Future<void> checkPermissions() async { Future<void> checkPermissions() async {
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) return; if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux))
return;
await Permission.camera.request(); await Permission.camera.request();
await Permission.microphone.request(); await Permission.microphone.request();
@ -131,7 +136,8 @@ class ChatCallInstance {
throw Exception("unauthorized"); throw Exception("unauthorized");
} }
var uri = getRequestUri('messaging', '/api/channels/${channel.alias}/calls/ongoing/token'); var uri = getRequestUri(
'messaging', '/api/channels/${channel.alias}/calls/ongoing/token');
var res = await auth.client!.post(uri); var res = await auth.client!.post(uri);
if (res.statusCode == 200) { if (res.statusCode == 200) {
@ -184,10 +190,12 @@ class ChatCallInstance {
useiOSBroadcastExtension: true, useiOSBroadcastExtension: true,
params: VideoParameters( params: VideoParameters(
dimensions: VideoDimensionsPresets.h1080_169, dimensions: VideoDimensionsPresets.h1080_169,
encoding: VideoEncoding(maxBitrate: 3 * 1000 * 1000, maxFramerate: 30), encoding:
VideoEncoding(maxBitrate: 3 * 1000 * 1000, maxFramerate: 30),
), ),
), ),
defaultCameraCaptureOptions: CameraCaptureOptions(maxFrameRate: 30, params: videoParameters), defaultCameraCaptureOptions:
CameraCaptureOptions(maxFrameRate: 30, params: videoParameters),
), ),
fastConnectOptions: FastConnectOptions( fastConnectOptions: FastConnectOptions(
microphone: TrackOption(track: audioTrack), microphone: TrackOption(track: audioTrack),
@ -220,7 +228,8 @@ class ChatCallInstance {
room.addListener(onRoomDidUpdate); room.addListener(onRoomDidUpdate);
setupRoomListeners(context); setupRoomListeners(context);
sortParticipants(); sortParticipants();
WidgetsBindingCompatible.instance?.addPostFrameCallback((_) => autoPublish(context)); WidgetsBindingCompatible.instance
?.addPostFrameCallback((_) => autoPublish(context));
if (lkPlatformIsMobile()) { if (lkPlatformIsMobile()) {
Hardware.instance.setSpeakerphoneOn(true); Hardware.instance.setSpeakerphoneOn(true);
@ -295,7 +304,8 @@ class ChatCallInstance {
} }
// First joined people first // First joined people first
return a.participant.joinedAt.millisecondsSinceEpoch - b.participant.joinedAt.millisecondsSinceEpoch; return a.participant.joinedAt.millisecondsSinceEpoch -
b.participant.joinedAt.millisecondsSinceEpoch;
}); });
ParticipantTrack localTrack = ParticipantTrack( ParticipantTrack localTrack = ParticipantTrack(
@ -304,7 +314,8 @@ class ChatCallInstance {
isScreenShare: false, isScreenShare: false,
); );
if (room.localParticipant != null) { if (room.localParticipant != null) {
final localParticipantTracks = room.localParticipant?.videoTrackPublications; final localParticipantTracks =
room.localParticipant?.videoTrackPublications;
if (localParticipantTracks != null) { if (localParticipantTracks != null) {
for (var t in localParticipantTracks) { for (var t in localParticipantTracks) {
localTrack.videoTrack = t.track; localTrack.videoTrack = t.track;
@ -317,7 +328,8 @@ class ChatCallInstance {
if (focusTrack == null) { if (focusTrack == null) {
focusTrack = participantTracks.first; focusTrack = participantTracks.first;
} else { } else {
final idx = participantTracks.indexWhere((x) => focusTrack!.participant.sid == x.participant.sid); final idx = participantTracks
.indexWhere((x) => focusTrack!.participant.sid == x.participant.sid);
focusTrack = participantTracks[idx]; focusTrack = participantTracks[idx];
} }

View File

@ -16,11 +16,11 @@ class FriendProvider extends ChangeNotifier {
var res = await auth.client!.get(uri); var res = await auth.client!.get(uri);
if (res.statusCode == 200) { if (res.statusCode == 200) {
final result = jsonDecode(utf8.decode(res.bodyBytes)) as List<dynamic>; final result = jsonDecode(utf8.decode(res.bodyBytes)) as List<dynamic>;
friends = result.map((x) => Friendship.fromJson(x)).toList(); friends = result.map((x) => Friendship.fromJson(x)).toList();
notifyListeners(); notifyListeners();
} else { } else {
var message = utf8.decode(res.bodyBytes); var message = utf8.decode(res.bodyBytes);
throw Exception(message); throw Exception(message);
} }
} }
} }

View File

@ -17,7 +17,8 @@ class NotifyProvider extends ChangeNotifier {
List<model.Notification> notifications = List.empty(growable: true); List<model.Notification> notifications = List.empty(growable: true);
final FlutterLocalNotificationsPlugin localNotify = FlutterLocalNotificationsPlugin(); final FlutterLocalNotificationsPlugin localNotify =
FlutterLocalNotificationsPlugin();
NotifyProvider() { NotifyProvider() {
initNotify(); initNotify();
@ -31,8 +32,10 @@ class NotifyProvider extends ChangeNotifier {
DarwinNotificationCategory("general"), DarwinNotificationCategory("general"),
], ],
); );
const linuxSettings = LinuxInitializationSettings(defaultActionName: 'Open notification'); const linuxSettings =
const InitializationSettings initializationSettings = InitializationSettings( LinuxInitializationSettings(defaultActionName: 'Open notification');
const InitializationSettings initializationSettings =
InitializationSettings(
android: androidSettings, android: androidSettings,
iOS: darwinSettings, iOS: darwinSettings,
macOS: darwinSettings, macOS: darwinSettings,
@ -43,7 +46,8 @@ class NotifyProvider extends ChangeNotifier {
} }
Future<void> requestPermissions() async { Future<void> requestPermissions() async {
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) return; if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux))
return;
await Permission.notification.request(); await Permission.notification.request();
} }
@ -53,8 +57,11 @@ class NotifyProvider extends ChangeNotifier {
var uri = getRequestUri('passport', '/api/notifications?skip=0&take=25'); var uri = getRequestUri('passport', '/api/notifications?skip=0&take=25');
var res = await auth.client!.get(uri); var res = await auth.client!.get(uri);
if (res.statusCode == 200) { if (res.statusCode == 200) {
final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); final result =
notifications = result.data?.map((x) => model.Notification.fromJson(x)).toList() ?? List.empty(growable: true); PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
notifications =
result.data?.map((x) => model.Notification.fromJson(x)).toList() ??
List.empty(growable: true);
} }
notifyListeners(); notifyListeners();
@ -71,7 +78,9 @@ class NotifyProvider extends ChangeNotifier {
scheme: ori.scheme.replaceFirst('http', 'ws'), scheme: ori.scheme.replaceFirst('http', 'ws'),
host: ori.host, host: ori.host,
path: ori.path, path: ori.path,
queryParameters: {'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)}, queryParameters: {
'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)
},
); );
final channel = WebSocketChannel.connect(uri); final channel = WebSocketChannel.connect(uri);

View File

@ -4,6 +4,7 @@ import 'package:solian/models/channel.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/screens/account.dart'; import 'package:solian/screens/account.dart';
import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/friend.dart';
import 'package:solian/screens/auth/signup.dart';
import 'package:solian/screens/chat/call.dart'; import 'package:solian/screens/chat/call.dart';
import 'package:solian/screens/chat/chat.dart'; import 'package:solian/screens/chat/chat.dart';
import 'package:solian/screens/chat/index.dart'; import 'package:solian/screens/chat/index.dart';
@ -15,7 +16,7 @@ import 'package:solian/screens/notification.dart';
import 'package:solian/screens/posts/comment_editor.dart'; import 'package:solian/screens/posts/comment_editor.dart';
import 'package:solian/screens/posts/moment_editor.dart'; import 'package:solian/screens/posts/moment_editor.dart';
import 'package:solian/screens/posts/screen.dart'; import 'package:solian/screens/posts/screen.dart';
import 'package:solian/screens/signin.dart'; import 'package:solian/screens/auth/signin.dart';
final router = GoRouter( final router = GoRouter(
routes: [ routes: [
@ -37,12 +38,14 @@ final router = GoRouter(
GoRoute( GoRoute(
path: '/chat/create', path: '/chat/create',
name: 'chat.channel.editor', name: 'chat.channel.editor',
builder: (context, state) => ChannelEditorScreen(editing: state.extra as Channel?), builder: (context, state) =>
ChannelEditorScreen(editing: state.extra as Channel?),
), ),
GoRoute( GoRoute(
path: '/chat/c/:channel', path: '/chat/c/:channel',
name: 'chat.channel', name: 'chat.channel',
builder: (context, state) => ChatScreen(alias: state.pathParameters['channel'] as String), builder: (context, state) =>
ChatScreen(alias: state.pathParameters['channel'] as String),
), ),
GoRoute( GoRoute(
path: '/chat/c/:channel/call', path: '/chat/c/:channel/call',
@ -52,12 +55,14 @@ final router = GoRouter(
GoRoute( GoRoute(
path: '/chat/c/:channel/manage', path: '/chat/c/:channel/manage',
name: 'chat.channel.manage', name: 'chat.channel.manage',
builder: (context, state) => ChatManageScreen(channel: state.extra as Channel), builder: (context, state) =>
ChatManageScreen(channel: state.extra as Channel),
), ),
GoRoute( GoRoute(
path: '/chat/c/:channel/member', path: '/chat/c/:channel/member',
name: 'chat.channel.member', name: 'chat.channel.member',
builder: (context, state) => ChatMemberScreen(channel: state.extra as Channel), builder: (context, state) =>
ChatMemberScreen(channel: state.extra as Channel),
), ),
GoRoute( GoRoute(
path: '/account', path: '/account',
@ -67,14 +72,16 @@ final router = GoRouter(
GoRoute( GoRoute(
path: '/posts/publish/moments', path: '/posts/publish/moments',
name: 'posts.moments.editor', name: 'posts.moments.editor',
builder: (context, state) => MomentEditorScreen(editing: state.extra as Post?), builder: (context, state) =>
MomentEditorScreen(editing: state.extra as Post?),
), ),
GoRoute( GoRoute(
path: '/posts/publish/comments', path: '/posts/publish/comments',
name: 'posts.comments.editor', name: 'posts.comments.editor',
builder: (context, state) { builder: (context, state) {
final args = state.extra as CommentPostArguments; final args = state.extra as CommentPostArguments;
return CommentEditorScreen(editing: args.editing, related: args.related); return CommentEditorScreen(
editing: args.editing, related: args.related);
}, },
), ),
GoRoute( GoRoute(
@ -90,6 +97,11 @@ final router = GoRouter(
name: 'auth.sign-in', name: 'auth.sign-in',
builder: (context, state) => SignInScreen(), builder: (context, state) => SignInScreen(),
), ),
GoRoute(
path: '/auth/sign-up',
name: 'auth.sign-up',
builder: (context, state) => SignUpScreen(),
),
GoRoute( GoRoute(
path: '/account/friend', path: '/account/friend',
name: 'account.friend', name: 'account.friend',

View File

@ -83,7 +83,7 @@ class _AccountScreenState extends State<AccountScreen> {
title: AppLocalizations.of(context)!.signUp, title: AppLocalizations.of(context)!.signUp,
caption: AppLocalizations.of(context)!.signUpCaption, caption: AppLocalizations.of(context)!.signUpCaption,
onTap: () { onTap: () {
launchUrl(getRequestUri('passport', '/sign-up')); router.pushNamed('auth.sign-up');
}, },
), ),
], ],
@ -131,7 +131,8 @@ class NameCard extends StatelessWidget {
children: [ children: [
FutureBuilder( FutureBuilder(
future: renderAvatar(context), future: renderAvatar(context),
builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) { builder:
(BuildContext context, AsyncSnapshot<Widget> snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
return snapshot.data!; return snapshot.data!;
} else { } else {
@ -142,7 +143,8 @@ class NameCard extends StatelessWidget {
const SizedBox(width: 20), const SizedBox(width: 20),
FutureBuilder( FutureBuilder(
future: renderLabel(context), future: renderLabel(context),
builder: (BuildContext context, AsyncSnapshot<Column> snapshot) { builder:
(BuildContext context, AsyncSnapshot<Column> snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
return snapshot.data!; return snapshot.data!;
} else { } else {
@ -164,7 +166,12 @@ class ActionCard extends StatelessWidget {
final String caption; final String caption;
final Function onTap; final Function onTap;
const ActionCard({super.key, required this.onTap, required this.title, required this.caption, required this.icon}); const ActionCard(
{super.key,
required this.onTap,
required this.title,
required this.caption,
required this.icon});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -120,14 +120,16 @@ class _FriendScreenState extends State<FriendScreen> {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context)!.username, labelText: AppLocalizations.of(context)!.username,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
], ],
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), foregroundColor:
Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
), ),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.cancel), child: Text(AppLocalizations.of(context)!.cancel),
@ -155,7 +157,8 @@ class _FriendScreenState extends State<FriendScreen> {
DismissDirection getDismissDirection(Friendship relation) { DismissDirection getDismissDirection(Friendship relation) {
if (relation.status == 2) return DismissDirection.endToStart; if (relation.status == 2) return DismissDirection.endToStart;
if (relation.status == 1) return DismissDirection.startToEnd; if (relation.status == 1) return DismissDirection.startToEnd;
if (relation.status == 0 && relation.relatedId != _selfId) return DismissDirection.startToEnd; if (relation.status == 0 && relation.relatedId != _selfId)
return DismissDirection.startToEnd;
return DismissDirection.horizontal; return DismissDirection.horizontal;
} }
@ -220,12 +223,18 @@ class _FriendScreenState extends State<FriendScreen> {
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
child: _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), child: _isSubmitting
? const LinearProgressIndicator().animate().scaleX()
: Container(),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), padding:
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8), const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.8),
child: Text(AppLocalizations.of(context)!.friendPending), child: Text(AppLocalizations.of(context)!.friendPending),
), ),
), ),
@ -235,8 +244,12 @@ class _FriendScreenState extends State<FriendScreen> {
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), padding:
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8), const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.8),
child: Text(AppLocalizations.of(context)!.friendActive), child: Text(AppLocalizations.of(context)!.friendActive),
), ),
), ),
@ -246,8 +259,12 @@ class _FriendScreenState extends State<FriendScreen> {
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), padding:
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8), const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.8),
child: Text(AppLocalizations.of(context)!.friendBlocked), child: Text(AppLocalizations.of(context)!.friendBlocked),
), ),
), ),
@ -260,7 +277,10 @@ class _FriendScreenState extends State<FriendScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
top: BorderSide( top: BorderSide(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8), color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.8),
width: 0.3, width: 0.3,
)), )),
), ),

View File

@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/utils/service_url.dart';
import 'package:solian/widgets/exts.dart';
import 'package:solian/widgets/indent_wrapper.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SignInScreen extends StatelessWidget {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
SignInScreen({super.key});
void performSignIn(BuildContext context) {
final auth = context.read<AuthProvider>();
final username = _usernameController.value.text;
final password = _passwordController.value.text;
if (username.isEmpty || password.isEmpty) return;
auth.signin(context, username, password).then((_) {
router.pop(true);
}).catchError((e) {
List<String> messages = e.toString().split('\n');
if (messages.last.contains("risk")) {
final ticketId = RegExp(r"ticketId=(\d+)").firstMatch(messages.last);
if (ticketId == null) {
context.showErrorDialog(
"requested to multi-factor authenticate, but the ticket id was not found");
}
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(AppLocalizations.of(context)!.riskDetection),
content: Text(AppLocalizations.of(context)!.signInRiskDetected),
actions: [
TextButton(
child: Text(AppLocalizations.of(context)!.next),
onPressed: () {
launchUrlString(
getRequestUri(
'passport', '/mfa?ticket=${ticketId!.group(1)}')
.toString(),
);
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
},
)
],
);
},
);
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(messages.last),
));
}
});
}
@override
Widget build(BuildContext context) {
return IndentWrapper(
title: AppLocalizations.of(context)!.signIn,
hideDrawer: true,
child: Center(
child: Container(
width: MediaQuery.of(context).size.width * 0.6,
constraints: const BoxConstraints(maxWidth: 360),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Image.asset('assets/logo.png', width: 72, height: 72),
),
TextField(
autocorrect: false,
enableSuggestions: false,
controller: _usernameController,
autofillHints: const [AutofillHints.username],
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context)!.username,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 12),
TextField(
obscureText: true,
autocorrect: false,
enableSuggestions: false,
autofillHints: const [AutofillHints.password],
controller: _passwordController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context)!.password,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) => performSignIn(context),
),
const SizedBox(height: 16),
ElevatedButton(
child: Text(AppLocalizations.of(context)!.signIn),
onPressed: () => performSignIn(context),
)
],
),
),
),
);
}
}

View File

@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:solian/providers/auth.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:solian/widgets/indent_wrapper.dart';
class SignUpScreen extends StatelessWidget {
final _emailController = TextEditingController();
final _usernameController = TextEditingController();
final _nicknameController = TextEditingController();
final _passwordController = TextEditingController();
SignUpScreen({super.key});
void performSignIn(BuildContext context) {
final auth = context.read<AuthProvider>();
final email = _emailController.value.text;
final username = _usernameController.value.text;
final nickname = _passwordController.value.text;
final password = _passwordController.value.text;
if (email.isEmpty ||
username.isEmpty ||
nickname.isEmpty ||
password.isEmpty) return;
}
@override
Widget build(BuildContext context) {
return IndentWrapper(
title: AppLocalizations.of(context)!.signUp,
hideDrawer: true,
child: Center(
child: Container(
width: MediaQuery.of(context).size.width * 0.6,
constraints: const BoxConstraints(maxWidth: 360),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Image.asset('assets/logo.png', width: 72, height: 72),
),
TextField(
autocorrect: false,
enableSuggestions: false,
controller: _usernameController,
autofillHints: const [AutofillHints.username],
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context)!.username,
prefixText: '@',
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 12),
TextField(
autocorrect: false,
enableSuggestions: false,
controller: _nicknameController,
autofillHints: const [AutofillHints.nickname],
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context)!.nickname,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 12),
TextField(
autocorrect: false,
enableSuggestions: false,
controller: _emailController,
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context)!.email,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 12),
TextField(
obscureText: true,
autocorrect: false,
enableSuggestions: false,
autofillHints: const [AutofillHints.password],
controller: _passwordController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context)!.password,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) => performSignIn(context),
),
const SizedBox(height: 16),
ElevatedButton(
child: Text(AppLocalizations.of(context)!.signUp),
onPressed: () => performSignIn(context),
)
],
),
),
),
);
}
}

View File

@ -92,14 +92,16 @@ class _ChatCallState extends State<ChatCall> {
itemCount: math.max(0, _call.participantTracks.length), itemCount: math.max(0, _call.participantTracks.length),
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final track = _call.participantTracks[index]; final track = _call.participantTracks[index];
if (track.participant.sid == _call.focusTrack?.participant.sid) { if (track.participant.sid ==
_call.focusTrack?.participant.sid) {
return Container(); return Container();
} }
return Padding( return Padding(
padding: const EdgeInsets.only(top: 8, left: 8), padding: const EdgeInsets.only(top: 8, left: 8),
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget( child: InteractiveParticipantWidget(
isFixed: true, isFixed: true,
width: 120, width: 120,
@ -107,7 +109,8 @@ class _ChatCallState extends State<ChatCall> {
color: Theme.of(context).cardColor, color: Theme.of(context).cardColor,
participant: track, participant: track,
onTap: () { onTap: () {
if (track.participant.sid != _call.focusTrack?.participant.sid) { if (track.participant.sid !=
_call.focusTrack?.participant.sid) {
_call.changeFocusTrack(track); _call.changeFocusTrack(track);
} }
}, },

View File

@ -113,10 +113,13 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
constraints: const BoxConstraints(maxWidth: 640), constraints: const BoxConstraints(maxWidth: 640),
child: Column( child: Column(
children: [ children: [
_isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), _isSubmitting
? const LinearProgressIndicator().animate().scaleX()
: Container(),
ListTile( ListTile(
title: Text(AppLocalizations.of(context)!.chatChannelUsage), title: Text(AppLocalizations.of(context)!.chatChannelUsage),
subtitle: Text(AppLocalizations.of(context)!.chatChannelUsageCaption), subtitle:
Text(AppLocalizations.of(context)!.chatChannelUsageCaption),
leading: const CircleAvatar( leading: const CircleAvatar(
backgroundColor: Colors.teal, backgroundColor: Colors.teal,
child: Icon(Icons.tag, color: Colors.white), child: Icon(Icons.tag, color: Colors.white),
@ -124,7 +127,8 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
), ),
const Divider(thickness: 0.3), const Divider(thickness: 0.3),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
@ -132,15 +136,18 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
autofocus: true, autofocus: true,
controller: _aliasController, controller: _aliasController,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
hintText: AppLocalizations.of(context)!.chatChannelAliasLabel, hintText: AppLocalizations.of(context)!
.chatChannelAliasLabel,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
TextButton( TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
shape: const CircleBorder(), shape: const CircleBorder(),
visualDensity: const VisualDensity(horizontal: -2, vertical: -2), visualDensity:
const VisualDensity(horizontal: -2, vertical: -2),
), ),
onPressed: () => randomizeAlias(), onPressed: () => randomizeAlias(),
child: const Icon(Icons.refresh), child: const Icon(Icons.refresh),
@ -150,20 +157,24 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
), ),
const Divider(thickness: 0.3), const Divider(thickness: 0.3),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField( child: TextField(
autocorrect: true, autocorrect: true,
controller: _nameController, controller: _nameController,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
hintText: AppLocalizations.of(context)!.chatChannelNameLabel, hintText:
AppLocalizations.of(context)!.chatChannelNameLabel,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
const Divider(thickness: 0.3), const Divider(thickness: 0.3),
Expanded( Expanded(
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: TextField( child: TextField(
minLines: 5, minLines: 5,
maxLines: null, maxLines: null,
@ -171,9 +182,11 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
controller: _descriptionController, controller: _descriptionController,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
hintText: AppLocalizations.of(context)!.chatChannelDescriptionLabel, hintText: AppLocalizations.of(context)!
.chatChannelDescriptionLabel,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
), ),

View File

@ -36,7 +36,8 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
_selfId = prof['id']; _selfId = prof['id'];
var uri = getRequestUri('messaging', '/api/channels/${widget.channel.alias}/members'); var uri = getRequestUri(
'messaging', '/api/channels/${widget.channel.alias}/members');
var res = await auth.client!.get(uri); var res = await auth.client!.get(uri);
if (res.statusCode == 200) { if (res.statusCode == 200) {
@ -59,7 +60,8 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
return; return;
} }
var uri = getRequestUri('messaging', '/api/channels/${widget.channel.alias}/kick'); var uri = getRequestUri(
'messaging', '/api/channels/${widget.channel.alias}/kick');
var res = await auth.client!.post( var res = await auth.client!.post(
uri, uri,
@ -89,7 +91,8 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
return; return;
} }
var uri = getRequestUri('messaging', '/api/channels/${widget.channel.alias}/invite'); var uri = getRequestUri(
'messaging', '/api/channels/${widget.channel.alias}/invite');
var res = await auth.client!.post( var res = await auth.client!.post(
uri, uri,
@ -153,7 +156,9 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
child: _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), child: _isSubmitting
? const LinearProgressIndicator().animate().scaleX()
: Container(),
), ),
SliverList.builder( SliverList.builder(
itemCount: _members.length, itemCount: _members.length,
@ -164,7 +169,9 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
return Dismissible( return Dismissible(
key: Key(randomId.toString()), key: Key(randomId.toString()),
direction: getKickable(element) ? DismissDirection.startToEnd : DismissDirection.none, direction: getKickable(element)
? DismissDirection.startToEnd
: DismissDirection.none,
background: Container( background: Container(
color: Colors.red, color: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
@ -172,7 +179,8 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
child: const Icon(Icons.remove, color: Colors.white), child: const Icon(Icons.remove, color: Colors.white),
), ),
child: ListTile( child: ListTile(
leading: AccountAvatar(source: element.account.avatar, direct: true), leading: AccountAvatar(
source: element.account.avatar, direct: true),
title: Text(element.account.nick), title: Text(element.account.nick),
subtitle: Text(element.account.name), subtitle: Text(element.account.name),
), ),

View File

@ -34,7 +34,8 @@ class _ChatScreenState extends State<ChatScreen> {
Call? _ongoingCall; Call? _ongoingCall;
Channel? _channelMeta; Channel? _channelMeta;
final PagingController<int, Message> _pagingController = PagingController(firstPageKey: 0); final PagingController<int, Message> _pagingController =
PagingController(firstPageKey: 0);
final http.Client _client = http.Client(); final http.Client _client = http.Client();
@ -53,7 +54,8 @@ class _ChatScreenState extends State<ChatScreen> {
} }
Future<Call?> fetchCall() async { Future<Call?> fetchCall() async {
var uri = getRequestUri('messaging', '/api/channels/${widget.alias}/calls/ongoing'); var uri = getRequestUri(
'messaging', '/api/channels/${widget.alias}/calls/ongoing');
var res = await _client.get(uri); var res = await _client.get(uri);
if (res.statusCode == 200) { if (res.statusCode == 200) {
final result = jsonDecode(utf8.decode(res.bodyBytes)); final result = jsonDecode(utf8.decode(res.bodyBytes));
@ -82,8 +84,10 @@ class _ChatScreenState extends State<ChatScreen> {
var res = await auth.client!.get(uri); var res = await auth.client!.get(uri);
if (res.statusCode == 200) { if (res.statusCode == 200) {
final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); final result =
final items = result.data?.map((x) => Message.fromJson(x)).toList() ?? List.empty(); PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
final items =
result.data?.map((x) => Message.fromJson(x)).toList() ?? List.empty();
final isLastPage = (result.count - pageKey) < take; final isLastPage = (result.count - pageKey) < take;
if (isLastPage || result.data == null) { if (isLastPage || result.data == null) {
_pagingController.appendLastPage(items); _pagingController.appendLastPage(items);
@ -97,7 +101,7 @@ class _ChatScreenState extends State<ChatScreen> {
} }
bool getMessageMergeable(Message? a, Message? b) { bool getMessageMergeable(Message? a, Message? b) {
if (a?.replyTo != null || b?.replyTo != null) return false; if (a?.replyTo != null) return false;
if (a == null || b == null) return false; if (a == null || b == null) return false;
if (a.senderId != b.senderId) return false; if (a.senderId != b.senderId) return false;
return a.createdAt.difference(b.createdAt).inMinutes <= 5; return a.createdAt.difference(b.createdAt).inMinutes <= 5;
@ -111,13 +115,16 @@ class _ChatScreenState extends State<ChatScreen> {
void updateMessage(Message item) { void updateMessage(Message item) {
setState(() { setState(() {
_pagingController.itemList = _pagingController.itemList?.map((x) => x.id == item.id ? item : x).toList(); _pagingController.itemList = _pagingController.itemList
?.map((x) => x.id == item.id ? item : x)
.toList();
}); });
} }
void deleteMessage(Message item) { void deleteMessage(Message item) {
setState(() { setState(() {
_pagingController.itemList = _pagingController.itemList?.where((x) => x.id != item.id).toList(); _pagingController.itemList =
_pagingController.itemList?.where((x) => x.id != item.id).toList();
}); });
} }
@ -147,7 +154,8 @@ class _ChatScreenState extends State<ChatScreen> {
fetchCall(); fetchCall();
}); });
_pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context)); _pagingController
.addPageRequestListener((pageKey) => fetchMessages(pageKey, context));
super.initState(); super.initState();
} }
@ -157,10 +165,12 @@ class _ChatScreenState extends State<ChatScreen> {
Widget chatHistoryBuilder(context, item, index) { Widget chatHistoryBuilder(context, item, index) {
bool isMerged = false, hasMerged = false; bool isMerged = false, hasMerged = false;
if (index > 0) { if (index > 0) {
hasMerged = getMessageMergeable(_pagingController.itemList?[index - 1], item); hasMerged =
getMessageMergeable(_pagingController.itemList?[index - 1], item);
} }
if (index + 1 < (_pagingController.itemList?.length ?? 0)) { if (index + 1 < (_pagingController.itemList?.length ?? 0)) {
isMerged = getMessageMergeable(item, _pagingController.itemList?[index + 1]); isMerged =
getMessageMergeable(item, _pagingController.itemList?[index + 1]);
} }
return InkWell( return InkWell(
child: Container( child: Container(
@ -183,7 +193,8 @@ class _ChatScreenState extends State<ChatScreen> {
final callBanner = MaterialBanner( final callBanner = MaterialBanner(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20),
leading: const Icon(Icons.call_received), leading: const Icon(Icons.call_received),
backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9), backgroundColor:
Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9),
dividerColor: const Color.fromARGB(1, 0, 0, 0), dividerColor: const Color.fromARGB(1, 0, 0, 0),
content: Text(AppLocalizations.of(context)!.chatCallOngoing), content: Text(AppLocalizations.of(context)!.chatCallOngoing),
actions: [ actions: [
@ -205,8 +216,12 @@ class _ChatScreenState extends State<ChatScreen> {
title: _channelMeta?.name ?? "Loading...", title: _channelMeta?.name ?? "Loading...",
appBarActions: _channelMeta != null appBarActions: _channelMeta != null
? [ ? [
ChannelCallAction(call: _ongoingCall, channel: _channelMeta!, onUpdate: () => fetchMetadata()), ChannelCallAction(
ChannelManageAction(channel: _channelMeta!, onUpdate: () => fetchMetadata()), call: _ongoingCall,
channel: _channelMeta!,
onUpdate: () => fetchMetadata()),
ChannelManageAction(
channel: _channelMeta!, onUpdate: () => fetchMetadata()),
] ]
: [], : [],
child: FutureBuilder( child: FutureBuilder(
@ -243,7 +258,9 @@ class _ChatScreenState extends State<ChatScreen> {
), ),
], ],
), ),
_ongoingCall != null ? callBanner.animate().slideY() : Container(), _ongoingCall != null
? callBanner.animate().slideY()
: Container(),
], ],
), ),
onInsertMessage: (message) => addMessage(message), onInsertMessage: (message) => addMessage(message),

View File

@ -105,7 +105,7 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> {
'channel': element.alias, 'channel': element.alias,
}, },
); );
switch(result) { switch (result) {
case 'refresh': case 'refresh':
fetchChannels(); fetchChannels();
} }

View File

@ -53,7 +53,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
title: Text(AppLocalizations.of(context)!.settings), title: Text(AppLocalizations.of(context)!.settings),
onTap: () async { onTap: () async {
router.pushNamed('chat.channel.editor', extra: widget.channel).then((did) { router
.pushNamed('chat.channel.editor', extra: widget.channel)
.then((did) {
if (did == true) { if (did == true) {
if (router.canPop()) router.pop('refresh'); if (router.canPop()) router.pop('refresh');
} }
@ -79,10 +81,14 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(
Text(widget.channel.name, style: Theme.of(context).textTheme.bodyLarge), crossAxisAlignment: CrossAxisAlignment.start,
Text(widget.channel.description, style: Theme.of(context).textTheme.bodySmall), children: [
]), Text(widget.channel.name,
style: Theme.of(context).textTheme.bodyLarge),
Text(widget.channel.description,
style: Theme.of(context).textTheme.bodySmall),
]),
) )
], ],
), ),
@ -110,8 +116,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
...(_isOwned ? authorizedItems : List.empty()), ...(_isOwned ? authorizedItems : List.empty()),
const Divider(thickness: 0.3), const Divider(thickness: 0.3),
ListTile( ListTile(
leading: _isOwned ? const Icon(Icons.delete) : const Icon(Icons.exit_to_app), leading: _isOwned
title: Text(_isOwned ? AppLocalizations.of(context)!.delete : AppLocalizations.of(context)!.exit), ? const Icon(Icons.delete)
: const Icon(Icons.exit_to_app),
title: Text(_isOwned
? AppLocalizations.of(context)!.delete
: AppLocalizations.of(context)!.exit),
onTap: () => promptLeaveChannel(), onTap: () => promptLeaveChannel(),
), ),
], ],

View File

@ -22,7 +22,8 @@ class ExploreScreen extends StatefulWidget {
} }
class _ExploreScreenState extends State<ExploreScreen> { class _ExploreScreenState extends State<ExploreScreen> {
final PagingController<int, Post> _pagingController = PagingController(firstPageKey: 0); final PagingController<int, Post> _pagingController =
PagingController(firstPageKey: 0);
final http.Client _client = http.Client(); final http.Client _client = http.Client();
@ -30,12 +31,15 @@ class _ExploreScreenState extends State<ExploreScreen> {
final offset = pageKey; final offset = pageKey;
const take = 5; const take = 5;
var uri = getRequestUri('interactive', '/api/feed?take=$take&offset=$offset'); var uri =
getRequestUri('interactive', '/api/feed?take=$take&offset=$offset');
var res = await _client.get(uri); var res = await _client.get(uri);
if (res.statusCode == 200) { if (res.statusCode == 200) {
final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); final result =
final items = result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty(); PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
final items =
result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty();
final isLastPage = (result.count - pageKey) < take; final isLastPage = (result.count - pageKey) < take;
if (isLastPage || result.data == null) { if (isLastPage || result.data == null) {
_pagingController.appendLastPage(items); _pagingController.appendLastPage(items);

View File

@ -41,7 +41,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
child: ListTile( child: ListTile(
leading: const Icon(Icons.check), leading: const Icon(Icons.check),
title: Text(AppLocalizations.of(context)!.notifyDone), title: Text(AppLocalizations.of(context)!.notifyDone),
subtitle: Text(AppLocalizations.of(context)!.notifyDoneCaption), subtitle: Text(
AppLocalizations.of(context)!.notifyDoneCaption),
), ),
), ),
) )
@ -78,7 +79,8 @@ class NotificationItem extends StatelessWidget {
final model.Notification item; final model.Notification item;
final void Function()? onDismiss; final void Function()? onDismiss;
const NotificationItem({super.key, required this.index, required this.item, this.onDismiss}); const NotificationItem(
{super.key, required this.index, required this.item, this.onDismiss});
bool hasLinks() => item.links != null && item.links!.isNotEmpty; bool hasLinks() => item.links != null && item.links!.isNotEmpty;
@ -92,7 +94,8 @@ class NotificationItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 34, bottom: 12), padding: const EdgeInsets.only(
left: 16, right: 16, top: 34, bottom: 12),
child: Text( child: Text(
"Links", "Links",
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
@ -121,7 +124,8 @@ class NotificationItem extends StatelessWidget {
); );
} }
Future<void> markAsRead(model.Notification element, BuildContext context) async { Future<void> markAsRead(
model.Notification element, BuildContext context) async {
if (element.isRealtime) return; if (element.isRealtime) return;
final auth = context.read<AuthProvider>(); final auth = context.read<AuthProvider>();

View File

@ -129,69 +129,68 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
child: Text(AppLocalizations.of(context)!.postVerb.toUpperCase()), child: Text(AppLocalizations.of(context)!.postVerb.toUpperCase()),
), ),
], ],
child: Center( child: Column(
child: Container( children: [
constraints: const BoxConstraints(maxWidth: 640), _isSubmitting
child: Column( ? const LinearProgressIndicator().animate().scaleX()
children: [ : Container(),
_isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), FutureBuilder(
FutureBuilder( future: auth.getProfiles(),
future: auth.getProfiles(), builder: (context, snapshot) {
builder: (context, snapshot) { if (snapshot.hasData) {
if (snapshot.hasData) { var userinfo = snapshot.data;
var userinfo = snapshot.data; return ListTile(
return ListTile( title: Text(userinfo["nick"]),
title: Text(userinfo["nick"]), subtitle: Text(
subtitle: Text( AppLocalizations.of(context)!.postIdentityNotify,
AppLocalizations.of(context)!.postIdentityNotify,
),
leading: AccountAvatar(
source: userinfo["picture"],
direct: true,
),
);
} else {
return Container();
}
},
),
const Divider(thickness: 0.3),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
maxLines: null,
autofocus: true,
autocorrect: true,
keyboardType: TextInputType.multiline,
controller: _textController,
decoration: InputDecoration.collapsed(
hintText: AppLocalizations.of(context)!.postContentPlaceholder,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
), leading: AccountAvatar(
), source: userinfo["picture"],
widget.editing != null ? editingBanner : Container(), direct: true,
Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(width: 0.3, color: Color(0xffdedede)),
), ),
), );
child: Row( } else {
children: [ return Container();
TextButton( }
style: TextButton.styleFrom(shape: const CircleBorder()), },
child: const Icon(Icons.camera_alt),
onPressed: () => viewAttachments(context),
)
],
),
),
],
), ),
), const Divider(thickness: 0.3),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
maxLines: null,
autofocus: true,
autocorrect: true,
keyboardType: TextInputType.multiline,
controller: _textController,
decoration: InputDecoration.collapsed(
hintText:
AppLocalizations.of(context)!.postContentPlaceholder,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
),
widget.editing != null ? editingBanner : Container(),
Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(width: 0.3, color: Color(0xffdedede)),
),
),
child: Row(
children: [
TextButton(
style: TextButton.styleFrom(shape: const CircleBorder()),
child: const Icon(Icons.camera_alt),
onPressed: () => viewAttachments(context),
)
],
),
),
],
), ),
); );
} }

View File

@ -119,69 +119,68 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
child: Text(AppLocalizations.of(context)!.postVerb.toUpperCase()), child: Text(AppLocalizations.of(context)!.postVerb.toUpperCase()),
), ),
], ],
child: Center( child: Column(
child: Container( children: [
constraints: const BoxConstraints(maxWidth: 640), _isSubmitting
child: Column( ? const LinearProgressIndicator().animate().scaleX()
children: [ : Container(),
_isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), FutureBuilder(
FutureBuilder( future: auth.getProfiles(),
future: auth.getProfiles(), builder: (context, snapshot) {
builder: (context, snapshot) { if (snapshot.hasData) {
if (snapshot.hasData) { var userinfo = snapshot.data;
var userinfo = snapshot.data; return ListTile(
return ListTile( title: Text(userinfo["nick"]),
title: Text(userinfo["nick"]), subtitle: Text(
subtitle: Text( AppLocalizations.of(context)!.postIdentityNotify,
AppLocalizations.of(context)!.postIdentityNotify,
),
leading: AccountAvatar(
source: userinfo["picture"],
direct: true,
),
);
} else {
return Container();
}
},
),
const Divider(thickness: 0.3),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
maxLines: null,
autofocus: true,
autocorrect: true,
keyboardType: TextInputType.multiline,
controller: _textController,
decoration: InputDecoration.collapsed(
hintText: AppLocalizations.of(context)!.postContentPlaceholder,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
), leading: AccountAvatar(
), source: userinfo["picture"],
widget.editing != null ? editingBanner : Container(), direct: true,
Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(width: 0.3, color: Color(0xffdedede)),
), ),
), );
child: Row( } else {
children: [ return Container();
TextButton( }
style: TextButton.styleFrom(shape: const CircleBorder()), },
child: const Icon(Icons.camera_alt),
onPressed: () => viewAttachments(context),
)
],
),
),
],
), ),
), const Divider(thickness: 0.3),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
maxLines: null,
autofocus: true,
autocorrect: true,
keyboardType: TextInputType.multiline,
controller: _textController,
decoration: InputDecoration.collapsed(
hintText:
AppLocalizations.of(context)!.postContentPlaceholder,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
),
widget.editing != null ? editingBanner : Container(),
Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(width: 0.3, color: Color(0xffdedede)),
),
),
child: Row(
children: [
TextButton(
style: TextButton.styleFrom(shape: const CircleBorder()),
child: const Icon(Icons.camera_alt),
onPressed: () => viewAttachments(context),
)
],
),
),
],
), ),
); );
} }

View File

@ -24,10 +24,12 @@ class PostScreen extends StatefulWidget {
class _PostScreenState extends State<PostScreen> { class _PostScreenState extends State<PostScreen> {
final _client = http.Client(); final _client = http.Client();
final PagingController<int, Post> _commentPagingController = PagingController(firstPageKey: 0); final PagingController<int, Post> _commentPagingController =
PagingController(firstPageKey: 0);
Future<Post?> fetchPost(BuildContext context) async { Future<Post?> fetchPost(BuildContext context) async {
final uri = getRequestUri('interactive', '/api/p/${widget.dataset}/${widget.alias}'); final uri = getRequestUri(
'interactive', '/api/p/${widget.dataset}/${widget.alias}');
final res = await _client.get(uri); final res = await _client.get(uri);
if (res.statusCode != 200) { if (res.statusCode != 200) {
final err = utf8.decode(res.bodyBytes); final err = utf8.decode(res.bodyBytes);

View File

@ -1,108 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/utils/service_url.dart';
import 'package:solian/widgets/exts.dart';
import 'package:solian/widgets/indent_wrapper.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SignInScreen extends StatelessWidget {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
SignInScreen({super.key});
@override
Widget build(BuildContext context) {
final auth = context.read<AuthProvider>();
return IndentWrapper(
title: AppLocalizations.of(context)!.signIn,
hideDrawer: true,
child: Center(
child: Container(
width: MediaQuery.of(context).size.width * 0.6,
constraints: const BoxConstraints(maxWidth: 360),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
autocorrect: false,
enableSuggestions: false,
controller: _usernameController,
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: AppLocalizations.of(context)!.username,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 12),
TextField(
obscureText: true,
autocorrect: false,
enableSuggestions: false,
controller: _passwordController,
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: AppLocalizations.of(context)!.password,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextButton(
child: Text(AppLocalizations.of(context)!.next),
onPressed: () {
final username = _usernameController.value.text;
final password = _passwordController.value.text;
if (username.isEmpty || password.isEmpty) return;
auth.signin(context, username, password).then((_) {
router.pop(true);
}).catchError((e) {
List<String> messages = e.toString().split('\n');
if (messages.last.contains("risk")) {
final ticketId = RegExp(r"ticketId=(\d+)").firstMatch(messages.last);
if (ticketId == null) {
context
.showErrorDialog("requested to multi-factor authenticate, but the ticket id was not found");
}
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(AppLocalizations.of(context)!.riskDetection),
content: Text(AppLocalizations.of(context)!.signInRiskDetected),
actions: [
TextButton(
child: Text(AppLocalizations.of(context)!.next),
onPressed: () {
launchUrlString(
getRequestUri('passport', '/mfa?ticket=${ticketId!.group(1)}').toString(),
);
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
},
)
],
);
},
);
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(messages.last),
));
}
});
},
)
],
),
),
),
);
}
}

View File

@ -4,4 +4,4 @@ import 'package:media_kit/media_kit.dart';
void initVideo() { void initVideo() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
} }

View File

@ -37,7 +37,8 @@ class _FriendPickerState extends State<FriendPicker> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: const EdgeInsets.only(left: 16, right: 16, top: 32, bottom: 12), padding:
const EdgeInsets.only(left: 16, right: 16, top: 32, bottom: 12),
child: Text( child: Text(
AppLocalizations.of(context)!.friend, AppLocalizations.of(context)!.friend,
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,

View File

@ -41,7 +41,8 @@ class _ControlsWidgetState extends State<ControlsWidget> {
void initState() { void initState() {
super.initState(); super.initState();
participant.addListener(onChange); participant.addListener(onChange);
_subscription = Hardware.instance.onDeviceChange.stream.listen((List<MediaDevice> devices) { _subscription = Hardware.instance.onDeviceChange.stream
.listen((List<MediaDevice> devices) {
revertDevices(devices); revertDevices(devices);
}); });
Hardware.instance.enumerateDevices().then(revertDevices); Hardware.instance.enumerateDevices().then(revertDevices);
@ -161,18 +162,23 @@ class _ControlsWidgetState extends State<ControlsWidget> {
if (!isRetry) { if (!isRetry) {
const androidConfig = FlutterBackgroundAndroidConfig( const androidConfig = FlutterBackgroundAndroidConfig(
notificationTitle: 'Screen Sharing', notificationTitle: 'Screen Sharing',
notificationText: 'A Solar Messager\'s Call is sharing your screen', notificationText:
'A Solar Messager\'s Call is sharing your screen',
notificationImportance: AndroidNotificationImportance.Default, notificationImportance: AndroidNotificationImportance.Default,
notificationIcon: AndroidResource(name: 'launcher_icon', defType: 'mipmap'), notificationIcon:
AndroidResource(name: 'launcher_icon', defType: 'mipmap'),
); );
hasPermissions = await FlutterBackground.initialize(androidConfig: androidConfig); hasPermissions = await FlutterBackground.initialize(
androidConfig: androidConfig);
} }
if (hasPermissions && !FlutterBackground.isBackgroundExecutionEnabled) { if (hasPermissions &&
!FlutterBackground.isBackgroundExecutionEnabled) {
await FlutterBackground.enableBackgroundExecution(); await FlutterBackground.enableBackgroundExecution();
} }
} catch (e) { } catch (e) {
if (!isRetry) { if (!isRetry) {
return await Future<void>.delayed(const Duration(seconds: 1), () => requestBackgroundPermission(true)); return await Future<void>.delayed(const Duration(seconds: 1),
() => requestBackgroundPermission(true));
} }
} }
} }
@ -223,7 +229,8 @@ class _ControlsWidgetState extends State<ControlsWidget> {
runSpacing: 5, runSpacing: 5,
children: [ children: [
IconButton( IconButton(
icon: Transform.flip(flipX: true, child: const Icon(Icons.exit_to_app)), icon: Transform.flip(
flipX: true, child: const Icon(Icons.exit_to_app)),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
onPressed: disconnect, onPressed: disconnect,
), ),
@ -253,7 +260,8 @@ class _ControlsWidgetState extends State<ControlsWidget> {
return PopupMenuItem<MediaDevice>( return PopupMenuItem<MediaDevice>(
value: device, value: device,
child: ListTile( child: ListTile(
leading: (device.deviceId == widget.room.selectedAudioInputDeviceId) leading: (device.deviceId ==
widget.room.selectedAudioInputDeviceId)
? const Icon(Icons.check_box_outlined) ? const Icon(Icons.check_box_outlined)
: const Icon(Icons.check_box_outline_blank), : const Icon(Icons.check_box_outline_blank),
title: Text(device.label), title: Text(device.label),
@ -281,7 +289,8 @@ class _ControlsWidgetState extends State<ControlsWidget> {
onTap: disableVideo, onTap: disableVideo,
child: ListTile( child: ListTile(
leading: const Icon(Icons.videocam_off), leading: const Icon(Icons.videocam_off),
title: Text(AppLocalizations.of(context)!.chatCallVideoOff), title:
Text(AppLocalizations.of(context)!.chatCallVideoOff),
), ),
), ),
if (_videoInputs != null) if (_videoInputs != null)
@ -289,7 +298,8 @@ class _ControlsWidgetState extends State<ControlsWidget> {
return PopupMenuItem<MediaDevice>( return PopupMenuItem<MediaDevice>(
value: device, value: device,
child: ListTile( child: ListTile(
leading: (device.deviceId == widget.room.selectedVideoInputDeviceId) leading: (device.deviceId ==
widget.room.selectedVideoInputDeviceId)
? const Icon(Icons.check_box_outlined) ? const Icon(Icons.check_box_outlined)
: const Icon(Icons.check_box_outline_blank), : const Icon(Icons.check_box_outline_blank),
title: Text(device.label), title: Text(device.label),
@ -308,7 +318,9 @@ class _ControlsWidgetState extends State<ControlsWidget> {
tooltip: AppLocalizations.of(context)!.chatCallVideoOn, tooltip: AppLocalizations.of(context)!.chatCallVideoOn,
), ),
IconButton( IconButton(
icon: Icon(position == CameraPosition.back ? Icons.video_camera_back : Icons.video_camera_front), icon: Icon(position == CameraPosition.back
? Icons.video_camera_back
: Icons.video_camera_front),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
onPressed: () => toggleCamera(), onPressed: () => toggleCamera(),
tooltip: AppLocalizations.of(context)!.chatCallVideoFlip, tooltip: AppLocalizations.of(context)!.chatCallVideoFlip,
@ -330,7 +342,8 @@ class _ControlsWidgetState extends State<ControlsWidget> {
return PopupMenuItem<MediaDevice>( return PopupMenuItem<MediaDevice>(
value: device, value: device,
child: ListTile( child: ListTile(
leading: (device.deviceId == widget.room.selectedAudioOutputDeviceId) leading: (device.deviceId ==
widget.room.selectedAudioOutputDeviceId)
? const Icon(Icons.check_box_outlined) ? const Icon(Icons.check_box_outlined)
: const Icon(Icons.check_box_outline_blank), : const Icon(Icons.check_box_outline_blank),
title: Text(device.label), title: Text(device.label),
@ -343,9 +356,12 @@ class _ControlsWidgetState extends State<ControlsWidget> {
), ),
if (!kIsWeb && lkPlatformIs(PlatformType.iOS)) if (!kIsWeb && lkPlatformIs(PlatformType.iOS))
IconButton( IconButton(
onPressed: Hardware.instance.canSwitchSpeakerphone ? setSpeakerphoneOn : null, onPressed: Hardware.instance.canSwitchSpeakerphone
? setSpeakerphoneOn
: null,
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
icon: Icon(_speakerphoneOn ? Icons.speaker_phone : Icons.phone_android), icon: Icon(
_speakerphoneOn ? Icons.speaker_phone : Icons.phone_android),
tooltip: AppLocalizations.of(context)!.chatCallChangeSpeaker, tooltip: AppLocalizations.of(context)!.chatCallChangeSpeaker,
), ),
if (participant.isScreenShareEnabled()) if (participant.isScreenShareEnabled())

View File

@ -20,7 +20,8 @@ class NoContentWidget extends StatefulWidget {
State<NoContentWidget> createState() => _NoContentWidgetState(); State<NoContentWidget> createState() => _NoContentWidgetState();
} }
class _NoContentWidgetState extends State<NoContentWidget> with SingleTickerProviderStateMixin { class _NoContentWidgetState extends State<NoContentWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController; late final AnimationController _animationController;
@override @override
@ -35,7 +36,9 @@ class _NoContentWidgetState extends State<NoContentWidget> with SingleTickerProv
if (widget.isSpeaking) { if (widget.isSpeaking) {
_animationController.repeat(reverse: true); _animationController.repeat(reverse: true);
} else { } else {
_animationController.animateTo(0, duration: 300.ms).then((_) => _animationController.reset()); _animationController
.animateTo(0, duration: 300.ms)
.then((_) => _animationController.reset());
} }
} }
@ -63,7 +66,9 @@ class _NoContentWidgetState extends State<NoContentWidget> with SingleTickerProv
builder: (context, value, child) => Container( builder: (context, value, child) => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(radius + 8)), borderRadius: BorderRadius.all(Radius.circular(radius + 8)),
border: value > 0 ? Border.all(color: Colors.green, width: value) : null, border: value > 0
? Border.all(color: Colors.green, width: value)
: null,
), ),
child: child, child: child,
), ),

View File

@ -95,7 +95,8 @@ class RemoteParticipantWidget extends ParticipantWidget {
State<StatefulWidget> createState() => _RemoteParticipantWidgetState(); State<StatefulWidget> createState() => _RemoteParticipantWidgetState();
} }
abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends State<T> { abstract class _ParticipantWidgetState<T extends ParticipantWidget>
extends State<T> {
VideoTrack? get _activeVideoTrack; VideoTrack? get _activeVideoTrack;
TrackPublication? get _firstAudioPublication; TrackPublication? get _firstAudioPublication;
@ -126,7 +127,8 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
void onParticipantChanged() { void onParticipantChanged() {
setState(() { setState(() {
if (widget.participant.metadata != null) { if (widget.participant.metadata != null) {
_userinfoMetadata = Account.fromJson(jsonDecode(widget.participant.metadata!)); _userinfoMetadata =
Account.fromJson(jsonDecode(widget.participant.metadata!));
} }
}); });
} }
@ -158,8 +160,11 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ParticipantInfoWidget( ParticipantInfoWidget(
title: widget.participant.name.isNotEmpty ? widget.participant.name : widget.participant.identity, title: widget.participant.name.isNotEmpty
audioAvailable: _firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true, ? widget.participant.name
: widget.participant.identity,
audioAvailable: _firstAudioPublication?.muted == false &&
_firstAudioPublication?.subscribed == true,
connectionQuality: widget.participant.connectionQuality, connectionQuality: widget.participant.connectionQuality,
isScreenShare: widget.isScreenShare, isScreenShare: widget.isScreenShare,
), ),
@ -171,7 +176,8 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
} }
} }
class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticipantWidget> { class _LocalParticipantWidgetState
extends _ParticipantWidgetState<LocalParticipantWidget> {
@override @override
LocalTrackPublication<LocalAudioTrack>? get _firstAudioPublication => LocalTrackPublication<LocalAudioTrack>? get _firstAudioPublication =>
widget.participant.audioTrackPublications.firstOrNull; widget.participant.audioTrackPublications.firstOrNull;
@ -180,7 +186,8 @@ class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticip
VideoTrack? get _activeVideoTrack => widget.videoTrack; VideoTrack? get _activeVideoTrack => widget.videoTrack;
} }
class _RemoteParticipantWidgetState extends _ParticipantWidgetState<RemoteParticipantWidget> { class _RemoteParticipantWidgetState
extends _ParticipantWidgetState<RemoteParticipantWidget> {
@override @override
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication => RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
widget.participant.audioTrackPublications.firstOrNull; widget.participant.audioTrackPublications.firstOrNull;

View File

@ -55,7 +55,9 @@ class ParticipantInfoWidget extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.only(left: 5), padding: const EdgeInsets.only(left: 5),
child: Icon( child: Icon(
connectionQuality == ConnectionQuality.poor ? Icons.wifi_off_outlined : Icons.wifi, connectionQuality == ConnectionQuality.poor
? Icons.wifi_off_outlined
: Icons.wifi,
color: { color: {
ConnectionQuality.excellent: Colors.green, ConnectionQuality.excellent: Colors.green,
ConnectionQuality.good: Colors.orange, ConnectionQuality.good: Colors.orange,

View File

@ -22,7 +22,9 @@ class ParticipantMenu extends StatefulWidget {
class _ParticipantMenuState extends State<ParticipantMenu> { class _ParticipantMenuState extends State<ParticipantMenu> {
RemoteTrackPublication<RemoteVideoTrack>? get _videoPublication => RemoteTrackPublication<RemoteVideoTrack>? get _videoPublication =>
widget.participant.videoTrackPublications.where((element) => element.sid == widget.videoTrack?.sid).firstOrNull; widget.participant.videoTrackPublications
.where((element) => element.sid == widget.videoTrack?.sid)
.firstOrNull;
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication => RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
widget.participant.audioTrackPublications.firstOrNull; widget.participant.audioTrackPublications.firstOrNull;
@ -39,7 +41,8 @@ class _ParticipantMenuState extends State<ParticipantMenu> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12), padding:
const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
@ -59,9 +62,14 @@ class _ParticipantMenuState extends State<ParticipantMenu> {
leading: Icon( leading: Icon(
Icons.volume_up, Icons.volume_up,
color: { color: {
TrackSubscriptionState.notAllowed: Theme.of(context).colorScheme.error, TrackSubscriptionState.notAllowed:
TrackSubscriptionState.unsubscribed: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), Theme.of(context).colorScheme.error,
TrackSubscriptionState.subscribed: Theme.of(context).colorScheme.primary, TrackSubscriptionState.unsubscribed: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.6),
TrackSubscriptionState.subscribed:
Theme.of(context).colorScheme.primary,
}[_firstAudioPublication!.subscriptionState], }[_firstAudioPublication!.subscriptionState],
), ),
title: Text( title: Text(
@ -83,9 +91,14 @@ class _ParticipantMenuState extends State<ParticipantMenu> {
leading: Icon( leading: Icon(
widget.isScreenShare ? Icons.monitor : Icons.videocam, widget.isScreenShare ? Icons.monitor : Icons.videocam,
color: { color: {
TrackSubscriptionState.notAllowed: Theme.of(context).colorScheme.error, TrackSubscriptionState.notAllowed:
TrackSubscriptionState.unsubscribed: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), Theme.of(context).colorScheme.error,
TrackSubscriptionState.subscribed: Theme.of(context).colorScheme.primary, TrackSubscriptionState.unsubscribed: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.6),
TrackSubscriptionState.subscribed:
Theme.of(context).colorScheme.primary,
}[_videoPublication!.subscriptionState], }[_videoPublication!.subscriptionState],
), ),
title: Text( title: Text(
@ -107,7 +120,9 @@ class _ParticipantMenuState extends State<ParticipantMenu> {
...[30, 15, 8].map( ...[30, 15, 8].map(
(x) => ListTile( (x) => ListTile(
leading: Icon( leading: Icon(
_videoPublication?.fps == x ? Icons.check_box_outlined : Icons.check_box_outline_blank, _videoPublication?.fps == x
? Icons.check_box_outlined
: Icons.check_box_outline_blank,
), ),
title: Text('Set preferred frame-per-second to $x'), title: Text('Set preferred frame-per-second to $x'),
onTap: () { onTap: () {
@ -125,7 +140,9 @@ class _ParticipantMenuState extends State<ParticipantMenu> {
].map( ].map(
(x) => ListTile( (x) => ListTile(
leading: Icon( leading: Icon(
_videoPublication?.videoQuality == x.$2 ? Icons.check_box_outlined : Icons.check_box_outline_blank, _videoPublication?.videoQuality == x.$2
? Icons.check_box_outlined
: Icons.check_box_outline_blank,
), ),
title: Text('Set preferred quality to ${x.$1}'), title: Text('Set preferred quality to ${x.$1}'),
onTap: () { onTap: () {

View File

@ -28,11 +28,14 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
stats['layer-$key'] = stats['layer-$key'] =
'${value.frameWidth ?? 0}x${value.frameHeight ?? 0} ${value.framesPerSecond?.toDouble() ?? 0} fps, ${event.bitrateForLayers[key] ?? 0} kbps'; '${value.frameWidth ?? 0}x${value.frameHeight ?? 0} ${value.framesPerSecond?.toDouble() ?? 0} fps, ${event.bitrateForLayers[key] ?? 0} kbps';
}); });
var firstStats = event.stats['f'] ?? event.stats['h'] ?? event.stats['q']; var firstStats =
event.stats['f'] ?? event.stats['h'] ?? event.stats['q'];
if (firstStats != null) { if (firstStats != null) {
stats['encoder'] = firstStats.encoderImplementation ?? ''; stats['encoder'] = firstStats.encoderImplementation ?? '';
stats['video codec'] = '${firstStats.mimeType}, ${firstStats.clockRate}hz, pt: ${firstStats.payloadType}'; stats['video codec'] =
stats['qualityLimitationReason'] = firstStats.qualityLimitationReason ?? ''; '${firstStats.mimeType}, ${firstStats.clockRate}hz, pt: ${firstStats.payloadType}';
stats['qualityLimitationReason'] =
firstStats.qualityLimitationReason ?? '';
} }
}); });
}); });
@ -41,7 +44,8 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
listener.on<VideoReceiverStatsEvent>((event) { listener.on<VideoReceiverStatsEvent>((event) {
setState(() { setState(() {
stats['video rx'] = '${event.currentBitrate.toInt()} kpbs'; stats['video rx'] = '${event.currentBitrate.toInt()} kpbs';
stats['video codec'] = '${event.stats.mimeType}, ${event.stats.clockRate}hz, pt: ${event.stats.payloadType}'; stats['video codec'] =
'${event.stats.mimeType}, ${event.stats.clockRate}hz, pt: ${event.stats.payloadType}';
stats['video size'] = stats['video size'] =
'${event.stats.frameWidth}x${event.stats.frameHeight} ${event.stats.framesPerSecond?.toDouble()}fps'; '${event.stats.frameWidth}x${event.stats.frameHeight} ${event.stats.framesPerSecond?.toDouble()}fps';
stats['video jitter'] = '${event.stats.jitter} s'; stats['video jitter'] = '${event.stats.jitter} s';
@ -70,7 +74,8 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
stats['audio codec'] = stats['audio codec'] =
'${event.stats.mimeType}, ${event.stats.clockRate}hz, ${event.stats.channels}ch, pt: ${event.stats.payloadType}'; '${event.stats.mimeType}, ${event.stats.clockRate}hz, ${event.stats.channels}ch, pt: ${event.stats.payloadType}';
stats['audio jitter'] = '${event.stats.jitter} s'; stats['audio jitter'] = '${event.stats.jitter} s';
stats['audio concealed samples'] = '${event.stats.concealedSamples} / ${event.stats.concealmentEvents}'; stats['audio concealed samples'] =
'${event.stats.concealedSamples} / ${event.stats.concealmentEvents}';
stats['audio packets lost'] = '${event.stats.packetsLost}'; stats['audio packets lost'] = '${event.stats.packetsLost}';
stats['audio packets received'] = '${event.stats.packetsReceived}'; stats['audio packets received'] = '${event.stats.packetsReceived}';
}); });
@ -83,7 +88,10 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
element.dispose(); element.dispose();
} }
listeners.clear(); listeners.clear();
for (var track in [...widget.participant.videoTrackPublications, ...widget.participant.audioTrackPublications]) { for (var track in [
...widget.participant.videoTrackPublications,
...widget.participant.audioTrackPublications
]) {
if (track.track != null) { if (track.track != null) {
_setUpListener(track.track!); _setUpListener(track.track!);
} }
@ -117,7 +125,8 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
horizontal: 8, horizontal: 8,
), ),
child: Column( child: Column(
children: stats.entries.map((e) => Text('${e.key}: ${e.value}')).toList(), children:
stats.entries.map((e) => Text('${e.key}: ${e.value}')).toList(),
), ),
); );
} }

View File

@ -14,7 +14,8 @@ class ChannelCallAction extends StatefulWidget {
final Channel channel; final Channel channel;
final Function onUpdate; final Function onUpdate;
const ChannelCallAction({super.key, this.call, required this.channel, required this.onUpdate}); const ChannelCallAction(
{super.key, this.call, required this.channel, required this.onUpdate});
@override @override
State<ChannelCallAction> createState() => _ChannelCallActionState(); State<ChannelCallAction> createState() => _ChannelCallActionState();
@ -32,7 +33,8 @@ class _ChannelCallActionState extends State<ChannelCallAction> {
return; return;
} }
var uri = getRequestUri('messaging', '/api/channels/${widget.channel.alias}/calls'); var uri = getRequestUri(
'messaging', '/api/channels/${widget.channel.alias}/calls');
var res = await auth.client!.post(uri); var res = await auth.client!.post(uri);
if (res.statusCode != 200) { if (res.statusCode != 200) {
@ -52,7 +54,8 @@ class _ChannelCallActionState extends State<ChannelCallAction> {
return; return;
} }
var uri = getRequestUri('messaging', '/api/channels/${widget.channel.alias}/calls/ongoing'); var uri = getRequestUri(
'messaging', '/api/channels/${widget.channel.alias}/calls/ongoing');
var res = await auth.client!.delete(uri); var res = await auth.client!.delete(uri);
if (res.statusCode != 200) { if (res.statusCode != 200) {
@ -75,7 +78,9 @@ class _ChannelCallActionState extends State<ChannelCallAction> {
endsCall(); endsCall();
} }
}, },
icon: widget.call == null ? const Icon(Icons.call) : const Icon(Icons.call_end), icon: widget.call == null
? const Icon(Icons.call)
: const Icon(Icons.call_end),
); );
} }
} }
@ -84,7 +89,8 @@ class ChannelManageAction extends StatelessWidget {
final Channel channel; final Channel channel;
final Function onUpdate; final Function onUpdate;
const ChannelManageAction({super.key, required this.channel, required this.onUpdate}); const ChannelManageAction(
{super.key, required this.channel, required this.onUpdate});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -12,7 +12,8 @@ class ChannelDeletion extends StatefulWidget {
final Channel channel; final Channel channel;
final bool isOwned; final bool isOwned;
const ChannelDeletion({super.key, required this.channel, required this.isOwned}); const ChannelDeletion(
{super.key, required this.channel, required this.isOwned});
@override @override
State<ChannelDeletion> createState() => _ChannelDeletionState(); State<ChannelDeletion> createState() => _ChannelDeletionState();

View File

@ -55,19 +55,23 @@ class _ChatMaintainerState extends State<ChatMaintainer> {
switch (result.method) { switch (result.method) {
case 'messages.new': case 'messages.new':
final payload = Message.fromJson(result.payload!); final payload = Message.fromJson(result.payload!);
if (payload.channelId == widget.channel.id) widget.onInsertMessage(payload); if (payload.channelId == widget.channel.id)
widget.onInsertMessage(payload);
break; break;
case 'messages.update': case 'messages.update':
final payload = Message.fromJson(result.payload!); final payload = Message.fromJson(result.payload!);
if (payload.channelId == widget.channel.id) widget.onUpdateMessage(payload); if (payload.channelId == widget.channel.id)
widget.onUpdateMessage(payload);
break; break;
case 'messages.burnt': case 'messages.burnt':
final payload = Message.fromJson(result.payload!); final payload = Message.fromJson(result.payload!);
if (payload.channelId == widget.channel.id) widget.onDeleteMessage(payload); if (payload.channelId == widget.channel.id)
widget.onDeleteMessage(payload);
break; break;
case 'calls.new': case 'calls.new':
final payload = Call.fromJson(result.payload!); final payload = Call.fromJson(result.payload!);
if (payload.channelId == widget.channel.id) widget.onCallStarted(payload); if (payload.channelId == widget.channel.id)
widget.onCallStarted(payload);
break; break;
case 'calls.end': case 'calls.end':
final payload = Call.fromJson(result.payload!); final payload = Call.fromJson(result.payload!);

View File

@ -79,7 +79,9 @@ class ChatMessageAction extends StatelessWidget {
return ListView( return ListView(
children: [ children: [
...(snapshot.data['id'] == item.sender.account.externalId ? authorizedItems : List.empty()), ...(snapshot.data['id'] == item.sender.account.externalId
? authorizedItems
: List.empty()),
ListTile( ListTile(
leading: const Icon(Icons.reply), leading: const Icon(Icons.reply),
title: Text(AppLocalizations.of(context)!.reply), title: Text(AppLocalizations.of(context)!.reply),

View File

@ -19,7 +19,8 @@ class ChatMessageDeletionDialog extends StatefulWidget {
}); });
@override @override
State<ChatMessageDeletionDialog> createState() => _ChatMessageDeletionDialogState(); State<ChatMessageDeletionDialog> createState() =>
_ChatMessageDeletionDialogState();
} }
class _ChatMessageDeletionDialogState extends State<ChatMessageDeletionDialog> { class _ChatMessageDeletionDialogState extends State<ChatMessageDeletionDialog> {
@ -29,7 +30,8 @@ class _ChatMessageDeletionDialogState extends State<ChatMessageDeletionDialog> {
final auth = context.read<AuthProvider>(); final auth = context.read<AuthProvider>();
if (!await auth.isAuthorized()) return; if (!await auth.isAuthorized()) return;
final uri = getRequestUri('messaging', '/api/channels/${widget.channel}/messages/${widget.item.id}'); final uri = getRequestUri('messaging',
'/api/channels/${widget.channel}/messages/${widget.item.id}');
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
final res = await auth.client!.delete(uri); final res = await auth.client!.delete(uri);

View File

@ -18,7 +18,12 @@ class ChatMessageEditor extends StatefulWidget {
final Message? replying; final Message? replying;
final Function? onReset; final Function? onReset;
const ChatMessageEditor({super.key, required this.channel, this.editing, this.replying, this.onReset}); const ChatMessageEditor(
{super.key,
required this.channel,
this.editing,
this.replying,
this.onReset});
@override @override
State<ChatMessageEditor> createState() => _ChatMessageEditorState(); State<ChatMessageEditor> createState() => _ChatMessageEditorState();
@ -51,7 +56,8 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
final uri = widget.editing == null final uri = widget.editing == null
? getRequestUri('messaging', '/api/channels/${widget.channel}/messages') ? getRequestUri('messaging', '/api/channels/${widget.channel}/messages')
: getRequestUri('messaging', '/api/channels/${widget.channel}/messages/${widget.editing!.id}'); : getRequestUri('messaging',
'/api/channels/${widget.channel}/messages/${widget.editing!.id}');
final req = Request(widget.editing == null ? "POST" : "PUT", uri); final req = Request(widget.editing == null ? "POST" : "PUT", uri);
req.headers['Content-Type'] = 'application/json'; req.headers['Content-Type'] = 'application/json';
@ -84,7 +90,8 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
setState(() { setState(() {
_prevEditingId = widget.editing!.id; _prevEditingId = widget.editing!.id;
_textController.text = widget.editing!.content; _textController.text = widget.editing!.content;
_attachments = widget.editing!.attachments ?? List.empty(growable: true); _attachments =
widget.editing!.attachments ?? List.empty(growable: true);
}); });
} }
} }
@ -147,11 +154,15 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
children: [ children: [
badge.Badge( badge.Badge(
showBadge: _attachments.isNotEmpty, showBadge: _attachments.isNotEmpty,
badgeContent: Text(_attachments.length.toString(), style: const TextStyle(color: Colors.white)), badgeContent: Text(_attachments.length.toString(),
style: const TextStyle(color: Colors.white)),
position: badge.BadgePosition.custom(top: -2, end: 8), position: badge.BadgePosition.custom(top: -2, end: 8),
child: TextButton( child: TextButton(
style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)), style: TextButton.styleFrom(
onPressed: !_isSubmitting ? () => viewAttachments(context) : null, shape: const CircleBorder(),
padding: const EdgeInsets.all(4)),
onPressed:
!_isSubmitting ? () => viewAttachments(context) : null,
child: const Icon(Icons.attach_file), child: const Icon(Icons.attach_file),
), ),
), ),
@ -163,14 +174,18 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
autocorrect: true, autocorrect: true,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
hintText: AppLocalizations.of(context)!.chatMessagePlaceholder, hintText:
AppLocalizations.of(context)!.chatMessagePlaceholder,
), ),
onSubmitted: (_) => sendMessage(context), onSubmitted: (_) => sendMessage(context),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
TextButton( TextButton(
style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)), style: TextButton.styleFrom(
shape: const CircleBorder(),
padding: const EdgeInsets.all(4)),
onPressed: !_isSubmitting ? () => sendMessage(context) : null, onPressed: !_isSubmitting ? () => sendMessage(context) : null,
child: const Icon(Icons.send), child: const Icon(Icons.send),
) )

View File

@ -8,7 +8,8 @@ extension SolianCommonExtensions on BuildContext {
if (message.trim().isEmpty) return ''; if (message.trim().isEmpty) return '';
return message return message
.split(' ') .split(' ')
.map((element) => "${element[0].toUpperCase()}${element.substring(1).toLowerCase()}") .map((element) =>
"${element[0].toUpperCase()}${element.substring(1).toLowerCase()}")
.join(" "); .join(" ");
} }

View File

@ -22,10 +22,12 @@ class IndentWrapper extends LayoutWrapper {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: hideDrawer ? IconButton( leading: hideDrawer
icon: const Icon(Icons.arrow_back), ? IconButton(
onPressed: () => router.pop(), icon: const Icon(Icons.arrow_back),
) : null, onPressed: () => router.pop(),
)
: null,
title: Text(title), title: Text(title),
actions: appBarActions, actions: appBarActions,
), ),

View File

@ -49,7 +49,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
); );
} }
Future<void> pickImageToUpload(BuildContext context, ImageSource source) async { Future<void> pickImageToUpload(
BuildContext context, ImageSource source) async {
final auth = context.read<AuthProvider>(); final auth = context.read<AuthProvider>();
if (!await auth.isAuthorized()) return; if (!await auth.isAuthorized()) return;
@ -74,7 +75,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
} }
} }
Future<void> pickVideoToUpload(BuildContext context, ImageSource source) async { Future<void> pickVideoToUpload(
BuildContext context, ImageSource source) async {
final auth = context.read<AuthProvider>(); final auth = context.read<AuthProvider>();
if (!await auth.isAuthorized()) return; if (!await auth.isAuthorized()) return;
@ -102,7 +104,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
Future<void> uploadAttachment(File file, String hashcode) async { Future<void> uploadAttachment(File file, String hashcode) async {
final auth = context.read<AuthProvider>(); final auth = context.read<AuthProvider>();
final req = MultipartRequest('POST', getRequestUri(widget.provider, '/api/attachments')); final req = MultipartRequest(
'POST', getRequestUri(widget.provider, '/api/attachments'));
req.files.add(await MultipartFile.fromPath('attachment', file.path)); req.files.add(await MultipartFile.fromPath('attachment', file.path));
req.fields['hashcode'] = hashcode; req.fields['hashcode'] = hashcode;
@ -118,10 +121,12 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
} }
} }
Future<void> disposeAttachment(BuildContext context, Attachment item, int index) async { Future<void> disposeAttachment(
BuildContext context, Attachment item, int index) async {
final auth = context.read<AuthProvider>(); final auth = context.read<AuthProvider>();
final req = MultipartRequest('DELETE', getRequestUri(widget.provider, '/api/attachments/${item.id}')); final req = MultipartRequest('DELETE',
getRequestUri(widget.provider, '/api/attachments/${item.id}'));
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
var res = await auth.client!.send(req); var res = await auth.client!.send(req);
@ -162,7 +167,17 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
if (bytes == 0) return '0 Bytes'; if (bytes == 0) return '0 Bytes';
const k = 1024; const k = 1024;
final dm = decimals < 0 ? 0 : decimals; final dm = decimals < 0 ? 0 : decimals;
final sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; final sizes = [
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB'
];
final i = (math.log(bytes) / math.log(k)).floor().toInt(); final i = (math.log(bytes) / math.log(k)).floor().toInt();
return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}'; return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
} }
@ -180,7 +195,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
return Column( return Column(
children: [ children: [
Container( Container(
padding: const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12), padding:
const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -199,7 +215,9 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data == true) { if (snapshot.hasData && snapshot.data == true) {
return TextButton( return TextButton(
onPressed: _isSubmitting ? null : () => viewAttachMethods(context), onPressed: _isSubmitting
? null
: () => viewAttachMethods(context),
style: TextButton.styleFrom(shape: const CircleBorder()), style: TextButton.styleFrom(shape: const CircleBorder()),
child: const Icon(Icons.add_circle), child: const Icon(Icons.add_circle),
); );
@ -211,7 +229,9 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
], ],
), ),
), ),
_isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), _isSubmitting
? const LinearProgressIndicator().animate().scaleX()
: Container(),
Expanded( Expanded(
child: ListView.separated( child: ListView.separated(
itemCount: _attachments.length, itemCount: _attachments.length,
@ -243,7 +263,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
foregroundColor: Colors.red, foregroundColor: Colors.red,
), ),
child: const Icon(Icons.delete), child: const Icon(Icons.delete),
onPressed: () => disposeAttachment(context, element, index), onPressed: () =>
disposeAttachment(context, element, index),
), ),
], ],
), ),
@ -303,7 +324,8 @@ class AttachmentEditorMethodPopup extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.add_photo_alternate, color: Colors.indigo), const Icon(Icons.add_photo_alternate,
color: Colors.indigo),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(AppLocalizations.of(context)!.pickPhoto), Text(AppLocalizations.of(context)!.pickPhoto),
], ],

View File

@ -36,6 +36,17 @@ class _AttachmentItemState extends State<AttachmentItem> {
); );
late final _videoController = VideoController(_videoPlayer); late final _videoController = VideoController(_videoPlayer);
@override
void initState() {
super.initState();
if (widget.type != 1) {
_videoPlayer.open(
Media(widget.url),
play: false,
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const borderRadius = Radius.circular(8); const borderRadius = Radius.circular(8);
@ -53,6 +64,7 @@ class _AttachmentItemState extends State<AttachmentItem> {
children: [ children: [
Image.network( Image.network(
widget.url, widget.url,
key: Key(getTag()),
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
fit: BoxFit.cover, fit: BoxFit.cover,
@ -63,6 +75,7 @@ class _AttachmentItemState extends State<AttachmentItem> {
right: 12, right: 12,
bottom: 8, bottom: 8,
child: Material( child: Material(
color: Colors.transparent,
child: Chip(label: Text(widget.badge!)), child: Chip(label: Text(widget.badge!)),
), ),
) )
@ -83,11 +96,6 @@ class _AttachmentItemState extends State<AttachmentItem> {
}, },
); );
} else { } else {
_videoPlayer.open(
Media(widget.url),
play: false,
);
content = ClipRRect( content = ClipRRect(
borderRadius: const BorderRadius.all(borderRadius), borderRadius: const BorderRadius.all(borderRadius),
child: Video( child: Video(
@ -121,9 +129,11 @@ class AttachmentList extends StatelessWidget {
final List<Attachment> items; final List<Attachment> items;
final String provider; final String provider;
const AttachmentList({super.key, required this.items, required this.provider}); const AttachmentList(
{super.key, required this.items, required this.provider});
Uri getFileUri(String fileId) => getRequestUri(provider, '/api/attachments/o/$fileId'); Uri getFileUri(String fileId) =>
getRequestUri(provider, '/api/attachments/o/$fileId');
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -47,7 +47,8 @@ class _PostItemState extends State<PostItem> {
} }
void viewComments() { void viewComments() {
final PagingController<int, Post> commentPaging = PagingController(firstPageKey: 0); final PagingController<int, Post> commentPaging =
PagingController(firstPageKey: 0);
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -87,10 +88,12 @@ class _PostItemState extends State<PostItem> {
Widget renderAttachments() { Widget renderAttachments() {
if (widget.item.modelType == 'article') return Container(); if (widget.item.modelType == 'article') return Container();
if (widget.item.attachments != null && widget.item.attachments!.isNotEmpty) { if (widget.item.attachments != null &&
widget.item.attachments!.isNotEmpty) {
return Padding( return Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: AttachmentList(items: widget.item.attachments!, provider: 'interactive'), child: AttachmentList(
items: widget.item.attachments!, provider: 'interactive'),
); );
} else { } else {
return Container(); return Container();
@ -130,8 +133,9 @@ class _PostItemState extends State<PostItem> {
); );
} }
String getAuthorDescribe() => String getAuthorDescribe() => widget.item.author.description.isNotEmpty
widget.item.author.description.isNotEmpty ? widget.item.author.description : 'No description yet.'; ? widget.item.author.description
: 'No description yet.';
@override @override
void initState() { void initState() {
@ -177,7 +181,8 @@ class _PostItemState extends State<PostItem> {
children: [ children: [
...headingParts, ...headingParts,
Padding( Padding(
padding: const EdgeInsets.only(left: 12, right: 12, top: 4), padding:
const EdgeInsets.only(left: 12, right: 12, top: 4),
child: renderContent(), child: renderContent(),
), ),
renderAttachments(), renderAttachments(),

View File

@ -106,7 +106,8 @@ class _ReactionActionPopupState extends State<ReactionActionPopup> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12), padding:
const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
@ -118,7 +119,9 @@ class _ReactionActionPopupState extends State<ReactionActionPopup> {
), ),
), ),
), ),
_isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), _isSubmitting
? const LinearProgressIndicator().animate().scaleX()
: Container(),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: reactions.length, itemCount: reactions.length,

View File

@ -36,5 +36,4 @@ class SignInRequiredScreen extends StatelessWidget {
}, },
); );
} }
}
}