Compare commits
46 Commits
b70d3795d1
...
1.2.0+9
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 8b3c45ab29 | |||
| adb415700a | |||
| 1e4b44a78b | |||
| 9765b200b9 | |||
| 47d03ce1e5 | |||
| c41a71388d | |||
| 7655dfdf37 | |||
| 190bb34958 | |||
| d02ed68afa | |||
| 2bc4513bb6 | |||
| f10393f6d0 | |||
| ecef8dab0c | |||
| 52e58fce3d | |||
| 31d50bfb1f | |||
| ca8ad12d93 | |||
| f799900450 | |||
| dfdf7b23c8 | |||
| 771b2029b0 | |||
| cc9c99f375 |
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
channel: stable
|
||||
cache: true
|
||||
- run: flutter pub get
|
||||
- run: flutter build web
|
||||
- run: flutter build web --release --base-href=/
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -70,6 +70,12 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
||||
BIN
assets/fonts/Comfortaa-Bold.ttf
Executable file
BIN
assets/fonts/Comfortaa-Bold.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/Comfortaa-Regular.ttf
Executable file
BIN
assets/fonts/Comfortaa-Regular.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/NotoColorEmoji-Regular.ttf
Executable file
BIN
assets/fonts/NotoColorEmoji-Regular.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/NotoSansHK-Bold.ttf
Executable file
BIN
assets/fonts/NotoSansHK-Bold.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/NotoSansHK-Regular.ttf
Executable file
BIN
assets/fonts/NotoSansHK-Regular.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/NotoSansJP-Bold.ttf
Executable file
BIN
assets/fonts/NotoSansJP-Bold.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/NotoSansJP-Regular.ttf
Executable file
BIN
assets/fonts/NotoSansJP-Regular.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/NotoSansSC-Bold.ttf
Executable file
BIN
assets/fonts/NotoSansSC-Bold.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/NotoSansSC-Regular.ttf
Executable file
BIN
assets/fonts/NotoSansSC-Regular.ttf
Executable file
Binary file not shown.
@@ -1,3 +1,4 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
- provider: true
|
||||
@@ -38,19 +38,19 @@ PODS:
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- Firebase/CoreOnly (10.28.0):
|
||||
- FirebaseCore (= 10.28.0)
|
||||
- Firebase/Messaging (10.28.0):
|
||||
- Firebase/CoreOnly (10.29.0):
|
||||
- FirebaseCore (= 10.29.0)
|
||||
- Firebase/Messaging (10.29.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 10.28.0)
|
||||
- firebase_core (3.2.0):
|
||||
- Firebase/CoreOnly (= 10.28.0)
|
||||
- FirebaseMessaging (~> 10.29.0)
|
||||
- firebase_core (3.3.0):
|
||||
- Firebase/CoreOnly (= 10.29.0)
|
||||
- Flutter
|
||||
- firebase_messaging (15.0.3):
|
||||
- Firebase/Messaging (= 10.28.0)
|
||||
- firebase_messaging (15.0.4):
|
||||
- Firebase/Messaging (= 10.29.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseCore (10.28.0):
|
||||
- FirebaseCore (10.29.0):
|
||||
- FirebaseCoreInternal (~> 10.0)
|
||||
- GoogleUtilities/Environment (~> 7.12)
|
||||
- GoogleUtilities/Logger (~> 7.12)
|
||||
@@ -61,7 +61,7 @@ PODS:
|
||||
- GoogleUtilities/Environment (~> 7.8)
|
||||
- GoogleUtilities/UserDefaults (~> 7.8)
|
||||
- PromisesObjC (~> 2.1)
|
||||
- FirebaseMessaging (10.28.0):
|
||||
- FirebaseMessaging (10.29.0):
|
||||
- FirebaseCore (~> 10.0)
|
||||
- FirebaseInstallations (~> 10.0)
|
||||
- GoogleDataTransport (~> 9.3)
|
||||
@@ -76,6 +76,9 @@ PODS:
|
||||
- flutter_webrtc (0.11.3):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.04)
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleDataTransport (9.4.1):
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
- nanopb (< 2.30911.0, >= 2.30908.0)
|
||||
@@ -105,9 +108,12 @@ PODS:
|
||||
- GoogleUtilities/UserDefaults (7.13.3):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- image_cropper (0.0.4):
|
||||
- Flutter
|
||||
- TOCropViewController (~> 2.7.4)
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- livekit_client (2.2.2):
|
||||
- livekit_client (2.2.3):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.04)
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
@@ -139,7 +145,7 @@ PODS:
|
||||
- SDWebImage/Core (= 5.19.4)
|
||||
- SDWebImage/Core (5.19.4)
|
||||
- Sentry/HybridSDK (8.32.0)
|
||||
- sentry_flutter (8.5.0):
|
||||
- sentry_flutter (8.6.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 8.32.0)
|
||||
@@ -152,6 +158,7 @@ PODS:
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- SwiftyGif (5.4.5)
|
||||
- TOCropViewController (2.7.4)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
@@ -169,6 +176,8 @@ DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
@@ -204,6 +213,7 @@ SPEC REPOS:
|
||||
- SDWebImage
|
||||
- Sentry
|
||||
- SwiftyGif
|
||||
- TOCropViewController
|
||||
- WebRTC-SDK
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@@ -223,6 +233,10 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
flutter_webrtc:
|
||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
||||
gal:
|
||||
:path: ".symlinks/plugins/gal/darwin"
|
||||
image_cropper:
|
||||
:path: ".symlinks/plugins/image_cropper/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
livekit_client:
|
||||
@@ -266,20 +280,22 @@ SPEC CHECKSUMS:
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
Firebase: 5121c624121af81cbc81df3bda414b3c28c4f3c3
|
||||
firebase_core: a9d0180d5285527884d07a41eb4a9ec9ed12cdb6
|
||||
firebase_messaging: ccc82a143a74de75f440a4e413dbbb37ec3fddbc
|
||||
FirebaseCore: 857dc1c6dd1255675047404d8466f7dfaac5d779
|
||||
Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d
|
||||
firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb
|
||||
firebase_messaging: c862b3d2b973ecc769194dc8de09bd22c77ae757
|
||||
FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16
|
||||
FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934
|
||||
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
|
||||
FirebaseMessaging: 087a7c7cadef7b9239f005bc4db823894844f323
|
||||
FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
|
||||
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
livekit_client: c767049a635d5b6d43de3273dca3c439b8a6e970
|
||||
livekit_client: bad83a7776a41abc42e1f26d903eeac9164c8a9f
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
@@ -293,11 +309,12 @@ SPEC CHECKSUMS:
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
|
||||
Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb
|
||||
sentry_flutter: f1d86adcb93a959bc47a40d8d55059bdf7569bc5
|
||||
sentry_flutter: 090351ce1ff5f96a4b33ef9455b7e3b28185387d
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/providers/content/realm.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
@@ -21,6 +26,7 @@ class BootstrapperShell extends StatefulWidget {
|
||||
class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
bool _isBusy = true;
|
||||
bool _isErrored = false;
|
||||
bool _isDismissable = true;
|
||||
String? _subtitle;
|
||||
|
||||
Color get _unFocusColor =>
|
||||
@@ -29,6 +35,38 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
int _periodCursor = 0;
|
||||
|
||||
late final List<({String label, Future<void> Function() action})> _periods = [
|
||||
(
|
||||
label: 'bsLoadingTheme',
|
||||
action: () async {
|
||||
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',
|
||||
action: () async {
|
||||
@@ -38,12 +76,14 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
setState(() {
|
||||
_isErrored = true;
|
||||
_subtitle = 'bsCheckingServerDown'.tr;
|
||||
_isDismissable = false;
|
||||
});
|
||||
throw Exception('unable connect to server');
|
||||
} else if (resp.statusCode == null) {
|
||||
setState(() {
|
||||
_isErrored = true;
|
||||
_subtitle = 'bsCheckingServerFail'.tr;
|
||||
_isDismissable = false;
|
||||
});
|
||||
throw Exception('unable connect to server');
|
||||
}
|
||||
@@ -74,8 +114,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isTrue) {
|
||||
await Future.wait([
|
||||
Get.find<RealmProvider>().refreshAvailableRealms(),
|
||||
Get.find<ChannelProvider>().refreshAvailableChannel(),
|
||||
Get.find<RelationshipProvider>().refreshFriendList(),
|
||||
Get.find<RelationshipProvider>().refreshRelativeList(),
|
||||
]);
|
||||
}
|
||||
},
|
||||
@@ -138,9 +179,11 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
GestureDetector(
|
||||
child: Column(
|
||||
children: [
|
||||
if (_isErrored)
|
||||
const Icon(Icons.cancel, size: 24)
|
||||
else
|
||||
if (_isErrored && !_isDismissable)
|
||||
const Icon(Icons.cancel, size: 24),
|
||||
if (_isErrored && _isDismissable)
|
||||
const Icon(Icons.warning, size: 24),
|
||||
if (!_isErrored && _isBusy)
|
||||
const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
@@ -151,15 +194,24 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
maxWidth: 280,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
_subtitle ??
|
||||
'${_periods[_periodCursor].label.tr} (${_periodCursor + 1}/${_periods.length})',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _unFocusColor,
|
||||
if (_subtitle == null)
|
||||
Text(
|
||||
'${_periods[_periodCursor].label.tr} (${_periodCursor + 1}/${_periods.length})',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_subtitle != null)
|
||||
Text(
|
||||
_subtitle!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
).paddingOnly(bottom: 4),
|
||||
Text(
|
||||
'2024 © Solsynth LLC',
|
||||
textAlign: TextAlign.center,
|
||||
@@ -175,12 +227,19 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
),
|
||||
onTap: () {
|
||||
if (_isBusy) return;
|
||||
setState(() {
|
||||
_isBusy = true;
|
||||
_isErrored = false;
|
||||
_periodCursor = 0;
|
||||
});
|
||||
_runPeriods();
|
||||
if (_isDismissable) {
|
||||
setState(() {
|
||||
_isBusy = false;
|
||||
_isErrored = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_isBusy = true;
|
||||
_isErrored = false;
|
||||
_periodCursor = 0;
|
||||
});
|
||||
_runPeriods();
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
|
||||
@@ -16,7 +16,7 @@ class ChatEventController {
|
||||
Channel? channel;
|
||||
String? scope;
|
||||
|
||||
initialize() async {
|
||||
Future<void> initialize() async {
|
||||
if (!PlatformInfo.isWeb) {
|
||||
database = await createHistoryDb();
|
||||
}
|
||||
@@ -131,6 +131,8 @@ class ChatEventController {
|
||||
}
|
||||
|
||||
insertEvent(LocalEvent entry) {
|
||||
if (entry.channelId != channel?.id) return;
|
||||
|
||||
final idx = currentEvents.indexWhere((x) => x.data.uuid == entry.data.uuid);
|
||||
if (idx != -1) {
|
||||
currentEvents[idx] = entry;
|
||||
|
||||
@@ -7,7 +7,9 @@ import 'package:solian/models/post.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_editor.dart';
|
||||
import 'package:solian/widgets/posts/editor/post_editor_categories_tags.dart';
|
||||
import 'package:solian/widgets/posts/editor/post_editor_date.dart';
|
||||
import 'package:solian/widgets/posts/editor/post_editor_overview.dart';
|
||||
import 'package:solian/widgets/posts/editor/post_editor_publish_zone.dart';
|
||||
import 'package:solian/widgets/posts/editor/post_editor_visibility.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -25,6 +27,8 @@ class PostEditorController extends GetxController {
|
||||
Rx<Post?> replyTo = Rx(null);
|
||||
Rx<Post?> repostTo = Rx(null);
|
||||
Rx<Realm?> realmZone = Rx(null);
|
||||
Rx<DateTime?> publishedAt = Rx(null);
|
||||
Rx<DateTime?> publishedUntil = Rx(null);
|
||||
RxList<int> attachments = RxList<int>.empty(growable: true);
|
||||
RxList<String> tags = RxList<String>.empty(growable: true);
|
||||
|
||||
@@ -41,7 +45,6 @@ class PostEditorController extends GetxController {
|
||||
PostEditorController() {
|
||||
SharedPreferences.getInstance().then((inst) {
|
||||
_prefs = inst;
|
||||
localRead();
|
||||
_saveTimer = Timer.periodic(
|
||||
const Duration(seconds: 3),
|
||||
(Timer t) {
|
||||
@@ -88,16 +91,35 @@ class PostEditorController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> editPublishZone(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => PostEditorPublishZoneDialog(
|
||||
controller: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> editPublishDate(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => PostEditorDateDialog(
|
||||
controller: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> editAttachment(BuildContext context) {
|
||||
return showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => AttachmentEditorPopup(
|
||||
usage: 'i.attachment',
|
||||
current: attachments,
|
||||
onUpdate: (value) {
|
||||
attachments.value = value;
|
||||
attachments.refresh();
|
||||
initialAttachments: attachments,
|
||||
onAdd: (int value) {
|
||||
attachments.add(value);
|
||||
},
|
||||
onRemove: (int value) {
|
||||
attachments.remove(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -122,10 +144,12 @@ class PostEditorController extends GetxController {
|
||||
}
|
||||
|
||||
void localRead() {
|
||||
if (_prefs.containsKey('post_editor_local_save')) {
|
||||
isRestoreFromLocal.value = true;
|
||||
payload = jsonDecode(_prefs.getString('post_editor_local_save')!);
|
||||
}
|
||||
SharedPreferences.getInstance().then((inst) {
|
||||
if (inst.containsKey('post_editor_local_save')) {
|
||||
isRestoreFromLocal.value = true;
|
||||
payload = jsonDecode(inst.getString('post_editor_local_save')!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void localClear() {
|
||||
@@ -141,6 +165,8 @@ class PostEditorController extends GetxController {
|
||||
visibleUsers.clear();
|
||||
invisibleUsers.clear();
|
||||
visibility.value = 0;
|
||||
publishedAt.value = null;
|
||||
publishedUntil.value = null;
|
||||
isDraft.value = false;
|
||||
isRestoreFromLocal.value = false;
|
||||
lastSaveTime.value = null;
|
||||
@@ -163,6 +189,11 @@ class PostEditorController extends GetxController {
|
||||
titleController.text = value.body['title'] ?? '';
|
||||
descriptionController.text = value.body['description'] ?? '';
|
||||
contentController.text = value.body['content'] ?? '';
|
||||
publishedAt.value = value.publishedAt;
|
||||
publishedUntil.value = value.publishedUntil;
|
||||
tags.value =
|
||||
value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty();
|
||||
tags.refresh();
|
||||
attachments.value = value.body['attachments']?.cast<int>() ?? List.empty();
|
||||
attachments.refresh();
|
||||
|
||||
@@ -215,11 +246,14 @@ class PostEditorController extends GetxController {
|
||||
'title': title,
|
||||
'description': description,
|
||||
'content': contentController.text,
|
||||
'tags': tags,
|
||||
'tags': tags.map((x) => {'alias': x}).toList(),
|
||||
'attachments': attachments,
|
||||
'visible_users': visibleUsers,
|
||||
'invisible_users': invisibleUsers,
|
||||
'visibility': visibility.value,
|
||||
'published_at': publishedAt.value?.toUtc().toIso8601String() ??
|
||||
DateTime.now().toUtc().toIso8601String(),
|
||||
'published_until': publishedUntil.value?.toUtc().toIso8601String(),
|
||||
'is_draft': isDraft.value,
|
||||
if (replyTo.value != null) 'reply_to': replyTo.value!.id,
|
||||
if (repostTo.value != null) 'repost_to': repostTo.value!.id,
|
||||
@@ -229,6 +263,7 @@ class PostEditorController extends GetxController {
|
||||
|
||||
set payload(Map<String, dynamic> value) {
|
||||
type = value['type'];
|
||||
tags.value = value['tags'].map((x) => x['alias']).toList().cast<String>();
|
||||
titleController.text = value['title'] ?? '';
|
||||
descriptionController.text = value['description'] ?? '';
|
||||
contentController.text = value['content'] ?? '';
|
||||
@@ -242,6 +277,12 @@ class PostEditorController extends GetxController {
|
||||
if (value['invisible_users'] != null) {
|
||||
invisibleUsers.value = value['invisible_users'].cast<int>();
|
||||
}
|
||||
if (value['published_at'] != null) {
|
||||
publishedAt.value = DateTime.parse(value['published_at']).toLocal();
|
||||
}
|
||||
if (value['published_until'] != null) {
|
||||
publishedAt.value = DateTime.parse(value['published_until']).toLocal();
|
||||
}
|
||||
if (value['reply_to'] != null) {
|
||||
replyTo.value = Post.fromJson(value['reply_to']);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
extension SolianExtenions on BuildContext {
|
||||
void showSnackbar(String content) {
|
||||
void showSnackbar(String content, {SnackBarAction? action}) {
|
||||
ScaffoldMessenger.of(this).showSnackBar(SnackBar(
|
||||
content: Text(content),
|
||||
action: action,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -29,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) {
|
||||
var stack = StackTrace.current;
|
||||
var stackTrace = '$stack';
|
||||
|
||||
@@ -5,10 +5,13 @@ import 'package:get/get.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:protocol_handler/protocol_handler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:solian/bootstrapper.dart';
|
||||
import 'package:solian/firebase_options.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/attachment_uploader.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
@@ -71,33 +74,45 @@ Future<void> _initializePlatformComponents() async {
|
||||
}
|
||||
}
|
||||
|
||||
final themeSwitcher = ThemeSwitcher(
|
||||
lightThemeData: SolianTheme.build(Brightness.light),
|
||||
darkThemeData: SolianTheme.build(Brightness.dark),
|
||||
);
|
||||
|
||||
class SolianApp extends StatelessWidget {
|
||||
const SolianApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetMaterialApp.router(
|
||||
title: 'Solian',
|
||||
theme: SolianTheme.build(Brightness.light),
|
||||
darkTheme: SolianTheme.build(Brightness.dark),
|
||||
themeMode: ThemeMode.system,
|
||||
routerDelegate: AppRouter.instance.routerDelegate,
|
||||
routeInformationParser: AppRouter.instance.routeInformationParser,
|
||||
routeInformationProvider: AppRouter.instance.routeInformationProvider,
|
||||
backButtonDispatcher: AppRouter.instance.backButtonDispatcher,
|
||||
translations: SolianMessages(),
|
||||
locale: Get.deviceLocale,
|
||||
fallbackLocale: const Locale('en', 'US'),
|
||||
onInit: () => _initializeProviders(context),
|
||||
builder: (context, child) {
|
||||
return SystemShell(
|
||||
child: ScaffoldMessenger(
|
||||
child: BootstrapperShell(
|
||||
child: child ?? const SizedBox(),
|
||||
),
|
||||
),
|
||||
return ChangeNotifierProvider.value(
|
||||
value: themeSwitcher,
|
||||
child: Builder(builder: (context) {
|
||||
final theme = Provider.of<ThemeSwitcher>(context);
|
||||
|
||||
return GetMaterialApp.router(
|
||||
title: 'Solian',
|
||||
theme: theme.lightThemeData,
|
||||
darkTheme: theme.darkThemeData,
|
||||
themeMode: ThemeMode.system,
|
||||
routerDelegate: AppRouter.instance.routerDelegate,
|
||||
routeInformationParser: AppRouter.instance.routeInformationParser,
|
||||
routeInformationProvider: AppRouter.instance.routeInformationProvider,
|
||||
backButtonDispatcher: AppRouter.instance.backButtonDispatcher,
|
||||
translations: SolianMessages(),
|
||||
locale: Get.deviceLocale,
|
||||
fallbackLocale: const Locale('en', 'US'),
|
||||
onInit: () => _initializeProviders(context),
|
||||
builder: (context, child) {
|
||||
return SystemShell(
|
||||
child: ScaffoldMessenger(
|
||||
child: BootstrapperShell(
|
||||
child: child ?? const SizedBox(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,5 +126,6 @@ class SolianApp extends StatelessWidget {
|
||||
Get.lazyPut(() => ChannelProvider());
|
||||
Get.lazyPut(() => RealmProvider());
|
||||
Get.lazyPut(() => ChatCallProvider());
|
||||
Get.lazyPut(() => AttachmentUploaderController());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ class Call {
|
||||
String externalId;
|
||||
int founderId;
|
||||
int channelId;
|
||||
List<dynamic> participants;
|
||||
Channel channel;
|
||||
|
||||
Call({
|
||||
@@ -21,6 +22,7 @@ class Call {
|
||||
required this.externalId,
|
||||
required this.founderId,
|
||||
required this.channelId,
|
||||
required this.participants,
|
||||
required this.channel,
|
||||
});
|
||||
|
||||
@@ -34,6 +36,7 @@ class Call {
|
||||
externalId: json['external_id'],
|
||||
founderId: json['founder_id'],
|
||||
channelId: json['channel_id'],
|
||||
participants: json['participants'] ?? List.empty(),
|
||||
channel: Channel.fromJson(json['channel']),
|
||||
);
|
||||
|
||||
@@ -46,6 +49,7 @@ class Call {
|
||||
'external_id': externalId,
|
||||
'founder_id': founderId,
|
||||
'channel_id': channelId,
|
||||
'participants': participants,
|
||||
'channel': channel.toJson(),
|
||||
};
|
||||
}
|
||||
@@ -63,6 +67,7 @@ class ParticipantTrack {
|
||||
{required this.participant,
|
||||
required this.videoTrack,
|
||||
required this.isScreenShare});
|
||||
|
||||
VideoTrack? videoTrack;
|
||||
Participant participant;
|
||||
bool isScreenShare;
|
||||
|
||||
@@ -20,6 +20,7 @@ class Post {
|
||||
Post? repostTo;
|
||||
Realm? realm;
|
||||
DateTime? publishedAt;
|
||||
DateTime? publishedUntil;
|
||||
DateTime? pinnedAt;
|
||||
bool? isDraft;
|
||||
int authorId;
|
||||
@@ -44,6 +45,7 @@ class Post {
|
||||
required this.repostTo,
|
||||
required this.realm,
|
||||
required this.publishedAt,
|
||||
required this.publishedUntil,
|
||||
required this.pinnedAt,
|
||||
required this.isDraft,
|
||||
required this.authorId,
|
||||
@@ -80,6 +82,9 @@ class Post {
|
||||
publishedAt: json['published_at'] != null
|
||||
? DateTime.parse(json['published_at'])
|
||||
: null,
|
||||
publishedUntil: json['published_until'] != null
|
||||
? DateTime.parse(json['published_until'])
|
||||
: null,
|
||||
pinnedAt: json['pinned_at'] != null
|
||||
? DateTime.parse(json['pinned_at'])
|
||||
: null,
|
||||
@@ -108,6 +113,7 @@ class Post {
|
||||
'repost_to': repostTo?.toJson(),
|
||||
'realm': realm?.toJson(),
|
||||
'published_at': publishedAt?.toIso8601String(),
|
||||
'published_until': publishedUntil?.toIso8601String(),
|
||||
'pinned_at': pinnedAt?.toIso8601String(),
|
||||
'is_draft': isDraft,
|
||||
'author_id': authorId,
|
||||
|
||||
195
lib/providers/attachment_uploader.dart
Normal file
195
lib/providers/attachment_uploader.dart
Normal file
@@ -0,0 +1,195 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
|
||||
class AttachmentUploadTask {
|
||||
File file;
|
||||
String usage;
|
||||
Map<String, dynamic>? metadata;
|
||||
|
||||
double progress = 0;
|
||||
bool isUploading = false;
|
||||
bool isCompleted = false;
|
||||
|
||||
AttachmentUploadTask({
|
||||
required this.file,
|
||||
required this.usage,
|
||||
this.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
class AttachmentUploaderController extends GetxController {
|
||||
RxBool isUploading = false.obs;
|
||||
RxDouble progressOfUpload = 0.0.obs;
|
||||
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) {
|
||||
if (isUploading.value) throw Exception('uploading blocked');
|
||||
queueOfUpload.add(task);
|
||||
}
|
||||
|
||||
void enqueueTaskBatch(Iterable<AttachmentUploadTask> tasks) {
|
||||
if (isUploading.value) throw Exception('uploading blocked');
|
||||
queueOfUpload.addAll(tasks);
|
||||
}
|
||||
|
||||
void dequeueTask(AttachmentUploadTask task) {
|
||||
if (isUploading.value) throw Exception('uploading blocked');
|
||||
queueOfUpload.remove(task);
|
||||
}
|
||||
|
||||
Future<Attachment> performSingleTask(int queueIndex) async {
|
||||
isUploading.value = true;
|
||||
progressOfUpload.value = 0;
|
||||
|
||||
_startProgressSyncTimer();
|
||||
queueOfUpload[queueIndex].isUploading = true;
|
||||
|
||||
final task = queueOfUpload[queueIndex];
|
||||
final result = await _rawUploadAttachment(
|
||||
await task.file.readAsBytes(),
|
||||
task.file.path,
|
||||
task.usage,
|
||||
null,
|
||||
onProgress: (value) {
|
||||
queueOfUpload[queueIndex].progress = value;
|
||||
_progressOfUpload = value;
|
||||
},
|
||||
);
|
||||
|
||||
queueOfUpload.removeAt(queueIndex);
|
||||
_stopProgressSyncTimer();
|
||||
_syncProgress();
|
||||
|
||||
isUploading.value = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> performUploadQueue({
|
||||
required Function(Attachment item) onData,
|
||||
}) async {
|
||||
isUploading.value = true;
|
||||
progressOfUpload.value = 0;
|
||||
|
||||
_startProgressSyncTimer();
|
||||
|
||||
for (var idx = 0; idx < queueOfUpload.length; idx++) {
|
||||
queueOfUpload[idx].isUploading = true;
|
||||
|
||||
final task = queueOfUpload[idx];
|
||||
final result = await _rawUploadAttachment(
|
||||
await task.file.readAsBytes(),
|
||||
task.file.path,
|
||||
task.usage,
|
||||
null,
|
||||
onProgress: (value) {
|
||||
queueOfUpload[idx].progress = value;
|
||||
_progressOfUpload = (idx + value) / queueOfUpload.length;
|
||||
},
|
||||
);
|
||||
_progressOfUpload = (idx + 1) / queueOfUpload.length;
|
||||
onData(result);
|
||||
|
||||
queueOfUpload[idx].isUploading = false;
|
||||
queueOfUpload[idx].isCompleted = false;
|
||||
}
|
||||
|
||||
queueOfUpload.clear();
|
||||
_stopProgressSyncTimer();
|
||||
_syncProgress();
|
||||
|
||||
isUploading.value = false;
|
||||
}
|
||||
|
||||
Future<void> uploadAttachmentWithCallback(
|
||||
Uint8List data,
|
||||
String path,
|
||||
String usage,
|
||||
Map<String, dynamic>? metadata,
|
||||
Function(Attachment) callback,
|
||||
) async {
|
||||
if (isUploading.value) throw Exception('uploading blocked');
|
||||
|
||||
isUploading.value = true;
|
||||
final result = await _rawUploadAttachment(
|
||||
data,
|
||||
path,
|
||||
usage,
|
||||
metadata,
|
||||
onProgress: (progress) {
|
||||
progressOfUpload.value = progress;
|
||||
},
|
||||
);
|
||||
isUploading.value = false;
|
||||
callback(result);
|
||||
}
|
||||
|
||||
Future<Attachment> uploadAttachment(
|
||||
Uint8List data,
|
||||
String path,
|
||||
String usage,
|
||||
Map<String, dynamic>? metadata,
|
||||
) async {
|
||||
if (isUploading.value) throw Exception('uploading blocked');
|
||||
|
||||
isUploading.value = true;
|
||||
final result = await _rawUploadAttachment(
|
||||
data,
|
||||
path,
|
||||
usage,
|
||||
metadata,
|
||||
onProgress: (progress) {
|
||||
progressOfUpload.value = progress;
|
||||
},
|
||||
);
|
||||
isUploading.value = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Attachment> _rawUploadAttachment(
|
||||
Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
|
||||
{Function(double)? onProgress}) async {
|
||||
final AttachmentProvider provider = Get.find();
|
||||
try {
|
||||
final result = await provider.createAttachment(
|
||||
data,
|
||||
path,
|
||||
usage,
|
||||
metadata,
|
||||
onProgress: onProgress,
|
||||
);
|
||||
return result;
|
||||
} catch (err) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ class ChatCallProvider extends GetxController {
|
||||
|
||||
RxBool isReady = false.obs;
|
||||
RxBool isMounted = false.obs;
|
||||
RxBool isInitialized = false.obs;
|
||||
|
||||
String? token;
|
||||
String? endpoint;
|
||||
@@ -151,6 +152,8 @@ class ChatCallProvider extends GetxController {
|
||||
void onRoomDidUpdate() => sortParticipants();
|
||||
|
||||
void setupRoom() {
|
||||
if(isInitialized.value) return;
|
||||
|
||||
sortParticipants();
|
||||
room.addListener(onRoomDidUpdate);
|
||||
WidgetsBindingCompatible.instance?.addPostFrameCallback(
|
||||
@@ -160,6 +163,8 @@ class ChatCallProvider extends GetxController {
|
||||
if (lkPlatformIsMobile()) {
|
||||
Hardware.instance.setSpeakerphoneOn(true);
|
||||
}
|
||||
|
||||
isInitialized.value = true;
|
||||
}
|
||||
|
||||
void setupRoomListeners({
|
||||
@@ -362,6 +367,7 @@ class ChatCallProvider extends GetxController {
|
||||
|
||||
void disposeRoom() {
|
||||
isMounted.value = false;
|
||||
isInitialized.value = false;
|
||||
current.value = null;
|
||||
channel.value = null;
|
||||
room.removeListener(onRoomDidUpdate);
|
||||
|
||||
@@ -4,8 +4,10 @@ import 'dart:typed_data';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:dio/dio.dart' as dio;
|
||||
|
||||
class AttachmentProvider extends GetConnect {
|
||||
static Map<String, String> mimetypeOverrides = {
|
||||
@@ -20,6 +22,48 @@ class AttachmentProvider extends GetConnect {
|
||||
|
||||
final Map<int, Attachment> _cachedResponses = {};
|
||||
|
||||
Future<List<Attachment?>> listMetadata(
|
||||
List<int> id, {
|
||||
noCache = false,
|
||||
}) async {
|
||||
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 {
|
||||
if (!noCache && _cachedResponses.containsKey(id)) {
|
||||
return _cachedResponses[id]!;
|
||||
@@ -37,21 +81,14 @@ class AttachmentProvider extends GetConnect {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Response> createAttachment(
|
||||
Uint8List data,
|
||||
String path,
|
||||
String usage,
|
||||
Map<String, dynamic>? metadata,
|
||||
) async {
|
||||
Future<Attachment> createAttachment(
|
||||
Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
|
||||
{Function(double)? onProgress}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
|
||||
final client = auth.configureClient(
|
||||
'files',
|
||||
timeout: const Duration(minutes: 3),
|
||||
);
|
||||
|
||||
final filePayload = MultipartFile(data, filename: basename(path));
|
||||
final filePayload =
|
||||
dio.MultipartFile.fromBytes(data, filename: basename(path));
|
||||
final fileAlt = basename(path).contains('.')
|
||||
? basename(path).substring(0, basename(path).lastIndexOf('.'))
|
||||
: basename(path);
|
||||
@@ -64,19 +101,30 @@ class AttachmentProvider extends GetConnect {
|
||||
if (mimetypeOverrides.keys.contains(fileExt)) {
|
||||
mimetypeOverride = mimetypeOverrides[fileExt];
|
||||
}
|
||||
final payload = FormData({
|
||||
final payload = dio.FormData.fromMap({
|
||||
'alt': fileAlt,
|
||||
'file': filePayload,
|
||||
'usage': usage,
|
||||
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
|
||||
'metadata': jsonEncode(metadata),
|
||||
});
|
||||
final resp = await client.post('/attachments', payload);
|
||||
final resp = await dio.Dio(
|
||||
dio.BaseOptions(
|
||||
baseUrl: ServiceFinder.buildUrl('files', null),
|
||||
headers: {'Authorization': 'Bearer ${auth.credentials!.accessToken}'},
|
||||
),
|
||||
).post(
|
||||
'/attachments',
|
||||
data: payload,
|
||||
onSendProgress: (count, total) {
|
||||
if (onProgress != null) onProgress(count / total);
|
||||
},
|
||||
);
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw Exception(resp.data);
|
||||
}
|
||||
|
||||
return resp;
|
||||
return Attachment.fromJson(resp.data);
|
||||
}
|
||||
|
||||
Future<Response> updateAttachment(
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
|
||||
class RealmProvider extends GetxController {
|
||||
RxBool isLoading = false.obs;
|
||||
RxList<Realm> availableRealms = RxList.empty(growable: true);
|
||||
|
||||
Future<void> refreshAvailableRealms() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
|
||||
isLoading.value = true;
|
||||
final resp = await listAvailableRealm();
|
||||
isLoading.value = false;
|
||||
|
||||
availableRealms.value =
|
||||
resp.body.map((x) => Realm.fromJson(x)).toList().cast<Realm>();
|
||||
availableRealms.refresh();
|
||||
}
|
||||
|
||||
Future<Response> getRealm(String alias) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
|
||||
@@ -4,15 +4,19 @@ import 'package:solian/models/relations.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
|
||||
class RelationshipProvider extends GetxController {
|
||||
final RxInt friendRequestCount = 0.obs;
|
||||
|
||||
final RxList<Relationship> _friends = RxList.empty(growable: true);
|
||||
|
||||
Future<void> refreshFriendList() async {
|
||||
final resp = await listRelationWithStatus(1);
|
||||
_friends.value = resp.body
|
||||
Future<void> refreshRelativeList() async {
|
||||
final resp = await listRelation();
|
||||
final List<Relationship> result = resp.body
|
||||
.map((e) => Relationship.fromJson(e))
|
||||
.toList()
|
||||
.cast<Relationship>();
|
||||
_friends.value = result.where((x) => x.status == 1).toList();
|
||||
_friends.refresh();
|
||||
friendRequestCount.value = result.where((x) => x.status == 0).length;
|
||||
}
|
||||
|
||||
bool hasFriend(Account account) {
|
||||
|
||||
30
lib/providers/theme_switcher.dart
Normal file
30
lib/providers/theme_switcher.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
|
||||
class ThemeSwitcher extends ChangeNotifier {
|
||||
ThemeData lightThemeData;
|
||||
ThemeData darkThemeData;
|
||||
|
||||
ThemeSwitcher({
|
||||
required this.lightThemeData,
|
||||
required this.darkThemeData,
|
||||
});
|
||||
|
||||
Future<void> restoreTheme() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.containsKey('global_theme_color')) {
|
||||
final value = prefs.getInt('global_theme_color')!;
|
||||
final color = Color(value);
|
||||
lightThemeData = SolianTheme.build(Brightness.light, seedColor: color);
|
||||
darkThemeData = SolianTheme.build(Brightness.dark, seedColor: color);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void setTheme(ThemeData light, dark) {
|
||||
lightThemeData = light;
|
||||
darkThemeData = dark;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
import 'package:solian/screens/about.dart';
|
||||
@@ -18,6 +20,7 @@ import 'package:solian/screens/realms/realm_organize.dart';
|
||||
import 'package:solian/screens/realms/realm_view.dart';
|
||||
import 'package:solian/screens/home.dart';
|
||||
import 'package:solian/screens/posts/post_editor.dart';
|
||||
import 'package:solian/screens/settings.dart';
|
||||
import 'package:solian/shells/root_shell.dart';
|
||||
import 'package:solian/shells/title_shell.dart';
|
||||
|
||||
@@ -34,6 +37,22 @@ abstract class AppRouter {
|
||||
_chatRoute,
|
||||
_realmRoute,
|
||||
_accountRoute,
|
||||
GoRoute(
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: const AboutScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: const SettingScreen(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -76,14 +95,26 @@ abstract class AppRouter {
|
||||
GoRoute(
|
||||
path: '/posts/editor',
|
||||
name: 'postEditor',
|
||||
builder: (context, state) {
|
||||
pageBuilder: (context, state) {
|
||||
final arguments = state.extra as PostPublishArguments?;
|
||||
return PostPublishScreen(
|
||||
edit: arguments?.edit,
|
||||
reply: arguments?.reply,
|
||||
repost: arguments?.repost,
|
||||
realm: arguments?.realm,
|
||||
mode: int.tryParse(state.uri.queryParameters['mode'] ?? '0') ?? 0,
|
||||
return CustomTransitionPage(
|
||||
child: PostPublishScreen(
|
||||
edit: arguments?.edit,
|
||||
reply: arguments?.reply,
|
||||
repost: arguments?.repost,
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -210,14 +241,6 @@ abstract class AppRouter {
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: const AboutScreen(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
@@ -17,7 +18,9 @@ class AboutScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||
Image.asset('assets/logo.png', width: 64, height: 64)
|
||||
.animate(onPlay: (c) => c.repeat())
|
||||
.rotate(duration: 1000.ms),
|
||||
Text(
|
||||
'Solian',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
@@ -44,17 +47,29 @@ class AboutScreen extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: const Text('More Information'),
|
||||
onPressed: () {
|
||||
launchUrlString('https://solsynth.dev/products/solar-network');
|
||||
child: const Text('App Details'),
|
||||
onPressed: () async {
|
||||
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: Image.asset(
|
||||
'assets/logo.png',
|
||||
width: 56,
|
||||
height: 56,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: const Text('Project Website'),
|
||||
onPressed: () {
|
||||
launchUrlString('https://solsynth.dev');
|
||||
launchUrlString('https://solsynth.dev/products/solar-network');
|
||||
},
|
||||
child: const Text('Official Website'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
|
||||
@@ -3,11 +3,13 @@ import 'package:get/get.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/account_status.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/auth/signin.dart';
|
||||
import 'package:solian/screens/auth/signup.dart';
|
||||
import 'package:solian/widgets/account/account_heading.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
|
||||
class AccountScreen extends StatefulWidget {
|
||||
const AccountScreen({super.key});
|
||||
@@ -23,10 +25,27 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
(
|
||||
const Icon(Icons.color_lens),
|
||||
'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.diversity_1), 'accountFriend'.tr, 'accountFriend'),
|
||||
(const Icon(Icons.info_outline), 'about'.tr, 'about'),
|
||||
];
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
@@ -41,7 +60,10 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ActionCard(
|
||||
icon: const Icon(Icons.login, color: Colors.white),
|
||||
icon: Icon(
|
||||
Icons.login,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
title: 'signin'.tr,
|
||||
caption: 'signinCaption'.tr,
|
||||
onTap: () {
|
||||
@@ -59,7 +81,10 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
},
|
||||
),
|
||||
ActionCard(
|
||||
icon: const Icon(Icons.add, color: Colors.white),
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
title: 'signup'.tr,
|
||||
caption: 'signupCaption'.tr,
|
||||
onTap: () {
|
||||
@@ -184,7 +209,7 @@ class ActionCard extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: Colors.indigo,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: icon,
|
||||
).paddingOnly(bottom: 12),
|
||||
Text(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/relations.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/relative_list.dart';
|
||||
|
||||
class FriendScreen extends StatefulWidget {
|
||||
@@ -21,15 +21,15 @@ class _FriendScreenState extends State<FriendScreen>
|
||||
|
||||
List<Relationship> _relations = List.empty();
|
||||
|
||||
List<Relationship> filterByStatus(int status) {
|
||||
List<Relationship> _filterByStatus(int status) {
|
||||
return _relations.where((x) => x.status == status).toList();
|
||||
}
|
||||
|
||||
Future<void> loadRelations() async {
|
||||
Future<void> _loadRelations() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final RelationshipProvider provider = Get.find();
|
||||
final resp = await provider.listRelation();
|
||||
final RelationshipProvider relations = Get.find();
|
||||
final resp = await relations.listRelation();
|
||||
|
||||
setState(() {
|
||||
_relations = resp.body
|
||||
@@ -38,6 +38,9 @@ class _FriendScreenState extends State<FriendScreen>
|
||||
.cast<Relationship>();
|
||||
_isBusy = false;
|
||||
});
|
||||
|
||||
relations.friendRequestCount.value =
|
||||
_relations.where((x) => x.status == 0).length;
|
||||
}
|
||||
|
||||
void promptAddFriend() async {
|
||||
@@ -104,8 +107,8 @@ class _FriendScreenState extends State<FriendScreen>
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
|
||||
loadRelations().then((_) {
|
||||
if (filterByStatus(0).isEmpty) {
|
||||
_loadRelations().then((_) {
|
||||
if (_filterByStatus(0).isEmpty) {
|
||||
_tabController.animateTo(1);
|
||||
}
|
||||
});
|
||||
@@ -119,6 +122,19 @@ class _FriendScreenState extends State<FriendScreen>
|
||||
appBar: AppBar(
|
||||
centerTitle: false,
|
||||
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(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
@@ -136,46 +152,34 @@ class _FriendScreenState extends State<FriendScreen>
|
||||
controller: _tabController,
|
||||
children: [
|
||||
RefreshIndicator(
|
||||
onRefresh: () => loadRelations(),
|
||||
onRefresh: () => _loadRelations(),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (_isBusy)
|
||||
SliverToBoxAdapter(
|
||||
child: const LinearProgressIndicator().animate().scaleX(),
|
||||
),
|
||||
SilverRelativeList(
|
||||
items: filterByStatus(0),
|
||||
onUpdate: () => loadRelations(),
|
||||
items: _filterByStatus(0),
|
||||
onUpdate: () => _loadRelations(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: () => loadRelations(),
|
||||
onRefresh: () => _loadRelations(),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (_isBusy)
|
||||
SliverToBoxAdapter(
|
||||
child: const LinearProgressIndicator().animate().scaleX(),
|
||||
),
|
||||
SilverRelativeList(
|
||||
items: filterByStatus(1),
|
||||
onUpdate: () => loadRelations(),
|
||||
items: _filterByStatus(1),
|
||||
onUpdate: () => _loadRelations(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: () => loadRelations(),
|
||||
onRefresh: () => _loadRelations(),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (_isBusy)
|
||||
SliverToBoxAdapter(
|
||||
child: const LinearProgressIndicator().animate().scaleX(),
|
||||
),
|
||||
SilverRelativeList(
|
||||
items: filterByStatus(3),
|
||||
onUpdate: () => loadRelations(),
|
||||
items: _filterByStatus(3),
|
||||
onUpdate: () => _loadRelations(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,9 +3,11 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/services.dart';
|
||||
@@ -34,7 +36,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
void selectBirthday() async {
|
||||
void _selectBirthday() async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _birthday?.toLocal(),
|
||||
@@ -49,48 +51,67 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void syncWidget() async {
|
||||
setState(() => _isBusy = true);
|
||||
void _syncWidget() async {
|
||||
_isBusy = true;
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
final prof = auth.userProfile.value!;
|
||||
setState(() {
|
||||
_usernameController.text = prof['name'];
|
||||
_nicknameController.text = prof['nick'];
|
||||
_descriptionController.text = prof['description'];
|
||||
_firstNameController.text = prof['profile']['first_name'];
|
||||
_lastNameController.text = prof['profile']['last_name'];
|
||||
_avatar = prof['avatar'];
|
||||
_banner = prof['banner'];
|
||||
if (prof['profile']['birthday'] != null) {
|
||||
_birthday = DateTime.parse(prof['profile']['birthday']);
|
||||
_birthdayController.text =
|
||||
DateFormat('yyyy-MM-dd').format(_birthday!.toLocal());
|
||||
}
|
||||
_usernameController.text = prof['name'];
|
||||
_nicknameController.text = prof['nick'];
|
||||
_descriptionController.text = prof['description'];
|
||||
_firstNameController.text = prof['profile']['first_name'];
|
||||
_lastNameController.text = prof['profile']['last_name'];
|
||||
_avatar = prof['avatar'];
|
||||
_banner = prof['banner'];
|
||||
if (prof['profile']['birthday'] != null) {
|
||||
_birthday = DateTime.parse(prof['profile']['birthday']);
|
||||
_birthdayController.text =
|
||||
DateFormat('yyyy-MM-dd').format(_birthday!.toLocal());
|
||||
}
|
||||
|
||||
_isBusy = false;
|
||||
});
|
||||
_isBusy = false;
|
||||
}
|
||||
|
||||
Future<void> updateImage(String position) async {
|
||||
Future<void> _editImage(String position) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
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: [CropAspectRatioPreset.square],
|
||||
),
|
||||
IOSUiSettings(
|
||||
title: 'cropImage'.tr,
|
||||
aspectRatioPresets: [CropAspectRatioPreset.square],
|
||||
),
|
||||
WebUiSettings(
|
||||
context: context,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (croppedFile == null) return;
|
||||
final file = File(croppedFile.path);
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final AttachmentProvider provider = Get.find();
|
||||
|
||||
Response? attachResp;
|
||||
Attachment? attachResult;
|
||||
try {
|
||||
final file = File(image.path);
|
||||
attachResp = await provider.createAttachment(
|
||||
attachResult = await provider.createAttachment(
|
||||
await file.readAsBytes(),
|
||||
file.path,
|
||||
'p.$position',
|
||||
null
|
||||
null,
|
||||
);
|
||||
} catch (e) {
|
||||
setState(() => _isBusy = false);
|
||||
@@ -102,10 +123,10 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
|
||||
final resp = await client.put(
|
||||
'/users/me/$position',
|
||||
{'attachment': attachResp.body['id']},
|
||||
{'attachment': attachResult.id},
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
syncWidget();
|
||||
_syncWidget();
|
||||
context.showSnackbar('accountPersonalizeApplied'.tr);
|
||||
} else {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
@@ -114,7 +135,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
void updatePersonalize() async {
|
||||
void _editUserInfo() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
@@ -134,7 +155,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
},
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
syncWidget();
|
||||
_syncWidget();
|
||||
context.showSnackbar('accountPersonalizeApplied'.tr);
|
||||
} else {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
@@ -146,8 +167,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
Future.delayed(Duration.zero, () => syncWidget());
|
||||
_syncWidget();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -168,7 +188,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
left: 40,
|
||||
child: FloatingActionButton.small(
|
||||
heroTag: const Key('avatar-editor'),
|
||||
onPressed: () => updateImage('avatar'),
|
||||
onPressed: () => _editImage('avatar'),
|
||||
child: const Icon(
|
||||
Icons.camera,
|
||||
),
|
||||
@@ -187,7 +207,8 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? Image.network(
|
||||
ServiceFinder.buildUrl('files', '/attachments/$_banner'),
|
||||
ServiceFinder.buildUrl(
|
||||
'files', '/attachments/$_banner'),
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (BuildContext context, Widget child,
|
||||
ImageChunkEvent? loadingProgress) {
|
||||
@@ -212,7 +233,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
right: 16,
|
||||
child: FloatingActionButton(
|
||||
heroTag: const Key('banner-editor'),
|
||||
onPressed: () => updateImage('banner'),
|
||||
onPressed: () => _editImage('banner'),
|
||||
child: const Icon(
|
||||
Icons.camera_alt,
|
||||
),
|
||||
@@ -293,18 +314,18 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'birthday'.tr,
|
||||
),
|
||||
onTap: () => selectBirthday(),
|
||||
onTap: () => _selectBirthday(),
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => syncWidget(),
|
||||
onPressed: _isBusy ? null : () => _syncWidget(),
|
||||
child: Text('reset'.tr),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _isBusy ? null : () => updatePersonalize(),
|
||||
onPressed: _isBusy ? null : () => _editUserInfo(),
|
||||
child: Text('apply'.tr),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -13,8 +13,8 @@ import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||
import 'package:solian/widgets/feed/feed_list.dart';
|
||||
import 'package:solian/widgets/posts/post_list.dart';
|
||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
|
||||
class AccountProfilePage extends StatefulWidget {
|
||||
@@ -292,7 +292,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
if (_userinfo != null)
|
||||
FeedListWidget(
|
||||
PostWarpedListWidget(
|
||||
isPinned: false,
|
||||
controller: _postController.pagingController,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/providers/call.dart';
|
||||
@@ -16,11 +17,26 @@ class CallScreen extends StatefulWidget {
|
||||
State<CallScreen> createState() => _CallScreenState();
|
||||
}
|
||||
|
||||
class _CallScreenState extends State<CallScreen> {
|
||||
Timer? timer;
|
||||
String currentDuration = '00:00:00';
|
||||
class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
Timer? _timer;
|
||||
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();
|
||||
if (provider.current.value == null) return '00:00:00';
|
||||
Duration duration =
|
||||
@@ -34,9 +50,142 @@ class _CallScreenState extends State<CallScreen> {
|
||||
return formattedTime;
|
||||
}
|
||||
|
||||
void updateDuration() {
|
||||
void _updateDuration() {
|
||||
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();
|
||||
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
|
||||
@@ -68,80 +228,71 @@ class _CallScreenState extends State<CallScreen> {
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: currentDuration,
|
||||
text: _currentDuration,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Obx(
|
||||
() => Stack(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Column(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
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,
|
||||
SizeTransition(
|
||||
sizeFactor: _controlsAnimation,
|
||||
axis: Axis.vertical,
|
||||
child: SizedBox(
|
||||
height: 128,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: math.max(0, provider.participantTracks.length),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final track = provider.participantTracks[index];
|
||||
if (track.participant.sid ==
|
||||
provider.focusTrack.value?.participant.sid) {
|
||||
return Container();
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: 64,
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(child: SizedBox()),
|
||||
IconButton(
|
||||
icon: _layoutMode == 0
|
||||
? const Icon(Icons.view_list)
|
||||
: 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
|
||||
void deactivate() {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
void activate() {
|
||||
timer ??= Timer.periodic(
|
||||
_timer ??= Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(_) => updateDuration(),
|
||||
(_) => _updateDuration(),
|
||||
);
|
||||
super.activate();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/event.dart';
|
||||
import 'package:solian/models/packet.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/call.dart';
|
||||
import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/router.dart';
|
||||
@@ -19,7 +18,7 @@ import 'package:solian/screens/channel/channel_detail.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.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/chat_event.dart';
|
||||
import 'package:solian/widgets/chat/chat_event_list.dart';
|
||||
@@ -53,7 +52,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
|
||||
late final ChatEventController _chatController;
|
||||
|
||||
getChannel({String? alias}) async {
|
||||
_getChannel({String? alias}) async {
|
||||
final ChannelProvider provider = Get.find();
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
@@ -80,7 +79,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
getOngoingCall() async {
|
||||
_getOngoingCall() async {
|
||||
final ChannelProvider provider = Get.find();
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
@@ -100,7 +99,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
void listenMessages() {
|
||||
void _listenMessages() {
|
||||
final WebSocketProvider provider = Get.find();
|
||||
_subscription = provider.stream.stream.listen((event) {
|
||||
switch (event.method) {
|
||||
@@ -110,26 +109,20 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
break;
|
||||
case 'calls.new':
|
||||
final payload = Call.fromJson(event.payload!);
|
||||
setState(() => _ongoingCall = payload);
|
||||
if (payload.channel.id == _channel!.id) {
|
||||
setState(() => _ongoingCall = payload);
|
||||
}
|
||||
break;
|
||||
case 'calls.end':
|
||||
setState(() => _ongoingCall = null);
|
||||
final payload = Call.fromJson(event.payload!);
|
||||
if (payload.channel.id == _channel!.id) {
|
||||
setState(() => _ongoingCall = null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void showCallPrejoin() {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => ChatCallPrejoinPopup(
|
||||
ongoingCall: _ongoingCall!,
|
||||
channel: _channel!,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Event? _messageToReplying;
|
||||
Event? _messageToEditing;
|
||||
|
||||
@@ -149,13 +142,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
_chatController = ChatEventController();
|
||||
_chatController.initialize();
|
||||
|
||||
getChannel().then((_) {
|
||||
_getOngoingCall();
|
||||
_getChannel().then((_) {
|
||||
_chatController.getEvents(_channel!, widget.realm);
|
||||
listenMessages();
|
||||
_listenMessages();
|
||||
});
|
||||
|
||||
getOngoingCall();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -183,8 +175,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
final ChatCallProvider call = Get.find();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
@@ -219,7 +209,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
if (value == false) AppRouter.instance.pop();
|
||||
if (value != null) {
|
||||
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(
|
||||
children: [
|
||||
if (_ongoingCall != null)
|
||||
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: 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),
|
||||
);
|
||||
}
|
||||
})
|
||||
],
|
||||
ChannelCallIndicator(
|
||||
channel: _channel!,
|
||||
ongoingCall: _ongoingCall!,
|
||||
),
|
||||
Expanded(
|
||||
child: ChatEventList(
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/widgets/feed/feed_list.dart';
|
||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
||||
|
||||
import '../../models/post.dart';
|
||||
|
||||
@@ -77,7 +77,7 @@ class _FeedSearchScreenState extends State<FeedSearchScreen> {
|
||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
FeedListWidget(controller: _pagingController),
|
||||
PostWarpedListWidget(controller: _pagingController),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,12 +4,13 @@ import 'package:solian/controllers/post_list_controller.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/account/notification.dart';
|
||||
import 'package:solian/screens/posts/post_editor.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/app_bar_title.dart';
|
||||
import 'package:solian/widgets/current_state_action.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/feed/feed_list.dart';
|
||||
import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
|
||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
@@ -51,7 +52,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const PostCreatePopup(),
|
||||
builder: (context) => PostCreatePopup(
|
||||
controller: _postController,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -95,7 +98,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
RefreshIndicator(
|
||||
onRefresh: () => _postController.reloadAllOver(),
|
||||
child: CustomScrollView(slivers: [
|
||||
FeedListWidget(
|
||||
PostWarpedListWidget(
|
||||
controller: _postController.pagingController,
|
||||
),
|
||||
]),
|
||||
@@ -118,12 +121,12 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
class PostCreatePopup extends StatelessWidget {
|
||||
final bool hideDraftBox;
|
||||
final Function? onCreated;
|
||||
final PostListController controller;
|
||||
|
||||
const PostCreatePopup({
|
||||
super.key,
|
||||
this.hideDraftBox = false,
|
||||
this.onCreated,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -140,11 +143,13 @@ class PostCreatePopup extends StatelessWidget {
|
||||
label: 'postEditorModeStory'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
AppRouter.instance.pushNamed('postEditor', queryParameters: {
|
||||
'mode': 0.toString(),
|
||||
}).then((val) {
|
||||
if (val != null && onCreated != null) onCreated!();
|
||||
});
|
||||
AppRouter.instance.pushNamed(
|
||||
'postEditor',
|
||||
extra: PostPublishArguments(postListController: controller),
|
||||
queryParameters: {
|
||||
'mode': 0.toString(),
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
(
|
||||
@@ -152,11 +157,13 @@ class PostCreatePopup extends StatelessWidget {
|
||||
label: 'postEditorModeArticle'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
AppRouter.instance.pushNamed('postEditor', queryParameters: {
|
||||
'mode': 1.toString(),
|
||||
}).then((val) {
|
||||
if (val != null && onCreated != null) onCreated!();
|
||||
});
|
||||
AppRouter.instance.pushNamed(
|
||||
'postEditor',
|
||||
extra: PostPublishArguments(postListController: controller),
|
||||
queryParameters: {
|
||||
'mode': 1.toString(),
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
(
|
||||
|
||||
@@ -3,14 +3,18 @@ import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/post.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_replies.dart';
|
||||
|
||||
class PostDetailScreen extends StatefulWidget {
|
||||
final String id;
|
||||
final Post? post;
|
||||
|
||||
const PostDetailScreen({super.key, required this.id});
|
||||
const PostDetailScreen({
|
||||
super.key,
|
||||
required this.id,
|
||||
this.post,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
||||
@@ -20,6 +24,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
Post? item;
|
||||
|
||||
Future<Post?> getDetail() async {
|
||||
if (widget.post != null) {
|
||||
item = widget.post;
|
||||
return widget.post;
|
||||
}
|
||||
|
||||
final PostProvider provider = Get.find();
|
||||
|
||||
try {
|
||||
@@ -48,14 +57,12 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: CenteredContainer(
|
||||
child: PostItem(
|
||||
item: item!,
|
||||
isClickable: true,
|
||||
isFullDate: true,
|
||||
isShowReply: false,
|
||||
isContentSelectable: true,
|
||||
),
|
||||
child: PostItem(
|
||||
item: item!,
|
||||
isClickable: true,
|
||||
isFullDate: true,
|
||||
isShowReply: false,
|
||||
isContentSelectable: true,
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
@@ -63,14 +70,12 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
.paddingOnly(top: 4),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: CenteredContainer(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'postReplies'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 16),
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'postReplies'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 16),
|
||||
),
|
||||
),
|
||||
PostReplyList(item: item!),
|
||||
|
||||
@@ -3,10 +3,13 @@ import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:markdown_toolbar/markdown_toolbar.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/models/post.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
import 'package:solian/providers/attachment_uploader.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
@@ -20,8 +23,15 @@ class PostPublishArguments {
|
||||
final Post? reply;
|
||||
final Post? repost;
|
||||
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 {
|
||||
@@ -29,6 +39,7 @@ class PostPublishScreen extends StatefulWidget {
|
||||
final Post? reply;
|
||||
final Post? repost;
|
||||
final Realm? realm;
|
||||
final PostListController? postListController;
|
||||
final int mode;
|
||||
|
||||
const PostPublishScreen({
|
||||
@@ -37,6 +48,7 @@ class PostPublishScreen extends StatefulWidget {
|
||||
this.reply,
|
||||
this.repost,
|
||||
this.realm,
|
||||
this.postListController,
|
||||
required this.mode,
|
||||
});
|
||||
|
||||
@@ -46,6 +58,7 @@ class PostPublishScreen extends StatefulWidget {
|
||||
|
||||
class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
final _editorController = PostEditorController();
|
||||
final _contentFocusNode = FocusNode();
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
@@ -54,6 +67,14 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
if (auth.isAuthorized.isFalse) 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);
|
||||
|
||||
final client = auth.configureClient('interactive');
|
||||
@@ -74,34 +95,49 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
} else {
|
||||
_editorController.localClear();
|
||||
if (widget.postListController != null) {
|
||||
widget.postListController!.reloadAllOver();
|
||||
}
|
||||
AppRouter.instance.pop(resp.body);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
void syncWidget() {
|
||||
void _syncWidget() {
|
||||
_editorController.mode.value = widget.mode;
|
||||
if (widget.edit != null) {
|
||||
_editorController.editTarget = widget.edit;
|
||||
}
|
||||
if (widget.realm != null) {
|
||||
_editorController.realmZone.value = widget.realm;
|
||||
}
|
||||
}
|
||||
|
||||
void cancelAction() {
|
||||
void _cancelAction() {
|
||||
_editorController.localClear();
|
||||
AppRouter.instance.pop();
|
||||
}
|
||||
|
||||
Post? get _editTo => _editorController.editTo.value;
|
||||
|
||||
Post? get _replyTo => _editorController.replyTo.value;
|
||||
|
||||
Post? get _repostTo => _editorController.repostTo.value;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
syncWidget();
|
||||
if (widget.edit == null) _editorController.localRead();
|
||||
_editorController.contentController.addListener(() => setState(() {}));
|
||||
_syncWidget();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final notifyBannerActions = [
|
||||
TextButton(
|
||||
onPressed: cancelAction,
|
||||
onPressed: _cancelAction,
|
||||
child: Text('cancel'.tr),
|
||||
)
|
||||
];
|
||||
@@ -134,6 +170,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
@@ -162,68 +199,55 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_editTo != null && _editTo!.isDraft != true)
|
||||
MaterialBanner(
|
||||
leading: const Icon(Icons.edit),
|
||||
leadingPadding: const EdgeInsets.only(left: 10, right: 20),
|
||||
dividerColor: Colors.transparent,
|
||||
content: Text('postEditingNotify'.tr),
|
||||
actions: notifyBannerActions,
|
||||
),
|
||||
if (_replyTo != null)
|
||||
ExpansionTile(
|
||||
leading: const FaIcon(
|
||||
FontAwesomeIcons.reply,
|
||||
size: 18,
|
||||
).paddingOnly(left: 2),
|
||||
title: Text('postReplyingNotify'.trParams(
|
||||
{'username': '@${widget.reply!.author.name}'},
|
||||
)),
|
||||
collapsedBackgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
children: [
|
||||
PostItem(
|
||||
item: _replyTo!,
|
||||
isReactable: false,
|
||||
).paddingOnly(bottom: 8),
|
||||
],
|
||||
),
|
||||
if (_repostTo != null)
|
||||
ExpansionTile(
|
||||
leading: const FaIcon(
|
||||
FontAwesomeIcons.retweet,
|
||||
size: 18,
|
||||
).paddingOnly(left: 2),
|
||||
title: Text('postRepostingNotify'.trParams(
|
||||
{'username': '@${widget.repost!.author.name}'},
|
||||
)),
|
||||
collapsedBackgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
children: [
|
||||
PostItem(
|
||||
item: _repostTo!,
|
||||
isReactable: false,
|
||||
).paddingOnly(bottom: 8),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
if (_isBusy)
|
||||
const LinearProgressIndicator().animate().scaleX(),
|
||||
if (widget.edit != null && widget.edit!.isDraft != true)
|
||||
MaterialBanner(
|
||||
leading: const Icon(Icons.edit),
|
||||
leadingPadding:
|
||||
const EdgeInsets.only(left: 10, right: 20),
|
||||
dividerColor: Colors.transparent,
|
||||
content: Text('postEditingNotify'.tr),
|
||||
actions: notifyBannerActions,
|
||||
),
|
||||
if (widget.reply != null)
|
||||
ExpansionTile(
|
||||
leading: const FaIcon(
|
||||
FontAwesomeIcons.reply,
|
||||
size: 18,
|
||||
).paddingOnly(left: 2),
|
||||
title: Text('postReplyingNotify'.trParams(
|
||||
{'username': '@${widget.reply!.author.name}'},
|
||||
)),
|
||||
collapsedBackgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
children: [
|
||||
PostItem(
|
||||
item: widget.reply!,
|
||||
isReactable: false,
|
||||
).paddingOnly(bottom: 8),
|
||||
],
|
||||
),
|
||||
if (widget.repost != null)
|
||||
ExpansionTile(
|
||||
leading: const FaIcon(
|
||||
FontAwesomeIcons.retweet,
|
||||
size: 18,
|
||||
).paddingOnly(left: 2),
|
||||
title: Text('postRepostingNotify'.trParams(
|
||||
{'username': '@${widget.repost!.author.name}'},
|
||||
)),
|
||||
collapsedBackgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
children: [
|
||||
PostItem(
|
||||
item: widget.repost!,
|
||||
isReactable: false,
|
||||
).paddingOnly(bottom: 8),
|
||||
],
|
||||
),
|
||||
if (widget.realm != null)
|
||||
MaterialBanner(
|
||||
leading: const Icon(Icons.group),
|
||||
leadingPadding:
|
||||
const EdgeInsets.only(left: 10, right: 20),
|
||||
dividerColor: Colors.transparent,
|
||||
content: Text(
|
||||
'postInRealmNotify'
|
||||
.trParams({'realm': '#${widget.realm!.alias}'}),
|
||||
),
|
||||
actions: notifyBannerActions,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
@@ -235,8 +259,8 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
autocorrect: true,
|
||||
keyboardType: TextInputType.multiline,
|
||||
controller: _editorController.contentController,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
focusNode: _contentFocusNode,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'postContentPlaceholder'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
@@ -383,12 +407,72 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.tag),
|
||||
icon: Obx(() {
|
||||
return badges.Badge(
|
||||
badgeContent: Text(
|
||||
_editorController.tags.length.toString(),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
showBadge: _editorController.tags.isNotEmpty,
|
||||
position: badges.BadgePosition.topEnd(
|
||||
top: -12,
|
||||
end: -8,
|
||||
),
|
||||
child: const Icon(Icons.label),
|
||||
);
|
||||
}),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () {
|
||||
_editorController.editCategoriesAndTags(context);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Obx(() {
|
||||
return badges.Badge(
|
||||
showBadge:
|
||||
_editorController.realmZone.value != null,
|
||||
position: badges.BadgePosition.topEnd(
|
||||
top: -4,
|
||||
end: -6,
|
||||
),
|
||||
child: const Icon(Icons.workspaces),
|
||||
);
|
||||
}),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () {
|
||||
_editorController.editPublishZone(context);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Obx(() {
|
||||
return badges.Badge(
|
||||
showBadge:
|
||||
_editorController.publishedAt.value != null ||
|
||||
_editorController.publishedUntil.value !=
|
||||
null,
|
||||
position: badges.BadgePosition.topEnd(
|
||||
top: -4,
|
||||
end: -6,
|
||||
),
|
||||
child: const Icon(Icons.schedule),
|
||||
);
|
||||
}),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () {
|
||||
_editorController.editPublishDate(context);
|
||||
},
|
||||
),
|
||||
MarkdownToolbar(
|
||||
hideImage: true,
|
||||
useIncludedTextField: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
iconColor: Theme.of(context).colorScheme.onSurface,
|
||||
controller: _editorController.contentController,
|
||||
focusNode: _contentFocusNode,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(20)),
|
||||
width: 40,
|
||||
).paddingSymmetric(horizontal: 4),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 6, vertical: 8),
|
||||
),
|
||||
@@ -403,6 +487,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_contentFocusNode.dispose();
|
||||
_editorController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
|
||||
final List<Realm> _realms = List.empty(growable: true);
|
||||
|
||||
getRealms() async {
|
||||
_getRealms() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
@@ -48,7 +48,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getRealms();
|
||||
_getRealms();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -71,7 +71,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
onPressed: () {
|
||||
AppRouter.instance.pushNamed('realmOrganizing').then(
|
||||
(value) {
|
||||
if (value != null) getRealms();
|
||||
if (value != null) _getRealms();
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -84,7 +84,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
body: Obx(() {
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return SigninRequiredOverlay(
|
||||
onSignedIn: () => getRealms(),
|
||||
onSignedIn: () => _getRealms(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,12 +94,12 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
Expanded(
|
||||
child: CenteredContainer(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => getRealms(),
|
||||
onRefresh: () => _getRealms(),
|
||||
child: ListView.builder(
|
||||
itemCount: _realms.length,
|
||||
itemBuilder: (context, 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(
|
||||
child: ClipRRect(
|
||||
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/router.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/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/channel/channel_list.dart';
|
||||
@@ -136,6 +135,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
||||
}
|
||||
|
||||
return TabBarView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
RealmPostListWidget(realm: _realm!),
|
||||
RealmChannelListWidget(
|
||||
@@ -189,7 +189,6 @@ class _RealmPostListWidgetState extends State<RealmPostListWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener(getPosts);
|
||||
}
|
||||
|
||||
@@ -199,28 +198,6 @@ class _RealmPostListWidgetState extends State<RealmPostListWidget> {
|
||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||
child: CustomScrollView(
|
||||
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),
|
||||
],
|
||||
),
|
||||
|
||||
102
lib/screens/settings.dart
Normal file
102
lib/screens/settings.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
|
||||
class SettingScreen extends StatefulWidget {
|
||||
const SettingScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SettingScreen> createState() => _SettingScreenState();
|
||||
}
|
||||
|
||||
class _SettingScreenState extends State<SettingScreen> {
|
||||
late final SharedPreferences _prefs;
|
||||
|
||||
Widget _buildCaptionHeader(String title) {
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Text(title),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemeColorButton(String label, Color color) {
|
||||
return IconButton(
|
||||
icon: Icon(Icons.circle, color: color),
|
||||
tooltip: label,
|
||||
onPressed: () {
|
||||
context.read<ThemeSwitcher>().setTheme(
|
||||
SolianTheme.build(
|
||||
Brightness.light,
|
||||
seedColor: color,
|
||||
),
|
||||
SolianTheme.build(
|
||||
Brightness.dark,
|
||||
seedColor: color,
|
||||
),
|
||||
);
|
||||
_prefs.setInt('global_theme_color', color.value);
|
||||
context.clearSnackbar();
|
||||
context.showSnackbar('themeColorApplied'.tr);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static final List<(String, Color)> _presentTheme = [
|
||||
('themeColorRed', const Color.fromRGBO(154, 98, 91, 1)),
|
||||
('themeColorBlue', const Color.fromRGBO(103, 96, 193, 1)),
|
||||
('themeColorMiku', const Color.fromRGBO(56, 120, 126, 1)),
|
||||
('themeColorKagamine', const Color.fromRGBO(244, 183, 63, 1)),
|
||||
('themeColorLuka', const Color.fromRGBO(243, 174, 218, 1)),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SharedPreferences.getInstance().then((inst) {
|
||||
_prefs = inst;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: ListView(
|
||||
children: [
|
||||
_buildCaptionHeader('themeColor'.tr),
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: _presentTheme
|
||||
.map((x) => _buildThemeColorButton(x.$1, x.$2))
|
||||
.toList(),
|
||||
).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 {
|
||||
final bool showAppBar;
|
||||
final bool isCenteredTitle;
|
||||
final GoRouterState state;
|
||||
final String? title;
|
||||
final GoRouterState? state;
|
||||
final Widget child;
|
||||
|
||||
const TitleShell({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.state,
|
||||
this.title,
|
||||
this.state,
|
||||
this.showAppBar = true,
|
||||
this.isCenteredTitle = false,
|
||||
});
|
||||
@@ -25,7 +27,9 @@ class TitleShell extends StatelessWidget {
|
||||
appBar: showAppBar
|
||||
? AppBar(
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
title: AppBarTitle(state.topRoute?.name?.tr ?? 'page'.tr),
|
||||
title: AppBarTitle(
|
||||
title ?? (state!.topRoute?.name?.tr ?? 'page'.tr),
|
||||
),
|
||||
centerTitle: isCenteredTitle,
|
||||
toolbarHeight: SolianTheme.toolbarHeight(context),
|
||||
)
|
||||
|
||||
@@ -27,13 +27,25 @@ abstract class SolianTheme {
|
||||
}
|
||||
}
|
||||
|
||||
static ThemeData build(Brightness brightness) {
|
||||
static ThemeData build(Brightness brightness, {Color? seedColor}) {
|
||||
return ThemeData(
|
||||
brightness: brightness,
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
brightness: brightness,
|
||||
seedColor: const Color.fromRGBO(154, 98, 91, 1),
|
||||
seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1),
|
||||
),
|
||||
fontFamily: 'Comfortaa',
|
||||
fontFamilyFallback: [
|
||||
'NotoSansSC',
|
||||
'NotoSansHK',
|
||||
'NotoSansJP',
|
||||
if (PlatformInfo.isWeb) 'NotoSansEmoji',
|
||||
],
|
||||
typography: Typography.material2021(
|
||||
colorScheme: brightness == Brightness.light
|
||||
? const ColorScheme.light()
|
||||
: const ColorScheme.dark(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ const i18nEnglish = {
|
||||
'draft': 'Draft',
|
||||
'draftSave': 'Save',
|
||||
'draftBox': 'Draft Box',
|
||||
'more': 'More',
|
||||
'share': 'Share',
|
||||
'feed': 'Feed',
|
||||
'unlink': 'Unlink',
|
||||
'feedSearch': 'Search Feed',
|
||||
'feedSearchWithTag': 'Searching with tag #@key',
|
||||
'feedSearchWithCategory': 'Searching in category @category',
|
||||
@@ -27,11 +29,13 @@ const i18nEnglish = {
|
||||
'about': 'About',
|
||||
'edit': 'Edit',
|
||||
'delete': 'Delete',
|
||||
'settings': 'Settings',
|
||||
'search': 'Search',
|
||||
'post': 'Post',
|
||||
'article': 'Article',
|
||||
'reply': 'Reply',
|
||||
'repost': 'Repost',
|
||||
'openInAlbum': 'Open in album',
|
||||
'openInBrowser': 'Open in browser',
|
||||
'notification': 'Notification',
|
||||
'errorHappened': 'An error occurred',
|
||||
@@ -99,6 +103,11 @@ const i18nEnglish = {
|
||||
'postRestoreFromLocal': 'Restore from local',
|
||||
'postAutoSaveAt': 'Auto saved at @date',
|
||||
'postCategoriesAndTags': 'Categories n\' Tags',
|
||||
'postPublishDate': 'Publish Date',
|
||||
'postPublishAt': 'Publish At',
|
||||
'postPublishedUntil': 'Publish Until',
|
||||
'postPublishZone': 'Publish Zone',
|
||||
'postPublishZoneNone': 'None',
|
||||
'postVisibility': 'Visibility',
|
||||
'postVisibilityAll': 'Everyone',
|
||||
'postVisibilityFriends': 'Friends',
|
||||
@@ -122,7 +131,7 @@ const i18nEnglish = {
|
||||
'postAction': 'Post',
|
||||
'postEdited': 'Edited at @date',
|
||||
'postNewCreated': 'Created at @date',
|
||||
'postAttachmentTip': '@count attachment(s)',
|
||||
'attachmentHint': '@count attachment(s)',
|
||||
'postInRealm': 'In @realm',
|
||||
'postDetail': 'Post',
|
||||
'postReplies': 'Replies',
|
||||
@@ -150,6 +159,12 @@ const i18nEnglish = {
|
||||
'reactCompleted': 'Your reaction has been added',
|
||||
'reactUncompleted': 'Your reaction has been removed',
|
||||
'attachmentUploadBy': 'Upload by',
|
||||
'attachmentAutoUpload': 'Auto Upload',
|
||||
'attachmentUploadQueue': 'Upload Queue',
|
||||
'attachmentUploadQueueStart': 'Start All',
|
||||
'attachmentUploadInProgress': 'There are attachments being uploaded. Please wait until all attachments have been uploaded before proceeding...',
|
||||
'attachmentAttached': 'Exists Files',
|
||||
'attachmentUploadBlocked': 'Upload blocked, there is currently a task in progress...',
|
||||
'attachmentAdd': 'Attach attachments',
|
||||
'attachmentAddGalleryPhoto': 'Gallery photo',
|
||||
'attachmentAddGalleryVideo': 'Gallery video',
|
||||
@@ -157,6 +172,9 @@ const i18nEnglish = {
|
||||
'attachmentAddCameraVideo': 'Capture video',
|
||||
'attachmentAddClipboard': 'Paste file',
|
||||
'attachmentAddFile': 'Attach file',
|
||||
'attachmentAddLink': 'Link attachments',
|
||||
'attachmentAddLinkHint': 'Enter attachment serial number to link that attachment',
|
||||
'attachmentAddLinkInput': 'Serial number',
|
||||
'attachmentSetting': 'Adjust attachment',
|
||||
'attachmentAlt': 'Alternative text',
|
||||
'attachmentLoadFailed': 'Load Attachment Failed',
|
||||
@@ -239,6 +257,8 @@ const i18nEnglish = {
|
||||
'Are your sure to delete message @id? This action cannot be undone!',
|
||||
'call': 'Call',
|
||||
'callOngoing': 'A call is ongoing...',
|
||||
'callOngoingEmpty': 'A call is on hold...',
|
||||
'callOngoingParticipants': '@count people are calling...',
|
||||
'callJoin': 'Join',
|
||||
'callResume': 'Resume',
|
||||
'callMicrophone': 'Microphone',
|
||||
@@ -293,6 +313,12 @@ const i18nEnglish = {
|
||||
'accountStatusNegative': 'Negative',
|
||||
'accountStatusNeutral': 'Neutral',
|
||||
'accountStatusPositive': 'Positive',
|
||||
'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',
|
||||
'bsCheckingServerFail':
|
||||
'Unable connect to server, check your network connection',
|
||||
@@ -304,4 +330,13 @@ const i18nEnglish = {
|
||||
'postShareContent':
|
||||
'@content\n\n@username on the Solar Network\nCheck it out: @link',
|
||||
'postShareSubject': '@username posted a post on the Solar Network',
|
||||
'themeColor': 'Global Theme Color',
|
||||
'themeColorRed': 'Modern Red',
|
||||
'themeColorBlue': 'Classic Blue',
|
||||
'themeColorMiku': 'Miku Blue',
|
||||
'themeColorKagamine': 'Kagamine Yellow',
|
||||
'themeColorLuka': 'Luka Pink',
|
||||
'themeColorApplied': 'Global theme color has been applied.',
|
||||
'attachmentSaved': 'Attachment saved to your system album.',
|
||||
'cropImage': 'Crop Image',
|
||||
};
|
||||
|
||||
@@ -13,12 +13,15 @@ const i18nSimplifiedChinese = {
|
||||
'about': '关于',
|
||||
'edit': '编辑',
|
||||
'delete': '删除',
|
||||
'settings': '设置',
|
||||
'page': '页面',
|
||||
'draft': '草稿',
|
||||
'draftSave': '存为草稿',
|
||||
'draftBox': '草稿箱',
|
||||
'more': '更多',
|
||||
'share': '分享',
|
||||
'feed': '资讯',
|
||||
'unlink': '移除链接',
|
||||
'feedSearch': '搜索资讯',
|
||||
'feedSearchWithTag': '检索带有 #@key 标签的资讯',
|
||||
'feedSearchWithCategory': '检索位于分类 @category 的资讯',
|
||||
@@ -32,6 +35,7 @@ const i18nSimplifiedChinese = {
|
||||
'article': '文章',
|
||||
'reply': '回复',
|
||||
'repost': '转帖',
|
||||
'openInAlbum': '在相簿中打开',
|
||||
'openInBrowser': '在浏览器中打开',
|
||||
'notification': '通知',
|
||||
'errorHappened': '发生错误了',
|
||||
@@ -93,6 +97,11 @@ const i18nSimplifiedChinese = {
|
||||
'postRestoreFromLocal': '内容从本地暂存回复',
|
||||
'postAutoSaveAt': '已自动保存于 @date',
|
||||
'postCategoriesAndTags': '分类与标签',
|
||||
'postPublishDate': '发布时间',
|
||||
'postPublishAt': '发布帖子于',
|
||||
'postPublishedUntil': '取消发布于',
|
||||
'postPublishZone': '帖子发布区',
|
||||
'postPublishZoneNone': '无所属领域',
|
||||
'postVisibility': '帖子可见性',
|
||||
'postVisibilityAll': '所有人可见',
|
||||
'postVisibilityFriends': '仅好友可见',
|
||||
@@ -117,7 +126,7 @@ const i18nSimplifiedChinese = {
|
||||
'postEdited': '编辑于 @date',
|
||||
'postNewCreated': '创建于 @date',
|
||||
'postInRealm': '发表于 @realm',
|
||||
'postAttachmentTip': '@count 个附件',
|
||||
'attachmentHint': '@count 个附件',
|
||||
'postDetail': '帖子详情',
|
||||
'postReplies': '帖子回复',
|
||||
'postPublish': '编辑帖子',
|
||||
@@ -139,6 +148,12 @@ const i18nSimplifiedChinese = {
|
||||
'reactCompleted': '你的反应已被添加',
|
||||
'reactUncompleted': '你的反应已被移除',
|
||||
'attachmentUploadBy': '由上传',
|
||||
'attachmentAutoUpload': '自动上传',
|
||||
'attachmentUploadQueue': '上传队列',
|
||||
'attachmentUploadQueueStart': '整队上传',
|
||||
'attachmentUploadInProgress': '有附件正在上传,请等待所有附件上传完毕后再进行操作……',
|
||||
'attachmentAttached': '已附附件',
|
||||
'attachmentUploadBlocked': '上传受阻,当前已有任务进行中……',
|
||||
'attachmentAdd': '附加附件',
|
||||
'attachmentAddGalleryPhoto': '相册照片',
|
||||
'attachmentAddGalleryVideo': '相册视频',
|
||||
@@ -146,6 +161,9 @@ const i18nSimplifiedChinese = {
|
||||
'attachmentAddCameraVideo': '拍摄视频',
|
||||
'attachmentAddClipboard': '粘贴文件',
|
||||
'attachmentAddFile': '附加文件',
|
||||
'attachmentAddLink': '链接附件',
|
||||
'attachmentAddLinkHint': '输入附件的神秘代号来链接对应附件',
|
||||
'attachmentAddLinkInput': '神秘代号',
|
||||
'attachmentSetting': '调整附件',
|
||||
'attachmentAlt': '替代文字',
|
||||
'attachmentLoadFailed': '加载失败',
|
||||
@@ -220,6 +238,8 @@ const i18nSimplifiedChinese = {
|
||||
'messageDeletionConfirmCaption': '你确定要删除消息 @id 吗?该操作不可撤销。',
|
||||
'call': '通话',
|
||||
'callOngoing': '一则通话正在进行中…',
|
||||
'callOngoingEmpty': '一则通话待机中…',
|
||||
'callOngoingParticipants': '@count 人正在进行通话…',
|
||||
'callJoin': '加入',
|
||||
'callResume': '恢复',
|
||||
'callMicrophone': '麦克风',
|
||||
@@ -272,6 +292,12 @@ const i18nSimplifiedChinese = {
|
||||
'accountStatusNegative': '负面',
|
||||
'accountStatusNeutral': '中性',
|
||||
'accountStatusPositive': '积极',
|
||||
'bsLoadingTheme': '正在装载主题',
|
||||
'bsCheckForUpdate': '正在检查更新',
|
||||
'bsCheckForUpdateFailed': '无法检查更新',
|
||||
'bsCheckForUpdateNew': '发现新版本',
|
||||
'bsCheckForUpdateDescApple': '请前往 TestFlight 并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。',
|
||||
'bsCheckForUpdateDescCommon': '请前往我们的网站下载并安装最新版本的应用程序,以防止出现错误并获取最新功能。',
|
||||
'bsCheckingServer': '检查服务器状态中',
|
||||
'bsCheckingServerFail': '无法连接至服务器,请检查你的网络连接状态',
|
||||
'bsCheckingServerDown': '当前服务器不可用,请稍后重试',
|
||||
@@ -281,4 +307,13 @@ const i18nSimplifiedChinese = {
|
||||
'bsRegisteringPushNotify': '正在启用推送通知',
|
||||
'postShareContent': '@content\n\n@username 在 Solar Network\n原帖地址:@link',
|
||||
'postShareSubject': '@username 在 Solar Network 上发布了一篇帖子',
|
||||
'themeColor': '全局主题色',
|
||||
'themeColorRed': '现代红',
|
||||
'themeColorBlue': '经典蓝',
|
||||
'themeColorMiku': '未来蓝',
|
||||
'themeColorKagamine': '镜音黄',
|
||||
'themeColorLuka': '流音粉',
|
||||
'themeColorApplied': '全局主题颜色已应用',
|
||||
'attachmentSaved': '附件已保存到系统相册',
|
||||
'cropImage': '裁剪图片',
|
||||
};
|
||||
|
||||
132
lib/widgets/attachments/attachment_attr_editor.dart
Normal file
132
lib/widgets/attachments/attachment_attr_editor.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
|
||||
class AttachmentAttrEditorDialog extends StatefulWidget {
|
||||
final Attachment item;
|
||||
final Function(Attachment item) onUpdate;
|
||||
|
||||
const AttachmentAttrEditorDialog({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.onUpdate,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AttachmentAttrEditorDialog> createState() => _AttachmentAttrEditorDialogState();
|
||||
}
|
||||
|
||||
class _AttachmentAttrEditorDialogState extends State<AttachmentAttrEditorDialog> {
|
||||
final _altController = TextEditingController();
|
||||
|
||||
bool _isBusy = false;
|
||||
bool _isMature = false;
|
||||
|
||||
Future<Attachment?> _updateAttachment() async {
|
||||
final AttachmentProvider provider = Get.find();
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final resp = await provider.updateAttachment(
|
||||
widget.item.id,
|
||||
_altController.value.text,
|
||||
widget.item.usage,
|
||||
isMature: _isMature,
|
||||
);
|
||||
|
||||
Get.find<AttachmentProvider>().clearCache(id: widget.item.id);
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
return Attachment.fromJson(resp.body);
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
setState(() => _isBusy = false);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void syncWidget() {
|
||||
_isMature = widget.item.isMature;
|
||||
_altController.text = widget.item.alt;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
syncWidget();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('attachmentSetting'.tr),
|
||||
content: Container(
|
||||
constraints: const BoxConstraints(minWidth: 400),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_isBusy)
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: const LinearProgressIndicator().animate().scaleX(),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
TextField(
|
||||
controller: _altController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
prefixIcon: const Icon(Icons.image_not_supported),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'attachmentAlt'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 4, right: 18),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
title: Text('matureContent'.tr),
|
||||
secondary: const Icon(Icons.visibility_off),
|
||||
value: _isMature,
|
||||
onChanged: (newValue) {
|
||||
setState(() => _isMature = newValue ?? false);
|
||||
},
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actionsAlignment: MainAxisAlignment.spaceBetween,
|
||||
actions: <Widget>[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('cancel'.tr),
|
||||
),
|
||||
TextButton(
|
||||
child: Text('apply'.tr),
|
||||
onPressed: () {
|
||||
_updateAttachment().then((value) {
|
||||
if (value != null) {
|
||||
widget.onUpdate(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
296
lib/widgets/attachments/attachment_fullscreen.dart
Normal file
296
lib/widgets/attachments/attachment_fullscreen.dart
Normal file
@@ -0,0 +1,296 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_item.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:path/path.dart' show extension;
|
||||
|
||||
class AttachmentFullScreen extends StatefulWidget {
|
||||
final String parentId;
|
||||
final Attachment item;
|
||||
|
||||
const AttachmentFullScreen(
|
||||
{super.key, required this.parentId, required this.item});
|
||||
|
||||
@override
|
||||
State<AttachmentFullScreen> createState() => _AttachmentFullScreenState();
|
||||
}
|
||||
|
||||
class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
|
||||
bool _showDetails = true;
|
||||
|
||||
bool _isDownloading = false;
|
||||
bool _isCompletedDownload = false;
|
||||
double? _progressOfDownload = 0;
|
||||
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
String _formatBytes(int bytes, {int decimals = 2}) {
|
||||
if (bytes == 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
final dm = decimals < 0 ? 0 : decimals;
|
||||
final sizes = [
|
||||
'Bytes',
|
||||
'KiB',
|
||||
'MiB',
|
||||
'GiB',
|
||||
'TiB',
|
||||
'PiB',
|
||||
'EiB',
|
||||
'ZiB',
|
||||
'YiB'
|
||||
];
|
||||
final i = (math.log(bytes) / math.log(k)).floor().toInt();
|
||||
return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
|
||||
}
|
||||
|
||||
double _getRatio() {
|
||||
final value = widget.item.metadata?['ratio'];
|
||||
if (value == null) return 1;
|
||||
if (value is int) return value.toDouble();
|
||||
if (value is double) return value;
|
||||
return 1;
|
||||
}
|
||||
|
||||
Future<void> _saveToAlbum() async {
|
||||
final url = ServiceFinder.buildUrl(
|
||||
'files',
|
||||
'/attachments/${widget.item.id}',
|
||||
);
|
||||
|
||||
if (PlatformInfo.isWeb) {
|
||||
await launchUrlString(url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await Gal.hasAccess(toAlbum: true)) {
|
||||
if (!await Gal.requestAccess(toAlbum: true)) return;
|
||||
}
|
||||
|
||||
setState(() => _isDownloading = true);
|
||||
|
||||
var extName = extension(widget.item.name);
|
||||
if (extName.isEmpty) extName = '.png';
|
||||
final imagePath =
|
||||
'${Directory.systemTemp.path}/${widget.item.uuid}$extName';
|
||||
await Dio().download(
|
||||
url,
|
||||
imagePath,
|
||||
onReceiveProgress: (count, total) {
|
||||
setState(() => _progressOfDownload = count / total);
|
||||
},
|
||||
);
|
||||
|
||||
bool isSuccess = false;
|
||||
try {
|
||||
await Gal.putImage(imagePath);
|
||||
isSuccess = true;
|
||||
} on GalException catch (e) {
|
||||
context.showErrorDialog(e.type.message);
|
||||
}
|
||||
|
||||
context.showSnackbar(
|
||||
'attachmentSaved'.tr,
|
||||
action: SnackBarAction(
|
||||
label: 'openInAlbum'.tr,
|
||||
onPressed: () async => Gal.open(),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_isDownloading = false;
|
||||
_isCompletedDownload = isSuccess;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final metaTextStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
color: _unFocusColor,
|
||||
);
|
||||
|
||||
return DismissiblePage(
|
||||
key: Key('attachment-dismissible${widget.item.id}'),
|
||||
direction: DismissiblePageDismissDirection.vertical,
|
||||
onDismissed: () => Navigator.pop(context),
|
||||
dismissThresholds: const {
|
||||
DismissiblePageDismissDirection.vertical: 0.0,
|
||||
},
|
||||
onDragStart: () {
|
||||
setState(() => _showDetails = false);
|
||||
},
|
||||
onDragEnd: () {
|
||||
setState(() => _showDetails = true);
|
||||
},
|
||||
child: GestureDetector(
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: InteractiveViewer(
|
||||
boundaryMargin: EdgeInsets.zero,
|
||||
minScale: 1,
|
||||
maxScale: 16,
|
||||
panEnabled: true,
|
||||
scaleEnabled: true,
|
||||
child: AttachmentItem(
|
||||
parentId: widget.parentId,
|
||||
showHideButton: false,
|
||||
item: widget.item,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surface.withOpacity(0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate(target: _showDetails ? 1 : 0)
|
||||
.fadeIn(curve: Curves.fastEaseInToSlowEaseOut),
|
||||
Positioned(
|
||||
bottom: math.max(MediaQuery.of(context).padding.bottom, 16),
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.item.account != null)
|
||||
Row(
|
||||
children: [
|
||||
IgnorePointer(
|
||||
child: AccountAvatar(
|
||||
content: widget.item.account!.avatar,
|
||||
radius: 19,
|
||||
),
|
||||
),
|
||||
const IgnorePointer(child: SizedBox(width: 8)),
|
||||
Expanded(
|
||||
child: IgnorePointer(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'attachmentUploadBy'.tr,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
widget.item.account!.nick,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -2,
|
||||
),
|
||||
icon: !_isDownloading
|
||||
? !_isCompletedDownload
|
||||
? const Icon(Icons.save_alt)
|
||||
: const Icon(Icons.download_done)
|
||||
: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
value: _progressOfDownload,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
onPressed:
|
||||
_isDownloading ? null : () => _saveToAlbum(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const IgnorePointer(child: SizedBox(height: 4)),
|
||||
IgnorePointer(
|
||||
child: Text(
|
||||
widget.item.alt,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const IgnorePointer(child: SizedBox(height: 2)),
|
||||
IgnorePointer(
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (widget.item.metadata?['width'] != null &&
|
||||
widget.item.metadata?['height'] != null)
|
||||
Text(
|
||||
'${widget.item.metadata?['width']}x${widget.item.metadata?['height']}',
|
||||
style: metaTextStyle,
|
||||
),
|
||||
if (widget.item.metadata?['ratio'] != null)
|
||||
Text(
|
||||
'${_getRatio().toPrecision(2)}',
|
||||
style: metaTextStyle,
|
||||
),
|
||||
Text(
|
||||
_formatBytes(widget.item.size),
|
||||
style: metaTextStyle,
|
||||
),
|
||||
Text(
|
||||
widget.item.mimetype,
|
||||
style: metaTextStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate(target: _showDetails ? 1 : 0)
|
||||
.fadeIn(curve: Curves.fastEaseInToSlowEaseOut),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
setState(() => _showDetails = !_showDetails);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import 'package:get/get.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_item.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_list_fullscreen.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
|
||||
|
||||
class AttachmentList extends StatefulWidget {
|
||||
final String parentId;
|
||||
@@ -45,7 +45,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
List<Attachment?> _attachmentsMeta = List.empty();
|
||||
|
||||
void _getMetadataList() {
|
||||
final AttachmentProvider provider = Get.find();
|
||||
final AttachmentProvider attach = Get.find();
|
||||
|
||||
if (widget.attachmentsId.isEmpty) {
|
||||
return;
|
||||
@@ -53,25 +53,16 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
_attachmentsMeta = List.filled(widget.attachmentsId.length, null);
|
||||
}
|
||||
|
||||
int progress = 0;
|
||||
for (var idx = 0; idx < widget.attachmentsId.length; idx++) {
|
||||
provider.getMetadata(widget.attachmentsId[idx]).then((resp) {
|
||||
progress++;
|
||||
if (resp != null) {
|
||||
_attachmentsMeta[idx] = resp;
|
||||
}
|
||||
if (progress == widget.attachmentsId.length) {
|
||||
calculateAspectRatio();
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
attach.listMetadata(widget.attachmentsId).then((result) {
|
||||
setState(() {
|
||||
_attachmentsMeta = result;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
_calculateAspectRatio();
|
||||
});
|
||||
}
|
||||
|
||||
void calculateAspectRatio() {
|
||||
void _calculateAspectRatio() {
|
||||
bool isConsistent = true;
|
||||
double? consistentValue;
|
||||
int portrait = 0, square = 0, landscape = 0;
|
||||
@@ -320,9 +311,9 @@ class AttachmentListEntry extends StatelessWidget {
|
||||
onReveal(true);
|
||||
} else if (['image'].contains(item!.mimetype.split('/').first)) {
|
||||
context.pushTransparentRoute(
|
||||
AttachmentListFullScreen(
|
||||
AttachmentFullScreen(
|
||||
parentId: parentId,
|
||||
attachment: item!,
|
||||
item: item!,
|
||||
),
|
||||
rootNavigator: true,
|
||||
);
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_item.dart';
|
||||
|
||||
class AttachmentListFullScreen extends StatefulWidget {
|
||||
final String parentId;
|
||||
final Attachment attachment;
|
||||
|
||||
const AttachmentListFullScreen(
|
||||
{super.key, required this.parentId, required this.attachment});
|
||||
|
||||
@override
|
||||
State<AttachmentListFullScreen> createState() =>
|
||||
_AttachmentListFullScreenState();
|
||||
}
|
||||
|
||||
class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
|
||||
bool _showDetails = true;
|
||||
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
String _formatBytes(int bytes, {int decimals = 2}) {
|
||||
if (bytes == 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
final dm = decimals < 0 ? 0 : decimals;
|
||||
final sizes = [
|
||||
'Bytes',
|
||||
'KiB',
|
||||
'MiB',
|
||||
'GiB',
|
||||
'TiB',
|
||||
'PiB',
|
||||
'EiB',
|
||||
'ZiB',
|
||||
'YiB'
|
||||
];
|
||||
final i = (math.log(bytes) / math.log(k)).floor().toInt();
|
||||
return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DismissiblePage(
|
||||
key: Key('attachment-dismissible${widget.attachment.id}'),
|
||||
direction: DismissiblePageDismissDirection.vertical,
|
||||
onDismissed: () => Navigator.pop(context),
|
||||
dismissThresholds: const {
|
||||
DismissiblePageDismissDirection.vertical: 0.0,
|
||||
},
|
||||
onDragStart: () {
|
||||
setState(() => _showDetails = false);
|
||||
},
|
||||
onDragEnd: () {
|
||||
setState(() => _showDetails = true);
|
||||
},
|
||||
child: GestureDetector(
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: InteractiveViewer(
|
||||
boundaryMargin: EdgeInsets.zero,
|
||||
minScale: 1,
|
||||
maxScale: 16,
|
||||
panEnabled: true,
|
||||
scaleEnabled: true,
|
||||
child: AttachmentItem(
|
||||
parentId: widget.parentId,
|
||||
showHideButton: false,
|
||||
item: widget.attachment,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surface.withOpacity(0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate(target: _showDetails ? 1 : 0)
|
||||
.fadeIn(curve: Curves.fastEaseInToSlowEaseOut),
|
||||
Positioned(
|
||||
bottom: math.max(MediaQuery.of(context).padding.bottom, 16),
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: IgnorePointer(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.attachment.account != null)
|
||||
Row(
|
||||
children: [
|
||||
AccountAvatar(
|
||||
content: widget.attachment.account!.avatar,
|
||||
radius: 19,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'attachmentUploadBy'.tr,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
widget.attachment.account!.nick,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.attachment.alt,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (widget.attachment.metadata?['width'] != null &&
|
||||
widget.attachment.metadata?['height'] != null)
|
||||
Text(
|
||||
'${widget.attachment.metadata?['width']}x${widget.attachment.metadata?['height']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
),
|
||||
if (widget.attachment.metadata?['ratio'] != null)
|
||||
Text(
|
||||
'${(widget.attachment.metadata?['ratio'] as double).toPrecision(2)}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatBytes(widget.attachment.size),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate(target: _showDetails ? 1 : 0)
|
||||
.fadeIn(curve: Curves.fastEaseInToSlowEaseOut),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
setState(() => _showDetails = !_showDetails);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
89
lib/widgets/channel/channel_call_indicator.dart
Normal file
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:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/controllers/chat_events_controller.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/router.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 Map<String, List<Channel>> _inRealms = {};
|
||||
|
||||
void mapChannels() {
|
||||
final ChatEventController _eventController = ChatEventController();
|
||||
|
||||
void _mapChannels() {
|
||||
_inRealms.clear();
|
||||
_globalChannels.clear();
|
||||
|
||||
@@ -55,16 +59,17 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||
@override
|
||||
void didUpdateWidget(covariant ChannelListWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
setState(() => mapChannels());
|
||||
setState(() => _mapChannels());
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
mapChannels();
|
||||
_mapChannels();
|
||||
_eventController.initialize();
|
||||
}
|
||||
|
||||
void gotoChannel(Channel item) {
|
||||
void _gotoChannel(Channel item) {
|
||||
if (widget.useReplace) {
|
||||
AppRouter.instance.pushReplacementNamed(
|
||||
'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
|
||||
? const EdgeInsets.symmetric(horizontal: 20)
|
||||
: const EdgeInsets.symmetric(horizontal: 16);
|
||||
@@ -102,37 +135,36 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||
leading: AccountAvatar(
|
||||
content: otherside.account.avatar,
|
||||
radius: widget.isDense ? 12 : 20,
|
||||
bgColor: Colors.indigo,
|
||||
feColor: Colors.white,
|
||||
bgColor: Theme.of(context).colorScheme.primary,
|
||||
feColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
contentPadding: padding,
|
||||
title: Text(otherside.account.nick),
|
||||
subtitle: !widget.isDense
|
||||
? Text(
|
||||
'channelDirectDescription'.trParams(
|
||||
{'username': '@${otherside.account.name}'},
|
||||
),
|
||||
)
|
||||
? _buildDirectMessageDescription(item, otherside)
|
||||
: null,
|
||||
onTap: () => gotoChannel(item),
|
||||
onTap: () => _gotoChannel(item),
|
||||
);
|
||||
} else {
|
||||
return ListTile(
|
||||
minTileHeight: widget.isDense ? 48 : null,
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
item.realmId == null ? Colors.indigo : Colors.transparent,
|
||||
backgroundColor: item.realmId == null
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.transparent,
|
||||
radius: widget.isDense ? 12 : 20,
|
||||
child: FaIcon(
|
||||
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,
|
||||
),
|
||||
),
|
||||
contentPadding: padding,
|
||||
title: Text(item.name),
|
||||
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,
|
||||
itemBuilder: (context, index) {
|
||||
final element = _globalChannels[index];
|
||||
return buildItem(element);
|
||||
return _buildEntry(element);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -159,13 +191,13 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||
itemCount: _globalChannels.length,
|
||||
itemBuilder: (context, index) {
|
||||
final element = _globalChannels[index];
|
||||
return buildItem(element);
|
||||
return _buildEntry(element);
|
||||
},
|
||||
),
|
||||
SliverList.list(
|
||||
children: _inRealms.entries.map((element) {
|
||||
return ExpansionTile(
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
tilePadding: const EdgeInsets.only(left: 20, right: 24),
|
||||
minTileHeight: 48,
|
||||
title: Text(element.value.first.realm!.name),
|
||||
leading: CircleAvatar(
|
||||
@@ -177,7 +209,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||
size: widget.isDense ? 12 : 16,
|
||||
),
|
||||
),
|
||||
children: element.value.map((x) => buildItem(x)).toList(),
|
||||
children: element.value.map((x) => _buildEntry(x)).toList(),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
@@ -23,7 +23,7 @@ class ControlsWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
CameraPosition position = CameraPosition.front;
|
||||
CameraPosition _position = CameraPosition.front;
|
||||
|
||||
List<MediaDevice>? _audioInputs;
|
||||
List<MediaDevice>? _audioOutputs;
|
||||
@@ -36,25 +36,25 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
participant.addListener(onChange);
|
||||
_participant.addListener(onChange);
|
||||
_subscription = Hardware.instance.onDeviceChange.stream
|
||||
.listen((List<MediaDevice> devices) {
|
||||
revertDevices(devices);
|
||||
_revertDevices(devices);
|
||||
});
|
||||
Hardware.instance.enumerateDevices().then(revertDevices);
|
||||
Hardware.instance.enumerateDevices().then(_revertDevices);
|
||||
_speakerphoneOn = Hardware.instance.speakerOn ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
participant.removeListener(onChange);
|
||||
_participant.removeListener(onChange);
|
||||
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();
|
||||
_audioOutputs = devices.where((d) => d.kind == 'audiooutput').toList();
|
||||
_videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
|
||||
@@ -63,7 +63,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
|
||||
void onChange() => setState(() {});
|
||||
|
||||
bool get isMuted => participant.isMuted;
|
||||
bool get isMuted => _participant.isMuted;
|
||||
|
||||
Future<bool?> showDisconnectDialog() {
|
||||
return showDialog<bool>(
|
||||
@@ -85,7 +85,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
void disconnect() async {
|
||||
void _disconnect() async {
|
||||
if (await showDisconnectDialog() != true) return;
|
||||
|
||||
final ChatCallProvider provider = Get.find();
|
||||
@@ -95,59 +95,59 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
void disableAudio() async {
|
||||
await participant.setMicrophoneEnabled(false);
|
||||
void _disableAudio() async {
|
||||
await _participant.setMicrophoneEnabled(false);
|
||||
}
|
||||
|
||||
void enableAudio() async {
|
||||
await participant.setMicrophoneEnabled(true);
|
||||
void _enableAudio() async {
|
||||
await _participant.setMicrophoneEnabled(true);
|
||||
}
|
||||
|
||||
void disableVideo() async {
|
||||
await participant.setCameraEnabled(false);
|
||||
void _disableVideo() async {
|
||||
await _participant.setCameraEnabled(false);
|
||||
}
|
||||
|
||||
void enableVideo() async {
|
||||
await participant.setCameraEnabled(true);
|
||||
void _enableVideo() async {
|
||||
await _participant.setCameraEnabled(true);
|
||||
}
|
||||
|
||||
void selectAudioOutput(MediaDevice device) async {
|
||||
void _selectAudioOutput(MediaDevice device) async {
|
||||
await widget.room.setAudioOutputDevice(device);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void selectAudioInput(MediaDevice device) async {
|
||||
void _selectAudioInput(MediaDevice device) async {
|
||||
await widget.room.setAudioInputDevice(device);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void selectVideoInput(MediaDevice device) async {
|
||||
void _selectVideoInput(MediaDevice device) async {
|
||||
await widget.room.setVideoInputDevice(device);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setSpeakerphoneOn() {
|
||||
void _setSpeakerphoneOn() {
|
||||
_speakerphoneOn = !_speakerphoneOn;
|
||||
Hardware.instance.setSpeakerphoneOn(_speakerphoneOn);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void toggleCamera() async {
|
||||
final track = participant.videoTrackPublications.firstOrNull?.track;
|
||||
void _toggleCamera() async {
|
||||
final track = _participant.videoTrackPublications.firstOrNull?.track;
|
||||
if (track == null) return;
|
||||
|
||||
try {
|
||||
final newPosition = position.switched();
|
||||
final newPosition = _position.switched();
|
||||
await track.setCameraPosition(newPosition);
|
||||
setState(() {
|
||||
position = newPosition;
|
||||
_position = newPosition;
|
||||
});
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void enableScreenShare() async {
|
||||
void _enableScreenShare() async {
|
||||
if (lkPlatformIsDesktop()) {
|
||||
try {
|
||||
final source = await showDialog<DesktopCapturerSource>(
|
||||
@@ -163,7 +163,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
maxFrameRate: 15.0,
|
||||
),
|
||||
);
|
||||
await participant.publishVideoTrack(track);
|
||||
await _participant.publishVideoTrack(track);
|
||||
} catch (e) {
|
||||
final message = e.toString();
|
||||
context.showErrorDialog(message);
|
||||
@@ -177,7 +177,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
maxFrameRate: 30.0,
|
||||
),
|
||||
);
|
||||
await participant.publishVideoTrack(track);
|
||||
await _participant.publishVideoTrack(track);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -188,11 +188,11 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
return;
|
||||
}
|
||||
|
||||
await participant.setScreenShareEnabled(true, captureScreenAudio: true);
|
||||
await _participant.setScreenShareEnabled(true, captureScreenAudio: true);
|
||||
}
|
||||
|
||||
void disableScreenShare() async {
|
||||
await participant.setScreenShareEnabled(false);
|
||||
void _disableScreenShare() async {
|
||||
await _participant.setScreenShareEnabled(false);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -210,12 +210,12 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
icon: Transform.flip(
|
||||
flipX: true, child: const Icon(Icons.exit_to_app)),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: disconnect,
|
||||
onPressed: _disconnect,
|
||||
),
|
||||
if (participant.isMicrophoneEnabled())
|
||||
if (_participant.isMicrophoneEnabled())
|
||||
if (lkPlatformIs(PlatformType.android))
|
||||
IconButton(
|
||||
onPressed: disableAudio,
|
||||
onPressed: _disableAudio,
|
||||
icon: const Icon(Icons.mic),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
tooltip: 'callMicrophoneOff'.tr,
|
||||
@@ -227,7 +227,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
return [
|
||||
PopupMenuItem<MediaDevice>(
|
||||
value: null,
|
||||
onTap: isMuted ? enableAudio : disableAudio,
|
||||
onTap: isMuted ? _enableAudio : _disableAudio,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.mic_off),
|
||||
title: Text(isMuted
|
||||
@@ -246,7 +246,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
: const Icon(Icons.check_box_outline_blank),
|
||||
title: Text(device.label),
|
||||
),
|
||||
onTap: () => selectAudioInput(device),
|
||||
onTap: () => _selectAudioInput(device),
|
||||
);
|
||||
})
|
||||
];
|
||||
@@ -254,19 +254,19 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
onPressed: enableAudio,
|
||||
onPressed: _enableAudio,
|
||||
icon: const Icon(Icons.mic_off),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
tooltip: 'callMicrophoneOn'.tr,
|
||||
),
|
||||
if (participant.isCameraEnabled())
|
||||
if (_participant.isCameraEnabled())
|
||||
PopupMenuButton<MediaDevice>(
|
||||
icon: const Icon(Icons.videocam_sharp),
|
||||
itemBuilder: (BuildContext context) {
|
||||
return [
|
||||
PopupMenuItem<MediaDevice>(
|
||||
value: null,
|
||||
onTap: disableVideo,
|
||||
onTap: _disableVideo,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.videocam_off),
|
||||
title: Text('callCameraOff'.tr),
|
||||
@@ -283,7 +283,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
: const Icon(Icons.check_box_outline_blank),
|
||||
title: Text(device.label),
|
||||
),
|
||||
onTap: () => selectVideoInput(device),
|
||||
onTap: () => _selectVideoInput(device),
|
||||
);
|
||||
})
|
||||
];
|
||||
@@ -291,17 +291,17 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
onPressed: enableVideo,
|
||||
onPressed: _enableVideo,
|
||||
icon: const Icon(Icons.videocam_off),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
tooltip: 'callCameraOn'.tr,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(position == CameraPosition.back
|
||||
icon: Icon(_position == CameraPosition.back
|
||||
? Icons.video_camera_back
|
||||
: Icons.video_camera_front),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => toggleCamera(),
|
||||
onPressed: () => _toggleCamera(),
|
||||
tooltip: 'callVideoFlip'.tr,
|
||||
),
|
||||
if (!lkPlatformIs(PlatformType.iOS))
|
||||
@@ -327,7 +327,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
: const Icon(Icons.check_box_outline_blank),
|
||||
title: Text(device.label),
|
||||
),
|
||||
onTap: () => selectAudioOutput(device),
|
||||
onTap: () => _selectAudioOutput(device),
|
||||
);
|
||||
})
|
||||
];
|
||||
@@ -336,7 +336,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
if (!kIsWeb && lkPlatformIs(PlatformType.iOS))
|
||||
IconButton(
|
||||
onPressed: Hardware.instance.canSwitchSpeakerphone
|
||||
? setSpeakerphoneOn
|
||||
? _setSpeakerphoneOn
|
||||
: null,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
icon: Icon(
|
||||
@@ -344,18 +344,18 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
),
|
||||
tooltip: 'callSpeakerphoneToggle'.tr,
|
||||
),
|
||||
if (participant.isScreenShareEnabled())
|
||||
if (_participant.isScreenShareEnabled())
|
||||
IconButton(
|
||||
icon: const Icon(Icons.monitor_outlined),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => disableScreenShare(),
|
||||
onPressed: () => _disableScreenShare(),
|
||||
tooltip: 'callScreenOff'.tr,
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: const Icon(Icons.monitor),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => enableScreenShare(),
|
||||
onPressed: () => _enableScreenShare(),
|
||||
tooltip: 'callScreenOn'.tr,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -201,7 +201,7 @@ class InteractiveParticipantWidget extends StatelessWidget {
|
||||
final double? width;
|
||||
final double? height;
|
||||
final Color? color;
|
||||
final bool isFixed;
|
||||
final bool isFixedAvatar;
|
||||
final ParticipantTrack participant;
|
||||
final Function() onTap;
|
||||
|
||||
@@ -210,35 +210,32 @@ class InteractiveParticipantWidget extends StatelessWidget {
|
||||
this.width,
|
||||
this.height,
|
||||
this.color,
|
||||
this.isFixed = false,
|
||||
this.isFixedAvatar = false,
|
||||
required this.participant,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
child: Container(
|
||||
width: width,
|
||||
height: height,
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
return GestureDetector(
|
||||
child: Container(
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
child: ParticipantWidget.widgetFor(participant, isFixed: isFixedAvatar),
|
||||
),
|
||||
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:get/get.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
|
||||
class ParticipantInfoWidget extends StatelessWidget {
|
||||
@@ -34,38 +35,42 @@ class ParticipantInfoWidget extends StatelessWidget {
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
isScreenShare
|
||||
? const Padding(
|
||||
padding: EdgeInsets.only(left: 5),
|
||||
child: Icon(
|
||||
Icons.monitor,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
? const Icon(
|
||||
Icons.monitor,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(left: 5),
|
||||
child: Icon(
|
||||
audioAvailable ? Icons.mic : Icons.mic_off,
|
||||
color: audioAvailable ? Colors.white : Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
: Icon(
|
||||
audioAvailable ? Icons.mic : Icons.mic_off,
|
||||
color: audioAvailable ? Colors.white : Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
if (connectionQuality != ConnectionQuality.unknown)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 5),
|
||||
child: Icon(
|
||||
connectionQuality == ConnectionQuality.poor
|
||||
? Icons.wifi_off_outlined
|
||||
: Icons.wifi,
|
||||
color: {
|
||||
ConnectionQuality.excellent: Colors.green,
|
||||
ConnectionQuality.good: Colors.orange,
|
||||
ConnectionQuality.poor: Colors.red,
|
||||
}[connectionQuality],
|
||||
size: 16,
|
||||
Icon(
|
||||
{
|
||||
ConnectionQuality.excellent: Icons.signal_cellular_alt,
|
||||
ConnectionQuality.good: Icons.signal_cellular_alt_2_bar,
|
||||
ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar,
|
||||
}[connectionQuality],
|
||||
color: {
|
||||
ConnectionQuality.excellent: Colors.green,
|
||||
ConnectionQuality.good: Colors.orange,
|
||||
ConnectionQuality.poor: Colors.red,
|
||||
}[connectionQuality],
|
||||
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) {
|
||||
final ChatCallProvider provider = Get.find();
|
||||
|
||||
if (provider.current.value == null || provider.channel.value == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return Obx(() {
|
||||
if (provider.current.value == null || provider.channel.value == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
leading: const Icon(Icons.call),
|
||||
title: Text(provider.channel.value!.name),
|
||||
subtitle: Text('callAlreadyOngoing'.tr),
|
||||
onTap: () {
|
||||
provider.gotoScreen(context);
|
||||
},
|
||||
);
|
||||
return ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
leading: const Icon(Icons.call),
|
||||
title: Text(provider.channel.value!.name),
|
||||
subtitle: Text('callAlreadyOngoing'.tr),
|
||||
onTap: () {
|
||||
provider.gotoScreen(context);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,33 @@ class ChatEvent extends StatelessWidget {
|
||||
return '$negativeSign${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds';
|
||||
}
|
||||
|
||||
Widget _buildAttachment(BuildContext context) {
|
||||
Widget _buildAttachment(BuildContext context, {bool isMinimal = false}) {
|
||||
final attachments = item.body['attachments'] != null
|
||||
? List<int>.from(item.body['attachments'].map((x) => x))
|
||||
: List<int>.empty();
|
||||
|
||||
if (attachments.isEmpty) return const SizedBox();
|
||||
|
||||
if (isMinimal) {
|
||||
final unFocusColor =
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.attachment,
|
||||
size: 18,
|
||||
color: unFocusColor,
|
||||
).paddingOnly(right: 6),
|
||||
Text(
|
||||
'attachmentHint'.trParams(
|
||||
{'count': attachments.length.toString()},
|
||||
),
|
||||
style: TextStyle(color: unFocusColor),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
key: Key('m${item.uuid}attachments-box'),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
@@ -92,7 +112,9 @@ class ChatEvent extends StatelessWidget {
|
||||
case 'messages.edit':
|
||||
return ChatEventMessageActionLog(
|
||||
icon: const Icon(Icons.edit_note, size: 16),
|
||||
text: 'messageEditDesc'.trParams({'id': '#${item.id}'}),
|
||||
text: 'messageEditDesc'.trParams({
|
||||
'id': '#${item.body['related_event']}',
|
||||
}),
|
||||
isMerged: isMerged,
|
||||
isHasMerged: isHasMerged,
|
||||
isQuote: isQuote,
|
||||
@@ -100,7 +122,9 @@ class ChatEvent extends StatelessWidget {
|
||||
case 'messages.delete':
|
||||
return ChatEventMessageActionLog(
|
||||
icon: const Icon(Icons.cancel_schedule_send, size: 16),
|
||||
text: 'messageDeleteDesc'.trParams({'id': '#${item.id}'}),
|
||||
text: 'messageDeleteDesc'.trParams({
|
||||
'id': '#${item.body['related_event']}',
|
||||
}),
|
||||
isMerged: isMerged,
|
||||
isHasMerged: isHasMerged,
|
||||
isQuote: isQuote,
|
||||
@@ -164,7 +188,8 @@ class ChatEvent extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
).paddingOnly(right: 12),
|
||||
_buildAttachment(context),
|
||||
_buildAttachment(context, isMinimal: isContentPreviewing)
|
||||
.paddingOnly(left: isContentPreviewing ? 12 : 0),
|
||||
],
|
||||
);
|
||||
} else if (isQuote) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/controllers/chat_events_controller.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
@@ -23,7 +24,7 @@ class ChatEventList extends StatelessWidget {
|
||||
required this.onReply,
|
||||
});
|
||||
|
||||
bool checkMessageMergeable(Event? a, Event? b) {
|
||||
bool _checkMessageMergeable(Event? a, Event? b) {
|
||||
if (a == null || b == null) return false;
|
||||
if (a.sender.account.id != b.sender.account.id) return false;
|
||||
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
||||
@@ -41,13 +42,13 @@ class ChatEventList extends StatelessWidget {
|
||||
itemBuilder: (context, index) {
|
||||
bool isMerged = false, hasMerged = false;
|
||||
if (index > 0) {
|
||||
hasMerged = checkMessageMergeable(
|
||||
hasMerged = _checkMessageMergeable(
|
||||
chatController.currentEvents[index - 1].data,
|
||||
chatController.currentEvents[index].data,
|
||||
);
|
||||
}
|
||||
if (index + 1 < chatController.currentEvents.length) {
|
||||
isMerged = checkMessageMergeable(
|
||||
isMerged = _checkMessageMergeable(
|
||||
chatController.currentEvents[index].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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -24,7 +24,8 @@ class ChatEventMessage extends StatelessWidget {
|
||||
final hasAttachment = body.attachments?.isNotEmpty ?? false;
|
||||
|
||||
if (body.text.isEmpty && hasAttachment) {
|
||||
final unFocusColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
final unFocusColor =
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
@@ -33,7 +34,7 @@ class ChatEventMessage extends StatelessWidget {
|
||||
color: unFocusColor,
|
||||
).paddingOnly(right: 6),
|
||||
Text(
|
||||
'postAttachmentTip'.trParams(
|
||||
'attachmentHint'.trParams(
|
||||
{'count': body.attachments?.length.toString() ?? 0.toString()},
|
||||
),
|
||||
style: TextStyle(color: unFocusColor),
|
||||
@@ -46,9 +47,7 @@ class ChatEventMessage extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context) {
|
||||
if (isContentPreviewing) {
|
||||
return _buildContent(context);
|
||||
} else if (isMerged) {
|
||||
if (isMerged) {
|
||||
return _buildContent(context).paddingOnly(left: 52);
|
||||
} else {
|
||||
return _buildContent(context);
|
||||
@@ -64,7 +63,7 @@ class ChatEventMessage extends StatelessWidget {
|
||||
left: isQuote ? 0 : 12,
|
||||
right: isQuote ? 0 : 12,
|
||||
top: body.quoteEvent == null ? 2 : 0,
|
||||
bottom: hasAttachment ? 4 : (isHasMerged ? 2 : 0),
|
||||
bottom: hasAttachment && !isContentPreviewing ? 4 : (isHasMerged ? 2 : 0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/event.dart';
|
||||
import 'package:solian/providers/attachment_uploader.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_editor.dart';
|
||||
import 'package:solian/widgets/chat/chat_event.dart';
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ChatMessageInput extends StatefulWidget {
|
||||
@@ -38,47 +40,92 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
List<int> _attachments = List.empty(growable: true);
|
||||
final List<int> _attachments = List.empty(growable: true);
|
||||
|
||||
Event? _editTo;
|
||||
Event? _replyTo;
|
||||
|
||||
void showAttachments() {
|
||||
void _editAttachments() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => AttachmentEditorPopup(
|
||||
usage: 'm.attachment',
|
||||
current: _attachments,
|
||||
onUpdate: (value) => _attachments = value,
|
||||
initialAttachments: _attachments,
|
||||
onAdd: (value) {
|
||||
setState(() {
|
||||
_attachments.add(value);
|
||||
});
|
||||
},
|
||||
onRemove: (value) {
|
||||
setState(() {
|
||||
_attachments.remove(value);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> sendMessage() async {
|
||||
List<String> _findMentionedUsers(String text) {
|
||||
RegExp regExp = RegExp(r'@[a-zA-Z0-9_]+');
|
||||
Iterable<RegExpMatch> matches = regExp.allMatches(text);
|
||||
|
||||
List<String> mentionedUsers =
|
||||
matches.map((match) => match.group(0)!.substring(1)).toList();
|
||||
|
||||
return mentionedUsers;
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
_focusNode.requestFocus();
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
final prof = auth.userProfile.value!;
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final AttachmentUploaderController uploader = Get.find();
|
||||
if (uploader.queueOfUpload.any(
|
||||
((x) => x.usage == 'm.attachment' && x.isUploading),
|
||||
)) {
|
||||
context.showErrorDialog('attachmentUploadInProgress'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO Deal with the @ ping (query uid with username), and then add into related_user and replace the @ with internal link in body
|
||||
Response resp;
|
||||
|
||||
final mentionedUserNames = _findMentionedUsers(_textController.text);
|
||||
final mentionedUserIds = List<int>.empty(growable: true);
|
||||
|
||||
var client = auth.configureClient('auth');
|
||||
if (mentionedUserNames.isNotEmpty) {
|
||||
resp = await client.get('/users?name=${mentionedUserNames.join(',')}');
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
return;
|
||||
} else {
|
||||
mentionedUserIds.addAll(
|
||||
resp.body.map((x) => Account.fromJson(x).id).toList().cast<int>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
client = auth.configureClient('messaging');
|
||||
|
||||
const uuid = Uuid();
|
||||
final payload = {
|
||||
'uuid': uuid.v4(),
|
||||
'type': _editTo == null ? 'messages.new' : 'messages.edit',
|
||||
'body': {
|
||||
'text': _textController.value.text,
|
||||
'text': _textController.text,
|
||||
'algorithm': 'plain',
|
||||
'attachments': List.from(_attachments),
|
||||
'related_users': [
|
||||
if (_replyTo != null) _replyTo!.sender.accountId,
|
||||
...mentionedUserIds,
|
||||
],
|
||||
if (_replyTo != null) 'quote_event': _replyTo!.id,
|
||||
if (_editTo != null) 'related_event': _editTo!.id,
|
||||
if (_editTo != null && _editTo!.body['quote_event'] != null)
|
||||
'quote_event': _editTo!.body['quote_event'],
|
||||
}
|
||||
};
|
||||
|
||||
@@ -111,7 +158,6 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
|
||||
resetInput();
|
||||
|
||||
Response resp;
|
||||
if (_editTo != null) {
|
||||
resp = await client.put(
|
||||
'/channels/${widget.realm}/${widget.channel.alias}/messages/${_editTo!.id}',
|
||||
@@ -217,20 +263,31 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
{'channel': '#${widget.channel.alias}'},
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => sendMessage(),
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
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,
|
||||
onPressed: () => showAttachments(),
|
||||
onPressed: () => _editAttachments(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () => sendMessage(),
|
||||
onPressed: () => _sendMessage(),
|
||||
)
|
||||
],
|
||||
).paddingOnly(left: 20, right: 16),
|
||||
|
||||
@@ -19,7 +19,9 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
data: content,
|
||||
padding: EdgeInsets.zero,
|
||||
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith(
|
||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
||||
Theme.of(context),
|
||||
).copyWith(
|
||||
horizontalRuleDecoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:solian/models/account_status.dart';
|
||||
import 'package:solian/providers/account_status.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/shells/root_shell.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
@@ -29,7 +30,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
|
||||
late final ChannelProvider _channels;
|
||||
|
||||
void getStatus() async {
|
||||
Future<void> _getStatus() async {
|
||||
final StatusProvider provider = Get.find();
|
||||
|
||||
final resp = await provider.getCurrentStatus();
|
||||
@@ -40,7 +41,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
});
|
||||
}
|
||||
|
||||
void detectSelectedIndex() {
|
||||
void _detectSelectedIndex() {
|
||||
if (widget.routeName == null) return;
|
||||
|
||||
final nameList = AppNavigation.destinations.map((x) => x.page).toList();
|
||||
@@ -49,22 +50,32 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
_selectedIndex = idx != -1 ? idx : null;
|
||||
}
|
||||
|
||||
void closeDrawer() {
|
||||
void _closeDrawer() {
|
||||
rootScaffoldKey.currentState!.closeDrawer();
|
||||
}
|
||||
|
||||
Widget _buildSettingButton() {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () {
|
||||
AppRouter.instance.pushNamed('settings');
|
||||
setState(() => _selectedIndex = null);
|
||||
_closeDrawer();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_channels = Get.find();
|
||||
detectSelectedIndex();
|
||||
getStatus();
|
||||
_detectSelectedIndex();
|
||||
_getStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
detectSelectedIndex();
|
||||
_detectSelectedIndex();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -78,7 +89,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
onDestinationSelected: (idx) {
|
||||
setState(() => _selectedIndex = idx);
|
||||
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page);
|
||||
closeDrawer();
|
||||
_closeDrawer();
|
||||
},
|
||||
children: [
|
||||
Obx(() {
|
||||
@@ -88,10 +99,11 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
leading: const Icon(Icons.account_circle),
|
||||
title: Text('guest'.tr),
|
||||
subtitle: Text('unsignedIn'.tr),
|
||||
trailing: _buildSettingButton(),
|
||||
onTap: () {
|
||||
AppRouter.instance.goNamed('account');
|
||||
setState(() => _selectedIndex = null);
|
||||
closeDrawer();
|
||||
_closeDrawer();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -118,43 +130,55 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
);
|
||||
},
|
||||
),
|
||||
leading: Builder(builder: (context) {
|
||||
final badgeColor = _accountStatus != null
|
||||
leading: Obx(() {
|
||||
final statusBadgeColor = _accountStatus != null
|
||||
? StatusProvider.determineStatus(
|
||||
_accountStatus!,
|
||||
).$2
|
||||
: Colors.grey;
|
||||
|
||||
final RelationshipProvider relations = Get.find();
|
||||
final accountNotifications = relations.friendRequestCount.value;
|
||||
|
||||
return badges.Badge(
|
||||
showBadge: _accountStatus != null,
|
||||
badgeStyle: badges.BadgeStyle(badgeColor: badgeColor),
|
||||
position: badges.BadgePosition.bottomEnd(
|
||||
bottom: 0,
|
||||
end: -2,
|
||||
badgeContent: Text(
|
||||
accountNotifications.toString(),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
child: AccountAvatar(
|
||||
content: auth.userProfile.value!['avatar'],
|
||||
showBadge: accountNotifications > 0,
|
||||
position: badges.BadgePosition.topEnd(
|
||||
top: -10,
|
||||
end: -6,
|
||||
),
|
||||
child: badges.Badge(
|
||||
showBadge: _accountStatus != null,
|
||||
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
|
||||
position: badges.BadgePosition.bottomEnd(
|
||||
bottom: 0,
|
||||
end: -2,
|
||||
),
|
||||
child: AccountAvatar(
|
||||
content: auth.userProfile.value!['avatar'],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.face_retouching_natural),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => AccountStatusAction(
|
||||
currentStatus: _accountStatus!.status,
|
||||
),
|
||||
).then((val) {
|
||||
if (val == true) getStatus();
|
||||
});
|
||||
},
|
||||
),
|
||||
trailing: _buildSettingButton(),
|
||||
onTap: () {
|
||||
AppRouter.instance.goNamed('account');
|
||||
setState(() => _selectedIndex = null);
|
||||
closeDrawer();
|
||||
_closeDrawer();
|
||||
},
|
||||
onLongPress: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => AccountStatusAction(
|
||||
currentStatus: _accountStatus!.status,
|
||||
),
|
||||
).then((val) {
|
||||
if (val == true) _getStatus();
|
||||
});
|
||||
},
|
||||
);
|
||||
}).paddingOnly(top: 8),
|
||||
@@ -199,7 +223,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
useReplace: true,
|
||||
onSelected: (_) {
|
||||
setState(() => _selectedIndex = null);
|
||||
closeDrawer();
|
||||
_closeDrawer();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -16,8 +16,7 @@ class PostEditorCategoriesDialog extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TagsField(
|
||||
initialTags:
|
||||
controller.editTo.value?.tags?.map((x) => x.alias).toList(),
|
||||
initialTags: controller.tags,
|
||||
hintText: 'postTagsPlaceholder'.tr,
|
||||
onUpdate: (value) {
|
||||
controller.tags.value = value;
|
||||
|
||||
118
lib/widgets/posts/editor/post_editor_date.dart
Normal file
118
lib/widgets/posts/editor/post_editor_date.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:solian/controllers/post_editor_controller.dart';
|
||||
|
||||
class PostEditorDateDialog extends StatefulWidget {
|
||||
final PostEditorController controller;
|
||||
|
||||
const PostEditorDateDialog({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
State<PostEditorDateDialog> createState() => _PostEditorDateDialogState();
|
||||
}
|
||||
|
||||
class _PostEditorDateDialogState extends State<PostEditorDateDialog> {
|
||||
final TextEditingController _publishedAtController = TextEditingController();
|
||||
final TextEditingController _publishedUntilController =
|
||||
TextEditingController();
|
||||
|
||||
final _dateFormatter = DateFormat('yyyy-MM-dd HH:mm:ss');
|
||||
|
||||
void _selectDate(int mode) async {
|
||||
final initial = mode == 0
|
||||
? widget.controller.publishedAt.value
|
||||
: widget.controller.publishedUntil.value;
|
||||
final DateTime? pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: initial?.toLocal(),
|
||||
firstDate: DateTime(DateTime.now().year),
|
||||
lastDate: DateTime(DateTime.now().year + 5),
|
||||
);
|
||||
if (pickedDate == null) return;
|
||||
final TimeOfDay? pickedTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.now(),
|
||||
);
|
||||
if (pickedTime == null) return;
|
||||
final picked = pickedDate.copyWith(
|
||||
hour: pickedTime.hour,
|
||||
minute: pickedTime.minute,
|
||||
);
|
||||
if (mode == 0) {
|
||||
setState(() {
|
||||
widget.controller.publishedAt.value = picked;
|
||||
_publishedAtController.text = _dateFormatter.format(picked);
|
||||
});
|
||||
} else {
|
||||
widget.controller.publishedUntil.value = pickedDate;
|
||||
_publishedUntilController.text = _dateFormatter.format(picked);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.controller.publishedAt.value != null) {
|
||||
_publishedAtController.text =
|
||||
_dateFormatter.format(widget.controller.publishedAt.value!);
|
||||
}
|
||||
if (widget.controller.publishedUntil.value != null) {
|
||||
_publishedUntilController.text =
|
||||
_dateFormatter.format(widget.controller.publishedUntil.value!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_publishedAtController.dispose();
|
||||
_publishedUntilController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('postPublishDate'.tr),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _publishedAtController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'postPublishAt'.tr,
|
||||
),
|
||||
onTap: () => _selectDate(0),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _publishedUntilController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'postPublishedUntil'.tr,
|
||||
),
|
||||
onTap: () => _selectDate(1),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
widget.controller.publishedAt.value = null;
|
||||
widget.controller.publishedUntil.value = null;
|
||||
_publishedAtController.clear();
|
||||
_publishedUntilController.clear();
|
||||
},
|
||||
child: Text('clear'.tr),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('confirm'.tr),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
70
lib/widgets/posts/editor/post_editor_publish_zone.dart
Normal file
70
lib/widgets/posts/editor/post_editor_publish_zone.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/controllers/post_editor_controller.dart';
|
||||
import 'package:solian/providers/content/realm.dart';
|
||||
|
||||
class PostEditorPublishZoneDialog extends StatelessWidget {
|
||||
final PostEditorController controller;
|
||||
|
||||
const PostEditorPublishZoneDialog({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final RealmProvider realms = Get.find();
|
||||
|
||||
return AlertDialog(
|
||||
title: Text('postPublishZone'.tr),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Obx(() {
|
||||
return DropdownButtonFormField2<int>(
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem<int>(
|
||||
value: null,
|
||||
child: Text(
|
||||
'postPublishZoneNone'.tr,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
...realms.availableRealms.map(
|
||||
(x) => DropdownMenuItem<int>(
|
||||
value: x.id,
|
||||
child: Text(
|
||||
'${x.name} (${x.alias})',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
value: controller.realmZone.value?.id,
|
||||
onChanged: (int? value) {
|
||||
if (value == null) {
|
||||
controller.realmZone.value = null;
|
||||
} else {
|
||||
controller.realmZone.value =
|
||||
realms.availableRealms.firstWhere((x) => x.id == value);
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(height: 20),
|
||||
menuItemStyleData: const MenuItemStyleData(height: 40),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('confirm'.tr),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get_utils/get_utils.dart';
|
||||
import 'package:intl/intl.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_profile_popup.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||
import 'package:solian/widgets/markdown_text_content.dart';
|
||||
import 'package:solian/widgets/feed/feed_tags.dart';
|
||||
import 'package:solian/widgets/posts/post_tags.dart';
|
||||
import 'package:solian/widgets/posts/post_quick_action.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:timeago/timeago.dart' show format;
|
||||
@@ -22,7 +24,8 @@ class PostItem extends StatefulWidget {
|
||||
final bool isShowEmbed;
|
||||
final bool isFullDate;
|
||||
final bool isContentSelectable;
|
||||
final String? overrideAttachmentParent;
|
||||
final String? attachmentParent;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const PostItem({
|
||||
super.key,
|
||||
@@ -34,7 +37,8 @@ class PostItem extends StatefulWidget {
|
||||
this.isShowEmbed = true,
|
||||
this.isFullDate = false,
|
||||
this.isContentSelectable = false,
|
||||
this.overrideAttachmentParent,
|
||||
this.attachmentParent,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -129,7 +133,7 @@ class _PostItemState extends State<PostItem> {
|
||||
List<Widget> widgets = List.empty(growable: true);
|
||||
|
||||
if (widget.item.tags?.isNotEmpty ?? false) {
|
||||
widgets.add(FeedTagsList(tags: widget.item.tags!));
|
||||
widgets.add(PostTagsList(tags: widget.item.tags!));
|
||||
}
|
||||
if (labels.isNotEmpty) {
|
||||
widgets.add(Text(
|
||||
@@ -159,66 +163,92 @@ class _PostItemState extends State<PostItem> {
|
||||
}
|
||||
|
||||
Widget _buildReply(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FaIcon(
|
||||
FontAwesomeIcons.reply,
|
||||
size: 16,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'postRepliedNotify'.trParams(
|
||||
{'username': '@${widget.item.replyTo!.author.name}'},
|
||||
),
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
).paddingOnly(left: 6),
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 12),
|
||||
Card(
|
||||
elevation: 1,
|
||||
child: PostItem(
|
||||
item: widget.item.replyTo!,
|
||||
isCompact: true,
|
||||
overrideAttachmentParent: widget.item.id.toString(),
|
||||
).paddingSymmetric(vertical: 8),
|
||||
return OpenContainer(
|
||||
closedBuilder: (_, openContainer) => Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FaIcon(
|
||||
FontAwesomeIcons.reply,
|
||||
size: 16,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'postRepliedNotify'.trParams(
|
||||
{'username': '@${widget.item.replyTo!.author.name}'},
|
||||
),
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
).paddingOnly(left: 6),
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 12),
|
||||
Card(
|
||||
elevation: 1,
|
||||
child: PostItem(
|
||||
item: widget.item.replyTo!,
|
||||
isCompact: true,
|
||||
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) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FaIcon(
|
||||
FontAwesomeIcons.retweet,
|
||||
size: 16,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'postRepostedNotify'.trParams(
|
||||
{'username': '@${widget.item.repostTo!.author.name}'},
|
||||
),
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
).paddingOnly(left: 6),
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 12),
|
||||
Card(
|
||||
elevation: 1,
|
||||
child: PostItem(
|
||||
item: widget.item.repostTo!,
|
||||
isCompact: true,
|
||||
overrideAttachmentParent: widget.item.id.toString(),
|
||||
).paddingSymmetric(vertical: 8),
|
||||
return OpenContainer(
|
||||
closedBuilder: (_, openContainer) => Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FaIcon(
|
||||
FontAwesomeIcons.retweet,
|
||||
size: 16,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'postRepostedNotify'.trParams(
|
||||
{'username': '@${widget.item.repostTo!.author.name}'},
|
||||
),
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
).paddingOnly(left: 6),
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 12),
|
||||
Card(
|
||||
elevation: 1,
|
||||
child: PostItem(
|
||||
item: widget.item.repostTo!,
|
||||
isCompact: true,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -253,7 +283,7 @@ class _PostItemState extends State<PostItem> {
|
||||
color: _unFocusColor,
|
||||
).paddingOnly(right: 6),
|
||||
Text(
|
||||
'postAttachmentTip'.trParams(
|
||||
'attachmentHint'.trParams(
|
||||
{'count': attachments.length.toString()},
|
||||
),
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
@@ -264,100 +294,91 @@ class _PostItemState extends State<PostItem> {
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: AccountAvatar(content: item.author.avatar.toString()),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountProfilePopup(
|
||||
account: item.author,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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(),
|
||||
},
|
||||
);
|
||||
},
|
||||
return OpenContainer(
|
||||
closedBuilder: (_, openContainer) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: AccountAvatar(content: item.author.avatar.toString()),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountProfilePopup(
|
||||
account: item.author,
|
||||
),
|
||||
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),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
).paddingOnly(
|
||||
top: 10,
|
||||
bottom: hasAttachment ? 10 : 0,
|
||||
right: 16,
|
||||
left: 16,
|
||||
),
|
||||
AttachmentList(
|
||||
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;
|
||||
});
|
||||
},
|
||||
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)
|
||||
_buildReply(context).paddingOnly(top: 4),
|
||||
if (widget.item.repostTo != null && widget.isShowEmbed)
|
||||
_buildRepost(context).paddingOnly(top: 4),
|
||||
_buildFooter().paddingOnly(left: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingOnly(
|
||||
top: hasAttachment ? 10 : 6,
|
||||
left: hasAttachment ? 24 : 60,
|
||||
top: 10,
|
||||
bottom: hasAttachment ? 10 : 0,
|
||||
right: 16,
|
||||
bottom: 10,
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
left: 16,
|
||||
),
|
||||
AttachmentList(
|
||||
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:solian/models/post.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_item.dart';
|
||||
|
||||
@@ -13,6 +11,7 @@ class PostListWidget extends StatelessWidget {
|
||||
final bool isClickable;
|
||||
final bool isNestedClickable;
|
||||
final PagingController<int, Post> controller;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const PostListWidget({
|
||||
super.key,
|
||||
@@ -20,6 +19,7 @@ class PostListWidget extends StatelessWidget {
|
||||
this.isShowEmbed = true,
|
||||
this.isClickable = true,
|
||||
this.isNestedClickable = true,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -29,16 +29,15 @@ class PostListWidget extends StatelessWidget {
|
||||
pagingController: controller,
|
||||
builderDelegate: PagedChildBuilderDelegate<Post>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return CenteredContainer(
|
||||
child: PostListEntryWidget(
|
||||
isShowEmbed: isShowEmbed,
|
||||
isNestedClickable: isNestedClickable,
|
||||
isClickable: isClickable,
|
||||
item: item,
|
||||
onUpdate: () {
|
||||
controller.refresh();
|
||||
},
|
||||
),
|
||||
return PostListEntryWidget(
|
||||
isShowEmbed: isShowEmbed,
|
||||
isNestedClickable: isNestedClickable,
|
||||
isClickable: isClickable,
|
||||
item: item,
|
||||
backgroundColor: backgroundColor,
|
||||
onUpdate: () {
|
||||
controller.refresh();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -48,19 +47,23 @@ class PostListWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
class PostListEntryWidget extends StatelessWidget {
|
||||
final int renderOrder;
|
||||
final bool isShowEmbed;
|
||||
final bool isNestedClickable;
|
||||
final bool isClickable;
|
||||
final Post item;
|
||||
final Function onUpdate;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const PostListEntryWidget({
|
||||
super.key,
|
||||
this.renderOrder = 0,
|
||||
required this.isShowEmbed,
|
||||
required this.isNestedClickable,
|
||||
required this.isClickable,
|
||||
required this.item,
|
||||
required this.onUpdate,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -71,14 +74,8 @@ class PostListEntryWidget extends StatelessWidget {
|
||||
item: item,
|
||||
isShowEmbed: isShowEmbed,
|
||||
isClickable: isNestedClickable,
|
||||
backgroundColor: backgroundColor,
|
||||
).paddingSymmetric(vertical: 8),
|
||||
onTap: () {
|
||||
if (!isClickable) return;
|
||||
AppRouter.instance.pushNamed(
|
||||
'postDetail',
|
||||
pathParameters: {'id': item.id.toString()},
|
||||
);
|
||||
},
|
||||
onLongPress: () {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
@@ -85,65 +85,65 @@ class _PostQuickActionState extends State<PostQuickAction> {
|
||||
Widget build(BuildContext context) {
|
||||
const density = VisualDensity(horizontal: -4, vertical: -3);
|
||||
|
||||
final reactionEntries = widget.item.metric!.reactionList.entries.toList();
|
||||
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
child: CustomScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
slivers: [
|
||||
if (widget.isReactable && widget.isShowReply)
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.comment),
|
||||
label: Text(widget.item.metric!.replyCount.toString()),
|
||||
visualDensity: density,
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return PostReplyListPopup(item: widget.item);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (widget.isReactable && widget.isShowReply)
|
||||
const VerticalDivider(
|
||||
thickness: 0.3,
|
||||
width: 0.3,
|
||||
indent: 8,
|
||||
endIndent: 8,
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
...widget.item.metric!.reactionList.entries.map((x) {
|
||||
final info = reactions[x.key];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ActionChip(
|
||||
avatar: Text(info!.icon),
|
||||
label: Text(x.value.toString()),
|
||||
tooltip: ':${x.key}:',
|
||||
visualDensity: density,
|
||||
onPressed: _isSubmitting
|
||||
? null
|
||||
: () => doWidgetReact(x.key, info.attitude),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: ActionChip(
|
||||
avatar: const Icon(Icons.comment),
|
||||
label: Text(widget.item.metric!.replyCount.toString()),
|
||||
visualDensity: density,
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return PostReplyListPopup(item: widget.item);
|
||||
},
|
||||
);
|
||||
}),
|
||||
if (widget.isReactable)
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.add_reaction, color: Colors.teal),
|
||||
label: Text('reactAdd'.tr),
|
||||
visualDensity: density,
|
||||
onPressed: () => showReactMenu(),
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
),
|
||||
if (widget.isReactable && widget.isShowReply)
|
||||
SliverToBoxAdapter(
|
||||
child: const VerticalDivider(
|
||||
thickness: 0.3,
|
||||
width: 0.3,
|
||||
indent: 8,
|
||||
endIndent: 8,
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
),
|
||||
SliverList.builder(
|
||||
itemCount: reactionEntries.length,
|
||||
itemBuilder: (context, index) {
|
||||
final x = reactionEntries[index];
|
||||
final info = reactions[x.key];
|
||||
return ActionChip(
|
||||
avatar: Text(info!.icon),
|
||||
label: Text(x.value.toString()),
|
||||
tooltip: ':${x.key}:',
|
||||
visualDensity: density,
|
||||
onPressed: _isSubmitting
|
||||
? null
|
||||
: () => doWidgetReact(x.key, info.attitude),
|
||||
).paddingOnly(right: 8);
|
||||
},
|
||||
),
|
||||
if (widget.isReactable)
|
||||
SliverToBoxAdapter(
|
||||
child: ActionChip(
|
||||
avatar: const Icon(Icons.add_reaction, color: Colors.teal),
|
||||
label: Text('reactAdd'.tr),
|
||||
visualDensity: density,
|
||||
onPressed: () => showReactMenu(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -8,10 +8,12 @@ import 'package:solian/widgets/posts/post_list.dart';
|
||||
|
||||
class PostReplyList extends StatefulWidget {
|
||||
final Post item;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const PostReplyList({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -45,7 +47,6 @@ class _PostReplyListState extends State<PostReplyList> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener(getReplies);
|
||||
}
|
||||
|
||||
@@ -54,6 +55,7 @@ class _PostReplyListState extends State<PostReplyList> {
|
||||
return PostListWidget(
|
||||
isShowEmbed: false,
|
||||
controller: _pagingController,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -74,7 +76,12 @@ class PostReplyListPopup extends StatelessWidget {
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
Expanded(
|
||||
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,
|
||||
isNestedClickable: true,
|
||||
onUpdate: onUpdate,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:solian/models/feed.dart';
|
||||
import 'package:solian/router.dart';
|
||||
|
||||
class FeedTagsList extends StatelessWidget {
|
||||
class PostTagsList extends StatelessWidget {
|
||||
final List<Tag> tags;
|
||||
|
||||
const FeedTagsList({
|
||||
const PostTagsList({
|
||||
super.key,
|
||||
required this.tags,
|
||||
});
|
||||
@@ -3,14 +3,14 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/widgets/posts/post_list.dart';
|
||||
|
||||
class FeedListWidget extends StatelessWidget {
|
||||
class PostWarpedListWidget extends StatelessWidget {
|
||||
final bool isShowEmbed;
|
||||
final bool isClickable;
|
||||
final bool isNestedClickable;
|
||||
final bool isPinned;
|
||||
final PagingController<int, Post> controller;
|
||||
|
||||
const FeedListWidget({
|
||||
const PostWarpedListWidget({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.isShowEmbed = true,
|
||||
@@ -30,6 +30,7 @@ class FeedListWidget extends StatelessWidget {
|
||||
return const SizedBox();
|
||||
}
|
||||
return PostListEntryWidget(
|
||||
renderOrder: index,
|
||||
isShowEmbed: isShowEmbed,
|
||||
isNestedClickable: isNestedClickable,
|
||||
isClickable: isClickable,
|
||||
@@ -13,6 +13,7 @@ import firebase_core
|
||||
import firebase_messaging
|
||||
import flutter_secure_storage_macos
|
||||
import flutter_webrtc
|
||||
import gal
|
||||
import livekit_client
|
||||
import macos_window_utils
|
||||
import media_kit_libs_macos_video
|
||||
@@ -38,6 +39,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
||||
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
|
||||
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
|
||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||
|
||||
@@ -8,20 +8,20 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- file_selector_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- Firebase/CoreOnly (10.28.1):
|
||||
- FirebaseCore (= 10.28.1)
|
||||
- Firebase/Messaging (10.28.1):
|
||||
- Firebase/CoreOnly (10.29.0):
|
||||
- FirebaseCore (= 10.29.0)
|
||||
- Firebase/Messaging (10.29.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 10.28.0)
|
||||
- firebase_core (3.2.0):
|
||||
- Firebase/CoreOnly (~> 10.28.0)
|
||||
- FirebaseMessaging (~> 10.29.0)
|
||||
- firebase_core (3.3.0):
|
||||
- Firebase/CoreOnly (~> 10.29.0)
|
||||
- FlutterMacOS
|
||||
- firebase_messaging (15.0.3):
|
||||
- Firebase/CoreOnly (~> 10.28.0)
|
||||
- Firebase/Messaging (~> 10.28.0)
|
||||
- firebase_messaging (15.0.4):
|
||||
- Firebase/CoreOnly (~> 10.29.0)
|
||||
- Firebase/Messaging (~> 10.29.0)
|
||||
- firebase_core
|
||||
- FlutterMacOS
|
||||
- FirebaseCore (10.28.1):
|
||||
- FirebaseCore (10.29.0):
|
||||
- FirebaseCoreInternal (~> 10.0)
|
||||
- GoogleUtilities/Environment (~> 7.12)
|
||||
- GoogleUtilities/Logger (~> 7.12)
|
||||
@@ -32,7 +32,7 @@ PODS:
|
||||
- GoogleUtilities/Environment (~> 7.8)
|
||||
- GoogleUtilities/UserDefaults (~> 7.8)
|
||||
- PromisesObjC (~> 2.1)
|
||||
- FirebaseMessaging (10.28.0):
|
||||
- FirebaseMessaging (10.29.0):
|
||||
- FirebaseCore (~> 10.0)
|
||||
- FirebaseInstallations (~> 10.0)
|
||||
- GoogleDataTransport (~> 9.3)
|
||||
@@ -47,6 +47,9 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 125.6422.04)
|
||||
- FlutterMacOS (1.0.0)
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleDataTransport (9.4.1):
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
- nanopb (< 2.30911.0, >= 2.30908.0)
|
||||
@@ -111,6 +114,9 @@ PODS:
|
||||
- Sentry/HybridSDK (= 8.32.0)
|
||||
- share_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -130,6 +136,7 @@ DEPENDENCIES:
|
||||
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
||||
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
||||
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
||||
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
|
||||
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
||||
@@ -142,6 +149,7 @@ DEPENDENCIES:
|
||||
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
|
||||
- sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`)
|
||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
@@ -179,6 +187,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
gal:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
|
||||
livekit_client:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
|
||||
macos_window_utils:
|
||||
@@ -203,6 +213,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos
|
||||
share_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||
shared_preferences_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
sqflite:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
|
||||
url_launcher_macos:
|
||||
@@ -215,16 +227,17 @@ SPEC CHECKSUMS:
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
|
||||
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
|
||||
Firebase: 49e62242b3ae422a002ab647a7e62a332a8c3ec1
|
||||
firebase_core: d8af40a9c8a9ce3112a94692aac83675627c0486
|
||||
firebase_messaging: 7871cfa8af1e863324e46ae9e90343c452626c02
|
||||
FirebaseCore: dfc33f0dffba05f76181da9cc0151171ebb3bd10
|
||||
Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d
|
||||
firebase_core: 73185b844efc8a534e5744d68152e75e740922d2
|
||||
firebase_messaging: 167fdd90971720e0b62ccd6fa8d430b8af4ca6e9
|
||||
FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16
|
||||
FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934
|
||||
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
|
||||
FirebaseMessaging: 087a7c7cadef7b9239f005bc4db823894844f323
|
||||
FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366
|
||||
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
|
||||
flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||
livekit_client: c24af2b8474a39325596e714118e05551ec5eacc
|
||||
@@ -242,6 +255,7 @@ SPEC CHECKSUMS:
|
||||
Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb
|
||||
sentry_flutter: f1d86adcb93a959bc47a40d8d55059bdf7569bc5
|
||||
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
|
||||
232
pubspec.lock
232
pubspec.lock
@@ -13,10 +13,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: b46f62516902afb04befa4b30eb6a12ac1f58ca8cb25fb9d632407259555dd3d
|
||||
sha256: b1595874fbc8f7a50da90f5d8f327bb0bfd6a95dc906c390efe991540c3b54aa
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.39"
|
||||
version: "1.3.40"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -25,6 +25,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
animations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: animations
|
||||
sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.11"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -42,13 +50,21 @@ packages:
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -133,26 +149,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cached_network_image
|
||||
sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f"
|
||||
sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
version: "3.4.0"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_platform_interface
|
||||
sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f"
|
||||
sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "4.1.0"
|
||||
cached_network_image_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7"
|
||||
sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
carousel_slider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -245,10 +261,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32"
|
||||
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.4+1"
|
||||
version: "0.3.4+2"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -301,10 +317,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dev_build
|
||||
sha256: "5600100e28f7424ed53728e8e7aa6bc0e0506ec04bb49a82616f62112c1822c3"
|
||||
sha256: f526d1fbe68875f6119ffc333f114dfe6aa93ad04439276d53968f7977cc410e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0+8"
|
||||
version: "1.0.0+11"
|
||||
device_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -321,6 +337,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.0+1"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
dismissible_page:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -337,14 +369,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -413,50 +437,50 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "5159984ce9b70727473eb388394650677c02c925aaa6c9439905e1f30966a4d5"
|
||||
sha256: "3187f4f8e49968573fd7403011dca67ba95aae419bc0d8131500fae160d94f92"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.3.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: "1003a5a03a61fc9a22ef49f37cbcb9e46c86313a7b2e7029b9390cf8c6fc32cb"
|
||||
sha256: "3c3a1e92d6f4916c32deea79c4a7587aa0e9dbbe5889c7a16afcf005a485ee02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
version: "5.2.0"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: "23509cb3cddfb3c910c143279ac3f07f06d3120f7d835e4a5d4b42558e978712"
|
||||
sha256: e8d1e22de72cb21cdcfc5eed7acddab3e99cd83f3b317f54f7a96c32f25fd11e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.17.3"
|
||||
version: "2.17.4"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "156c4292aa63a6a7d508c68ded984cb38730d2823c3265e573cb1e94983e2025"
|
||||
sha256: "1b0a4f9ecbaf9007771bac152afad738ddfacc4b8431a7591c00829480d99553"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.3"
|
||||
version: "15.0.4"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "10408c5ca242b7fc632dd5eab4caf8fdf18ebe88db6052980fa71a18d88bd200"
|
||||
sha256: c5a6443e66ae064fe186901d740ee7ce648ca2a6fd0484b8c5e963849ac0fc28
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.41"
|
||||
version: "4.5.42"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: c7a756e3750679407948de665735e69a368cb902940466e5d68a00ea7aba1aaa
|
||||
sha256: "232ef63b986467ae5b5577a09c2502b26e2e2aebab5b85e6c966a5ca9b038b89"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.8.11"
|
||||
version: "3.8.12"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -522,10 +546,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: ceff65d74d907b1b772e22cf04daad60fb472461638977d9fae8b00a63e01e3d
|
||||
sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.3"
|
||||
version: "3.4.0"
|
||||
flutter_card_swiper:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -652,10 +676,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_webrtc
|
||||
sha256: d305793e6737c59a81c45b18484e1f985710827704eeb9092573387efcbae272
|
||||
sha256: f46bd76cef6e8d787dc707d0c591f0e89c912a2970c7b5e68a55b9cca1bdde4c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.5"
|
||||
version: "0.11.6"
|
||||
font_awesome_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -672,6 +696,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
gal:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: gal
|
||||
sha256: "54c9b72528efce7c66234f3b6dd01cb0304fd8af8196de15571d7bdddb940977"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
get:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -692,10 +724,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "39dd52168d6c59984454183148dc3a5776960c61083adfc708cc79a7b3ce1ba8"
|
||||
sha256: d380de0355788c5c784fe9f81b43fc833b903991c25ecc4e2a416a67faefa722
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.1"
|
||||
version: "14.2.2"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -729,13 +761,37 @@ packages:
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
image:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
image_cropper:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_cropper
|
||||
sha256: "7c3a85e54ec591738ee68f3cc9b8849bab0ec8e908bf8970645be959a079e17f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.1"
|
||||
image_cropper_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_cropper_for_web
|
||||
sha256: "65f3f23fb82ff153601f22864e07e2ec14a7859db9e6ee5f643f0cde5243e9f2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
image_cropper_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_cropper_platform_interface
|
||||
sha256: e8e9d2ca36360387aee39295ce49029362ae4df3071f23e8e71f2b81e40b7531
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -756,10 +812,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3"
|
||||
sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.4"
|
||||
version: "3.0.5"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -884,10 +940,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: livekit_client
|
||||
sha256: e6b1e8a3cdcae95f7e62c0371590648444bac245fce3a1bcfb4ec05889ad82f3
|
||||
sha256: be2a3375851a6147d5de94a870edd6e831ab8d3d793e3563ba1ff1b05490b3de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.2.3"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -912,6 +968,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.2"
|
||||
markdown_toolbar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: markdown_toolbar
|
||||
sha256: "5f4e2548afe96cc2ccdad22da87d380e4726d2d3385ddbb7035aa94daf9a4d47"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1016,6 +1080,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
nm:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1028,10 +1100,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: octo_image
|
||||
sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d"
|
||||
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.1.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1076,10 +1148,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
|
||||
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1272,6 +1344,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1364,18 +1444,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sentry
|
||||
sha256: "60756499f09c3ed944640d7993ac527a89f7c3033f13ec12ae145706b269b5c8"
|
||||
sha256: "76ad4fab90ff82427c26939bd79dc4df345a081e2b1cd5954b947e340b9af9a5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.5.0"
|
||||
version: "8.6.0"
|
||||
sentry_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry_flutter
|
||||
sha256: "26cfe89cb08a60d9bc0c4e748a0508e223ae378265aec8ed2a2b48f0d2c936b9"
|
||||
sha256: a7c92014701093a7c0a373e1a47c54ec428d8468d8bf2b793fee7ea1b085c21b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.5.0"
|
||||
version: "8.6.0"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1396,58 +1476,58 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
|
||||
sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
version: "2.3.1"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "3d4571b3c5eb58ce52a419d86e655493d0bc3020672da79f72fa0c16ca3a8ec1"
|
||||
sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
version: "2.3.0"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
|
||||
sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.5.0"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
|
||||
sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.0"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "034650b71e73629ca08a0bd789fd1d83cc63c2d1e405946f7cef7bc37432f93a"
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.4.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
|
||||
sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.4.1"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
|
||||
sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1613,14 +1693,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1681,10 +1753,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "678979703e10d7862c551c736fe6b9f185261bddf141b46672063b99790bc700"
|
||||
sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.7"
|
||||
version: "6.3.8"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1721,10 +1793,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
|
||||
sha256: a36e2d7981122fa185006b216eb6b5b97ede3f9a54b7a511bc966971ab98d049
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.2"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1825,10 +1897,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
|
||||
sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.1"
|
||||
version: "5.5.3"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
38
pubspec.yaml
38
pubspec.yaml
@@ -2,7 +2,7 @@ name: solian
|
||||
description: "The Solar Network App"
|
||||
publish_to: "none"
|
||||
|
||||
version: 1.2.0+1
|
||||
version: 1.2.0+9
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.4 <4.0.0"
|
||||
@@ -27,7 +27,6 @@ dependencies:
|
||||
crypto: ^3.0.3
|
||||
path: ^1.9.0
|
||||
intl: ^0.19.0
|
||||
image: ^4.1.7
|
||||
font_awesome_flutter: ^10.7.0
|
||||
web_socket_channel: ^3.0.0
|
||||
permission_handler: ^11.3.1
|
||||
@@ -50,7 +49,6 @@ dependencies:
|
||||
media_kit: ^1.1.10+1
|
||||
media_kit_video: ^1.2.4
|
||||
media_kit_libs_video: ^1.0.4
|
||||
textfield_tags: ^3.0.1
|
||||
pasteboard: ^0.2.0
|
||||
desktop_drop: ^0.4.4
|
||||
badges: ^3.1.2
|
||||
@@ -60,7 +58,14 @@ dependencies:
|
||||
flutter_cache_manager: ^3.3.3
|
||||
flutter_markdown_selectionarea: ^0.6.17+1
|
||||
shared_preferences: ^2.2.3
|
||||
easy_debounce: ^2.0.3
|
||||
provider: ^6.1.2
|
||||
gal: ^2.3.0
|
||||
dio: ^5.5.0+1
|
||||
image_cropper: ^8.0.1
|
||||
markdown_toolbar: ^0.5.0
|
||||
animations: ^2.0.11
|
||||
avatar_stack: ^1.2.0
|
||||
async: ^2.11.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -80,6 +85,31 @@ flutter:
|
||||
assets:
|
||||
- assets/logo.png
|
||||
|
||||
fonts:
|
||||
- family: NotoSansEmoji
|
||||
fonts:
|
||||
- asset: assets/fonts/NotoColorEmoji-Regular.ttf
|
||||
- family: NotoSansSC
|
||||
fonts:
|
||||
- asset: assets/fonts/NotoSansSC-Regular.ttf
|
||||
- asset: assets/fonts/NotoSansSC-Bold.ttf
|
||||
weight: 700
|
||||
- family: NotoSansHK
|
||||
fonts:
|
||||
- asset: assets/fonts/NotoSansHK-Regular.ttf
|
||||
- asset: assets/fonts/NotoSansHK-Bold.ttf
|
||||
weight: 700
|
||||
- family: NotoSansJP
|
||||
fonts:
|
||||
- asset: assets/fonts/NotoSansJP-Regular.ttf
|
||||
- asset: assets/fonts/NotoSansJP-Bold.ttf
|
||||
weight: 700
|
||||
- family: Comfortaa
|
||||
fonts:
|
||||
- asset: assets/fonts/Comfortaa-Regular.ttf
|
||||
- asset: assets/fonts/Comfortaa-Bold.ttf
|
||||
weight: 700
|
||||
|
||||
flutter_launcher_icons:
|
||||
android:
|
||||
generate: "launcher_icon"
|
||||
|
||||
108
web/index.html
108
web/index.html
@@ -1,8 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<!--
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
@@ -15,65 +14,68 @@
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
<base href="$FLUTTER_BASE_HREF" />
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible" />
|
||||
<meta name="description" content="A new Flutter project." />
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="solian">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<meta name="apple-mobile-web-app-title" content="solian" />
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<!-- Cropper.js -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.js"></script>
|
||||
|
||||
<!-- Loading styles -->
|
||||
<style>
|
||||
.loader-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
|
||||
.loader {
|
||||
border: 10px solid #f3f3f3;
|
||||
border-top: 10px solid #8f94ca;
|
||||
border-radius: 50%;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
animation: spin .35s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
<!-- Loading styles -->
|
||||
<style>
|
||||
.loader-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
.loader {
|
||||
border: 10px solid #f3f3f3;
|
||||
border-top: 10px solid #8f94ca;
|
||||
border-radius: 50%;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
animation: spin 0.35s linear infinite;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<title>Solian</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
<body>
|
||||
<div class="loader-container">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
</body>
|
||||
<title>Solian</title>
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="loader-container">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <flutter_acrylic/flutter_acrylic_plugin.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
#include <gal/gal_plugin_c_api.h>
|
||||
#include <livekit_client/live_kit_plugin.h>
|
||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||
@@ -39,6 +40,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
FlutterWebRTCPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
||||
GalPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("GalPluginCApi"));
|
||||
LiveKitPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("LiveKitPlugin"));
|
||||
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
||||
|
||||
@@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_acrylic
|
||||
flutter_secure_storage_windows
|
||||
flutter_webrtc
|
||||
gal
|
||||
livekit_client
|
||||
media_kit_libs_windows_video
|
||||
media_kit_video
|
||||
|
||||
Reference in New Issue
Block a user