Compare commits
No commits in common. "cd5cfedb2fbeca512531637017ab7bf11996b912" and "5d7969276658372f74f917db1792ce3f1e3d3663" have entirely different histories.
cd5cfedb2f
...
5d79692766
@ -21,9 +21,8 @@ linter:
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
lines_longer_than_80_chars: false
|
||||
avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
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
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
@ -16,8 +16,6 @@
|
||||
"confirmation": "Confirmation",
|
||||
"confirmCancel": "Not sure",
|
||||
"confirmOkay": "OK",
|
||||
"email": "Email Address",
|
||||
"nickname": "Nickname",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"next": "Next",
|
||||
|
@ -16,8 +16,6 @@
|
||||
"confirmation": "确认",
|
||||
"confirmCancel": "不太确定",
|
||||
"confirmOkay": "确定",
|
||||
"email": "邮箱地址",
|
||||
"nickname": "显示名",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"next": "下一步",
|
||||
|
@ -25,29 +25,28 @@ class Call {
|
||||
});
|
||||
|
||||
factory Call.fromJson(Map<String, dynamic> json) => Call(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
endedAt:
|
||||
json["ended_at"] != null ? DateTime.parse(json["ended_at"]) : null,
|
||||
externalId: json["external_id"],
|
||||
founderId: json["founder_id"],
|
||||
channelId: json["channel_id"],
|
||||
channel: Channel.fromJson(json["channel"]),
|
||||
);
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
endedAt: json["ended_at"] != null ? DateTime.parse(json["ended_at"]) : null,
|
||||
externalId: json["external_id"],
|
||||
founderId: json["founder_id"],
|
||||
channelId: json["channel_id"],
|
||||
channel: Channel.fromJson(json["channel"]),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"ended_at": endedAt?.toIso8601String(),
|
||||
"external_id": externalId,
|
||||
"founder_id": founderId,
|
||||
"channel_id": channelId,
|
||||
"channel": channel.toJson(),
|
||||
};
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"ended_at": endedAt?.toIso8601String(),
|
||||
"external_id": externalId,
|
||||
"founder_id": founderId,
|
||||
"channel_id": channelId,
|
||||
"channel": channel.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
enum ParticipantStatsType {
|
||||
@ -61,9 +60,9 @@ enum ParticipantStatsType {
|
||||
class ParticipantTrack {
|
||||
ParticipantTrack(
|
||||
{required this.participant,
|
||||
required this.videoTrack,
|
||||
required this.isScreenShare});
|
||||
required this.videoTrack,
|
||||
required this.isScreenShare});
|
||||
VideoTrack? videoTrack;
|
||||
Participant participant;
|
||||
bool isScreenShare;
|
||||
}
|
||||
}
|
@ -32,36 +32,36 @@ class Channel {
|
||||
});
|
||||
|
||||
factory Channel.fromJson(Map<String, dynamic> json) => Channel(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
alias: json["alias"],
|
||||
name: json["name"],
|
||||
description: json["description"],
|
||||
members: json["members"],
|
||||
calls: json["calls"],
|
||||
type: json["type"],
|
||||
account: Account.fromJson(json["account"]),
|
||||
accountId: json["account_id"],
|
||||
realmId: json["realm_id"],
|
||||
);
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
alias: json["alias"],
|
||||
name: json["name"],
|
||||
description: json["description"],
|
||||
members: json["members"],
|
||||
calls: json["calls"],
|
||||
type: json["type"],
|
||||
account: Account.fromJson(json["account"]),
|
||||
accountId: json["account_id"],
|
||||
realmId: json["realm_id"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"alias": alias,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"members": members,
|
||||
"calls": calls,
|
||||
"type": type,
|
||||
"account": account,
|
||||
"account_id": accountId,
|
||||
"realm_id": realmId,
|
||||
};
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"alias": alias,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"members": members,
|
||||
"calls": calls,
|
||||
"type": type,
|
||||
"account": account,
|
||||
"account_id": accountId,
|
||||
"realm_id": realmId,
|
||||
};
|
||||
}
|
||||
|
||||
class ChannelMember {
|
||||
@ -86,24 +86,24 @@ class ChannelMember {
|
||||
});
|
||||
|
||||
factory ChannelMember.fromJson(Map<String, dynamic> json) => ChannelMember(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
channelId: json["channel_id"],
|
||||
accountId: json["account_id"],
|
||||
account: Account.fromJson(json["account"]),
|
||||
notify: json["notify"],
|
||||
);
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
channelId: json["channel_id"],
|
||||
accountId: json["account_id"],
|
||||
account: Account.fromJson(json["account"]),
|
||||
notify: json["notify"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"channel_id": channelId,
|
||||
"account_id": accountId,
|
||||
"account": account.toJson(),
|
||||
"notify": notify,
|
||||
};
|
||||
}
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"channel_id": channelId,
|
||||
"account_id": accountId,
|
||||
"account": account.toJson(),
|
||||
"notify": notify,
|
||||
};
|
||||
}
|
@ -26,30 +26,30 @@ class Friendship {
|
||||
});
|
||||
|
||||
factory Friendship.fromJson(Map<String, dynamic> json) => Friendship(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
accountId: json["account_id"],
|
||||
relatedId: json["related_id"],
|
||||
blockedBy: json["blocked_by"],
|
||||
account: Account.fromJson(json["account"]),
|
||||
related: Account.fromJson(json["related"]),
|
||||
status: json["status"],
|
||||
);
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
accountId: json["account_id"],
|
||||
relatedId: json["related_id"],
|
||||
blockedBy: json["blocked_by"],
|
||||
account: Account.fromJson(json["account"]),
|
||||
related: Account.fromJson(json["related"]),
|
||||
status: json["status"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"account_id": accountId,
|
||||
"related_id": relatedId,
|
||||
"blocked_by": blockedBy,
|
||||
"account": account.toJson(),
|
||||
"related": related.toJson(),
|
||||
"status": status,
|
||||
};
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"account_id": accountId,
|
||||
"related_id": relatedId,
|
||||
"blocked_by": blockedBy,
|
||||
"account": account.toJson(),
|
||||
"related": related.toJson(),
|
||||
"status": status,
|
||||
};
|
||||
|
||||
Account getOtherside(int selfId) {
|
||||
if (accountId != selfId) {
|
||||
@ -58,4 +58,4 @@ class Friendship {
|
||||
return related;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -43,15 +43,11 @@ class Message {
|
||||
content: json["content"],
|
||||
metadata: json["metadata"],
|
||||
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"]),
|
||||
sender: Sender.fromJson(json["sender"]),
|
||||
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"],
|
||||
senderId: json["sender_id"],
|
||||
);
|
||||
@ -64,8 +60,7 @@ class Message {
|
||||
"content": content,
|
||||
"metadata": metadata,
|
||||
"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(),
|
||||
"sender": sender.toJson(),
|
||||
"reply_id": replyId,
|
||||
|
@ -34,9 +34,7 @@ class Notification {
|
||||
deletedAt: json["deleted_at"],
|
||||
subject: json["subject"],
|
||||
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"],
|
||||
isRealtime: json["is_realtime"],
|
||||
readAt: json["read_at"],
|
||||
@ -51,9 +49,7 @@ class Notification {
|
||||
"deleted_at": deletedAt,
|
||||
"subject": subject,
|
||||
"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_realtime": isRealtime,
|
||||
"read_at": readAt,
|
||||
|
@ -10,14 +10,14 @@ class NetworkPackage {
|
||||
});
|
||||
|
||||
factory NetworkPackage.fromJson(Map<String, dynamic> json) => NetworkPackage(
|
||||
method: json["w"],
|
||||
message: json["m"],
|
||||
payload: json["p"],
|
||||
);
|
||||
method: json["w"],
|
||||
message: json["m"],
|
||||
payload: json["p"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"w": method,
|
||||
"m": message,
|
||||
"p": payload,
|
||||
};
|
||||
}
|
||||
"w": method,
|
||||
"m": message,
|
||||
"p": payload,
|
||||
};
|
||||
}
|
@ -8,8 +8,7 @@ import 'package:solian/utils/service_url.dart';
|
||||
class AuthProvider extends ChangeNotifier {
|
||||
AuthProvider();
|
||||
|
||||
final deviceEndpoint =
|
||||
getRequestUri('passport', '/api/notifications/subscribe');
|
||||
final deviceEndpoint = getRequestUri('passport', '/api/notifications/subscribe');
|
||||
final tokenEndpoint = getRequestUri('passport', '/api/auth/token');
|
||||
final userinfoEndpoint = getRequestUri('passport', '/api/users/me');
|
||||
final redirectUrl = Uri.parse('solian://auth');
|
||||
@ -27,13 +26,11 @@ class AuthProvider extends ChangeNotifier {
|
||||
|
||||
DateTime? lastRefreshedAt;
|
||||
|
||||
Future<bool> loadClient() async {
|
||||
Future<bool> pickClient() async {
|
||||
if (await storage.containsKey(key: storageKey)) {
|
||||
try {
|
||||
final credentials =
|
||||
oauth2.Credentials.fromJson((await storage.read(key: storageKey))!);
|
||||
client = oauth2.Client(credentials,
|
||||
identifier: clientId, secret: clientSecret);
|
||||
final credentials = oauth2.Credentials.fromJson((await storage.read(key: storageKey))!);
|
||||
client = oauth2.Client(credentials, identifier: clientId, secret: clientSecret);
|
||||
await fetchProfiles();
|
||||
return true;
|
||||
} catch (e) {
|
||||
@ -45,9 +42,8 @@ class AuthProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<oauth2.Client> createClient(
|
||||
BuildContext context, String username, String password) async {
|
||||
if (await loadClient()) {
|
||||
Future<oauth2.Client> createClient(BuildContext context, String username, String password) async {
|
||||
if (await pickClient()) {
|
||||
return client!;
|
||||
}
|
||||
|
||||
@ -72,17 +68,15 @@ class AuthProvider extends ChangeNotifier {
|
||||
|
||||
Future<void> refreshToken() async {
|
||||
if (client != null) {
|
||||
final credentials = await client!.credentials.refresh(
|
||||
identifier: clientId, secret: clientSecret, basicAuth: false);
|
||||
client = oauth2.Client(credentials,
|
||||
identifier: clientId, secret: clientSecret);
|
||||
final credentials =
|
||||
await client!.credentials.refresh(identifier: clientId, secret: clientSecret, basicAuth: false);
|
||||
client = oauth2.Client(credentials, identifier: clientId, secret: clientSecret);
|
||||
storage.write(key: storageKey, value: credentials.toJson());
|
||||
}
|
||||
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);
|
||||
storage.write(key: storageKey, value: client!.credentials.toJson());
|
||||
|
||||
@ -98,12 +92,9 @@ class AuthProvider extends ChangeNotifier {
|
||||
const storage = FlutterSecureStorage();
|
||||
if (await storage.containsKey(key: storageKey)) {
|
||||
if (client == null) {
|
||||
await loadClient();
|
||||
await pickClient();
|
||||
}
|
||||
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();
|
||||
lastRefreshedAt = DateTime.now();
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ class ChatProvider extends ChangeNotifier {
|
||||
ChatCallInstance? call;
|
||||
|
||||
Future<WebSocketChannel?> connect(AuthProvider auth) async {
|
||||
if (auth.client == null) await auth.loadClient();
|
||||
if (auth.client == null) await auth.pickClient();
|
||||
if (!await auth.isAuthorized()) return null;
|
||||
|
||||
await auth.refreshToken();
|
||||
@ -32,9 +32,7 @@ class ChatProvider extends ChangeNotifier {
|
||||
scheme: ori.scheme.replaceFirst('http', 'ws'),
|
||||
host: ori.host,
|
||||
path: ori.path,
|
||||
queryParameters: {
|
||||
'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)
|
||||
},
|
||||
queryParameters: {'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)},
|
||||
);
|
||||
|
||||
final channel = WebSocketChannel.connect(uri);
|
||||
@ -43,8 +41,7 @@ class ChatProvider extends ChangeNotifier {
|
||||
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;
|
||||
|
||||
this.call = ChatCallInstance(
|
||||
@ -109,8 +106,7 @@ class ChatCallInstance {
|
||||
});
|
||||
|
||||
void init() {
|
||||
subscription =
|
||||
Hardware.instance.onDeviceChange.stream.listen(revertDevices);
|
||||
subscription = Hardware.instance.onDeviceChange.stream.listen(revertDevices);
|
||||
room = Room();
|
||||
listener = room.createListener();
|
||||
Hardware.instance.enumerateDevices().then(revertDevices);
|
||||
@ -118,8 +114,7 @@ class ChatCallInstance {
|
||||
}
|
||||
|
||||
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.microphone.request();
|
||||
@ -136,8 +131,7 @@ class ChatCallInstance {
|
||||
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);
|
||||
if (res.statusCode == 200) {
|
||||
@ -190,12 +184,10 @@ class ChatCallInstance {
|
||||
useiOSBroadcastExtension: true,
|
||||
params: VideoParameters(
|
||||
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(
|
||||
microphone: TrackOption(track: audioTrack),
|
||||
@ -228,8 +220,7 @@ class ChatCallInstance {
|
||||
room.addListener(onRoomDidUpdate);
|
||||
setupRoomListeners(context);
|
||||
sortParticipants();
|
||||
WidgetsBindingCompatible.instance
|
||||
?.addPostFrameCallback((_) => autoPublish(context));
|
||||
WidgetsBindingCompatible.instance?.addPostFrameCallback((_) => autoPublish(context));
|
||||
|
||||
if (lkPlatformIsMobile()) {
|
||||
Hardware.instance.setSpeakerphoneOn(true);
|
||||
@ -304,8 +295,7 @@ class ChatCallInstance {
|
||||
}
|
||||
|
||||
// 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(
|
||||
@ -314,8 +304,7 @@ class ChatCallInstance {
|
||||
isScreenShare: false,
|
||||
);
|
||||
if (room.localParticipant != null) {
|
||||
final localParticipantTracks =
|
||||
room.localParticipant?.videoTrackPublications;
|
||||
final localParticipantTracks = room.localParticipant?.videoTrackPublications;
|
||||
if (localParticipantTracks != null) {
|
||||
for (var t in localParticipantTracks) {
|
||||
localTrack.videoTrack = t.track;
|
||||
@ -328,8 +317,7 @@ class ChatCallInstance {
|
||||
if (focusTrack == null) {
|
||||
focusTrack = participantTracks.first;
|
||||
} 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];
|
||||
}
|
||||
|
||||
|
@ -16,11 +16,11 @@ class FriendProvider extends ChangeNotifier {
|
||||
var res = await auth.client!.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
final result = jsonDecode(utf8.decode(res.bodyBytes)) as List<dynamic>;
|
||||
friends = result.map((x) => Friendship.fromJson(x)).toList();
|
||||
notifyListeners();
|
||||
friends = result.map((x) => Friendship.fromJson(x)).toList();
|
||||
notifyListeners();
|
||||
} else {
|
||||
var message = utf8.decode(res.bodyBytes);
|
||||
throw Exception(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -17,8 +17,7 @@ class NotifyProvider extends ChangeNotifier {
|
||||
|
||||
List<model.Notification> notifications = List.empty(growable: true);
|
||||
|
||||
final FlutterLocalNotificationsPlugin localNotify =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
final FlutterLocalNotificationsPlugin localNotify = FlutterLocalNotificationsPlugin();
|
||||
|
||||
NotifyProvider() {
|
||||
initNotify();
|
||||
@ -32,10 +31,8 @@ class NotifyProvider extends ChangeNotifier {
|
||||
DarwinNotificationCategory("general"),
|
||||
],
|
||||
);
|
||||
const linuxSettings =
|
||||
LinuxInitializationSettings(defaultActionName: 'Open notification');
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
const linuxSettings = LinuxInitializationSettings(defaultActionName: 'Open notification');
|
||||
const InitializationSettings initializationSettings = InitializationSettings(
|
||||
android: androidSettings,
|
||||
iOS: darwinSettings,
|
||||
macOS: darwinSettings,
|
||||
@ -46,8 +43,7 @@ class NotifyProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> requestPermissions() async {
|
||||
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux))
|
||||
return;
|
||||
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) return;
|
||||
await Permission.notification.request();
|
||||
}
|
||||
|
||||
@ -57,18 +53,15 @@ class NotifyProvider extends ChangeNotifier {
|
||||
var uri = getRequestUri('passport', '/api/notifications?skip=0&take=25');
|
||||
var res = await auth.client!.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
final result =
|
||||
PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
|
||||
notifications =
|
||||
result.data?.map((x) => model.Notification.fromJson(x)).toList() ??
|
||||
List.empty(growable: true);
|
||||
final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
|
||||
notifications = result.data?.map((x) => model.Notification.fromJson(x)).toList() ?? List.empty(growable: true);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<WebSocketChannel?> connect(AuthProvider auth) async {
|
||||
if (auth.client == null) await auth.loadClient();
|
||||
if (auth.client == null) await auth.pickClient();
|
||||
if (!await auth.isAuthorized()) return null;
|
||||
|
||||
await auth.refreshToken();
|
||||
@ -78,9 +71,7 @@ class NotifyProvider extends ChangeNotifier {
|
||||
scheme: ori.scheme.replaceFirst('http', 'ws'),
|
||||
host: ori.host,
|
||||
path: ori.path,
|
||||
queryParameters: {
|
||||
'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)
|
||||
},
|
||||
queryParameters: {'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)},
|
||||
);
|
||||
|
||||
final channel = WebSocketChannel.connect(uri);
|
||||
|
@ -4,7 +4,6 @@ import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/screens/account.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/chat.dart';
|
||||
import 'package:solian/screens/chat/index.dart';
|
||||
@ -16,7 +15,7 @@ import 'package:solian/screens/notification.dart';
|
||||
import 'package:solian/screens/posts/comment_editor.dart';
|
||||
import 'package:solian/screens/posts/moment_editor.dart';
|
||||
import 'package:solian/screens/posts/screen.dart';
|
||||
import 'package:solian/screens/auth/signin.dart';
|
||||
import 'package:solian/screens/signin.dart';
|
||||
|
||||
final router = GoRouter(
|
||||
routes: [
|
||||
@ -38,14 +37,12 @@ final router = GoRouter(
|
||||
GoRoute(
|
||||
path: '/chat/create',
|
||||
name: 'chat.channel.editor',
|
||||
builder: (context, state) =>
|
||||
ChannelEditorScreen(editing: state.extra as Channel?),
|
||||
builder: (context, state) => ChannelEditorScreen(editing: state.extra as Channel?),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat/c/: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(
|
||||
path: '/chat/c/:channel/call',
|
||||
@ -55,14 +52,12 @@ final router = GoRouter(
|
||||
GoRoute(
|
||||
path: '/chat/c/: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(
|
||||
path: '/chat/c/: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(
|
||||
path: '/account',
|
||||
@ -72,16 +67,14 @@ final router = GoRouter(
|
||||
GoRoute(
|
||||
path: '/posts/publish/moments',
|
||||
name: 'posts.moments.editor',
|
||||
builder: (context, state) =>
|
||||
MomentEditorScreen(editing: state.extra as Post?),
|
||||
builder: (context, state) => MomentEditorScreen(editing: state.extra as Post?),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/posts/publish/comments',
|
||||
name: 'posts.comments.editor',
|
||||
builder: (context, state) {
|
||||
final args = state.extra as CommentPostArguments;
|
||||
return CommentEditorScreen(
|
||||
editing: args.editing, related: args.related);
|
||||
return CommentEditorScreen(editing: args.editing, related: args.related);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@ -97,11 +90,6 @@ final router = GoRouter(
|
||||
name: 'auth.sign-in',
|
||||
builder: (context, state) => SignInScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth/sign-up',
|
||||
name: 'auth.sign-up',
|
||||
builder: (context, state) => SignUpScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/friend',
|
||||
name: 'account.friend',
|
||||
|
@ -83,7 +83,7 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
title: AppLocalizations.of(context)!.signUp,
|
||||
caption: AppLocalizations.of(context)!.signUpCaption,
|
||||
onTap: () {
|
||||
router.pushNamed('auth.sign-up');
|
||||
launchUrl(getRequestUri('passport', '/sign-up'));
|
||||
},
|
||||
),
|
||||
],
|
||||
@ -131,8 +131,7 @@ class NameCard extends StatelessWidget {
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: renderAvatar(context),
|
||||
builder:
|
||||
(BuildContext context, AsyncSnapshot<Widget> snapshot) {
|
||||
builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return snapshot.data!;
|
||||
} else {
|
||||
@ -143,8 +142,7 @@ class NameCard extends StatelessWidget {
|
||||
const SizedBox(width: 20),
|
||||
FutureBuilder(
|
||||
future: renderLabel(context),
|
||||
builder:
|
||||
(BuildContext context, AsyncSnapshot<Column> snapshot) {
|
||||
builder: (BuildContext context, AsyncSnapshot<Column> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return snapshot.data!;
|
||||
} else {
|
||||
@ -166,12 +164,7 @@ class ActionCard extends StatelessWidget {
|
||||
final String caption;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -120,16 +120,14 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.username,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
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),
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
@ -157,8 +155,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
DismissDirection getDismissDirection(Friendship relation) {
|
||||
if (relation.status == 2) return DismissDirection.endToStart;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -223,18 +220,12 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: _isSubmitting
|
||||
? const LinearProgressIndicator().animate().scaleX()
|
||||
: Container(),
|
||||
child: _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant
|
||||
.withOpacity(0.8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8),
|
||||
child: Text(AppLocalizations.of(context)!.friendPending),
|
||||
),
|
||||
),
|
||||
@ -244,12 +235,8 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant
|
||||
.withOpacity(0.8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8),
|
||||
child: Text(AppLocalizations.of(context)!.friendActive),
|
||||
),
|
||||
),
|
||||
@ -259,12 +246,8 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant
|
||||
.withOpacity(0.8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8),
|
||||
child: Text(AppLocalizations.of(context)!.friendBlocked),
|
||||
),
|
||||
),
|
||||
@ -277,10 +260,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant
|
||||
.withOpacity(0.8),
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8),
|
||||
width: 0.3,
|
||||
)),
|
||||
),
|
||||
|
@ -1,121 +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});
|
||||
|
||||
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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -92,16 +92,14 @@ class _ChatCallState extends State<ChatCall> {
|
||||
itemCount: math.max(0, _call.participantTracks.length),
|
||||
itemBuilder: (BuildContext context, int 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 Padding(
|
||||
padding: const EdgeInsets.only(top: 8, left: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: InteractiveParticipantWidget(
|
||||
isFixed: true,
|
||||
width: 120,
|
||||
@ -109,8 +107,7 @@ class _ChatCallState extends State<ChatCall> {
|
||||
color: Theme.of(context).cardColor,
|
||||
participant: track,
|
||||
onTap: () {
|
||||
if (track.participant.sid !=
|
||||
_call.focusTrack?.participant.sid) {
|
||||
if (track.participant.sid != _call.focusTrack?.participant.sid) {
|
||||
_call.changeFocusTrack(track);
|
||||
}
|
||||
},
|
||||
|
@ -113,13 +113,10 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: Column(
|
||||
children: [
|
||||
_isSubmitting
|
||||
? const LinearProgressIndicator().animate().scaleX()
|
||||
: Container(),
|
||||
_isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(),
|
||||
ListTile(
|
||||
title: Text(AppLocalizations.of(context)!.chatChannelUsage),
|
||||
subtitle:
|
||||
Text(AppLocalizations.of(context)!.chatChannelUsageCaption),
|
||||
subtitle: Text(AppLocalizations.of(context)!.chatChannelUsageCaption),
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.teal,
|
||||
child: Icon(Icons.tag, color: Colors.white),
|
||||
@ -127,8 +124,7 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
|
||||
),
|
||||
const Divider(thickness: 0.3),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -136,18 +132,15 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
|
||||
autofocus: true,
|
||||
controller: _aliasController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: AppLocalizations.of(context)!
|
||||
.chatChannelAliasLabel,
|
||||
hintText: AppLocalizations.of(context)!.chatChannelAliasLabel,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -2, vertical: -2),
|
||||
visualDensity: const VisualDensity(horizontal: -2, vertical: -2),
|
||||
),
|
||||
onPressed: () => randomizeAlias(),
|
||||
child: const Icon(Icons.refresh),
|
||||
@ -157,24 +150,20 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
|
||||
),
|
||||
const Divider(thickness: 0.3),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: TextField(
|
||||
autocorrect: true,
|
||||
controller: _nameController,
|
||||
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),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: TextField(
|
||||
minLines: 5,
|
||||
maxLines: null,
|
||||
@ -182,11 +171,9 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
|
||||
keyboardType: TextInputType.multiline,
|
||||
controller: _descriptionController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: AppLocalizations.of(context)!
|
||||
.chatChannelDescriptionLabel,
|
||||
hintText: AppLocalizations.of(context)!.chatChannelDescriptionLabel,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -36,8 +36,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
|
||||
|
||||
_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);
|
||||
if (res.statusCode == 200) {
|
||||
@ -60,8 +59,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
|
||||
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(
|
||||
uri,
|
||||
@ -91,8 +89,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
|
||||
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(
|
||||
uri,
|
||||
@ -156,9 +153,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: _isSubmitting
|
||||
? const LinearProgressIndicator().animate().scaleX()
|
||||
: Container(),
|
||||
child: _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(),
|
||||
),
|
||||
SliverList.builder(
|
||||
itemCount: _members.length,
|
||||
@ -169,9 +164,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
|
||||
|
||||
return Dismissible(
|
||||
key: Key(randomId.toString()),
|
||||
direction: getKickable(element)
|
||||
? DismissDirection.startToEnd
|
||||
: DismissDirection.none,
|
||||
direction: getKickable(element) ? DismissDirection.startToEnd : DismissDirection.none,
|
||||
background: Container(
|
||||
color: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
@ -179,8 +172,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
|
||||
child: const Icon(Icons.remove, color: Colors.white),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: AccountAvatar(
|
||||
source: element.account.avatar, direct: true),
|
||||
leading: AccountAvatar(source: element.account.avatar, direct: true),
|
||||
title: Text(element.account.nick),
|
||||
subtitle: Text(element.account.name),
|
||||
),
|
||||
|
@ -34,8 +34,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Call? _ongoingCall;
|
||||
Channel? _channelMeta;
|
||||
|
||||
final PagingController<int, Message> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
final PagingController<int, Message> _pagingController = PagingController(firstPageKey: 0);
|
||||
|
||||
final http.Client _client = http.Client();
|
||||
|
||||
@ -54,8 +53,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
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);
|
||||
if (res.statusCode == 200) {
|
||||
final result = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
@ -84,10 +82,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
var res = await auth.client!.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
final result =
|
||||
PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
|
||||
final items =
|
||||
result.data?.map((x) => Message.fromJson(x)).toList() ?? List.empty();
|
||||
final result = 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;
|
||||
if (isLastPage || result.data == null) {
|
||||
_pagingController.appendLastPage(items);
|
||||
@ -101,7 +97,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
bool getMessageMergeable(Message? a, Message? b) {
|
||||
if (a?.replyTo != null) return false;
|
||||
if (a?.replyTo != null || b?.replyTo != null) return false;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.senderId != b.senderId) return false;
|
||||
return a.createdAt.difference(b.createdAt).inMinutes <= 5;
|
||||
@ -115,16 +111,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
void updateMessage(Message item) {
|
||||
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) {
|
||||
setState(() {
|
||||
_pagingController.itemList =
|
||||
_pagingController.itemList?.where((x) => x.id != item.id).toList();
|
||||
_pagingController.itemList = _pagingController.itemList?.where((x) => x.id != item.id).toList();
|
||||
});
|
||||
}
|
||||
|
||||
@ -154,8 +147,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
fetchCall();
|
||||
});
|
||||
|
||||
_pagingController
|
||||
.addPageRequestListener((pageKey) => fetchMessages(pageKey, context));
|
||||
_pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context));
|
||||
|
||||
super.initState();
|
||||
}
|
||||
@ -165,12 +157,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Widget chatHistoryBuilder(context, item, index) {
|
||||
bool isMerged = false, hasMerged = false;
|
||||
if (index > 0) {
|
||||
hasMerged =
|
||||
getMessageMergeable(_pagingController.itemList?[index - 1], item);
|
||||
hasMerged = getMessageMergeable(_pagingController.itemList?[index - 1], item);
|
||||
}
|
||||
if (index + 1 < (_pagingController.itemList?.length ?? 0)) {
|
||||
isMerged =
|
||||
getMessageMergeable(item, _pagingController.itemList?[index + 1]);
|
||||
isMerged = getMessageMergeable(item, _pagingController.itemList?[index + 1]);
|
||||
}
|
||||
return InkWell(
|
||||
child: Container(
|
||||
@ -193,8 +183,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final callBanner = MaterialBanner(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20),
|
||||
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),
|
||||
content: Text(AppLocalizations.of(context)!.chatCallOngoing),
|
||||
actions: [
|
||||
@ -216,12 +205,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
title: _channelMeta?.name ?? "Loading...",
|
||||
appBarActions: _channelMeta != null
|
||||
? [
|
||||
ChannelCallAction(
|
||||
call: _ongoingCall,
|
||||
channel: _channelMeta!,
|
||||
onUpdate: () => fetchMetadata()),
|
||||
ChannelManageAction(
|
||||
channel: _channelMeta!, onUpdate: () => fetchMetadata()),
|
||||
ChannelCallAction(call: _ongoingCall, channel: _channelMeta!, onUpdate: () => fetchMetadata()),
|
||||
ChannelManageAction(channel: _channelMeta!, onUpdate: () => fetchMetadata()),
|
||||
]
|
||||
: [],
|
||||
child: FutureBuilder(
|
||||
@ -258,9 +243,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
_ongoingCall != null
|
||||
? callBanner.animate().slideY()
|
||||
: Container(),
|
||||
_ongoingCall != null ? callBanner.animate().slideY() : Container(),
|
||||
],
|
||||
),
|
||||
onInsertMessage: (message) => addMessage(message),
|
||||
|
@ -105,7 +105,7 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> {
|
||||
'channel': element.alias,
|
||||
},
|
||||
);
|
||||
switch (result) {
|
||||
switch(result) {
|
||||
case 'refresh':
|
||||
fetchChannels();
|
||||
}
|
||||
|
@ -53,9 +53,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
leading: const Icon(Icons.settings),
|
||||
title: Text(AppLocalizations.of(context)!.settings),
|
||||
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 (router.canPop()) router.pop('refresh');
|
||||
}
|
||||
@ -81,14 +79,10 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.channel.name,
|
||||
style: Theme.of(context).textTheme.bodyLarge),
|
||||
Text(widget.channel.description,
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
]),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(widget.channel.name, style: Theme.of(context).textTheme.bodyLarge),
|
||||
Text(widget.channel.description, style: Theme.of(context).textTheme.bodySmall),
|
||||
]),
|
||||
)
|
||||
],
|
||||
),
|
||||
@ -116,12 +110,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
...(_isOwned ? authorizedItems : List.empty()),
|
||||
const Divider(thickness: 0.3),
|
||||
ListTile(
|
||||
leading: _isOwned
|
||||
? const Icon(Icons.delete)
|
||||
: const Icon(Icons.exit_to_app),
|
||||
title: Text(_isOwned
|
||||
? AppLocalizations.of(context)!.delete
|
||||
: AppLocalizations.of(context)!.exit),
|
||||
leading: _isOwned ? const Icon(Icons.delete) : const Icon(Icons.exit_to_app),
|
||||
title: Text(_isOwned ? AppLocalizations.of(context)!.delete : AppLocalizations.of(context)!.exit),
|
||||
onTap: () => promptLeaveChannel(),
|
||||
),
|
||||
],
|
||||
|
@ -22,8 +22,7 @@ class ExploreScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@ -31,15 +30,12 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
final offset = pageKey;
|
||||
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);
|
||||
if (res.statusCode == 200) {
|
||||
final result =
|
||||
PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
|
||||
final items =
|
||||
result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty();
|
||||
final result = 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;
|
||||
if (isLastPage || result.data == null) {
|
||||
_pagingController.appendLastPage(items);
|
||||
|
@ -41,8 +41,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.check),
|
||||
title: Text(AppLocalizations.of(context)!.notifyDone),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)!.notifyDoneCaption),
|
||||
subtitle: Text(AppLocalizations.of(context)!.notifyDoneCaption),
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -79,8 +78,7 @@ class NotificationItem extends StatelessWidget {
|
||||
final model.Notification item;
|
||||
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;
|
||||
|
||||
@ -94,8 +92,7 @@ class NotificationItem extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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(
|
||||
"Links",
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
@ -124,8 +121,7 @@ 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;
|
||||
|
||||
final auth = context.read<AuthProvider>();
|
||||
|
@ -129,68 +129,69 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
|
||||
child: Text(AppLocalizations.of(context)!.postVerb.toUpperCase()),
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
children: [
|
||||
_isSubmitting
|
||||
? const LinearProgressIndicator().animate().scaleX()
|
||||
: Container(),
|
||||
FutureBuilder(
|
||||
future: auth.getProfiles(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var userinfo = snapshot.data;
|
||||
return ListTile(
|
||||
title: Text(userinfo["nick"]),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)!.postIdentityNotify,
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: Column(
|
||||
children: [
|
||||
_isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(),
|
||||
FutureBuilder(
|
||||
future: auth.getProfiles(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var userinfo = snapshot.data;
|
||||
return ListTile(
|
||||
title: Text(userinfo["nick"]),
|
||||
subtitle: Text(
|
||||
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"],
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
widget.editing != null ? editingBanner : Container(),
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(width: 0.3, color: Color(0xffdedede)),
|
||||
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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(shape: const CircleBorder()),
|
||||
child: const Icon(Icons.camera_alt),
|
||||
onPressed: () => viewAttachments(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -119,68 +119,69 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
|
||||
child: Text(AppLocalizations.of(context)!.postVerb.toUpperCase()),
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
children: [
|
||||
_isSubmitting
|
||||
? const LinearProgressIndicator().animate().scaleX()
|
||||
: Container(),
|
||||
FutureBuilder(
|
||||
future: auth.getProfiles(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var userinfo = snapshot.data;
|
||||
return ListTile(
|
||||
title: Text(userinfo["nick"]),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)!.postIdentityNotify,
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: Column(
|
||||
children: [
|
||||
_isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(),
|
||||
FutureBuilder(
|
||||
future: auth.getProfiles(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var userinfo = snapshot.data;
|
||||
return ListTile(
|
||||
title: Text(userinfo["nick"]),
|
||||
subtitle: Text(
|
||||
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"],
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
widget.editing != null ? editingBanner : Container(),
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(width: 0.3, color: Color(0xffdedede)),
|
||||
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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(shape: const CircleBorder()),
|
||||
child: const Icon(Icons.camera_alt),
|
||||
onPressed: () => viewAttachments(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -24,12 +24,10 @@ class PostScreen extends StatefulWidget {
|
||||
class _PostScreenState extends State<PostScreen> {
|
||||
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 {
|
||||
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);
|
||||
if (res.statusCode != 200) {
|
||||
final err = utf8.decode(res.bodyBytes);
|
||||
|
100
lib/screens/signin.dart
Normal file
100
lib/screens/signin.dart
Normal file
@ -0,0 +1,100 @@
|
||||
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/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")) {
|
||||
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', '/sign-in').toString());
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(messages.last),
|
||||
));
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -4,4 +4,4 @@ import 'package:media_kit/media_kit.dart';
|
||||
void initVideo() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
MediaKit.ensureInitialized();
|
||||
}
|
||||
}
|
@ -37,8 +37,7 @@ class _FriendPickerState extends State<FriendPicker> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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(
|
||||
AppLocalizations.of(context)!.friend,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
|
@ -41,8 +41,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
participant.addListener(onChange);
|
||||
_subscription = Hardware.instance.onDeviceChange.stream
|
||||
.listen((List<MediaDevice> devices) {
|
||||
_subscription = Hardware.instance.onDeviceChange.stream.listen((List<MediaDevice> devices) {
|
||||
revertDevices(devices);
|
||||
});
|
||||
Hardware.instance.enumerateDevices().then(revertDevices);
|
||||
@ -162,23 +161,18 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
if (!isRetry) {
|
||||
const androidConfig = FlutterBackgroundAndroidConfig(
|
||||
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,
|
||||
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();
|
||||
}
|
||||
} catch (e) {
|
||||
if (!isRetry) {
|
||||
return await Future<void>.delayed(const Duration(seconds: 1),
|
||||
() => requestBackgroundPermission(true));
|
||||
return await Future<void>.delayed(const Duration(seconds: 1), () => requestBackgroundPermission(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -229,8 +223,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
runSpacing: 5,
|
||||
children: [
|
||||
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,
|
||||
onPressed: disconnect,
|
||||
),
|
||||
@ -260,8 +253,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
return PopupMenuItem<MediaDevice>(
|
||||
value: device,
|
||||
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_outline_blank),
|
||||
title: Text(device.label),
|
||||
@ -289,8 +281,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
onTap: disableVideo,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.videocam_off),
|
||||
title:
|
||||
Text(AppLocalizations.of(context)!.chatCallVideoOff),
|
||||
title: Text(AppLocalizations.of(context)!.chatCallVideoOff),
|
||||
),
|
||||
),
|
||||
if (_videoInputs != null)
|
||||
@ -298,8 +289,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
return PopupMenuItem<MediaDevice>(
|
||||
value: device,
|
||||
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_outline_blank),
|
||||
title: Text(device.label),
|
||||
@ -318,9 +308,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
tooltip: AppLocalizations.of(context)!.chatCallVideoOn,
|
||||
),
|
||||
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,
|
||||
onPressed: () => toggleCamera(),
|
||||
tooltip: AppLocalizations.of(context)!.chatCallVideoFlip,
|
||||
@ -342,8 +330,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
return PopupMenuItem<MediaDevice>(
|
||||
value: device,
|
||||
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_outline_blank),
|
||||
title: Text(device.label),
|
||||
@ -356,12 +343,9 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
),
|
||||
if (!kIsWeb && lkPlatformIs(PlatformType.iOS))
|
||||
IconButton(
|
||||
onPressed: Hardware.instance.canSwitchSpeakerphone
|
||||
? setSpeakerphoneOn
|
||||
: null,
|
||||
onPressed: Hardware.instance.canSwitchSpeakerphone ? setSpeakerphoneOn : null,
|
||||
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,
|
||||
),
|
||||
if (participant.isScreenShareEnabled())
|
||||
|
@ -20,8 +20,7 @@ class NoContentWidget extends StatefulWidget {
|
||||
State<NoContentWidget> createState() => _NoContentWidgetState();
|
||||
}
|
||||
|
||||
class _NoContentWidgetState extends State<NoContentWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
class _NoContentWidgetState extends State<NoContentWidget> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _animationController;
|
||||
|
||||
@override
|
||||
@ -36,9 +35,7 @@ class _NoContentWidgetState extends State<NoContentWidget>
|
||||
if (widget.isSpeaking) {
|
||||
_animationController.repeat(reverse: true);
|
||||
} else {
|
||||
_animationController
|
||||
.animateTo(0, duration: 300.ms)
|
||||
.then((_) => _animationController.reset());
|
||||
_animationController.animateTo(0, duration: 300.ms).then((_) => _animationController.reset());
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,9 +63,7 @@ class _NoContentWidgetState extends State<NoContentWidget>
|
||||
builder: (context, value, child) => Container(
|
||||
decoration: BoxDecoration(
|
||||
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,
|
||||
),
|
||||
|
@ -95,8 +95,7 @@ class RemoteParticipantWidget extends ParticipantWidget {
|
||||
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;
|
||||
|
||||
TrackPublication? get _firstAudioPublication;
|
||||
@ -127,8 +126,7 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
|
||||
void onParticipantChanged() {
|
||||
setState(() {
|
||||
if (widget.participant.metadata != null) {
|
||||
_userinfoMetadata =
|
||||
Account.fromJson(jsonDecode(widget.participant.metadata!));
|
||||
_userinfoMetadata = Account.fromJson(jsonDecode(widget.participant.metadata!));
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -160,11 +158,8 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ParticipantInfoWidget(
|
||||
title: widget.participant.name.isNotEmpty
|
||||
? widget.participant.name
|
||||
: widget.participant.identity,
|
||||
audioAvailable: _firstAudioPublication?.muted == false &&
|
||||
_firstAudioPublication?.subscribed == true,
|
||||
title: widget.participant.name.isNotEmpty ? widget.participant.name : widget.participant.identity,
|
||||
audioAvailable: _firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true,
|
||||
connectionQuality: widget.participant.connectionQuality,
|
||||
isScreenShare: widget.isScreenShare,
|
||||
),
|
||||
@ -176,8 +171,7 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
|
||||
}
|
||||
}
|
||||
|
||||
class _LocalParticipantWidgetState
|
||||
extends _ParticipantWidgetState<LocalParticipantWidget> {
|
||||
class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticipantWidget> {
|
||||
@override
|
||||
LocalTrackPublication<LocalAudioTrack>? get _firstAudioPublication =>
|
||||
widget.participant.audioTrackPublications.firstOrNull;
|
||||
@ -186,8 +180,7 @@ class _LocalParticipantWidgetState
|
||||
VideoTrack? get _activeVideoTrack => widget.videoTrack;
|
||||
}
|
||||
|
||||
class _RemoteParticipantWidgetState
|
||||
extends _ParticipantWidgetState<RemoteParticipantWidget> {
|
||||
class _RemoteParticipantWidgetState extends _ParticipantWidgetState<RemoteParticipantWidget> {
|
||||
@override
|
||||
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
|
||||
widget.participant.audioTrackPublications.firstOrNull;
|
||||
|
@ -55,9 +55,7 @@ class ParticipantInfoWidget extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 5),
|
||||
child: Icon(
|
||||
connectionQuality == ConnectionQuality.poor
|
||||
? Icons.wifi_off_outlined
|
||||
: Icons.wifi,
|
||||
connectionQuality == ConnectionQuality.poor ? Icons.wifi_off_outlined : Icons.wifi,
|
||||
color: {
|
||||
ConnectionQuality.excellent: Colors.green,
|
||||
ConnectionQuality.good: Colors.orange,
|
||||
|
@ -22,9 +22,7 @@ class ParticipantMenu extends StatefulWidget {
|
||||
|
||||
class _ParticipantMenuState extends State<ParticipantMenu> {
|
||||
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 =>
|
||||
widget.participant.audioTrackPublications.firstOrNull;
|
||||
@ -41,8 +39,7 @@ class _ParticipantMenuState extends State<ParticipantMenu> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
@ -62,14 +59,9 @@ class _ParticipantMenuState extends State<ParticipantMenu> {
|
||||
leading: Icon(
|
||||
Icons.volume_up,
|
||||
color: {
|
||||
TrackSubscriptionState.notAllowed:
|
||||
Theme.of(context).colorScheme.error,
|
||||
TrackSubscriptionState.unsubscribed: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
TrackSubscriptionState.subscribed:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
TrackSubscriptionState.notAllowed: Theme.of(context).colorScheme.error,
|
||||
TrackSubscriptionState.unsubscribed: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
TrackSubscriptionState.subscribed: Theme.of(context).colorScheme.primary,
|
||||
}[_firstAudioPublication!.subscriptionState],
|
||||
),
|
||||
title: Text(
|
||||
@ -91,14 +83,9 @@ class _ParticipantMenuState extends State<ParticipantMenu> {
|
||||
leading: Icon(
|
||||
widget.isScreenShare ? Icons.monitor : Icons.videocam,
|
||||
color: {
|
||||
TrackSubscriptionState.notAllowed:
|
||||
Theme.of(context).colorScheme.error,
|
||||
TrackSubscriptionState.unsubscribed: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
TrackSubscriptionState.subscribed:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
TrackSubscriptionState.notAllowed: Theme.of(context).colorScheme.error,
|
||||
TrackSubscriptionState.unsubscribed: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
TrackSubscriptionState.subscribed: Theme.of(context).colorScheme.primary,
|
||||
}[_videoPublication!.subscriptionState],
|
||||
),
|
||||
title: Text(
|
||||
@ -120,9 +107,7 @@ class _ParticipantMenuState extends State<ParticipantMenu> {
|
||||
...[30, 15, 8].map(
|
||||
(x) => ListTile(
|
||||
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'),
|
||||
onTap: () {
|
||||
@ -140,9 +125,7 @@ class _ParticipantMenuState extends State<ParticipantMenu> {
|
||||
].map(
|
||||
(x) => ListTile(
|
||||
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}'),
|
||||
onTap: () {
|
||||
|
@ -28,14 +28,11 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
|
||||
stats['layer-$key'] =
|
||||
'${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) {
|
||||
stats['encoder'] = firstStats.encoderImplementation ?? '';
|
||||
stats['video codec'] =
|
||||
'${firstStats.mimeType}, ${firstStats.clockRate}hz, pt: ${firstStats.payloadType}';
|
||||
stats['qualityLimitationReason'] =
|
||||
firstStats.qualityLimitationReason ?? '';
|
||||
stats['video codec'] = '${firstStats.mimeType}, ${firstStats.clockRate}hz, pt: ${firstStats.payloadType}';
|
||||
stats['qualityLimitationReason'] = firstStats.qualityLimitationReason ?? '';
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -44,8 +41,7 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
|
||||
listener.on<VideoReceiverStatsEvent>((event) {
|
||||
setState(() {
|
||||
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'] =
|
||||
'${event.stats.frameWidth}x${event.stats.frameHeight} ${event.stats.framesPerSecond?.toDouble()}fps';
|
||||
stats['video jitter'] = '${event.stats.jitter} s';
|
||||
@ -74,8 +70,7 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
|
||||
stats['audio codec'] =
|
||||
'${event.stats.mimeType}, ${event.stats.clockRate}hz, ${event.stats.channels}ch, pt: ${event.stats.payloadType}';
|
||||
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 received'] = '${event.stats.packetsReceived}';
|
||||
});
|
||||
@ -88,10 +83,7 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
|
||||
element.dispose();
|
||||
}
|
||||
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) {
|
||||
_setUpListener(track.track!);
|
||||
}
|
||||
@ -125,8 +117,7 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Column(
|
||||
children:
|
||||
stats.entries.map((e) => Text('${e.key}: ${e.value}')).toList(),
|
||||
children: stats.entries.map((e) => Text('${e.key}: ${e.value}')).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -14,8 +14,7 @@ class ChannelCallAction extends StatefulWidget {
|
||||
final Channel channel;
|
||||
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
|
||||
State<ChannelCallAction> createState() => _ChannelCallActionState();
|
||||
@ -33,8 +32,7 @@ class _ChannelCallActionState extends State<ChannelCallAction> {
|
||||
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);
|
||||
if (res.statusCode != 200) {
|
||||
@ -54,8 +52,7 @@ class _ChannelCallActionState extends State<ChannelCallAction> {
|
||||
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);
|
||||
if (res.statusCode != 200) {
|
||||
@ -78,9 +75,7 @@ class _ChannelCallActionState extends State<ChannelCallAction> {
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -89,8 +84,7 @@ class ChannelManageAction extends StatelessWidget {
|
||||
final Channel channel;
|
||||
final Function onUpdate;
|
||||
|
||||
const ChannelManageAction(
|
||||
{super.key, required this.channel, required this.onUpdate});
|
||||
const ChannelManageAction({super.key, required this.channel, required this.onUpdate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -12,8 +12,7 @@ class ChannelDeletion extends StatefulWidget {
|
||||
final Channel channel;
|
||||
final bool isOwned;
|
||||
|
||||
const ChannelDeletion(
|
||||
{super.key, required this.channel, required this.isOwned});
|
||||
const ChannelDeletion({super.key, required this.channel, required this.isOwned});
|
||||
|
||||
@override
|
||||
State<ChannelDeletion> createState() => _ChannelDeletionState();
|
||||
|
@ -55,23 +55,19 @@ class _ChatMaintainerState extends State<ChatMaintainer> {
|
||||
switch (result.method) {
|
||||
case 'messages.new':
|
||||
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;
|
||||
case 'messages.update':
|
||||
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;
|
||||
case 'messages.burnt':
|
||||
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;
|
||||
case 'calls.new':
|
||||
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;
|
||||
case 'calls.end':
|
||||
final payload = Call.fromJson(result.payload!);
|
||||
|
@ -79,9 +79,7 @@ class ChatMessageAction extends StatelessWidget {
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
...(snapshot.data['id'] == item.sender.account.externalId
|
||||
? authorizedItems
|
||||
: List.empty()),
|
||||
...(snapshot.data['id'] == item.sender.account.externalId ? authorizedItems : List.empty()),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.reply),
|
||||
title: Text(AppLocalizations.of(context)!.reply),
|
||||
|
@ -19,8 +19,7 @@ class ChatMessageDeletionDialog extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatMessageDeletionDialog> createState() =>
|
||||
_ChatMessageDeletionDialogState();
|
||||
State<ChatMessageDeletionDialog> createState() => _ChatMessageDeletionDialogState();
|
||||
}
|
||||
|
||||
class _ChatMessageDeletionDialogState extends State<ChatMessageDeletionDialog> {
|
||||
@ -30,8 +29,7 @@ class _ChatMessageDeletionDialogState extends State<ChatMessageDeletionDialog> {
|
||||
final auth = context.read<AuthProvider>();
|
||||
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);
|
||||
final res = await auth.client!.delete(uri);
|
||||
|
@ -18,12 +18,7 @@ class ChatMessageEditor extends StatefulWidget {
|
||||
final Message? replying;
|
||||
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
|
||||
State<ChatMessageEditor> createState() => _ChatMessageEditorState();
|
||||
@ -56,8 +51,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
||||
|
||||
final uri = widget.editing == null
|
||||
? 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);
|
||||
req.headers['Content-Type'] = 'application/json';
|
||||
@ -90,8 +84,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
||||
setState(() {
|
||||
_prevEditingId = widget.editing!.id;
|
||||
_textController.text = widget.editing!.content;
|
||||
_attachments =
|
||||
widget.editing!.attachments ?? List.empty(growable: true);
|
||||
_attachments = widget.editing!.attachments ?? List.empty(growable: true);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -154,15 +147,11 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
||||
children: [
|
||||
badge.Badge(
|
||||
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),
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
padding: const EdgeInsets.all(4)),
|
||||
onPressed:
|
||||
!_isSubmitting ? () => viewAttachments(context) : null,
|
||||
style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)),
|
||||
onPressed: !_isSubmitting ? () => viewAttachments(context) : null,
|
||||
child: const Icon(Icons.attach_file),
|
||||
),
|
||||
),
|
||||
@ -174,18 +163,14 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
||||
autocorrect: true,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText:
|
||||
AppLocalizations.of(context)!.chatMessagePlaceholder,
|
||||
hintText: AppLocalizations.of(context)!.chatMessagePlaceholder,
|
||||
),
|
||||
onSubmitted: (_) => sendMessage(context),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
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,
|
||||
child: const Icon(Icons.send),
|
||||
)
|
||||
|
@ -2,29 +2,17 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
extension SolianCommonExtensions on BuildContext {
|
||||
Future<void> showErrorDialog(dynamic exception) {
|
||||
String formatMessage(dynamic exception) {
|
||||
final message = exception.toString();
|
||||
if (message.trim().isEmpty) return '';
|
||||
return message
|
||||
.split(' ')
|
||||
.map((element) =>
|
||||
"${element[0].toUpperCase()}${element.substring(1).toLowerCase()}")
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
return showDialog<void>(
|
||||
context: this,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(this)!.errorHappened),
|
||||
content: Text(formatMessage(exception)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: Text(AppLocalizations.of(this)!.confirmOkay),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
Future<void> showErrorDialog(dynamic exception) => showDialog<void>(
|
||||
context: this,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(this)!.errorHappened),
|
||||
content: Text(exception.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: Text(AppLocalizations.of(this)!.confirmOkay),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -22,12 +22,10 @@ class IndentWrapper extends LayoutWrapper {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: hideDrawer
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => router.pop(),
|
||||
)
|
||||
: null,
|
||||
leading: hideDrawer ? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => router.pop(),
|
||||
) : null,
|
||||
title: Text(title),
|
||||
actions: appBarActions,
|
||||
),
|
||||
|
@ -49,8 +49,7 @@ 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>();
|
||||
if (!await auth.isAuthorized()) return;
|
||||
|
||||
@ -75,8 +74,7 @@ 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>();
|
||||
if (!await auth.isAuthorized()) return;
|
||||
|
||||
@ -104,8 +102,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
Future<void> uploadAttachment(File file, String hashcode) async {
|
||||
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.fields['hashcode'] = hashcode;
|
||||
|
||||
@ -121,12 +118,10 @@ 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 req = MultipartRequest('DELETE',
|
||||
getRequestUri(widget.provider, '/api/attachments/${item.id}'));
|
||||
final req = MultipartRequest('DELETE', getRequestUri(widget.provider, '/api/attachments/${item.id}'));
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
var res = await auth.client!.send(req);
|
||||
@ -167,17 +162,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
if (bytes == 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
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();
|
||||
return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
|
||||
}
|
||||
@ -195,8 +180,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
return Column(
|
||||
children: [
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@ -215,9 +199,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data == true) {
|
||||
return TextButton(
|
||||
onPressed: _isSubmitting
|
||||
? null
|
||||
: () => viewAttachMethods(context),
|
||||
onPressed: _isSubmitting ? null : () => viewAttachMethods(context),
|
||||
style: TextButton.styleFrom(shape: const CircleBorder()),
|
||||
child: const Icon(Icons.add_circle),
|
||||
);
|
||||
@ -229,9 +211,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
],
|
||||
),
|
||||
),
|
||||
_isSubmitting
|
||||
? const LinearProgressIndicator().animate().scaleX()
|
||||
: Container(),
|
||||
_isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: _attachments.length,
|
||||
@ -263,8 +243,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: const Icon(Icons.delete),
|
||||
onPressed: () =>
|
||||
disposeAttachment(context, element, index),
|
||||
onPressed: () => disposeAttachment(context, element, index),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -324,8 +303,7 @@ class AttachmentEditorMethodPopup extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.add_photo_alternate,
|
||||
color: Colors.indigo),
|
||||
const Icon(Icons.add_photo_alternate, color: Colors.indigo),
|
||||
const SizedBox(height: 8),
|
||||
Text(AppLocalizations.of(context)!.pickPhoto),
|
||||
],
|
||||
|
@ -36,17 +36,6 @@ class _AttachmentItemState extends State<AttachmentItem> {
|
||||
);
|
||||
late final _videoController = VideoController(_videoPlayer);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.type != 1) {
|
||||
_videoPlayer.open(
|
||||
Media(widget.url),
|
||||
play: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const borderRadius = Radius.circular(8);
|
||||
@ -64,7 +53,6 @@ class _AttachmentItemState extends State<AttachmentItem> {
|
||||
children: [
|
||||
Image.network(
|
||||
widget.url,
|
||||
key: Key(getTag()),
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
@ -75,7 +63,6 @@ class _AttachmentItemState extends State<AttachmentItem> {
|
||||
right: 12,
|
||||
bottom: 8,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Chip(label: Text(widget.badge!)),
|
||||
),
|
||||
)
|
||||
@ -96,6 +83,11 @@ class _AttachmentItemState extends State<AttachmentItem> {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
_videoPlayer.open(
|
||||
Media(widget.url),
|
||||
play: false,
|
||||
);
|
||||
|
||||
content = ClipRRect(
|
||||
borderRadius: const BorderRadius.all(borderRadius),
|
||||
child: Video(
|
||||
@ -129,11 +121,9 @@ class AttachmentList extends StatelessWidget {
|
||||
final List<Attachment> items;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -47,8 +47,7 @@ class _PostItemState extends State<PostItem> {
|
||||
}
|
||||
|
||||
void viewComments() {
|
||||
final PagingController<int, Post> commentPaging =
|
||||
PagingController(firstPageKey: 0);
|
||||
final PagingController<int, Post> commentPaging = PagingController(firstPageKey: 0);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@ -88,12 +87,10 @@ class _PostItemState extends State<PostItem> {
|
||||
Widget renderAttachments() {
|
||||
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(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: AttachmentList(
|
||||
items: widget.item.attachments!, provider: 'interactive'),
|
||||
child: AttachmentList(items: widget.item.attachments!, provider: 'interactive'),
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
@ -133,9 +130,8 @@ class _PostItemState extends State<PostItem> {
|
||||
);
|
||||
}
|
||||
|
||||
String getAuthorDescribe() => widget.item.author.description.isNotEmpty
|
||||
? widget.item.author.description
|
||||
: 'No description yet.';
|
||||
String getAuthorDescribe() =>
|
||||
widget.item.author.description.isNotEmpty ? widget.item.author.description : 'No description yet.';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -181,8 +177,7 @@ class _PostItemState extends State<PostItem> {
|
||||
children: [
|
||||
...headingParts,
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 12, right: 12, top: 4),
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 4),
|
||||
child: renderContent(),
|
||||
),
|
||||
renderAttachments(),
|
||||
|
@ -106,8 +106,7 @@ class _ReactionActionPopupState extends State<ReactionActionPopup> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
@ -119,9 +118,7 @@ class _ReactionActionPopupState extends State<ReactionActionPopup> {
|
||||
),
|
||||
),
|
||||
),
|
||||
_isSubmitting
|
||||
? const LinearProgressIndicator().animate().scaleX()
|
||||
: Container(),
|
||||
_isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: reactions.length,
|
||||
|
@ -36,4 +36,5 @@ class SignInRequiredScreen extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user