Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
48ca885a2c | |||
09cb340a9d | |||
b6ebd6bef6 | |||
2ec25fd1a2 | |||
bc99865ba8 | |||
f834351ce2 | |||
0f1a02f65b | |||
6ad0a34645 | |||
fdc71475fc | |||
047defebd1 | |||
6148e889aa | |||
1d7affcd84 | |||
cc1e0599aa | |||
221b97901f | |||
498bb0e5fb | |||
aa94dfcfe0 | |||
65d9253876 |
@ -1,17 +1,17 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-feature android:name="android.hardware.camera"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
@ -19,31 +19,31 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:label="Solian"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true">
|
||||
android:label="Solian"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true">
|
||||
<receiver android:exported="false"
|
||||
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"/>
|
||||
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
|
||||
<receiver android:exported="false"
|
||||
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
|
||||
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
@ -58,29 +58,36 @@
|
||||
<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>
|
||||
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2"/>
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
@ -89,8 +96,8 @@
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
</manifest>
|
@ -112,15 +112,19 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
label: 'bsPreparingData',
|
||||
action: () async {
|
||||
final AuthProvider auth = Get.find();
|
||||
await Future.wait([
|
||||
Get.find<StickerProvider>().refreshAvailableStickers(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<ChannelProvider>().refreshAvailableChannel(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<RelationshipProvider>().refreshRelativeList(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<RealmProvider>().refreshAvailableRealms(),
|
||||
]);
|
||||
try {
|
||||
await Future.wait([
|
||||
Get.find<StickerProvider>().refreshAvailableStickers(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<ChannelProvider>().refreshAvailableChannel(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<RelationshipProvider>().refreshRelativeList(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<RealmProvider>().refreshAvailableRealms(),
|
||||
]);
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
}
|
||||
},
|
||||
),
|
||||
(
|
||||
|
10
lib/exceptions/request.dart
Normal file
10
lib/exceptions/request.dart
Normal 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}';
|
||||
}
|
6
lib/exceptions/unauthorized.dart
Normal file
6
lib/exceptions/unauthorized.dart
Normal file
@ -0,0 +1,6 @@
|
||||
class UnauthorizedException implements Exception {
|
||||
const UnauthorizedException();
|
||||
|
||||
@override
|
||||
String toString() => 'Unauthorized';
|
||||
}
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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,32 +76,36 @@ 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,
|
||||
null,
|
||||
onProgress: (value) {
|
||||
queueOfUpload[queueIndex].progress = value;
|
||||
_progressOfUpload = value;
|
||||
},
|
||||
onError: (err) {
|
||||
queueOfUpload[queueIndex].error = err;
|
||||
queueOfUpload[queueIndex].isUploading = false;
|
||||
},
|
||||
);
|
||||
try {
|
||||
final result = await _chunkedUploadAttachment(
|
||||
task.file,
|
||||
task.pool,
|
||||
null,
|
||||
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);
|
||||
}
|
||||
_stopProgressSyncTimer();
|
||||
_syncProgress();
|
||||
|
||||
if (queueOfUpload[queueIndex].error == null) {
|
||||
queueOfUpload.removeAt(queueIndex);
|
||||
isUploading.value = false;
|
||||
}
|
||||
_stopProgressSyncTimer();
|
||||
_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,
|
||||
null,
|
||||
onProgress: (value) {
|
||||
queueOfUpload[idx].progress = value;
|
||||
_progressOfUpload = (idx + value) / queueOfUpload.length;
|
||||
},
|
||||
onError: (err) {
|
||||
queueOfUpload[idx].error = err;
|
||||
queueOfUpload[idx].isUploading = false;
|
||||
},
|
||||
);
|
||||
_progressOfUpload = (idx + 1) / queueOfUpload.length;
|
||||
if (result != null) onData(result);
|
||||
try {
|
||||
final result = await _chunkedUploadAttachment(
|
||||
task.file,
|
||||
task.pool,
|
||||
null,
|
||||
onData: (_) {},
|
||||
onProgress: (progress) {
|
||||
queueOfUpload[idx].progress = progress;
|
||||
},
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
@ -88,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');
|
||||
|
||||
@ -101,7 +103,7 @@ class ChatCallProvider extends GetxController {
|
||||
endpoint = 'wss://${resp.body['endpoint']}';
|
||||
return (token!, endpoint!);
|
||||
} else {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,20 +30,20 @@ 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);
|
||||
}
|
||||
|
||||
Future<(List<Event>, int)?> getRemoteEvents(
|
||||
Channel channel,
|
||||
String scope, {
|
||||
required int remainDepth,
|
||||
bool Function(List<Event> items)? onBrake,
|
||||
take = 10,
|
||||
offset = 0,
|
||||
}) async {
|
||||
Channel channel,
|
||||
String scope, {
|
||||
required int remainDepth,
|
||||
bool Function(List<Event> items)? onBrake,
|
||||
take = 10,
|
||||
offset = 0,
|
||||
}) async {
|
||||
if (remainDepth <= 0) {
|
||||
return null;
|
||||
}
|
||||
@ -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);
|
||||
@ -69,13 +70,13 @@ Future<(List<Event>, int)?> getRemoteEvents(
|
||||
}
|
||||
|
||||
final expandResult = (await getRemoteEvents(
|
||||
channel,
|
||||
scope,
|
||||
remainDepth: remainDepth - 1,
|
||||
take: take,
|
||||
offset: offset + result.length,
|
||||
))
|
||||
?.$1 ??
|
||||
channel,
|
||||
scope,
|
||||
remainDepth: remainDepth - 1,
|
||||
take: take,
|
||||
offset: offset + result.length,
|
||||
))
|
||||
?.$1 ??
|
||||
List.empty();
|
||||
|
||||
return ([...result, ...expandResult], response.count);
|
||||
|
@ -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;
|
||||
|
@ -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,31 +51,31 @@ class WebSocketProvider extends GetxController {
|
||||
}
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
await auth.ensureCredentials();
|
||||
|
||||
if (auth.credentials == null) await auth.loadCredentials();
|
||||
|
||||
final uri = Uri.parse(ServiceFinder.buildUrl(
|
||||
'dealer',
|
||||
'/api/ws?tk=${auth.credentials!.accessToken}',
|
||||
).replaceFirst('http', 'ws'));
|
||||
|
||||
isConnecting.value = true;
|
||||
|
||||
try {
|
||||
await auth.ensureCredentials();
|
||||
|
||||
final uri = Uri.parse(ServiceFinder.buildUrl(
|
||||
'dealer',
|
||||
'/api/ws?tk=${auth.credentials!.accessToken}',
|
||||
).replaceFirst('http', 'ws'));
|
||||
|
||||
isConnecting.value = true;
|
||||
|
||||
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);
|
||||
}
|
||||
} finally {
|
||||
isConnecting.value = false;
|
||||
}
|
||||
|
||||
listen();
|
||||
|
||||
isConnected.value = true;
|
||||
isConnecting.value = false;
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
@ -148,7 +149,7 @@ class WebSocketProvider extends GetxController {
|
||||
'device_id': deviceUuid,
|
||||
});
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -13,8 +13,13 @@ import 'package:livekit_client/livekit_client.dart' as livekit;
|
||||
|
||||
class CallScreen extends StatefulWidget {
|
||||
final bool hideAppBar;
|
||||
final bool isExpandable;
|
||||
|
||||
const CallScreen({super.key, this.hideAppBar = false});
|
||||
const CallScreen({
|
||||
super.key,
|
||||
this.hideAppBar = false,
|
||||
this.isExpandable = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CallScreen> createState() => _CallScreenState();
|
||||
@ -308,13 +313,24 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
),
|
||||
);
|
||||
}),
|
||||
IconButton(
|
||||
icon: _layoutMode == 0
|
||||
? const Icon(Icons.view_list)
|
||||
: const Icon(Icons.grid_view),
|
||||
onPressed: () {
|
||||
_switchLayout();
|
||||
},
|
||||
Row(
|
||||
children: [
|
||||
if (widget.isExpandable)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.fullscreen),
|
||||
onPressed: () {
|
||||
ctrl.gotoScreen(context);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: _layoutMode == 0
|
||||
? const Icon(Icons.view_list)
|
||||
: const Icon(Icons.grid_view),
|
||||
onPressed: () {
|
||||
_switchLayout();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 20, right: 16),
|
||||
|
@ -141,7 +141,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:
|
||||
@ -269,26 +269,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();
|
||||
@ -331,7 +311,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
child: Row(children: [
|
||||
VerticalDivider(width: 0.3, thickness: 0.3),
|
||||
Expanded(
|
||||
child: CallScreen(hideAppBar: true),
|
||||
child: CallScreen(
|
||||
hideAppBar: true,
|
||||
isExpandable: true,
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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',
|
||||
@ -381,4 +383,6 @@ const i18nEnglish = {
|
||||
'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',
|
||||
};
|
||||
|
@ -41,6 +41,12 @@ const i18nSimplifiedChinese = {
|
||||
'openInBrowser': '在浏览器中打开',
|
||||
'notification': '通知',
|
||||
'errorHappened': '发生错误了',
|
||||
'errorHappenedUnauthorized': '未经授权的请求,请登录或尝试重新登录。',
|
||||
'errorHappenedRequestBad': '请求错误,服务器拒绝处理该请求,请检查您的请求数据。',
|
||||
'errorHappenedRequestForbidden': '请求错误,权限不足。',
|
||||
'errorHappenedRequestNotFound': '请求错误,请求的数据不存在。',
|
||||
'errorHappenedRequestConnection': '网络请求失败,请检查连接状态与服务状态后再试。',
|
||||
'errorHappenedRequestUnknown': '请求错误,类型未知,请将本提示完整截图提交反馈。',
|
||||
'forgotPassword': '忘记密码',
|
||||
'email': '邮件地址',
|
||||
'username': '用户名',
|
||||
@ -347,4 +353,6 @@ const i18nSimplifiedChinese = {
|
||||
'messageOutOfSyncCaption': '由于 App 进入后台,消息列表可能与服务器存在时差,点击刷新。',
|
||||
'messageHistoryWipe': '清除消息记录',
|
||||
'unknown': '未知',
|
||||
'collapse': '折叠',
|
||||
'expand': '展开',
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
if (item == null) return;
|
||||
widget.onAdd(item.rid);
|
||||
if (mounted) {
|
||||
setState(() => _attachments.add(item));
|
||||
if (widget.singleMode) Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
_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}) {
|
||||
@ -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',
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'In queue #${index + 1}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
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(
|
||||
'${(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,15 +606,13 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'attachmentAdd'.tr,
|
||||
style:
|
||||
Theme.of(context).textTheme.headlineSmall,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
'attachmentAdd'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Obx(() {
|
||||
|
@ -157,7 +157,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
final element = _attachmentsMeta[idx];
|
||||
idx++;
|
||||
if (element == null) return const SizedBox();
|
||||
double ratio = element.metadata!['ratio']?.toDouble() ?? 16 / 9;
|
||||
double ratio = element.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: widget.columnMaxWidth,
|
||||
|
@ -239,7 +239,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 +247,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,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.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';
|
||||
@ -12,6 +13,9 @@ class LinkExpansion extends StatelessWidget {
|
||||
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)
|
||||
: Image.network(url, width: width, height: height);
|
||||
@ -20,10 +24,7 @@ class LinkExpansion extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final linkRegex = RegExp(
|
||||
r'(?:(?:https?|ftp):\/\/|www\.)'
|
||||
r'(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)'
|
||||
r'[^\s<]*'
|
||||
r'[^\s<?!.,:*_~]',
|
||||
r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]',
|
||||
);
|
||||
final matches = linkRegex.allMatches(content);
|
||||
if (matches.isEmpty) {
|
||||
@ -46,7 +47,7 @@ class LinkExpansion extends StatelessWidget {
|
||||
}
|
||||
|
||||
final isRichDescription = [
|
||||
"solsynth.dev",
|
||||
'solsynth.dev',
|
||||
].contains(Uri.parse(snapshot.data!.url).host);
|
||||
|
||||
return GestureDetector(
|
||||
|
@ -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 {
|
||||
@ -40,137 +56,228 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
}
|
||||
}
|
||||
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
Widget _buildUserInfo() {
|
||||
return Obx(() {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
|
||||
if (_isCollapsed) {
|
||||
return InkWell(
|
||||
child: const Icon(Icons.account_circle).paddingSymmetric(
|
||||
horizontal: 28,
|
||||
vertical: 20,
|
||||
),
|
||||
onTap: () {
|
||||
AppRouter.instance.goNamed('account');
|
||||
_closeDrawer();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
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();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final leading = Obx(() {
|
||||
final statusBadgeColor = _accountStatus != null
|
||||
? StatusProvider.determineStatus(_accountStatus!).$2
|
||||
: Colors.grey;
|
||||
|
||||
final RelationshipProvider relations = Get.find();
|
||||
final accountNotifications = relations.friendRequestCount.value;
|
||||
|
||||
return badges.Badge(
|
||||
badgeContent: Text(
|
||||
accountNotifications.toString(),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
showBadge: accountNotifications > 0,
|
||||
position: badges.BadgePosition.topEnd(
|
||||
top: -10,
|
||||
end: -6,
|
||||
),
|
||||
child: badges.Badge(
|
||||
showBadge: _accountStatus != null,
|
||||
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
|
||||
position: badges.BadgePosition.bottomEnd(
|
||||
bottom: 0,
|
||||
end: -2,
|
||||
),
|
||||
child: AccountAvatar(
|
||||
content: auth.userProfile.value!['avatar'],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
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();
|
||||
},
|
||||
onLongPress: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => AccountStatusAction(
|
||||
currentStatus: _accountStatus!.status,
|
||||
),
|
||||
).then((val) {
|
||||
if (val == true) _getStatus();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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.animateTo(
|
||||
1,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@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) {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
return Drawer(
|
||||
backgroundColor:
|
||||
SolianTheme.isLargeScreen(context) ? Colors.transparent : null,
|
||||
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: [
|
||||
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),
|
||||
onTap: () {
|
||||
AppRouter.instance.goNamed('account');
|
||||
_closeDrawer();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
leading: Obx(() {
|
||||
final statusBadgeColor = _accountStatus != null
|
||||
? StatusProvider.determineStatus(
|
||||
_accountStatus!,
|
||||
).$2
|
||||
: Colors.grey;
|
||||
|
||||
final RelationshipProvider relations = Get.find();
|
||||
final accountNotifications =
|
||||
relations.friendRequestCount.value;
|
||||
|
||||
return badges.Badge(
|
||||
badgeContent: Text(
|
||||
accountNotifications.toString(),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
showBadge: accountNotifications > 0,
|
||||
position: badges.BadgePosition.topEnd(
|
||||
top: -10,
|
||||
end: -6,
|
||||
),
|
||||
child: badges.Badge(
|
||||
showBadge: _accountStatus != null,
|
||||
badgeStyle:
|
||||
badges.BadgeStyle(badgeColor: statusBadgeColor),
|
||||
position: badges.BadgePosition.bottomEnd(
|
||||
bottom: 0,
|
||||
end: -2,
|
||||
),
|
||||
child: AccountAvatar(
|
||||
content: auth.userProfile.value!['avatar'],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
onTap: () {
|
||||
AppRouter.instance.goNamed('account');
|
||||
_closeDrawer();
|
||||
},
|
||||
onLongPress: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => AccountStatusAction(
|
||||
currentStatus: _accountStatus!.status,
|
||||
),
|
||||
).then((val) {
|
||||
if (val == true) _getStatus();
|
||||
});
|
||||
},
|
||||
);
|
||||
}).paddingSymmetric(vertical: 8),
|
||||
_buildUserInfo().paddingSymmetric(vertical: 8),
|
||||
const Divider(thickness: 0.3, height: 1),
|
||||
Column(
|
||||
children: AppNavigation.destinations
|
||||
.map(
|
||||
(e) => ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
),
|
||||
leading: Icon(e.icon, size: 20).paddingAll(2),
|
||||
title: Text(e.label),
|
||||
enabled: true,
|
||||
onTap: () {
|
||||
AppRouter.instance.goNamed(e.page);
|
||||
_closeDrawer();
|
||||
},
|
||||
),
|
||||
(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: !_isCollapsed ? Text(e.label) : null,
|
||||
enabled: true,
|
||||
onTap: () {
|
||||
AppRouter.instance.goNamed(e.page);
|
||||
_closeDrawer();
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
).paddingSymmetric(vertical: 8),
|
||||
),
|
||||
const Divider(thickness: 0.3, height: 1),
|
||||
Expanded(
|
||||
child: AppNavigationRegions(
|
||||
child: AppNavigationRegion(
|
||||
isCollapsed: _isCollapsed,
|
||||
onSelected: (item) {
|
||||
_closeDrawer();
|
||||
},
|
||||
@ -179,18 +286,63 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
const Divider(thickness: 0.3, height: 1),
|
||||
Column(
|
||||
children: [
|
||||
ListTile(
|
||||
minTileHeight: 0,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
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(
|
||||
horizontal: 20,
|
||||
),
|
||||
leading: const Icon(Icons.settings, size: 20).paddingAll(2),
|
||||
title: Text('settings'.tr),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('settings');
|
||||
_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();
|
||||
},
|
||||
),
|
||||
leading: const Icon(Icons.settings, size: 20).paddingAll(2),
|
||||
title: Text('settings'.tr),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('settings');
|
||||
_closeDrawer();
|
||||
},
|
||||
),
|
||||
],
|
||||
).paddingOnly(
|
||||
top: 8,
|
||||
|
@ -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)),
|
@ -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
|
||||
|
||||
|
42
pubspec.lock
42
pubspec.lock
@ -263,7 +263,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||
@ -787,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
|
||||
@ -1293,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:
|
||||
@ -1978,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:
|
||||
|
@ -2,7 +2,7 @@ name: solian
|
||||
description: "The Solar Network App"
|
||||
publish_to: "none"
|
||||
|
||||
version: 1.2.1+19
|
||||
version: 1.2.1+22
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.4 <4.0.0"
|
||||
@ -71,6 +71,8 @@ dependencies:
|
||||
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:
|
||||
|
Reference in New Issue
Block a user