Compare commits

...

33 Commits

Author SHA1 Message Date
cc9081b011 🐛 Fix image loading issue on web 2024-08-24 11:47:40 +08:00
14e8f7b775 🐛 Bug fixes and optimization 2024-08-23 23:16:41 +08:00
a70e6c7118 Typing indicator 2024-08-23 22:43:04 +08:00
48ca885a2c 🚀 Launch 1.2.1+22 2024-08-22 01:06:10 +08:00
09cb340a9d 🐛 Fix personalize page issue 2024-08-22 00:42:17 +08:00
b6ebd6bef6 🐛 Drawer will expand on mobile device 2024-08-21 20:55:22 +08:00
2ec25fd1a2 Drawer tooltip on collapse mode 2024-08-21 19:35:29 +08:00
bc99865ba8 💫 Animated collapsible sidebar 2024-08-21 19:11:27 +08:00
f834351ce2 Basis collapse sidebar 2024-08-21 17:00:59 +08:00
0f1a02f65b 🐛 Try to fix protocol handler issue on android 2024-08-21 16:02:00 +08:00
6ad0a34645 Call on large screen able to full screen 2024-08-21 15:57:45 +08:00
fdc71475fc 💄 Optimize message hint 2024-08-21 15:45:55 +08:00
047defebd1 🥅 Better request failed exceptions 2024-08-21 15:39:29 +08:00
6148e889aa 🥅 Better unauthorized exceptions 2024-08-21 15:25:50 +08:00
1d7affcd84 🐛 Bug fixes 2024-08-21 13:14:40 +08:00
cc1e0599aa 🐛 Fix link expand match markdown link 2024-08-21 10:06:05 +08:00
221b97901f 💄 Optimize uploader 2024-08-21 10:01:09 +08:00
498bb0e5fb Run upload chunks at the same time (max 3) 2024-08-21 09:33:34 +08:00
aa94dfcfe0 Multipart upload 2024-08-21 01:53:16 +08:00
65d9253876 🐛 Fix svg site icon cause invalid image data 2024-08-21 00:48:51 +08:00
3ac510c4b1 🐛 Bug fixes 2024-08-20 01:19:18 +08:00
253cd1ecbd Call in same screen on large screen 2024-08-20 01:10:15 +08:00
c82c48dfec 🐛 Fix attachments padding 2024-08-19 22:45:22 +08:00
433beec2dd 💄 Optimize large screen ux 2024-08-19 22:38:36 +08:00
3a1e7537dd 🐛 Fix alignment issue 2024-08-19 22:25:49 +08:00
9170ae6be7 💄 Line up attachments & expansion of link 2024-08-19 22:25:17 +08:00
a5ee5b7f09 💄 Better attachment layout 2024-08-19 22:13:25 +08:00
32e6658f3d Better link expand layout on large screen 2024-08-19 20:13:08 +08:00
e45d9b39d5 Post link expand
 Cache link expansion image
2024-08-19 19:56:44 +08:00
cf1cfecb08 Link expand 2024-08-19 19:36:01 +08:00
95ea3e558f 🚀 Launch 1.2.1+18 2024-08-19 09:43:25 +08:00
0006a94632 🐛 Fix local db old data cause crash 2024-08-19 09:19:29 +08:00
7ea18dbe12 💄 Update styles 2024-08-19 01:54:32 +08:00
54 changed files with 2023 additions and 800 deletions

View File

@ -58,6 +58,13 @@
<data android:host="sn.solsynth.dev" />
<data android:scheme="https" />
<data android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="solink" />
</intent-filter>

View File

@ -227,6 +227,12 @@ PODS:
- livekit_client (2.2.4):
- Flutter
- WebRTC-SDK (= 125.6422.04)
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_native_event_loop (1.0.0):
- Flutter
- media_kit_video (0.0.1):
- Flutter
- nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0)
@ -248,6 +254,8 @@ PODS:
- PromisesObjC (= 2.4.0)
- protocol_handler_ios (0.0.1):
- Flutter
- screen_brightness_ios (0.1.0):
- Flutter
- SDWebImage (5.19.6):
- SDWebImage/Core (= 5.19.6)
- SDWebImage/Core (5.19.6)
@ -263,9 +271,8 @@ PODS:
- TOCropViewController (2.7.4)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- volume_controller (0.0.1):
- Flutter
- FlutterMacOS
- wakelock_plus (0.0.1):
- Flutter
- WebRTC-SDK (125.6422.04)
@ -287,17 +294,21 @@ DEPENDENCIES:
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
@ -362,6 +373,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_picker_ios/ios"
livekit_client:
:path: ".symlinks/plugins/livekit_client/ios"
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop:
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
pasteboard:
@ -374,6 +391,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
protocol_handler_ios:
:path: ".symlinks/plugins/protocol_handler_ios/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@ -382,8 +401,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
volume_controller:
:path: ".symlinks/plugins/volume_controller/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
@ -423,6 +442,9 @@ SPEC CHECKSUMS:
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
livekit_client: d079c5f040d4bf2b80440ff0ae997725a183e4bc
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
nanopb: 438bc412db1928dac798aa6fd75726007be04262
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
@ -432,6 +454,7 @@ SPEC CHECKSUMS:
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: a79252b60f4678812d94316c91da69ec83089c9f
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
@ -439,7 +462,7 @@ SPEC CHECKSUMS:
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3

View File

@ -254,6 +254,7 @@
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
B1CDA9DD5B638A2BB88053CB /* [CP] Check Pods Manifest.lock */,
7356FAC42C72724B0051A465 /* [Crashlytics] Clear dSYM */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
@ -263,7 +264,7 @@
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
287A33C298CA352A7E7F32A4 /* [CP] Embed Pods Frameworks */,
0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */,
1A9FD6BE5DEE99CDA7399504 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */,
1A9FD6BE5DEE99CDA7399504 /* [Crashlytics] Upload dSYM */,
);
buildRules = (
);
@ -366,7 +367,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
1A9FD6BE5DEE99CDA7399504 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
1A9FD6BE5DEE99CDA7399504 /* [Crashlytics] Upload dSYM */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -375,14 +376,14 @@
);
inputPaths = (
);
name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\"";
name = "[Crashlytics] Upload dSYM";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nPATH=${PATH}:$FLUTTER_ROOT/bin:$HOME/.pub-cache/bin\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=$PODS_ROOT/FirebaseCrashlytics/upload-symbols --platform=ios --apple-project-path=${SRCROOT} --env-platform-name=${PLATFORM_NAME} --env-configuration=${CONFIGURATION} --env-project-dir=${PROJECT_DIR} --env-built-products-dir=${BUILT_PRODUCTS_DIR} --env-dwarf-dsym-folder-path=${DWARF_DSYM_FOLDER_PATH} --env-dwarf-dsym-file-name=${DWARF_DSYM_FILE_NAME} --env-infoplist-path=${INFOPLIST_PATH} --default-config=default\n";
shellScript = "\n#!/bin/bash\nsleep 1 # Without this, there seems a chance that the script runs before dSYM generation is finished \n$PODS_ROOT/FirebaseCrashlytics/upload-symbols -gsp $PROJECT_DIR/Runner/GoogleService-Info.plist -p ios $DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME\n";
};
259653AE41D478F4C6BAE9B2 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
@ -439,6 +440,24 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
7356FAC42C72724B0051A465 /* [Crashlytics] Clear dSYM */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[Crashlytics] Clear dSYM";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nrm -rf $DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME\n";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@ -452,7 +471,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
B1CDA9DD5B638A2BB88053CB /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;

View File

@ -112,6 +112,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
label: 'bsPreparingData',
action: () async {
final AuthProvider auth = Get.find();
try {
await Future.wait([
Get.find<StickerProvider>().refreshAvailableStickers(),
if (auth.isAuthorized.isTrue)
@ -121,6 +122,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
if (auth.isAuthorized.isTrue)
Get.find<RealmProvider>().refreshAvailableRealms(),
]);
} catch (e) {
context.showErrorDialog(e);
}
},
),
(

View File

@ -0,0 +1,10 @@
import 'package:get/get.dart';
class RequestException implements Exception {
final Response data;
const RequestException(this.data);
@override
String toString() => 'Request failed ${data.statusCode}: ${data.bodyString}';
}

View File

@ -0,0 +1,6 @@
class UnauthorizedException implements Exception {
const UnauthorizedException();
@override
String toString() => 'Unauthorized';
}

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
extension SolianExtenions on BuildContext {
void showSnackbar(String content, {SnackBarAction? action}) {
@ -48,15 +50,48 @@ extension SolianExtenions on BuildContext {
}
Future<void> showErrorDialog(dynamic exception) {
var stack = StackTrace.current;
var stackTrace = '$stack';
Widget content = Text(exception.toString().capitalize!);
if (exception is UnauthorizedException) {
content = Text('errorHappenedUnauthorized'.tr);
}
if (exception is RequestException) {
String overall;
switch (exception.data.statusCode) {
case 400:
overall = 'errorHappenedRequestBad'.tr;
break;
case 401:
overall = 'errorHappenedUnauthorized'.tr;
break;
case 403:
overall = 'errorHappenedRequestForbidden'.tr;
break;
case 404:
overall = 'errorHappenedRequestNotFound'.tr;
break;
case null:
overall = 'errorHappenedRequestConnection'.tr;
break;
default:
overall = 'errorHappenedRequestUnknown'.tr;
break;
}
if (exception.data.statusCode != null) {
content = Text(
'$overall\n\n(${exception.data.statusCode}) ${exception.data.bodyString}',
);
} else {
content = Text(overall);
}
}
return showDialog<void>(
useRootNavigator: true,
context: this,
builder: (ctx) => AlertDialog(
title: Text('errorHappened'.tr),
content: Text('${exception.toString().capitalize!}\n\nStack Trace: $stackTrace'),
content: content,
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),

View File

@ -6,12 +6,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:get/get.dart';
import 'package:go_router/go_router.dart';
import 'package:media_kit/media_kit.dart';
import 'package:protocol_handler/protocol_handler.dart';
import 'package:provider/provider.dart';
import 'package:solian/bootstrapper.dart';
import 'package:solian/firebase_options.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/link_expander.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart';
@ -31,6 +33,7 @@ import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
await Future.wait([
_initializeFirebase(),
@ -126,5 +129,6 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => RealmProvider());
Get.lazyPut(() => ChatCallProvider());
Get.lazyPut(() => AttachmentUploaderController());
Get.lazyPut(() => LinkExpandController());
}
}

View File

@ -1,5 +1,30 @@
import 'package:solian/models/account.dart';
class AttachmentPlaceholder {
int chunkCount;
int chunkSize;
Attachment meta;
AttachmentPlaceholder({
required this.chunkCount,
required this.chunkSize,
required this.meta,
});
factory AttachmentPlaceholder.fromJson(Map<String, dynamic> json) =>
AttachmentPlaceholder(
chunkCount: json['chunk_count'],
chunkSize: json['chunk_size'],
meta: Attachment.fromJson(json['meta']),
);
Map<String, dynamic> toJson() => {
'chunk_count': chunkCount,
'chunk_size': chunkSize,
'meta': meta.toJson(),
};
}
class Attachment {
int id;
DateTime createdAt;
@ -14,7 +39,9 @@ class Attachment {
String hash;
int destination;
bool isAnalyzed;
bool isUploaded;
Map<String, dynamic>? metadata;
Map<String, dynamic>? fileChunks;
bool isMature;
Account? account;
int? accountId;
@ -33,7 +60,9 @@ class Attachment {
required this.hash,
required this.destination,
required this.isAnalyzed,
required this.isUploaded,
required this.metadata,
required this.fileChunks,
required this.isMature,
required this.account,
required this.accountId,
@ -55,7 +84,9 @@ class Attachment {
hash: json['hash'],
destination: json['destination'],
isAnalyzed: json['is_analyzed'],
isUploaded: json['is_uploaded'],
metadata: json['metadata'],
fileChunks: json['file_chunks'],
isMature: json['is_mature'],
account:
json['account'] != null ? Account.fromJson(json['account']) : null,
@ -76,7 +107,9 @@ class Attachment {
'hash': hash,
'destination': destination,
'is_analyzed': isAnalyzed,
'is_uploaded': isUploaded,
'metadata': metadata,
'file_chunks': fileChunks,
'is_mature': isMature,
'account': account?.toJson(),
'account_id': accountId,

View File

@ -82,7 +82,7 @@ class EventMessageBody {
text: json['text'] ?? '',
algorithm: json['algorithm'] ?? 'plain',
attachments: json['attachments'] != null
? List<String>.from(json['attachments'].map((x) => x))
? List<String>.from(json['attachments']?.whereType<String>())
: null,
quoteEvent: json['quote_event'],
relatedEvent: json['related_event'],

65
lib/models/link.dart Normal file
View File

@ -0,0 +1,65 @@
class LinkMeta {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String entryId;
String? icon;
String url;
String? title;
String? image;
String? video;
String? audio;
String? description;
String? siteName;
LinkMeta({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.entryId,
required this.icon,
required this.url,
required this.title,
required this.image,
required this.video,
required this.audio,
required this.description,
required this.siteName,
});
factory LinkMeta.fromJson(Map<String, dynamic> json) => LinkMeta(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
entryId: json['entry_id'],
icon: json['icon'],
url: json['url'],
title: json['title'],
image: json['image'],
video: json['video'],
audio: json['audio'],
description: json['description'],
siteName: json['site_name'],
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'entry_id': entryId,
'icon': icon,
'url': url,
'title': title,
'image': image,
'video': video,
'audio': audio,
'description': description,
'site_name': siteName,
};
}

View File

@ -1,22 +1,26 @@
class NetworkPackage {
String method;
String? endpoint;
String? message;
Map<String, dynamic>? payload;
NetworkPackage({
required this.method,
this.endpoint,
this.message,
this.payload,
});
factory NetworkPackage.fromJson(Map<String, dynamic> json) => NetworkPackage(
method: json['w'],
endpoint: json['e'],
message: json['m'],
payload: json['p'],
);
Map<String, dynamic> toJson() => {
'w': method,
'e': endpoint,
'm': message,
'p': payload,
};

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/models/account_status.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
@ -33,15 +35,14 @@ class StatusProvider extends GetConnect {
Future<Response> getCurrentStatus() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('auth');
return await client.get('/users/me/status');
}
Future<Response> getSomeoneStatus(String name) =>
get('/users/$name/status');
Future<Response> getSomeoneStatus(String name) => get('/users/$name/status');
Future<Response> setStatus(
String type,
@ -53,7 +54,7 @@ class StatusProvider extends GetConnect {
DateTime? clearAt,
}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('auth');
@ -74,7 +75,7 @@ class StatusProvider extends GetConnect {
}
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -82,13 +83,13 @@ class StatusProvider extends GetConnect {
Future<Response> clearStatus() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('auth');
final resp = await client.delete('/users/me/status');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;

View File

@ -1,24 +1,27 @@
import 'dart:async';
import 'dart:io';
import 'dart:collection';
import 'dart:typed_data';
import 'package:cross_file/cross_file.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' show basename;
import 'package:solian/models/attachment.dart';
import 'package:solian/providers/content/attachment.dart';
class AttachmentUploadTask {
File file;
String usage;
XFile file;
String pool;
Map<String, dynamic>? metadata;
Map<String, int>? chunkFiles;
double progress = 0;
double? progress;
bool isUploading = false;
bool isCompleted = false;
dynamic error;
AttachmentUploadTask({
required this.file,
required this.usage,
required this.pool,
this.metadata,
});
}
@ -73,23 +76,26 @@ class AttachmentUploaderController extends GetxController {
_startProgressSyncTimer();
queueOfUpload[queueIndex].isUploading = true;
queueOfUpload[queueIndex].progress = 0;
final task = queueOfUpload[queueIndex];
final result = await _rawUploadAttachment(
await task.file.readAsBytes(),
task.file.path,
task.usage,
try {
final result = await _chunkedUploadAttachment(
task.file,
task.pool,
null,
onProgress: (value) {
queueOfUpload[queueIndex].progress = value;
_progressOfUpload = value;
},
onError: (err) {
queueOfUpload[queueIndex].error = err;
queueOfUpload[queueIndex].isUploading = false;
onData: (_) {},
onProgress: (progress) {
queueOfUpload[queueIndex].progress = progress;
_progressOfUpload = progress;
},
);
return result;
} catch (err) {
queueOfUpload[queueIndex].error = err;
queueOfUpload[queueIndex].isUploading = false;
} finally {
_progressOfUpload = 1;
if (queueOfUpload[queueIndex].error == null) {
queueOfUpload.removeAt(queueIndex);
}
@ -97,8 +103,9 @@ class AttachmentUploaderController extends GetxController {
_syncProgress();
isUploading.value = false;
}
return result;
return null;
}
Future<void> performUploadQueue({
@ -115,24 +122,26 @@ class AttachmentUploaderController extends GetxController {
}
queueOfUpload[idx].isUploading = true;
queueOfUpload[idx].progress = 0;
final task = queueOfUpload[idx];
final result = await _rawUploadAttachment(
await task.file.readAsBytes(),
task.file.path,
task.usage,
try {
final result = await _chunkedUploadAttachment(
task.file,
task.pool,
null,
onProgress: (value) {
queueOfUpload[idx].progress = value;
_progressOfUpload = (idx + value) / queueOfUpload.length;
},
onError: (err) {
queueOfUpload[idx].error = err;
queueOfUpload[idx].isUploading = false;
onData: (_) {},
onProgress: (progress) {
queueOfUpload[idx].progress = progress;
},
);
_progressOfUpload = (idx + 1) / queueOfUpload.length;
if (result != null) onData(result);
} catch (err) {
queueOfUpload[idx].error = err;
queueOfUpload[idx].isUploading = false;
} finally {
_progressOfUpload = (idx + 1) / queueOfUpload.length;
}
queueOfUpload[idx].isUploading = false;
queueOfUpload[idx].isCompleted = true;
@ -145,69 +154,94 @@ class AttachmentUploaderController extends GetxController {
isUploading.value = false;
}
Future<void> uploadAttachmentWithCallback(
Uint8List data,
String path,
String pool,
Map<String, dynamic>? metadata,
Function(Attachment?) callback,
) async {
if (isUploading.value) throw Exception('uploading blocked');
isUploading.value = true;
final result = await _rawUploadAttachment(
data,
path,
pool,
metadata,
onProgress: (progress) {
progressOfUpload.value = progress;
},
);
isUploading.value = false;
callback(result);
}
Future<Attachment?> uploadAttachment(
Future<Attachment?> uploadAttachmentFromData(
Uint8List data,
String path,
String pool,
Map<String, dynamic>? metadata,
) async {
if (isUploading.value) throw Exception('uploading blocked');
isUploading.value = true;
final result = await _rawUploadAttachment(
data,
path,
pool,
metadata,
onProgress: (progress) {
progressOfUpload.value = progress;
},
);
isUploading.value = false;
return result;
}
Future<Attachment?> _rawUploadAttachment(
Uint8List data, String path, String pool, Map<String, dynamic>? metadata,
{Function(double)? onProgress, Function(dynamic err)? onError}) async {
final AttachmentProvider provider = Get.find();
final AttachmentProvider attach = Get.find();
try {
final result = await provider.createAttachment(
final result = await attach.createAttachmentDirectly(
data,
path,
pool,
metadata,
onProgress: onProgress,
);
return result;
} catch (err) {
if (onError != null) {
onError(err);
}
} catch (_) {
return null;
} finally {
isUploading.value = false;
}
}
Future<Attachment?> _chunkedUploadAttachment(
XFile file,
String pool,
Map<String, dynamic>? metadata, {
required Function(AttachmentPlaceholder) onData,
required Function(double) onProgress,
}) async {
final AttachmentProvider attach = Get.find();
final holder = await attach.createAttachmentMultipartPlaceholder(
await file.length(),
file.path,
pool,
metadata,
);
onData(holder);
onProgress(0);
final filename = basename(file.path);
final chunks = holder.meta.fileChunks ?? {};
var currentTask = 0;
final queue = Queue<Future<void>>();
final activeTasks = <Future<void>>[];
for (final entry in chunks.entries) {
queue.add(() async {
final beginCursor = entry.value * holder.chunkSize;
final endCursor = (entry.value + 1) * holder.chunkSize;
final data = Uint8List.fromList(await file
.openRead(beginCursor, endCursor)
.expand((chunk) => chunk)
.toList());
final out = await attach.uploadAttachmentMultipartChunk(
data,
filename,
holder.meta.rid,
entry.key,
);
holder.meta = out;
currentTask++;
onProgress(currentTask / chunks.length);
onData(holder);
}());
}
while (queue.isNotEmpty || activeTasks.isNotEmpty) {
while (activeTasks.length < 3 && queue.isNotEmpty) {
final task = queue.removeFirst();
activeTasks.add(task);
task.then((_) => activeTasks.remove(task));
}
if (activeTasks.isNotEmpty) {
await Future.any(activeTasks);
}
}
return holder.meta;
}
}

View File

@ -7,6 +7,8 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get/get.dart';
import 'package:get/get_connect/http/src/request/request.dart';
import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart';
@ -81,7 +83,7 @@ class AuthProvider extends GetConnect {
'grant_type': 'refresh_token',
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
credentials = TokenSet(
accessToken: resp.body['access_token'],
@ -128,7 +130,7 @@ class AuthProvider extends GetConnect {
}
Future<void> ensureCredentials() async {
if (isAuthorized.isFalse) throw Exception('unauthorized');
if (isAuthorized.isFalse) throw const UnauthorizedException();
if (credentials == null) await loadCredentials();
if (credentials!.isExpired) {
@ -158,7 +160,7 @@ class AuthProvider extends GetConnect {
'password': password,
});
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
} else if (resp.body['is_finished'] == false) {
throw RiskyAuthenticateException(resp.body['ticket']['id']);
}
@ -218,7 +220,7 @@ class AuthProvider extends GetConnect {
final client = configureClient('auth');
final resp = await client.get('/users/me');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
userProfile.value = resp.body;

View File

@ -2,6 +2,8 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:solian/models/call.dart';
@ -17,6 +19,10 @@ class ChatCallProvider extends GetxController {
RxBool isReady = false.obs;
RxBool isMounted = false.obs;
RxBool isInitialized = false.obs;
RxBool isBusy = false.obs;
RxString lastDuration = '00:00:00'.obs;
Timer? lastDurationUpdateTimer;
String? token;
String? endpoint;
@ -38,6 +44,34 @@ class ChatCallProvider extends GetxController {
RxList<ParticipantTrack> participantTracks = RxList.empty(growable: true);
Rx<ParticipantTrack?> focusTrack = Rx(null);
void _updateDuration() {
if (current.value == null) {
lastDuration.value = '00:00:00';
return;
}
Duration duration = DateTime.now().difference(current.value!.createdAt);
String twoDigits(int n) => n.toString().padLeft(2, '0');
String formattedTime = '${twoDigits(duration.inHours)}:'
'${twoDigits(duration.inMinutes.remainder(60))}:'
'${twoDigits(duration.inSeconds.remainder(60))}';
lastDuration.value = formattedTime;
}
void enableDurationUpdater() {
_updateDuration();
lastDurationUpdateTimer = Timer.periodic(
const Duration(seconds: 1),
(_) => _updateDuration(),
);
}
void disableDurationUpdater() {
lastDurationUpdateTimer?.cancel();
lastDurationUpdateTimer = null;
}
Future<void> checkPermissions() async {
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
return;
@ -56,7 +90,7 @@ class ChatCallProvider extends GetxController {
Future<(String, String)> getRoomToken() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
@ -69,7 +103,7 @@ class ChatCallProvider extends GetxController {
endpoint = 'wss://${resp.body['endpoint']}';
return (token!, endpoint!);
} else {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
}
@ -119,8 +153,6 @@ class ChatCallProvider extends GetxController {
void joinRoom(String url, String token) async {
if (isMounted.value) {
return;
} else {
isMounted.value = true;
}
try {
@ -134,6 +166,8 @@ class ChatCallProvider extends GetxController {
);
} catch (e) {
rethrow;
} finally {
isMounted.value = true;
}
}
@ -165,6 +199,7 @@ class ChatCallProvider extends GetxController {
Hardware.instance.setSpeakerphoneOn(true);
}
isBusy.value = false;
isInitialized.value = true;
}
@ -367,6 +402,7 @@ class ChatCallProvider extends GetxController {
}
void disposeRoom() {
isBusy.value = false;
isMounted.value = false;
isInitialized.value = false;
current.value = null;

View File

@ -2,12 +2,13 @@ import 'dart:convert';
import 'dart:typed_data';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:path/path.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
import 'package:dio/dio.dart' as dio;
class AttachmentProvider extends GetConnect {
static Map<String, String> mimetypeOverrides = {
@ -83,16 +84,21 @@ class AttachmentProvider extends GetConnect {
return null;
}
Future<Attachment> createAttachment(
Uint8List data, String path, String pool, Map<String, dynamic>? metadata,
{Function(double)? onProgress}) async {
Future<Attachment> createAttachmentDirectly(
Uint8List data,
String path,
String pool,
Map<String, dynamic>? metadata,
) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
await auth.ensureCredentials();
final client = auth.configureClient(
'uc',
timeout: const Duration(minutes: 3),
);
final filePayload =
dio.MultipartFile.fromBytes(data, filename: basename(path));
final filePayload = MultipartFile(data, filename: basename(path));
final fileAlt = basename(path).contains('.')
? basename(path).substring(0, basename(path).lastIndexOf('.'))
: basename(path);
@ -105,30 +111,82 @@ class AttachmentProvider extends GetConnect {
if (mimetypeOverrides.keys.contains(fileExt)) {
mimetypeOverride = mimetypeOverrides[fileExt];
}
final payload = dio.FormData.fromMap({
final payload = FormData({
'alt': fileAlt,
'file': filePayload,
'pool': pool,
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
'metadata': jsonEncode(metadata),
});
final resp = await dio.Dio(
dio.BaseOptions(
baseUrl: ServiceFinder.buildUrl('files', null),
headers: {'Authorization': 'Bearer ${auth.credentials!.accessToken}'},
),
).post(
'/attachments',
data: payload,
onSendProgress: (count, total) {
if (onProgress != null) onProgress(count / total);
},
);
final resp = await client.post('/attachments', payload);
if (resp.statusCode != 200) {
throw Exception(resp.data);
throw RequestException(resp);
}
return Attachment.fromJson(resp.data);
return Attachment.fromJson(resp.body);
}
Future<AttachmentPlaceholder> createAttachmentMultipartPlaceholder(
int size,
String path,
String pool,
Map<String, dynamic>? metadata,
) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('uc');
final fileAlt = basename(path).contains('.')
? basename(path).substring(0, basename(path).lastIndexOf('.'))
: basename(path);
final fileExt = basename(path)
.substring(basename(path).lastIndexOf('.') + 1)
.toLowerCase();
// Override for some files cannot be detected mimetype by server-side
String? mimetypeOverride;
if (mimetypeOverrides.keys.contains(fileExt)) {
mimetypeOverride = mimetypeOverrides[fileExt];
}
final resp = await client.post('/attachments/multipart', {
'alt': fileAlt,
'name': basename(path),
'size': size,
'pool': pool,
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
'metadata': metadata,
});
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return AttachmentPlaceholder.fromJson(resp.body);
}
Future<Attachment> uploadAttachmentMultipartChunk(
Uint8List data,
String name,
String rid,
String cid,
) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient(
'uc',
timeout: const Duration(minutes: 3),
);
final payload = FormData({
'file': MultipartFile(data, filename: name),
});
final resp = await client.post('/attachments/multipart/$rid/$cid', payload);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Attachment.fromJson(resp.body);
}
Future<Response> updateAttachment(
@ -137,7 +195,7 @@ class AttachmentProvider extends GetConnect {
bool isMature = false,
}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('files');
@ -147,7 +205,7 @@ class AttachmentProvider extends GetConnect {
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -155,13 +213,13 @@ class AttachmentProvider extends GetConnect {
Future<Response> deleteAttachment(int id) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('files');
var resp = await client.delete('/attachments/$id');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/widgets/account/relative_select.dart';
@ -16,7 +18,7 @@ class ChannelProvider extends GetxController {
Future<void> refreshAvailableChannel() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
isLoading.value = true;
final resp = await listAvailableChannel();
@ -29,13 +31,13 @@ class ChannelProvider extends GetxController {
Future<Response> getChannel(String alias, {String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
final resp = await client.get('/channels/$realm/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -44,13 +46,13 @@ class ChannelProvider extends GetxController {
Future<Response> getMyChannelProfile(String alias,
{String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
final resp = await client.get('/channels/$realm/$alias/me');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -59,7 +61,7 @@ class ChannelProvider extends GetxController {
Future<Response?> getChannelOngoingCall(String alias,
{String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
@ -67,7 +69,7 @@ class ChannelProvider extends GetxController {
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -75,13 +77,13 @@ class ChannelProvider extends GetxController {
Future<Response> listChannel({String scope = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
final resp = await client.get('/channels/$scope');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -89,13 +91,13 @@ class ChannelProvider extends GetxController {
Future<Response> listAvailableChannel({String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
final resp = await client.get('/channels/$realm/me/available');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -103,13 +105,13 @@ class ChannelProvider extends GetxController {
Future<Response> createChannel(String scope, dynamic payload) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
final resp = await client.post('/channels/$scope', payload);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -118,7 +120,7 @@ class ChannelProvider extends GetxController {
Future<Response?> createDirectChannel(
BuildContext context, String scope) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final related = await showModalBottomSheet(
useRootNavigator: true,
@ -141,7 +143,7 @@ class ChannelProvider extends GetxController {
'is_encrypted': false,
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -149,13 +151,13 @@ class ChannelProvider extends GetxController {
Future<Response> updateChannel(String scope, int id, dynamic payload) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
final resp = await client.put('/channels/$scope/$id', payload);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;

View File

@ -1,4 +1,6 @@
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
@ -28,7 +30,7 @@ class PostProvider extends GetConnect {
: '/recommendations/$channel?${queries.join('&')}',
);
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
}
return resp;
@ -36,7 +38,7 @@ class PostProvider extends GetConnect {
Future<Response> listDraft(int page) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final queries = [
'take=${10}',
@ -45,7 +47,7 @@ class PostProvider extends GetConnect {
final client = auth.configureClient('interactive');
final resp = await client.get('/posts/drafts?${queries.join('&')}');
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
}
return resp;
@ -63,7 +65,7 @@ class PostProvider extends GetConnect {
];
final resp = await get('/posts?${queries.join('&')}');
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
}
return resp;
@ -72,7 +74,7 @@ class PostProvider extends GetConnect {
Future<Response> listPostReplies(String alias, int page) async {
final resp = await get('/posts/$alias/replies?take=${10}&offset=$page');
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
}
return resp;
@ -81,7 +83,7 @@ class PostProvider extends GetConnect {
Future<Response> getPost(String alias) async {
final resp = await get('/posts/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
}
return resp;
@ -90,7 +92,7 @@ class PostProvider extends GetConnect {
Future<Response> getArticle(String alias) async {
final resp = await get('/articles/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
}
return resp;

View File

@ -1,4 +1,6 @@
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/providers/auth.dart';
@ -8,7 +10,7 @@ class RealmProvider extends GetxController {
Future<void> refreshAvailableRealms() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
isLoading.value = true;
final resp = await listAvailableRealm();
@ -21,13 +23,13 @@ class RealmProvider extends GetxController {
Future<Response> getRealm(String alias) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('auth');
final resp = await client.get('/realms/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -35,13 +37,13 @@ class RealmProvider extends GetxController {
Future<Response> listAvailableRealm() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('auth');
final resp = await client.get('/realms/me/available');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;

View File

@ -0,0 +1,25 @@
import 'dart:convert';
import 'dart:developer';
import 'package:get/get.dart';
import 'package:solian/models/link.dart';
import 'package:solian/services.dart';
class LinkExpandController extends GetxController {
final Map<String, LinkMeta?> _cachedResponse = {};
Future<LinkMeta?> expandLink(String url) async {
final target = utf8.fuse(base64).encode(url);
if (_cachedResponse.containsKey(target)) return _cachedResponse[target];
final client = ServiceFinder.configureClient('dealer');
final resp = await client.get('/api/links/$target');
if (resp.statusCode != 200) {
log('Unable to expand link ($url), status: ${resp.statusCode}, response: ${resp.body}');
_cachedResponse[target] = null;
return null;
}
final result = LinkMeta.fromJson(resp.body);
_cachedResponse[target] = result;
return result;
}
}

View File

@ -1,5 +1,6 @@
import 'package:floor/floor.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/models/pagination.dart';
@ -29,7 +30,7 @@ Future<Event?> getRemoteEvent(int id, Channel channel, String scope) async {
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return Event.fromJson(resp.body);
@ -57,7 +58,7 @@ Future<(List<Event>, int)?> getRemoteEvents(
);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);

View File

@ -1,4 +1,5 @@
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/relations.dart';
import 'package:solian/providers/auth.dart';
@ -42,7 +43,7 @@ class RelationshipProvider extends GetxController {
final client = auth.configureClient('auth');
final resp = await client.post('/users/me/relations?related=$username', {});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -57,7 +58,7 @@ class RelationshipProvider extends GetxController {
{},
);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -71,7 +72,7 @@ class RelationshipProvider extends GetxController {
{'status': status},
);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;

View File

@ -6,6 +6,7 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/notification.dart';
import 'package:solian/models/packet.dart';
import 'package:solian/models/pagination.dart';
@ -50,9 +51,9 @@ class WebSocketProvider extends GetxController {
}
final AuthProvider auth = Get.find();
await auth.ensureCredentials();
if (auth.credentials == null) await auth.loadCredentials();
try {
await auth.ensureCredentials();
final uri = Uri.parse(ServiceFinder.buildUrl(
'dealer',
@ -61,21 +62,21 @@ class WebSocketProvider extends GetxController {
isConnecting.value = true;
try {
websocket = WebSocketChannel.connect(uri);
await websocket?.ready;
} catch (e) {
listen();
isConnected.value = true;
} catch (err) {
log('Unable connect dealer via websocket... $err');
if (!noRetry) {
await auth.refreshCredentials();
return connect(noRetry: true);
}
}
listen();
isConnected.value = true;
} finally {
isConnecting.value = false;
}
}
void disconnect() {
websocket?.sink.close(WebSocketStatus.normalClosure);
@ -87,6 +88,7 @@ class WebSocketProvider extends GetxController {
websocket?.stream.listen(
(event) {
final packet = NetworkPackage.fromJson(jsonDecode(event));
log('Websocket incoming message: ${packet.method} ${packet.message}');
stream.sink.add(packet);
},
onDone: () {
@ -147,8 +149,8 @@ class WebSocketProvider extends GetxController {
'device_token': token,
'device_id': deviceUuid,
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
if (resp.statusCode != 200 && resp.statusCode != 400) {
throw RequestException(resp);
}
}

View File

@ -30,8 +30,8 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
final _descriptionController = TextEditingController();
final _birthdayController = TextEditingController();
int? _avatar;
int? _banner;
String? _avatar;
String? _banner;
DateTime? _birthday;
bool _isBusy = false;
@ -109,11 +109,11 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
setState(() => _isBusy = true);
final AttachmentProvider provider = Get.find();
final AttachmentProvider attach = Get.find();
Attachment? attachResult;
try {
attachResult = await provider.createAttachment(
attachResult = await attach.createAttachmentDirectly(
await file.readAsBytes(),
file.path,
'avatar',
@ -129,7 +129,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
final resp = await client.put(
'/users/me/$position',
{'attachment': attachResult.id},
{'attachment': attachResult.rid},
);
if (resp.statusCode == 200) {
_syncWidget();

View File

@ -12,16 +12,20 @@ import 'package:solian/widgets/chat/call/call_participant.dart';
import 'package:livekit_client/livekit_client.dart' as livekit;
class CallScreen extends StatefulWidget {
const CallScreen({super.key});
final bool hideAppBar;
final bool isExpandable;
const CallScreen({
super.key,
this.hideAppBar = false,
this.isExpandable = false,
});
@override
State<CallScreen> createState() => _CallScreenState();
}
class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
Timer? _timer;
String _currentDuration = '00:00:00';
int _layoutMode = 0;
bool _showControls = true;
@ -37,26 +41,6 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
curve: Curves.fastOutSlowIn,
);
String _parseDuration() {
final ChatCallProvider provider = Get.find();
if (provider.current.value == null) return '00:00:00';
Duration duration =
DateTime.now().difference(provider.current.value!.createdAt);
String twoDigits(int n) => n.toString().padLeft(2, '0');
String formattedTime = '${twoDigits(duration.inHours)}:'
'${twoDigits(duration.inMinutes.remainder(60))}:'
'${twoDigits(duration.inSeconds.remainder(60))}';
return formattedTime;
}
void _updateDuration() {
setState(() {
_currentDuration = _parseDuration();
});
}
void _switchLayout() {
if (_layoutMode < 1) {
setState(() => _layoutMode++);
@ -191,15 +175,15 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
@override
void initState() {
Get.find<ChatCallProvider>().setupRoom();
super.initState();
_updateDuration();
Future.delayed(Duration.zero, () {
Get.find<ChatCallProvider>()
..setupRoom()
..enableDurationUpdater();
_planAutoHideControls();
_timer = Timer.periodic(
const Duration(seconds: 1),
(_) => _updateDuration(),
);
});
}
@override
@ -210,16 +194,19 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
final ChatCallProvider provider = Get.find();
final ChatCallProvider ctrl = Get.find();
return Material(
color: Theme.of(context).colorScheme.surface,
child: Scaffold(
appBar: AppBar(
appBar: widget.hideAppBar
? null
: AppBar(
leading: AppBarLeadingButton.adaptive(context),
centerTitle: true,
toolbarHeight: SolianTheme.toolbarHeight(context),
title: RichText(
title: Obx(
() => RichText(
textAlign: TextAlign.center,
text: TextSpan(children: [
TextSpan(
@ -228,12 +215,13 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
),
const TextSpan(text: '\n'),
TextSpan(
text: _currentDuration,
text: ctrl.lastDuration.value,
style: Theme.of(context).textTheme.bodySmall,
),
]),
),
),
),
body: SafeArea(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
@ -259,13 +247,21 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
Obx(() {
return Row(
children: [
Text(call.room.serverRegion ?? 'unknown'),
const SizedBox(width: 6),
Text(call.room.serverVersion ?? 'unknown')
],
Text(
call.channel.value?.name ??
'unknown'.tr,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 6),
Text(call.lastDuration.value)
],
);
}),
Row(
children: [
Text(
@ -317,6 +313,15 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
),
);
}),
Row(
children: [
if (widget.isExpandable)
IconButton(
icon: const Icon(Icons.fullscreen),
onPressed: () {
ctrl.gotoScreen(context);
},
),
IconButton(
icon: _layoutMode == 0
? const Icon(Icons.view_list)
@ -326,13 +331,14 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
},
),
],
),
],
).paddingOnly(left: 20, right: 16),
),
),
Expanded(
child: Material(
color: Theme.of(context).colorScheme.surfaceContainerLow,
elevation: 2,
child: Builder(
builder: (context) {
switch (_layoutMode) {
@ -345,15 +351,15 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
),
),
),
if (provider.room.localParticipant != null)
if (ctrl.room.localParticipant != null)
SizeTransition(
sizeFactor: _controlsAnimation,
axis: Axis.vertical,
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: ControlsWidget(
provider.room,
provider.room.localParticipant!,
ctrl.room,
ctrl.room.localParticipant!,
),
),
),
@ -370,17 +376,13 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
@override
void deactivate() {
_timer?.cancel();
_timer = null;
Get.find<ChatCallProvider>().disableDurationUpdater();
super.deactivate();
}
@override
void activate() {
_timer ??= Timer.periodic(
const Duration(seconds: 1),
(_) => _updateDuration(),
);
Get.find<ChatCallProvider>().enableDurationUpdater();
super.activate();
}
}

View File

@ -11,9 +11,11 @@ import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/models/packet.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/call.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/channel/call/call.dart';
import 'package:solian/screens/channel/channel_detail.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart';
@ -22,6 +24,7 @@ import 'package:solian/widgets/channel/channel_call_indicator.dart';
import 'package:solian/widgets/chat/call/chat_call_action.dart';
import 'package:solian/widgets/chat/chat_event_list.dart';
import 'package:solian/widgets/chat/chat_message_input.dart';
import 'package:solian/widgets/chat/chat_typing_indicator.dart';
import 'package:solian/widgets/current_state_action.dart';
class ChannelChatScreen extends StatefulWidget {
@ -39,7 +42,7 @@ class ChannelChatScreen extends StatefulWidget {
}
class _ChannelChatScreenState extends State<ChannelChatScreen>
with WidgetsBindingObserver {
with WidgetsBindingObserver, TickerProviderStateMixin {
DateTime? _isOutOfSyncSince;
bool _isBusy = false;
@ -101,12 +104,18 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
setState(() => _isBusy = false);
}
final List<ChannelMember> _typingUsers = List.empty(growable: true);
final Map<int, Timer> _typingInactiveTimer = {};
void _listenMessages() {
final WebSocketProvider provider = Get.find();
_subscription = provider.stream.stream.listen((event) {
final WebSocketProvider ws = Get.find();
_subscription = ws.stream.stream.listen((event) {
switch (event.method) {
case 'events.new':
final payload = Event.fromJson(event.payload!);
final typingIdx =
_typingUsers.indexWhere((x) => x.id == payload.senderId);
if (typingIdx != -1) _typingUsers.removeAt(typingIdx);
_chatController.receiveEvent(payload);
break;
case 'calls.new':
@ -121,6 +130,25 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
setState(() => _ongoingCall = null);
}
break;
case 'status.typing':
if (event.payload?['channel_id'] != _channel!.id) break;
final member = ChannelMember.fromJson(event.payload!['member']);
if (member.id == _channelProfile!.id) break;
if (!_typingUsers.any((x) => x.id == member.id)) {
setState(() {
_typingUsers.add(member);
});
}
_typingInactiveTimer[member.id]?.cancel();
_typingInactiveTimer[member.id] = Timer(
const Duration(seconds: 3),
() {
setState(() {
_typingUsers.removeWhere((x) => x.id == member.id);
_typingInactiveTimer.remove(member.id);
});
},
);
}
});
}
@ -139,7 +167,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
switch (state) {
case AppLifecycleState.resumed:
if (_isOutOfSyncSince == null) break;
if (DateTime.now().difference(_isOutOfSyncSince!).inSeconds < 60) break;
if (DateTime.now().difference(_isOutOfSyncSince!).inSeconds < 30) break;
_keepUpdateWithServer();
break;
case AppLifecycleState.paused:
@ -238,12 +266,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
);
}
return Column(
return Row(
children: [
Expanded(
child: Column(
children: [
if (_ongoingCall != null)
ChannelCallIndicator(
channel: _channel!,
ongoingCall: _ongoingCall!,
onJoin: () {
if (!SolianTheme.isLargeScreen(context)) {
final ChatCallProvider call = Get.find();
call.gotoScreen(context);
}
},
),
Expanded(
child: ChatEventList(
@ -258,25 +295,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
},
),
),
if (_isOutOfSyncSince != null)
ListTile(
contentPadding: const EdgeInsets.only(left: 16, right: 8),
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
leading: const Icon(Icons.history_toggle_off),
title: Text('messageOutOfSync'.tr),
subtitle: Text('messageOutOfSyncCaption'.tr),
trailing: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() => _isOutOfSyncSince = null);
},
),
onTap: _isBusy
? null
: () {
_keepUpdateWithServer();
},
),
Obx(() {
if (_chatController.isLoading.isTrue) {
return const LinearProgressIndicator().animate().slideY();
@ -288,7 +306,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
child: SafeArea(
child: ChatMessageInput(
child: Column(
children: [
ChatTypingIndicator(users: _typingUsers),
ChatMessageInput(
edit: _messageToEditing,
reply: _messageToReplying,
realm: widget.realm,
@ -306,9 +327,31 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
});
},
),
],
),
),
),
),
],
),
),
Obx(() {
final ChatCallProvider call = Get.find();
if (call.isMounted.value && SolianTheme.isLargeScreen(context)) {
return const Expanded(
child: Row(children: [
VerticalDivider(width: 0.3, thickness: 0.3),
Expanded(
child: CallScreen(
hideAppBar: true,
isExpandable: true,
),
),
]),
);
}
return const SizedBox();
}),
],
);
}),
@ -317,6 +360,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
@override
void dispose() {
for (var timer in _typingInactiveTimer.values) {
timer.cancel();
}
_subscription?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();

View File

@ -80,13 +80,15 @@ class _ChatScreenState extends State<ChatScreen> {
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
),
onTap: () {
final ChannelProvider provider = Get.find();
provider
final ChannelProvider channels = Get.find();
channels
.createDirectChannel(context, 'global')
.then((resp) {
if (resp != null) {
_channels.refreshAvailableChannel();
}
}).catchError((e) {
context.showErrorDialog(e);
});
},
),
@ -125,6 +127,7 @@ class _ChatScreenState extends State<ChatScreen> {
noCategory: true,
channels: _channels.directChannels,
selfId: selfId,
useReplace: true,
),
),
),

View File

@ -41,6 +41,8 @@ const i18nEnglish = {
'openInBrowser': 'Open in browser',
'notification': 'Notification',
'errorHappened': 'An error occurred',
'errorHappenedUnauthorized':
'Unauthorized request, please sign in or try resign in.',
'forgotPassword': 'Forgot password',
'email': 'Email',
'username': 'Username',
@ -174,7 +176,7 @@ const i18nEnglish = {
'attachmentAttached': 'Exists Files',
'attachmentUploadBlocked':
'Upload blocked, there is currently a task in progress...',
'attachmentAdd': 'Attach attachments',
'attachmentAdd': 'Attach file',
'attachmentAddGalleryPhoto': 'Gallery photo',
'attachmentAddGalleryVideo': 'Gallery video',
'attachmentAddCameraPhoto': 'Capture photo',
@ -269,6 +271,7 @@ const i18nEnglish = {
'callOngoing': 'A call is ongoing...',
'callOngoingEmpty': 'A call is on hold...',
'callOngoingParticipants': '@count people are calling...',
'callOngoingJoined': 'Call last @duration',
'callJoin': 'Join',
'callResume': 'Resume',
'callMicrophone': 'Microphone',
@ -379,4 +382,8 @@ const i18nEnglish = {
'messageOutOfSyncCaption':
'Since the App has entered the background, there may be a time difference between the message list and the server. Click to Refresh.',
'messageHistoryWipe': 'Wipe local message history',
'unknown': 'Unknown',
'collapse': 'Collapse',
'expand': 'Expand',
'typingMessage': '@user are typing...',
};

View File

@ -41,6 +41,12 @@ const i18nSimplifiedChinese = {
'openInBrowser': '在浏览器中打开',
'notification': '通知',
'errorHappened': '发生错误了',
'errorHappenedUnauthorized': '未经授权的请求,请登录或尝试重新登录。',
'errorHappenedRequestBad': '请求错误,服务器拒绝处理该请求,请检查您的请求数据。',
'errorHappenedRequestForbidden': '请求错误,权限不足。',
'errorHappenedRequestNotFound': '请求错误,请求的数据不存在。',
'errorHappenedRequestConnection': '网络请求失败,请检查连接状态与服务状态后再试。',
'errorHappenedRequestUnknown': '请求错误,类型未知,请将本提示完整截图提交反馈。',
'forgotPassword': '忘记密码',
'email': '邮件地址',
'username': '用户名',
@ -247,6 +253,7 @@ const i18nSimplifiedChinese = {
'callOngoing': '一则通话正在进行中…',
'callOngoingEmpty': '一则通话待机中…',
'callOngoingParticipants': '@count 人正在进行通话…',
'callOngoingJoined': '通话进行 @duration',
'callJoin': '加入',
'callResume': '恢复',
'callMicrophone': '麦克风',
@ -345,4 +352,8 @@ const i18nSimplifiedChinese = {
'messageOutOfSync': '消息可能与服务器脱节',
'messageOutOfSyncCaption': '由于 App 进入后台,消息列表可能与服务器存在时差,点击刷新。',
'messageHistoryWipe': '清除消息记录',
'unknown': '未知',
'collapse': '折叠',
'expand': '展开',
'typingMessage': '@user 正在输入中…',
};

View File

@ -28,11 +28,11 @@ class _AttachmentAttrEditorDialogState
bool _isMature = false;
Future<Attachment?> _updateAttachment() async {
final AttachmentProvider provider = Get.find();
final AttachmentProvider attach = Get.find();
setState(() => _isBusy = true);
try {
final resp = await provider.updateAttachment(
final resp = await attach.updateAttachment(
widget.item.id,
_altController.value.text,
isMature: _isMature,

View File

@ -64,7 +64,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
if (widget.singleMode) {
if (!widget.singleMode) {
final medias = await _imagePicker.pickMultiImage(
maxWidth: widget.imageMaxWidth,
maxHeight: widget.imageMaxHeight,
@ -72,8 +72,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
if (medias.isEmpty) return;
_enqueueTaskBatch(medias.map((x) {
final file = File(x.path);
return AttachmentUploadTask(file: file, usage: widget.pool);
final file = XFile(x.path);
return AttachmentUploadTask(file: file, pool: widget.pool);
}));
} else {
final media = await _imagePicker.pickMedia(
@ -83,7 +83,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
if (media == null) return;
_enqueueTask(
AttachmentUploadTask(file: File(media.path), usage: widget.pool),
AttachmentUploadTask(file: XFile(media.path), pool: widget.pool),
);
}
}
@ -95,9 +95,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final media = await _imagePicker.pickVideo(source: ImageSource.gallery);
if (media == null) return;
final file = File(media.path);
_enqueueTask(
AttachmentUploadTask(file: file, usage: widget.pool),
AttachmentUploadTask(file: XFile(media.path), pool: widget.pool),
);
}
@ -113,7 +112,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
List<File> files = result.paths.map((path) => File(path!)).toList();
_enqueueTaskBatch(files.map((x) {
return AttachmentUploadTask(file: x, usage: widget.pool);
return AttachmentUploadTask(file: XFile(x.path), pool: widget.pool);
}));
}
@ -129,9 +128,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
}
if (media == null) return;
final file = File(media.path);
_enqueueTask(
AttachmentUploadTask(file: file, usage: widget.pool),
AttachmentUploadTask(file: XFile(media.path), pool: widget.pool),
);
}
@ -197,20 +195,16 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
if (_uploadController.isUploading.value) return;
_uploadController.uploadAttachmentWithCallback(
data,
'Pasted Image',
widget.pool,
null,
(item) {
_uploadController
.uploadAttachmentFromData(data, 'Pasted Image', widget.pool, null)
.then((item) {
if (item == null) return;
widget.onAdd(item.rid);
if (mounted) {
setState(() => _attachments.add(item));
if (widget.singleMode) Navigator.pop(context);
}
},
);
});
}
String _formatBytes(int bytes, {int decimals = 2}) {
@ -252,7 +246,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
.listMetadata(widget.initialAttachments ?? List.empty())
.then((result) {
setState(() {
_attachments = result;
_attachments = List.from(result, growable: true);
_isBusy = false;
_isFirstTimeBusy = false;
});
@ -304,7 +298,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
],
);
if (croppedFile == null) return;
_uploadController.queueOfUpload[queueIndex].file = File(croppedFile.path);
_uploadController.queueOfUpload[queueIndex].file = XFile(croppedFile.path);
_uploadController.queueOfUpload.refresh();
}
@ -347,9 +341,25 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
fontFamily: 'monospace',
),
),
Row(
children: [
FutureBuilder(
future: element.file.length(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox();
return Text(
_formatBytes(snapshot.data!),
style: Theme.of(context).textTheme.bodySmall,
);
},
),
const SizedBox(width: 6),
if (element.progress != null)
Text(
'In queue #${index + 1}',
style: const TextStyle(fontSize: 12),
'${(element.progress! * 100).toStringAsFixed(2)}%',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
],
),
@ -581,8 +591,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
onDragDone: (detail) async {
if (_uploadController.isUploading.value) return;
_enqueueTaskBatch(detail.files.map((x) {
final file = File(x.path);
return AttachmentUploadTask(file: file, usage: widget.pool);
final file = XFile(x.path);
return AttachmentUploadTask(file: file, pool: widget.pool);
}));
},
child: Column(
@ -596,16 +606,14 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
children: [
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
Text(
'attachmentAdd'.tr,
style:
Theme.of(context).textTheme.headlineSmall,
style: Theme.of(context).textTheme.headlineSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 10),
Obx(() {
if (_uploadController.isUploading.value) {

View File

@ -1,14 +1,14 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/platform.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:video_player/video_player.dart';
class AttachmentItem extends StatefulWidget {
final String parentId;
@ -147,17 +147,21 @@ class _AttachmentItemImage extends StatelessWidget {
errorWidget: (context, url, error) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Center(
child: const Icon(Icons.close, size: 32)
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.close, size: 32)
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
Text(error.toString()),
],
),
);
},
)
else
Image.network(
ServiceFinder.buildUrl('files', '/attachments/${item.id}'),
ServiceFinder.buildUrl('files', '/attachments/${item.rid}'),
fit: fit,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
@ -174,10 +178,14 @@ class _AttachmentItemImage extends StatelessWidget {
errorBuilder: (context, error, stackTrace) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Center(
child: const Icon(Icons.close, size: 32)
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.close, size: 32)
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
Text(error.toString()),
],
),
);
},
@ -231,22 +239,20 @@ class _AttachmentItemVideo extends StatefulWidget {
}
class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
VideoPlayerController? _playerController;
ChewieController? _chewieController;
late final _player = Player(
configuration: const PlayerConfiguration(
logLevel: MPVLogLevel.error,
),
);
late final _controller = VideoController(_player);
bool _showContent = false;
Future<void> _startLoad() async {
final ratio = widget.item.metadata?['ratio'] ?? 16 / 9;
_playerController = VideoPlayerController.networkUrl(
Uri.parse(
ServiceFinder.buildUrl('files', '/attachments/${widget.item.rid}'),
),
);
_playerController!.initialize();
_chewieController = ChewieController(
aspectRatio: ratio,
videoPlayerController: _playerController!,
await _player.open(
Media(ServiceFinder.buildUrl('files', '/attachments/${widget.item.rid}')),
play: false,
);
setState(() => _showContent = true);
}
@ -259,17 +265,10 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
}
}
@override
void dispose() {
_playerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ratio = widget.item.metadata?['ratio'] ?? 16 / 9;
if (!_showContent || _chewieController == null) {
if (!_showContent) {
return GestureDetector(
child: AspectRatio(
aspectRatio: ratio,
@ -307,8 +306,15 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
);
}
return Chewie(
controller: _chewieController!,
return Video(
aspectRatio: ratio,
controller: _controller,
);
}
@override
void dispose() {
_player.dispose();
super.dispose();
}
}

View File

@ -1,4 +1,4 @@
import 'dart:math' show min;
import 'dart:math' as math;
import 'dart:ui';
import 'package:carousel_slider/carousel_slider.dart';
@ -16,9 +16,11 @@ class AttachmentList extends StatefulWidget {
final String parentId;
final List<String> attachmentsId;
final bool isGrid;
final bool isColumn;
final bool isForceGrid;
final bool autoload;
final double flatMaxHeight;
final double columnMaxWidth;
final double? width;
final double? viewport;
@ -28,9 +30,11 @@ class AttachmentList extends StatefulWidget {
required this.parentId,
required this.attachmentsId,
this.isGrid = false,
this.isColumn = false,
this.isForceGrid = false,
this.autoload = false,
this.flatMaxHeight = 720,
this.columnMaxWidth = 480,
this.width,
this.viewport,
});
@ -72,7 +76,8 @@ class _AttachmentListState extends State<AttachmentList> {
double? consistentValue;
int portrait = 0, square = 0, landscape = 0;
for (var entry in _attachmentsMeta) {
if (entry!.metadata?['ratio'] != null) {
if (entry == null) continue;
if (entry.metadata?['ratio'] != null) {
if (entry.metadata?['ratio'] is int) {
consistentValue ??= entry.metadata?['ratio'].toDouble();
} else {
@ -104,13 +109,14 @@ class _AttachmentListState extends State<AttachmentList> {
}
}
Widget _buildEntry(Attachment? element, int idx) {
Widget _buildEntry(Attachment? element, int idx, {double? width}) {
return AttachmentListEntry(
item: element,
parentId: widget.parentId,
width: widget.width,
width: width ?? widget.width,
badgeContent: '${idx + 1}/${_attachmentsMeta.length}',
showBadge: _attachmentsMeta.length > 1 && !widget.isGrid,
showBadge:
_attachmentsMeta.length > 1 && !widget.isGrid && !widget.isColumn,
showBorder: widget.attachmentsId.length > 1,
showMature: _showMature,
autoload: widget.autoload,
@ -126,6 +132,9 @@ class _AttachmentListState extends State<AttachmentList> {
_getMetadataList();
}
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
@override
Widget build(BuildContext context) {
if (widget.attachmentsId.isEmpty) {
@ -133,11 +142,63 @@ class _AttachmentListState extends State<AttachmentList> {
}
if (_isLoading) {
return Row(
children: [
Icon(
Icons.file_copy,
size: 12,
color: _unFocusColor,
).paddingOnly(right: 5),
Text(
'attachmentHint'.trParams(
{'count': widget.attachmentsId.length.toString()},
),
style: TextStyle(color: _unFocusColor, fontSize: 12),
)
],
)
.paddingSymmetric(horizontal: 8)
.animate(onPlay: (c) => c.repeat(reverse: true))
.fadeIn(duration: 1250.ms);
}
if (widget.isColumn) {
var idx = 0;
const radius = BorderRadius.all(Radius.circular(8));
return Wrap(
spacing: 8,
runSpacing: 8,
children: widget.attachmentsId.map((x) {
final element = _attachmentsMeta[idx];
idx++;
if (element == null) return const SizedBox();
double ratio = element.metadata?['ratio']?.toDouble() ?? 16 / 9;
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
child: const LinearProgressIndicator(),
constraints: BoxConstraints(
maxWidth: widget.columnMaxWidth,
maxHeight: 640,
),
child: AspectRatio(
aspectRatio: ratio,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius,
),
child: ClipRRect(
borderRadius: radius,
child: _buildEntry(element, idx),
),
),
),
);
}).toList(),
);
}
@ -152,7 +213,7 @@ class _AttachmentListState extends State<AttachmentList> {
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: min(3, widget.attachmentsId.length),
crossAxisCount: math.min(3, widget.attachmentsId.length),
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
),
@ -161,6 +222,7 @@ class _AttachmentListState extends State<AttachmentList> {
final element = _attachmentsMeta[idx];
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
@ -212,6 +274,7 @@ class AttachmentListEntry extends StatelessWidget {
final Attachment? item;
final String? badgeContent;
final double? width;
final double? height;
final bool showBorder;
final bool showBadge;
final bool showMature;
@ -226,6 +289,7 @@ class AttachmentListEntry extends StatelessWidget {
this.item,
this.badgeContent,
this.width,
this.height,
this.showBorder = false,
this.showBadge = false,
this.showMature = false,
@ -250,6 +314,7 @@ class AttachmentListEntry extends StatelessWidget {
return GestureDetector(
child: Container(
width: width ?? MediaQuery.of(context).size.width,
height: height,
decoration: BoxDecoration(
border: showBorder
? Border.symmetric(
@ -372,7 +437,6 @@ class _AttachmentSelfContainedEntryState
isDense: widget.isDense,
parentId: widget.parentId,
showMature: _showMature,
showBorder: true,
onReveal: (value) {
setState(() => _showMature = value);
},

View File

@ -9,14 +9,20 @@ import 'package:solian/models/call.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/call.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/chat/call/call_prejoin.dart';
class ChannelCallIndicator extends StatelessWidget {
final Channel channel;
final Call ongoingCall;
final Function onJoin;
const ChannelCallIndicator(
{super.key, required this.channel, required this.ongoingCall});
const ChannelCallIndicator({
super.key,
required this.channel,
required this.ongoingCall,
required this.onJoin,
});
void _showCallPrejoin(BuildContext context) {
showModalBottomSheet(
@ -25,6 +31,7 @@ class ChannelCallIndicator extends StatelessWidget {
builder: (context) => ChatCallPrejoinPopup(
ongoingCall: ongoingCall,
channel: channel,
onJoin: onJoin,
),
);
}
@ -40,48 +47,72 @@ class ChannelCallIndicator extends StatelessWidget {
dividerColor: Colors.transparent,
content: Row(
children: [
if (ongoingCall.participants.isEmpty) Text('callOngoingEmpty'.tr),
if (ongoingCall.participants.isNotEmpty)
Text('callOngoingParticipants'.trParams({
Obx(() {
if (call.isInitialized.value) {
return Text('callOngoingJoined'.trParams({
'duration': call.lastDuration.value,
}));
} else if (ongoingCall.participants.isEmpty) {
return Text('callOngoingEmpty'.tr);
} else {
return Text('callOngoingParticipants'.trParams({
'count': ongoingCall.participants.length.toString(),
})),
}));
}
}),
const SizedBox(width: 6),
if (ongoingCall.participants.isNotEmpty)
Container(
Obx(() {
if (call.isInitialized.value) {
return const SizedBox();
}
if (ongoingCall.participants.isNotEmpty) {
return Container(
height: 28,
constraints: const BoxConstraints(maxWidth: 120),
child: AvatarStack(
height: 28,
borderWidth: 0,
avatars: ongoingCall.participants.map((x) {
final userinfo = Account.fromJson(jsonDecode(x['metadata']));
final userinfo =
Account.fromJson(jsonDecode(x['metadata']));
return PlatformInfo.canCacheImage
? CachedNetworkImageProvider(userinfo.avatar)
as ImageProvider
: NetworkImage(userinfo.avatar) as ImageProvider;
}).toList(),
),
),
);
}
return const SizedBox();
})
],
),
actions: [
Obx(() {
if (call.current.value == null) {
if (call.isBusy.value) {
return const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 3),
).paddingAll(16);
} else if (call.current.value == null) {
return TextButton(
onPressed: () => _showCallPrejoin(context),
child: Text('callJoin'.tr),
);
} else if (call.channel.value?.id == channel.id) {
} else if (call.channel.value?.id == channel.id &&
!SolianTheme.isLargeScreen(context)) {
return TextButton(
onPressed: () => call.gotoScreen(context),
onPressed: () => onJoin(),
child: Text('callResume'.tr),
);
} else {
} else if (!SolianTheme.isLargeScreen(context)) {
return TextButton(
onPressed: null,
child: Text('callJoin'.tr),
);
}
return const SizedBox();
})
],
);

View File

@ -88,12 +88,14 @@ class _ControlsWidgetState extends State<ControlsWidget> {
void _disconnect() async {
if (await showDisconnectDialog() != true) return;
final ChatCallProvider provider = Get.find();
if (provider.current.value != null) {
provider.disposeRoom();
final ChatCallProvider call = Get.find();
if (call.current.value != null) {
call.disposeRoom();
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
}
}
void _disableAudio() async {
await _participant.setMicrophoneEnabled(false);
@ -209,8 +211,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
runSpacing: 5,
children: [
IconButton(
icon: Transform.flip(
flipX: true, child: const Icon(Icons.exit_to_app)),
icon: const Icon(Icons.exit_to_app),
color: Theme.of(context).colorScheme.onSurface,
onPressed: _disconnect,
),

View File

@ -11,11 +11,13 @@ import 'package:solian/providers/call.dart';
class ChatCallPrejoinPopup extends StatefulWidget {
final Call ongoingCall;
final Channel channel;
final Function onJoin;
const ChatCallPrejoinPopup({
super.key,
required this.ongoingCall,
required this.channel,
required this.onJoin,
});
@override
@ -25,22 +27,23 @@ class ChatCallPrejoinPopup extends StatefulWidget {
class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> {
bool _isBusy = false;
void performJoin() async {
void _performJoin() async {
final AuthProvider auth = Get.find();
final ChatCallProvider provider = Get.find();
final ChatCallProvider call = Get.find();
if (auth.isAuthorized.isFalse) return;
setState(() => _isBusy = true);
provider.setCall(widget.ongoingCall, widget.channel);
call.setCall(widget.ongoingCall, widget.channel);
call.isBusy.value = true;
try {
final resp = await provider.getRoomToken();
final resp = await call.getRoomToken();
final token = resp.$1;
final endpoint = resp.$2;
provider.initRoom();
provider.setupRoomListeners(
call.initRoom();
call.setupRoomListeners(
onDisconnected: (reason) {
context.showSnackbar(
'callDisconnected'.trParams({'reason': reason.toString()}),
@ -48,23 +51,22 @@ class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> {
},
);
provider.joinRoom(endpoint, token);
call.joinRoom(endpoint, token);
provider.gotoScreen(context).then((_) {
Navigator.pop(context);
});
} catch (e) {
context.showErrorDialog(e);
}
widget.onJoin();
setState(() => _isBusy = false);
}
@override
void initState() {
final ChatCallProvider provider = Get.find();
provider.checkPermissions().then((_) {
provider.initHardware();
final ChatCallProvider call = Get.find();
call.checkPermissions().then((_) {
call.initHardware();
});
super.initState();
@ -169,7 +171,7 @@ class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> {
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
),
onPressed: _isBusy ? null : performJoin,
onPressed: _isBusy ? null : _performJoin,
child: Text('callJoin'.tr),
),
],

View File

@ -7,10 +7,10 @@ class ChatCallCurrentIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ChatCallProvider provider = Get.find();
final ChatCallProvider call = Get.find();
return Obx(() {
if (provider.current.value == null || provider.channel.value == null) {
if (call.current.value == null || call.channel.value == null) {
return const SizedBox();
}
@ -18,11 +18,8 @@ class ChatCallCurrentIndicator extends StatelessWidget {
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
leading: const Icon(Icons.call),
title: Text(provider.channel.value!.name),
title: Text(call.channel.value!.name),
subtitle: Text('callAlreadyOngoing'.tr),
onTap: () {
provider.gotoScreen(context);
},
);
});
}

View File

@ -8,6 +8,7 @@ import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/chat/chat_event_action_log.dart';
import 'package:solian/widgets/chat/chat_event_message.dart';
import 'package:solian/widgets/link_expansion.dart';
import 'package:timeago/timeago.dart' show format;
class ChatEvent extends StatelessWidget {
@ -37,9 +38,14 @@ class ChatEvent extends StatelessWidget {
return '$negativeSign${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds';
}
Widget _buildLinkExpansion() {
if (item.body['text'] == null) return const SizedBox();
return LinkExpansion(content: item.body['text']);
}
Widget _buildAttachment(BuildContext context, {bool isMinimal = false}) {
final attachments = item.body['attachments'] != null
? List<String>.from(item.body['attachments'].map((x) => x))
? List<String>.from(item.body['attachments']?.whereType<String>())
: List<String>.empty();
if (attachments.isEmpty) return const SizedBox();
@ -67,7 +73,7 @@ class ChatEvent extends StatelessWidget {
return Container(
key: Key('m${item.uuid}attachments-box'),
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.only(top: isMerged ? 0 : 4),
padding: EdgeInsets.only(top: isMerged ? 0 : 4, bottom: 4),
constraints: const BoxConstraints(
maxHeight: 720,
),
@ -75,7 +81,7 @@ class ChatEvent extends StatelessWidget {
key: Key('m${item.uuid}attachments'),
parentId: item.uuid,
attachmentsId: attachments,
viewport: 1,
isColumn: true,
),
);
}
@ -90,10 +96,13 @@ class ChatEvent extends StatelessWidget {
return const SizedBox();
}
return ChatEvent(
return Container(
constraints: const BoxConstraints(maxWidth: 480),
child: ChatEvent(
item: snapshot.data!.data,
isMerged: false,
isQuote: true,
),
).paddingOnly(left: isMerged ? 52 : 0);
},
);
@ -188,8 +197,9 @@ class ChatEvent extends StatelessWidget {
),
],
).paddingOnly(right: 12),
_buildAttachment(context, isMinimal: isContentPreviewing)
.paddingOnly(left: isContentPreviewing ? 12 : 0),
_buildAttachment(context, isMinimal: isContentPreviewing).paddingOnly(
left: isContentPreviewing ? 12 : 56,
),
],
);
} else if (isQuote) {
@ -210,7 +220,9 @@ class ChatEvent extends StatelessWidget {
Row(
children: [
AccountAvatar(
content: item.sender.account.avatar, radius: 9),
content: item.sender.account.avatar,
radius: 9,
),
const SizedBox(width: 5),
Text(
item.sender.account.nick,
@ -221,8 +233,7 @@ class ChatEvent extends StatelessWidget {
],
),
_buildContent().paddingOnly(left: 0.5),
_buildAttachment(context, isMinimal: true)
.paddingOnly(left: 0),
_buildAttachment(context, isMinimal: true),
],
),
),
@ -231,6 +242,7 @@ class ChatEvent extends StatelessWidget {
).paddingOnly(left: isMerged ? 52 : 0, right: 4);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
key: Key('m${item.uuid}'),
children: [
Row(
@ -284,7 +296,8 @@ class ChatEvent extends StatelessWidget {
),
],
).paddingSymmetric(horizontal: 12),
_buildAttachment(context),
_buildLinkExpansion().paddingOnly(left: 52, right: 8),
_buildAttachment(context).paddingOnly(left: 56, right: 8),
],
);
}

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/models/channel.dart';
@ -83,11 +82,6 @@ class ChatEventList extends StatelessWidget {
),
);
},
).animate(key: Key('m-animation${item.uuid}')).slideY(
duration: 250.ms,
curve: Curves.fastEaseInToSlowEaseOut,
end: 0,
begin: 0.5,
);
},
);

View File

@ -1,3 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
@ -7,10 +10,12 @@ import 'package:solian/exts.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/models/packet.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart';
import 'package:solian/widgets/chat/chat_event.dart';
@ -196,6 +201,36 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
}
}
Timer? _typingNotifyTimer;
bool _typingStatus = false;
Future<void> _sendTypingStatus() async {
final WebSocketProvider ws = Get.find();
ws.websocket?.sink.add(jsonEncode(
NetworkPackage(
method: 'status.typing',
endpoint: 'messaging',
payload: {
'channel_id': widget.channel.id,
},
).toJson(),
));
}
void _pingEnterMessageStatus() {
if (!_typingStatus) {
_sendTypingStatus();
_typingStatus = true;
}
if (_typingNotifyTimer == null || !_typingNotifyTimer!.isActive) {
_typingNotifyTimer?.cancel();
_typingNotifyTimer = Timer(const Duration(milliseconds: 1850), () {
_typingStatus = false;
});
}
}
void _resetInput() {
if (widget.onReset != null) widget.onReset!();
_editTo = null;
@ -239,7 +274,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
var insertText = '';
if (suggestion.type == 'emotes') {
insertText = suggestion.content;
insertText = '${suggestion.content} ';
startText = replaceText.replaceFirstMapped(
RegExp(r':(?:([-\w]+)~)?([-\w]+)$'),
(Match m) => insertText,
@ -247,7 +282,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
}
if (suggestion.type == 'users') {
insertText = suggestion.content;
insertText = '${suggestion.content} ';
startText = replaceText.replaceFirstMapped(
RegExp(r'(?:\s|^)@([-\w]+)$'),
(Match m) => insertText,
@ -269,6 +304,20 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
super.didUpdateWidget(oldWidget);
}
@override
void initState() {
super.initState();
_textController.addListener(_pingEnterMessageStatus);
}
@override
void dispose() {
_textController.removeListener(_pingEnterMessageStatus);
_textController.dispose();
_typingNotifyTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final notifyBannerActions = [

View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
class ChatTypingIndicator extends StatefulWidget {
final List<ChannelMember> users;
const ChatTypingIndicator({super.key, required this.users});
@override
State<ChatTypingIndicator> createState() => _ChatTypingIndicatorState();
}
class _ChatTypingIndicatorState extends State<ChatTypingIndicator>
with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
duration: const Duration(milliseconds: 250),
vsync: this,
);
late final Animation<double> _animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(covariant ChatTypingIndicator oldWidget) {
if (widget.users.isNotEmpty) {
_controller.animateTo(1);
} else {
_controller.animateTo(0);
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return SizeTransition(
sizeFactor: _animation,
axis: Axis.vertical,
axisAlignment: -1,
child: Row(
children: [
const Icon(Icons.more_horiz),
const SizedBox(width: 6),
Text('typingMessage'.trParams({
'user': widget.users.map((x) => x.account.nick).join(', '),
})),
],
).paddingSymmetric(horizontal: 16),
);
}
}

View File

@ -0,0 +1,153 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_svg/svg.dart';
import 'package:get/get.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/link_expander.dart';
import 'package:url_launcher/url_launcher_string.dart';
class LinkExpansion extends StatelessWidget {
final String content;
const LinkExpansion({super.key, required this.content});
Widget _buildImage(String url, {double? width, double? height}) {
if (url.endsWith('svg')) {
return SvgPicture.network(url, width: width, height: height);
}
return PlatformInfo.canCacheImage
? CachedNetworkImage(
imageUrl: url,
width: width,
height: height,
errorWidget: (context, url, error) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Center(
child: const Icon(Icons.close, size: 32)
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
),
);
},
)
: Image.network(
url,
width: width,
height: height,
errorBuilder: (context, error, stackTrace) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Center(
child: const Icon(Icons.close, size: 32)
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
),
);
},
);
}
@override
Widget build(BuildContext context) {
final linkRegex = RegExp(
r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]',
);
final matches = linkRegex.allMatches(content);
if (matches.isEmpty) {
return const SizedBox();
}
final LinkExpandController expandController = Get.find();
return Wrap(
children: matches.map((x) {
return Container(
constraints: BoxConstraints(
maxWidth: matches.length == 1 ? 480 : 340,
),
child: FutureBuilder(
future: expandController.expandLink(x.group(0)!),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
final isRichDescription = [
'solsynth.dev',
].contains(Uri.parse(snapshot.data!.url).host);
return GestureDetector(
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if ([
(snapshot.data!.icon?.isNotEmpty ?? false),
snapshot.data!.siteName != null
].any((x) => x))
Row(
children: [
if (snapshot.data!.icon?.isNotEmpty ?? false)
ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: _buildImage(
snapshot.data!.icon!,
width: 32,
height: 32,
),
).paddingOnly(right: 8),
if (snapshot.data!.siteName != null)
Text(
snapshot.data!.siteName!,
style: Theme.of(context).textTheme.labelLarge,
),
],
).paddingOnly(
bottom: (snapshot.data!.icon?.isNotEmpty ?? false)
? 8
: 4,
),
if (snapshot.data!.image != null &&
(snapshot.data!.image?.startsWith('http') ?? false))
ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: _buildImage(
snapshot.data!.image!,
),
).paddingOnly(bottom: 8),
Text(
snapshot.data!.title ?? 'No Title',
maxLines: 1,
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.bodyLarge,
),
if (snapshot.data!.description != null &&
isRichDescription)
MarkdownBody(data: snapshot.data!.description!)
else if (snapshot.data!.description != null)
Text(
snapshot.data!.description!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
).paddingAll(12),
),
onTap: () {
launchUrlString(x.group(0)!);
},
);
},
),
);
}).toList(),
);
}
}

View File

@ -13,7 +13,7 @@ import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_status_action.dart';
import 'package:solian/widgets/navigation/app_navigation.dart';
import 'package:badges/badges.dart' as badges;
import 'package:solian/widgets/navigation/app_navigation_regions.dart';
import 'package:solian/widgets/navigation/app_navigation_region.dart';
class AppNavigationDrawer extends StatefulWidget {
final String? routeName;
@ -24,7 +24,23 @@ class AppNavigationDrawer extends StatefulWidget {
State<AppNavigationDrawer> createState() => _AppNavigationDrawerState();
}
class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
class _AppNavigationDrawerState extends State<AppNavigationDrawer>
with TickerProviderStateMixin {
bool _isCollapsed = false;
late final AnimationController _drawerAnimationController =
AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
late final Animation<double> _drawerAnimation = Tween<double>(
begin: 80.0,
end: 304.0,
).animate(CurvedAnimation(
parent: _drawerAnimationController,
curve: Curves.easeInOut,
));
AccountStatus? _accountStatus;
Future<void> _getStatus() async {
@ -33,39 +49,26 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
final resp = await provider.getCurrentStatus();
final status = AccountStatus.fromJson(resp.body);
if (mounted) {
setState(() {
_accountStatus = status;
});
}
void _closeDrawer() {
rootScaffoldKey.currentState!.closeDrawer();
}
@override
void initState() {
super.initState();
_getStatus();
}
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
@override
Widget build(BuildContext context) {
Widget _buildUserInfo() {
return Obx(() {
final AuthProvider auth = Get.find();
return Drawer(
backgroundColor:
SolianTheme.isLargeScreen(context) ? Colors.transparent : null,
child: SafeArea(
bottom: false,
child: Column(
children: [
Obx(() {
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 28),
leading: const Icon(Icons.account_circle),
title: Text('guest'.tr),
subtitle: Text('unsignedIn'.tr),
if (_isCollapsed) {
return InkWell(
child: const Icon(Icons.account_circle).paddingSymmetric(
horizontal: 28,
vertical: 20,
),
onTap: () {
AppRouter.instance.goNamed('account');
_closeDrawer();
@ -74,37 +77,24 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
}
return ListTile(
contentPadding: const EdgeInsets.only(left: 20, right: 20),
title: Text(
auth.userProfile.value!['nick'],
maxLines: 1,
overflow: TextOverflow.fade,
),
subtitle: Builder(
builder: (context) {
if (_accountStatus == null) {
return Text('loading'.tr);
}
final info = StatusProvider.determineStatus(
_accountStatus!,
);
return Text(
info.$3,
maxLines: 1,
overflow: TextOverflow.fade,
);
contentPadding: const EdgeInsets.symmetric(horizontal: 28),
leading: const Icon(Icons.account_circle),
title: !_isCollapsed ? Text('guest'.tr) : null,
subtitle: !_isCollapsed ? Text('unsignedIn'.tr) : null,
onTap: () {
AppRouter.instance.goNamed('account');
_closeDrawer();
},
),
leading: Obx(() {
);
}
final leading = Obx(() {
final statusBadgeColor = _accountStatus != null
? StatusProvider.determineStatus(
_accountStatus!,
).$2
? StatusProvider.determineStatus(_accountStatus!).$2
: Colors.grey;
final RelationshipProvider relations = Get.find();
final accountNotifications =
relations.friendRequestCount.value;
final accountNotifications = relations.friendRequestCount.value;
return badges.Badge(
badgeContent: Text(
@ -118,8 +108,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
),
child: badges.Badge(
showBadge: _accountStatus != null,
badgeStyle:
badges.BadgeStyle(badgeColor: statusBadgeColor),
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
position: badges.BadgePosition.bottomEnd(
bottom: 0,
end: -2,
@ -129,7 +118,47 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
),
),
);
}),
});
return InkWell(
child: !_isCollapsed
? Row(
children: [
leading,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
auth.userProfile.value!['nick'],
maxLines: 1,
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.bodyLarge,
).paddingOnly(left: 16),
Builder(
builder: (context) {
if (_accountStatus == null) {
return Text('loading'.tr).paddingOnly(left: 16);
}
final info = StatusProvider.determineStatus(
_accountStatus!,
);
return Text(
info.$3,
maxLines: 1,
overflow: TextOverflow.fade,
style: TextStyle(
color: _unFocusColor,
),
).paddingOnly(left: 16);
},
),
],
),
),
],
).paddingSymmetric(horizontal: 20, vertical: 16)
: leading.paddingSymmetric(horizontal: 20, vertical: 16),
onTap: () {
AppRouter.instance.goNamed('account');
_closeDrawer();
@ -146,17 +175,93 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
});
},
);
}).paddingSymmetric(vertical: 8),
});
}
void _expandDrawer() {
_drawerAnimationController.animateTo(1);
}
void _collapseDrawer() {
_drawerAnimationController.animateTo(0);
}
void _closeDrawer() {
_autoResize();
rootScaffoldKey.currentState!.closeDrawer();
}
void _autoResize() {
if (SolianTheme.isExtraLargeScreen(context)) {
_expandDrawer();
} else if (SolianTheme.isLargeScreen(context)) {
_collapseDrawer();
} else {
_drawerAnimationController.value = 1;
}
}
@override
void initState() {
super.initState();
_getStatus();
Future.delayed(Duration.zero, () => _autoResize());
_drawerAnimationController.addListener(() {
if (_drawerAnimation.value > 180 && _isCollapsed) {
setState(() => _isCollapsed = false);
} else if (_drawerAnimation.value < 180 && !_isCollapsed) {
setState(() => _isCollapsed = true);
}
});
}
@override
void dispose() {
_drawerAnimationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _drawerAnimation,
builder: (context, child) {
return Drawer(
width: _drawerAnimation.value,
backgroundColor:
SolianTheme.isLargeScreen(context) ? Colors.transparent : null,
child: child,
);
},
child: SafeArea(
bottom: false,
child: Column(
children: [
_buildUserInfo().paddingSymmetric(vertical: 8),
const Divider(thickness: 0.3, height: 1),
Column(
children: AppNavigation.destinations
.map(
(e) => ListTile(
(e) => _isCollapsed
? Tooltip(
message: e.label,
child: InkWell(
child: Icon(e.icon, size: 20).paddingSymmetric(
horizontal: 28,
vertical: 16,
),
onTap: () {
AppRouter.instance.goNamed(e.page);
_closeDrawer();
},
),
)
: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
leading: Icon(e.icon, size: 20).paddingAll(2),
title: Text(e.label),
title: !_isCollapsed ? Text(e.label) : null,
enabled: true,
onTap: () {
AppRouter.instance.goNamed(e.page);
@ -165,10 +270,11 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
),
)
.toList(),
).paddingSymmetric(vertical: 8),
),
const Divider(thickness: 0.3, height: 1),
Expanded(
child: AppNavigationRegions(
child: AppNavigationRegion(
isCollapsed: _isCollapsed,
onSelected: (item) {
_closeDrawer();
},
@ -177,6 +283,24 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
const Divider(thickness: 0.3, height: 1),
Column(
children: [
if (_isCollapsed)
Tooltip(
message: 'settings'.tr,
child: InkWell(
child: const Icon(
Icons.settings,
size: 20,
).paddingSymmetric(
horizontal: 28,
vertical: 10,
),
onTap: () {
AppRouter.instance.pushNamed('settings');
_closeDrawer();
},
),
)
else
ListTile(
minTileHeight: 0,
contentPadding: const EdgeInsets.symmetric(
@ -189,6 +313,33 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
_closeDrawer();
},
),
if (_isCollapsed)
Tooltip(
message: 'expand'.tr,
child: InkWell(
child: const Icon(Icons.chevron_right, size: 20)
.paddingSymmetric(
horizontal: 28,
vertical: 10,
),
onTap: () {
_expandDrawer();
},
),
)
else
ListTile(
minTileHeight: 0,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
leading:
const Icon(Icons.chevron_left, size: 20).paddingAll(2),
title: Text('collapse'.tr),
onTap: () {
_collapseDrawer();
},
),
],
).paddingOnly(
top: 8,

View File

@ -5,10 +5,15 @@ import 'package:solian/providers/content/channel.dart';
import 'package:solian/router.dart';
import 'package:collection/collection.dart';
class AppNavigationRegions extends StatelessWidget {
class AppNavigationRegion extends StatelessWidget {
final bool isCollapsed;
final Function(Channel item) onSelected;
const AppNavigationRegions({super.key, required this.onSelected});
const AppNavigationRegion({
super.key,
required this.onSelected,
this.isCollapsed = false,
});
void _gotoChannel(Channel item) {
AppRouter.instance.pushReplacementNamed(
@ -25,6 +30,16 @@ class AppNavigationRegions extends StatelessWidget {
Widget _buildEntry(BuildContext context, Channel item) {
const padding = EdgeInsets.symmetric(horizontal: 20);
if (isCollapsed) {
return InkWell(
child: const Icon(Icons.tag_outlined, size: 20).paddingSymmetric(
horizontal: 20,
vertical: 16,
),
onTap: () => _gotoChannel(item),
);
}
return ListTile(
minTileHeight: 0,
leading: const Icon(Icons.tag_outlined),
@ -51,6 +66,27 @@ class AppNavigationRegions extends StatelessWidget {
.where((x) => x.type == 0 && x.realmId != null)
.toList();
if (isCollapsed) {
return CustomScrollView(
slivers: [
const SliverPadding(padding: EdgeInsets.only(top: 8)),
SliverList.builder(
itemCount:
noRealmGroupChannels.length + hasRealmGroupChannels.length,
itemBuilder: (context, index) {
final element = index >= noRealmGroupChannels.length
? hasRealmGroupChannels[index - noRealmGroupChannels.length]
: noRealmGroupChannels[index];
return Tooltip(
message: element.name,
child: _buildEntry(context, element),
);
},
),
],
);
}
return CustomScrollView(
slivers: [
const SliverPadding(padding: EdgeInsets.only(top: 8)),

View File

@ -10,6 +10,7 @@ import 'package:solian/shells/title_shell.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/link_expansion.dart';
import 'package:solian/widgets/markdown_text_content.dart';
import 'package:solian/widgets/posts/post_tags.dart';
import 'package:solian/widgets/posts/post_quick_action.dart';
@ -288,12 +289,41 @@ class _PostItemState extends State<PostItem> {
);
}
Widget _buildAttachments() {
final List<String> attachments = item.body['attachments'] is List
? List.from(item.body['attachments']?.whereType<String>())
: List.empty();
if (attachments.length > 3) {
return AttachmentList(
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: true,
isGrid: true,
).paddingOnly(left: 36, top: 4, bottom: 4);
} else if (attachments.length > 1) {
return AttachmentList(
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: true,
isColumn: true,
).paddingOnly(left: 60, right: 24);
} else {
return AttachmentList(
flatMaxHeight: MediaQuery.of(context).size.width,
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: true,
);
}
}
double _contentHeight = 0;
@override
Widget build(BuildContext context) {
final List<String> attachments = item.body['attachments'] is List
? item.body['attachments']?.cast<String>()
? List.from(item.body['attachments']?.whereType<String>())
: List.empty();
final hasAttachment = attachments.isNotEmpty;
@ -301,7 +331,7 @@ class _PostItemState extends State<PostItem> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildThumbnail(),
_buildThumbnail().paddingOnly(bottom: 8),
_buildHeader().paddingSymmetric(horizontal: 12),
_buildHeaderDivider().paddingSymmetric(horizontal: 12),
Stack(
@ -349,6 +379,11 @@ class _PostItemState extends State<PostItem> {
),
],
),
LinkExpansion(content: item.body['content']).paddingOnly(
left: 8,
right: 8,
top: 4,
),
_buildFooter().paddingOnly(left: 16),
if (attachments.isNotEmpty)
Row(
@ -441,27 +476,31 @@ class _PostItemState extends State<PostItem> {
],
),
if (widget.item.replyTo != null && widget.isShowEmbed)
_buildReply(context).paddingOnly(top: 4),
Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 4),
child: _buildReply(context),
),
if (widget.item.repostTo != null && widget.isShowEmbed)
_buildRepost(context).paddingOnly(top: 4),
Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 4),
child: _buildRepost(context),
),
_buildFooter().paddingOnly(left: 12),
LinkExpansion(content: item.body['content'])
.paddingOnly(top: 4),
],
),
),
],
).paddingOnly(
top: 10,
bottom: hasAttachment ? 10 : 0,
bottom: attachments.length == 1 ? 10 : 0,
right: 16,
left: 16,
),
AttachmentList(
flatMaxHeight: MediaQuery.of(context).size.width,
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: true,
isGrid: attachments.length > 1,
),
_buildAttachments(),
if (widget.isShowReply || widget.isReactable)
PostQuickAction(
isShowReply: widget.isShowReply,
@ -474,8 +513,8 @@ class _PostItemState extends State<PostItem> {
});
},
).paddingOnly(
top: hasAttachment ? 10 : 6,
left: hasAttachment ? 24 : 60,
top: attachments.length == 1 ? 10 : 6,
left: attachments.length == 1 ? 24 : 60,
right: 16,
bottom: 10,
)

View File

@ -11,6 +11,8 @@
#include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
#include <pasteboard/pasteboard_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
@ -30,6 +32,12 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
pasteboard_plugin_register_with_registrar(pasteboard_registrar);

View File

@ -8,11 +8,14 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_acrylic
flutter_secure_storage_linux
flutter_webrtc
media_kit_libs_linux
media_kit_video
pasteboard
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
media_kit_native_event_loop
)
set(PLUGIN_BUNDLED_LIBRARIES)

View File

@ -18,15 +18,17 @@ import flutter_webrtc
import gal
import livekit_client
import macos_window_utils
import media_kit_libs_macos_video
import media_kit_video
import package_info_plus
import pasteboard
import path_provider_foundation
import protocol_handler_macos
import screen_brightness_macos
import share_plus
import shared_preferences_foundation
import sqflite
import url_launcher_macos
import video_player_avfoundation
import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@ -43,14 +45,16 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ProtocolHandlerMacosPlugin.register(with: registry.registrar(forPlugin: "ProtocolHandlerMacosPlugin"))
ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
}

View File

@ -163,6 +163,12 @@ PODS:
- WebRTC-SDK (= 125.6422.04)
- macos_window_utils (1.0.0):
- FlutterMacOS
- media_kit_libs_macos_video (1.0.4):
- FlutterMacOS
- media_kit_native_event_loop (1.0.0):
- FlutterMacOS
- media_kit_video (0.0.1):
- FlutterMacOS
- nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0)
@ -180,6 +186,8 @@ PODS:
- PromisesObjC (= 2.4.0)
- protocol_handler_macos (0.0.1):
- FlutterMacOS
- screen_brightness_macos (0.1.0):
- FlutterMacOS
- share_plus (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
@ -190,9 +198,6 @@ PODS:
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- wakelock_plus (0.0.1):
- FlutterMacOS
- WebRTC-SDK (125.6422.04)
@ -212,15 +217,18 @@ DEPENDENCIES:
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`)
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- protocol_handler_macos (from `Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos`)
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
SPEC REPOS:
@ -272,6 +280,12 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
macos_window_utils:
:path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos
media_kit_libs_macos_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
media_kit_native_event_loop:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos
media_kit_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
pasteboard:
@ -280,6 +294,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
protocol_handler_macos:
:path: Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos
screen_brightness_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos
share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation:
@ -288,8 +304,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
video_player_avfoundation:
:path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin
wakelock_plus:
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
@ -321,6 +335,9 @@ SPEC CHECKSUMS:
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
livekit_client: 95f3b71e6545845aa658a6df0a3a62dcc3471d7c
macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
nanopb: 438bc412db1928dac798aa6fd75726007be04262
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
@ -328,11 +345,11 @@ SPEC CHECKSUMS:
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3

View File

@ -206,14 +206,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
chewie:
dependency: "direct main"
description:
name: chewie
sha256: "2243e41e79e865d426d9dd9c1a9624aa33c4ad11de2d0cd680f826e2cd30e879"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
cli_util:
dependency: transitive
description:
@ -271,7 +263,7 @@ packages:
source: hosted
version: "3.1.1"
cross_file:
dependency: transitive
dependency: "direct main"
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
@ -286,14 +278,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.5"
csslib:
dependency: transitive
description:
name: csslib
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
cupertino_icons:
dependency: "direct main"
description:
@ -803,6 +787,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.0"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
url: "https://pub.dev"
source: hosted
version: "2.0.10+1"
flutter_test:
dependency: "direct dev"
description: flutter
@ -873,10 +865,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: ddc16d34b0d74cb313986918c0f0885a7ba2fc24d8fb8419de75f0015144ccfe
sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
url: "https://pub.dev"
source: hosted
version: "14.2.3"
version: "14.2.7"
graphs:
dependency: transitive
description:
@ -893,14 +885,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
html:
dependency: transitive
description:
name: html
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
url: "https://pub.dev"
source: hosted
version: "0.15.4"
http:
dependency: transitive
description:
@ -969,10 +953,10 @@ packages:
dependency: transitive
description:
name: image_picker_android
sha256: "8c5abf0dcc24fe6e8e0b4a5c0b51a5cf30cefdf6407a3213dae61edc75a70f56"
sha256: c0a6763d50b354793d0192afd0a12560b823147d3ded7c6b77daf658fa05cc85
url: "https://pub.dev"
source: hosted
version: "0.8.12+12"
version: "0.8.12+13"
image_picker_for_web:
dependency: transitive
description:
@ -1165,6 +1149,78 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.11.1"
media_kit:
dependency: "direct main"
description:
name: media_kit
sha256: "1f1deee148533d75129a6f38251ff8388e33ee05fc2d20a6a80e57d6051b7b62"
url: "https://pub.dev"
source: hosted
version: "1.1.11"
media_kit_libs_android_video:
dependency: transitive
description:
name: media_kit_libs_android_video
sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c"
url: "https://pub.dev"
source: hosted
version: "1.3.6"
media_kit_libs_ios_video:
dependency: transitive
description:
name: media_kit_libs_ios_video
sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991
url: "https://pub.dev"
source: hosted
version: "1.1.4"
media_kit_libs_linux:
dependency: transitive
description:
name: media_kit_libs_linux
sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310
url: "https://pub.dev"
source: hosted
version: "1.1.3"
media_kit_libs_macos_video:
dependency: transitive
description:
name: media_kit_libs_macos_video
sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d
url: "https://pub.dev"
source: hosted
version: "1.1.4"
media_kit_libs_video:
dependency: "direct main"
description:
name: media_kit_libs_video
sha256: "20bb4aefa8fece282b59580e1cd8528117297083a6640c98c2e98cfc96b93288"
url: "https://pub.dev"
source: hosted
version: "1.0.5"
media_kit_libs_windows_video:
dependency: transitive
description:
name: media_kit_libs_windows_video
sha256: "32654572167825c42c55466f5d08eee23ea11061c84aa91b09d0e0f69bdd0887"
url: "https://pub.dev"
source: hosted
version: "1.0.10"
media_kit_native_event_loop:
dependency: transitive
description:
name: media_kit_native_event_loop
sha256: "7d82e3b3e9ded5c35c3146c5ba1da3118d1dd8ac3435bac7f29f458181471b40"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
media_kit_video:
dependency: "direct main"
description:
name: media_kit_video
sha256: "2cc3b966679963ba25a4ce5b771e532a521ebde7c6aa20e9802bec95d9916c8f"
url: "https://pub.dev"
source: hosted
version: "1.2.5"
meta:
dependency: transitive
description:
@ -1245,6 +1301,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
url: "https://pub.dev"
source: hosted
version: "1.0.1"
path_provider:
dependency: transitive
description:
@ -1321,10 +1385,10 @@ packages:
dependency: transitive
description:
name: permission_handler_html
sha256: d220eb8476b466d58b161e10b3001d93999010a26228a3fb89c4280db1249546
sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
url: "https://pub.dev"
source: hosted
version: "0.1.3+1"
version: "0.1.3+2"
permission_handler_platform_interface:
dependency: transitive
description:
@ -1417,10 +1481,10 @@ packages:
dependency: transitive
description:
name: process_run
sha256: c917dfb5f7afad4c7485bc00a4df038621248fce046105020cea276d1a87c820
sha256: "112a77da35be50617ed9e2230df68d0817972f225e7f97ce8336f76b4e601606"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.2.0"
protobuf:
dependency: transitive
description:
@ -1509,6 +1573,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.28.0"
safe_local_storage:
dependency: transitive
description:
name: safe_local_storage
sha256: ede4eb6cb7d88a116b3d3bf1df70790b9e2038bc37cb19112e381217c74d9440
url: "https://pub.dev"
source: hosted
version: "1.0.2"
screen_brightness:
dependency: transitive
description:
name: screen_brightness
sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
screen_brightness_android:
dependency: transitive
description:
name: screen_brightness_android
sha256: "3df10961e3a9e968a5e076fe27e7f4741fa8a1d3950bdeb48cf121ed529d0caf"
url: "https://pub.dev"
source: hosted
version: "0.1.0+2"
screen_brightness_ios:
dependency: transitive
description:
name: screen_brightness_ios
sha256: "99adc3ca5490b8294284aad5fcc87f061ad685050e03cf45d3d018fe398fd9a2"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
screen_brightness_macos:
dependency: transitive
description:
name: screen_brightness_macos
sha256: "64b34e7e3f4900d7687c8e8fb514246845a73ecec05ab53483ed025bd4a899fd"
url: "https://pub.dev"
source: hosted
version: "0.1.0+1"
screen_brightness_platform_interface:
dependency: transitive
description:
name: screen_brightness_platform_interface
sha256: b211d07f0c96637a15fb06f6168617e18030d5d74ad03795dd8547a52717c171
url: "https://pub.dev"
source: hosted
version: "0.1.0"
screen_brightness_windows:
dependency: transitive
description:
name: screen_brightness_windows
sha256: "9261bf33d0fc2707d8cf16339ce25768100a65e70af0fcabaf032fc12408ba86"
url: "https://pub.dev"
source: hosted
version: "0.1.3"
sdp_transform:
dependency: transitive
description:
@ -1786,6 +1906,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.1"
universal_platform:
dependency: transitive
description:
name: universal_platform
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
uri_parser:
dependency: transitive
description:
name: uri_parser
sha256: "6543c9fd86d2862fac55d800a43e67c0dcd1a41677cb69c2f8edfe73bbcf1835"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
url_launcher:
dependency: "direct main"
description:
@ -1798,10 +1934,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79
sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab
url: "https://pub.dev"
source: hosted
version: "6.3.9"
version: "6.3.10"
url_launcher_ios:
dependency: transitive
description:
@ -1858,6 +1994,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.4.2"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
vector_math:
dependency: transitive
description:
@ -1866,46 +2026,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
video_player:
dependency: "direct main"
description:
name: video_player
sha256: e30df0d226c4ef82e2c150ebf6834b3522cf3f654d8e2f9419d376cdc071425d
url: "https://pub.dev"
source: hosted
version: "2.9.1"
video_player_android:
dependency: transitive
description:
name: video_player_android
sha256: "4de50df9ee786f5891d3281e1e633d7b142ef1acf47392592eb91cba5d355849"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c
url: "https://pub.dev"
source: hosted
version: "2.6.1"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6"
url: "https://pub.dev"
source: hosted
version: "6.2.2"
video_player_web:
dependency: transitive
description:
name: video_player_web
sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
vm_service:
dependency: transitive
description:
@ -1914,6 +2034,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "14.2.4"
volume_controller:
dependency: transitive
description:
name: volume_controller
sha256: c71d4c62631305df63b72da79089e078af2659649301807fa746088f365cb48e
url: "https://pub.dev"
source: hosted
version: "2.0.8"
wakelock_plus:
dependency: "direct main"
description:
@ -2012,4 +2140,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.22.0"
flutter: ">=3.24.0"

View File

@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App"
publish_to: "none"
version: 1.2.1+17
version: 1.2.1+23
environment:
sdk: ">=3.3.4 <4.0.0"
@ -67,9 +67,12 @@ dependencies:
collection: ^1.18.0
firebase_crashlytics: ^4.0.4
firebase_analytics: ^11.2.1
video_player: ^2.9.1
chewie: ^1.8.3
firebase_performance: ^0.10.0+4
media_kit: ^1.1.10+1
media_kit_video: ^1.2.4
media_kit_libs_video: ^1.0.4
flutter_svg: ^2.0.10+1
cross_file: ^0.3.4+2
dev_dependencies:
flutter_test:

View File

@ -15,9 +15,12 @@
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <gal/gal_plugin_c_api.h>
#include <livekit_client/live_kit_plugin.h>
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
#include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <pasteboard/pasteboard_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
#include <screen_brightness_windows/screen_brightness_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
@ -40,12 +43,18 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("GalPluginCApi"));
LiveKitPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LiveKitPlugin"));
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
MediaKitVideoPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
PasteboardPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PasteboardPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi"));
ScreenBrightnessWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(

View File

@ -12,14 +12,18 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_webrtc
gal
livekit_client
media_kit_libs_windows_video
media_kit_video
pasteboard
permission_handler_windows
protocol_handler_windows
screen_brightness_windows
share_plus
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
media_kit_native_event_loop
)
set(PLUGIN_BUNDLED_LIBRARIES)