Compare commits

...

40 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
4238ea6fdc Call grid layout 2024-08-02 18:49:28 +08:00
7d45c06302 💄 Optimized signal indicator 2024-08-02 18:29:01 +08:00
7e8993fbd2 💫 Auto hide or show call controls 2024-08-02 18:09:07 +08:00
c88fcc84da Show call participants 2024-08-02 17:14:23 +08:00
11fb79623e Attachment can link exists things
 Optimize upload progress
2024-08-02 15:49:32 +08:00
98cc313a91 💫 Optimize chat event list animation 2024-08-02 14:14:09 +08:00
bc3401a897 🐛 Fix post item color mismatch 2024-08-02 05:10:10 +08:00
5b6a5d9046 🐛 Fix post popup color mismatch 2024-08-02 05:04:31 +08:00
6cbd78e836 💫 Optimize post editor transition 2024-08-02 04:59:35 +08:00
aefcbad02f 💫 Better animated post list 2024-08-02 04:42:38 +08:00
70617be687 💫 Animated chat 2024-08-02 04:24:12 +08:00
cccb3d5c16 🐛 Fix post won't refresh after post 2024-08-02 01:00:31 +08:00
a0a3a8d182 DM message last preview 2024-08-02 00:54:19 +08:00
c6b2ef8459 💄 Better about 2024-08-02 00:41:12 +08:00
34a2fe3988 Move about page link from account to settings 2024-08-02 00:29:51 +08:00
0a5604d0ff Crop image in personalize 2024-08-02 00:12:16 +08:00
5e754ad233 💫 About page icon will rotate 2024-08-01 23:51:03 +08:00
5b9c92e4d3 Crop image 2024-08-01 23:44:07 +08:00
b2a6ca7244 Improve attachments queue performance 2024-08-01 23:10:19 +08:00
27c60fc8cb Block user action when attachments isn't ready 2024-08-01 22:36:00 +08:00
103 changed files with 2343 additions and 843 deletions

View File

@@ -70,6 +70,12 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data

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):
@@ -145,7 +145,7 @@ PODS:
- SDWebImage/Core (= 5.19.4) - SDWebImage/Core (= 5.19.4)
- SDWebImage/Core (5.19.4) - SDWebImage/Core (5.19.4)
- 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)
@@ -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
@@ -309,7 +309,7 @@ SPEC CHECKSUMS:
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb
sentry_flutter: f1d86adcb93a959bc47a40d8d55059bdf7569bc5 sentry_flutter: 090351ce1ff5f96a4b33ef9455b7e3b28185387d
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec

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

@@ -16,7 +16,7 @@ class ChatEventController {
Channel? channel; Channel? channel;
String? scope; String? scope;
initialize() async { Future<void> initialize() async {
if (!PlatformInfo.isWeb) { if (!PlatformInfo.isWeb) {
database = await createHistoryDb(); database = await createHistoryDb();
} }
@@ -131,6 +131,8 @@ class ChatEventController {
} }
insertEvent(LocalEvent entry) { insertEvent(LocalEvent entry) {
if (entry.channelId != channel?.id) return;
final idx = currentEvents.indexWhere((x) => x.data.uuid == entry.data.uuid); final idx = currentEvents.indexWhere((x) => x.data.uuid == entry.data.uuid);
if (idx != -1) { if (idx != -1) {
currentEvents[idx] = entry; currentEvents[idx] = entry;

View File

@@ -115,10 +115,10 @@ class PostEditorController extends GetxController {
builder: (context) => AttachmentEditorPopup( builder: (context) => AttachmentEditorPopup(
usage: 'i.attachment', usage: 'i.attachment',
initialAttachments: attachments, initialAttachments: attachments,
onAdd: (value) { onAdd: (int value) {
attachments.add(value); attachments.add(value);
}, },
onRemove: (value) { onRemove: (int value) {
attachments.remove(value); attachments.remove(value);
}, },
), ),

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());

View File

@@ -10,6 +10,7 @@ class Call {
String externalId; String externalId;
int founderId; int founderId;
int channelId; int channelId;
List<dynamic> participants;
Channel channel; Channel channel;
Call({ Call({
@@ -21,6 +22,7 @@ class Call {
required this.externalId, required this.externalId,
required this.founderId, required this.founderId,
required this.channelId, required this.channelId,
required this.participants,
required this.channel, required this.channel,
}); });
@@ -34,6 +36,7 @@ class Call {
externalId: json['external_id'], externalId: json['external_id'],
founderId: json['founder_id'], founderId: json['founder_id'],
channelId: json['channel_id'], channelId: json['channel_id'],
participants: json['participants'] ?? List.empty(),
channel: Channel.fromJson(json['channel']), channel: Channel.fromJson(json['channel']),
); );
@@ -46,6 +49,7 @@ class Call {
'external_id': externalId, 'external_id': externalId,
'founder_id': founderId, 'founder_id': founderId,
'channel_id': channelId, 'channel_id': channelId,
'participants': participants,
'channel': channel.toJson(), 'channel': channel.toJson(),
}; };
} }
@@ -63,6 +67,7 @@ class ParticipantTrack {
{required this.participant, {required this.participant,
required this.videoTrack, required this.videoTrack,
required this.isScreenShare}); required this.isScreenShare});
VideoTrack? videoTrack; VideoTrack? videoTrack;
Participant participant; Participant participant;
bool isScreenShare; bool isScreenShare;

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

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
@@ -13,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,
@@ -26,22 +28,51 @@ class AttachmentUploaderController extends GetxController {
RxDouble progressOfUpload = 0.0.obs; RxDouble progressOfUpload = 0.0.obs;
RxList<AttachmentUploadTask> queueOfUpload = RxList.empty(growable: true); RxList<AttachmentUploadTask> queueOfUpload = RxList.empty(growable: true);
Timer? _progressSyncTimer;
double _progressOfUpload = 0.0;
void _syncProgress() {
progressOfUpload.value = _progressOfUpload;
queueOfUpload.refresh();
}
void _startProgressSyncTimer() {
if (_progressSyncTimer != null) {
_progressSyncTimer!.cancel();
}
_progressSyncTimer = Timer.periodic(
const Duration(milliseconds: 500),
(_) => _syncProgress(),
);
}
void _stopProgressSyncTimer() {
if (_progressSyncTimer == null) return;
_progressSyncTimer!.cancel();
_progressSyncTimer = null;
}
void enqueueTask(AttachmentUploadTask task) { void enqueueTask(AttachmentUploadTask task) {
if (isUploading.value) throw Exception('uploading blocked'); if (isUploading.value) throw Exception('uploading blocked');
queueOfUpload.add(task); queueOfUpload.add(task);
} }
void enqueueTaskBatch(Iterable<AttachmentUploadTask> tasks) {
if (isUploading.value) throw Exception('uploading blocked');
queueOfUpload.addAll(tasks);
}
void dequeueTask(AttachmentUploadTask task) { void dequeueTask(AttachmentUploadTask task) {
if (isUploading.value) throw Exception('uploading blocked'); if (isUploading.value) throw Exception('uploading blocked');
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;
_startProgressSyncTimer();
queueOfUpload[queueIndex].isUploading = true; queueOfUpload[queueIndex].isUploading = true;
queueOfUpload.refresh();
final task = queueOfUpload[queueIndex]; final task = queueOfUpload[queueIndex];
final result = await _rawUploadAttachment( final result = await _rawUploadAttachment(
@@ -51,13 +82,19 @@ class AttachmentUploaderController extends GetxController {
null, null,
onProgress: (value) { onProgress: (value) {
queueOfUpload[queueIndex].progress = value; queueOfUpload[queueIndex].progress = value;
queueOfUpload.refresh(); _progressOfUpload = value;
progressOfUpload.value = value; },
onError: (err) {
queueOfUpload[queueIndex].error = err;
queueOfUpload[queueIndex].isUploading = false;
}, },
); );
queueOfUpload.removeAt(queueIndex); if (queueOfUpload[queueIndex].error == null) {
queueOfUpload.refresh(); queueOfUpload.removeAt(queueIndex);
}
_stopProgressSyncTimer();
_syncProgress();
isUploading.value = false; isUploading.value = false;
@@ -70,9 +107,14 @@ class AttachmentUploaderController extends GetxController {
isUploading.value = true; isUploading.value = true;
progressOfUpload.value = 0; progressOfUpload.value = 0;
_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;
queueOfUpload.refresh();
final task = queueOfUpload[idx]; final task = queueOfUpload[idx];
final result = await _rawUploadAttachment( final result = await _rawUploadAttachment(
@@ -82,20 +124,25 @@ class AttachmentUploaderController extends GetxController {
null, null,
onProgress: (value) { onProgress: (value) {
queueOfUpload[idx].progress = value; queueOfUpload[idx].progress = value;
queueOfUpload.refresh(); _progressOfUpload = (idx + value) / queueOfUpload.length;
progressOfUpload.value = (idx + value) / queueOfUpload.length; },
onError: (err) {
queueOfUpload[idx].error = err;
queueOfUpload[idx].isUploading = false;
}, },
); );
progressOfUpload.value = (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.refresh();
} }
queueOfUpload.clear(); queueOfUpload.value =
queueOfUpload.refresh(); queueOfUpload.where((x) => x.error == null).toList(growable: true);
_stopProgressSyncTimer();
_syncProgress();
isUploading.value = false; isUploading.value = false;
} }
@@ -104,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');
@@ -122,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,
@@ -144,22 +191,24 @@ 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 resp = await provider.createAttachment( final result = await provider.createAttachment(
data, data,
path, path,
usage, usage,
metadata, metadata,
onProgress: onProgress, onProgress: onProgress,
); );
var result = Attachment.fromJson(resp.body);
return result; return result;
} catch (err) { } catch (err) {
rethrow; if (onError != null) {
onError(err);
}
return null;
} }
} }
} }

View File

@@ -16,6 +16,7 @@ class ChatCallProvider extends GetxController {
RxBool isReady = false.obs; RxBool isReady = false.obs;
RxBool isMounted = false.obs; RxBool isMounted = false.obs;
RxBool isInitialized = false.obs;
String? token; String? token;
String? endpoint; String? endpoint;
@@ -151,6 +152,8 @@ class ChatCallProvider extends GetxController {
void onRoomDidUpdate() => sortParticipants(); void onRoomDidUpdate() => sortParticipants();
void setupRoom() { void setupRoom() {
if(isInitialized.value) return;
sortParticipants(); sortParticipants();
room.addListener(onRoomDidUpdate); room.addListener(onRoomDidUpdate);
WidgetsBindingCompatible.instance?.addPostFrameCallback( WidgetsBindingCompatible.instance?.addPostFrameCallback(
@@ -160,6 +163,8 @@ class ChatCallProvider extends GetxController {
if (lkPlatformIsMobile()) { if (lkPlatformIsMobile()) {
Hardware.instance.setSpeakerphoneOn(true); Hardware.instance.setSpeakerphoneOn(true);
} }
isInitialized.value = true;
} }
void setupRoomListeners({ void setupRoomListeners({
@@ -362,6 +367,7 @@ class ChatCallProvider extends GetxController {
void disposeRoom() { void disposeRoom() {
isMounted.value = false; isMounted.value = false;
isInitialized.value = false;
current.value = null; current.value = null;
channel.value = null; channel.value = null;
room.removeListener(onRoomDidUpdate); room.removeListener(onRoomDidUpdate);

View File

@@ -4,8 +4,10 @@ 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;
class AttachmentProvider extends GetConnect { class AttachmentProvider extends GetConnect {
static Map<String, String> mimetypeOverrides = { static Map<String, String> mimetypeOverrides = {
@@ -20,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]!;
@@ -37,18 +83,14 @@ class AttachmentProvider extends GetConnect {
return null; return null;
} }
Future<Response> createAttachment( Future<Attachment> createAttachment(
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}) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized'); if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient( final filePayload =
'files', dio.MultipartFile.fromBytes(data, filename: basename(path));
timeout: const Duration(minutes: 3),
);
final filePayload = MultipartFile(data, filename: basename(path));
final fileAlt = basename(path).contains('.') final fileAlt = basename(path).contains('.')
? basename(path).substring(0, basename(path).lastIndexOf('.')) ? basename(path).substring(0, basename(path).lastIndexOf('.'))
: basename(path); : basename(path);
@@ -61,25 +103,30 @@ class AttachmentProvider extends GetConnect {
if (mimetypeOverrides.keys.contains(fileExt)) { if (mimetypeOverrides.keys.contains(fileExt)) {
mimetypeOverride = mimetypeOverrides[fileExt]; mimetypeOverride = mimetypeOverrides[fileExt];
} }
final payload = FormData({ final payload = dio.FormData.fromMap({
'alt': fileAlt, 'alt': fileAlt,
'file': filePayload, 'file': filePayload,
'usage': usage, 'usage': usage,
if (mimetypeOverride != null) 'mimetype': mimetypeOverride, if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
'metadata': jsonEncode(metadata), 'metadata': jsonEncode(metadata),
}); });
final resp = await client.post( final resp = await dio.Dio(
dio.BaseOptions(
baseUrl: ServiceFinder.buildUrl('files', null),
headers: {'Authorization': 'Bearer ${auth.credentials!.accessToken}'},
),
).post(
'/attachments', '/attachments',
payload, data: payload,
uploadProgress: (progress) { onSendProgress: (count, total) {
if (onProgress != null) onProgress(progress); if (onProgress != null) onProgress(count / total);
}, },
); );
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.data);
} }
return resp; return Attachment.fromJson(resp.data);
} }
Future<Response> updateAttachment( Future<Response> updateAttachment(

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

@@ -1,3 +1,5 @@
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
import 'package:solian/screens/about.dart'; import 'package:solian/screens/about.dart';
@@ -5,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';
@@ -93,14 +96,26 @@ abstract class AppRouter {
GoRoute( GoRoute(
path: '/posts/editor', path: '/posts/editor',
name: 'postEditor', name: 'postEditor',
builder: (context, state) { pageBuilder: (context, state) {
final arguments = state.extra as PostPublishArguments?; final arguments = state.extra as PostPublishArguments?;
return PostPublishScreen( return CustomTransitionPage(
edit: arguments?.edit, child: PostPublishScreen(
reply: arguments?.reply, edit: arguments?.edit,
repost: arguments?.repost, reply: arguments?.reply,
realm: arguments?.realm, repost: arguments?.repost,
mode: int.tryParse(state.uri.queryParameters['mode'] ?? '0') ?? 0, realm: arguments?.realm,
postListController: arguments?.postListController,
mode: int.tryParse(state.uri.queryParameters['mode'] ?? '0') ?? 0,
),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
fillColor: Theme.of(context).scaffoldBackgroundColor,
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
); );
}, },
), ),
@@ -212,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

@@ -17,7 +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(
borderRadius: const BorderRadius.all(Radius.circular(16)),
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,
@@ -44,17 +48,28 @@ class AboutScreen extends StatelessWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
TextButton( TextButton(
style: denseButtonStyle, style: denseButtonStyle,
child: const Text('More Information'), child: const Text('App Details'),
onPressed: () { onPressed: () async {
launchUrlString('https://solsynth.dev/products/solar-network'); final info = await PackageInfo.fromPlatform();
showAboutDialog(
context: context,
applicationVersion: '${info.version} (${info.buildNumber})',
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.',
applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Image.asset('assets/logo.png', width: 60, height: 60),
),
);
}, },
), ),
TextButton( TextButton(
style: denseButtonStyle, style: denseButtonStyle,
child: const Text('Project Website'),
onPressed: () { onPressed: () {
launchUrlString('https://solsynth.dev'); launchUrlString('https://solsynth.dev/products/solar-network');
}, },
child: const Text('Official Website'),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text( const Text(

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,10 +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'),
(const Icon(Icons.info_outline), 'about'.tr, 'about'),
]; ];
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
@@ -41,7 +65,10 @@ class _AccountScreenState extends State<AccountScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ActionCard( ActionCard(
icon: const Icon(Icons.login, color: Colors.white), icon: Icon(
Icons.login,
color: Theme.of(context).colorScheme.onPrimary,
),
title: 'signin'.tr, title: 'signin'.tr,
caption: 'signinCaption'.tr, caption: 'signinCaption'.tr,
onTap: () { onTap: () {
@@ -59,7 +86,10 @@ class _AccountScreenState extends State<AccountScreen> {
}, },
), ),
ActionCard( ActionCard(
icon: const Icon(Icons.add, color: Colors.white), icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.onPrimary,
),
title: 'signup'.tr, title: 'signup'.tr,
caption: 'signupCaption'.tr, caption: 'signupCaption'.tr,
onTap: () { onTap: () {
@@ -184,7 +214,7 @@ class ActionCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CircleAvatar( CircleAvatar(
backgroundColor: Colors.indigo, backgroundColor: Theme.of(context).colorScheme.primary,
child: icon, child: icon,
).paddingOnly(bottom: 12), ).paddingOnly(bottom: 12),
Text( Text(

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

@@ -3,9 +3,11 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart'; import 'package:solian/providers/content/attachment.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@@ -50,26 +52,24 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
} }
void _syncWidget() async { void _syncWidget() async {
setState(() => _isBusy = true); _isBusy = true;
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
final prof = auth.userProfile.value!; final prof = auth.userProfile.value!;
setState(() { _usernameController.text = prof['name'];
_usernameController.text = prof['name']; _nicknameController.text = prof['nick'];
_nicknameController.text = prof['nick']; _descriptionController.text = prof['description'];
_descriptionController.text = prof['description']; _firstNameController.text = prof['profile']['first_name'];
_firstNameController.text = prof['profile']['first_name']; _lastNameController.text = prof['profile']['last_name'];
_lastNameController.text = prof['profile']['last_name']; _avatar = prof['avatar'];
_avatar = prof['avatar']; _banner = prof['banner'];
_banner = prof['banner']; if (prof['profile']['birthday'] != null) {
if (prof['profile']['birthday'] != null) { _birthday = DateTime.parse(prof['profile']['birthday']);
_birthday = DateTime.parse(prof['profile']['birthday']); _birthdayController.text =
_birthdayController.text = DateFormat('yyyy-MM-dd').format(_birthday!.toLocal());
DateFormat('yyyy-MM-dd').format(_birthday!.toLocal()); }
}
_isBusy = false; _isBusy = false;
});
} }
Future<void> _editImage(String position) async { Future<void> _editImage(String position) async {
@@ -79,15 +79,46 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
final image = await _imagePicker.pickImage(source: ImageSource.gallery); final image = await _imagePicker.pickImage(source: ImageSource.gallery);
if (image == null) return; if (image == null) return;
CroppedFile? croppedFile = await ImageCropper().cropImage(
sourcePath: image.path,
uiSettings: [
AndroidUiSettings(
toolbarTitle: 'cropImage'.tr,
toolbarColor: Theme.of(context).colorScheme.primary,
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
),
IOSUiSettings(
title: 'cropImage'.tr,
aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
),
WebUiSettings(
context: context,
),
],
);
if (croppedFile == null) return;
final file = File(croppedFile.path);
setState(() => _isBusy = true); setState(() => _isBusy = true);
final AttachmentProvider provider = Get.find(); final AttachmentProvider provider = Get.find();
Response? attachResp; Attachment? attachResult;
try { try {
final file = File(image.path); attachResult = await provider.createAttachment(
attachResp = await provider.createAttachment( await file.readAsBytes(),
await file.readAsBytes(), file.path, 'p.$position', null); file.path,
'p.$position',
null,
);
} catch (e) { } catch (e) {
setState(() => _isBusy = false); setState(() => _isBusy = false);
context.showErrorDialog(e); context.showErrorDialog(e);
@@ -98,7 +129,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
final resp = await client.put( final resp = await client.put(
'/users/me/$position', '/users/me/$position',
{'attachment': attachResp.body['id']}, {'attachment': attachResult.id},
); );
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
_syncWidget(); _syncWidget();
@@ -142,8 +173,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_syncWidget();
Future.delayed(Duration.zero, () => _syncWidget());
} }
@override @override
@@ -322,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

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:async/async.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/providers/call.dart'; import 'package:solian/providers/call.dart';
@@ -16,11 +17,26 @@ class CallScreen extends StatefulWidget {
State<CallScreen> createState() => _CallScreenState(); State<CallScreen> createState() => _CallScreenState();
} }
class _CallScreenState extends State<CallScreen> { class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
Timer? timer; Timer? _timer;
String currentDuration = '00:00:00'; String _currentDuration = '00:00:00';
String parseDuration() { int _layoutMode = 0;
bool _showControls = true;
CancelableOperation? _hideControlsOperation;
late final AnimationController _controlsAnimationController =
AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
late final Animation<double> _controlsAnimation = CurvedAnimation(
parent: _controlsAnimationController,
curve: Curves.fastOutSlowIn,
);
String _parseDuration() {
final ChatCallProvider provider = Get.find(); final ChatCallProvider provider = Get.find();
if (provider.current.value == null) return '00:00:00'; if (provider.current.value == null) return '00:00:00';
Duration duration = Duration duration =
@@ -34,9 +50,142 @@ class _CallScreenState extends State<CallScreen> {
return formattedTime; return formattedTime;
} }
void updateDuration() { void _updateDuration() {
setState(() { setState(() {
currentDuration = parseDuration(); _currentDuration = _parseDuration();
});
}
void _switchLayout() {
if (_layoutMode < 1) {
setState(() => _layoutMode++);
} else {
setState(() => _layoutMode = 0);
}
}
void _toggleControls() {
if (_showControls) {
setState(() => _showControls = false);
_controlsAnimationController.animateTo(0);
_hideControlsOperation?.cancel();
} else {
setState(() => _showControls = true);
_controlsAnimationController.animateTo(1);
_planAutoHideControls();
}
}
void _planAutoHideControls() {
_hideControlsOperation = CancelableOperation.fromFuture(
Future.delayed(const Duration(seconds: 3), () {
if (!mounted) return;
if (_showControls) _toggleControls();
}),
);
}
Widget _buildListLayout() {
final ChatCallProvider call = Get.find();
return Obx(
() => Stack(
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: call.focusTrack.value != null
? InteractiveParticipantWidget(
isFixedAvatar: false,
participant: call.focusTrack.value!,
onTap: () {},
)
: const SizedBox(),
),
Positioned(
left: 0,
right: 0,
top: 0,
child: SizedBox(
height: 128,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: math.max(0, call.participantTracks.length),
itemBuilder: (BuildContext context, int index) {
final track = call.participantTracks[index];
if (track.participant.sid ==
call.focusTrack.value?.participant.sid) {
return Container();
}
return Padding(
padding: const EdgeInsets.only(top: 8, left: 8),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget(
isFixedAvatar: true,
width: 120,
height: 120,
color: Theme.of(context).cardColor,
participant: track,
onTap: () {
if (track.participant.sid !=
call.focusTrack.value?.participant.sid) {
call.changeFocusTrack(track);
}
},
),
),
);
},
),
),
),
],
),
);
}
Widget _buildGridLayout() {
final ChatCallProvider call = Get.find();
return LayoutBuilder(builder: (context, constraints) {
double screenWidth = constraints.maxWidth;
double screenHeight = constraints.maxHeight;
int columns = (math.sqrt(call.participantTracks.length)).ceil();
int rows = (call.participantTracks.length / columns).ceil();
double tileWidth = screenWidth / columns;
double tileHeight = screenHeight / rows;
return Obx(
() => GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
childAspectRatio: tileWidth / tileHeight,
),
itemCount: math.max(0, call.participantTracks.length),
itemBuilder: (BuildContext context, int index) {
final track = call.participantTracks[index];
return Padding(
padding: const EdgeInsets.all(16),
child: Card(
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
participant: track,
onTap: () {
if (track.participant.sid !=
call.focusTrack.value?.participant.sid) {
call.changeFocusTrack(track);
}
},
),
),
),
);
},
),
);
}); });
} }
@@ -45,7 +194,18 @@ class _CallScreenState extends State<CallScreen> {
Get.find<ChatCallProvider>().setupRoom(); Get.find<ChatCallProvider>().setupRoom();
super.initState(); super.initState();
timer = Timer.periodic(const Duration(seconds: 1), (_) => updateDuration()); _updateDuration();
_planAutoHideControls();
_timer = Timer.periodic(
const Duration(seconds: 1),
(_) => _updateDuration(),
);
}
@override
void dispose() {
_controlsAnimationController.dispose();
super.dispose();
} }
@override @override
@@ -68,80 +228,71 @@ class _CallScreenState extends State<CallScreen> {
), ),
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: currentDuration, text: _currentDuration,
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
]), ]),
), ),
), ),
body: SafeArea( body: SafeArea(
child: Obx( child: GestureDetector(
() => Stack( behavior: HitTestBehavior.translucent,
child: Column(
children: [ children: [
Column( SizeTransition(
children: [ sizeFactor: _controlsAnimation,
Expanded( axis: Axis.vertical,
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: provider.focusTrack.value != null
? InteractiveParticipantWidget(
isFixed: false,
participant: provider.focusTrack.value!,
onTap: () {},
)
: const SizedBox(),
),
),
if (provider.room.localParticipant != null)
ControlsWidget(
provider.room,
provider.room.localParticipant!,
),
],
),
Positioned(
left: 0,
right: 0,
top: 0,
child: SizedBox( child: SizedBox(
height: 128, width: MediaQuery.of(context).size.width,
child: ListView.builder( height: 64,
scrollDirection: Axis.horizontal, child: Row(
itemCount: math.max(0, provider.participantTracks.length), children: [
itemBuilder: (BuildContext context, int index) { const Expanded(child: SizedBox()),
final track = provider.participantTracks[index]; IconButton(
if (track.participant.sid == icon: _layoutMode == 0
provider.focusTrack.value?.participant.sid) { ? const Icon(Icons.view_list)
return Container(); : const Icon(Icons.grid_view),
onPressed: () {
_switchLayout();
},
),
],
).paddingSymmetric(horizontal: 10),
),
),
Expanded(
child: Material(
color: Theme.of(context).colorScheme.surfaceContainerLow,
elevation: 2,
child: Builder(
builder: (context) {
switch (_layoutMode) {
case 1:
return _buildGridLayout();
default:
return _buildListLayout();
} }
return Padding(
padding: const EdgeInsets.only(top: 8, left: 8),
child: ClipRRect(
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget(
isFixed: true,
width: 120,
height: 120,
color: Theme.of(context).cardColor,
participant: track,
onTap: () {
if (track.participant.sid !=
provider
.focusTrack.value?.participant.sid) {
provider.changeFocusTrack(track);
}
},
),
),
);
}, },
), ),
), ),
), ),
if (provider.room.localParticipant != null)
SizeTransition(
sizeFactor: _controlsAnimation,
axis: Axis.vertical,
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: ControlsWidget(
provider.room,
provider.room.localParticipant!,
),
),
),
], ],
), ),
onTap: () {
_toggleControls();
},
), ),
), ),
), ),
@@ -150,16 +301,16 @@ class _CallScreenState extends State<CallScreen> {
@override @override
void deactivate() { void deactivate() {
timer?.cancel(); _timer?.cancel();
timer = null; _timer = null;
super.deactivate(); super.deactivate();
} }
@override @override
void activate() { void activate() {
timer ??= Timer.periodic( _timer ??= Timer.periodic(
const Duration(seconds: 1), const Duration(seconds: 1),
(_) => updateDuration(), (_) => _updateDuration(),
); );
super.activate(); super.activate();
} }

View File

@@ -11,7 +11,6 @@ import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart'; import 'package:solian/models/event.dart';
import 'package:solian/models/packet.dart'; import 'package:solian/models/packet.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/call.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
@@ -19,7 +18,7 @@ import 'package:solian/screens/channel/channel_detail.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/chat/call/call_prejoin.dart'; import 'package:solian/widgets/channel/channel_call_indicator.dart';
import 'package:solian/widgets/chat/call/chat_call_action.dart'; import 'package:solian/widgets/chat/call/chat_call_action.dart';
import 'package:solian/widgets/chat/chat_event.dart'; import 'package:solian/widgets/chat/chat_event.dart';
import 'package:solian/widgets/chat/chat_event_list.dart'; import 'package:solian/widgets/chat/chat_event_list.dart';
@@ -53,7 +52,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
late final ChatEventController _chatController; late final ChatEventController _chatController;
getChannel({String? alias}) async { _getChannel({String? alias}) async {
final ChannelProvider provider = Get.find(); final ChannelProvider provider = Get.find();
setState(() => _isBusy = true); setState(() => _isBusy = true);
@@ -80,7 +79,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
getOngoingCall() async { _getOngoingCall() async {
final ChannelProvider provider = Get.find(); final ChannelProvider provider = Get.find();
setState(() => _isBusy = true); setState(() => _isBusy = true);
@@ -100,7 +99,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void listenMessages() { void _listenMessages() {
final WebSocketProvider provider = Get.find(); final WebSocketProvider provider = Get.find();
_subscription = provider.stream.stream.listen((event) { _subscription = provider.stream.stream.listen((event) {
switch (event.method) { switch (event.method) {
@@ -110,26 +109,20 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
break; break;
case 'calls.new': case 'calls.new':
final payload = Call.fromJson(event.payload!); final payload = Call.fromJson(event.payload!);
setState(() => _ongoingCall = payload); if (payload.channel.id == _channel!.id) {
setState(() => _ongoingCall = payload);
}
break; break;
case 'calls.end': case 'calls.end':
setState(() => _ongoingCall = null); final payload = Call.fromJson(event.payload!);
if (payload.channel.id == _channel!.id) {
setState(() => _ongoingCall = null);
}
break; break;
} }
}); });
} }
void showCallPrejoin() {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => ChatCallPrejoinPopup(
ongoingCall: _ongoingCall!,
channel: _channel!,
),
);
}
Event? _messageToReplying; Event? _messageToReplying;
Event? _messageToEditing; Event? _messageToEditing;
@@ -149,13 +142,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_chatController = ChatEventController(); _chatController = ChatEventController();
_chatController.initialize(); _chatController.initialize();
getChannel().then((_) { _getOngoingCall();
_getChannel().then((_) {
_chatController.getEvents(_channel!, widget.realm); _chatController.getEvents(_channel!, widget.realm);
listenMessages(); _listenMessages();
}); });
getOngoingCall();
super.initState(); super.initState();
} }
@@ -183,8 +175,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
final ChatCallProvider call = Get.find();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: AppBarLeadingButton.adaptive(context), leading: AppBarLeadingButton.adaptive(context),
@@ -219,7 +209,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
if (value == false) AppRouter.instance.pop(); if (value == false) AppRouter.instance.pop();
if (value != null) { if (value != null) {
final resp = Channel.fromJson(value as Map<String, dynamic>); final resp = Channel.fromJson(value as Map<String, dynamic>);
getChannel(alias: resp.alias); _getChannel(alias: resp.alias);
} }
}); });
}, },
@@ -232,32 +222,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
body: Column( body: Column(
children: [ children: [
if (_ongoingCall != null) if (_ongoingCall != null)
MaterialBanner( ChannelCallIndicator(
padding: const EdgeInsets.only(left: 16, top: 4, bottom: 4), channel: _channel!,
leading: const Icon(Icons.call_received), ongoingCall: _ongoingCall!,
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
dividerColor: Colors.transparent,
content: Text('callOngoing'.tr),
actions: [
Obx(() {
if (call.current.value == null) {
return TextButton(
onPressed: showCallPrejoin,
child: Text('callJoin'.tr),
);
} else if (call.channel.value?.id == _channel?.id) {
return TextButton(
onPressed: () => call.gotoScreen(context),
child: Text('callResume'.tr),
);
} else {
return TextButton(
onPressed: null,
child: Text('callJoin'.tr),
);
}
})
],
), ),
Expanded( Expanded(
child: ChatEventList( child: ChatEventList(

View File

@@ -4,6 +4,7 @@ import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart'; import 'package:solian/screens/account/notification.dart';
import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
@@ -51,7 +52,9 @@ class _HomeScreenState extends State<HomeScreen>
useRootNavigator: true, useRootNavigator: true,
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: (context) => const PostCreatePopup(), builder: (context) => PostCreatePopup(
controller: _postController,
),
); );
}, },
), ),
@@ -118,12 +121,12 @@ class _HomeScreenState extends State<HomeScreen>
class PostCreatePopup extends StatelessWidget { class PostCreatePopup extends StatelessWidget {
final bool hideDraftBox; final bool hideDraftBox;
final Function? onCreated; final PostListController controller;
const PostCreatePopup({ const PostCreatePopup({
super.key, super.key,
this.hideDraftBox = false, this.hideDraftBox = false,
this.onCreated, required this.controller,
}); });
@override @override
@@ -140,11 +143,13 @@ class PostCreatePopup extends StatelessWidget {
label: 'postEditorModeStory'.tr, label: 'postEditorModeStory'.tr,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
AppRouter.instance.pushNamed('postEditor', queryParameters: { AppRouter.instance.pushNamed(
'mode': 0.toString(), 'postEditor',
}).then((val) { extra: PostPublishArguments(postListController: controller),
if (val != null && onCreated != null) onCreated!(); queryParameters: {
}); 'mode': 0.toString(),
},
);
}, },
), ),
( (
@@ -152,11 +157,13 @@ class PostCreatePopup extends StatelessWidget {
label: 'postEditorModeArticle'.tr, label: 'postEditorModeArticle'.tr,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
AppRouter.instance.pushNamed('postEditor', queryParameters: { AppRouter.instance.pushNamed(
'mode': 1.toString(), 'postEditor',
}).then((val) { extra: PostPublishArguments(postListController: controller),
if (val != null && onCreated != null) onCreated!(); queryParameters: {
}); 'mode': 1.toString(),
},
);
}, },
), ),
( (

View File

@@ -3,14 +3,18 @@ import 'package:get/get.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:solian/widgets/posts/post_item.dart'; import 'package:solian/widgets/posts/post_item.dart';
import 'package:solian/widgets/posts/post_replies.dart'; import 'package:solian/widgets/posts/post_replies.dart';
class PostDetailScreen extends StatefulWidget { class PostDetailScreen extends StatefulWidget {
final String id; final String id;
final Post? post;
const PostDetailScreen({super.key, required this.id}); const PostDetailScreen({
super.key,
required this.id,
this.post,
});
@override @override
State<PostDetailScreen> createState() => _PostDetailScreenState(); State<PostDetailScreen> createState() => _PostDetailScreenState();
@@ -20,6 +24,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
Post? item; Post? item;
Future<Post?> getDetail() async { Future<Post?> getDetail() async {
if (widget.post != null) {
item = widget.post;
return widget.post;
}
final PostProvider provider = Get.find(); final PostProvider provider = Get.find();
try { try {
@@ -48,14 +57,12 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
return CustomScrollView( return CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
child: CenteredContainer( child: PostItem(
child: PostItem( item: item!,
item: item!, isClickable: true,
isClickable: true, isFullDate: true,
isFullDate: true, isShowReply: false,
isShowReply: false, isContentSelectable: true,
isContentSelectable: true,
),
), ),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
@@ -63,14 +70,12 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
.paddingOnly(top: 4), .paddingOnly(top: 4),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: CenteredContainer( child: Align(
child: Align( alignment: Alignment.centerLeft,
alignment: Alignment.centerLeft, child: Text(
child: Text( 'postReplies'.tr,
'postReplies'.tr, style: Theme.of(context).textTheme.headlineSmall,
style: Theme.of(context).textTheme.headlineSmall, ).paddingOnly(left: 24, right: 24, top: 16),
).paddingOnly(left: 24, right: 24, top: 16),
),
), ),
), ),
PostReplyList(item: item!), PostReplyList(item: item!),

View File

@@ -5,9 +5,11 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:markdown_toolbar/markdown_toolbar.dart'; import 'package:markdown_toolbar/markdown_toolbar.dart';
import 'package:solian/controllers/post_editor_controller.dart'; import 'package:solian/controllers/post_editor_controller.dart';
import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
@@ -21,8 +23,15 @@ class PostPublishArguments {
final Post? reply; final Post? reply;
final Post? repost; final Post? repost;
final Realm? realm; final Realm? realm;
final PostListController? postListController;
PostPublishArguments({this.edit, this.reply, this.repost, this.realm}); PostPublishArguments({
this.edit,
this.reply,
this.repost,
this.realm,
this.postListController,
});
} }
class PostPublishScreen extends StatefulWidget { class PostPublishScreen extends StatefulWidget {
@@ -30,6 +39,7 @@ class PostPublishScreen extends StatefulWidget {
final Post? reply; final Post? reply;
final Post? repost; final Post? repost;
final Realm? realm; final Realm? realm;
final PostListController? postListController;
final int mode; final int mode;
const PostPublishScreen({ const PostPublishScreen({
@@ -38,6 +48,7 @@ class PostPublishScreen extends StatefulWidget {
this.reply, this.reply,
this.repost, this.repost,
this.realm, this.realm,
this.postListController,
required this.mode, required this.mode,
}); });
@@ -56,6 +67,14 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
if (_editorController.isEmpty) return; if (_editorController.isEmpty) return;
final AttachmentUploaderController uploader = Get.find();
if (uploader.queueOfUpload.any(
((x) => x.usage == 'i.attachment' && x.isUploading),
)) {
context.showErrorDialog('attachmentUploadInProgress'.tr);
return;
}
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = auth.configureClient('interactive'); final client = auth.configureClient('interactive');
@@ -76,6 +95,9 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);
} else { } else {
_editorController.localClear(); _editorController.localClear();
if (widget.postListController != null) {
widget.postListController!.reloadAllOver();
}
AppRouter.instance.pop(resp.body); AppRouter.instance.pop(resp.body);
} }
@@ -92,20 +114,29 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
} }
} }
void cancelAction() { void _cancelAction() {
_editorController.localClear(); _editorController.localClear();
AppRouter.instance.pop(); AppRouter.instance.pop();
} }
Post? get _editTo => _editorController.editTo.value; Post? get _editTo => _editorController.editTo.value;
Post? get _replyTo => _editorController.replyTo.value; Post? get _replyTo => _editorController.replyTo.value;
Post? get _repostTo => _editorController.repostTo.value; Post? get _repostTo => _editorController.repostTo.value;
Realm? get _realm => _editorController.realmZone.value;
@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();
} }
@@ -114,7 +145,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final notifyBannerActions = [ final notifyBannerActions = [
TextButton( TextButton(
onPressed: cancelAction, onPressed: _cancelAction,
child: Text('cancel'.tr), child: Text('cancel'.tr),
) )
]; ];
@@ -196,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)
@@ -214,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

@@ -26,7 +26,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
final List<Realm> _realms = List.empty(growable: true); final List<Realm> _realms = List.empty(growable: true);
getRealms() async { _getRealms() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
@@ -48,7 +48,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
getRealms(); _getRealms();
} }
@override @override
@@ -71,7 +71,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
onPressed: () { onPressed: () {
AppRouter.instance.pushNamed('realmOrganizing').then( AppRouter.instance.pushNamed('realmOrganizing').then(
(value) { (value) {
if (value != null) getRealms(); if (value != null) _getRealms();
}, },
); );
}, },
@@ -84,7 +84,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
body: Obx(() { body: Obx(() {
if (auth.isAuthorized.isFalse) { if (auth.isAuthorized.isFalse) {
return SigninRequiredOverlay( return SigninRequiredOverlay(
onSignedIn: () => getRealms(), onSignedIn: () => _getRealms(),
); );
} }
@@ -94,12 +94,12 @@ class _RealmListScreenState extends State<RealmListScreen> {
Expanded( Expanded(
child: CenteredContainer( child: CenteredContainer(
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () => getRealms(), onRefresh: () => _getRealms(),
child: ListView.builder( child: ListView.builder(
itemCount: _realms.length, itemCount: _realms.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final element = _realms[index]; final element = _realms[index];
return buildRealm(element); return _buildEntry(element);
}, },
), ),
), ),
@@ -112,7 +112,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
); );
} }
Widget buildRealm(Realm element) { Widget _buildEntry(Realm element) {
return Card( return Card(
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),

View File

@@ -12,7 +12,6 @@ import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/channel/channel_organize.dart'; import 'package:solian/screens/channel/channel_organize.dart';
import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/channel/channel_list.dart'; import 'package:solian/widgets/channel/channel_list.dart';
@@ -136,6 +135,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
} }
return TabBarView( return TabBarView(
physics: const NeverScrollableScrollPhysics(),
children: [ children: [
RealmPostListWidget(realm: _realm!), RealmPostListWidget(realm: _realm!),
RealmChannelListWidget( RealmChannelListWidget(
@@ -189,7 +189,6 @@ class _RealmPostListWidgetState extends State<RealmPostListWidget> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_pagingController.addPageRequestListener(getPosts); _pagingController.addPageRequestListener(getPosts);
} }
@@ -199,28 +198,6 @@ class _RealmPostListWidgetState extends State<RealmPostListWidget> {
onRefresh: () => Future.sync(() => _pagingController.refresh()), onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter(
child: ListTile(
leading: const Icon(Icons.post_add),
contentPadding: const EdgeInsets.only(left: 24, right: 8),
tileColor: Theme.of(context).colorScheme.surfaceContainer,
title: Text('postNew'.tr),
subtitle: Text(
'postNewInRealmHint'
.trParams({'realm': '#${widget.realm.alias}'}),
),
onTap: () {
AppRouter.instance
.pushNamed(
'postEditor',
extra: PostPublishArguments(realm: widget.realm),
)
.then((value) {
if (value != null) _pagingController.refresh();
});
},
),
),
PostListWidget(controller: _pagingController), PostListWidget(controller: _pagingController),
], ],
), ),

View File

@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/router.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
class SettingScreen extends StatefulWidget { class SettingScreen extends StatefulWidget {
@@ -79,6 +80,21 @@ class _SettingScreenState extends State<SettingScreen> {
.toList(), .toList(),
).paddingSymmetric(horizontal: 12, vertical: 8), ).paddingSymmetric(horizontal: 12, vertical: 8),
), ),
_buildCaptionHeader('more'.tr),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
TextButton(
style: const ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
),
child: Text('about'.tr),
onPressed: () {
AppRouter.instance.pushNamed('about');
},
),
],
).paddingSymmetric(horizontal: 12, vertical: 8),
], ],
), ),
); );

View File

@@ -8,13 +8,15 @@ import 'package:solian/widgets/app_bar_leading.dart';
class TitleShell extends StatelessWidget { class TitleShell extends StatelessWidget {
final bool showAppBar; final bool showAppBar;
final bool isCenteredTitle; final bool isCenteredTitle;
final GoRouterState state; final String? title;
final GoRouterState? state;
final Widget child; final Widget child;
const TitleShell({ const TitleShell({
super.key, super.key,
required this.child, required this.child,
required this.state, this.title,
this.state,
this.showAppBar = true, this.showAppBar = true,
this.isCenteredTitle = false, this.isCenteredTitle = false,
}); });
@@ -25,7 +27,9 @@ class TitleShell extends StatelessWidget {
appBar: showAppBar appBar: showAppBar
? AppBar( ? AppBar(
leading: AppBarLeadingButton.adaptive(context), leading: AppBarLeadingButton.adaptive(context),
title: AppBarTitle(state.topRoute?.name?.tr ?? 'page'.tr), title: AppBarTitle(
title ?? (state!.topRoute?.name?.tr ?? 'page'.tr),
),
centerTitle: isCenteredTitle, centerTitle: isCenteredTitle,
toolbarHeight: SolianTheme.toolbarHeight(context), toolbarHeight: SolianTheme.toolbarHeight(context),
) )

View File

@@ -10,8 +10,10 @@ const i18nEnglish = {
'draft': 'Draft', 'draft': 'Draft',
'draftSave': 'Save', 'draftSave': 'Save',
'draftBox': 'Draft Box', 'draftBox': 'Draft Box',
'more': 'More',
'share': 'Share', 'share': 'Share',
'feed': 'Feed', 'feed': 'Feed',
'unlink': 'Unlink',
'feedSearch': 'Search Feed', 'feedSearch': 'Search Feed',
'feedSearchWithTag': 'Searching with tag #@key', 'feedSearchWithTag': 'Searching with tag #@key',
'feedSearchWithCategory': 'Searching in category @category', 'feedSearchWithCategory': 'Searching in category @category',
@@ -50,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':
@@ -160,8 +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...',
'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',
@@ -169,6 +175,10 @@ const i18nEnglish = {
'attachmentAddCameraVideo': 'Capture video', 'attachmentAddCameraVideo': 'Capture video',
'attachmentAddClipboard': 'Paste file', 'attachmentAddClipboard': 'Paste file',
'attachmentAddFile': 'Attach file', 'attachmentAddFile': 'Attach file',
'attachmentAddLink': 'Link attachments',
'attachmentAddLinkHint':
'Enter attachment serial number to link that attachment',
'attachmentAddLinkInput': 'Serial number',
'attachmentSetting': 'Adjust attachment', 'attachmentSetting': 'Adjust attachment',
'attachmentAlt': 'Alternative text', 'attachmentAlt': 'Alternative text',
'attachmentLoadFailed': 'Load Attachment Failed', 'attachmentLoadFailed': 'Load Attachment Failed',
@@ -251,6 +261,8 @@ const i18nEnglish = {
'Are your sure to delete message @id? This action cannot be undone!', 'Are your sure to delete message @id? This action cannot be undone!',
'call': 'Call', 'call': 'Call',
'callOngoing': 'A call is ongoing...', 'callOngoing': 'A call is ongoing...',
'callOngoingEmpty': 'A call is on hold...',
'callOngoingParticipants': '@count people are calling...',
'callJoin': 'Join', 'callJoin': 'Join',
'callResume': 'Resume', 'callResume': 'Resume',
'callMicrophone': 'Microphone', 'callMicrophone': 'Microphone',
@@ -306,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',
@@ -323,6 +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',
'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

@@ -18,8 +18,10 @@ const i18nSimplifiedChinese = {
'draft': '草稿', 'draft': '草稿',
'draftSave': '存为草稿', 'draftSave': '存为草稿',
'draftBox': '草稿箱', 'draftBox': '草稿箱',
'more': '更多',
'share': '分享', 'share': '分享',
'feed': '资讯', 'feed': '资讯',
'unlink': '移除链接',
'feedSearch': '搜索资讯', 'feedSearch': '搜索资讯',
'feedSearchWithTag': '检索带有 #@key 标签的资讯', 'feedSearchWithTag': '检索带有 #@key 标签的资讯',
'feedSearchWithCategory': '检索位于分类 @category 的资讯', 'feedSearchWithCategory': '检索位于分类 @category 的资讯',
@@ -50,6 +52,7 @@ const i18nSimplifiedChinese = {
'account': '账号', 'account': '账号',
'accountPersonalize': '个性化', 'accountPersonalize': '个性化',
'accountPersonalizeApplied': '账户的个性化设置已保存。', 'accountPersonalizeApplied': '账户的个性化设置已保存。',
'accountStickers': '贴图',
'accountFriend': '好友', 'accountFriend': '好友',
'accountFriendNew': '添加好友', 'accountFriendNew': '添加好友',
'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!', 'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!',
@@ -149,6 +152,7 @@ const i18nSimplifiedChinese = {
'attachmentAutoUpload': '自动上传', 'attachmentAutoUpload': '自动上传',
'attachmentUploadQueue': '上传队列', 'attachmentUploadQueue': '上传队列',
'attachmentUploadQueueStart': '整队上传', 'attachmentUploadQueueStart': '整队上传',
'attachmentUploadInProgress': '有附件正在上传,请等待所有附件上传完毕后再进行操作……',
'attachmentAttached': '已附附件', 'attachmentAttached': '已附附件',
'attachmentUploadBlocked': '上传受阻,当前已有任务进行中……', 'attachmentUploadBlocked': '上传受阻,当前已有任务进行中……',
'attachmentAdd': '附加附件', 'attachmentAdd': '附加附件',
@@ -158,6 +162,9 @@ const i18nSimplifiedChinese = {
'attachmentAddCameraVideo': '拍摄视频', 'attachmentAddCameraVideo': '拍摄视频',
'attachmentAddClipboard': '粘贴文件', 'attachmentAddClipboard': '粘贴文件',
'attachmentAddFile': '附加文件', 'attachmentAddFile': '附加文件',
'attachmentAddLink': '链接附件',
'attachmentAddLinkHint': '输入附件的神秘代号来链接对应附件',
'attachmentAddLinkInput': '神秘代号',
'attachmentSetting': '调整附件', 'attachmentSetting': '调整附件',
'attachmentAlt': '替代文字', 'attachmentAlt': '替代文字',
'attachmentLoadFailed': '加载失败', 'attachmentLoadFailed': '加载失败',
@@ -232,6 +239,8 @@ const i18nSimplifiedChinese = {
'messageDeletionConfirmCaption': '你确定要删除消息 @id 吗?该操作不可撤销。', 'messageDeletionConfirmCaption': '你确定要删除消息 @id 吗?该操作不可撤销。',
'call': '通话', 'call': '通话',
'callOngoing': '一则通话正在进行中…', 'callOngoing': '一则通话正在进行中…',
'callOngoingEmpty': '一则通话待机中…',
'callOngoingParticipants': '@count 人正在进行通话…',
'callJoin': '加入', 'callJoin': '加入',
'callResume': '恢复', 'callResume': '恢复',
'callMicrophone': '麦克风', 'callMicrophone': '麦克风',
@@ -285,6 +294,11 @@ const i18nSimplifiedChinese = {
'accountStatusNeutral': '中性', 'accountStatusNeutral': '中性',
'accountStatusPositive': '积极', 'accountStatusPositive': '积极',
'bsLoadingTheme': '正在装载主题', 'bsLoadingTheme': '正在装载主题',
'bsCheckForUpdate': '正在检查更新',
'bsCheckForUpdateFailed': '无法检查更新',
'bsCheckForUpdateNew': '发现新版本',
'bsCheckForUpdateDescApple': '请前往 TestFlight 并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。',
'bsCheckForUpdateDescCommon': '请前往我们的网站下载并安装最新版本的应用程序,以防止出现错误并获取最新功能。',
'bsCheckingServer': '检查服务器状态中', 'bsCheckingServer': '检查服务器状态中',
'bsCheckingServerFail': '无法连接至服务器,请检查你的网络连接状态', 'bsCheckingServerFail': '无法连接至服务器,请检查你的网络连接状态',
'bsCheckingServerDown': '当前服务器不可用,请稍后重试', 'bsCheckingServerDown': '当前服务器不可用,请稍后重试',
@@ -301,5 +315,17 @@ const i18nSimplifiedChinese = {
'themeColorKagamine': '镜音黄', 'themeColorKagamine': '镜音黄',
'themeColorLuka': '流音粉', 'themeColorLuka': '流音粉',
'themeColorApplied': '全局主题颜色已应用', 'themeColorApplied': '全局主题颜色已应用',
'stickerDeletionConfirm': '确认删除贴图',
'stickerDeletionConfirmCaption': '你确认要删除贴图 @name 吗?该操作不可撤销。',
'attachmentSaved': '附件已保存到系统相册', 'attachmentSaved': '附件已保存到系统相册',
'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

@@ -8,9 +8,10 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:pasteboard/pasteboard.dart'; import 'package:pasteboard/pasteboard.dart';
import 'package:path/path.dart' show basename; import 'package:path/path.dart' show basename, extension;
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
@@ -22,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
@@ -42,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;
@@ -53,13 +64,26 @@ 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) {
final file = File(x.path);
return AttachmentUploadTask(file: file, usage: widget.usage);
}));
} else {
final media = await _imagePicker.pickMedia(
maxWidth: widget.imageMaxWidth,
maxHeight: widget.imageMaxHeight,
);
if (media == null) return;
for (final media in medias) {
final file = File(media.path);
_enqueueTask( _enqueueTask(
AttachmentUploadTask(file: file, usage: widget.usage), AttachmentUploadTask(file: File(media.path), usage: widget.usage),
); );
} }
} }
@@ -88,11 +112,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
List<File> files = result.paths.map((path) => File(path!)).toList(); List<File> files = result.paths.map((path) => File(path!)).toList();
for (final file in files) { _enqueueTaskBatch(files.map((x) {
_enqueueTask( return AttachmentUploadTask(file: x, usage: widget.usage);
AttachmentUploadTask(file: file, usage: widget.usage), }));
);
}
} }
Future<void> _takeMediaToUpload(bool isVideo) async { Future<void> _takeMediaToUpload(bool isVideo) async {
@@ -113,6 +135,64 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
); );
} }
Future<void> _linkAttachments() async {
final controller = TextEditingController();
final input = await showDialog<String?>(
context: context,
builder: (context) {
return AlertDialog(
title: Text('attachmentAddLink'.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('attachmentAddLinkHint'.tr, textAlign: TextAlign.left),
const SizedBox(height: 18),
TextField(
controller: controller,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'attachmentAddLinkInput'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: <Widget>[
TextButton(
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
onPressed: () => Navigator.pop(context),
child: Text('cancel'.tr),
),
TextButton(
child: Text('next'.tr),
onPressed: () {
Navigator.pop(context, controller.text);
},
),
],
);
},
);
WidgetsBinding.instance.addPostFrameCallback((_) => controller.dispose());
if (input == null || input.isEmpty) return;
final value = int.tryParse(input);
if (value == null) return;
final AttachmentProvider attach = Get.find();
final result = await attach.getMetadata(value);
if (result != null) {
widget.onAdd(result.id);
setState(() => _attachments.add(result));
if (widget.singleMode) Navigator.pop(context);
}
}
void _pasteFileToUpload() async { void _pasteFileToUpload() async {
final data = await Pasteboard.image; final data = await Pasteboard.image;
if (data == null) return; if (data == null) return;
@@ -125,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);
} }
}, },
); );
@@ -153,30 +235,30 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
} }
void _revertMetadataList() { void _revertMetadataList() {
final AttachmentProvider provider = 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(widget.initialAttachments.length, null); _attachments = List.filled(
widget.initialAttachments!.length,
null,
growable: true,
);
} }
setState(() => _isBusy = true); setState(() => _isBusy = true);
int progress = 0; attach
for (var idx = 0; idx < widget.initialAttachments.length; idx++) { .listMetadata(widget.initialAttachments ?? List.empty())
provider.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) {
@@ -203,6 +285,31 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
); );
} }
Future<void> _cropAttachment(int queueIndex) async {
final task = _uploadController.queueOfUpload[queueIndex];
CroppedFile? croppedFile = await ImageCropper().cropImage(
sourcePath: task.file.path,
uiSettings: [
AndroidUiSettings(
toolbarTitle: 'cropImage'.tr,
toolbarColor: Theme.of(context).colorScheme.primary,
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
aspectRatioPresets: CropAspectRatioPreset.values,
),
IOSUiSettings(
title: 'cropImage'.tr,
aspectRatioPresets: CropAspectRatioPreset.values,
),
WebUiSettings(
context: context,
),
],
);
if (croppedFile == null) return;
_uploadController.queueOfUpload[queueIndex].file = File(croppedFile.path);
_uploadController.queueOfUpload.refresh();
}
Future<void> _deleteAttachment(Attachment element) async { Future<void> _deleteAttachment(Attachment element) async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
try { try {
@@ -216,6 +323,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
} }
Widget _buildQueueEntry(AttachmentUploadTask element, int index) { Widget _buildQueueEntry(AttachmentUploadTask element, int index) {
final extName = extension(element.file.path).substring(1);
final canBeCrop = ['png', 'jpg', 'jpeg', 'gif'].contains(extName);
return Container( return Container(
padding: const EdgeInsets.only(left: 16, right: 8, bottom: 16), padding: const EdgeInsets.only(left: 16, right: 8, bottom: 16),
child: Card( child: Card(
@@ -239,20 +349,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
fontFamily: 'monospace', fontFamily: 'monospace',
), ),
), ),
FutureBuilder( Text(
future: element.file.length(), 'In queue #${index + 1}',
builder: (context, snapshot) { style: const TextStyle(fontSize: 12),
if (!snapshot.hasData) {
return const Text(
'- Bytes',
style: TextStyle(fontSize: 12),
);
}
return Text(
_formatBytes(snapshot.data!),
style: const TextStyle(fontSize: 12),
);
},
), ),
], ],
), ),
@@ -280,23 +379,52 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
child: Icon(Icons.check), child: Icon(Icons.check),
), ),
), ),
if (!element.isCompleted && !element.isUploading) if (element.error != null)
IconButton( IconButton(
color: Colors.green, tooltip: element.error!.toString(),
icon: const Icon(Icons.play_arrow), icon: const Icon(Icons.warning),
visualDensity: const VisualDensity(horizontal: -4), onPressed: () {},
onPressed: _uploadController.isUploading.value ),
? null if (!element.isCompleted &&
: () { element.error == null &&
_uploadController canBeCrop)
.performSingleTask(index) Obx(
.then((r) { () => IconButton(
widget.onAdd(r.id); color: Colors.teal,
if (mounted) { icon: const Icon(Icons.crop),
setState(() => _attachments.add(r)); visualDensity: const VisualDensity(horizontal: -4),
} onPressed: _uploadController.isUploading.value
}); ? null
}, : () {
_cropAttachment(index);
},
),
),
if (!element.isCompleted &&
!element.isUploading &&
element.error == null)
Obx(
() => IconButton(
color: Colors.green,
icon: const Icon(Icons.play_arrow),
visualDensity: const VisualDensity(horizontal: -4),
onPressed: _uploadController.isUploading.value
? null
: () {
_uploadController
.performSingleTask(index)
.then((r) {
if (r == null) return;
widget.onAdd(r.id);
if (mounted) {
setState(() => _attachments.add(r));
if (widget.singleMode) {
Navigator.pop(context);
}
}
});
},
),
), ),
if (!element.isCompleted && !element.isUploading) if (!element.isCompleted && !element.isUploading)
IconButton( IconButton(
@@ -392,6 +520,19 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
}); });
}, },
), ),
PopupMenuItem(
child: ListTile(
title: Text('unlink'.tr),
leading: const Icon(Icons.link_off),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
),
),
onTap: () {
widget.onRemove(element.id);
setState(() => _attachments.removeAt(index));
},
),
], ],
), ),
], ],
@@ -410,11 +551,19 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
} }
} }
void _enqueueTaskBatch(Iterable<AttachmentUploadTask> tasks) {
_uploadController.enqueueTaskBatch(tasks);
if (_isAutoUpload) {
_startUploading();
}
}
void _startUploading() { void _startUploading() {
_uploadController.performUploadQueue(onData: (r) { _uploadController.performUploadQueue(onData: (r) {
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);
} }
}); });
} }
@@ -433,11 +582,10 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
child: DropTarget( child: DropTarget(
onDragDone: (detail) async { onDragDone: (detail) async {
if (_uploadController.isUploading.value) return; if (_uploadController.isUploading.value) return;
for (final file in detail.files) { _enqueueTaskBatch(detail.files.map((x) {
_enqueueTask( final file = File(x.path);
AttachmentUploadTask(file: File(file.path), usage: widget.usage), return AttachmentUploadTask(file: file, usage: widget.usage);
); }));
}
}, },
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -448,18 +596,23 @@ 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(() {
if (_uploadController.isUploading.value) { if (_uploadController.isUploading.value) {
return const SizedBox( return SizedBox(
width: 18, width: 18,
height: 18, height: 18,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2.5, strokeWidth: 2.5,
value: _uploadController.progressOfUpload.value,
), ),
); );
} }
@@ -566,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(
@@ -582,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),
@@ -597,30 +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),
onPressed: () => _pickFileToUpload(),
),
if (!widget.imageOnly)
ElevatedButton.icon(
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

@@ -0,0 +1,89 @@
import 'dart:convert';
import 'package:avatar_stack/avatar_stack.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/call.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/call.dart';
import 'package:solian/widgets/chat/call/call_prejoin.dart';
class ChannelCallIndicator extends StatelessWidget {
final Channel channel;
final Call ongoingCall;
const ChannelCallIndicator(
{super.key, required this.channel, required this.ongoingCall});
void _showCallPrejoin(BuildContext context) {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => ChatCallPrejoinPopup(
ongoingCall: ongoingCall,
channel: channel,
),
);
}
@override
Widget build(BuildContext context) {
final ChatCallProvider call = Get.find();
return MaterialBanner(
padding: const EdgeInsets.only(left: 16, top: 4, bottom: 4),
leading: const Icon(Icons.call_received),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
dividerColor: Colors.transparent,
content: Row(
children: [
if (ongoingCall.participants.isEmpty) Text('callOngoingEmpty'.tr),
if (ongoingCall.participants.isNotEmpty)
Text('callOngoingParticipants'.trParams({
'count': ongoingCall.participants.length.toString(),
})),
const SizedBox(width: 6),
if (ongoingCall.participants.isNotEmpty)
Container(
height: 28,
constraints: const BoxConstraints(maxWidth: 120),
child: AvatarStack(
height: 28,
borderWidth: 0,
avatars: ongoingCall.participants.map((x) {
final userinfo = Account.fromJson(jsonDecode(x['metadata']));
return PlatformInfo.canCacheImage
? CachedNetworkImageProvider(userinfo.avatar)
as ImageProvider
: NetworkImage(userinfo.avatar) as ImageProvider;
}).toList(),
),
),
],
),
actions: [
Obx(() {
if (call.current.value == null) {
return TextButton(
onPressed: () => _showCallPrejoin(context),
child: Text('callJoin'.tr),
);
} else if (call.channel.value?.id == channel.id) {
return TextButton(
onPressed: () => call.gotoScreen(context),
child: Text('callResume'.tr),
);
} else {
return TextButton(
onPressed: null,
child: Text('callJoin'.tr),
);
}
})
],
);
}
}

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
import 'package:solian/platform.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
@@ -31,7 +33,9 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
final List<Channel> _globalChannels = List.empty(growable: true); final List<Channel> _globalChannels = List.empty(growable: true);
final Map<String, List<Channel>> _inRealms = {}; final Map<String, List<Channel>> _inRealms = {};
void mapChannels() { final ChatEventController _eventController = ChatEventController();
void _mapChannels() {
_inRealms.clear(); _inRealms.clear();
_globalChannels.clear(); _globalChannels.clear();
@@ -55,16 +59,17 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
@override @override
void didUpdateWidget(covariant ChannelListWidget oldWidget) { void didUpdateWidget(covariant ChannelListWidget oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
setState(() => mapChannels()); setState(() => _mapChannels());
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
mapChannels(); _mapChannels();
_eventController.initialize();
} }
void gotoChannel(Channel item) { void _gotoChannel(Channel item) {
if (widget.useReplace) { if (widget.useReplace) {
AppRouter.instance.pushReplacementNamed( AppRouter.instance.pushReplacementNamed(
'channelChat', 'channelChat',
@@ -88,7 +93,35 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
} }
} }
Widget buildItem(Channel item) { Widget _buildDirectMessageDescription(Channel item, ChannelMember otherside) {
if (PlatformInfo.isWeb) {
return Text('channelDirectDescription'.trParams(
{'username': '@${otherside.account.name}'},
));
}
return FutureBuilder(
future: Future.delayed(
const Duration(milliseconds: 500),
() => _eventController.database.localEvents.findLastByChannel(item.id),
),
builder: (context, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Text('channelDirectDescription'.trParams(
{'username': '@${otherside.account.name}'},
));
}
return Text(
'${snapshot.data!.data.sender.account.nick}: ${snapshot.data!.data.body['text'] ?? 'Unsupported message to preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
},
);
}
Widget _buildEntry(Channel item) {
final padding = widget.isDense final padding = widget.isDense
? const EdgeInsets.symmetric(horizontal: 20) ? const EdgeInsets.symmetric(horizontal: 20)
: const EdgeInsets.symmetric(horizontal: 16); : const EdgeInsets.symmetric(horizontal: 16);
@@ -102,37 +135,36 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
leading: AccountAvatar( leading: AccountAvatar(
content: otherside.account.avatar, content: otherside.account.avatar,
radius: widget.isDense ? 12 : 20, radius: widget.isDense ? 12 : 20,
bgColor: Colors.indigo, bgColor: Theme.of(context).colorScheme.primary,
feColor: Colors.white, feColor: Theme.of(context).colorScheme.onPrimary,
), ),
contentPadding: padding, contentPadding: padding,
title: Text(otherside.account.nick), title: Text(otherside.account.nick),
subtitle: !widget.isDense subtitle: !widget.isDense
? Text( ? _buildDirectMessageDescription(item, otherside)
'channelDirectDescription'.trParams(
{'username': '@${otherside.account.name}'},
),
)
: null, : null,
onTap: () => gotoChannel(item), onTap: () => _gotoChannel(item),
); );
} else { } else {
return ListTile( return ListTile(
minTileHeight: widget.isDense ? 48 : null, minTileHeight: widget.isDense ? 48 : null,
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: backgroundColor: item.realmId == null
item.realmId == null ? Colors.indigo : Colors.transparent, ? Theme.of(context).colorScheme.primary
: Colors.transparent,
radius: widget.isDense ? 12 : 20, radius: widget.isDense ? 12 : 20,
child: FaIcon( child: FaIcon(
FontAwesomeIcons.hashtag, FontAwesomeIcons.hashtag,
color: item.realmId == null ? Colors.white : Colors.indigo, color: item.realmId == null
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.primary,
size: widget.isDense ? 12 : 16, size: widget.isDense ? 12 : 16,
), ),
), ),
contentPadding: padding, contentPadding: padding,
title: Text(item.name), title: Text(item.name),
subtitle: !widget.isDense ? Text(item.description) : null, subtitle: !widget.isDense ? Text(item.description) : null,
onTap: () => gotoChannel(item), onTap: () => _gotoChannel(item),
); );
} }
} }
@@ -146,7 +178,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
itemCount: _globalChannels.length, itemCount: _globalChannels.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final element = _globalChannels[index]; final element = _globalChannels[index];
return buildItem(element); return _buildEntry(element);
}, },
), ),
], ],
@@ -159,13 +191,13 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
itemCount: _globalChannels.length, itemCount: _globalChannels.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final element = _globalChannels[index]; final element = _globalChannels[index];
return buildItem(element); return _buildEntry(element);
}, },
), ),
SliverList.list( SliverList.list(
children: _inRealms.entries.map((element) { children: _inRealms.entries.map((element) {
return ExpansionTile( return ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 20), tilePadding: const EdgeInsets.only(left: 20, right: 24),
minTileHeight: 48, minTileHeight: 48,
title: Text(element.value.first.realm!.name), title: Text(element.value.first.realm!.name),
leading: CircleAvatar( leading: CircleAvatar(
@@ -177,7 +209,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
size: widget.isDense ? 12 : 16, size: widget.isDense ? 12 : 16,
), ),
), ),
children: element.value.map((x) => buildItem(x)).toList(), children: element.value.map((x) => _buildEntry(x)).toList(),
); );
}).toList(), }).toList(),
), ),

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

@@ -23,7 +23,7 @@ class ControlsWidget extends StatefulWidget {
} }
class _ControlsWidgetState extends State<ControlsWidget> { class _ControlsWidgetState extends State<ControlsWidget> {
CameraPosition position = CameraPosition.front; CameraPosition _position = CameraPosition.front;
List<MediaDevice>? _audioInputs; List<MediaDevice>? _audioInputs;
List<MediaDevice>? _audioOutputs; List<MediaDevice>? _audioOutputs;
@@ -36,25 +36,25 @@ class _ControlsWidgetState extends State<ControlsWidget> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
participant.addListener(onChange); _participant.addListener(onChange);
_subscription = Hardware.instance.onDeviceChange.stream _subscription = Hardware.instance.onDeviceChange.stream
.listen((List<MediaDevice> devices) { .listen((List<MediaDevice> devices) {
revertDevices(devices); _revertDevices(devices);
}); });
Hardware.instance.enumerateDevices().then(revertDevices); Hardware.instance.enumerateDevices().then(_revertDevices);
_speakerphoneOn = Hardware.instance.speakerOn ?? false; _speakerphoneOn = Hardware.instance.speakerOn ?? false;
} }
@override @override
void dispose() { void dispose() {
_subscription?.cancel(); _subscription?.cancel();
participant.removeListener(onChange); _participant.removeListener(onChange);
super.dispose(); super.dispose();
} }
LocalParticipant get participant => widget.participant; LocalParticipant get _participant => widget.participant;
void revertDevices(List<MediaDevice> devices) async { void _revertDevices(List<MediaDevice> devices) async {
_audioInputs = devices.where((d) => d.kind == 'audioinput').toList(); _audioInputs = devices.where((d) => d.kind == 'audioinput').toList();
_audioOutputs = devices.where((d) => d.kind == 'audiooutput').toList(); _audioOutputs = devices.where((d) => d.kind == 'audiooutput').toList();
_videoInputs = devices.where((d) => d.kind == 'videoinput').toList(); _videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
@@ -63,7 +63,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
void onChange() => setState(() {}); void onChange() => setState(() {});
bool get isMuted => participant.isMuted; bool get isMuted => _participant.isMuted;
Future<bool?> showDisconnectDialog() { Future<bool?> showDisconnectDialog() {
return showDialog<bool>( return showDialog<bool>(
@@ -85,7 +85,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
); );
} }
void disconnect() async { void _disconnect() async {
if (await showDisconnectDialog() != true) return; if (await showDisconnectDialog() != true) return;
final ChatCallProvider provider = Get.find(); final ChatCallProvider provider = Get.find();
@@ -95,59 +95,59 @@ class _ControlsWidgetState extends State<ControlsWidget> {
} }
} }
void disableAudio() async { void _disableAudio() async {
await participant.setMicrophoneEnabled(false); await _participant.setMicrophoneEnabled(false);
} }
void enableAudio() async { void _enableAudio() async {
await participant.setMicrophoneEnabled(true); await _participant.setMicrophoneEnabled(true);
} }
void disableVideo() async { void _disableVideo() async {
await participant.setCameraEnabled(false); await _participant.setCameraEnabled(false);
} }
void enableVideo() async { void _enableVideo() async {
await participant.setCameraEnabled(true); await _participant.setCameraEnabled(true);
} }
void selectAudioOutput(MediaDevice device) async { void _selectAudioOutput(MediaDevice device) async {
await widget.room.setAudioOutputDevice(device); await widget.room.setAudioOutputDevice(device);
setState(() {}); setState(() {});
} }
void selectAudioInput(MediaDevice device) async { void _selectAudioInput(MediaDevice device) async {
await widget.room.setAudioInputDevice(device); await widget.room.setAudioInputDevice(device);
setState(() {}); setState(() {});
} }
void selectVideoInput(MediaDevice device) async { void _selectVideoInput(MediaDevice device) async {
await widget.room.setVideoInputDevice(device); await widget.room.setVideoInputDevice(device);
setState(() {}); setState(() {});
} }
void setSpeakerphoneOn() { void _setSpeakerphoneOn() {
_speakerphoneOn = !_speakerphoneOn; _speakerphoneOn = !_speakerphoneOn;
Hardware.instance.setSpeakerphoneOn(_speakerphoneOn); Hardware.instance.setSpeakerphoneOn(_speakerphoneOn);
setState(() {}); setState(() {});
} }
void toggleCamera() async { void _toggleCamera() async {
final track = participant.videoTrackPublications.firstOrNull?.track; final track = _participant.videoTrackPublications.firstOrNull?.track;
if (track == null) return; if (track == null) return;
try { try {
final newPosition = position.switched(); final newPosition = _position.switched();
await track.setCameraPosition(newPosition); await track.setCameraPosition(newPosition);
setState(() { setState(() {
position = newPosition; _position = newPosition;
}); });
} catch (error) { } catch (error) {
return; return;
} }
} }
void enableScreenShare() async { void _enableScreenShare() async {
if (lkPlatformIsDesktop()) { if (lkPlatformIsDesktop()) {
try { try {
final source = await showDialog<DesktopCapturerSource>( final source = await showDialog<DesktopCapturerSource>(
@@ -163,7 +163,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
maxFrameRate: 15.0, maxFrameRate: 15.0,
), ),
); );
await participant.publishVideoTrack(track); await _participant.publishVideoTrack(track);
} catch (e) { } catch (e) {
final message = e.toString(); final message = e.toString();
context.showErrorDialog(message); context.showErrorDialog(message);
@@ -177,7 +177,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
maxFrameRate: 30.0, maxFrameRate: 30.0,
), ),
); );
await participant.publishVideoTrack(track); await _participant.publishVideoTrack(track);
return; return;
} }
@@ -188,11 +188,11 @@ class _ControlsWidgetState extends State<ControlsWidget> {
return; return;
} }
await participant.setScreenShareEnabled(true, captureScreenAudio: true); await _participant.setScreenShareEnabled(true, captureScreenAudio: true);
} }
void disableScreenShare() async { void _disableScreenShare() async {
await participant.setScreenShareEnabled(false); await _participant.setScreenShareEnabled(false);
} }
@override @override
@@ -210,12 +210,12 @@ class _ControlsWidgetState extends State<ControlsWidget> {
icon: Transform.flip( icon: Transform.flip(
flipX: true, child: const Icon(Icons.exit_to_app)), flipX: true, child: const Icon(Icons.exit_to_app)),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
onPressed: disconnect, onPressed: _disconnect,
), ),
if (participant.isMicrophoneEnabled()) if (_participant.isMicrophoneEnabled())
if (lkPlatformIs(PlatformType.android)) if (lkPlatformIs(PlatformType.android))
IconButton( IconButton(
onPressed: disableAudio, onPressed: _disableAudio,
icon: const Icon(Icons.mic), icon: const Icon(Icons.mic),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
tooltip: 'callMicrophoneOff'.tr, tooltip: 'callMicrophoneOff'.tr,
@@ -227,7 +227,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
return [ return [
PopupMenuItem<MediaDevice>( PopupMenuItem<MediaDevice>(
value: null, value: null,
onTap: isMuted ? enableAudio : disableAudio, onTap: isMuted ? _enableAudio : _disableAudio,
child: ListTile( child: ListTile(
leading: const Icon(Icons.mic_off), leading: const Icon(Icons.mic_off),
title: Text(isMuted title: Text(isMuted
@@ -246,7 +246,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
: const Icon(Icons.check_box_outline_blank), : const Icon(Icons.check_box_outline_blank),
title: Text(device.label), title: Text(device.label),
), ),
onTap: () => selectAudioInput(device), onTap: () => _selectAudioInput(device),
); );
}) })
]; ];
@@ -254,19 +254,19 @@ class _ControlsWidgetState extends State<ControlsWidget> {
) )
else else
IconButton( IconButton(
onPressed: enableAudio, onPressed: _enableAudio,
icon: const Icon(Icons.mic_off), icon: const Icon(Icons.mic_off),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
tooltip: 'callMicrophoneOn'.tr, tooltip: 'callMicrophoneOn'.tr,
), ),
if (participant.isCameraEnabled()) if (_participant.isCameraEnabled())
PopupMenuButton<MediaDevice>( PopupMenuButton<MediaDevice>(
icon: const Icon(Icons.videocam_sharp), icon: const Icon(Icons.videocam_sharp),
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return [ return [
PopupMenuItem<MediaDevice>( PopupMenuItem<MediaDevice>(
value: null, value: null,
onTap: disableVideo, onTap: _disableVideo,
child: ListTile( child: ListTile(
leading: const Icon(Icons.videocam_off), leading: const Icon(Icons.videocam_off),
title: Text('callCameraOff'.tr), title: Text('callCameraOff'.tr),
@@ -283,7 +283,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
: const Icon(Icons.check_box_outline_blank), : const Icon(Icons.check_box_outline_blank),
title: Text(device.label), title: Text(device.label),
), ),
onTap: () => selectVideoInput(device), onTap: () => _selectVideoInput(device),
); );
}) })
]; ];
@@ -291,17 +291,17 @@ class _ControlsWidgetState extends State<ControlsWidget> {
) )
else else
IconButton( IconButton(
onPressed: enableVideo, onPressed: _enableVideo,
icon: const Icon(Icons.videocam_off), icon: const Icon(Icons.videocam_off),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
tooltip: 'callCameraOn'.tr, tooltip: 'callCameraOn'.tr,
), ),
IconButton( IconButton(
icon: Icon(position == CameraPosition.back icon: Icon(_position == CameraPosition.back
? Icons.video_camera_back ? Icons.video_camera_back
: Icons.video_camera_front), : Icons.video_camera_front),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
onPressed: () => toggleCamera(), onPressed: () => _toggleCamera(),
tooltip: 'callVideoFlip'.tr, tooltip: 'callVideoFlip'.tr,
), ),
if (!lkPlatformIs(PlatformType.iOS)) if (!lkPlatformIs(PlatformType.iOS))
@@ -327,7 +327,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
: const Icon(Icons.check_box_outline_blank), : const Icon(Icons.check_box_outline_blank),
title: Text(device.label), title: Text(device.label),
), ),
onTap: () => selectAudioOutput(device), onTap: () => _selectAudioOutput(device),
); );
}) })
]; ];
@@ -336,7 +336,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
if (!kIsWeb && lkPlatformIs(PlatformType.iOS)) if (!kIsWeb && lkPlatformIs(PlatformType.iOS))
IconButton( IconButton(
onPressed: Hardware.instance.canSwitchSpeakerphone onPressed: Hardware.instance.canSwitchSpeakerphone
? setSpeakerphoneOn ? _setSpeakerphoneOn
: null, : null,
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
icon: Icon( icon: Icon(
@@ -344,18 +344,18 @@ class _ControlsWidgetState extends State<ControlsWidget> {
), ),
tooltip: 'callSpeakerphoneToggle'.tr, tooltip: 'callSpeakerphoneToggle'.tr,
), ),
if (participant.isScreenShareEnabled()) if (_participant.isScreenShareEnabled())
IconButton( IconButton(
icon: const Icon(Icons.monitor_outlined), icon: const Icon(Icons.monitor_outlined),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
onPressed: () => disableScreenShare(), onPressed: () => _disableScreenShare(),
tooltip: 'callScreenOff'.tr, tooltip: 'callScreenOff'.tr,
) )
else else
IconButton( IconButton(
icon: const Icon(Icons.monitor), icon: const Icon(Icons.monitor),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
onPressed: () => enableScreenShare(), onPressed: () => _enableScreenShare(),
tooltip: 'callScreenOn'.tr, tooltip: 'callScreenOn'.tr,
), ),
], ],

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,35 +210,32 @@ 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,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return GestureDetector(
color: Colors.transparent, child: Container(
child: InkWell( width: width,
child: Container( height: height,
width: width, color: color,
height: height, child: ParticipantWidget.widgetFor(participant, isFixed: isFixedAvatar),
color: color,
child: ParticipantWidget.widgetFor(participant, isFixed: isFixed),
),
onTap: () => onTap(),
onLongPress: () {
if (participant.participant is LocalParticipant) return;
showModalBottomSheet(
context: context,
builder: (context) => ParticipantMenu(
participant: participant.participant as RemoteParticipant,
videoTrack: participant.videoTrack,
isScreenShare: participant.isScreenShare,
),
);
},
), ),
onTap: () => onTap(),
onLongPress: () {
if (participant.participant is LocalParticipant) return;
showModalBottomSheet(
context: context,
builder: (context) => ParticipantMenu(
participant: participant.participant as RemoteParticipant,
videoTrack: participant.videoTrack,
isScreenShare: participant.isScreenShare,
),
);
},
); );
} }
} }

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
class ParticipantInfoWidget extends StatelessWidget { class ParticipantInfoWidget extends StatelessWidget {
@@ -34,38 +35,42 @@ class ParticipantInfoWidget extends StatelessWidget {
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
), ),
const SizedBox(width: 5),
isScreenShare isScreenShare
? const Padding( ? const Icon(
padding: EdgeInsets.only(left: 5), Icons.monitor,
child: Icon( color: Colors.white,
Icons.monitor, size: 16,
color: Colors.white,
size: 16,
),
) )
: Padding( : Icon(
padding: const EdgeInsets.only(left: 5), audioAvailable ? Icons.mic : Icons.mic_off,
child: Icon( color: audioAvailable ? Colors.white : Colors.red,
audioAvailable ? Icons.mic : Icons.mic_off, size: 16,
color: audioAvailable ? Colors.white : Colors.red,
size: 16,
),
), ),
const SizedBox(width: 3),
if (connectionQuality != ConnectionQuality.unknown) if (connectionQuality != ConnectionQuality.unknown)
Padding( Icon(
padding: const EdgeInsets.only(left: 5), {
child: Icon( ConnectionQuality.excellent: Icons.signal_cellular_alt,
connectionQuality == ConnectionQuality.poor ConnectionQuality.good: Icons.signal_cellular_alt_2_bar,
? Icons.wifi_off_outlined ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar,
: Icons.wifi, }[connectionQuality],
color: { color: {
ConnectionQuality.excellent: Colors.green, ConnectionQuality.excellent: Colors.green,
ConnectionQuality.good: Colors.orange, ConnectionQuality.good: Colors.orange,
ConnectionQuality.poor: Colors.red, ConnectionQuality.poor: Colors.red,
}[connectionQuality], }[connectionQuality],
size: 16, size: 16,
)
else
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
), ),
), ).paddingAll(3),
], ],
), ),
); );

View File

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

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,4 +1,5 @@
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/controllers/chat_events_controller.dart'; import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
@@ -23,7 +24,7 @@ class ChatEventList extends StatelessWidget {
required this.onReply, required this.onReply,
}); });
bool checkMessageMergeable(Event? a, Event? b) { bool _checkMessageMergeable(Event? a, Event? b) {
if (a == null || b == null) return false; if (a == null || b == null) return false;
if (a.sender.account.id != b.sender.account.id) return false; if (a.sender.account.id != b.sender.account.id) return false;
return a.createdAt.difference(b.createdAt).inMinutes <= 3; return a.createdAt.difference(b.createdAt).inMinutes <= 3;
@@ -41,13 +42,13 @@ class ChatEventList extends StatelessWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
bool isMerged = false, hasMerged = false; bool isMerged = false, hasMerged = false;
if (index > 0) { if (index > 0) {
hasMerged = checkMessageMergeable( hasMerged = _checkMessageMergeable(
chatController.currentEvents[index - 1].data, chatController.currentEvents[index - 1].data,
chatController.currentEvents[index].data, chatController.currentEvents[index].data,
); );
} }
if (index + 1 < chatController.currentEvents.length) { if (index + 1 < chatController.currentEvents.length) {
isMerged = checkMessageMergeable( isMerged = _checkMessageMergeable(
chatController.currentEvents[index].data, chatController.currentEvents[index].data,
chatController.currentEvents[index + 1].data, chatController.currentEvents[index + 1].data,
); );
@@ -82,7 +83,12 @@ class ChatEventList extends StatelessWidget {
), ),
); );
}, },
); ).animate(key: Key('m-animation${item.uuid}')).slideY(
duration: 250.ms,
curve: Curves.fastEaseInToSlowEaseOut,
end: 0,
begin: 0.5,
);
}, },
); );
}), }),

View File

@@ -5,9 +5,11 @@ import 'package:solian/exts.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart'; import 'package:solian/models/event.dart';
import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart'; import 'package:solian/widgets/attachments/attachment_editor.dart';
import 'package:solian/widgets/chat/chat_event.dart'; import 'package:solian/widgets/chat/chat_event.dart';
import 'package:badges/badges.dart' as badges;
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ChatMessageInput extends StatefulWidget { class ChatMessageInput extends StatefulWidget {
@@ -80,6 +82,14 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
final prof = auth.userProfile.value!; final prof = auth.userProfile.value!;
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
final AttachmentUploaderController uploader = Get.find();
if (uploader.queueOfUpload.any(
((x) => x.usage == 'm.attachment' && x.isUploading),
)) {
context.showErrorDialog('attachmentUploadInProgress'.tr);
return;
}
Response resp; Response resp;
final mentionedUserNames = _findMentionedUsers(_textController.text); final mentionedUserNames = _findMentionedUsers(_textController.text);
@@ -259,7 +269,18 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
), ),
), ),
IconButton( IconButton(
icon: const Icon(Icons.attach_file), icon: badges.Badge(
badgeContent: Text(
_attachments.length.toString(),
style: const TextStyle(color: Colors.white),
),
showBadge: _attachments.isNotEmpty,
position: badges.BadgePosition.topEnd(
top: -12,
end: -8,
),
child: const Icon(Icons.file_present_rounded),
),
color: Colors.teal, color: Colors.teal,
onPressed: () => _editAttachments(), onPressed: () => _editAttachments(),
), ),

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

@@ -1,9 +1,11 @@
import 'package:animations/animations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get_utils/get_utils.dart'; import 'package:get/get_utils/get_utils.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/router.dart'; import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/shells/title_shell.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart'; import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
@@ -22,7 +24,8 @@ class PostItem extends StatefulWidget {
final bool isShowEmbed; final bool isShowEmbed;
final bool isFullDate; final bool isFullDate;
final bool isContentSelectable; final bool isContentSelectable;
final String? overrideAttachmentParent; final String? attachmentParent;
final Color? backgroundColor;
const PostItem({ const PostItem({
super.key, super.key,
@@ -34,7 +37,8 @@ class PostItem extends StatefulWidget {
this.isShowEmbed = true, this.isShowEmbed = true,
this.isFullDate = false, this.isFullDate = false,
this.isContentSelectable = false, this.isContentSelectable = false,
this.overrideAttachmentParent, this.attachmentParent,
this.backgroundColor,
}); });
@override @override
@@ -159,66 +163,92 @@ class _PostItemState extends State<PostItem> {
} }
Widget _buildReply(BuildContext context) { Widget _buildReply(BuildContext context) {
return Column( return OpenContainer(
children: [ closedBuilder: (_, openContainer) => Column(
Row( children: [
children: [ Row(
FaIcon( children: [
FontAwesomeIcons.reply, FaIcon(
size: 16, FontAwesomeIcons.reply,
color: _unFocusColor, size: 16,
), color: _unFocusColor,
Expanded( ),
child: Text( Expanded(
'postRepliedNotify'.trParams( child: Text(
{'username': '@${widget.item.replyTo!.author.name}'}, 'postRepliedNotify'.trParams(
), {'username': '@${widget.item.replyTo!.author.name}'},
style: TextStyle(color: _unFocusColor), ),
).paddingOnly(left: 6), style: TextStyle(color: _unFocusColor),
), ).paddingOnly(left: 6),
], ),
).paddingOnly(left: 12), ],
Card( ).paddingOnly(left: 12),
elevation: 1, Card(
child: PostItem( elevation: 1,
item: widget.item.replyTo!, child: PostItem(
isCompact: true, item: widget.item.replyTo!,
overrideAttachmentParent: widget.item.id.toString(), isCompact: true,
).paddingSymmetric(vertical: 8), attachmentParent: widget.item.id.toString(),
).paddingSymmetric(vertical: 8),
),
],
),
openBuilder: (_, __) => TitleShell(
title: 'postDetail'.tr,
child: PostDetailScreen(
id: widget.item.replyTo!.id.toString(),
post: widget.item.replyTo!,
), ),
], ),
closedElevation: 0,
openElevation: 0,
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface,
); );
} }
Widget _buildRepost(BuildContext context) { Widget _buildRepost(BuildContext context) {
return Column( return OpenContainer(
children: [ closedBuilder: (_, openContainer) => Column(
Row( children: [
children: [ Row(
FaIcon( children: [
FontAwesomeIcons.retweet, FaIcon(
size: 16, FontAwesomeIcons.retweet,
color: _unFocusColor, size: 16,
), color: _unFocusColor,
Expanded( ),
child: Text( Expanded(
'postRepostedNotify'.trParams( child: Text(
{'username': '@${widget.item.repostTo!.author.name}'}, 'postRepostedNotify'.trParams(
), {'username': '@${widget.item.repostTo!.author.name}'},
style: TextStyle(color: _unFocusColor), ),
).paddingOnly(left: 6), style: TextStyle(color: _unFocusColor),
), ).paddingOnly(left: 6),
], ),
).paddingOnly(left: 12), ],
Card( ).paddingOnly(left: 12),
elevation: 1, Card(
child: PostItem( elevation: 1,
item: widget.item.repostTo!, child: PostItem(
isCompact: true, item: widget.item.repostTo!,
overrideAttachmentParent: widget.item.id.toString(), isCompact: true,
).paddingSymmetric(vertical: 8), attachmentParent: widget.item.id.toString(),
).paddingSymmetric(vertical: 8),
),
],
),
openBuilder: (_, __) => TitleShell(
title: 'postDetail'.tr,
child: PostDetailScreen(
id: widget.item.repostTo!.id.toString(),
post: widget.item.repostTo!,
), ),
], ),
closedElevation: 0,
openElevation: 0,
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface,
); );
} }
@@ -264,100 +294,91 @@ class _PostItemState extends State<PostItem> {
); );
} }
return Column( return OpenContainer(
crossAxisAlignment: CrossAxisAlignment.start, closedBuilder: (_, openContainer) => Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Row( children: [
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
GestureDetector( children: [
child: AccountAvatar(content: item.author.avatar.toString()), GestureDetector(
onTap: () { child: AccountAvatar(content: item.author.avatar.toString()),
showModalBottomSheet( onTap: () {
useRootNavigator: true, showModalBottomSheet(
isScrollControlled: true, useRootNavigator: true,
backgroundColor: Theme.of(context).colorScheme.surface, isScrollControlled: true,
context: context, backgroundColor: Theme.of(context).colorScheme.surface,
builder: (context) => AccountProfilePopup( context: context,
account: item.author, builder: (context) => AccountProfilePopup(
), name: item.author.name,
);
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
SizedContainer(
maxWidth: 640,
child: MarkdownTextContent(
content: item.body['content'],
isSelectable: widget.isContentSelectable,
).paddingOnly(left: 12, right: 8),
),
if (widget.item.replyTo != null && widget.isShowEmbed)
GestureDetector(
child: _buildReply(context).paddingOnly(top: 4),
onTap: () {
if (!widget.isClickable) return;
AppRouter.instance.pushNamed(
'postDetail',
pathParameters: {
'id': widget.item.replyTo!.id.toString(),
},
);
},
), ),
if (widget.item.repostTo != null && widget.isShowEmbed) );
GestureDetector( },
child: _buildRepost(context).paddingOnly(top: 4),
onTap: () {
if (!widget.isClickable) return;
AppRouter.instance.pushNamed(
'postDetail',
pathParameters: {
'alias': widget.item.repostTo!.id.toString(),
},
);
},
),
_buildFooter().paddingOnly(left: 12),
],
), ),
), Expanded(
], child: Column(
).paddingOnly( crossAxisAlignment: CrossAxisAlignment.start,
top: 10, children: [
bottom: hasAttachment ? 10 : 0, _buildHeader(),
right: 16, SizedContainer(
left: 16, maxWidth: 640,
), child: MarkdownTextContent(
AttachmentList( content: item.body['content'],
parentId: widget.item.id.toString(), isSelectable: widget.isContentSelectable,
attachmentsId: attachments, ).paddingOnly(left: 12, right: 8),
isGrid: attachments.length > 1, ),
), if (widget.item.replyTo != null && widget.isShowEmbed)
if (widget.isShowReply && widget.isReactable) _buildReply(context).paddingOnly(top: 4),
PostQuickAction( if (widget.item.repostTo != null && widget.isShowEmbed)
isShowReply: widget.isShowReply, _buildRepost(context).paddingOnly(top: 4),
isReactable: widget.isReactable, _buildFooter().paddingOnly(left: 12),
item: widget.item, ],
onReact: (symbol, changes) { ),
setState(() { ),
item.metric!.reactionList[symbol] = ],
(item.metric!.reactionList[symbol] ?? 0) + changes;
});
},
).paddingOnly( ).paddingOnly(
top: hasAttachment ? 10 : 6, top: 10,
left: hasAttachment ? 24 : 60, bottom: hasAttachment ? 10 : 0,
right: 16, right: 16,
bottom: 10, left: 16,
) ),
else AttachmentList(
const SizedBox(height: 10), parentId: widget.item.id.toString(),
], attachmentsId: attachments,
isGrid: attachments.length > 1,
),
if (widget.isShowReply && widget.isReactable)
PostQuickAction(
isShowReply: widget.isShowReply,
isReactable: widget.isReactable,
item: widget.item,
onReact: (symbol, changes) {
setState(() {
item.metric!.reactionList[symbol] =
(item.metric!.reactionList[symbol] ?? 0) + changes;
});
},
).paddingOnly(
top: hasAttachment ? 10 : 6,
left: hasAttachment ? 24 : 60,
right: 16,
bottom: 10,
)
else
const SizedBox(height: 10),
],
),
openBuilder: (_, __) => TitleShell(
title: 'postDetail'.tr,
child: PostDetailScreen(
id: item.id.toString(),
post: item,
),
),
closedElevation: 0,
openElevation: 0,
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface,
); );
} }
} }

View File

@@ -3,8 +3,6 @@ import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:solian/widgets/posts/post_action.dart'; import 'package:solian/widgets/posts/post_action.dart';
import 'package:solian/widgets/posts/post_item.dart'; import 'package:solian/widgets/posts/post_item.dart';
@@ -13,6 +11,7 @@ class PostListWidget extends StatelessWidget {
final bool isClickable; final bool isClickable;
final bool isNestedClickable; final bool isNestedClickable;
final PagingController<int, Post> controller; final PagingController<int, Post> controller;
final Color? backgroundColor;
const PostListWidget({ const PostListWidget({
super.key, super.key,
@@ -20,6 +19,7 @@ class PostListWidget extends StatelessWidget {
this.isShowEmbed = true, this.isShowEmbed = true,
this.isClickable = true, this.isClickable = true,
this.isNestedClickable = true, this.isNestedClickable = true,
this.backgroundColor,
}); });
@override @override
@@ -29,16 +29,15 @@ class PostListWidget extends StatelessWidget {
pagingController: controller, pagingController: controller,
builderDelegate: PagedChildBuilderDelegate<Post>( builderDelegate: PagedChildBuilderDelegate<Post>(
itemBuilder: (context, item, index) { itemBuilder: (context, item, index) {
return CenteredContainer( return PostListEntryWidget(
child: PostListEntryWidget( isShowEmbed: isShowEmbed,
isShowEmbed: isShowEmbed, isNestedClickable: isNestedClickable,
isNestedClickable: isNestedClickable, isClickable: isClickable,
isClickable: isClickable, item: item,
item: item, backgroundColor: backgroundColor,
onUpdate: () { onUpdate: () {
controller.refresh(); controller.refresh();
}, },
),
); );
}, },
), ),
@@ -54,6 +53,7 @@ class PostListEntryWidget extends StatelessWidget {
final bool isClickable; final bool isClickable;
final Post item; final Post item;
final Function onUpdate; final Function onUpdate;
final Color? backgroundColor;
const PostListEntryWidget({ const PostListEntryWidget({
super.key, super.key,
@@ -63,6 +63,7 @@ class PostListEntryWidget extends StatelessWidget {
required this.isClickable, required this.isClickable,
required this.item, required this.item,
required this.onUpdate, required this.onUpdate,
this.backgroundColor,
}); });
@override @override
@@ -73,14 +74,8 @@ class PostListEntryWidget extends StatelessWidget {
item: item, item: item,
isShowEmbed: isShowEmbed, isShowEmbed: isShowEmbed,
isClickable: isNestedClickable, isClickable: isNestedClickable,
backgroundColor: backgroundColor,
).paddingSymmetric(vertical: 8), ).paddingSymmetric(vertical: 8),
onTap: () {
if (!isClickable) return;
AppRouter.instance.pushNamed(
'postDetail',
pathParameters: {'id': item.id.toString()},
);
},
onLongPress: () { onLongPress: () {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;

View File

@@ -8,10 +8,12 @@ import 'package:solian/widgets/posts/post_list.dart';
class PostReplyList extends StatefulWidget { class PostReplyList extends StatefulWidget {
final Post item; final Post item;
final Color? backgroundColor;
const PostReplyList({ const PostReplyList({
super.key, super.key,
required this.item, required this.item,
this.backgroundColor,
}); });
@override @override
@@ -45,7 +47,6 @@ class _PostReplyListState extends State<PostReplyList> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_pagingController.addPageRequestListener(getReplies); _pagingController.addPageRequestListener(getReplies);
} }
@@ -54,6 +55,7 @@ class _PostReplyListState extends State<PostReplyList> {
return PostListWidget( return PostListWidget(
isShowEmbed: false, isShowEmbed: false,
controller: _pagingController, controller: _pagingController,
backgroundColor: widget.backgroundColor,
); );
} }
} }
@@ -74,7 +76,12 @@ class PostReplyListPopup extends StatelessWidget {
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded( Expanded(
child: CustomScrollView( child: CustomScrollView(
slivers: [PostReplyList(item: item)], slivers: [
PostReplyList(
item: item,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
),
],
), ),
), ),
], ],

View File

@@ -24,6 +24,7 @@ class PostSingleDisplay extends StatelessWidget {
isShowEmbed: true, isShowEmbed: true,
isNestedClickable: true, isNestedClickable: true,
onUpdate: onUpdate, onUpdate: onUpdate,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
), ),
), ),
), ),

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

@@ -25,6 +25,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.4.1" version: "6.4.1"
animations:
dependency: "direct main"
description:
name: animations
sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb
url: "https://pub.dev"
source: hosted
version: "2.0.11"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@@ -42,13 +50,21 @@ packages:
source: hosted source: hosted
version: "2.5.0" version: "2.5.0"
async: async:
dependency: transitive dependency: "direct main"
description: description:
name: async name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" version: "2.11.0"
avatar_stack:
dependency: "direct main"
description:
name: avatar_stack
sha256: e4a1576f7478add964bbb8aa5e530db39288fbbf81c30c4fb4b81162dd68aa49
url: "https://pub.dev"
source: hosted
version: "1.2.0"
badges: badges:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -133,26 +149,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cached_network_image name: cached_network_image
sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" version: "3.4.0"
cached_network_image_platform_interface: cached_network_image_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: cached_network_image_platform_interface name: cached_network_image_platform_interface
sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.1.0"
cached_network_image_web: cached_network_image_web:
dependency: transitive dependency: transitive
description: description:
name: cached_network_image_web name: cached_network_image_web
sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
carousel_slider: carousel_slider:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -245,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:
@@ -301,10 +317,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dev_build name: dev_build
sha256: "5600100e28f7424ed53728e8e7aa6bc0e0506ec04bb49a82616f62112c1822c3" sha256: f526d1fbe68875f6119ffc333f114dfe6aa93ad04439276d53968f7977cc410e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0+8" version: "1.0.0+11"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -353,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:
@@ -668,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:
@@ -716,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:
@@ -753,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"
@@ -804,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:
@@ -932,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:
@@ -1092,10 +1100,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: octo_image name: octo_image
sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.1.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@@ -1436,18 +1444,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sentry name: sentry
sha256: "60756499f09c3ed944640d7993ac527a89f7c3033f13ec12ae145706b269b5c8" sha256: "76ad4fab90ff82427c26939bd79dc4df345a081e2b1cd5954b947e340b9af9a5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.5.0" version: "8.6.0"
sentry_flutter: sentry_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: sentry_flutter name: sentry_flutter
sha256: "26cfe89cb08a60d9bc0c4e748a0508e223ae378265aec8ed2a2b48f0d2c936b9" sha256: a7c92014701093a7c0a373e1a47c54ec428d8468d8bf2b793fee7ea1b085c21b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.5.0" version: "8.6.0"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1468,10 +1476,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: c3f888ba2d659f3e75f4686112cc1e71f46177f74452d40d8307edc332296ead sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
@@ -1508,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:
@@ -1685,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:
@@ -1793,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+6 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,12 +58,14 @@ 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
image_cropper: ^8.0.1 image_cropper: ^8.0.1
markdown_toolbar: ^0.5.0 markdown_toolbar: ^0.5.0
animations: ^2.0.11
avatar_stack: ^1.2.0
async: ^2.11.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -113,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

Some files were not shown because too many files have changed in this diff Show More