Compare commits

...

20 Commits

Author SHA1 Message Date
19eabfaba1 🚀 Launch 1.2.1+2 2024-08-04 13:27:14 +08:00
ec2eadad6d 🐛 Fix bootstrapper icon issue 2024-08-04 12:59:13 +08:00
54e176e75d 🐛 Fix post editor cannot reply either repost 2024-08-04 12:55:05 +08:00
0a7ccaeefa 🐛 Fix attachment editor title overflow 2024-08-04 12:23:39 +08:00
a5f093e185 🐛 Fix unauthorized wont load stickers 2024-08-04 11:10:25 +08:00
a4f68dd175 🚀 Launch 1.2.1+1 2024-08-04 01:54:35 +08:00
8067c35c70 Follow the manifest to load emotes 2024-08-04 01:53:52 +08:00
ebe381053e Load emojis 2024-08-04 01:37:54 +08:00
03f2470dae Basic sticker management 2024-08-04 01:03:09 +08:00
ea434815cf Create sticker
 Single file mode attachment editor and more options
2024-08-03 21:29:48 +08:00
bbea4b4359 🍱 Update app icons 2024-08-03 17:44:36 +08:00
e0b485cc81 🐛 Fix mis-style 2024-08-03 14:00:52 +08:00
87bb37ac01 ⚗️ Markdown embed content 2024-08-03 12:29:13 +08:00
989b5babd9 Auto update checking 2024-08-03 01:14:42 +08:00
9ea364640d 🚀 Launch 1.2.0+8 2024-08-02 23:24:36 +08:00
a9f55a489d ⬆️ Clean and upgrade packages 2024-08-02 23:22:50 +08:00
4616f3a3e2 Friend request indicator 2024-08-02 23:15:28 +08:00
425bae9d13 💄 Better friend page loading indicator 2024-08-02 22:54:56 +08:00
07771e8979 Improve the speed of fetching attachments meta via batch api 2024-08-02 22:46:48 +08:00
0ad4854443 💄 Grid view in call 2024-08-02 21:12:37 +08:00
81 changed files with 1265 additions and 353 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 360 KiB

View File

@@ -113,7 +113,7 @@ PODS:
- TOCropViewController (~> 2.7.4) - TOCropViewController (~> 2.7.4)
- image_picker_ios (0.0.1): - image_picker_ios (0.0.1):
- Flutter - Flutter
- livekit_client (2.2.2): - livekit_client (2.2.3):
- Flutter - Flutter
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
- media_kit_libs_ios_video (1.0.4): - media_kit_libs_ios_video (1.0.4):
@@ -295,7 +295,7 @@ SPEC CHECKSUMS:
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
livekit_client: c767049a635d5b6d43de3273dca3c439b8a6e970 livekit_client: bad83a7776a41abc42e1f26d903eeac9164c8a9f
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 B

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 815 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,12 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@@ -24,6 +26,7 @@ class BootstrapperShell extends StatefulWidget {
class _BootstrapperShellState extends State<BootstrapperShell> { class _BootstrapperShellState extends State<BootstrapperShell> {
bool _isBusy = true; bool _isBusy = true;
bool _isErrored = false; bool _isErrored = false;
bool _isDismissable = true;
String? _subtitle; String? _subtitle;
Color get _unFocusColor => Color get _unFocusColor =>
@@ -38,6 +41,32 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
await context.read<ThemeSwitcher>().restoreTheme(); await context.read<ThemeSwitcher>().restoreTheme();
}, },
), ),
(
label: 'bsCheckForUpdate',
action: () async {
if (PlatformInfo.isWeb) return;
try {
final info = await PackageInfo.fromPlatform();
final localVersionString = '${info.version}+${info.buildNumber}';
final resp = await GetConnect().get(
'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?limit=1',
);
if (resp.body[0]['name'] != localVersionString) {
setState(() {
_isErrored = true;
_subtitle = PlatformInfo.isIOS || PlatformInfo.isMacOS
? 'bsCheckForUpdateDescApple'.tr
: 'bsCheckForUpdateDescCommon'.tr;
});
}
} catch (e) {
setState(() {
_isErrored = true;
_subtitle = 'bsCheckForUpdateFailed'.tr;
});
}
},
),
( (
label: 'bsCheckingServer', label: 'bsCheckingServer',
action: () async { action: () async {
@@ -47,12 +76,14 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
setState(() { setState(() {
_isErrored = true; _isErrored = true;
_subtitle = 'bsCheckingServerDown'.tr; _subtitle = 'bsCheckingServerDown'.tr;
_isDismissable = false;
}); });
throw Exception('unable connect to server'); throw Exception('unable connect to server');
} else if (resp.statusCode == null) { } else if (resp.statusCode == null) {
setState(() { setState(() {
_isErrored = true; _isErrored = true;
_subtitle = 'bsCheckingServerFail'.tr; _subtitle = 'bsCheckingServerFail'.tr;
_isDismissable = false;
}); });
throw Exception('unable connect to server'); throw Exception('unable connect to server');
} }
@@ -81,13 +112,15 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
label: 'bsPreparingData', label: 'bsPreparingData',
action: () async { action: () async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isTrue) { await Future.wait([
await Future.wait([ Get.find<StickerProvider>().refreshAvailableStickers(),
Get.find<RealmProvider>().refreshAvailableRealms(), if (auth.isAuthorized.isTrue)
Get.find<ChannelProvider>().refreshAvailableChannel(), Get.find<ChannelProvider>().refreshAvailableChannel(),
Get.find<RelationshipProvider>().refreshFriendList(), if (auth.isAuthorized.isTrue)
]); Get.find<RelationshipProvider>().refreshRelativeList(),
} if (auth.isAuthorized.isTrue)
Get.find<RealmProvider>().refreshAvailableRealms(),
]);
}, },
), ),
( (
@@ -111,7 +144,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
try { try {
for (var idx = 0; idx < _periods.length; idx++) { for (var idx = 0; idx < _periods.length; idx++) {
await _periods[idx].action(); await _periods[idx].action();
if (_isErrored) break; if (_isErrored && !_isDismissable) break;
if (_periodCursor < _periods.length - 1) { if (_periodCursor < _periods.length - 1) {
setState(() => _periodCursor++); setState(() => _periodCursor++);
} }
@@ -140,17 +173,20 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
height: 280, height: 280,
child: Align( child: Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Image.asset('assets/logo.png', width: 80, height: 80) child: ClipRRect(
.animate(onPlay: (c) => c.repeat()) borderRadius: const BorderRadius.all(Radius.circular(16)),
.rotate(duration: 850.ms, curve: Curves.easeInOut), child: Image.asset('assets/logo.png', width: 80, height: 80),
),
), ),
), ),
GestureDetector( GestureDetector(
child: Column( child: Column(
children: [ children: [
if (_isErrored) if (_isErrored && !_isDismissable && !_isBusy)
const Icon(Icons.cancel, size: 24) const Icon(Icons.cancel, size: 24),
else if (_isErrored && _isDismissable && !_isBusy)
const Icon(Icons.warning, size: 24),
if ((_isErrored && _isDismissable && _isBusy) || _isBusy)
const SizedBox( const SizedBox(
width: 24, width: 24,
height: 24, height: 24,
@@ -161,15 +197,24 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
maxWidth: 280, maxWidth: 280,
child: Column( child: Column(
children: [ children: [
Text( if (_subtitle == null)
_subtitle ?? Text(
'${_periods[_periodCursor].label.tr} (${_periodCursor + 1}/${_periods.length})', '${_periods[_periodCursor].label.tr} (${_periodCursor + 1}/${_periods.length})',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: _unFocusColor, color: _unFocusColor,
),
), ),
), if (_subtitle != null)
Text(
_subtitle!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: _unFocusColor,
),
).paddingOnly(bottom: 4),
Text( Text(
'2024 © Solsynth LLC', '2024 © Solsynth LLC',
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -185,12 +230,19 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
), ),
onTap: () { onTap: () {
if (_isBusy) return; if (_isBusy) return;
setState(() { if (_isDismissable) {
_isBusy = true; setState(() {
_isErrored = false; _isBusy = false;
_periodCursor = 0; _isErrored = false;
}); });
_runPeriods(); } else {
setState(() {
_isBusy = true;
_isErrored = false;
_periodCursor = 0;
});
_runPeriods();
}
}, },
) )
], ],

View File

@@ -30,6 +30,23 @@ extension SolianExtenions on BuildContext {
); );
} }
Future<void> showInfoDialog(String title, body) {
return showDialog<void>(
useRootNavigator: true,
context: this,
builder: (ctx) => AlertDialog(
title: Text(title),
content: Text(body),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text('okay'.tr),
)
],
),
);
}
Future<void> showErrorDialog(dynamic exception) { Future<void> showErrorDialog(dynamic exception) {
var stack = StackTrace.current; var stack = StackTrace.current;
var stackTrace = '$stack'; var stackTrace = '$stack';

View File

@@ -11,6 +11,7 @@ import 'package:solian/bootstrapper.dart';
import 'package:solian/firebase_options.dart'; import 'package:solian/firebase_options.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/attachment_uploader.dart'; import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
@@ -120,6 +121,7 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => AuthProvider()); Get.lazyPut(() => AuthProvider());
Get.lazyPut(() => RelationshipProvider()); Get.lazyPut(() => RelationshipProvider());
Get.lazyPut(() => PostProvider()); Get.lazyPut(() => PostProvider());
Get.lazyPut(() => StickerProvider());
Get.lazyPut(() => AttachmentProvider()); Get.lazyPut(() => AttachmentProvider());
Get.lazyPut(() => WebSocketProvider()); Get.lazyPut(() => WebSocketProvider());
Get.lazyPut(() => StatusProvider()); Get.lazyPut(() => StatusProvider());

122
lib/models/stickers.dart Normal file
View File

@@ -0,0 +1,122 @@
import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart';
class Sticker {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String alias;
String name;
int attachmentId;
Attachment attachment;
int packId;
StickerPack? pack;
int accountId;
Account account;
Sticker({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.alias,
required this.name,
required this.attachmentId,
required this.attachment,
required this.packId,
required this.pack,
required this.accountId,
required this.account,
});
factory Sticker.fromJson(Map<String, dynamic> json) => Sticker(
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'])
: json['deleted_at'],
alias: json['alias'],
name: json['name'],
attachmentId: json['attachment_id'],
attachment: Attachment.fromJson(json['attachment']),
packId: json['pack_id'],
pack: json['pack'] != null ? StickerPack.fromJson(json['pack']) : null,
accountId: json['account_id'],
account: Account.fromJson(json['account']),
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'alias': alias,
'name': name,
'attachment_id': attachmentId,
'attachment': attachment.toJson(),
'pack_id': packId,
'account_id': accountId,
'account': account.toJson(),
};
}
class StickerPack {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String prefix;
String name;
String description;
List<Sticker>? stickers;
int accountId;
Account account;
StickerPack({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.prefix,
required this.name,
required this.description,
required this.stickers,
required this.accountId,
required this.account,
});
factory StickerPack.fromJson(Map<String, dynamic> json) => StickerPack(
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'])
: json['deleted_at'],
prefix: json['prefix'],
name: json['name'],
description: json['description'],
stickers: json['stickers'] == null
? []
: List<Sticker>.from(
json['stickers']!.map((x) => Sticker.fromJson(x))),
accountId: json['account_id'],
account: Account.fromJson(json['account']),
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'prefix': prefix,
'name': name,
'description': description,
'stickers': stickers == null
? []
: List<dynamic>.from(stickers!.map((x) => x.toJson())),
'account_id': accountId,
'account': account.toJson(),
};
}

View File

@@ -14,6 +14,7 @@ class AttachmentUploadTask {
double progress = 0; double progress = 0;
bool isUploading = false; bool isUploading = false;
bool isCompleted = false; bool isCompleted = false;
dynamic error;
AttachmentUploadTask({ AttachmentUploadTask({
required this.file, required this.file,
@@ -66,7 +67,7 @@ class AttachmentUploaderController extends GetxController {
queueOfUpload.remove(task); queueOfUpload.remove(task);
} }
Future<Attachment> performSingleTask(int queueIndex) async { Future<Attachment?> performSingleTask(int queueIndex) async {
isUploading.value = true; isUploading.value = true;
progressOfUpload.value = 0; progressOfUpload.value = 0;
@@ -83,9 +84,15 @@ class AttachmentUploaderController extends GetxController {
queueOfUpload[queueIndex].progress = value; queueOfUpload[queueIndex].progress = value;
_progressOfUpload = value; _progressOfUpload = value;
}, },
onError: (err) {
queueOfUpload[queueIndex].error = err;
queueOfUpload[queueIndex].isUploading = false;
},
); );
queueOfUpload.removeAt(queueIndex); if (queueOfUpload[queueIndex].error == null) {
queueOfUpload.removeAt(queueIndex);
}
_stopProgressSyncTimer(); _stopProgressSyncTimer();
_syncProgress(); _syncProgress();
@@ -103,6 +110,10 @@ class AttachmentUploaderController extends GetxController {
_startProgressSyncTimer(); _startProgressSyncTimer();
for (var idx = 0; idx < queueOfUpload.length; idx++) { for (var idx = 0; idx < queueOfUpload.length; idx++) {
if (queueOfUpload[idx].isUploading || queueOfUpload[idx].error != null) {
continue;
}
queueOfUpload[idx].isUploading = true; queueOfUpload[idx].isUploading = true;
final task = queueOfUpload[idx]; final task = queueOfUpload[idx];
@@ -115,15 +126,20 @@ class AttachmentUploaderController extends GetxController {
queueOfUpload[idx].progress = value; queueOfUpload[idx].progress = value;
_progressOfUpload = (idx + value) / queueOfUpload.length; _progressOfUpload = (idx + value) / queueOfUpload.length;
}, },
onError: (err) {
queueOfUpload[idx].error = err;
queueOfUpload[idx].isUploading = false;
},
); );
_progressOfUpload = (idx + 1) / queueOfUpload.length; _progressOfUpload = (idx + 1) / queueOfUpload.length;
onData(result); if (result != null) onData(result);
queueOfUpload[idx].isUploading = false; queueOfUpload[idx].isUploading = false;
queueOfUpload[idx].isCompleted = false; queueOfUpload[idx].isCompleted = true;
} }
queueOfUpload.clear(); queueOfUpload.value =
queueOfUpload.where((x) => x.error == null).toList(growable: true);
_stopProgressSyncTimer(); _stopProgressSyncTimer();
_syncProgress(); _syncProgress();
@@ -135,7 +151,7 @@ class AttachmentUploaderController extends GetxController {
String path, String path,
String usage, String usage,
Map<String, dynamic>? metadata, Map<String, dynamic>? metadata,
Function(Attachment) callback, Function(Attachment?) callback,
) async { ) async {
if (isUploading.value) throw Exception('uploading blocked'); if (isUploading.value) throw Exception('uploading blocked');
@@ -153,7 +169,7 @@ class AttachmentUploaderController extends GetxController {
callback(result); callback(result);
} }
Future<Attachment> uploadAttachment( Future<Attachment?> uploadAttachment(
Uint8List data, Uint8List data,
String path, String path,
String usage, String usage,
@@ -175,9 +191,9 @@ class AttachmentUploaderController extends GetxController {
return result; return result;
} }
Future<Attachment> _rawUploadAttachment( Future<Attachment?> _rawUploadAttachment(
Uint8List data, String path, String usage, Map<String, dynamic>? metadata, Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
{Function(double)? onProgress}) async { {Function(double)? onProgress, Function(dynamic err)? onError}) async {
final AttachmentProvider provider = Get.find(); final AttachmentProvider provider = Get.find();
try { try {
final result = await provider.createAttachment( final result = await provider.createAttachment(
@@ -189,7 +205,10 @@ class AttachmentUploaderController extends GetxController {
); );
return result; return result;
} catch (err) { } catch (err) {
rethrow; if (onError != null) {
onError(err);
}
return null;
} }
} }
} }

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_rx/get_rx.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:solian/models/call.dart'; import 'package:solian/models/call.dart';

View File

@@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:dio/dio.dart' as dio; import 'package:dio/dio.dart' as dio;
@@ -21,6 +22,50 @@ class AttachmentProvider extends GetConnect {
final Map<int, Attachment> _cachedResponses = {}; final Map<int, Attachment> _cachedResponses = {};
Future<List<Attachment?>> listMetadata(
List<int> id, {
noCache = false,
}) async {
if (id.isEmpty) return List.empty();
List<Attachment?> result = List.filled(id.length, null);
List<int> pendingQuery = List.empty(growable: true);
if (!noCache) {
for (var idx = 0; idx < id.length; idx++) {
if (_cachedResponses.containsKey(id[idx])) {
result[idx] = _cachedResponses[id[idx]];
} else {
pendingQuery.add(id[idx]);
}
}
}
final resp = await get(
'/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}',
);
if (resp.statusCode != 200) return result;
final rawOut = PaginationResult.fromJson(resp.body);
if (rawOut.data == null) return result;
final List<Attachment> out =
rawOut.data!.map((x) => Attachment.fromJson(x)).toList();
for (final item in out) {
if (item.destination != 0 && item.isAnalyzed) {
_cachedResponses[item.id] = item;
}
}
for (var i = 0; i < out.length; i++) {
for (var j = 0; j < id.length; j++) {
if (out[i].id == id[j]) {
result[j] = out[i];
}
}
}
return result;
}
Future<Attachment?> getMetadata(int id, {noCache = false}) async { Future<Attachment?> getMetadata(int id, {noCache = false}) async {
if (!noCache && _cachedResponses.containsKey(id)) { if (!noCache && _cachedResponses.containsKey(id)) {
return _cachedResponses[id]!; return _cachedResponses[id]!;

View File

@@ -4,15 +4,19 @@ import 'package:solian/models/relations.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
class RelationshipProvider extends GetxController { class RelationshipProvider extends GetxController {
final RxInt friendRequestCount = 0.obs;
final RxList<Relationship> _friends = RxList.empty(growable: true); final RxList<Relationship> _friends = RxList.empty(growable: true);
Future<void> refreshFriendList() async { Future<void> refreshRelativeList() async {
final resp = await listRelationWithStatus(1); final resp = await listRelation();
_friends.value = resp.body final List<Relationship> result = resp.body
.map((e) => Relationship.fromJson(e)) .map((e) => Relationship.fromJson(e))
.toList() .toList()
.cast<Relationship>(); .cast<Relationship>();
_friends.value = result.where((x) => x.status == 1).toList();
_friends.refresh(); _friends.refresh();
friendRequestCount.value = result.where((x) => x.status == 0).length;
} }
bool hasFriend(Account account) { bool hasFriend(Account account) {

View File

@@ -0,0 +1,38 @@
import 'package:get/get.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/stickers.dart';
import 'package:solian/services.dart';
class StickerProvider extends GetxController {
final RxMap<String, String> aliasImageMapping = RxMap();
final RxMap<String, List<Sticker>> availableStickers = RxMap();
Future<void> refreshAvailableStickers() async {
final client = ServiceFinder.configureClient('files');
final resp = await client.get(
'/stickers/manifest?take=100',
);
if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body);
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
if (out == null) return;
for (final pack in out) {
for (final sticker in (pack.stickers ?? List<Sticker>.empty())) {
sticker.pack = pack;
final imageUrl = ServiceFinder.buildUrl(
'files',
'/attachments/${sticker.attachmentId}',
);
aliasImageMapping['${pack.prefix}${sticker.alias}'.camelCase!] =
imageUrl;
if (availableStickers[pack.prefix] == null) {
availableStickers[pack.prefix] = List.empty(growable: true);
}
availableStickers[pack.prefix]!.add(sticker);
}
}
}
availableStickers.refresh();
}
}

View File

@@ -7,6 +7,7 @@ import 'package:solian/screens/account.dart';
import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/friend.dart';
import 'package:solian/screens/account/personalize.dart'; import 'package:solian/screens/account/personalize.dart';
import 'package:solian/screens/account/profile_page.dart'; import 'package:solian/screens/account/profile_page.dart';
import 'package:solian/screens/account/stickers.dart';
import 'package:solian/screens/channel/channel_chat.dart'; import 'package:solian/screens/channel/channel_chat.dart';
import 'package:solian/screens/channel/channel_detail.dart'; import 'package:solian/screens/channel/channel_detail.dart';
import 'package:solian/screens/channel/channel_organize.dart'; import 'package:solian/screens/channel/channel_organize.dart';
@@ -226,6 +227,14 @@ abstract class AppRouter {
name: 'accountFriend', name: 'accountFriend',
builder: (context, state) => const FriendScreen(), builder: (context, state) => const FriendScreen(),
), ),
GoRoute(
path: '/account/stickers',
name: 'accountStickers',
builder: (context, state) => TitleShell(
state: state,
child: const StickerScreen(),
),
),
GoRoute( GoRoute(
path: '/account/personalize', path: '/account/personalize',
name: 'accountPersonalize', name: 'accountPersonalize',

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@@ -18,9 +17,11 @@ class AboutScreen extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Image.asset('assets/logo.png', width: 64, height: 64) ClipRRect(
.animate(onPlay: (c) => c.repeat()) borderRadius: const BorderRadius.all(Radius.circular(16)),
.rotate(duration: 1000.ms), child: Image.asset('assets/logo.png', width: 120, height: 120),
),
const SizedBox(height: 8),
Text( Text(
'Solian', 'Solian',
style: Theme.of(context).textTheme.headlineMedium, style: Theme.of(context).textTheme.headlineMedium,
@@ -56,10 +57,9 @@ class AboutScreen extends StatelessWidget {
applicationVersion: '${info.version} (${info.buildNumber})', applicationVersion: '${info.version} (${info.buildNumber})',
applicationLegalese: applicationLegalese:
'The Solar Network App is an intuitive and self-hostable social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.', 'The Solar Network App is an intuitive and self-hostable social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
applicationIcon: Image.asset( applicationIcon: ClipRRect(
'assets/logo.png', borderRadius: const BorderRadius.all(Radius.circular(16)),
width: 56, child: Image.asset('assets/logo.png', width: 60, height: 60),
height: 56,
), ),
); );
}, },

View File

@@ -3,11 +3,13 @@ import 'package:get/get.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/auth/signin.dart'; import 'package:solian/screens/auth/signin.dart';
import 'package:solian/screens/auth/signup.dart'; import 'package:solian/screens/auth/signup.dart';
import 'package:solian/widgets/account/account_heading.dart'; import 'package:solian/widgets/account/account_heading.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
import 'package:badges/badges.dart' as badges;
class AccountScreen extends StatefulWidget { class AccountScreen extends StatefulWidget {
const AccountScreen({super.key}); const AccountScreen({super.key});
@@ -23,9 +25,32 @@ class _AccountScreenState extends State<AccountScreen> {
( (
const Icon(Icons.color_lens), const Icon(Icons.color_lens),
'accountPersonalize'.tr, 'accountPersonalize'.tr,
'accountPersonalize' 'accountPersonalize',
),
(
Obx(() {
final RelationshipProvider relations = Get.find();
return badges.Badge(
badgeContent: Text(
relations.friendRequestCount.value.toString(),
style: const TextStyle(color: Colors.white),
),
showBadge: relations.friendRequestCount.value > 0,
position: badges.BadgePosition.topEnd(
top: -12,
end: -8,
),
child: const Icon(Icons.diversity_1),
);
}),
'accountFriend'.tr,
'accountFriend',
),
(
const Icon(Icons.emoji_symbols),
'accountStickers'.tr,
'accountStickers',
), ),
(const Icon(Icons.diversity_1), 'accountFriend'.tr, 'accountFriend'),
]; ];
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/relations.dart'; import 'package:solian/models/relations.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/account/relative_list.dart'; import 'package:solian/widgets/account/relative_list.dart';
class FriendScreen extends StatefulWidget { class FriendScreen extends StatefulWidget {
@@ -21,15 +21,15 @@ class _FriendScreenState extends State<FriendScreen>
List<Relationship> _relations = List.empty(); List<Relationship> _relations = List.empty();
List<Relationship> filterByStatus(int status) { List<Relationship> _filterByStatus(int status) {
return _relations.where((x) => x.status == status).toList(); return _relations.where((x) => x.status == status).toList();
} }
Future<void> loadRelations() async { Future<void> _loadRelations() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final RelationshipProvider provider = Get.find(); final RelationshipProvider relations = Get.find();
final resp = await provider.listRelation(); final resp = await relations.listRelation();
setState(() { setState(() {
_relations = resp.body _relations = resp.body
@@ -38,9 +38,12 @@ class _FriendScreenState extends State<FriendScreen>
.cast<Relationship>(); .cast<Relationship>();
_isBusy = false; _isBusy = false;
}); });
relations.friendRequestCount.value =
_relations.where((x) => x.status == 0).length;
} }
void promptAddFriend() async { void _promptAddFriend() async {
final RelationshipProvider provider = Get.find(); final RelationshipProvider provider = Get.find();
final controller = TextEditingController(); final controller = TextEditingController();
@@ -104,8 +107,8 @@ class _FriendScreenState extends State<FriendScreen>
super.initState(); super.initState();
_tabController = TabController(length: 3, vsync: this); _tabController = TabController(length: 3, vsync: this);
loadRelations().then((_) { _loadRelations().then((_) {
if (filterByStatus(0).isEmpty) { if (_filterByStatus(0).isEmpty) {
_tabController.animateTo(1); _tabController.animateTo(1);
} }
}); });
@@ -119,6 +122,19 @@ class _FriendScreenState extends State<FriendScreen>
appBar: AppBar( appBar: AppBar(
centerTitle: false, centerTitle: false,
title: Text('accountFriend'.tr), title: Text('accountFriend'.tr),
actions: [
if (_isBusy)
SizedBox(
height: 48,
width: 48,
child: const CircularProgressIndicator(
strokeWidth: 3,
).paddingAll(14),
),
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
),
],
bottom: TabBar( bottom: TabBar(
controller: _tabController, controller: _tabController,
tabs: const [ tabs: const [
@@ -130,52 +146,40 @@ class _FriendScreenState extends State<FriendScreen>
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add), child: const Icon(Icons.add),
onPressed: () => promptAddFriend(), onPressed: () => _promptAddFriend(),
), ),
body: TabBarView( body: TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
RefreshIndicator( RefreshIndicator(
onRefresh: () => loadRelations(), onRefresh: () => _loadRelations(),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
SilverRelativeList( SilverRelativeList(
items: filterByStatus(0), items: _filterByStatus(0),
onUpdate: () => loadRelations(), onUpdate: () => _loadRelations(),
), ),
], ],
), ),
), ),
RefreshIndicator( RefreshIndicator(
onRefresh: () => loadRelations(), onRefresh: () => _loadRelations(),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
SilverRelativeList( SilverRelativeList(
items: filterByStatus(1), items: _filterByStatus(1),
onUpdate: () => loadRelations(), onUpdate: () => _loadRelations(),
), ),
], ],
), ),
), ),
RefreshIndicator( RefreshIndicator(
onRefresh: () => loadRelations(), onRefresh: () => _loadRelations(),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
SilverRelativeList( SilverRelativeList(
items: filterByStatus(3), items: _filterByStatus(3),
onUpdate: () => loadRelations(), onUpdate: () => _loadRelations(),
), ),
], ],
), ),

View File

@@ -86,11 +86,17 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
toolbarTitle: 'cropImage'.tr, toolbarTitle: 'cropImage'.tr,
toolbarColor: Theme.of(context).colorScheme.primary, toolbarColor: Theme.of(context).colorScheme.primary,
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary, toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
aspectRatioPresets: [CropAspectRatioPreset.square], aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
), ),
IOSUiSettings( IOSUiSettings(
title: 'cropImage'.tr, title: 'cropImage'.tr,
aspectRatioPresets: [CropAspectRatioPreset.square], aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
), ),
WebUiSettings( WebUiSettings(
context: context, context: context,
@@ -346,3 +352,11 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
super.dispose(); super.dispose();
} }
} }
class _BannerCropAspectRatioPreset extends CropAspectRatioPresetData {
@override
(int, int)? get data => (16, 7);
@override
String get name => '16x7';
}

View File

@@ -40,7 +40,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
List<Post> _pinnedPosts = List.empty(); List<Post> _pinnedPosts = List.empty();
int _totalUpvote = 0, _totalDownvote = 0; int _totalUpvote = 0, _totalDownvote = 0;
Future<void> getUserinfo() async { Future<void> _getUserinfo() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
var client = ServiceFinder.configureClient('auth'); var client = ServiceFinder.configureClient('auth');
@@ -114,7 +114,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
} }
}); });
getUserinfo(); _getUserinfo();
getPinnedPosts(); getPinnedPosts();
} }
@@ -189,8 +189,11 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
: () async { : () async {
setState(() => _isMakingFriend = true); setState(() => _isMakingFriend = true);
try { try {
await _relationshipProvider.makeFriend(widget.name); await _relationshipProvider
context.showSnackbar('accountFriendRequestSent'.tr); .makeFriend(widget.name);
context.showSnackbar(
'accountFriendRequestSent'.tr,
);
} catch (e) { } catch (e) {
context.showErrorDialog(e); context.showErrorDialog(e);
} finally { } finally {
@@ -274,6 +277,8 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
color: color:
Theme.of(context).colorScheme.surfaceContainerLow, Theme.of(context).colorScheme.surfaceContainerLow,
child: PostListEntryWidget( child: PostListEntryWidget(
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
item: element, item: element,
isClickable: true, isClickable: true,
isNestedClickable: true, isNestedClickable: true,

View File

@@ -0,0 +1,181 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/stickers.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/stickers/sticker_uploader.dart';
class StickerScreen extends StatefulWidget {
const StickerScreen({super.key});
@override
State<StickerScreen> createState() => _StickerScreenState();
}
class _StickerScreenState extends State<StickerScreen> {
final PagingController<int, StickerPack> _pagingController =
PagingController(firstPageKey: 0);
Future<bool> _promptDelete(Sticker item, String prefix) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return false;
final confirm = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('stickerDeletionConfirm'.tr),
content: Text(
'stickerDeletionConfirmCaption'.trParams({
'name': ':${'$prefix${item.alias}'.camelCase}:',
}),
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('cancel'.tr),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('confirm'.tr),
),
],
),
);
if (confirm != true) return false;
final client = auth.configureClient('files');
final resp = await client.delete('/stickers/${item.id}');
return resp.statusCode == 200;
}
Future<bool?> _promptUploadSticker({Sticker? edit}) {
return showDialog(
context: context,
builder: (context) => StickerUploadDialog(
edit: edit,
),
);
}
Widget _buildEmoteEntry(Sticker item, String prefix) {
final imageUrl = ServiceFinder.buildUrl(
'files',
'/attachments/${item.attachmentId}',
);
return ListTile(
title: Text(item.name),
subtitle: Text(':${'$prefix${item.alias}'.camelCase}:'),
contentPadding: const EdgeInsets.only(left: 16, right: 14),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit_square),
onPressed: () {
_promptUploadSticker(edit: item).then((value) {
if (value == true) _pagingController.refresh();
});
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
_promptDelete(item, prefix).then((value) {
if (value == true) _pagingController.refresh();
});
},
),
],
),
leading: PlatformInfo.canCacheImage
? CachedNetworkImage(
imageUrl: imageUrl,
width: 28,
height: 28,
)
: Image.network(
imageUrl,
width: 28,
height: 28,
),
);
}
@override
void initState() {
final AuthProvider auth = Get.find();
final name = auth.userProfile.value!['name'];
_pagingController.addPageRequestListener((pageKey) async {
final client = ServiceFinder.configureClient('files');
final resp = await client.get(
'/stickers/manifest?take=10&offset=$pageKey&author=$name',
);
if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body);
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
if (out != null && result.data!.length >= 10) {
_pagingController.appendPage(out, pageKey + out.length);
} else if (out != null) {
_pagingController.appendLastPage(out);
}
} else {
_pagingController.error = resp.bodyString;
}
});
super.initState();
}
@override
void dispose() {
final StickerProvider sticker = Get.find();
sticker.refreshAvailableStickers();
_pagingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
_promptUploadSticker().then((value) {
if (value == true) _pagingController.refresh();
});
},
),
body: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView(
slivers: [
PagedSliverList<int, StickerPack>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate(
itemBuilder: (BuildContext context, item, int index) {
return ExpansionTile(
title: Text(item.name),
subtitle: Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
children: item.stickers
?.map((x) => _buildEmoteEntry(x, item.prefix))
.toList() ??
List.empty(),
);
},
),
),
],
),
),
);
}
}

View File

@@ -87,75 +87,77 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
Widget _buildListLayout() { Widget _buildListLayout() {
final ChatCallProvider call = Get.find(); final ChatCallProvider call = Get.find();
return Stack( return Obx(
children: [ () => Stack(
Container( children: [
color: Theme.of(context).colorScheme.surfaceContainer, Container(
child: call.focusTrack.value != null color: Theme.of(context).colorScheme.surfaceContainer,
? InteractiveParticipantWidget( child: call.focusTrack.value != null
isFixed: false, ? InteractiveParticipantWidget(
participant: call.focusTrack.value!, isFixedAvatar: false,
onTap: () {}, participant: call.focusTrack.value!,
) onTap: () {},
: const SizedBox(), )
), : const SizedBox(),
Positioned( ),
left: 0, Positioned(
right: 0, left: 0,
top: 0, right: 0,
child: SizedBox( top: 0,
height: 128, child: SizedBox(
child: ListView.builder( height: 128,
scrollDirection: Axis.horizontal, child: ListView.builder(
itemCount: math.max(0, call.participantTracks.length), scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) { itemCount: math.max(0, call.participantTracks.length),
final track = call.participantTracks[index]; itemBuilder: (BuildContext context, int index) {
if (track.participant.sid == final track = call.participantTracks[index];
call.focusTrack.value?.participant.sid) { if (track.participant.sid ==
return Container(); call.focusTrack.value?.participant.sid) {
} return Container();
}
return Padding( return Padding(
padding: const EdgeInsets.only(top: 8, left: 8), padding: const EdgeInsets.only(top: 8, left: 8),
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget( child: InteractiveParticipantWidget(
isFixed: true, isFixedAvatar: true,
width: 120, width: 120,
height: 120, height: 120,
color: Theme.of(context).cardColor, color: Theme.of(context).cardColor,
participant: track, participant: track,
onTap: () { onTap: () {
if (track.participant.sid != if (track.participant.sid !=
call.focusTrack.value?.participant.sid) { call.focusTrack.value?.participant.sid) {
call.changeFocusTrack(track); call.changeFocusTrack(track);
} }
}, },
),
), ),
), );
); },
}, ),
), ),
), ),
), ],
], ),
); );
} }
Widget _buildGridLayout() { Widget _buildGridLayout() {
final ChatCallProvider call = Get.find(); final ChatCallProvider call = Get.find();
return LayoutBuilder( return LayoutBuilder(builder: (context, constraints) {
builder: (context, constraints) { double screenWidth = constraints.maxWidth;
double screenWidth = constraints.maxWidth; double screenHeight = constraints.maxHeight;
double screenHeight = constraints.maxHeight;
int columns = (math.sqrt(call.participantTracks.length)).ceil(); int columns = (math.sqrt(call.participantTracks.length)).ceil();
int rows = (call.participantTracks.length / columns).ceil(); int rows = (call.participantTracks.length / columns).ceil();
double tileWidth = screenWidth / columns; double tileWidth = screenWidth / columns;
double tileHeight = screenHeight / rows; double tileHeight = screenHeight / rows;
return GridView.builder( return Obx(
() => GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns, crossAxisCount: columns,
childAspectRatio: tileWidth / tileHeight, childAspectRatio: tileWidth / tileHeight,
@@ -165,25 +167,26 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
final track = call.participantTracks[index]; final track = call.participantTracks[index];
return Padding( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: ClipRRect( child: Card(
borderRadius: const BorderRadius.all(Radius.circular(8)), child: ClipRRect(
child: InteractiveParticipantWidget( borderRadius: const BorderRadius.all(Radius.circular(8)),
isFixed: true, child: InteractiveParticipantWidget(
color: Theme.of(context).colorScheme.surfaceContainerLow, color: Theme.of(context).colorScheme.surfaceContainerHigh,
participant: track, participant: track,
onTap: () { onTap: () {
if (track.participant.sid != if (track.participant.sid !=
call.focusTrack.value?.participant.sid) { call.focusTrack.value?.participant.sid) {
call.changeFocusTrack(track); call.changeFocusTrack(track);
} }
}, },
),
), ),
), ),
); );
}, },
); ),
} );
); });
} }
@override @override
@@ -234,31 +237,33 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
body: SafeArea( body: SafeArea(
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
child: Obx( child: Column(
() => Column( children: [
children: [ SizeTransition(
SizeTransition( sizeFactor: _controlsAnimation,
sizeFactor: _controlsAnimation, axis: Axis.vertical,
axis: Axis.vertical, child: SizedBox(
child: SizedBox( width: MediaQuery.of(context).size.width,
width: MediaQuery.of(context).size.width, height: 64,
height: 64, child: Row(
child: Row( children: [
children: [ const Expanded(child: SizedBox()),
const Expanded(child: SizedBox()), IconButton(
IconButton( icon: _layoutMode == 0
icon: _layoutMode == 0 ? const Icon(Icons.view_list)
? const Icon(Icons.view_list) : const Icon(Icons.grid_view),
: const Icon(Icons.grid_view), onPressed: () {
onPressed: () { _switchLayout();
_switchLayout(); },
}, ),
), ],
], ).paddingSymmetric(horizontal: 10),
).paddingSymmetric(horizontal: 10),
),
), ),
Expanded( ),
Expanded(
child: Material(
color: Theme.of(context).colorScheme.surfaceContainerLow,
elevation: 2,
child: Builder( child: Builder(
builder: (context) { builder: (context) {
switch (_layoutMode) { switch (_layoutMode) {
@@ -270,20 +275,20 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
}, },
), ),
), ),
if (provider.room.localParticipant != null) ),
SizeTransition( if (provider.room.localParticipant != null)
sizeFactor: _controlsAnimation, SizeTransition(
axis: Axis.vertical, sizeFactor: _controlsAnimation,
child: SizedBox( axis: Axis.vertical,
width: MediaQuery.of(context).size.width, child: SizedBox(
child: ControlsWidget( width: MediaQuery.of(context).size.width,
provider.room, child: ControlsWidget(
provider.room.localParticipant!, provider.room,
), provider.room.localParticipant!,
), ),
), ),
], ),
), ],
), ),
onTap: () { onTap: () {
_toggleControls(); _toggleControls();

View File

@@ -128,7 +128,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (widget.edit == null) _editorController.localRead(); if (widget.edit == null && widget.reply == null && widget.repost == null) {
_editorController.localRead();
}
if (widget.reply != null) {
_editorController.replyTo.value = widget.reply;
}
if (widget.repost != null) {
_editorController.repostTo.value = widget.repost;
}
_editorController.contentController.addListener(() => setState(() {})); _editorController.contentController.addListener(() => setState(() {}));
_syncWidget(); _syncWidget();
} }
@@ -219,10 +227,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
collapsedBackgroundColor: collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer, Theme.of(context).colorScheme.surfaceContainer,
children: [ children: [
PostItem( Container(
item: _replyTo!, constraints: const BoxConstraints(maxHeight: 280),
isReactable: false, child: SingleChildScrollView(
).paddingOnly(bottom: 8), child: PostItem(
item: _replyTo!,
isReactable: false,
).paddingOnly(bottom: 8),
),
),
], ],
), ),
if (_repostTo != null) if (_repostTo != null)
@@ -237,10 +250,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
collapsedBackgroundColor: collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer, Theme.of(context).colorScheme.surfaceContainer,
children: [ children: [
PostItem( Container(
item: _repostTo!, constraints: const BoxConstraints(maxHeight: 280),
isReactable: false, child: SingleChildScrollView(
).paddingOnly(bottom: 8), child: PostItem(
item: _repostTo!,
isReactable: false,
).paddingOnly(bottom: 8),
),
),
], ],
), ),
Expanded( Expanded(

View File

@@ -52,6 +52,7 @@ const i18nEnglish = {
'account': 'Account', 'account': 'Account',
'accountPersonalize': 'Personalize', 'accountPersonalize': 'Personalize',
'accountPersonalizeApplied': 'Account personalize settings has been saved.', 'accountPersonalizeApplied': 'Account personalize settings has been saved.',
'accountStickers': 'Stickers',
'accountFriend': 'Friend', 'accountFriend': 'Friend',
'accountFriendNew': 'New friend', 'accountFriendNew': 'New friend',
'accountFriendNewHint': 'accountFriendNewHint':
@@ -162,9 +163,11 @@ const i18nEnglish = {
'attachmentAutoUpload': 'Auto Upload', 'attachmentAutoUpload': 'Auto Upload',
'attachmentUploadQueue': 'Upload Queue', 'attachmentUploadQueue': 'Upload Queue',
'attachmentUploadQueueStart': 'Start All', 'attachmentUploadQueueStart': 'Start All',
'attachmentUploadInProgress': 'There are attachments being uploaded. Please wait until all attachments have been uploaded before proceeding...', 'attachmentUploadInProgress':
'There are attachments being uploaded. Please wait until all attachments have been uploaded before proceeding...',
'attachmentAttached': 'Exists Files', 'attachmentAttached': 'Exists Files',
'attachmentUploadBlocked': 'Upload blocked, there is currently a task in progress...', 'attachmentUploadBlocked':
'Upload blocked, there is currently a task in progress...',
'attachmentAdd': 'Attach attachments', 'attachmentAdd': 'Attach attachments',
'attachmentAddGalleryPhoto': 'Gallery photo', 'attachmentAddGalleryPhoto': 'Gallery photo',
'attachmentAddGalleryVideo': 'Gallery video', 'attachmentAddGalleryVideo': 'Gallery video',
@@ -173,7 +176,8 @@ const i18nEnglish = {
'attachmentAddClipboard': 'Paste file', 'attachmentAddClipboard': 'Paste file',
'attachmentAddFile': 'Attach file', 'attachmentAddFile': 'Attach file',
'attachmentAddLink': 'Link attachments', 'attachmentAddLink': 'Link attachments',
'attachmentAddLinkHint': 'Enter attachment serial number to link that attachment', 'attachmentAddLinkHint':
'Enter attachment serial number to link that attachment',
'attachmentAddLinkInput': 'Serial number', 'attachmentAddLinkInput': 'Serial number',
'attachmentSetting': 'Adjust attachment', 'attachmentSetting': 'Adjust attachment',
'attachmentAlt': 'Alternative text', 'attachmentAlt': 'Alternative text',
@@ -314,6 +318,13 @@ const i18nEnglish = {
'accountStatusNeutral': 'Neutral', 'accountStatusNeutral': 'Neutral',
'accountStatusPositive': 'Positive', 'accountStatusPositive': 'Positive',
'bsLoadingTheme': 'Loading Theme', 'bsLoadingTheme': 'Loading Theme',
'bsCheckForUpdate': 'Checking For Updates',
'bsCheckForUpdateFailed': 'Unable to Check Updates',
'bsCheckForUpdateNew': 'Found New Version',
'bsCheckForUpdateDescApple':
'Please head to TestFlight and update your app to latest version to prevent error happens and get latest functions.',
'bsCheckForUpdateDescCommon':
'Please head to our website download and install latest version of application to prevent error happens and get latest functions.',
'bsCheckingServer': 'Checking Server Status', 'bsCheckingServer': 'Checking Server Status',
'bsCheckingServerFail': 'bsCheckingServerFail':
'Unable connect to server, check your network connection', 'Unable connect to server, check your network connection',
@@ -331,7 +342,22 @@ const i18nEnglish = {
'themeColorMiku': 'Miku Blue', 'themeColorMiku': 'Miku Blue',
'themeColorKagamine': 'Kagamine Yellow', 'themeColorKagamine': 'Kagamine Yellow',
'themeColorLuka': 'Luka Pink', 'themeColorLuka': 'Luka Pink',
'stickerDeletionConfirm': 'Confirm sticker delete',
'stickerDeletionConfirmCaption':
'Are you sure to delete sticker @name? This action cannot be undo.',
'themeColorApplied': 'Global theme color has been applied.', 'themeColorApplied': 'Global theme color has been applied.',
'attachmentSaved': 'Attachment saved to your system album.', 'attachmentSaved': 'Attachment saved to your system album.',
'cropImage': 'Crop Image', 'cropImage': 'Crop Image',
'stickerUploader': 'Upload sticker',
'stickerUploaderAttachmentNew': 'Upload new attachment',
'stickerUploaderAttachment': 'Attachment serial number',
'stickerUploaderPack': 'Sticker pack serial number',
'stickerUploaderPackHint':
'Don\'t have pack id? Head to creator platform and create one!',
'stickerUploaderAlias': 'Alias',
'stickerUploaderAliasHint':
'Will be used as a placeholder with the sticker pack prefix when entered.',
'stickerUploaderName': 'Name',
'stickerUploaderNameHint':
'A human-friendly name given to the user in the sticker selection interface.',
}; };

View File

@@ -52,6 +52,7 @@ const i18nSimplifiedChinese = {
'account': '账号', 'account': '账号',
'accountPersonalize': '个性化', 'accountPersonalize': '个性化',
'accountPersonalizeApplied': '账户的个性化设置已保存。', 'accountPersonalizeApplied': '账户的个性化设置已保存。',
'accountStickers': '贴图',
'accountFriend': '好友', 'accountFriend': '好友',
'accountFriendNew': '添加好友', 'accountFriendNew': '添加好友',
'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!', 'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!',
@@ -293,6 +294,11 @@ const i18nSimplifiedChinese = {
'accountStatusNeutral': '中性', 'accountStatusNeutral': '中性',
'accountStatusPositive': '积极', 'accountStatusPositive': '积极',
'bsLoadingTheme': '正在装载主题', 'bsLoadingTheme': '正在装载主题',
'bsCheckForUpdate': '正在检查更新',
'bsCheckForUpdateFailed': '无法检查更新',
'bsCheckForUpdateNew': '发现新版本',
'bsCheckForUpdateDescApple': '请前往 TestFlight 并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。',
'bsCheckForUpdateDescCommon': '请前往我们的网站下载并安装最新版本的应用程序,以防止出现错误并获取最新功能。',
'bsCheckingServer': '检查服务器状态中', 'bsCheckingServer': '检查服务器状态中',
'bsCheckingServerFail': '无法连接至服务器,请检查你的网络连接状态', 'bsCheckingServerFail': '无法连接至服务器,请检查你的网络连接状态',
'bsCheckingServerDown': '当前服务器不可用,请稍后重试', 'bsCheckingServerDown': '当前服务器不可用,请稍后重试',
@@ -309,6 +315,17 @@ const i18nSimplifiedChinese = {
'themeColorKagamine': '镜音黄', 'themeColorKagamine': '镜音黄',
'themeColorLuka': '流音粉', 'themeColorLuka': '流音粉',
'themeColorApplied': '全局主题颜色已应用', 'themeColorApplied': '全局主题颜色已应用',
'stickerDeletionConfirm': '确认删除贴图',
'stickerDeletionConfirmCaption': '你确认要删除贴图 @name 吗?该操作不可撤销。',
'attachmentSaved': '附件已保存到系统相册', 'attachmentSaved': '附件已保存到系统相册',
'cropImage': '裁剪图片', 'cropImage': '裁剪图片',
'stickerUploader': '上传贴图',
'stickerUploaderAttachmentNew': '上传附件',
'stickerUploaderAttachment': '附件序列号',
'stickerUploaderPack': '贴图包序号',
'stickerUploaderPackHint': '没有该序号?请转到我们的创作者平台创建一个贴图包。',
'stickerUploaderAlias': '贴图别名',
'stickerUploaderAliasHint': '将会在输入时使用和贴图包前缀组成占位符。',
'stickerUploaderName': '贴图名称',
'stickerUploaderNameHint': '在贴图选择界面提供给用户的人类友好名称。',
}; };

View File

@@ -8,9 +8,9 @@ import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_heading.dart'; import 'package:solian/widgets/account/account_heading.dart';
class AccountProfilePopup extends StatefulWidget { class AccountProfilePopup extends StatefulWidget {
final Account account; final String name;
const AccountProfilePopup({super.key, required this.account}); const AccountProfilePopup({super.key, required this.name});
@override @override
State<AccountProfilePopup> createState() => _AccountProfilePopupState(); State<AccountProfilePopup> createState() => _AccountProfilePopupState();
@@ -21,11 +21,11 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
Account? _userinfo; Account? _userinfo;
void getUserinfo() async { void _getUserinfo() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = ServiceFinder.configureClient('auth'); final client = ServiceFinder.configureClient('auth');
final resp = await client.get('/users/${widget.account.name}'); final resp = await client.get('/users/${widget.name}');
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
_userinfo = Account.fromJson(resp.body); _userinfo = Account.fromJson(resp.body);
setState(() => _isBusy = false); setState(() => _isBusy = false);
@@ -38,7 +38,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
getUserinfo(); _getUserinfo();
} }
@override @override

View File

@@ -226,7 +226,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
onTapOutside: (_) => onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
const SizedBox(height: 5), const SizedBox(height: 8),
TextField( TextField(
controller: _clearAtController, controller: _clearAtController,
readOnly: true, readOnly: true,
@@ -238,7 +238,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
), ),
onTap: () => selectClearAt(), onTap: () => selectClearAt(),
), ),
const SizedBox(height: 5), const SizedBox(height: 8),
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Wrap( child: Wrap(
@@ -281,7 +281,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
], ],
), ),
), ),
const SizedBox(height: 5), const SizedBox(height: 8),
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Wrap( child: Wrap(

View File

@@ -35,7 +35,7 @@ class SilverRelativeList extends StatelessWidget {
context: context, context: context,
builder: (context) => builder: (context) =>
AccountProfilePopup( AccountProfilePopup(
account: element.related, name: element.related.name,
), ),
); );
}, },

View File

@@ -23,16 +23,26 @@ import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
class AttachmentEditorPopup extends StatefulWidget { class AttachmentEditorPopup extends StatefulWidget {
final String usage; final String usage;
final List<int> initialAttachments; final bool singleMode;
final bool imageOnly;
final bool autoUpload;
final double? imageMaxWidth;
final double? imageMaxHeight;
final List<int>? initialAttachments;
final void Function(int) onAdd; final void Function(int) onAdd;
final void Function(int) onRemove; final void Function(int) onRemove;
const AttachmentEditorPopup({ const AttachmentEditorPopup({
super.key, super.key,
required this.usage, required this.usage,
required this.initialAttachments,
required this.onAdd, required this.onAdd,
required this.onRemove, required this.onRemove,
this.singleMode = false,
this.imageOnly = false,
this.autoUpload = false,
this.imageMaxWidth,
this.imageMaxHeight,
this.initialAttachments,
}); });
@override @override
@@ -43,7 +53,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final _imagePicker = ImagePicker(); final _imagePicker = ImagePicker();
final AttachmentUploaderController _uploadController = Get.find(); final AttachmentUploaderController _uploadController = Get.find();
bool _isAutoUpload = false; late bool _isAutoUpload = widget.autoUpload;
bool _isBusy = false; bool _isBusy = false;
bool _isFirstTimeBusy = true; bool _isFirstTimeBusy = true;
@@ -54,13 +64,28 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
final medias = await _imagePicker.pickMultiImage(); if (widget.singleMode) {
if (medias.isEmpty) return; final medias = await _imagePicker.pickMultiImage(
maxWidth: widget.imageMaxWidth,
maxHeight: widget.imageMaxHeight,
);
if (medias.isEmpty) return;
_enqueueTaskBatch(medias.map((x) { _enqueueTaskBatch(medias.map((x) {
final file = File(x.path); final file = File(x.path);
return AttachmentUploadTask(file: file, usage: widget.usage); return AttachmentUploadTask(file: file, usage: widget.usage);
})); }));
} else {
final media = await _imagePicker.pickMedia(
maxWidth: widget.imageMaxWidth,
maxHeight: widget.imageMaxHeight,
);
if (media == null) return;
_enqueueTask(
AttachmentUploadTask(file: File(media.path), usage: widget.usage),
);
}
} }
Future<void> _pickVideoToUpload() async { Future<void> _pickVideoToUpload() async {
@@ -164,6 +189,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
if (result != null) { if (result != null) {
widget.onAdd(result.id); widget.onAdd(result.id);
setState(() => _attachments.add(result)); setState(() => _attachments.add(result));
if (widget.singleMode) Navigator.pop(context);
} }
} }
@@ -179,9 +205,11 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
widget.usage, widget.usage,
null, null,
(item) { (item) {
if (item == null) return;
widget.onAdd(item.id); widget.onAdd(item.id);
if (mounted) { if (mounted) {
setState(() => _attachments.add(item)); setState(() => _attachments.add(item));
if (widget.singleMode) Navigator.pop(context);
} }
}, },
); );
@@ -209,12 +237,12 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
void _revertMetadataList() { void _revertMetadataList() {
final AttachmentProvider attach = Get.find(); final AttachmentProvider attach = Get.find();
if (widget.initialAttachments.isEmpty) { if (widget.initialAttachments?.isEmpty ?? true) {
_isFirstTimeBusy = false; _isFirstTimeBusy = false;
return; return;
} else { } else {
_attachments = List.filled( _attachments = List.filled(
widget.initialAttachments.length, widget.initialAttachments!.length,
null, null,
growable: true, growable: true,
); );
@@ -222,19 +250,15 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
int progress = 0; attach
for (var idx = 0; idx < widget.initialAttachments.length; idx++) { .listMetadata(widget.initialAttachments ?? List.empty())
attach.getMetadata(widget.initialAttachments[idx]).then((resp) { .then((result) {
progress++; setState(() {
_attachments[idx] = resp; _attachments = result;
if (progress == widget.initialAttachments.length) { _isBusy = false;
setState(() { _isFirstTimeBusy = false;
_isBusy = false;
_isFirstTimeBusy = false;
});
}
}); });
} });
} }
void _showAttachmentPreview(Attachment element) { void _showAttachmentPreview(Attachment element) {
@@ -355,7 +379,15 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
child: Icon(Icons.check), child: Icon(Icons.check),
), ),
), ),
if (!element.isCompleted && canBeCrop) if (element.error != null)
IconButton(
tooltip: element.error!.toString(),
icon: const Icon(Icons.warning),
onPressed: () {},
),
if (!element.isCompleted &&
element.error == null &&
canBeCrop)
Obx( Obx(
() => IconButton( () => IconButton(
color: Colors.teal, color: Colors.teal,
@@ -368,7 +400,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
}, },
), ),
), ),
if (!element.isCompleted && !element.isUploading) if (!element.isCompleted &&
!element.isUploading &&
element.error == null)
Obx( Obx(
() => IconButton( () => IconButton(
color: Colors.green, color: Colors.green,
@@ -380,9 +414,13 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
_uploadController _uploadController
.performSingleTask(index) .performSingleTask(index)
.then((r) { .then((r) {
if (r == null) return;
widget.onAdd(r.id); widget.onAdd(r.id);
if (mounted) { if (mounted) {
setState(() => _attachments.add(r)); setState(() => _attachments.add(r));
if (widget.singleMode) {
Navigator.pop(context);
}
} }
}); });
}, },
@@ -525,6 +563,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
widget.onAdd(r.id); widget.onAdd(r.id);
if (mounted) { if (mounted) {
setState(() => _attachments.add(r)); setState(() => _attachments.add(r));
if (widget.singleMode) Navigator.pop(context);
} }
}); });
} }
@@ -557,9 +596,13 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Expanded(
'attachmentAdd'.tr, child: Text(
style: Theme.of(context).textTheme.headlineSmall, 'attachmentAdd'.tr,
style: Theme.of(context).textTheme.headlineSmall,
maxLines: 2,
overflow: TextOverflow.fade,
),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Obx(() { Obx(() {
@@ -676,6 +719,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
ignoring: _uploadController.isUploading.value, ignoring: _uploadController.isUploading.value,
child: Container( child: Container(
height: 64, height: 64,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
top: BorderSide( top: BorderSide(
@@ -692,9 +736,10 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center, runAlignment: WrapAlignment.center,
children: [ children: [
if (PlatformInfo.isDesktop || if ((PlatformInfo.isDesktop ||
PlatformInfo.isIOS || PlatformInfo.isIOS ||
PlatformInfo.isWeb) PlatformInfo.isWeb) &&
!widget.imageOnly)
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.paste), icon: const Icon(Icons.paste),
label: Text('attachmentAddClipboard'.tr), label: Text('attachmentAddClipboard'.tr),
@@ -707,36 +752,40 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _pickPhotoToUpload(), onPressed: () => _pickPhotoToUpload(),
), ),
ElevatedButton.icon( if (!widget.imageOnly)
icon: const Icon(Icons.add_road), ElevatedButton.icon(
label: Text('attachmentAddGalleryVideo'.tr), icon: const Icon(Icons.add_road),
style: const ButtonStyle(visualDensity: density), label: Text('attachmentAddGalleryVideo'.tr),
onPressed: () => _pickVideoToUpload(), style: const ButtonStyle(visualDensity: density),
), onPressed: () => _pickVideoToUpload(),
),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.photo_camera_back), icon: const Icon(Icons.photo_camera_back),
label: Text('attachmentAddCameraPhoto'.tr), label: Text('attachmentAddCameraPhoto'.tr),
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _takeMediaToUpload(false), onPressed: () => _takeMediaToUpload(false),
), ),
ElevatedButton.icon( if (!widget.imageOnly)
icon: const Icon(Icons.video_camera_back_outlined), ElevatedButton.icon(
label: Text('attachmentAddCameraVideo'.tr), icon: const Icon(Icons.video_camera_back_outlined),
style: const ButtonStyle(visualDensity: density), label: Text('attachmentAddCameraVideo'.tr),
onPressed: () => _takeMediaToUpload(true), style: const ButtonStyle(visualDensity: density),
), onPressed: () => _takeMediaToUpload(true),
ElevatedButton.icon( ),
icon: const Icon(Icons.file_present_rounded), if (!widget.imageOnly)
label: Text('attachmentAddFile'.tr), ElevatedButton.icon(
style: const ButtonStyle(visualDensity: density), icon: const Icon(Icons.file_present_rounded),
onPressed: () => _pickFileToUpload(), label: Text('attachmentAddFile'.tr),
), style: const ButtonStyle(visualDensity: density),
ElevatedButton.icon( onPressed: () => _pickFileToUpload(),
icon: const Icon(Icons.link), ),
label: Text('attachmentAddFile'.tr), if (!widget.imageOnly)
style: const ButtonStyle(visualDensity: density), ElevatedButton.icon(
onPressed: () => _linkAttachments(), icon: const Icon(Icons.link),
), label: Text('attachmentAddFile'.tr),
style: const ButtonStyle(visualDensity: density),
onPressed: () => _linkAttachments(),
),
], ],
).paddingSymmetric(horizontal: 12), ).paddingSymmetric(horizontal: 12),
), ),

View File

@@ -257,6 +257,10 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
child: Wrap( child: Wrap(
spacing: 6, spacing: 6,
children: [ children: [
Text(
'#${widget.item.id}',
style: metaTextStyle,
),
if (widget.item.metadata?['width'] != null && if (widget.item.metadata?['width'] != null &&
widget.item.metadata?['height'] != null) widget.item.metadata?['height'] != null)
Text( Text(

View File

@@ -45,7 +45,7 @@ class _AttachmentListState extends State<AttachmentList> {
List<Attachment?> _attachmentsMeta = List.empty(); List<Attachment?> _attachmentsMeta = List.empty();
void _getMetadataList() { void _getMetadataList() {
final AttachmentProvider provider = Get.find(); final AttachmentProvider attach = Get.find();
if (widget.attachmentsId.isEmpty) { if (widget.attachmentsId.isEmpty) {
return; return;
@@ -53,25 +53,16 @@ class _AttachmentListState extends State<AttachmentList> {
_attachmentsMeta = List.filled(widget.attachmentsId.length, null); _attachmentsMeta = List.filled(widget.attachmentsId.length, null);
} }
int progress = 0; attach.listMetadata(widget.attachmentsId).then((result) {
for (var idx = 0; idx < widget.attachmentsId.length; idx++) { setState(() {
provider.getMetadata(widget.attachmentsId[idx]).then((resp) { _attachmentsMeta = result;
progress++; _isLoading = false;
if (resp != null) {
_attachmentsMeta[idx] = resp;
}
if (progress == widget.attachmentsId.length) {
calculateAspectRatio();
if (mounted) {
setState(() => _isLoading = false);
}
}
}); });
} _calculateAspectRatio();
});
} }
void calculateAspectRatio() { void _calculateAspectRatio() {
bool isConsistent = true; bool isConsistent = true;
double? consistentValue; double? consistentValue;
int portrait = 0, square = 0, landscape = 0; int portrait = 0, square = 0, landscape = 0;

View File

@@ -57,7 +57,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void promptAddMember() async { void _promptAddMember() async {
final input = await showModalBottomSheet( final input = await showModalBottomSheet(
context: context, context: context,
builder: (context) { builder: (context) {
@@ -141,7 +141,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
'channelMembersAddHint' 'channelMembersAddHint'
.trParams({'channel': '#${widget.channel.alias}'}), .trParams({'channel': '#${widget.channel.alias}'}),
), ),
onTap: () => promptAddMember(), onTap: () => _promptAddMember(),
), ),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
@@ -160,7 +160,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
context: context, context: context,
builder: (context) => AccountProfilePopup( builder: (context) => AccountProfilePopup(
account: element.account, name: element.account.name,
), ),
); );
}, },

View File

@@ -201,7 +201,7 @@ class InteractiveParticipantWidget extends StatelessWidget {
final double? width; final double? width;
final double? height; final double? height;
final Color? color; final Color? color;
final bool isFixed; final bool isFixedAvatar;
final ParticipantTrack participant; final ParticipantTrack participant;
final Function() onTap; final Function() onTap;
@@ -210,7 +210,7 @@ class InteractiveParticipantWidget extends StatelessWidget {
this.width, this.width,
this.height, this.height,
this.color, this.color,
this.isFixed = false, this.isFixedAvatar = false,
required this.participant, required this.participant,
required this.onTap, required this.onTap,
}); });
@@ -222,7 +222,7 @@ class InteractiveParticipantWidget extends StatelessWidget {
width: width, width: width,
height: height, height: height,
color: color, color: color,
child: ParticipantWidget.widgetFor(participant, isFixed: isFixed), child: ParticipantWidget.widgetFor(participant, isFixed: isFixedAvatar),
), ),
onTap: () => onTap(), onTap: () => onTap(),
onLongPress: () { onLongPress: () {

View File

@@ -243,7 +243,7 @@ class ChatEvent extends StatelessWidget {
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
context: context, context: context,
builder: (context) => AccountProfilePopup( builder: (context) => AccountProfilePopup(
account: item.sender.account, name: item.sender.account.name,
), ),
); );
}, },

View File

@@ -1,8 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown_selectionarea/flutter_markdown.dart'; import 'package:flutter_markdown_selectionarea/flutter_markdown.dart';
import 'package:get/get.dart';
import 'package:markdown/markdown.dart' as markdown; import 'package:markdown/markdown.dart' as markdown;
import 'package:markdown/markdown.dart';
import 'package:solian/providers/stickers.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'account/account_profile_popup.dart';
class MarkdownTextContent extends StatelessWidget { class MarkdownTextContent extends StatelessWidget {
final String content; final String content;
final bool isSelectable; final bool isSelectable;
@@ -34,6 +39,8 @@ class MarkdownTextContent extends StatelessWidget {
extensionSet: markdown.ExtensionSet( extensionSet: markdown.ExtensionSet(
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
<markdown.InlineSyntax>[ <markdown.InlineSyntax>[
_UserNameCardInlineSyntax(),
_CustomEmoteInlineSyntax(),
markdown.EmojiSyntax(), markdown.EmojiSyntax(),
markdown.AutolinkExtensionSyntax(), markdown.AutolinkExtensionSyntax(),
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes ...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
@@ -41,6 +48,23 @@ class MarkdownTextContent extends StatelessWidget {
), ),
onTapLink: (text, href, title) async { onTapLink: (text, href, title) async {
if (href == null) return; if (href == null) return;
if (href.startsWith('solink://')) {
final segments = href.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'users':
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) => AccountProfilePopup(
name: segments[1],
),
);
}
return;
}
await launchUrlString( await launchUrlString(
href, href,
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
@@ -57,3 +81,39 @@ class MarkdownTextContent extends StatelessWidget {
return _buildContent(context); return _buildContent(context);
} }
} }
class _UserNameCardInlineSyntax extends InlineSyntax {
_UserNameCardInlineSyntax() : super(r'@[a-zA-Z0-9_]+');
@override
bool onMatch(markdown.InlineParser parser, Match match) {
final alias = match[0]!;
final anchor = markdown.Element.text('a', alias)
..attributes['href'] = Uri.encodeFull(
'solink://users/${alias.substring(1)}',
);
parser.addNode(anchor);
return true;
}
}
class _CustomEmoteInlineSyntax extends InlineSyntax {
_CustomEmoteInlineSyntax() : super(r':([a-z0-9_+-]+):');
@override
bool onMatch(markdown.InlineParser parser, Match match) {
final StickerProvider sticker = Get.find();
final alias = match[1]!;
if (sticker.aliasImageMapping[alias] == null) {
parser.advanceBy(1);
return false;
}
final element = markdown.Element.empty('img');
element.attributes['src'] = sticker.aliasImageMapping[alias]!;
parser.addNode(element);
return true;
}
}

View File

@@ -4,6 +4,7 @@ import 'package:solian/models/account_status.dart';
import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/shells/root_shell.dart'; import 'package:solian/shells/root_shell.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
@@ -60,8 +61,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
AppRouter.instance.pushNamed('settings'); AppRouter.instance.pushNamed('settings');
setState(() => _selectedIndex = null); setState(() => _selectedIndex = null);
_closeDrawer(); _closeDrawer();
} });
);
} }
@override @override
@@ -130,22 +130,36 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
); );
}, },
), ),
leading: Builder(builder: (context) { leading: Obx(() {
final badgeColor = _accountStatus != null final statusBadgeColor = _accountStatus != null
? StatusProvider.determineStatus( ? StatusProvider.determineStatus(
_accountStatus!, _accountStatus!,
).$2 ).$2
: Colors.grey; : Colors.grey;
final RelationshipProvider relations = Get.find();
final accountNotifications = relations.friendRequestCount.value;
return badges.Badge( return badges.Badge(
showBadge: _accountStatus != null, badgeContent: Text(
badgeStyle: badges.BadgeStyle(badgeColor: badgeColor), accountNotifications.toString(),
position: badges.BadgePosition.bottomEnd( style: const TextStyle(color: Colors.white),
bottom: 0,
end: -2,
), ),
child: AccountAvatar( showBadge: accountNotifications > 0,
content: auth.userProfile.value!['avatar'], 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'],
),
), ),
); );
}), }),

View File

@@ -310,7 +310,7 @@ class _PostItemState extends State<PostItem> {
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
context: context, context: context,
builder: (context) => AccountProfilePopup( builder: (context) => AccountProfilePopup(
account: item.author, name: item.author.name,
), ),
); );
}, },

View File

@@ -157,7 +157,7 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
context: context, context: context,
builder: (context) => AccountProfilePopup( builder: (context) => AccountProfilePopup(
account: element.account, name: element.account.name,
), ),
); );
}, },

View File

@@ -0,0 +1,211 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/stickers.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart';
class StickerUploadDialog extends StatefulWidget {
final Sticker? edit;
const StickerUploadDialog({super.key, this.edit});
@override
State<StickerUploadDialog> createState() => _StickerUploadDialogState();
}
class _StickerUploadDialogState extends State<StickerUploadDialog> {
final TextEditingController _attachmentController = TextEditingController();
final TextEditingController _packController = TextEditingController();
final TextEditingController _aliasController = TextEditingController();
final TextEditingController _nameController = TextEditingController();
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
bool _isBusy = false;
void _promptUploadNewAttachment() {
showModalBottomSheet(
context: context,
builder: (context) => AttachmentEditorPopup(
usage: 'sticker',
singleMode: true,
imageOnly: true,
autoUpload: true,
imageMaxHeight: 28,
imageMaxWidth: 28,
onAdd: (value) {
setState(() {
_attachmentController.text = value.toString();
});
},
initialAttachments: const [],
onRemove: (_) {},
),
);
}
Future<void> _applySticker() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
if ([
_nameController.text.isEmpty,
_aliasController.text.isEmpty,
_packController.text.isEmpty,
_attachmentController.text.isEmpty,
].any((x) => x)) {
return;
}
setState(() => _isBusy = true);
Response resp;
final client = auth.configureClient('files');
if (widget.edit == null) {
resp = await client.post('/stickers', {
'name': _nameController.text,
'alias': _aliasController.text,
'pack_id': int.tryParse(_packController.text),
'attachment_id': int.tryParse(_attachmentController.text),
});
} else {
resp = await client.put('/stickers/${widget.edit!.id}', {
'name': _nameController.text,
'alias': _aliasController.text,
'pack_id': int.tryParse(_packController.text),
'attachment_id': int.tryParse(_attachmentController.text),
});
}
setState(() => _isBusy = false);
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString);
} else {
Navigator.pop(context, true);
}
}
@override
void initState() {
super.initState();
if (widget.edit != null) {
_attachmentController.text = widget.edit!.attachmentId.toString();
_packController.text = widget.edit!.packId.toString();
_aliasController.text = widget.edit!.alias;
_nameController.text = widget.edit!.name;
}
}
@override
void dispose() {
_attachmentController.dispose();
_packController.dispose();
_aliasController.dispose();
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('stickerUploader'.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text('stickerUploaderAttachmentNew'.tr),
contentPadding: const EdgeInsets.only(left: 16, right: 13),
trailing: const Icon(Icons.chevron_right),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
onTap: () {
_promptUploadNewAttachment();
},
),
const SizedBox(height: 8),
TextField(
controller: _attachmentController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
prefixText: '#',
labelText: 'stickerUploaderAttachment'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 8),
TextField(
controller: _packController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
prefixText: '#',
labelText: 'stickerUploaderPack'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Container(
padding:
const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 6),
child: Text(
'stickerUploaderPackHint'.tr,
style: TextStyle(color: _unFocusColor),
),
),
TextField(
controller: _aliasController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: 'stickerUploaderAlias'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Container(
padding:
const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 6),
child: Text(
'stickerUploaderAliasHint'.tr,
style: TextStyle(color: _unFocusColor),
),
),
TextField(
controller: _nameController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: 'stickerUploaderName'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Container(
padding: const EdgeInsets.only(left: 8, right: 8, top: 4),
child: Text(
'stickerUploaderNameHint'.tr,
style: TextStyle(color: _unFocusColor),
),
),
],
),
actions: [
TextButton(
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
onPressed: _isBusy ? null : () => Navigator.pop(context),
child: Text('cancel'.tr),
),
TextButton(
onPressed: _isBusy ? null : () => _applySticker(),
child: Text('apply'.tr),
),
],
);
}
}

View File

@@ -79,7 +79,7 @@ PODS:
- GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/UserDefaults (7.13.3):
- GoogleUtilities/Logger - GoogleUtilities/Logger
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- livekit_client (2.2.2): - livekit_client (2.2.3):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
- macos_window_utils (1.0.0): - macos_window_utils (1.0.0):
@@ -108,7 +108,7 @@ PODS:
- screen_brightness_macos (0.1.0): - screen_brightness_macos (0.1.0):
- FlutterMacOS - FlutterMacOS
- Sentry/HybridSDK (8.32.0) - Sentry/HybridSDK (8.32.0)
- sentry_flutter (8.5.0): - sentry_flutter (8.6.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- Sentry/HybridSDK (= 8.32.0) - Sentry/HybridSDK (= 8.32.0)
@@ -240,7 +240,7 @@ SPEC CHECKSUMS:
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
livekit_client: c24af2b8474a39325596e714118e05551ec5eacc livekit_client: a59d8778582019242d96fe9da69d4ec48833b5ca
macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663 macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
@@ -253,7 +253,7 @@ SPEC CHECKSUMS:
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb
sentry_flutter: f1d86adcb93a959bc47a40d8d55059bdf7569bc5 sentry_flutter: 090351ce1ff5f96a4b33ef9455b7e3b28185387d
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 B

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -261,10 +261,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: cross_file name: cross_file
sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.4+1" version: "0.3.4+2"
crypto: crypto:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -317,10 +317,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dev_build name: dev_build
sha256: "76dd5b2587a891ab9c1e7f7eea6ca4a3504667321e0186ecb1e385183889d89b" sha256: f526d1fbe68875f6119ffc333f114dfe6aa93ad04439276d53968f7977cc410e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0+10" version: "1.0.0+11"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -369,14 +369,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.9" version: "2.3.9"
easy_debounce:
dependency: "direct main"
description:
name: easy_debounce
sha256: f082609cfb8f37defb9e37fc28bc978c6712dedf08d4c5a26f820fa10165a236
url: "https://pub.dev"
source: hosted
version: "2.0.3"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -684,10 +676,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_webrtc name: flutter_webrtc
sha256: d305793e6737c59a81c45b18484e1f985710827704eeb9092573387efcbae272 sha256: f46bd76cef6e8d787dc707d0c591f0e89c912a2970c7b5e68a55b9cca1bdde4c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.5" version: "0.11.6"
font_awesome_flutter: font_awesome_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -732,10 +724,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: "39dd52168d6c59984454183148dc3a5776960c61083adfc708cc79a7b3ce1ba8" sha256: d380de0355788c5c784fe9f81b43fc833b903991c25ecc4e2a416a67faefa722
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.1" version: "14.2.2"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@@ -769,7 +761,7 @@ packages:
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
image: image:
dependency: "direct main" dependency: transitive
description: description:
name: image name: image
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
@@ -820,10 +812,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_for_web name: image_picker_for_web
sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3" sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.4" version: "3.0.5"
image_picker_ios: image_picker_ios:
dependency: transitive dependency: transitive
description: description:
@@ -948,10 +940,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: livekit_client name: livekit_client
sha256: e6b1e8a3cdcae95f7e62c0371590648444bac245fce3a1bcfb4ec05889ad82f3 sha256: be2a3375851a6147d5de94a870edd6e831ab8d3d793e3563ba1ff1b05490b3de
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.2" version: "2.2.3"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@@ -1524,10 +1516,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_web name: shared_preferences_web
sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936" sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.1"
shared_preferences_windows: shared_preferences_windows:
dependency: transitive dependency: transitive
description: description:
@@ -1701,14 +1693,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.0" version: "0.7.0"
textfield_tags:
dependency: "direct main"
description:
name: textfield_tags
sha256: d1f2204114157a1296bb97c20d7f8c8c7fd036212812afb2e19de7bb34acc55b
url: "https://pub.dev"
source: hosted
version: "3.0.1"
timeago: timeago:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1809,10 +1793,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" sha256: a36e2d7981122fa185006b216eb6b5b97ede3f9a54b7a511bc966971ab98d049
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.1" version: "2.3.2"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:

View File

@@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App" description: "The Solar Network App"
publish_to: "none" publish_to: "none"
version: 1.2.0+7 version: 1.2.1+2
environment: environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
@@ -27,7 +27,6 @@ dependencies:
crypto: ^3.0.3 crypto: ^3.0.3
path: ^1.9.0 path: ^1.9.0
intl: ^0.19.0 intl: ^0.19.0
image: ^4.1.7
font_awesome_flutter: ^10.7.0 font_awesome_flutter: ^10.7.0
web_socket_channel: ^3.0.0 web_socket_channel: ^3.0.0
permission_handler: ^11.3.1 permission_handler: ^11.3.1
@@ -50,7 +49,6 @@ dependencies:
media_kit: ^1.1.10+1 media_kit: ^1.1.10+1
media_kit_video: ^1.2.4 media_kit_video: ^1.2.4
media_kit_libs_video: ^1.0.4 media_kit_libs_video: ^1.0.4
textfield_tags: ^3.0.1
pasteboard: ^0.2.0 pasteboard: ^0.2.0
desktop_drop: ^0.4.4 desktop_drop: ^0.4.4
badges: ^3.1.2 badges: ^3.1.2
@@ -60,7 +58,6 @@ dependencies:
flutter_cache_manager: ^3.3.3 flutter_cache_manager: ^3.3.3
flutter_markdown_selectionarea: ^0.6.17+1 flutter_markdown_selectionarea: ^0.6.17+1
shared_preferences: ^2.2.3 shared_preferences: ^2.2.3
easy_debounce: ^2.0.3
provider: ^6.1.2 provider: ^6.1.2
gal: ^2.3.0 gal: ^2.3.0
dio: ^5.5.0+1 dio: ^5.5.0+1
@@ -116,7 +113,7 @@ flutter:
flutter_launcher_icons: flutter_launcher_icons:
android: android:
generate: "launcher_icon" generate: "launcher_icon"
image_path: "assets/icon-fit.png" image_path: "assets/icon.png"
ios: true ios: true
image_path: "assets/icon.png" image_path: "assets/icon.png"
min_sdk_android: 21 min_sdk_android: 21

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 B

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 39 KiB