Compare commits
No commits in common. "7fb94eeafab5f6011fdf8756007bfcce4f5841ed" and "e665b44507944f48c683676742b6151f07c11fdd" have entirely different histories.
7fb94eeafa
...
e665b44507
@ -5,8 +5,6 @@ PODS:
|
|||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_local_notifications (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- flutter_secure_storage (6.0.0):
|
- flutter_secure_storage (6.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_webrtc (0.9.36):
|
- flutter_webrtc (0.9.36):
|
||||||
@ -46,7 +44,6 @@ DEPENDENCIES:
|
|||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
@ -74,8 +71,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_local_notifications:
|
|
||||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
flutter_webrtc:
|
flutter_webrtc:
|
||||||
@ -111,7 +106,6 @@ SPEC CHECKSUMS:
|
|||||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
|
||||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||||
flutter_webrtc: 9bc044b0b5bcaabd0fb7d52c90421fb540f8c35e
|
flutter_webrtc: 9bc044b0b5bcaabd0fb7d52c90421fb540f8c35e
|
||||||
image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18
|
image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"appName": "Solar Network",
|
"solian": "Solian",
|
||||||
"explore": "Explore",
|
"explore": "Explore",
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
@ -28,7 +28,6 @@
|
|||||||
"report": "Report",
|
"report": "Report",
|
||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"errorHappened": "An Error Occurred",
|
|
||||||
"notification": "Notification",
|
"notification": "Notification",
|
||||||
"notifyDone": "You're done!",
|
"notifyDone": "You're done!",
|
||||||
"notifyDoneCaption": "There are no notifications unread for you.",
|
"notifyDoneCaption": "There are no notifications unread for you.",
|
||||||
@ -78,7 +77,6 @@
|
|||||||
"chatChannelDeleteConfirm": "Are you sure you want to delete this channel? All messages in this channel will be gone forever. This operation cannot be revert!",
|
"chatChannelDeleteConfirm": "Are you sure you want to delete this channel? All messages in this channel will be gone forever. This operation cannot be revert!",
|
||||||
"chatCall": "Call",
|
"chatCall": "Call",
|
||||||
"chatCallOngoing": "A call is ongoing",
|
"chatCallOngoing": "A call is ongoing",
|
||||||
"chatCallOngoingShort": "Ongoing",
|
|
||||||
"chatCallJoin": "Join",
|
"chatCallJoin": "Join",
|
||||||
"chatCallMute": "Mute",
|
"chatCallMute": "Mute",
|
||||||
"chatCallUnMute": "Un-mute",
|
"chatCallUnMute": "Un-mute",
|
||||||
@ -87,8 +85,6 @@
|
|||||||
"chatCallVideoFlip": "Flip Camera",
|
"chatCallVideoFlip": "Flip Camera",
|
||||||
"chatCallScreenOn": "Start Screen Share",
|
"chatCallScreenOn": "Start Screen Share",
|
||||||
"chatCallScreenOff": "Stop Screen Share",
|
"chatCallScreenOff": "Stop Screen Share",
|
||||||
"chatCallDisconnect": "Disconnect",
|
|
||||||
"chatCallDisconnectConfirm": "Are you sure you want to disconnect? You can reconnect after this if you want.",
|
|
||||||
"chatCallChangeSpeaker": "Change Speaker",
|
"chatCallChangeSpeaker": "Change Speaker",
|
||||||
"chatMessagePlaceholder": "Write a message...",
|
"chatMessagePlaceholder": "Write a message...",
|
||||||
"chatMessageEditNotify": "You are about editing a message.",
|
"chatMessageEditNotify": "You are about editing a message.",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"appName": "Solar",
|
"solian": "索链",
|
||||||
"explore": "探索",
|
"explore": "探索",
|
||||||
"chat": "聊天",
|
"chat": "聊天",
|
||||||
"account": "账号",
|
"account": "账号",
|
||||||
@ -28,7 +28,6 @@
|
|||||||
"report": "举报",
|
"report": "举报",
|
||||||
"reply": "回复",
|
"reply": "回复",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"errorHappened": "发生了错误",
|
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"notifyDone": "所有通知已读!",
|
"notifyDone": "所有通知已读!",
|
||||||
"notifyDoneCaption": "这里没有什么东西可以给你看的了~",
|
"notifyDoneCaption": "这里没有什么东西可以给你看的了~",
|
||||||
@ -86,10 +85,7 @@
|
|||||||
"chatCallScreenOff": "停止屏幕分享",
|
"chatCallScreenOff": "停止屏幕分享",
|
||||||
"chatCallChangeSpeaker": "切换扬声器",
|
"chatCallChangeSpeaker": "切换扬声器",
|
||||||
"chatCallOngoing": "一则通话正在进行中",
|
"chatCallOngoing": "一则通话正在进行中",
|
||||||
"chatCallOngoingShort": "进行中",
|
|
||||||
"chatCallJoin": "加入",
|
"chatCallJoin": "加入",
|
||||||
"chatCallDisconnect": "断开连接",
|
|
||||||
"chatCallDisconnectConfirm": "你确定你要断开连接吗?你可以之后在任何时候重新连接。",
|
|
||||||
"chatMessagePlaceholder": "发条消息……",
|
"chatMessagePlaceholder": "发条消息……",
|
||||||
"chatMessageEditNotify": "你正在编辑信息中……",
|
"chatMessageEditNotify": "你正在编辑信息中……",
|
||||||
"chatMessageReplyNotify": "你正在回复消息中……",
|
"chatMessageReplyNotify": "你正在回复消息中……",
|
||||||
|
@ -9,7 +9,6 @@ import 'package:solian/router.dart';
|
|||||||
import 'package:solian/utils/timeago.dart';
|
import 'package:solian/utils/timeago.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:solian/utils/video_player.dart';
|
import 'package:solian/utils/video_player.dart';
|
||||||
import 'package:solian/widgets/chat/call/call_overlay.dart';
|
|
||||||
import 'package:solian/widgets/notification_notifier.dart';
|
import 'package:solian/widgets/notification_notifier.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -37,22 +36,21 @@ class SolianApp extends StatelessWidget {
|
|||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
|
return Overlay(
|
||||||
|
initialEntries: [
|
||||||
|
OverlayEntry(builder: (context) {
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
Provider(create: (_) => NavigationProvider()),
|
Provider(create: (_) => NavigationProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => AuthProvider()),
|
Provider(create: (_) => AuthProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => ChatProvider()),
|
Provider(create: (_) => ChatProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => NotifyProvider()),
|
ChangeNotifierProvider(create: (_) => NotifyProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => FriendProvider()),
|
ChangeNotifierProvider(create: (_) => FriendProvider()),
|
||||||
],
|
],
|
||||||
child: Overlay(
|
child: NotificationNotifier(child: child ?? Container()),
|
||||||
initialEntries: [
|
);
|
||||||
OverlayEntry(builder: (context) {
|
})
|
||||||
return NotificationNotifier(child: child ?? Container());
|
|
||||||
}),
|
|
||||||
OverlayEntry(builder: (context) => const CallOverlay()),
|
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||||||
import 'package:oauth2/oauth2.dart' as oauth2;
|
import 'package:oauth2/oauth2.dart' as oauth2;
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
|
|
||||||
class AuthProvider extends ChangeNotifier {
|
class AuthProvider {
|
||||||
AuthProvider();
|
AuthProvider();
|
||||||
|
|
||||||
final deviceEndpoint = getRequestUri('passport', '/api/notifications/subscribe');
|
final deviceEndpoint = getRequestUri('passport', '/api/notifications/subscribe');
|
||||||
@ -63,7 +63,6 @@ class AuthProvider extends ChangeNotifier {
|
|||||||
var userinfo = await client!.get(userinfoEndpoint);
|
var userinfo = await client!.get(userinfoEndpoint);
|
||||||
storage.write(key: profileKey, value: utf8.decode(userinfo.bodyBytes));
|
storage.write(key: profileKey, value: utf8.decode(userinfo.bodyBytes));
|
||||||
}
|
}
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshToken() async {
|
Future<void> refreshToken() async {
|
||||||
@ -73,7 +72,6 @@ class AuthProvider extends ChangeNotifier {
|
|||||||
client = oauth2.Client(credentials, identifier: clientId, secret: clientSecret);
|
client = oauth2.Client(credentials, identifier: clientId, secret: clientSecret);
|
||||||
storage.write(key: storageKey, value: credentials.toJson());
|
storage.write(key: storageKey, value: credentials.toJson());
|
||||||
}
|
}
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> signin(BuildContext context, String username, String password) async {
|
Future<void> signin(BuildContext context, String username, String password) async {
|
||||||
|
@ -1,25 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:solian/models/call.dart';
|
|
||||||
import 'package:solian/models/channel.dart';
|
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:solian/widgets/chat/call/exts.dart';
|
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
class ChatProvider extends ChangeNotifier {
|
class ChatProvider {
|
||||||
bool isOpened = false;
|
bool isOpened = false;
|
||||||
bool isShown = false;
|
|
||||||
|
|
||||||
ChatCallInstance? call;
|
|
||||||
|
|
||||||
Future<WebSocketChannel?> connect(AuthProvider auth) async {
|
Future<WebSocketChannel?> connect(AuthProvider auth) async {
|
||||||
if (auth.client == null) await auth.pickClient();
|
if (auth.client == null) await auth.pickClient();
|
||||||
@ -40,388 +26,4 @@ class ChatProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool handleCall(Call call, Channel channel, {Function? onUpdate, Function? onDispose}) {
|
|
||||||
if (this.call != null) return false;
|
|
||||||
|
|
||||||
this.call = ChatCallInstance(
|
|
||||||
onUpdate: () {
|
|
||||||
notifyListeners();
|
|
||||||
if (onUpdate != null) onUpdate();
|
|
||||||
},
|
|
||||||
onDispose: () {
|
|
||||||
this.call = null;
|
|
||||||
notifyListeners();
|
|
||||||
if (onDispose != null) onDispose();
|
|
||||||
},
|
|
||||||
channel: channel,
|
|
||||||
info: call,
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setShown(bool state) {
|
|
||||||
isShown = state;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatCallInstance {
|
|
||||||
final Function onUpdate;
|
|
||||||
final Function onDispose;
|
|
||||||
|
|
||||||
final Call info;
|
|
||||||
final Channel channel;
|
|
||||||
|
|
||||||
bool isMounted = false;
|
|
||||||
|
|
||||||
String? token;
|
|
||||||
String? endpoint;
|
|
||||||
|
|
||||||
StreamSubscription? subscription;
|
|
||||||
List<MediaDevice> audioInputs = [];
|
|
||||||
List<MediaDevice> videoInputs = [];
|
|
||||||
|
|
||||||
bool enableAudio = true;
|
|
||||||
bool enableVideo = false;
|
|
||||||
LocalAudioTrack? audioTrack;
|
|
||||||
LocalVideoTrack? videoTrack;
|
|
||||||
MediaDevice? videoDevice;
|
|
||||||
MediaDevice? audioDevice;
|
|
||||||
|
|
||||||
final VideoParameters videoParameters = VideoParametersPresets.h720_169;
|
|
||||||
|
|
||||||
late Room room;
|
|
||||||
late EventsListener<RoomEvent> listener;
|
|
||||||
|
|
||||||
List<ParticipantTrack> participantTracks = [];
|
|
||||||
ParticipantTrack? focusTrack;
|
|
||||||
|
|
||||||
ChatCallInstance({
|
|
||||||
required this.onUpdate,
|
|
||||||
required this.onDispose,
|
|
||||||
required this.channel,
|
|
||||||
required this.info,
|
|
||||||
});
|
|
||||||
|
|
||||||
void init() {
|
|
||||||
subscription = Hardware.instance.onDeviceChange.stream.listen(revertDevices);
|
|
||||||
room = Room();
|
|
||||||
listener = room.createListener();
|
|
||||||
Hardware.instance.enumerateDevices().then(revertDevices);
|
|
||||||
WakelockPlus.enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> checkPermissions() async {
|
|
||||||
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) return;
|
|
||||||
|
|
||||||
await Permission.camera.request();
|
|
||||||
await Permission.microphone.request();
|
|
||||||
await Permission.bluetooth.request();
|
|
||||||
await Permission.bluetoothConnect.request();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<(String, String)> exchangeToken(BuildContext context) async {
|
|
||||||
await checkPermissions();
|
|
||||||
|
|
||||||
final auth = context.read<AuthProvider>();
|
|
||||||
if (!await auth.isAuthorized()) {
|
|
||||||
onDispose();
|
|
||||||
throw Exception("unauthorized");
|
|
||||||
}
|
|
||||||
|
|
||||||
var uri = getRequestUri('messaging', '/api/channels/${channel.alias}/calls/ongoing/token');
|
|
||||||
|
|
||||||
var res = await auth.client!.post(uri);
|
|
||||||
if (res.statusCode == 200) {
|
|
||||||
final result = jsonDecode(utf8.decode(res.bodyBytes));
|
|
||||||
token = result['token'];
|
|
||||||
endpoint = 'wss://${result['endpoint']}';
|
|
||||||
joinRoom(context, endpoint!, token!);
|
|
||||||
return (token!, endpoint!);
|
|
||||||
} else {
|
|
||||||
var message = utf8.decode(res.bodyBytes);
|
|
||||||
context.showErrorDialog(message);
|
|
||||||
throw Exception(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void joinRoom(BuildContext context, String url, String token) async {
|
|
||||||
if (isMounted) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
isMounted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
|
||||||
|
|
||||||
final notify = ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(AppLocalizations.of(context)!.connectingServer),
|
|
||||||
duration: const Duration(minutes: 1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await room.connect(
|
|
||||||
url,
|
|
||||||
token,
|
|
||||||
roomOptions: RoomOptions(
|
|
||||||
dynacast: true,
|
|
||||||
adaptiveStream: true,
|
|
||||||
defaultAudioPublishOptions: const AudioPublishOptions(
|
|
||||||
name: 'call_voice',
|
|
||||||
stream: 'call_stream',
|
|
||||||
),
|
|
||||||
defaultVideoPublishOptions: const VideoPublishOptions(
|
|
||||||
name: 'callvideo',
|
|
||||||
stream: 'call_stream',
|
|
||||||
simulcast: true,
|
|
||||||
backupVideoCodec: BackupVideoCodec(enabled: true),
|
|
||||||
),
|
|
||||||
defaultScreenShareCaptureOptions: const ScreenShareCaptureOptions(
|
|
||||||
useiOSBroadcastExtension: true,
|
|
||||||
params: VideoParameters(
|
|
||||||
dimensions: VideoDimensionsPresets.h1080_169,
|
|
||||||
encoding: VideoEncoding(maxBitrate: 3 * 1000 * 1000, maxFramerate: 30),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
defaultCameraCaptureOptions: CameraCaptureOptions(maxFrameRate: 30, params: videoParameters),
|
|
||||||
),
|
|
||||||
fastConnectOptions: FastConnectOptions(
|
|
||||||
microphone: TrackOption(track: audioTrack),
|
|
||||||
camera: TrackOption(track: videoTrack),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
setupRoom(context);
|
|
||||||
} catch (e) {
|
|
||||||
context.showErrorDialog(e);
|
|
||||||
} finally {
|
|
||||||
notify.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void autoPublish(BuildContext context) async {
|
|
||||||
try {
|
|
||||||
if (enableVideo) await room.localParticipant?.setCameraEnabled(true);
|
|
||||||
} catch (error) {
|
|
||||||
await context.showErrorDialog(error);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (enableAudio) await room.localParticipant?.setMicrophoneEnabled(true);
|
|
||||||
} catch (error) {
|
|
||||||
await context.showErrorDialog(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupRoom(BuildContext context) {
|
|
||||||
room.addListener(onRoomDidUpdate);
|
|
||||||
setupRoomListeners(context);
|
|
||||||
sortParticipants();
|
|
||||||
WidgetsBindingCompatible.instance?.addPostFrameCallback((_) => autoPublish(context));
|
|
||||||
|
|
||||||
if (lkPlatformIsMobile()) {
|
|
||||||
Hardware.instance.setSpeakerphoneOn(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupRoomListeners(BuildContext context) {
|
|
||||||
listener
|
|
||||||
..on<RoomDisconnectedEvent>((event) async {
|
|
||||||
if (event.reason != null) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
||||||
content: Text('Call disconnected... ${event.reason}'),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
onDispose();
|
|
||||||
})
|
|
||||||
..on<ParticipantEvent>((event) => sortParticipants())
|
|
||||||
..on<LocalTrackPublishedEvent>((_) => sortParticipants())
|
|
||||||
..on<LocalTrackUnpublishedEvent>((_) => sortParticipants())
|
|
||||||
..on<TrackSubscribedEvent>((_) => sortParticipants())
|
|
||||||
..on<TrackUnsubscribedEvent>((_) => sortParticipants())
|
|
||||||
..on<ParticipantNameUpdatedEvent>((event) {
|
|
||||||
sortParticipants();
|
|
||||||
})
|
|
||||||
..on<AudioPlaybackStatusChanged>((event) async {
|
|
||||||
if (!room.canPlaybackAudio) {
|
|
||||||
bool? yesno = await context.showPlayAudioManuallyDialog();
|
|
||||||
if (yesno == true) {
|
|
||||||
await room.startAudio();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void sortParticipants() {
|
|
||||||
Map<String, ParticipantTrack> mediaTracks = {};
|
|
||||||
for (var participant in room.remoteParticipants.values) {
|
|
||||||
mediaTracks[participant.sid] = ParticipantTrack(
|
|
||||||
participant: participant,
|
|
||||||
videoTrack: null,
|
|
||||||
isScreenShare: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (var t in participant.videoTrackPublications) {
|
|
||||||
mediaTracks[participant.sid]?.videoTrack = t.track;
|
|
||||||
mediaTracks[participant.sid]?.isScreenShare = t.isScreenShare;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final mediaTrackList = mediaTracks.values.toList();
|
|
||||||
mediaTrackList.sort((a, b) {
|
|
||||||
// Loudest people first
|
|
||||||
if (a.participant.isSpeaking && b.participant.isSpeaking) {
|
|
||||||
if (a.participant.audioLevel > b.participant.audioLevel) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last spoke first
|
|
||||||
final aSpokeAt = a.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
|
||||||
final bSpokeAt = b.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
|
||||||
|
|
||||||
if (aSpokeAt != bSpokeAt) {
|
|
||||||
return aSpokeAt > bSpokeAt ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has video first
|
|
||||||
if (a.participant.hasVideo != b.participant.hasVideo) {
|
|
||||||
return a.participant.hasVideo ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// First joined people first
|
|
||||||
return a.participant.joinedAt.millisecondsSinceEpoch - b.participant.joinedAt.millisecondsSinceEpoch;
|
|
||||||
});
|
|
||||||
|
|
||||||
ParticipantTrack localTrack = ParticipantTrack(
|
|
||||||
participant: room.localParticipant!,
|
|
||||||
videoTrack: null,
|
|
||||||
isScreenShare: false,
|
|
||||||
);
|
|
||||||
if (room.localParticipant != null) {
|
|
||||||
final localParticipantTracks = room.localParticipant?.videoTrackPublications;
|
|
||||||
if (localParticipantTracks != null) {
|
|
||||||
for (var t in localParticipantTracks) {
|
|
||||||
localTrack.videoTrack = t.track;
|
|
||||||
localTrack.isScreenShare = t.isScreenShare;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
participantTracks = [localTrack, ...mediaTrackList];
|
|
||||||
if (focusTrack == null) {
|
|
||||||
focusTrack = participantTracks.first;
|
|
||||||
} else {
|
|
||||||
final idx = participantTracks.indexWhere((x) => focusTrack!.participant.sid == x.participant.sid);
|
|
||||||
focusTrack = participantTracks[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void revertDevices(List<MediaDevice> devices) async {
|
|
||||||
audioInputs = devices.where((d) => d.kind == 'audioinput').toList();
|
|
||||||
videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
|
|
||||||
|
|
||||||
if (audioInputs.isNotEmpty) {
|
|
||||||
if (audioDevice == null && enableAudio) {
|
|
||||||
audioDevice = audioInputs.first;
|
|
||||||
Future.delayed(const Duration(milliseconds: 100), () async {
|
|
||||||
await changeLocalAudioTrack();
|
|
||||||
onUpdate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoInputs.isNotEmpty) {
|
|
||||||
if (videoDevice == null && enableVideo) {
|
|
||||||
videoDevice = videoInputs.first;
|
|
||||||
Future.delayed(const Duration(milliseconds: 100), () async {
|
|
||||||
await changeLocalVideoTrack();
|
|
||||||
onUpdate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setEnableVideo(value) async {
|
|
||||||
enableVideo = value;
|
|
||||||
if (!enableVideo) {
|
|
||||||
await videoTrack?.stop();
|
|
||||||
videoTrack = null;
|
|
||||||
} else {
|
|
||||||
await changeLocalVideoTrack();
|
|
||||||
}
|
|
||||||
onUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setEnableAudio(value) async {
|
|
||||||
enableAudio = value;
|
|
||||||
if (!enableAudio) {
|
|
||||||
await audioTrack?.stop();
|
|
||||||
audioTrack = null;
|
|
||||||
} else {
|
|
||||||
await changeLocalAudioTrack();
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> changeLocalAudioTrack() async {
|
|
||||||
if (audioTrack != null) {
|
|
||||||
await audioTrack!.stop();
|
|
||||||
audioTrack = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audioDevice != null) {
|
|
||||||
audioTrack = await LocalAudioTrack.create(AudioCaptureOptions(
|
|
||||||
deviceId: audioDevice!.deviceId,
|
|
||||||
));
|
|
||||||
await audioTrack!.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> changeLocalVideoTrack() async {
|
|
||||||
if (videoTrack != null) {
|
|
||||||
await videoTrack!.stop();
|
|
||||||
videoTrack = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoDevice != null) {
|
|
||||||
videoTrack = await LocalVideoTrack.createCameraTrack(CameraCaptureOptions(
|
|
||||||
deviceId: videoDevice!.deviceId,
|
|
||||||
params: videoParameters,
|
|
||||||
));
|
|
||||||
await videoTrack!.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void changeFocusTrack(ParticipantTrack track) {
|
|
||||||
focusTrack = track;
|
|
||||||
onUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void onRoomDidUpdate() => sortParticipants();
|
|
||||||
|
|
||||||
void deactivate() {
|
|
||||||
subscription?.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
room.removeListener(onRoomDidUpdate);
|
|
||||||
(() async {
|
|
||||||
await listener.dispose();
|
|
||||||
await room.disconnect();
|
|
||||||
await room.dispose();
|
|
||||||
})();
|
|
||||||
WakelockPlus.disable();
|
|
||||||
onDispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:solian/models/pagination.dart';
|
import 'package:solian/models/pagination.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
@ -25,25 +24,17 @@ class NotifyProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void initNotify() {
|
void initNotify() {
|
||||||
const androidSettings = AndroidInitializationSettings('app_icon');
|
|
||||||
const darwinSettings = DarwinInitializationSettings(
|
|
||||||
notificationCategories: [
|
|
||||||
DarwinNotificationCategory("general"),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
const linuxSettings = LinuxInitializationSettings(defaultActionName: 'Open notification');
|
|
||||||
const InitializationSettings initializationSettings = InitializationSettings(
|
const InitializationSettings initializationSettings = InitializationSettings(
|
||||||
android: androidSettings,
|
android: AndroidInitializationSettings('app_icon'),
|
||||||
iOS: darwinSettings,
|
iOS: DarwinInitializationSettings(),
|
||||||
macOS: darwinSettings,
|
macOS: DarwinInitializationSettings(),
|
||||||
linux: linuxSettings,
|
linux: LinuxInitializationSettings(defaultActionName: 'Open notification'),
|
||||||
);
|
);
|
||||||
|
|
||||||
localNotify.initialize(initializationSettings);
|
localNotify.initialize(initializationSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> requestPermissions() async {
|
Future<void> requestPermissions() async {
|
||||||
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) return;
|
|
||||||
await Permission.notification.request();
|
await Permission.notification.request();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,30 +78,20 @@ class NotifyProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void notifyMessage(String title, String body) {
|
void notifyMessage(String title, String body) {
|
||||||
const androidSettings = AndroidNotificationDetails(
|
|
||||||
'general',
|
|
||||||
'General',
|
|
||||||
importance: Importance.high,
|
|
||||||
priority: Priority.high,
|
|
||||||
silent: true,
|
|
||||||
);
|
|
||||||
const darwinSettings = DarwinNotificationDetails(
|
|
||||||
presentAlert: true,
|
|
||||||
presentBanner: true,
|
|
||||||
presentBadge: true,
|
|
||||||
presentSound: false,
|
|
||||||
);
|
|
||||||
const linuxSettings = LinuxNotificationDetails();
|
|
||||||
|
|
||||||
localNotify.show(
|
localNotify.show(
|
||||||
math.max(1, math.Random().nextInt(100000000)),
|
math.max(1, math.Random().nextInt(100000000)),
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
const NotificationDetails(
|
const NotificationDetails(
|
||||||
android: androidSettings,
|
android: AndroidNotificationDetails(
|
||||||
iOS: darwinSettings,
|
'general',
|
||||||
macOS: darwinSettings,
|
'General',
|
||||||
linux: linuxSettings,
|
importance: Importance.high,
|
||||||
|
priority: Priority.high,
|
||||||
|
),
|
||||||
|
iOS: DarwinNotificationDetails(),
|
||||||
|
macOS: DarwinNotificationDetails(),
|
||||||
|
linux: LinuxNotificationDetails(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import 'package:solian/models/friendship.dart';
|
|||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:solian/widgets/account/avatar.dart';
|
import 'package:solian/widgets/account/avatar.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
import 'package:solian/widgets/indent_wrapper.dart';
|
import 'package:solian/widgets/indent_wrapper.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
@ -41,7 +40,9 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +65,9 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
await fetchFriendships();
|
await fetchFriendships();
|
||||||
} else {
|
} else {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _isSubmitting = false);
|
setState(() => _isSubmitting = false);
|
||||||
@ -94,7 +97,9 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
await fetchFriendships();
|
await fetchFriendships();
|
||||||
} else {
|
} else {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _isSubmitting = false);
|
setState(() => _isSubmitting = false);
|
||||||
|
@ -1,13 +1,21 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:solian/models/call.dart';
|
import 'package:solian/models/call.dart';
|
||||||
import 'package:solian/providers/chat.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:solian/widgets/chat/call/controls.dart';
|
import 'package:solian/widgets/chat/call/controls.dart';
|
||||||
|
import 'package:solian/widgets/chat/call/exts.dart';
|
||||||
import 'package:solian/widgets/chat/call/participant.dart';
|
import 'package:solian/widgets/chat/call/participant.dart';
|
||||||
import 'package:solian/widgets/chat/call/participant_menu.dart';
|
import 'package:solian/widgets/chat/call/participant_menu.dart';
|
||||||
import 'package:solian/widgets/indent_wrapper.dart';
|
import 'package:solian/widgets/indent_wrapper.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
class ChatCall extends StatefulWidget {
|
class ChatCall extends StatefulWidget {
|
||||||
@ -20,39 +28,355 @@ class ChatCall extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ChatCallState extends State<ChatCall> {
|
class _ChatCallState extends State<ChatCall> {
|
||||||
bool _isHandled = false;
|
String? _token;
|
||||||
|
String? _endpoint;
|
||||||
|
|
||||||
late ChatProvider _chat;
|
bool _isMounted = false;
|
||||||
|
|
||||||
ChatCallInstance get _call => _chat.call!;
|
StreamSubscription? _subscription;
|
||||||
|
List<MediaDevice> _audioInputs = [];
|
||||||
|
List<MediaDevice> _videoInputs = [];
|
||||||
|
|
||||||
|
bool _enableAudio = true;
|
||||||
|
bool _enableVideo = false;
|
||||||
|
LocalAudioTrack? _audioTrack;
|
||||||
|
LocalVideoTrack? _videoTrack;
|
||||||
|
MediaDevice? _videoDevice;
|
||||||
|
MediaDevice? _audioDevice;
|
||||||
|
|
||||||
|
final VideoParameters _videoParameters = VideoParametersPresets.h720_169;
|
||||||
|
|
||||||
|
late Room _callRoom;
|
||||||
|
late EventsListener<RoomEvent> _callListener;
|
||||||
|
|
||||||
|
List<ParticipantTrack> _participantTracks = [];
|
||||||
|
ParticipantTrack? _focusParticipant;
|
||||||
|
|
||||||
|
Future<void> checkPermissions() async {
|
||||||
|
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) return;
|
||||||
|
|
||||||
|
await Permission.camera.request();
|
||||||
|
await Permission.microphone.request();
|
||||||
|
await Permission.bluetooth.request();
|
||||||
|
await Permission.bluetoothConnect.request();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(String, String)> exchangeToken() async {
|
||||||
|
await checkPermissions();
|
||||||
|
|
||||||
|
final auth = context.read<AuthProvider>();
|
||||||
|
if (!await auth.isAuthorized()) {
|
||||||
|
router.pop();
|
||||||
|
throw Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
var uri = getRequestUri('messaging', '/api/channels/${widget.call.channel.alias}/calls/ongoing/token');
|
||||||
|
|
||||||
|
var res = await auth.client!.post(uri);
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
final result = jsonDecode(utf8.decode(res.bodyBytes));
|
||||||
|
_token = result['token'];
|
||||||
|
_endpoint = 'wss://${result['endpoint']}';
|
||||||
|
joinRoom(_endpoint!, _token!);
|
||||||
|
return (_token!, _endpoint!);
|
||||||
|
} else {
|
||||||
|
var message = utf8.decode(res.bodyBytes);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
|
throw Exception(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void joinRoom(String url, String token) async {
|
||||||
|
if (_isMounted) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
_isMounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
|
|
||||||
|
final notify = ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(AppLocalizations.of(context)!.connectingServer),
|
||||||
|
duration: const Duration(minutes: 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _callRoom.connect(
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
roomOptions: RoomOptions(
|
||||||
|
dynacast: true,
|
||||||
|
adaptiveStream: true,
|
||||||
|
defaultAudioPublishOptions: const AudioPublishOptions(
|
||||||
|
name: 'call_voice',
|
||||||
|
stream: 'call_stream',
|
||||||
|
),
|
||||||
|
defaultVideoPublishOptions: const VideoPublishOptions(
|
||||||
|
name: 'call_video',
|
||||||
|
stream: 'call_stream',
|
||||||
|
simulcast: true,
|
||||||
|
backupVideoCodec: BackupVideoCodec(enabled: true),
|
||||||
|
),
|
||||||
|
defaultScreenShareCaptureOptions: const ScreenShareCaptureOptions(
|
||||||
|
useiOSBroadcastExtension: true,
|
||||||
|
params: VideoParameters(
|
||||||
|
dimensions: VideoDimensionsPresets.h1080_169,
|
||||||
|
encoding: VideoEncoding(maxBitrate: 3 * 1000 * 1000, maxFramerate: 30),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
defaultCameraCaptureOptions: CameraCaptureOptions(maxFrameRate: 30, params: _videoParameters),
|
||||||
|
),
|
||||||
|
fastConnectOptions: FastConnectOptions(
|
||||||
|
microphone: TrackOption(track: _audioTrack),
|
||||||
|
camera: TrackOption(track: _videoTrack),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
setupRoom();
|
||||||
|
} catch (e) {
|
||||||
|
final message = e.toString();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
notify.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void autoPublish() async {
|
||||||
|
try {
|
||||||
|
if (_enableVideo) await _callRoom.localParticipant?.setCameraEnabled(true);
|
||||||
|
} catch (error) {
|
||||||
|
await context.showErrorDialog(error);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (_enableAudio) await _callRoom.localParticipant?.setMicrophoneEnabled(true);
|
||||||
|
} catch (error) {
|
||||||
|
await context.showErrorDialog(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupRoom() {
|
||||||
|
_callRoom.addListener(onRoomDidUpdate);
|
||||||
|
setupRoomListeners();
|
||||||
|
sortParticipants();
|
||||||
|
WidgetsBindingCompatible.instance?.addPostFrameCallback((_) => autoPublish());
|
||||||
|
|
||||||
|
if (lkPlatformIsMobile()) {
|
||||||
|
Hardware.instance.setSpeakerphoneOn(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupRoomListeners() {
|
||||||
|
_callListener
|
||||||
|
..on<RoomDisconnectedEvent>((event) async {
|
||||||
|
if (event.reason != null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text('Call disconnected... ${event.reason}'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (router.canPop()) router.pop();
|
||||||
|
})
|
||||||
|
..on<ParticipantEvent>((event) => sortParticipants())
|
||||||
|
..on<RoomRecordingStatusChanged>((event) {
|
||||||
|
context.showRecordingStatusChangedDialog(event.activeRecording);
|
||||||
|
})
|
||||||
|
..on<LocalTrackPublishedEvent>((_) => sortParticipants())
|
||||||
|
..on<LocalTrackUnpublishedEvent>((_) => sortParticipants())
|
||||||
|
..on<TrackSubscribedEvent>((_) => sortParticipants())
|
||||||
|
..on<TrackUnsubscribedEvent>((_) => sortParticipants())
|
||||||
|
..on<ParticipantNameUpdatedEvent>((event) {
|
||||||
|
sortParticipants();
|
||||||
|
})
|
||||||
|
..on<AudioPlaybackStatusChanged>((event) async {
|
||||||
|
if (!_callRoom.canPlaybackAudio) {
|
||||||
|
bool? yesno = await context.showPlayAudioManuallyDialog();
|
||||||
|
if (yesno == true) {
|
||||||
|
await _callRoom.startAudio();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void sortParticipants() {
|
||||||
|
List<ParticipantTrack> screenTracks = [];
|
||||||
|
Map<String, ParticipantTrack> userMediaTracks = {};
|
||||||
|
for (var participant in _callRoom.remoteParticipants.values) {
|
||||||
|
userMediaTracks[participant.sid] = ParticipantTrack(
|
||||||
|
participant: participant,
|
||||||
|
videoTrack: null,
|
||||||
|
isScreenShare: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (var t in participant.videoTrackPublications) {
|
||||||
|
if (t.isScreenShare) {
|
||||||
|
screenTracks.add(ParticipantTrack(
|
||||||
|
participant: participant,
|
||||||
|
videoTrack: t.track as VideoTrack,
|
||||||
|
isScreenShare: true,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
userMediaTracks[participant.sid]?.videoTrack = t.track;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final userMediaTrackList = userMediaTracks.values.toList();
|
||||||
|
userMediaTrackList.sort((a, b) {
|
||||||
|
// Loudest people first
|
||||||
|
if (a.participant.isSpeaking && b.participant.isSpeaking) {
|
||||||
|
if (a.participant.audioLevel > b.participant.audioLevel) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last spoke first
|
||||||
|
final aSpokeAt = a.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
||||||
|
final bSpokeAt = b.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
||||||
|
|
||||||
|
if (aSpokeAt != bSpokeAt) {
|
||||||
|
return aSpokeAt > bSpokeAt ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has video first
|
||||||
|
if (a.participant.hasVideo != b.participant.hasVideo) {
|
||||||
|
return a.participant.hasVideo ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First joined people first
|
||||||
|
return a.participant.joinedAt.millisecondsSinceEpoch - b.participant.joinedAt.millisecondsSinceEpoch;
|
||||||
|
});
|
||||||
|
|
||||||
|
ParticipantTrack localTrack = ParticipantTrack(
|
||||||
|
participant: _callRoom.localParticipant!,
|
||||||
|
videoTrack: null,
|
||||||
|
isScreenShare: false,
|
||||||
|
);
|
||||||
|
if (_callRoom.localParticipant != null) {
|
||||||
|
final localParticipantTracks = _callRoom.localParticipant?.videoTrackPublications;
|
||||||
|
if (localParticipantTracks != null) {
|
||||||
|
for (var t in localParticipantTracks) {
|
||||||
|
if (t.isScreenShare) {
|
||||||
|
screenTracks.add(ParticipantTrack(
|
||||||
|
participant: _callRoom.localParticipant!,
|
||||||
|
videoTrack: t.track as VideoTrack,
|
||||||
|
isScreenShare: true,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
localTrack.videoTrack = t.track;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_participantTracks = [...screenTracks, localTrack, ...userMediaTrackList];
|
||||||
|
_focusParticipant ??= _participantTracks.first;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onRoomDidUpdate() => sortParticipants();
|
||||||
|
|
||||||
|
void revertDevices(List<MediaDevice> devices) async {
|
||||||
|
_audioInputs = devices.where((d) => d.kind == 'audioinput').toList();
|
||||||
|
_videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
|
||||||
|
|
||||||
|
if (_audioInputs.isNotEmpty) {
|
||||||
|
if (_audioDevice == null && _enableAudio) {
|
||||||
|
_audioDevice = _audioInputs.first;
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () async {
|
||||||
|
await changeLocalAudioTrack();
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_videoInputs.isNotEmpty) {
|
||||||
|
if (_videoDevice == null && _enableVideo) {
|
||||||
|
_videoDevice = _videoInputs.first;
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () async {
|
||||||
|
await changeLocalVideoTrack();
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setEnableVideo(value) async {
|
||||||
|
_enableVideo = value;
|
||||||
|
if (!_enableVideo) {
|
||||||
|
await _videoTrack?.stop();
|
||||||
|
_videoTrack = null;
|
||||||
|
} else {
|
||||||
|
await changeLocalVideoTrack();
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setEnableAudio(value) async {
|
||||||
|
_enableAudio = value;
|
||||||
|
if (!_enableAudio) {
|
||||||
|
await _audioTrack?.stop();
|
||||||
|
_audioTrack = null;
|
||||||
|
} else {
|
||||||
|
await changeLocalAudioTrack();
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> changeLocalAudioTrack() async {
|
||||||
|
if (_audioTrack != null) {
|
||||||
|
await _audioTrack!.stop();
|
||||||
|
_audioTrack = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_audioDevice != null) {
|
||||||
|
_audioTrack = await LocalAudioTrack.create(AudioCaptureOptions(
|
||||||
|
deviceId: _audioDevice!.deviceId,
|
||||||
|
));
|
||||||
|
await _audioTrack!.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> changeLocalVideoTrack() async {
|
||||||
|
if (_videoTrack != null) {
|
||||||
|
await _videoTrack!.stop();
|
||||||
|
_videoTrack = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_videoDevice != null) {
|
||||||
|
_videoTrack = await LocalVideoTrack.createCameraTrack(CameraCaptureOptions(
|
||||||
|
deviceId: _videoDevice!.deviceId,
|
||||||
|
params: _videoParameters,
|
||||||
|
));
|
||||||
|
await _videoTrack!.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_subscription = Hardware.instance.onDeviceChange.stream.listen(revertDevices);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
_callRoom = Room();
|
||||||
_chat.setShown(true);
|
_callListener = _callRoom.createListener();
|
||||||
});
|
Hardware.instance.enumerateDevices().then(revertDevices);
|
||||||
|
WakelockPlus.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_chat = context.watch<ChatProvider>();
|
return IndentWrapper(
|
||||||
if (!_isHandled) {
|
title: AppLocalizations.of(context)!.chatCall,
|
||||||
_isHandled = true;
|
hideDrawer: true,
|
||||||
if (_chat.handleCall(widget.call, widget.call.channel)) {
|
child: FutureBuilder(
|
||||||
_chat.call?.init();
|
future: exchangeToken(),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget content;
|
|
||||||
if (_chat.call == null) {
|
|
||||||
content = const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
content = FutureBuilder(
|
|
||||||
future: _call.exchangeToken(context),
|
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData || snapshot.data == null) {
|
if (!snapshot.hasData || snapshot.data == null) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
@ -65,20 +389,15 @@ class _ChatCallState extends State<ChatCall> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
child: _call.focusTrack != null
|
child: _focusParticipant != null
|
||||||
? InteractiveParticipantWidget(
|
? InteractiveParticipantWidget(
|
||||||
isFixed: false,
|
participant: _focusParticipant!,
|
||||||
participant: _call.focusTrack!,
|
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
)
|
)
|
||||||
: Container(),
|
: Container(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_call.room.localParticipant != null)
|
if (_callRoom.localParticipant != null) ControlsWidget(_callRoom, _callRoom.localParticipant!),
|
||||||
ControlsWidget(
|
|
||||||
_call.room,
|
|
||||||
_call.room.localParticipant!,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@ -89,10 +408,10 @@ class _ChatCallState extends State<ChatCall> {
|
|||||||
height: 128,
|
height: 128,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: math.max(0, _call.participantTracks.length),
|
itemCount: math.max(0, _participantTracks.length),
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final track = _call.participantTracks[index];
|
final track = _participantTracks[index];
|
||||||
if (track.participant.sid == _call.focusTrack?.participant.sid) {
|
if (track.participant.sid == _focusParticipant?.participant.sid) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,14 +420,13 @@ class _ChatCallState extends State<ChatCall> {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: InteractiveParticipantWidget(
|
child: InteractiveParticipantWidget(
|
||||||
isFixed: true,
|
|
||||||
width: 120,
|
width: 120,
|
||||||
height: 120,
|
height: 120,
|
||||||
color: Theme.of(context).cardColor,
|
color: Theme.of(context).cardColor,
|
||||||
participant: track,
|
participant: track,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (track.participant.sid != _call.focusTrack?.participant.sid) {
|
if (track.participant.sid != _focusParticipant?.participant.sid) {
|
||||||
_call.changeFocusTrack(track);
|
setState(() => _focusParticipant = track);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -121,28 +439,33 @@ class _ChatCallState extends State<ChatCall> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
}
|
|
||||||
|
|
||||||
return IndentWrapper(
|
|
||||||
title: AppLocalizations.of(context)!.chatCall,
|
|
||||||
hideDrawer: true,
|
|
||||||
child: content,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void deactivate() {
|
void deactivate() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _chat.setShown(false));
|
_subscription?.cancel();
|
||||||
super.deactivate();
|
super.deactivate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WakelockPlus.disable();
|
||||||
|
(() async {
|
||||||
|
_callRoom.removeListener(onRoomDidUpdate);
|
||||||
|
await _callListener.dispose();
|
||||||
|
await _callRoom.disconnect();
|
||||||
|
await _callRoom.dispose();
|
||||||
|
})();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InteractiveParticipantWidget extends StatelessWidget {
|
class InteractiveParticipantWidget extends StatelessWidget {
|
||||||
final double? width;
|
final double? width;
|
||||||
final double? height;
|
final double? height;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
final bool? isFixed;
|
|
||||||
final ParticipantTrack participant;
|
final ParticipantTrack participant;
|
||||||
final Function() onTap;
|
final Function() onTap;
|
||||||
|
|
||||||
@ -151,7 +474,6 @@ class InteractiveParticipantWidget extends StatelessWidget {
|
|||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
this.color,
|
this.color,
|
||||||
this.isFixed = false,
|
|
||||||
required this.participant,
|
required this.participant,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
});
|
});
|
||||||
@ -163,7 +485,7 @@ class InteractiveParticipantWidget extends StatelessWidget {
|
|||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
color: color,
|
color: color,
|
||||||
child: ParticipantWidget.widgetFor(participant, isFixed: true),
|
child: ParticipantWidget.widgetFor(participant),
|
||||||
),
|
),
|
||||||
onTap: () => onTap(),
|
onTap: () => onTap(),
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
|
@ -8,7 +8,6 @@ import 'package:solian/models/channel.dart';
|
|||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
import 'package:solian/widgets/indent_wrapper.dart';
|
import 'package:solian/widgets/indent_wrapper.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@ -53,7 +52,9 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
|
|||||||
var res = await Response.fromStream(await auth.client!.send(req));
|
var res = await Response.fromStream(await auth.client!.send(req));
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (router.canPop()) {
|
if (router.canPop()) {
|
||||||
router.pop(true);
|
router.pop(true);
|
||||||
|
@ -9,7 +9,6 @@ import 'package:solian/providers/auth.dart';
|
|||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:solian/widgets/account/avatar.dart';
|
import 'package:solian/widgets/account/avatar.dart';
|
||||||
import 'package:solian/widgets/account/friend_picker.dart';
|
import 'package:solian/widgets/account/friend_picker.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
import 'package:solian/widgets/indent_wrapper.dart';
|
import 'package:solian/widgets/indent_wrapper.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
@ -46,7 +45,9 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +75,9 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
|
|||||||
await fetchMemberships();
|
await fetchMemberships();
|
||||||
} else {
|
} else {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _isSubmitting = false);
|
setState(() => _isSubmitting = false);
|
||||||
@ -104,7 +107,9 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
|
|||||||
await fetchMemberships();
|
await fetchMemberships();
|
||||||
} else {
|
} else {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _isSubmitting = false);
|
setState(() => _isSubmitting = false);
|
||||||
|
@ -16,7 +16,6 @@ import 'package:solian/widgets/chat/maintainer.dart';
|
|||||||
import 'package:solian/widgets/chat/message.dart';
|
import 'package:solian/widgets/chat/message.dart';
|
||||||
import 'package:solian/widgets/chat/message_action.dart';
|
import 'package:solian/widgets/chat/message_action.dart';
|
||||||
import 'package:solian/widgets/chat/message_editor.dart';
|
import 'package:solian/widgets/chat/message_editor.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
import 'package:solian/widgets/indent_wrapper.dart';
|
import 'package:solian/widgets/indent_wrapper.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
@ -47,7 +46,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
return _channelMeta!;
|
return _channelMeta!;
|
||||||
} else {
|
} else {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
throw Exception(message);
|
throw Exception(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,7 +62,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
return _ongoingCall;
|
return _ongoingCall;
|
||||||
} else if (res.statusCode != 404) {
|
} else if (res.statusCode != 404) {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
throw Exception(message);
|
throw Exception(message);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
@ -7,7 +7,6 @@ import 'package:solian/providers/auth.dart';
|
|||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:solian/widgets/chat/chat_new.dart';
|
import 'package:solian/widgets/chat/chat_new.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
import 'package:solian/widgets/indent_wrapper.dart';
|
import 'package:solian/widgets/indent_wrapper.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:solian/widgets/notification_notifier.dart';
|
import 'package:solian/widgets/notification_notifier.dart';
|
||||||
@ -37,7 +36,9 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,10 +21,6 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
final auth = context.read<AuthProvider>();
|
final auth = context.read<AuthProvider>();
|
||||||
final nty = context.watch<NotifyProvider>();
|
final nty = context.watch<NotifyProvider>();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
nty.allRead();
|
|
||||||
});
|
|
||||||
|
|
||||||
return IndentWrapper(
|
return IndentWrapper(
|
||||||
noSafeArea: true,
|
noSafeArea: true,
|
||||||
hideDrawer: true,
|
hideDrawer: true,
|
||||||
@ -52,7 +48,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
return NotificationItem(
|
return NotificationItem(
|
||||||
index: index,
|
index: index,
|
||||||
item: element,
|
item: element,
|
||||||
onDismiss: () => nty.clearAt(index),
|
onDismiss: () => setState(() {
|
||||||
|
nty.clearAt(index);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -71,6 +69,14 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
final nty = context.read<NotifyProvider>();
|
||||||
|
nty.allRead();
|
||||||
|
nty.clearRealtime();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationItem extends StatelessWidget {
|
class NotificationItem extends StatelessWidget {
|
||||||
@ -135,7 +141,7 @@ class NotificationItem extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Dismissible(
|
return Dismissible(
|
||||||
key: Key((DateTime.now().millisecondsSinceEpoch << 10).toString()),
|
key: Key('n$index'),
|
||||||
onDismissed: (direction) {
|
onDismissed: (direction) {
|
||||||
markAsRead(item, context).then((value) {
|
markAsRead(item, context).then((value) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
@ -10,7 +10,6 @@ import 'package:solian/router.dart';
|
|||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:solian/widgets/account/avatar.dart';
|
import 'package:solian/widgets/account/avatar.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
import 'package:solian/widgets/indent_wrapper.dart';
|
import 'package:solian/widgets/indent_wrapper.dart';
|
||||||
import 'package:solian/widgets/posts/attachment_editor.dart';
|
import 'package:solian/widgets/posts/attachment_editor.dart';
|
||||||
|
|
||||||
@ -76,7 +75,9 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
|
|||||||
var res = await Response.fromStream(await auth.client!.send(req));
|
var res = await Response.fromStream(await auth.client!.send(req));
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (router.canPop()) {
|
if (router.canPop()) {
|
||||||
router.pop(true);
|
router.pop(true);
|
||||||
|
@ -10,7 +10,6 @@ import 'package:solian/router.dart';
|
|||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:solian/widgets/account/avatar.dart';
|
import 'package:solian/widgets/account/avatar.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
import 'package:solian/widgets/indent_wrapper.dart';
|
import 'package:solian/widgets/indent_wrapper.dart';
|
||||||
import 'package:solian/widgets/posts/attachment_editor.dart';
|
import 'package:solian/widgets/posts/attachment_editor.dart';
|
||||||
|
|
||||||
@ -66,7 +65,9 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
|
|||||||
var res = await Response.fromStream(await auth.client!.send(req));
|
var res = await Response.fromStream(await auth.client!.send(req));
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (router.canPop()) {
|
if (router.canPop()) {
|
||||||
router.pop(true);
|
router.pop(true);
|
||||||
|
@ -5,7 +5,6 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
import 'package:solian/widgets/indent_wrapper.dart';
|
import 'package:solian/widgets/indent_wrapper.dart';
|
||||||
import 'package:solian/widgets/posts/comment_list.dart';
|
import 'package:solian/widgets/posts/comment_list.dart';
|
||||||
import 'package:solian/widgets/posts/item.dart';
|
import 'package:solian/widgets/posts/item.dart';
|
||||||
@ -31,7 +30,9 @@ class _PostScreenState extends State<PostScreen> {
|
|||||||
final res = await _client.get(uri);
|
final res = await _client.get(uri);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
final err = utf8.decode(res.bodyBytes);
|
final err = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(err);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $err")),
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return Post.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
|
return Post.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
||||||
import 'package:draggable_float_widget/draggable_float_widget.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:solian/providers/chat.dart';
|
|
||||||
import 'package:solian/router.dart';
|
|
||||||
|
|
||||||
class CallOverlay extends StatelessWidget {
|
|
||||||
const CallOverlay({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
const radius = BorderRadius.all(Radius.circular(8));
|
|
||||||
|
|
||||||
final chat = context.watch<ChatProvider>();
|
|
||||||
|
|
||||||
if (chat.isShown || chat.call == null) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
return DraggableFloatWidget(
|
|
||||||
config: const DraggableFloatWidgetBaseConfig(
|
|
||||||
initPositionYInTop: false,
|
|
||||||
initPositionYMarginBorder: 50,
|
|
||||||
borderTopContainTopBar: true,
|
|
||||||
borderBottom: defaultBorderWidth,
|
|
||||||
borderLeft: 8,
|
|
||||||
),
|
|
||||||
child: Material(
|
|
||||||
elevation: 6,
|
|
||||||
color: Colors.transparent,
|
|
||||||
borderRadius: radius,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: radius,
|
|
||||||
child: Container(
|
|
||||||
height: 80,
|
|
||||||
width: 80,
|
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.call, size: 18),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context)!.chatCallOngoingShort,
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
router.pushNamed(
|
|
||||||
'chat.channel.call',
|
|
||||||
extra: chat.call!.info,
|
|
||||||
pathParameters: {'channel': chat.call!.channel.alias},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,11 +6,6 @@ import 'package:flutter_background/flutter_background.dart';
|
|||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:solian/providers/chat.dart';
|
|
||||||
import 'package:solian/router.dart';
|
|
||||||
import 'package:solian/widgets/chat/call/exts.dart';
|
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
|
|
||||||
class ControlsWidget extends StatefulWidget {
|
class ControlsWidget extends StatefulWidget {
|
||||||
final Room room;
|
final Room room;
|
||||||
@ -68,22 +63,11 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
|||||||
|
|
||||||
bool get isMuted => participant.isMuted;
|
bool get isMuted => participant.isMuted;
|
||||||
|
|
||||||
void disconnect() async {
|
|
||||||
if (await context.showDisconnectDialog() != true) return;
|
|
||||||
|
|
||||||
final chat = context.read<ChatProvider>();
|
|
||||||
if (chat.call != null) {
|
|
||||||
chat.call!.deactivate();
|
|
||||||
chat.call!.dispose();
|
|
||||||
router.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void disableAudio() async {
|
void disableAudio() async {
|
||||||
await participant.setMicrophoneEnabled(false);
|
await participant.setMicrophoneEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void enableAudio() async {
|
Future<void> enableAudio() async {
|
||||||
await participant.setMicrophoneEnabled(true);
|
await participant.setMicrophoneEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,7 +134,9 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
|||||||
await participant.publishVideoTrack(track);
|
await participant.publishVideoTrack(track);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
final message = e.toString();
|
final message = e.toString();
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text('Something went wrong... $message'),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -222,17 +208,11 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
|||||||
spacing: 5,
|
spacing: 5,
|
||||||
runSpacing: 5,
|
runSpacing: 5,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
|
||||||
icon: Transform.flip(flipX: true, child: const Icon(Icons.exit_to_app)),
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
onPressed: disconnect,
|
|
||||||
),
|
|
||||||
if (participant.isMicrophoneEnabled())
|
if (participant.isMicrophoneEnabled())
|
||||||
if (lkPlatformIs(PlatformType.android))
|
if (lkPlatformIs(PlatformType.android))
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: disableAudio,
|
onPressed: disableAudio,
|
||||||
icon: const Icon(Icons.mic),
|
icon: const Icon(Icons.mic),
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
tooltip: AppLocalizations.of(context)!.chatCallMute,
|
tooltip: AppLocalizations.of(context)!.chatCallMute,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@ -268,9 +248,43 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
onPressed: enableAudio,
|
onPressed: enableAudio,
|
||||||
icon: const Icon(Icons.mic_off),
|
icon: const Icon(Icons.mic_off),
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
tooltip: AppLocalizations.of(context)!.chatCallUnMute,
|
tooltip: AppLocalizations.of(context)!.chatCallUnMute,
|
||||||
),
|
),
|
||||||
|
if (!lkPlatformIs(PlatformType.iOS))
|
||||||
|
PopupMenuButton<MediaDevice>(
|
||||||
|
icon: const Icon(Icons.volume_up),
|
||||||
|
itemBuilder: (BuildContext context) {
|
||||||
|
return [
|
||||||
|
const PopupMenuItem<MediaDevice>(
|
||||||
|
value: null,
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(Icons.speaker),
|
||||||
|
title: Text('Select Audio Output'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_audioOutputs != null)
|
||||||
|
..._audioOutputs!.map((device) {
|
||||||
|
return PopupMenuItem<MediaDevice>(
|
||||||
|
value: device,
|
||||||
|
child: ListTile(
|
||||||
|
leading: (device.deviceId == widget.room.selectedAudioOutputDeviceId)
|
||||||
|
? const Icon(Icons.check_box_outlined)
|
||||||
|
: const Icon(Icons.check_box_outline_blank),
|
||||||
|
title: Text(device.label),
|
||||||
|
),
|
||||||
|
onTap: () => selectAudioOutput(device),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!kIsWeb && lkPlatformIs(PlatformType.iOS))
|
||||||
|
IconButton(
|
||||||
|
disabledColor: Colors.grey,
|
||||||
|
onPressed: Hardware.instance.canSwitchSpeakerphone ? setSpeakerphoneOn : null,
|
||||||
|
icon: Icon(_speakerphoneOn ? Icons.speaker_phone : Icons.phone_android),
|
||||||
|
tooltip: AppLocalizations.of(context)!.chatCallChangeSpeaker,
|
||||||
|
),
|
||||||
if (participant.isCameraEnabled())
|
if (participant.isCameraEnabled())
|
||||||
PopupMenuButton<MediaDevice>(
|
PopupMenuButton<MediaDevice>(
|
||||||
icon: const Icon(Icons.videocam_sharp),
|
icon: const Icon(Icons.videocam_sharp),
|
||||||
@ -304,61 +318,22 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
onPressed: enableVideo,
|
onPressed: enableVideo,
|
||||||
icon: const Icon(Icons.videocam_off),
|
icon: const Icon(Icons.videocam_off),
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
tooltip: AppLocalizations.of(context)!.chatCallVideoOn,
|
tooltip: AppLocalizations.of(context)!.chatCallVideoOn,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(position == CameraPosition.back ? Icons.video_camera_back : Icons.video_camera_front),
|
icon: Icon(position == CameraPosition.back ? Icons.video_camera_back : Icons.video_camera_front),
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
onPressed: () => toggleCamera(),
|
onPressed: () => toggleCamera(),
|
||||||
tooltip: AppLocalizations.of(context)!.chatCallVideoFlip,
|
tooltip: AppLocalizations.of(context)!.chatCallVideoFlip,
|
||||||
),
|
),
|
||||||
if (!lkPlatformIs(PlatformType.iOS))
|
|
||||||
PopupMenuButton<MediaDevice>(
|
|
||||||
icon: const Icon(Icons.volume_up),
|
|
||||||
itemBuilder: (BuildContext context) {
|
|
||||||
return [
|
|
||||||
const PopupMenuItem<MediaDevice>(
|
|
||||||
value: null,
|
|
||||||
child: ListTile(
|
|
||||||
leading: Icon(Icons.speaker),
|
|
||||||
title: Text('Select Audio Output'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_audioOutputs != null)
|
|
||||||
..._audioOutputs!.map((device) {
|
|
||||||
return PopupMenuItem<MediaDevice>(
|
|
||||||
value: device,
|
|
||||||
child: ListTile(
|
|
||||||
leading: (device.deviceId == widget.room.selectedAudioOutputDeviceId)
|
|
||||||
? const Icon(Icons.check_box_outlined)
|
|
||||||
: const Icon(Icons.check_box_outline_blank),
|
|
||||||
title: Text(device.label),
|
|
||||||
),
|
|
||||||
onTap: () => selectAudioOutput(device),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
];
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (!kIsWeb && lkPlatformIs(PlatformType.iOS))
|
|
||||||
IconButton(
|
|
||||||
onPressed: Hardware.instance.canSwitchSpeakerphone ? setSpeakerphoneOn : null,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
icon: Icon(_speakerphoneOn ? Icons.speaker_phone : Icons.phone_android),
|
|
||||||
tooltip: AppLocalizations.of(context)!.chatCallChangeSpeaker,
|
|
||||||
),
|
|
||||||
if (participant.isScreenShareEnabled())
|
if (participant.isScreenShareEnabled())
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.monitor_outlined),
|
icon: const Icon(Icons.monitor_outlined),
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
onPressed: () => disableScreenShare(),
|
onPressed: () => disableScreenShare(),
|
||||||
tooltip: AppLocalizations.of(context)!.chatCallScreenOff,
|
tooltip: AppLocalizations.of(context)!.chatCallScreenOff,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.monitor),
|
icon: const Icon(Icons.monitor),
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
onPressed: () => enableScreenShare(),
|
onPressed: () => enableScreenShare(),
|
||||||
tooltip: AppLocalizations.of(context)!.chatCallScreenOn,
|
tooltip: AppLocalizations.of(context)!.chatCallScreenOn,
|
||||||
),
|
),
|
||||||
|
@ -1,14 +1,30 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
extension SolianCallExt on BuildContext {
|
extension SolianCallExt on BuildContext {
|
||||||
|
Future<bool?> showPublishDialog() => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Publish'),
|
||||||
|
content: const Text('Would you like to publish your Camera & Mic ?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text('NO'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('YES'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Future<bool?> showPlayAudioManuallyDialog() => showDialog<bool>(
|
Future<bool?> showPlayAudioManuallyDialog() => showDialog<bool>(
|
||||||
context: this,
|
context: this,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text('Play Audio'),
|
title: const Text('Play Audio'),
|
||||||
content: const Text(
|
content: const Text(
|
||||||
'You need to manually activate audio PlayBack for iOS Safari!',
|
'You need to manually activate audio PlayBack for iOS Safari !'),
|
||||||
),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx, false),
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
@ -22,19 +38,52 @@ extension SolianCallExt on BuildContext {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<bool?> showDisconnectDialog() => showDialog<bool>(
|
Future<bool?> showUnPublishDialog() => showDialog<bool>(
|
||||||
context: this,
|
context: this,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: Text(AppLocalizations.of(this)!.chatCallDisconnect),
|
title: const Text('UnPublish'),
|
||||||
content: Text(AppLocalizations.of(this)!.chatCallDisconnectConfirm),
|
content:
|
||||||
|
const Text('Would you like to un-publish your Camera & Mic ?'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx, false),
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
child: Text(AppLocalizations.of(this)!.confirmCancel),
|
child: const Text('NO'),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx, true),
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
child: Text(AppLocalizations.of(this)!.confirmOkay),
|
child: const Text('YES'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> showErrorDialog(dynamic exception) => showDialog<void>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Error'),
|
||||||
|
content: Text(exception.toString()),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: const Text('OK'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool?> showDisconnectDialog() => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Disconnect'),
|
||||||
|
content: const Text('Are you sure to disconnect?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('Disconnect'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -71,4 +120,87 @@ extension SolianCallExt on BuildContext {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<bool?> showSendDataDialog() => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Send data'),
|
||||||
|
content: const Text(
|
||||||
|
'This will send a sample data to all participants in the room'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('Send'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool?> showDataReceivedDialog(String data) => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Received data'),
|
||||||
|
content: Text(data),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool?> showRecordingStatusChangedDialog(bool isActiveRecording) =>
|
||||||
|
showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Room recording reminder'),
|
||||||
|
content: Text(isActiveRecording
|
||||||
|
? 'Room recording is active.'
|
||||||
|
: 'Room recording is stoped.'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool?> showSubscribePermissionDialog() => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Allow subscription'),
|
||||||
|
content: const Text(
|
||||||
|
'Allow all participants to subscribe tracks published by local participant?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text('NO'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('YES'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SimulateScenarioResult {
|
||||||
|
signalReconnect,
|
||||||
|
fullReconnect,
|
||||||
|
speakerUpdate,
|
||||||
|
nodeFailure,
|
||||||
|
migration,
|
||||||
|
serverLeave,
|
||||||
|
switchCandidate,
|
||||||
|
e2eeKeyRatchet,
|
||||||
|
participantName,
|
||||||
|
participantMetadata,
|
||||||
|
clear,
|
||||||
}
|
}
|
@ -7,14 +7,8 @@ import 'dart:math' as math;
|
|||||||
class NoContentWidget extends StatefulWidget {
|
class NoContentWidget extends StatefulWidget {
|
||||||
final Account? userinfo;
|
final Account? userinfo;
|
||||||
final bool isSpeaking;
|
final bool isSpeaking;
|
||||||
final bool isFixed;
|
|
||||||
|
|
||||||
const NoContentWidget({
|
const NoContentWidget({super.key, this.userinfo, required this.isSpeaking});
|
||||||
super.key,
|
|
||||||
this.userinfo,
|
|
||||||
this.isFixed = false,
|
|
||||||
required this.isSpeaking,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<NoContentWidget> createState() => _NoContentWidgetState();
|
State<NoContentWidget> createState() => _NoContentWidgetState();
|
||||||
@ -41,12 +35,7 @@ class _NoContentWidgetState extends State<NoContentWidget> with SingleTickerProv
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final double radius = widget.isFixed
|
final radius = math.min(MediaQuery.of(context).size.width, MediaQuery.of(context).size.height) * 0.1;
|
||||||
? 16
|
|
||||||
: math.min(
|
|
||||||
MediaQuery.of(context).size.width * 0.1,
|
|
||||||
MediaQuery.of(context).size.height * 0.1,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
@ -74,8 +63,7 @@ class _NoContentWidgetState extends State<NoContentWidget> with SingleTickerProv
|
|||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
radius: radius,
|
radius: radius,
|
||||||
direct: true,
|
direct: true,
|
||||||
),
|
)),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,24 +10,13 @@ import 'package:solian/widgets/chat/call/participant_info.dart';
|
|||||||
import 'package:solian/widgets/chat/call/participant_stats.dart';
|
import 'package:solian/widgets/chat/call/participant_stats.dart';
|
||||||
|
|
||||||
abstract class ParticipantWidget extends StatefulWidget {
|
abstract class ParticipantWidget extends StatefulWidget {
|
||||||
static ParticipantWidget widgetFor(ParticipantTrack participantTrack,
|
static ParticipantWidget widgetFor(ParticipantTrack participantTrack, {bool showStatsLayer = false}) {
|
||||||
{bool isFixed = false, bool showStatsLayer = false}) {
|
|
||||||
if (participantTrack.participant is LocalParticipant) {
|
if (participantTrack.participant is LocalParticipant) {
|
||||||
return LocalParticipantWidget(
|
return LocalParticipantWidget(participantTrack.participant as LocalParticipant, participantTrack.videoTrack,
|
||||||
participantTrack.participant as LocalParticipant,
|
participantTrack.isScreenShare, showStatsLayer);
|
||||||
participantTrack.videoTrack,
|
|
||||||
isFixed,
|
|
||||||
participantTrack.isScreenShare,
|
|
||||||
showStatsLayer,
|
|
||||||
);
|
|
||||||
} else if (participantTrack.participant is RemoteParticipant) {
|
} else if (participantTrack.participant is RemoteParticipant) {
|
||||||
return RemoteParticipantWidget(
|
return RemoteParticipantWidget(participantTrack.participant as RemoteParticipant, participantTrack.videoTrack,
|
||||||
participantTrack.participant as RemoteParticipant,
|
participantTrack.isScreenShare, showStatsLayer);
|
||||||
participantTrack.videoTrack,
|
|
||||||
isFixed,
|
|
||||||
participantTrack.isScreenShare,
|
|
||||||
showStatsLayer,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
throw UnimplementedError('Unknown participant type');
|
throw UnimplementedError('Unknown participant type');
|
||||||
}
|
}
|
||||||
@ -35,7 +24,6 @@ abstract class ParticipantWidget extends StatefulWidget {
|
|||||||
abstract final Participant participant;
|
abstract final Participant participant;
|
||||||
abstract final VideoTrack? videoTrack;
|
abstract final VideoTrack? videoTrack;
|
||||||
abstract final bool isScreenShare;
|
abstract final bool isScreenShare;
|
||||||
abstract final bool isFixed;
|
|
||||||
abstract final bool showStatsLayer;
|
abstract final bool showStatsLayer;
|
||||||
final VideoQuality quality;
|
final VideoQuality quality;
|
||||||
|
|
||||||
@ -51,8 +39,6 @@ class LocalParticipantWidget extends ParticipantWidget {
|
|||||||
@override
|
@override
|
||||||
final VideoTrack? videoTrack;
|
final VideoTrack? videoTrack;
|
||||||
@override
|
@override
|
||||||
final bool isFixed;
|
|
||||||
@override
|
|
||||||
final bool isScreenShare;
|
final bool isScreenShare;
|
||||||
@override
|
@override
|
||||||
final bool showStatsLayer;
|
final bool showStatsLayer;
|
||||||
@ -60,7 +46,6 @@ class LocalParticipantWidget extends ParticipantWidget {
|
|||||||
const LocalParticipantWidget(
|
const LocalParticipantWidget(
|
||||||
this.participant,
|
this.participant,
|
||||||
this.videoTrack,
|
this.videoTrack,
|
||||||
this.isFixed,
|
|
||||||
this.isScreenShare,
|
this.isScreenShare,
|
||||||
this.showStatsLayer, {
|
this.showStatsLayer, {
|
||||||
super.key,
|
super.key,
|
||||||
@ -76,8 +61,6 @@ class RemoteParticipantWidget extends ParticipantWidget {
|
|||||||
@override
|
@override
|
||||||
final VideoTrack? videoTrack;
|
final VideoTrack? videoTrack;
|
||||||
@override
|
@override
|
||||||
final bool isFixed;
|
|
||||||
@override
|
|
||||||
final bool isScreenShare;
|
final bool isScreenShare;
|
||||||
@override
|
@override
|
||||||
final bool showStatsLayer;
|
final bool showStatsLayer;
|
||||||
@ -85,7 +68,6 @@ class RemoteParticipantWidget extends ParticipantWidget {
|
|||||||
const RemoteParticipantWidget(
|
const RemoteParticipantWidget(
|
||||||
this.participant,
|
this.participant,
|
||||||
this.videoTrack,
|
this.videoTrack,
|
||||||
this.isFixed,
|
|
||||||
this.isScreenShare,
|
this.isScreenShare,
|
||||||
this.showStatsLayer, {
|
this.showStatsLayer, {
|
||||||
super.key,
|
super.key,
|
||||||
@ -98,6 +80,8 @@ class RemoteParticipantWidget extends ParticipantWidget {
|
|||||||
abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends State<T> {
|
abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends State<T> {
|
||||||
VideoTrack? get _activeVideoTrack;
|
VideoTrack? get _activeVideoTrack;
|
||||||
|
|
||||||
|
TrackPublication? get _videoPublication;
|
||||||
|
|
||||||
TrackPublication? get _firstAudioPublication;
|
TrackPublication? get _firstAudioPublication;
|
||||||
|
|
||||||
Account? _userinfoMetadata;
|
Account? _userinfoMetadata;
|
||||||
@ -142,14 +126,15 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
|||||||
)
|
)
|
||||||
: NoContentWidget(
|
: NoContentWidget(
|
||||||
userinfo: _userinfoMetadata,
|
userinfo: _userinfoMetadata,
|
||||||
isFixed: widget.isFixed,
|
|
||||||
isSpeaking: widget.participant.isSpeaking,
|
isSpeaking: widget.participant.isSpeaking,
|
||||||
),
|
),
|
||||||
if (widget.showStatsLayer)
|
if (widget.showStatsLayer)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 30,
|
top: 30,
|
||||||
right: 30,
|
right: 30,
|
||||||
child: ParticipantStatsWidget(participant: widget.participant),
|
child: ParticipantStatsWidget(
|
||||||
|
participant: widget.participant,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
@ -158,7 +143,9 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ParticipantInfoWidget(
|
ParticipantInfoWidget(
|
||||||
title: widget.participant.name.isNotEmpty ? widget.participant.name : widget.participant.identity,
|
title: widget.participant.name.isNotEmpty
|
||||||
|
? widget.participant.name
|
||||||
|
: widget.participant.identity,
|
||||||
audioAvailable: _firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true,
|
audioAvailable: _firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true,
|
||||||
connectionQuality: widget.participant.connectionQuality,
|
connectionQuality: widget.participant.connectionQuality,
|
||||||
isScreenShare: widget.isScreenShare,
|
isScreenShare: widget.isScreenShare,
|
||||||
@ -172,6 +159,10 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticipantWidget> {
|
class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticipantWidget> {
|
||||||
|
@override
|
||||||
|
LocalTrackPublication<LocalVideoTrack>? get _videoPublication =>
|
||||||
|
widget.participant.videoTrackPublications.where((element) => element.sid == widget.videoTrack?.sid).firstOrNull;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
LocalTrackPublication<LocalAudioTrack>? get _firstAudioPublication =>
|
LocalTrackPublication<LocalAudioTrack>? get _firstAudioPublication =>
|
||||||
widget.participant.audioTrackPublications.firstOrNull;
|
widget.participant.audioTrackPublications.firstOrNull;
|
||||||
@ -181,6 +172,10 @@ class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticip
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RemoteParticipantWidgetState extends _ParticipantWidgetState<RemoteParticipantWidget> {
|
class _RemoteParticipantWidgetState extends _ParticipantWidgetState<RemoteParticipantWidget> {
|
||||||
|
@override
|
||||||
|
RemoteTrackPublication<RemoteVideoTrack>? get _videoPublication =>
|
||||||
|
widget.participant.videoTrackPublications.where((element) => element.sid == widget.videoTrack?.sid).firstOrNull;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
|
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
|
||||||
widget.participant.audioTrackPublications.firstOrNull;
|
widget.participant.audioTrackPublications.firstOrNull;
|
||||||
|
@ -21,12 +21,17 @@ class ParticipantMenu extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ParticipantMenuState extends State<ParticipantMenu> {
|
class _ParticipantMenuState extends State<ParticipantMenu> {
|
||||||
|
@override
|
||||||
RemoteTrackPublication<RemoteVideoTrack>? get _videoPublication =>
|
RemoteTrackPublication<RemoteVideoTrack>? get _videoPublication =>
|
||||||
widget.participant.videoTrackPublications.where((element) => element.sid == widget.videoTrack?.sid).firstOrNull;
|
widget.participant.videoTrackPublications.where((element) => element.sid == widget.videoTrack?.sid).firstOrNull;
|
||||||
|
|
||||||
|
@override
|
||||||
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
|
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
|
||||||
widget.participant.audioTrackPublications.firstOrNull;
|
widget.participant.audioTrackPublications.firstOrNull;
|
||||||
|
|
||||||
|
@override
|
||||||
|
VideoTrack? get _activeVideoTrack => widget.videoTrack;
|
||||||
|
|
||||||
void tookAction() {
|
void tookAction() {
|
||||||
if (Navigator.canPop(context)) {
|
if (Navigator.canPop(context)) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
@ -7,7 +7,6 @@ import 'package:solian/models/channel.dart';
|
|||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
|
|
||||||
class ChannelCallAction extends StatefulWidget {
|
class ChannelCallAction extends StatefulWidget {
|
||||||
final Call? call;
|
final Call? call;
|
||||||
@ -37,7 +36,9 @@ class _ChannelCallActionState extends State<ChannelCallAction> {
|
|||||||
var res = await auth.client!.post(uri);
|
var res = await auth.client!.post(uri);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _isSubmitting = false);
|
setState(() => _isSubmitting = false);
|
||||||
@ -57,7 +58,9 @@ class _ChannelCallActionState extends State<ChannelCallAction> {
|
|||||||
var res = await auth.client!.delete(uri);
|
var res = await auth.client!.delete(uri);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _isSubmitting = false);
|
setState(() => _isSubmitting = false);
|
||||||
|
@ -6,7 +6,6 @@ import 'package:solian/models/channel.dart';
|
|||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
|
|
||||||
class ChannelDeletion extends StatefulWidget {
|
class ChannelDeletion extends StatefulWidget {
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
@ -35,7 +34,9 @@ class _ChannelDeletionState extends State<ChannelDeletion> {
|
|||||||
);
|
);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
} else if (Navigator.canPop(context)) {
|
} else if (Navigator.canPop(context)) {
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
}
|
}
|
||||||
@ -57,7 +58,9 @@ class _ChannelDeletionState extends State<ChannelDeletion> {
|
|||||||
);
|
);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
} else if (Navigator.canPop(context)) {
|
} else if (Navigator.canPop(context)) {
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import 'package:solian/models/message.dart';
|
|||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
|
|
||||||
class ChatMessageDeletionDialog extends StatefulWidget {
|
class ChatMessageDeletionDialog extends StatefulWidget {
|
||||||
final String channel;
|
final String channel;
|
||||||
@ -35,7 +34,9 @@ class _ChatMessageDeletionDialogState extends State<ChatMessageDeletionDialog> {
|
|||||||
final res = await auth.client!.delete(uri);
|
final res = await auth.client!.delete(uri);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
setState(() => _isSubmitting = false);
|
setState(() => _isSubmitting = false);
|
||||||
} else {
|
} else {
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
|
@ -8,7 +8,6 @@ import 'package:solian/models/message.dart';
|
|||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
import 'package:solian/widgets/posts/attachment_editor.dart';
|
import 'package:solian/widgets/posts/attachment_editor.dart';
|
||||||
import 'package:badges/badges.dart' as badge;
|
import 'package:badges/badges.dart' as badge;
|
||||||
|
|
||||||
@ -65,7 +64,9 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
|||||||
var res = await Response.fromStream(await auth.client!.send(req));
|
var res = await Response.fromStream(await auth.client!.send(req));
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ class LayoutWrapper extends StatelessWidget {
|
|||||||
final Widget? child;
|
final Widget? child;
|
||||||
final Widget? floatingActionButton;
|
final Widget? floatingActionButton;
|
||||||
final List<Widget>? appBarActions;
|
final List<Widget>? appBarActions;
|
||||||
final bool noSafeArea;
|
final bool? noSafeArea;
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
const LayoutWrapper({
|
const LayoutWrapper({
|
||||||
@ -14,7 +14,7 @@ class LayoutWrapper extends StatelessWidget {
|
|||||||
required this.title,
|
required this.title,
|
||||||
this.floatingActionButton,
|
this.floatingActionButton,
|
||||||
this.appBarActions,
|
this.appBarActions,
|
||||||
this.noSafeArea = false,
|
this.noSafeArea,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -25,7 +25,7 @@ class LayoutWrapper extends StatelessWidget {
|
|||||||
appBar: AppBar(title: Text(title), actions: appBarActions),
|
appBar: AppBar(title: Text(title), actions: appBarActions),
|
||||||
floatingActionButton: floatingActionButton,
|
floatingActionButton: floatingActionButton,
|
||||||
drawer: const SolianNavigationDrawer(),
|
drawer: const SolianNavigationDrawer(),
|
||||||
body: noSafeArea ? content : SafeArea(child: content),
|
body: (noSafeArea ?? false) ? content : SafeArea(child: content),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
extension SolianCommonExtensions on BuildContext {
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,10 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:solian/router.dart';
|
|
||||||
import 'package:solian/widgets/common_wrapper.dart';
|
import 'package:solian/widgets/common_wrapper.dart';
|
||||||
import 'package:solian/widgets/navigation_drawer.dart';
|
import 'package:solian/widgets/navigation_drawer.dart';
|
||||||
|
|
||||||
class IndentWrapper extends LayoutWrapper {
|
class IndentWrapper extends LayoutWrapper {
|
||||||
final bool hideDrawer;
|
final bool? hideDrawer;
|
||||||
|
|
||||||
const IndentWrapper({
|
const IndentWrapper({
|
||||||
super.key,
|
super.key,
|
||||||
@ -12,8 +11,8 @@ class IndentWrapper extends LayoutWrapper {
|
|||||||
required super.title,
|
required super.title,
|
||||||
super.floatingActionButton,
|
super.floatingActionButton,
|
||||||
super.appBarActions,
|
super.appBarActions,
|
||||||
this.hideDrawer = false,
|
this.hideDrawer,
|
||||||
super.noSafeArea = false,
|
super.noSafeArea,
|
||||||
}) : super();
|
}) : super();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -21,17 +20,10 @@ class IndentWrapper extends LayoutWrapper {
|
|||||||
final content = child ?? Container();
|
final content = child ?? Container();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: Text(title), actions: appBarActions),
|
||||||
leading: hideDrawer ? IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
|
||||||
onPressed: () => router.pop(),
|
|
||||||
) : null,
|
|
||||||
title: Text(title),
|
|
||||||
actions: appBarActions,
|
|
||||||
),
|
|
||||||
floatingActionButton: floatingActionButton,
|
floatingActionButton: floatingActionButton,
|
||||||
drawer: const SolianNavigationDrawer(),
|
drawer: (hideDrawer ?? false) ? null : const SolianNavigationDrawer(),
|
||||||
body: noSafeArea ? content : SafeArea(child: content),
|
body: (noSafeArea ?? false) ? content : SafeArea(child: content),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ class _SolianNavigationDrawerState extends State<SolianNavigationDrawer> {
|
|||||||
Image.asset("assets/logo.png", width: 26, height: 26),
|
Image.asset("assets/logo.png", width: 26, height: 26),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context)!.appName,
|
AppLocalizations.of(context)!.solian,
|
||||||
style: const TextStyle(fontWeight: FontWeight.w900),
|
style: const TextStyle(fontWeight: FontWeight.w900),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -12,7 +12,6 @@ import 'package:solian/models/post.dart';
|
|||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
|
|
||||||
class AttachmentEditor extends StatefulWidget {
|
class AttachmentEditor extends StatefulWidget {
|
||||||
final String provider;
|
final String provider;
|
||||||
@ -68,7 +67,9 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
|||||||
try {
|
try {
|
||||||
await uploadAttachment(file, hashcode);
|
await uploadAttachment(file, hashcode);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
context.showErrorDialog(err);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $err")),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isSubmitting = false);
|
setState(() => _isSubmitting = false);
|
||||||
}
|
}
|
||||||
@ -93,7 +94,9 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
|||||||
try {
|
try {
|
||||||
await uploadAttachment(file, hashcode);
|
await uploadAttachment(file, hashcode);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
context.showErrorDialog(err);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $err")),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isSubmitting = false);
|
setState(() => _isSubmitting = false);
|
||||||
}
|
}
|
||||||
@ -130,7 +133,9 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
|||||||
widget.onUpdate(_attachments);
|
widget.onUpdate(_attachments);
|
||||||
} else {
|
} else {
|
||||||
final err = utf8.decode(await res.stream.toBytes());
|
final err = utf8.decode(await res.stream.toBytes());
|
||||||
context.showErrorDialog(err);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $err")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
setState(() => _isSubmitting = false);
|
setState(() => _isSubmitting = false);
|
||||||
}
|
}
|
||||||
|
@ -62,9 +62,7 @@ class _AttachmentItemState extends State<AttachmentItem> {
|
|||||||
: Positioned(
|
: Positioned(
|
||||||
right: 12,
|
right: 12,
|
||||||
bottom: 8,
|
bottom: 8,
|
||||||
child: Material(
|
|
||||||
child: Chip(label: Text(widget.badge!)),
|
child: Chip(label: Text(widget.badge!)),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -6,7 +6,6 @@ import 'package:solian/models/post.dart';
|
|||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
|
|
||||||
class ItemDeletionDialog extends StatefulWidget {
|
class ItemDeletionDialog extends StatefulWidget {
|
||||||
final Post item;
|
final Post item;
|
||||||
@ -36,7 +35,9 @@ class _ItemDeletionDialogState extends State<ItemDeletionDialog> {
|
|||||||
final res = await auth.client!.delete(uri);
|
final res = await auth.client!.delete(uri);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
setState(() => _isSubmitting = false);
|
setState(() => _isSubmitting = false);
|
||||||
} else {
|
} else {
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
|
@ -7,7 +7,6 @@ import 'package:solian/models/reaction.dart';
|
|||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:solian/widgets/exts.dart';
|
|
||||||
|
|
||||||
Future<void> doReact(
|
Future<void> doReact(
|
||||||
String dataset,
|
String dataset,
|
||||||
@ -52,7 +51,9 @@ Future<void> doReact(
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final message = utf8.decode(res.bodyBytes);
|
final message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Navigator.canPop(context)) {
|
if (Navigator.canPop(context)) {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
|
@ -169,14 +169,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.0"
|
version: "7.0.0"
|
||||||
draggable_float_widget:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: draggable_float_widget
|
|
||||||
sha256: "075675c56f6b2bfc9f972a3937dc1b59838489a312f75fe7e90ba6844a84dce4"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.0"
|
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -66,7 +66,6 @@ dependencies:
|
|||||||
flutter_background: ^1.2.0
|
flutter_background: ^1.2.0
|
||||||
wakelock_plus: ^1.2.4
|
wakelock_plus: ^1.2.4
|
||||||
flutter_local_notifications: ^17.1.0
|
flutter_local_notifications: ^17.1.0
|
||||||
draggable_float_widget: ^0.1.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user