Compare commits
40 Commits
8b3c45ab29
...
1.2.1+2
| Author | SHA1 | Date | |
|---|---|---|---|
| 19eabfaba1 | |||
| ec2eadad6d | |||
| 54e176e75d | |||
| 0a7ccaeefa | |||
| a5f093e185 | |||
| a4f68dd175 | |||
| 8067c35c70 | |||
| ebe381053e | |||
| 03f2470dae | |||
| ea434815cf | |||
| bbea4b4359 | |||
| e0b485cc81 | |||
| 87bb37ac01 | |||
| 989b5babd9 | |||
| 9ea364640d | |||
| a9f55a489d | |||
| 4616f3a3e2 | |||
| 425bae9d13 | |||
| 07771e8979 | |||
| 0ad4854443 | |||
| 4238ea6fdc | |||
| 7d45c06302 | |||
| 7e8993fbd2 | |||
| c88fcc84da | |||
| 11fb79623e | |||
| 98cc313a91 | |||
| bc3401a897 | |||
| 5b6a5d9046 | |||
| 6cbd78e836 | |||
| aefcbad02f | |||
| 70617be687 | |||
| cccb3d5c16 | |||
| a0a3a8d182 | |||
| c6b2ef8459 | |||
| 34a2fe3988 | |||
| 0a5604d0ff | |||
| 5e754ad233 | |||
| 5b9c92e4d3 | |||
| b2a6ca7244 | |||
| 27c60fc8cb |
@@ -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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 406 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 360 KiB |
BIN
assets/logo.png
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 360 KiB |
@@ -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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 320 KiB |
|
Before Width: | Height: | Size: 541 B After Width: | Height: | Size: 822 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 815 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 20 KiB |
@@ -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();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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
@@ -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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
38
lib/providers/stickers.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
181
lib/screens/account/stickers.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -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!),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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': '在贴图选择界面提供给用户的人类友好名称。',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
89
lib/widgets/channel/channel_call_indicator.dart
Normal 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
211
lib/widgets/stickers/sticker_uploader.dart
Normal 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 354 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 582 B After Width: | Height: | Size: 698 B |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 5.0 KiB |
96
pubspec.lock
@@ -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:
|
||||||
|
|||||||
10
pubspec.yaml
@@ -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
|
||||||
|
|||||||
BIN
web/favicon.png
|
Before Width: | Height: | Size: 582 B After Width: | Height: | Size: 698 B |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 26 KiB |