Compare commits

...

22 Commits

Author SHA1 Message Date
3ac510c4b1 🐛 Bug fixes 2024-08-20 01:19:18 +08:00
253cd1ecbd Call in same screen on large screen 2024-08-20 01:10:15 +08:00
c82c48dfec 🐛 Fix attachments padding 2024-08-19 22:45:22 +08:00
433beec2dd 💄 Optimize large screen ux 2024-08-19 22:38:36 +08:00
3a1e7537dd 🐛 Fix alignment issue 2024-08-19 22:25:49 +08:00
9170ae6be7 💄 Line up attachments & expansion of link 2024-08-19 22:25:17 +08:00
a5ee5b7f09 💄 Better attachment layout 2024-08-19 22:13:25 +08:00
32e6658f3d Better link expand layout on large screen 2024-08-19 20:13:08 +08:00
e45d9b39d5 Post link expand
 Cache link expansion image
2024-08-19 19:56:44 +08:00
cf1cfecb08 Link expand 2024-08-19 19:36:01 +08:00
95ea3e558f 🚀 Launch 1.2.1+18 2024-08-19 09:43:25 +08:00
0006a94632 🐛 Fix local db old data cause crash 2024-08-19 09:19:29 +08:00
7ea18dbe12 💄 Update styles 2024-08-19 01:54:32 +08:00
6004b74724 🚀 Launch 1.2.1+17 2024-08-19 01:35:57 +08:00
4d82ae8058 🐛 Bug fixes
⬆️ Add firebase performance
2024-08-19 01:35:38 +08:00
7fe26d0df0 🚀 Launch 1.2.1+16 2024-08-19 00:33:20 +08:00
80bade0e03 View posts posted by friends 2024-08-19 00:33:03 +08:00
b63db7fe76 👽 Support use realm alias instead of id 2024-08-19 00:14:09 +08:00
49f73f5f04 ⬆️ Support new attachments system 2024-08-18 22:51:52 +08:00
98749f42c0 ⬆️ Upgrade deps 2024-08-17 19:18:51 +08:00
f0e6bd64f4 ♻️ Refactor video player 2024-08-17 19:02:57 +08:00
3bea3a114a Post alias 2024-08-17 18:44:20 +08:00
50 changed files with 1092 additions and 496 deletions

View File

@ -51,6 +51,9 @@ PODS:
- Firebase/Messaging (10.29.0): - Firebase/Messaging (10.29.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 10.29.0) - FirebaseMessaging (~> 10.29.0)
- Firebase/Performance (10.29.0):
- Firebase/CoreOnly
- FirebasePerformance (~> 10.29.0)
- firebase_analytics (11.2.1): - firebase_analytics (11.2.1):
- Firebase/Analytics (= 10.29.0) - Firebase/Analytics (= 10.29.0)
- firebase_core - firebase_core
@ -66,6 +69,12 @@ PODS:
- Firebase/Messaging (= 10.29.0) - Firebase/Messaging (= 10.29.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_performance (0.10.0-4):
- Firebase/Performance (= 10.29.0)
- firebase_core
- Flutter
- FirebaseABTesting (10.29.0):
- FirebaseCore (~> 10.0)
- FirebaseAnalytics (10.29.0): - FirebaseAnalytics (10.29.0):
- FirebaseAnalytics/AdIdSupport (= 10.29.0) - FirebaseAnalytics/AdIdSupport (= 10.29.0)
- FirebaseCore (~> 10.0) - FirebaseCore (~> 10.0)
@ -115,6 +124,25 @@ PODS:
- GoogleUtilities/Reachability (~> 7.8) - GoogleUtilities/Reachability (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8)
- nanopb (< 2.30911.0, >= 2.30908.0) - nanopb (< 2.30911.0, >= 2.30908.0)
- FirebasePerformance (10.29.0):
- FirebaseCore (~> 10.5)
- FirebaseInstallations (~> 10.0)
- FirebaseRemoteConfig (~> 10.0)
- FirebaseSessions (~> 10.5)
- GoogleDataTransport (~> 9.2)
- GoogleUtilities/Environment (~> 7.13)
- GoogleUtilities/ISASwizzler (~> 7.13)
- GoogleUtilities/MethodSwizzler (~> 7.13)
- GoogleUtilities/UserDefaults (~> 7.13)
- nanopb (< 2.30911.0, >= 2.30908.0)
- FirebaseRemoteConfig (10.29.0):
- FirebaseABTesting (~> 10.0)
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
- FirebaseRemoteConfigInterop (~> 10.23)
- FirebaseSharedSwift (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- "GoogleUtilities/NSData+zlib (~> 7.8)"
- FirebaseRemoteConfigInterop (10.29.0) - FirebaseRemoteConfigInterop (10.29.0)
- FirebaseSessions (10.29.0): - FirebaseSessions (10.29.0):
- FirebaseCore (~> 10.5) - FirebaseCore (~> 10.5)
@ -125,6 +153,7 @@ PODS:
- GoogleUtilities/UserDefaults (~> 7.13) - GoogleUtilities/UserDefaults (~> 7.13)
- nanopb (< 2.30911.0, >= 2.30908.0) - nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesSwift (~> 2.1) - PromisesSwift (~> 2.1)
- FirebaseSharedSwift (10.29.0)
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_keyboard_visibility (0.0.1): - flutter_keyboard_visibility (0.0.1):
- Flutter - Flutter
@ -168,6 +197,8 @@ PODS:
- GoogleUtilities/Environment (7.13.3): - GoogleUtilities/Environment (7.13.3):
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- PromisesObjC (< 3.0, >= 1.2) - PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/ISASwizzler (7.13.3):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Logger (7.13.3):
- GoogleUtilities/Environment - GoogleUtilities/Environment
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
@ -254,6 +285,7 @@ DEPENDENCIES:
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- firebase_performance (from `.symlinks/plugins/firebase_performance/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
@ -284,6 +316,7 @@ SPEC REPOS:
- DKImagePickerController - DKImagePickerController
- DKPhotoGallery - DKPhotoGallery
- Firebase - Firebase
- FirebaseABTesting
- FirebaseAnalytics - FirebaseAnalytics
- FirebaseCore - FirebaseCore
- FirebaseCoreExtension - FirebaseCoreExtension
@ -291,8 +324,11 @@ SPEC REPOS:
- FirebaseCrashlytics - FirebaseCrashlytics
- FirebaseInstallations - FirebaseInstallations
- FirebaseMessaging - FirebaseMessaging
- FirebasePerformance
- FirebaseRemoteConfig
- FirebaseRemoteConfigInterop - FirebaseRemoteConfigInterop
- FirebaseSessions - FirebaseSessions
- FirebaseSharedSwift
- GoogleAppMeasurement - GoogleAppMeasurement
- GoogleDataTransport - GoogleDataTransport
- GoogleUtilities - GoogleUtilities
@ -319,6 +355,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_crashlytics/ios" :path: ".symlinks/plugins/firebase_crashlytics/ios"
firebase_messaging: firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios" :path: ".symlinks/plugins/firebase_messaging/ios"
firebase_performance:
:path: ".symlinks/plugins/firebase_performance/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_keyboard_visibility: flutter_keyboard_visibility:
@ -379,6 +417,8 @@ SPEC CHECKSUMS:
firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb
firebase_crashlytics: e3d3e0c99bad5aaab5908385133dea8ec344693f firebase_crashlytics: e3d3e0c99bad5aaab5908385133dea8ec344693f
firebase_messaging: c862b3d2b973ecc769194dc8de09bd22c77ae757 firebase_messaging: c862b3d2b973ecc769194dc8de09bd22c77ae757
firebase_performance: 8643e815a354ee94da1192cd69335a48a7b625a4
FirebaseABTesting: d87f56707159bae64e269757a6e963d490f2eebe
FirebaseAnalytics: 23717de130b779aa506e757edb9713d24b6ffeda FirebaseAnalytics: 23717de130b779aa506e757edb9713d24b6ffeda
FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16 FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16
FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f
@ -386,8 +426,11 @@ SPEC CHECKSUMS:
FirebaseCrashlytics: 34647b41e18de773717fdd348a22206f2f9bc774 FirebaseCrashlytics: 34647b41e18de773717fdd348a22206f2f9bc774
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366 FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366
FirebasePerformance: d0ac4aa90f8c1aedeb8d0329a56e2d77d8d9e004
FirebaseRemoteConfig: 48ef3f243742a8d72422ccfc9f986e19d7de53fd
FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d
FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc
FirebaseSharedSwift: 20530f495084b8d840f78a100d8c5ee613375f6e
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12

View File

@ -254,6 +254,7 @@
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
B1CDA9DD5B638A2BB88053CB /* [CP] Check Pods Manifest.lock */, B1CDA9DD5B638A2BB88053CB /* [CP] Check Pods Manifest.lock */,
7356FAC42C72724B0051A465 /* [Crashlytics] Clear dSYM */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
@ -263,7 +264,7 @@
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
287A33C298CA352A7E7F32A4 /* [CP] Embed Pods Frameworks */, 287A33C298CA352A7E7F32A4 /* [CP] Embed Pods Frameworks */,
0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */, 0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */,
1A9FD6BE5DEE99CDA7399504 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */, 1A9FD6BE5DEE99CDA7399504 /* [Crashlytics] Upload dSYM */,
); );
buildRules = ( buildRules = (
); );
@ -366,7 +367,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
1A9FD6BE5DEE99CDA7399504 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = { 1A9FD6BE5DEE99CDA7399504 /* [Crashlytics] Upload dSYM */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -375,14 +376,14 @@
); );
inputPaths = ( inputPaths = (
); );
name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\""; name = "[Crashlytics] Upload dSYM";
outputFileListPaths = ( outputFileListPaths = (
); );
outputPaths = ( outputPaths = (
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nPATH=${PATH}:$FLUTTER_ROOT/bin:$HOME/.pub-cache/bin\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=$PODS_ROOT/FirebaseCrashlytics/upload-symbols --platform=ios --apple-project-path=${SRCROOT} --env-platform-name=${PLATFORM_NAME} --env-configuration=${CONFIGURATION} --env-project-dir=${PROJECT_DIR} --env-built-products-dir=${BUILT_PRODUCTS_DIR} --env-dwarf-dsym-folder-path=${DWARF_DSYM_FOLDER_PATH} --env-dwarf-dsym-file-name=${DWARF_DSYM_FILE_NAME} --env-infoplist-path=${INFOPLIST_PATH} --default-config=default\n"; shellScript = "\n#!/bin/bash\nsleep 1 # Without this, there seems a chance that the script runs before dSYM generation is finished \n$PODS_ROOT/FirebaseCrashlytics/upload-symbols -gsp $PROJECT_DIR/Runner/GoogleService-Info.plist -p ios $DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME\n";
}; };
259653AE41D478F4C6BAE9B2 /* [CP] Check Pods Manifest.lock */ = { 259653AE41D478F4C6BAE9B2 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
@ -439,6 +440,24 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
}; };
7356FAC42C72724B0051A465 /* [Crashlytics] Clear dSYM */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[Crashlytics] Clear dSYM";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nrm -rf $DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME\n";
};
9740EEB61CF901F6004384FC /* Run Script */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
@ -452,7 +471,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
}; };
B1CDA9DD5B638A2BB88053CB /* [CP] Check Pods Manifest.lock */ = { B1CDA9DD5B638A2BB88053CB /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;

View File

@ -17,6 +17,7 @@ import 'package:shared_preferences/shared_preferences.dart';
class PostEditorController extends GetxController { class PostEditorController extends GetxController {
late final SharedPreferences _prefs; late final SharedPreferences _prefs;
final aliasController = TextEditingController();
final titleController = TextEditingController(); final titleController = TextEditingController();
final descriptionController = TextEditingController(); final descriptionController = TextEditingController();
final contentController = TextEditingController(); final contentController = TextEditingController();
@ -30,9 +31,9 @@ class PostEditorController extends GetxController {
Rx<Realm?> realmZone = Rx(null); Rx<Realm?> realmZone = Rx(null);
Rx<DateTime?> publishedAt = Rx(null); Rx<DateTime?> publishedAt = Rx(null);
Rx<DateTime?> publishedUntil = Rx(null); Rx<DateTime?> publishedUntil = Rx(null);
RxList<int> attachments = RxList<int>.empty(growable: true); RxList<String> attachments = RxList<String>.empty(growable: true);
RxList<String> tags = RxList<String>.empty(growable: true); RxList<String> tags = RxList<String>.empty(growable: true);
Rx<int?> thumbnail = Rx(null); Rx<String?> thumbnail = Rx(null);
RxList<int> visibleUsers = RxList.empty(growable: true); RxList<int> visibleUsers = RxList.empty(growable: true);
RxList<int> invisibleUsers = RxList.empty(growable: true); RxList<int> invisibleUsers = RxList.empty(growable: true);
@ -115,12 +116,12 @@ class PostEditorController extends GetxController {
return showModalBottomSheet( return showModalBottomSheet(
context: context, context: context,
builder: (context) => AttachmentEditorPopup( builder: (context) => AttachmentEditorPopup(
usage: 'i.attachment', pool: 'interactive',
initialAttachments: attachments, initialAttachments: attachments,
onAdd: (int value) { onAdd: (String value) {
attachments.add(value); attachments.add(value);
}, },
onRemove: (int value) { onRemove: (String value) {
attachments.remove(value); attachments.remove(value);
}, },
), ),
@ -168,6 +169,7 @@ class PostEditorController extends GetxController {
} }
void currentClear() { void currentClear() {
aliasController.clear();
titleController.clear(); titleController.clear();
descriptionController.clear(); descriptionController.clear();
contentController.clear(); contentController.clear();
@ -197,16 +199,23 @@ class PostEditorController extends GetxController {
type = value.type; type = value.type;
editTo.value = value; editTo.value = value;
realmZone.value = value.realm;
isDraft.value = value.isDraft ?? false; isDraft.value = value.isDraft ?? false;
aliasController.text = value.alias ?? '';
titleController.text = value.body['title'] ?? ''; titleController.text = value.body['title'] ?? '';
descriptionController.text = value.body['description'] ?? ''; descriptionController.text = value.body['description'] ?? '';
contentController.text = value.body['content'] ?? ''; contentController.text = value.body['content'] ?? '';
publishedAt.value = value.publishedAt; publishedAt.value = value.publishedAt;
publishedUntil.value = value.publishedUntil; publishedUntil.value = value.publishedUntil;
tags.value = tags.value = List.from(
value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty(); value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty(),
growable: true,
);
tags.refresh(); tags.refresh();
attachments.value = value.body['attachments']?.cast<int>() ?? List.empty(); attachments.value = List.from(
value.body['attachments'] ?? List.empty(),
growable: true,
);
attachments.refresh(); attachments.refresh();
thumbnail.value = value.body['thumbnail']; thumbnail.value = value.body['thumbnail'];
@ -256,6 +265,7 @@ class PostEditorController extends GetxController {
Map<String, dynamic> get payload { Map<String, dynamic> get payload {
return { return {
'alias': aliasController.text,
'title': title, 'title': title,
'description': description, 'description': description,
'content': contentController.text, 'content': contentController.text,
@ -277,20 +287,33 @@ class PostEditorController extends GetxController {
set payload(Map<String, dynamic> value) { set payload(Map<String, dynamic> value) {
type = value['type']; type = value['type'];
tags.value = value['tags'].map((x) => x['alias']).toList().cast<String>(); tags.value = List.from(
value['tags'].map((x) => x['alias']).toList(),
growable: true,
);
aliasController.text = value['alias'] ?? '';
titleController.text = value['title'] ?? ''; titleController.text = value['title'] ?? '';
descriptionController.text = value['description'] ?? ''; descriptionController.text = value['description'] ?? '';
contentController.text = value['content'] ?? ''; contentController.text = value['content'] ?? '';
attachments.value = value['attachments'].cast<int>() ?? List.empty(); attachments.value = List.from(
value['attachments'] ?? List.empty(),
growable: true,
);
attachments.refresh(); attachments.refresh();
thumbnail.value = value['thumbnail']; thumbnail.value = value['thumbnail'];
visibility.value = value['visibility']; visibility.value = value['visibility'];
isDraft.value = value['is_draft']; isDraft.value = value['is_draft'];
if (value['visible_users'] != null) { if (value['visible_users'] != null) {
visibleUsers.value = value['visible_users'].cast<int>(); visibleUsers.value = List.from(
value['visible_users'],
growable: true,
);
} }
if (value['invisible_users'] != null) { if (value['invisible_users'] != null) {
invisibleUsers.value = value['invisible_users'].cast<int>(); invisibleUsers.value = List.from(
value['invisible_users'],
growable: true,
);
} }
if (value['published_at'] != null) { if (value['published_at'] != null) {
publishedAt.value = DateTime.parse(value['published_at']).toLocal(); publishedAt.value = DateTime.parse(value['published_at']).toLocal();
@ -319,6 +342,7 @@ class PostEditorController extends GetxController {
bool get isNotEmpty { bool get isNotEmpty {
return [ return [
aliasController.text.isNotEmpty,
titleController.text.isNotEmpty, titleController.text.isNotEmpty,
descriptionController.text.isNotEmpty, descriptionController.text.isNotEmpty,
contentController.text.isNotEmpty, contentController.text.isNotEmpty,

View File

@ -9,11 +9,12 @@ class PostListController extends GetxController {
/// The polling source modifier. /// The polling source modifier.
/// - `0`: default recommendations /// - `0`: default recommendations
/// - `1`: shuffle mode /// - `1`: friend mode
/// - `2`: shuffle mode
RxInt mode = 0.obs; RxInt mode = 0.obs;
/// The paging controller for infinite loading. /// The paging controller for infinite loading.
/// Only available when mode is `0`. /// Only available when mode is `0` or `1`.
PagingController<int, Post> pagingController = PagingController<int, Post> pagingController =
PagingController(firstPageKey: 0); PagingController(firstPageKey: 0);
@ -111,10 +112,23 @@ class PostListController extends GetxController {
author: author, author: author,
); );
} else { } else {
resp = await provider.listRecommendations( switch (mode.value) {
pageKey, case 2:
channel: mode.value == 0 ? null : 'shuffle', resp = await provider.listRecommendations(
); pageKey,
channel: 'shuffle',
);
break;
case 1:
resp = await provider.listRecommendations(
pageKey,
channel: 'friends',
);
break;
default:
resp = await provider.listRecommendations(pageKey);
break;
}
} }
} catch (e) { } catch (e) {
rethrow; rethrow;

View File

@ -13,6 +13,7 @@ import 'package:solian/bootstrapper.dart';
import 'package:solian/firebase_options.dart'; import 'package:solian/firebase_options.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/attachment_uploader.dart'; import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/link_expander.dart';
import 'package:solian/providers/stickers.dart'; import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
@ -128,5 +129,6 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => RealmProvider()); Get.lazyPut(() => RealmProvider());
Get.lazyPut(() => ChatCallProvider()); Get.lazyPut(() => ChatCallProvider());
Get.lazyPut(() => AttachmentUploaderController()); Get.lazyPut(() => AttachmentUploaderController());
Get.lazyPut(() => LinkExpandController());
} }
} }

View File

@ -5,11 +5,11 @@ class Attachment {
DateTime createdAt; DateTime createdAt;
DateTime updatedAt; DateTime updatedAt;
DateTime? deletedAt; DateTime? deletedAt;
String rid;
String uuid; String uuid;
int size; int size;
String name; String name;
String alt; String alt;
String usage;
String mimetype; String mimetype;
String hash; String hash;
int destination; int destination;
@ -24,11 +24,11 @@ class Attachment {
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
required this.deletedAt, required this.deletedAt,
required this.rid,
required this.uuid, required this.uuid,
required this.size, required this.size,
required this.name, required this.name,
required this.alt, required this.alt,
required this.usage,
required this.mimetype, required this.mimetype,
required this.hash, required this.hash,
required this.destination, required this.destination,
@ -40,42 +40,45 @@ class Attachment {
}); });
factory Attachment.fromJson(Map<String, dynamic> json) => Attachment( factory Attachment.fromJson(Map<String, dynamic> json) => Attachment(
id: json['id'], id: json['id'],
createdAt: DateTime.parse(json['created_at']), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null, deletedAt: json['deleted_at'] != null
uuid: json['uuid'], ? DateTime.parse(json['deleted_at'])
size: json['size'], : null,
name: json['name'], rid: json['rid'],
alt: json['alt'], uuid: json['uuid'],
usage: json['usage'], size: json['size'],
mimetype: json['mimetype'], name: json['name'],
hash: json['hash'], alt: json['alt'],
destination: json['destination'], mimetype: json['mimetype'],
isAnalyzed: json['is_analyzed'], hash: json['hash'],
metadata: json['metadata'], destination: json['destination'],
isMature: json['is_mature'], isAnalyzed: json['is_analyzed'],
account: json['account'] != null ? Account.fromJson(json['account']) : null, metadata: json['metadata'],
accountId: json['account_id'], isMature: json['is_mature'],
); account:
json['account'] != null ? Account.fromJson(json['account']) : null,
accountId: json['account_id'],
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'created_at': createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(), 'deleted_at': deletedAt?.toIso8601String(),
'uuid': uuid, 'rid': rid,
'size': size, 'uuid': uuid,
'name': name, 'size': size,
'alt': alt, 'name': name,
'usage': usage, 'alt': alt,
'mimetype': mimetype, 'mimetype': mimetype,
'hash': hash, 'hash': hash,
'destination': destination, 'destination': destination,
'is_analyzed': isAnalyzed, 'is_analyzed': isAnalyzed,
'metadata': metadata, 'metadata': metadata,
'is_mature': isMature, 'is_mature': isMature,
'account': account?.toJson(), 'account': account?.toJson(),
'account_id': accountId, 'account_id': accountId,
}; };
} }

View File

@ -63,7 +63,7 @@ class Event {
class EventMessageBody { class EventMessageBody {
String text; String text;
String algorithm; String algorithm;
List<int>? attachments; List<String>? attachments;
int? quoteEvent; int? quoteEvent;
int? relatedEvent; int? relatedEvent;
List<int>? relatedUsers; List<int>? relatedUsers;
@ -82,7 +82,7 @@ class EventMessageBody {
text: json['text'] ?? '', text: json['text'] ?? '',
algorithm: json['algorithm'] ?? 'plain', algorithm: json['algorithm'] ?? 'plain',
attachments: json['attachments'] != null attachments: json['attachments'] != null
? List<int>.from(json['attachments'].map((x) => x)) ? List<String>.from(json['attachments']?.whereType<String>())
: null, : null,
quoteEvent: json['quote_event'], quoteEvent: json['quote_event'],
relatedEvent: json['related_event'], relatedEvent: json['related_event'],

65
lib/models/link.dart Normal file
View File

@ -0,0 +1,65 @@
class LinkMeta {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String entryId;
String? icon;
String url;
String? title;
String? image;
String? video;
String? audio;
String? description;
String? siteName;
LinkMeta({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.entryId,
required this.icon,
required this.url,
required this.title,
required this.image,
required this.video,
required this.audio,
required this.description,
required this.siteName,
});
factory LinkMeta.fromJson(Map<String, dynamic> json) => LinkMeta(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
entryId: json['entry_id'],
icon: json['icon'],
url: json['url'],
title: json['title'],
image: json['image'],
video: json['video'],
audio: json['audio'],
description: json['description'],
siteName: json['site_name'],
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'entry_id': entryId,
'icon': icon,
'url': url,
'title': title,
'image': image,
'video': video,
'audio': audio,
'description': description,
'site_name': siteName,
};
}

View File

@ -8,6 +8,8 @@ class Post {
DateTime updatedAt; DateTime updatedAt;
DateTime? editedAt; DateTime? editedAt;
DateTime? deletedAt; DateTime? deletedAt;
String? alias;
String? areaAlias;
dynamic body; dynamic body;
List<Tag>? tags; List<Tag>? tags;
List<Category>? categories; List<Category>? categories;
@ -33,6 +35,8 @@ class Post {
required this.updatedAt, required this.updatedAt,
required this.editedAt, required this.editedAt,
required this.deletedAt, required this.deletedAt,
required this.alias,
required this.areaAlias,
required this.type, required this.type,
required this.body, required this.body,
required this.tags, required this.tags,
@ -60,6 +64,8 @@ class Post {
deletedAt: json['deleted_at'] != null deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at']) ? DateTime.parse(json['deleted_at'])
: null, : null,
alias: json['alias'],
areaAlias: json['area_alias'],
type: json['type'], type: json['type'],
body: json['body'], body: json['body'],
tags: json['tags']?.map((x) => Tag.fromJson(x)).toList().cast<Tag>(), tags: json['tags']?.map((x) => Tag.fromJson(x)).toList().cast<Tag>(),
@ -101,6 +107,8 @@ class Post {
'updated_at': updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
'edited_at': editedAt?.toIso8601String(), 'edited_at': editedAt?.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(), 'deleted_at': deletedAt?.toIso8601String(),
'alias': alias,
'area_alias': areaAlias,
'type': type, 'type': type,
'body': body, 'body': body,
'tags': tags, 'tags': tags,

View File

@ -36,7 +36,7 @@ class Sticker {
String get imageUrl => ServiceFinder.buildUrl( String get imageUrl => ServiceFinder.buildUrl(
'files', 'files',
'/attachments/$attachmentId', '/attachments/${attachment.rid}',
); );
factory Sticker.fromJson(Map<String, dynamic> json) => Sticker( factory Sticker.fromJson(Map<String, dynamic> json) => Sticker(

View File

@ -148,7 +148,7 @@ class AttachmentUploaderController extends GetxController {
Future<void> uploadAttachmentWithCallback( Future<void> uploadAttachmentWithCallback(
Uint8List data, Uint8List data,
String path, String path,
String usage, String pool,
Map<String, dynamic>? metadata, Map<String, dynamic>? metadata,
Function(Attachment?) callback, Function(Attachment?) callback,
) async { ) async {
@ -158,7 +158,7 @@ class AttachmentUploaderController extends GetxController {
final result = await _rawUploadAttachment( final result = await _rawUploadAttachment(
data, data,
path, path,
usage, pool,
metadata, metadata,
onProgress: (progress) { onProgress: (progress) {
progressOfUpload.value = progress; progressOfUpload.value = progress;
@ -171,7 +171,7 @@ class AttachmentUploaderController extends GetxController {
Future<Attachment?> uploadAttachment( Future<Attachment?> uploadAttachment(
Uint8List data, Uint8List data,
String path, String path,
String usage, String pool,
Map<String, dynamic>? metadata, Map<String, dynamic>? metadata,
) async { ) async {
if (isUploading.value) throw Exception('uploading blocked'); if (isUploading.value) throw Exception('uploading blocked');
@ -180,7 +180,7 @@ class AttachmentUploaderController extends GetxController {
final result = await _rawUploadAttachment( final result = await _rawUploadAttachment(
data, data,
path, path,
usage, pool,
metadata, metadata,
onProgress: (progress) { onProgress: (progress) {
progressOfUpload.value = progress; progressOfUpload.value = progress;
@ -191,14 +191,14 @@ class AttachmentUploaderController extends GetxController {
} }
Future<Attachment?> _rawUploadAttachment( Future<Attachment?> _rawUploadAttachment(
Uint8List data, String path, String usage, Map<String, dynamic>? metadata, Uint8List data, String path, String pool, Map<String, dynamic>? metadata,
{Function(double)? onProgress, Function(dynamic err)? onError}) async { {Function(double)? onProgress, Function(dynamic err)? onError}) async {
final AttachmentProvider provider = Get.find(); final AttachmentProvider provider = Get.find();
try { try {
final result = await provider.createAttachment( final result = await provider.createAttachment(
data, data,
path, path,
usage, pool,
metadata, metadata,
onProgress: onProgress, onProgress: onProgress,
); );

View File

@ -17,6 +17,10 @@ class ChatCallProvider extends GetxController {
RxBool isReady = false.obs; RxBool isReady = false.obs;
RxBool isMounted = false.obs; RxBool isMounted = false.obs;
RxBool isInitialized = false.obs; RxBool isInitialized = false.obs;
RxBool isBusy = false.obs;
RxString lastDuration = '00:00:00'.obs;
Timer? lastDurationUpdateTimer;
String? token; String? token;
String? endpoint; String? endpoint;
@ -38,6 +42,34 @@ class ChatCallProvider extends GetxController {
RxList<ParticipantTrack> participantTracks = RxList.empty(growable: true); RxList<ParticipantTrack> participantTracks = RxList.empty(growable: true);
Rx<ParticipantTrack?> focusTrack = Rx(null); Rx<ParticipantTrack?> focusTrack = Rx(null);
void _updateDuration() {
if (current.value == null) {
lastDuration.value = '00:00:00';
return;
}
Duration duration = DateTime.now().difference(current.value!.createdAt);
String twoDigits(int n) => n.toString().padLeft(2, '0');
String formattedTime = '${twoDigits(duration.inHours)}:'
'${twoDigits(duration.inMinutes.remainder(60))}:'
'${twoDigits(duration.inSeconds.remainder(60))}';
lastDuration.value = formattedTime;
}
void enableDurationUpdater() {
_updateDuration();
lastDurationUpdateTimer = Timer.periodic(
const Duration(seconds: 1),
(_) => _updateDuration(),
);
}
void disableDurationUpdater() {
lastDurationUpdateTimer?.cancel();
lastDurationUpdateTimer = null;
}
Future<void> checkPermissions() async { Future<void> checkPermissions() async {
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) { if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
return; return;
@ -88,7 +120,30 @@ class ChatCallProvider extends GetxController {
void initRoom() { void initRoom() {
initHardware(); initHardware();
room = Room(); room = Room(
roomOptions: const RoomOptions(
dynacast: true,
adaptiveStream: true,
defaultAudioPublishOptions: AudioPublishOptions(
name: 'call_voice',
stream: 'call_stream',
),
defaultVideoPublishOptions: VideoPublishOptions(
name: 'call_video',
stream: 'call_stream',
simulcast: true,
backupVideoCodec: BackupVideoCodec(enabled: true),
),
defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(
useiOSBroadcastExtension: true,
params: VideoParametersPresets.screenShareH1080FPS30,
),
defaultCameraCaptureOptions: CameraCaptureOptions(
maxFrameRate: 30,
params: VideoParametersPresets.h1080_169,
),
),
);
listener = room.createListener(); listener = room.createListener();
WakelockPlus.enable(); WakelockPlus.enable();
} }
@ -96,36 +151,12 @@ class ChatCallProvider extends GetxController {
void joinRoom(String url, String token) async { void joinRoom(String url, String token) async {
if (isMounted.value) { if (isMounted.value) {
return; return;
} else {
isMounted.value = true;
} }
try { try {
await room.connect( await room.connect(
url, url,
token, token,
roomOptions: const RoomOptions(
dynacast: true,
adaptiveStream: true,
defaultAudioPublishOptions: AudioPublishOptions(
name: 'call_voice',
stream: 'call_stream',
),
defaultVideoPublishOptions: VideoPublishOptions(
name: 'call_video',
stream: 'call_stream',
simulcast: true,
backupVideoCodec: BackupVideoCodec(enabled: true),
),
defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(
useiOSBroadcastExtension: true,
params: VideoParametersPresets.screenShareH1080FPS30,
),
defaultCameraCaptureOptions: CameraCaptureOptions(
maxFrameRate: 30,
params: VideoParametersPresets.h1080_169,
),
),
fastConnectOptions: FastConnectOptions( fastConnectOptions: FastConnectOptions(
microphone: TrackOption(track: audioTrack.value), microphone: TrackOption(track: audioTrack.value),
camera: TrackOption(track: videoTrack.value), camera: TrackOption(track: videoTrack.value),
@ -133,6 +164,8 @@ class ChatCallProvider extends GetxController {
); );
} catch (e) { } catch (e) {
rethrow; rethrow;
} finally {
isMounted.value = true;
} }
} }
@ -152,7 +185,7 @@ class ChatCallProvider extends GetxController {
void onRoomDidUpdate() => sortParticipants(); void onRoomDidUpdate() => sortParticipants();
void setupRoom() { void setupRoom() {
if(isInitialized.value) return; if (isInitialized.value) return;
sortParticipants(); sortParticipants();
room.addListener(onRoomDidUpdate); room.addListener(onRoomDidUpdate);
@ -164,6 +197,7 @@ class ChatCallProvider extends GetxController {
Hardware.instance.setSpeakerphoneOn(true); Hardware.instance.setSpeakerphoneOn(true);
} }
isBusy.value = false;
isInitialized.value = true; isInitialized.value = true;
} }
@ -366,6 +400,7 @@ class ChatCallProvider extends GetxController {
} }
void disposeRoom() { void disposeRoom() {
isBusy.value = false;
isMounted.value = false; isMounted.value = false;
isInitialized.value = false; isInitialized.value = false;
current.value = null; current.value = null;

View File

@ -20,22 +20,22 @@ class AttachmentProvider extends GetConnect {
httpClient.baseUrl = ServiceFinder.buildUrl('files', null); httpClient.baseUrl = ServiceFinder.buildUrl('files', null);
} }
final Map<int, Attachment> _cachedResponses = {}; final Map<String, Attachment> _cachedResponses = {};
Future<List<Attachment?>> listMetadata( Future<List<Attachment?>> listMetadata(
List<int> id, { List<String> rid, {
noCache = false, noCache = false,
}) async { }) async {
if (id.isEmpty) return List.empty(); if (rid.isEmpty) return List.empty();
List<Attachment?> result = List.filled(id.length, null); List<Attachment?> result = List.filled(rid.length, null);
List<int> pendingQuery = List.empty(growable: true); List<String> pendingQuery = List.empty(growable: true);
if (!noCache) { if (!noCache) {
for (var idx = 0; idx < id.length; idx++) { for (var idx = 0; idx < rid.length; idx++) {
if (_cachedResponses.containsKey(id[idx])) { if (_cachedResponses.containsKey(rid[idx])) {
result[idx] = _cachedResponses[id[idx]]; result[idx] = _cachedResponses[rid[idx]];
} else { } else {
pendingQuery.add(id[idx]); pendingQuery.add(rid[idx]);
} }
} }
} }
@ -52,12 +52,12 @@ class AttachmentProvider extends GetConnect {
rawOut.data!.map((x) => Attachment.fromJson(x)).toList(); rawOut.data!.map((x) => Attachment.fromJson(x)).toList();
for (final item in out) { for (final item in out) {
if (item.destination != 0 && item.isAnalyzed) { if (item.destination != 0 && item.isAnalyzed) {
_cachedResponses[item.id] = item; _cachedResponses[item.rid] = item;
} }
} }
for (var i = 0; i < out.length; i++) { for (var i = 0; i < out.length; i++) {
for (var j = 0; j < id.length; j++) { for (var j = 0; j < rid.length; j++) {
if (out[i].id == id[j]) { if (out[i].rid == rid[j]) {
result[j] = out[i]; result[j] = out[i];
} }
} }
@ -66,16 +66,16 @@ class AttachmentProvider extends GetConnect {
return result; return result;
} }
Future<Attachment?> getMetadata(int id, {noCache = false}) async { Future<Attachment?> getMetadata(String rid, {noCache = false}) async {
if (!noCache && _cachedResponses.containsKey(id)) { if (!noCache && _cachedResponses.containsKey(rid)) {
return _cachedResponses[id]!; return _cachedResponses[rid]!;
} }
final resp = await get('/attachments/$id/meta'); final resp = await get('/attachments/$rid/meta');
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
final result = Attachment.fromJson(resp.body); final result = Attachment.fromJson(resp.body);
if (result.destination != 0 && result.isAnalyzed) { if (result.destination != 0 && result.isAnalyzed) {
_cachedResponses[id] = result; _cachedResponses[rid] = result;
} }
return result; return result;
} }
@ -84,7 +84,7 @@ class AttachmentProvider extends GetConnect {
} }
Future<Attachment> createAttachment( Future<Attachment> createAttachment(
Uint8List data, String path, String usage, Map<String, dynamic>? metadata, Uint8List data, String path, String pool, Map<String, dynamic>? metadata,
{Function(double)? onProgress}) async { {Function(double)? onProgress}) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized'); if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
@ -108,7 +108,7 @@ class AttachmentProvider extends GetConnect {
final payload = dio.FormData.fromMap({ final payload = dio.FormData.fromMap({
'alt': fileAlt, 'alt': fileAlt,
'file': filePayload, 'file': filePayload,
'usage': usage, 'pool': pool,
if (mimetypeOverride != null) 'mimetype': mimetypeOverride, if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
'metadata': jsonEncode(metadata), 'metadata': jsonEncode(metadata),
}); });
@ -133,8 +133,7 @@ class AttachmentProvider extends GetConnect {
Future<Response> updateAttachment( Future<Response> updateAttachment(
int id, int id,
String alt, String alt, {
String usage, {
bool isMature = false, bool isMature = false,
}) async { }) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
@ -144,7 +143,6 @@ class AttachmentProvider extends GetConnect {
var resp = await client.put('/attachments/$id', { var resp = await client.put('/attachments/$id', {
'alt': alt, 'alt': alt,
'usage': usage,
'is_mature': isMature, 'is_mature': isMature,
}); });
@ -169,7 +167,7 @@ class AttachmentProvider extends GetConnect {
return resp; return resp;
} }
void clearCache({int? id}) { void clearCache({String? id}) {
if (id != null) { if (id != null) {
_cachedResponses.remove(id); _cachedResponses.remove(id);
} else { } else {

View File

@ -9,13 +9,20 @@ class PostProvider extends GetConnect {
} }
Future<Response> listRecommendations(int page, Future<Response> listRecommendations(int page,
{int? realm, String? channel}) async { {String? realm, String? channel}) async {
GetConnect client;
final AuthProvider auth = Get.find();
final queries = [ final queries = [
'take=${10}', 'take=${10}',
'offset=$page', 'offset=$page',
if (realm != null) 'realmId=$realm', if (realm != null) 'realm=$realm',
]; ];
final resp = await get( if (auth.isAuthorized.value) {
client = auth.configureClient('co');
} else {
client = ServiceFinder.configureClient('co');
}
final resp = await client.get(
channel == null channel == null
? '/recommendations?${queries.join('&')}' ? '/recommendations?${queries.join('&')}'
: '/recommendations/$channel?${queries.join('&')}', : '/recommendations/$channel?${queries.join('&')}',
@ -45,14 +52,14 @@ class PostProvider extends GetConnect {
} }
Future<Response> listPost(int page, Future<Response> listPost(int page,
{int? realm, String? author, tag, category}) async { {String? realm, String? author, tag, category}) async {
final queries = [ final queries = [
'take=${10}', 'take=${10}',
'offset=$page', 'offset=$page',
if (tag != null) 'tag=$tag', if (tag != null) 'tag=$tag',
if (category != null) 'category=$category', if (category != null) 'category=$category',
if (author != null) 'author=$author', if (author != null) 'author=$author',
if (realm != null) 'realmId=$realm', if (realm != null) 'realm=$realm',
]; ];
final resp = await get('/posts?${queries.join('&')}'); final resp = await get('/posts?${queries.join('&')}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {

View File

@ -0,0 +1,25 @@
import 'dart:convert';
import 'dart:developer';
import 'package:get/get.dart';
import 'package:solian/models/link.dart';
import 'package:solian/services.dart';
class LinkExpandController extends GetxController {
final Map<String, LinkMeta?> _cachedResponse = {};
Future<LinkMeta?> expandLink(String url) async {
final target = utf8.fuse(base64).encode(url);
if (_cachedResponse.containsKey(target)) return _cachedResponse[target];
final client = ServiceFinder.configureClient('dealer');
final resp = await client.get('/api/links/$target');
if (resp.statusCode != 200) {
log('Unable to expand link ($url), status: ${resp.statusCode}, response: ${resp.body}');
_cachedResponse[target] = null;
return null;
}
final result = LinkMeta.fromJson(resp.body);
_cachedResponse[target] = result;
return result;
}
}

View File

@ -116,7 +116,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
attachResult = await provider.createAttachment( attachResult = await provider.createAttachment(
await file.readAsBytes(), await file.readAsBytes(),
file.path, file.path,
'p.$position', 'avatar',
null, null,
); );
} catch (e) { } catch (e) {

View File

@ -66,7 +66,7 @@ class _StickerScreenState extends State<StickerScreen> {
Widget _buildEmoteEntry(Sticker item, String prefix) { Widget _buildEmoteEntry(Sticker item, String prefix) {
final imageUrl = ServiceFinder.buildUrl( final imageUrl = ServiceFinder.buildUrl(
'files', 'files',
'/attachments/${item.attachmentId}', '/attachments/${item.attachment.rid}',
); );
return ListTile( return ListTile(
title: Text(item.name), title: Text(item.name),

View File

@ -12,16 +12,15 @@ import 'package:solian/widgets/chat/call/call_participant.dart';
import 'package:livekit_client/livekit_client.dart' as livekit; import 'package:livekit_client/livekit_client.dart' as livekit;
class CallScreen extends StatefulWidget { class CallScreen extends StatefulWidget {
const CallScreen({super.key}); final bool hideAppBar;
const CallScreen({super.key, this.hideAppBar = false});
@override @override
State<CallScreen> createState() => _CallScreenState(); State<CallScreen> createState() => _CallScreenState();
} }
class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin { class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
Timer? _timer;
String _currentDuration = '00:00:00';
int _layoutMode = 0; int _layoutMode = 0;
bool _showControls = true; bool _showControls = true;
@ -37,26 +36,6 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
); );
String _parseDuration() {
final ChatCallProvider provider = Get.find();
if (provider.current.value == null) return '00:00:00';
Duration duration =
DateTime.now().difference(provider.current.value!.createdAt);
String twoDigits(int n) => n.toString().padLeft(2, '0');
String formattedTime = '${twoDigits(duration.inHours)}:'
'${twoDigits(duration.inMinutes.remainder(60))}:'
'${twoDigits(duration.inSeconds.remainder(60))}';
return formattedTime;
}
void _updateDuration() {
setState(() {
_currentDuration = _parseDuration();
});
}
void _switchLayout() { void _switchLayout() {
if (_layoutMode < 1) { if (_layoutMode < 1) {
setState(() => _layoutMode++); setState(() => _layoutMode++);
@ -191,15 +170,15 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
@override @override
void initState() { void initState() {
Get.find<ChatCallProvider>().setupRoom();
super.initState(); super.initState();
_updateDuration(); Future.delayed(Duration.zero, () {
_planAutoHideControls(); Get.find<ChatCallProvider>()
_timer = Timer.periodic( ..setupRoom()
const Duration(seconds: 1), ..enableDurationUpdater();
(_) => _updateDuration(),
); _planAutoHideControls();
});
} }
@override @override
@ -210,30 +189,34 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ChatCallProvider provider = Get.find(); final ChatCallProvider ctrl = Get.find();
return Material( return Material(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: widget.hideAppBar
leading: AppBarLeadingButton.adaptive(context), ? null
centerTitle: true, : AppBar(
toolbarHeight: SolianTheme.toolbarHeight(context), leading: AppBarLeadingButton.adaptive(context),
title: RichText( centerTitle: true,
textAlign: TextAlign.center, toolbarHeight: SolianTheme.toolbarHeight(context),
text: TextSpan(children: [ title: Obx(
TextSpan( () => RichText(
text: 'call'.tr, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge, text: TextSpan(children: [
TextSpan(
text: 'call'.tr,
style: Theme.of(context).textTheme.titleLarge,
),
const TextSpan(text: '\n'),
TextSpan(
text: ctrl.lastDuration.value,
style: Theme.of(context).textTheme.bodySmall,
),
]),
),
),
), ),
const TextSpan(text: '\n'),
TextSpan(
text: _currentDuration,
style: Theme.of(context).textTheme.bodySmall,
),
]),
),
),
body: SafeArea( body: SafeArea(
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
@ -259,13 +242,21 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Obx(() {
children: [ return Row(
Text(call.room.serverRegion ?? 'unknown'), children: [
const SizedBox(width: 6), Text(
Text(call.room.serverVersion ?? 'unknown') call.channel.value?.name ??
], 'unknown'.tr,
), style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 6),
Text(call.lastDuration.value)
],
);
}),
Row( Row(
children: [ children: [
Text( Text(
@ -332,7 +323,6 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
Expanded( Expanded(
child: Material( child: Material(
color: Theme.of(context).colorScheme.surfaceContainerLow, color: Theme.of(context).colorScheme.surfaceContainerLow,
elevation: 2,
child: Builder( child: Builder(
builder: (context) { builder: (context) {
switch (_layoutMode) { switch (_layoutMode) {
@ -345,15 +335,15 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
), ),
), ),
), ),
if (provider.room.localParticipant != null) if (ctrl.room.localParticipant != null)
SizeTransition( SizeTransition(
sizeFactor: _controlsAnimation, sizeFactor: _controlsAnimation,
axis: Axis.vertical, axis: Axis.vertical,
child: SizedBox( child: SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
child: ControlsWidget( child: ControlsWidget(
provider.room, ctrl.room,
provider.room.localParticipant!, ctrl.room.localParticipant!,
), ),
), ),
), ),
@ -370,17 +360,13 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
@override @override
void deactivate() { void deactivate() {
_timer?.cancel(); Get.find<ChatCallProvider>().disableDurationUpdater();
_timer = null;
super.deactivate(); super.deactivate();
} }
@override @override
void activate() { void activate() {
_timer ??= Timer.periodic( Get.find<ChatCallProvider>().enableDurationUpdater();
const Duration(seconds: 1),
(_) => _updateDuration(),
);
super.activate(); super.activate();
} }
} }

View File

@ -11,9 +11,11 @@ import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart'; import 'package:solian/models/event.dart';
import 'package:solian/models/packet.dart'; import 'package:solian/models/packet.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/call.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/channel/call/call.dart';
import 'package:solian/screens/channel/channel_detail.dart'; import 'package:solian/screens/channel/channel_detail.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
@ -39,7 +41,7 @@ class ChannelChatScreen extends StatefulWidget {
} }
class _ChannelChatScreenState extends State<ChannelChatScreen> class _ChannelChatScreenState extends State<ChannelChatScreen>
with WidgetsBindingObserver { with WidgetsBindingObserver, TickerProviderStateMixin {
DateTime? _isOutOfSyncSince; DateTime? _isOutOfSyncSince;
bool _isBusy = false; bool _isBusy = false;
@ -238,77 +240,104 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
); );
} }
return Column( return Row(
children: [ children: [
if (_ongoingCall != null)
ChannelCallIndicator(
channel: _channel!,
ongoingCall: _ongoingCall!,
),
Expanded( Expanded(
child: ChatEventList( child: Column(
scope: widget.realm, children: [
channel: _channel!, if (_ongoingCall != null)
chatController: _chatController, ChannelCallIndicator(
onEdit: (item) { channel: _channel!,
setState(() => _messageToEditing = item); ongoingCall: _ongoingCall!,
}, onJoin: () {
onReply: (item) { if (!SolianTheme.isLargeScreen(context)) {
setState(() => _messageToReplying = item); final ChatCallProvider call = Get.find();
}, call.gotoScreen(context);
), }
),
if (_isOutOfSyncSince != null)
ListTile(
contentPadding: const EdgeInsets.only(left: 16, right: 8),
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
leading: const Icon(Icons.history_toggle_off),
title: Text('messageOutOfSync'.tr),
subtitle: Text('messageOutOfSyncCaption'.tr),
trailing: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() => _isOutOfSyncSince = null);
},
),
onTap: _isBusy
? null
: () {
_keepUpdateWithServer();
}, },
), ),
Obx(() { Expanded(
if (_chatController.isLoading.isTrue) { child: ChatEventList(
return const LinearProgressIndicator().animate().slideY(); scope: widget.realm,
} else { channel: _channel!,
return const SizedBox(); chatController: _chatController,
} onEdit: (item) {
}), setState(() => _messageToEditing = item);
ClipRect( },
child: BackdropFilter( onReply: (item) {
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), setState(() => _messageToReplying = item);
child: SafeArea( },
child: ChatMessageInput( ),
edit: _messageToEditing,
reply: _messageToReplying,
realm: widget.realm,
placeholder: placeholder,
channel: _channel!,
onSent: (Event item) {
setState(() {
_chatController.addPendingEvent(item);
});
},
onReset: () {
setState(() {
_messageToReplying = null;
_messageToEditing = null;
});
},
), ),
), if (_isOutOfSyncSince != null)
ListTile(
contentPadding: const EdgeInsets.only(left: 16, right: 8),
tileColor:
Theme.of(context).colorScheme.surfaceContainerLow,
leading: const Icon(Icons.history_toggle_off),
title: Text('messageOutOfSync'.tr),
subtitle: Text('messageOutOfSyncCaption'.tr),
trailing: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() => _isOutOfSyncSince = null);
},
),
onTap: _isBusy
? null
: () {
_keepUpdateWithServer();
},
),
Obx(() {
if (_chatController.isLoading.isTrue) {
return const LinearProgressIndicator().animate().slideY();
} else {
return const SizedBox();
}
}),
ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
child: SafeArea(
child: ChatMessageInput(
edit: _messageToEditing,
reply: _messageToReplying,
realm: widget.realm,
placeholder: placeholder,
channel: _channel!,
onSent: (Event item) {
setState(() {
_chatController.addPendingEvent(item);
});
},
onReset: () {
setState(() {
_messageToReplying = null;
_messageToEditing = null;
});
},
),
),
),
),
],
), ),
), ),
Obx(() {
final ChatCallProvider call = Get.find();
if (call.isMounted.value && SolianTheme.isLargeScreen(context)) {
return const Expanded(
child: Row(children: [
VerticalDivider(width: 0.3, thickness: 0.3),
Expanded(
child: CallScreen(hideAppBar: true),
),
]),
);
}
return const SizedBox();
}),
], ],
); );
}), }),

View File

@ -5,6 +5,7 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart'; import 'package:solian/screens/account/notification.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/signin_required_overlay.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
@ -27,20 +28,18 @@ class _HomeScreenState extends State<HomeScreen>
void initState() { void initState() {
super.initState(); super.initState();
_postController = PostListController(); _postController = PostListController();
_tabController = TabController(length: 2, vsync: this); _tabController = TabController(length: 3, vsync: this);
_tabController.addListener(() { _tabController.addListener(() {
switch (_tabController.index) { if (_postController.mode.value == _tabController.index) return;
case 0: _postController.mode.value = _tabController.index;
case 1: _postController.reloadAllOver();
if (_postController.mode.value == _tabController.index) return;
_postController.mode.value = _tabController.index;
_postController.reloadAllOver();
}
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
return Material( return Material(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: Scaffold( child: Scaffold(
@ -82,6 +81,7 @@ class _HomeScreenState extends State<HomeScreen>
controller: _tabController, controller: _tabController,
tabs: [ tabs: [
Tab(text: 'postListNews'.tr), Tab(text: 'postListNews'.tr),
Tab(text: 'postListFriends'.tr),
Tab(text: 'postListShuffle'.tr), Tab(text: 'postListShuffle'.tr),
], ],
), ),
@ -108,6 +108,23 @@ class _HomeScreenState extends State<HomeScreen>
), ),
]), ]),
), ),
Obx(() {
if (auth.isAuthorized.value) {
return RefreshIndicator(
onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [
PostWarpedListWidget(
controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(),
),
]),
);
} else {
return SigninRequiredOverlay(
onSignedIn: () => _postController.reloadAllOver(),
);
}
}),
PostShuffleSwiper(controller: _postController), PostShuffleSwiper(controller: _postController),
], ],
); );

View File

@ -65,7 +65,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
final AttachmentUploaderController uploader = Get.find(); final AttachmentUploaderController uploader = Get.find();
if (uploader.queueOfUpload.any( if (uploader.queueOfUpload.any(
((x) => x.usage == 'i.attachment' && x.isUploading), ((x) => x.isUploading),
)) { )) {
context.showErrorDialog('attachmentUploadInProgress'.tr); context.showErrorDialog('attachmentUploadInProgress'.tr);
return; return;
@ -90,8 +90,8 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);
} else { } else {
_editorController.localClear();
_editorController.currentClear(); _editorController.currentClear();
_editorController.localClear();
AppRouter.instance.pop(resp.body); AppRouter.instance.pop(resp.body);
} }
@ -176,10 +176,19 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
children: [ children: [
ListTile( ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow, tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
title: Text( title: Row(
_editorController.title ?? 'title'.tr, children: [
maxLines: 1, Text(
overflow: TextOverflow.ellipsis, _editorController.title ?? 'title'.tr,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(width: 6),
if (_editorController.aliasController.text.isNotEmpty)
Badge(
label: Text('#${_editorController.aliasController.text}'),
),
],
), ),
subtitle: Text( subtitle: Text(
_editorController.description ?? 'description'.tr, _editorController.description ?? 'description'.tr,
@ -255,6 +264,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
), ),
], ],
), ),
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
Expanded( Expanded(
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -265,10 +275,6 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
Expanded( Expanded(
child: ListView( child: ListView(
children: [ children: [
if (_isBusy)
const LinearProgressIndicator()
.animate()
.scaleX(),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,

View File

@ -171,7 +171,7 @@ class _RealmPostListWidgetState extends State<RealmPostListWidget> {
Response resp; Response resp;
try { try {
resp = await provider.listPost(pageKey, realm: widget.realm.id); resp = await provider.listPost(pageKey, realm: widget.realm.alias);
} catch (e) { } catch (e) {
_pagingController.error = e; _pagingController.error = e;
return; return;

View File

@ -1,3 +1,4 @@
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
@ -29,6 +30,15 @@ class RootShell extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final routeName = state.topRoute?.name; final routeName = state.topRoute?.name;
if (routeName != null) {
FirebaseAnalytics.instance.logEvent(
name: 'screen_view',
parameters: {
'firebase_screen': routeName,
},
);
}
return Scaffold( return Scaffold(
key: rootScaffoldKey, key: rootScaffoldKey,
drawer: SolianTheme.isLargeScreen(context) drawer: SolianTheme.isLargeScreen(context)

View File

@ -13,6 +13,7 @@ const i18nEnglish = {
'more': 'More', 'more': 'More',
'share': 'Share', 'share': 'Share',
'shareNoUri': 'Share text content', 'shareNoUri': 'Share text content',
'alias': 'Alias',
'feed': 'Feed', 'feed': 'Feed',
'unlink': 'Unlink', 'unlink': 'Unlink',
'feedSearch': 'Search Feed', 'feedSearch': 'Search Feed',
@ -124,6 +125,7 @@ const i18nEnglish = {
'postThumbnailAttachment': 'Attachment serial number', 'postThumbnailAttachment': 'Attachment serial number',
'postPinned': 'Pinned', 'postPinned': 'Pinned',
'postListNews': 'News', 'postListNews': 'News',
'postListFriends': 'Friends',
'postListShuffle': 'Random', 'postListShuffle': 'Random',
'postEditorModeStory': 'Post a post', 'postEditorModeStory': 'Post a post',
'postEditorModeArticle': 'Post an article', 'postEditorModeArticle': 'Post an article',
@ -267,6 +269,7 @@ const i18nEnglish = {
'callOngoing': 'A call is ongoing...', 'callOngoing': 'A call is ongoing...',
'callOngoingEmpty': 'A call is on hold...', 'callOngoingEmpty': 'A call is on hold...',
'callOngoingParticipants': '@count people are calling...', 'callOngoingParticipants': '@count people are calling...',
'callOngoingJoined': 'Call last @duration',
'callJoin': 'Join', 'callJoin': 'Join',
'callResume': 'Resume', 'callResume': 'Resume',
'callMicrophone': 'Microphone', 'callMicrophone': 'Microphone',
@ -377,4 +380,5 @@ const i18nEnglish = {
'messageOutOfSyncCaption': 'messageOutOfSyncCaption':
'Since the App has entered the background, there may be a time difference between the message list and the server. Click to Refresh.', 'Since the App has entered the background, there may be a time difference between the message list and the server. Click to Refresh.',
'messageHistoryWipe': 'Wipe local message history', 'messageHistoryWipe': 'Wipe local message history',
'unknown': 'Unknown',
}; };

View File

@ -21,6 +21,7 @@ const i18nSimplifiedChinese = {
'more': '更多', 'more': '更多',
'share': '分享', 'share': '分享',
'shareNoUri': '分享文字内容', 'shareNoUri': '分享文字内容',
'alias': '别名',
'feed': '资讯', 'feed': '资讯',
'unlink': '移除链接', 'unlink': '移除链接',
'feedSearch': '搜索资讯', 'feedSearch': '搜索资讯',
@ -124,6 +125,7 @@ const i18nSimplifiedChinese = {
'articleDetail': '文章详情', 'articleDetail': '文章详情',
'draftBoxOpen': '打开草稿箱', 'draftBoxOpen': '打开草稿箱',
'postListNews': '新鲜事', 'postListNews': '新鲜事',
'postListFriends': '好友圈',
'postListShuffle': '打乱看', 'postListShuffle': '打乱看',
'postNew': '创建新帖子', 'postNew': '创建新帖子',
'postNewInRealmHint': '在领域 @realm 里发表新帖子', 'postNewInRealmHint': '在领域 @realm 里发表新帖子',
@ -245,6 +247,7 @@ const i18nSimplifiedChinese = {
'callOngoing': '一则通话正在进行中…', 'callOngoing': '一则通话正在进行中…',
'callOngoingEmpty': '一则通话待机中…', 'callOngoingEmpty': '一则通话待机中…',
'callOngoingParticipants': '@count 人正在进行通话…', 'callOngoingParticipants': '@count 人正在进行通话…',
'callOngoingJoined': '通话进行 @duration',
'callJoin': '加入', 'callJoin': '加入',
'callResume': '恢复', 'callResume': '恢复',
'callMicrophone': '麦克风', 'callMicrophone': '麦克风',
@ -343,4 +346,5 @@ const i18nSimplifiedChinese = {
'messageOutOfSync': '消息可能与服务器脱节', 'messageOutOfSync': '消息可能与服务器脱节',
'messageOutOfSyncCaption': '由于 App 进入后台,消息列表可能与服务器存在时差,点击刷新。', 'messageOutOfSyncCaption': '由于 App 进入后台,消息列表可能与服务器存在时差,点击刷新。',
'messageHistoryWipe': '清除消息记录', 'messageHistoryWipe': '清除消息记录',
'unknown': '未知',
}; };

View File

@ -24,7 +24,6 @@ class AccountAvatar extends StatelessWidget {
if (content is String) { if (content is String) {
direct = content.startsWith('http'); direct = content.startsWith('http');
if (!isEmpty) isEmpty = content.isEmpty; if (!isEmpty) isEmpty = content.isEmpty;
if (!isEmpty) isEmpty = content.endsWith('/attachments/0');
} }
final url = direct final url = direct

View File

@ -16,10 +16,12 @@ class AttachmentAttrEditorDialog extends StatefulWidget {
}); });
@override @override
State<AttachmentAttrEditorDialog> createState() => _AttachmentAttrEditorDialogState(); State<AttachmentAttrEditorDialog> createState() =>
_AttachmentAttrEditorDialogState();
} }
class _AttachmentAttrEditorDialogState extends State<AttachmentAttrEditorDialog> { class _AttachmentAttrEditorDialogState
extends State<AttachmentAttrEditorDialog> {
final _altController = TextEditingController(); final _altController = TextEditingController();
bool _isBusy = false; bool _isBusy = false;
@ -33,11 +35,10 @@ class _AttachmentAttrEditorDialogState extends State<AttachmentAttrEditorDialog>
final resp = await provider.updateAttachment( final resp = await provider.updateAttachment(
widget.item.id, widget.item.id,
_altController.value.text, _altController.value.text,
widget.item.usage,
isMature: _isMature, isMature: _isMature,
); );
Get.find<AttachmentProvider>().clearCache(id: widget.item.id); Get.find<AttachmentProvider>().clearCache(id: widget.item.rid);
setState(() => _isBusy = false); setState(() => _isBusy = false);
return Attachment.fromJson(resp.body); return Attachment.fromJson(resp.body);
@ -109,7 +110,7 @@ class _AttachmentAttrEditorDialogState extends State<AttachmentAttrEditorDialog>
TextButton( TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: foregroundColor:
Theme.of(context).colorScheme.onSurfaceVariant), Theme.of(context).colorScheme.onSurfaceVariant),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('cancel'.tr), child: Text('cancel'.tr),
), ),

View File

@ -22,19 +22,19 @@ import 'package:solian/widgets/attachments/attachment_attr_editor.dart';
import 'package:solian/widgets/attachments/attachment_fullscreen.dart'; import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
class AttachmentEditorPopup extends StatefulWidget { class AttachmentEditorPopup extends StatefulWidget {
final String usage; final String pool;
final bool singleMode; final bool singleMode;
final bool imageOnly; final bool imageOnly;
final bool autoUpload; final bool autoUpload;
final double? imageMaxWidth; final double? imageMaxWidth;
final double? imageMaxHeight; final double? imageMaxHeight;
final List<int>? initialAttachments; final List<String>? initialAttachments;
final void Function(int) onAdd; final void Function(String) onAdd;
final void Function(int) onRemove; final void Function(String) onRemove;
const AttachmentEditorPopup({ const AttachmentEditorPopup({
super.key, super.key,
required this.usage, required this.pool,
required this.onAdd, required this.onAdd,
required this.onRemove, required this.onRemove,
this.singleMode = false, this.singleMode = false,
@ -73,7 +73,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
_enqueueTaskBatch(medias.map((x) { _enqueueTaskBatch(medias.map((x) {
final file = File(x.path); final file = File(x.path);
return AttachmentUploadTask(file: file, usage: widget.usage); return AttachmentUploadTask(file: file, usage: widget.pool);
})); }));
} else { } else {
final media = await _imagePicker.pickMedia( final media = await _imagePicker.pickMedia(
@ -83,7 +83,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
if (media == null) return; if (media == null) return;
_enqueueTask( _enqueueTask(
AttachmentUploadTask(file: File(media.path), usage: widget.usage), AttachmentUploadTask(file: File(media.path), usage: widget.pool),
); );
} }
} }
@ -97,7 +97,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final file = File(media.path); final file = File(media.path);
_enqueueTask( _enqueueTask(
AttachmentUploadTask(file: file, usage: widget.usage), AttachmentUploadTask(file: file, usage: widget.pool),
); );
} }
@ -113,7 +113,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
List<File> files = result.paths.map((path) => File(path!)).toList(); List<File> files = result.paths.map((path) => File(path!)).toList();
_enqueueTaskBatch(files.map((x) { _enqueueTaskBatch(files.map((x) {
return AttachmentUploadTask(file: x, usage: widget.usage); return AttachmentUploadTask(file: x, usage: widget.pool);
})); }));
} }
@ -131,7 +131,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final file = File(media.path); final file = File(media.path);
_enqueueTask( _enqueueTask(
AttachmentUploadTask(file: file, usage: widget.usage), AttachmentUploadTask(file: file, usage: widget.pool),
); );
} }
@ -181,13 +181,11 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
WidgetsBinding.instance.addPostFrameCallback((_) => controller.dispose()); WidgetsBinding.instance.addPostFrameCallback((_) => controller.dispose());
if (input == null || input.isEmpty) return; if (input == null || input.isEmpty) return;
final value = int.tryParse(input);
if (value == null) return;
final AttachmentProvider attach = Get.find(); final AttachmentProvider attach = Get.find();
final result = await attach.getMetadata(value); final result = await attach.getMetadata(input);
if (result != null) { if (result != null) {
widget.onAdd(result.id); widget.onAdd(result.rid);
setState(() => _attachments.add(result)); setState(() => _attachments.add(result));
if (widget.singleMode) Navigator.pop(context); if (widget.singleMode) Navigator.pop(context);
} }
@ -202,11 +200,11 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
_uploadController.uploadAttachmentWithCallback( _uploadController.uploadAttachmentWithCallback(
data, data,
'Pasted Image', 'Pasted Image',
widget.usage, widget.pool,
null, null,
(item) { (item) {
if (item == null) return; if (item == null) return;
widget.onAdd(item.id); widget.onAdd(item.rid);
if (mounted) { if (mounted) {
setState(() => _attachments.add(item)); setState(() => _attachments.add(item));
if (widget.singleMode) Navigator.pop(context); if (widget.singleMode) Navigator.pop(context);
@ -254,7 +252,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
.listMetadata(widget.initialAttachments ?? List.empty()) .listMetadata(widget.initialAttachments ?? List.empty())
.then((result) { .then((result) {
setState(() { setState(() {
_attachments = result; _attachments = List.from(result, growable: true);
_isBusy = false; _isBusy = false;
_isFirstTimeBusy = false; _isFirstTimeBusy = false;
}); });
@ -413,11 +411,11 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
: () { : () {
_uploadController _uploadController
.performSingleTask(index) .performSingleTask(index)
.then((r) { .then((out) {
if (r == null) return; if (out == null) return;
widget.onAdd(r.id); widget.onAdd(out.rid);
if (mounted) { if (mounted) {
setState(() => _attachments.add(r)); setState(() => _attachments.add(out));
if (widget.singleMode) { if (widget.singleMode) {
Navigator.pop(context); Navigator.pop(context);
} }
@ -515,7 +513,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
), ),
onTap: () { onTap: () {
_deleteAttachment(element).then((_) { _deleteAttachment(element).then((_) {
widget.onRemove(element.id); widget.onRemove(element.rid);
setState(() => _attachments.removeAt(index)); setState(() => _attachments.removeAt(index));
}); });
}, },
@ -529,7 +527,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
), ),
), ),
onTap: () { onTap: () {
widget.onRemove(element.id); widget.onRemove(element.rid);
setState(() => _attachments.removeAt(index)); setState(() => _attachments.removeAt(index));
}, },
), ),
@ -560,7 +558,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
void _startUploading() { void _startUploading() {
_uploadController.performUploadQueue(onData: (r) { _uploadController.performUploadQueue(onData: (r) {
widget.onAdd(r.id); widget.onAdd(r.rid);
if (mounted) { if (mounted) {
setState(() => _attachments.add(r)); setState(() => _attachments.add(r));
if (widget.singleMode) Navigator.pop(context); if (widget.singleMode) Navigator.pop(context);
@ -584,7 +582,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
if (_uploadController.isUploading.value) return; if (_uploadController.isUploading.value) return;
_enqueueTaskBatch(detail.files.map((x) { _enqueueTaskBatch(detail.files.map((x) {
final file = File(x.path); final file = File(x.path);
return AttachmentUploadTask(file: file, usage: widget.usage); return AttachmentUploadTask(file: file, usage: widget.pool);
})); }));
}, },
child: Column( child: Column(

View File

@ -67,7 +67,7 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
Future<void> _saveToAlbum() async { Future<void> _saveToAlbum() async {
final url = ServiceFinder.buildUrl( final url = ServiceFinder.buildUrl(
'files', 'files',
'/attachments/${widget.item.id}', '/attachments/${widget.item.rid}',
); );
if (PlatformInfo.isWeb || PlatformInfo.isDesktop) { if (PlatformInfo.isWeb || PlatformInfo.isDesktop) {
@ -258,7 +258,7 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
spacing: 6, spacing: 6,
children: [ children: [
Text( Text(
'#${widget.item.id}', '#${widget.item.rid}',
style: metaTextStyle, style: metaTextStyle,
), ),
if (widget.item.metadata?['width'] != null && if (widget.item.metadata?['width'] != null &&

View File

@ -91,7 +91,7 @@ class _AttachmentItemState extends State<AttachmentItem> {
launchUrlString( launchUrlString(
ServiceFinder.buildUrl( ServiceFinder.buildUrl(
'files', 'files',
'/attachments/${widget.item.id}', '/attachments/${widget.item.rid}',
), ),
); );
}, },
@ -135,7 +135,7 @@ class _AttachmentItemImage extends StatelessWidget {
fit: fit, fit: fit,
imageUrl: ServiceFinder.buildUrl( imageUrl: ServiceFinder.buildUrl(
'files', 'files',
'/attachments/${item.id}', '/attachments/${item.rid}',
), ),
progressIndicatorBuilder: (context, url, downloadProgress) { progressIndicatorBuilder: (context, url, downloadProgress) {
return Center( return Center(
@ -232,15 +232,18 @@ class _AttachmentItemVideo extends StatefulWidget {
class _AttachmentItemVideoState extends State<_AttachmentItemVideo> { class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
late final _player = Player( late final _player = Player(
configuration: const PlayerConfiguration(logLevel: MPVLogLevel.error), configuration: const PlayerConfiguration(
logLevel: MPVLogLevel.error,
),
); );
late final _controller = VideoController(_player); late final _controller = VideoController(_player);
bool _showContent = false; bool _showContent = false;
void _startLoad() { Future<void> _startLoad() async {
_player.open( await _player.open(
Media(ServiceFinder.buildUrl('files', '/attachments/${widget.item.id}')), Media(ServiceFinder.buildUrl('files', '/attachments/${widget.item.rid}')),
play: false, play: false,
); );
setState(() => _showContent = true); setState(() => _showContent = true);
@ -249,7 +252,9 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_showContent = widget.autoload; if (widget.autoload) {
_startLoad();
}
} }
@override @override

View File

@ -1,4 +1,4 @@
import 'dart:math' show min; import 'dart:math' as math;
import 'dart:ui'; import 'dart:ui';
import 'package:carousel_slider/carousel_slider.dart'; import 'package:carousel_slider/carousel_slider.dart';
@ -14,11 +14,13 @@ import 'package:solian/widgets/sized_container.dart';
class AttachmentList extends StatefulWidget { class AttachmentList extends StatefulWidget {
final String parentId; final String parentId;
final List<int> attachmentsId; final List<String> attachmentsId;
final bool isGrid; final bool isGrid;
final bool isColumn;
final bool isForceGrid; final bool isForceGrid;
final bool autoload; final bool autoload;
final double flatMaxHeight; final double flatMaxHeight;
final double columnMaxWidth;
final double? width; final double? width;
final double? viewport; final double? viewport;
@ -28,9 +30,11 @@ class AttachmentList extends StatefulWidget {
required this.parentId, required this.parentId,
required this.attachmentsId, required this.attachmentsId,
this.isGrid = false, this.isGrid = false,
this.isColumn = false,
this.isForceGrid = false, this.isForceGrid = false,
this.autoload = false, this.autoload = false,
this.flatMaxHeight = 720, this.flatMaxHeight = 720,
this.columnMaxWidth = 480,
this.width, this.width,
this.viewport, this.viewport,
}); });
@ -57,10 +61,12 @@ class _AttachmentListState extends State<AttachmentList> {
} }
attach.listMetadata(widget.attachmentsId).then((result) { attach.listMetadata(widget.attachmentsId).then((result) {
setState(() { if (mounted) {
_attachmentsMeta = result; setState(() {
_isLoading = false; _attachmentsMeta = result;
}); _isLoading = false;
});
}
_calculateAspectRatio(); _calculateAspectRatio();
}); });
} }
@ -70,7 +76,8 @@ class _AttachmentListState extends State<AttachmentList> {
double? consistentValue; double? consistentValue;
int portrait = 0, square = 0, landscape = 0; int portrait = 0, square = 0, landscape = 0;
for (var entry in _attachmentsMeta) { for (var entry in _attachmentsMeta) {
if (entry!.metadata?['ratio'] != null) { if (entry == null) continue;
if (entry.metadata?['ratio'] != null) {
if (entry.metadata?['ratio'] is int) { if (entry.metadata?['ratio'] is int) {
consistentValue ??= entry.metadata?['ratio'].toDouble(); consistentValue ??= entry.metadata?['ratio'].toDouble();
} else { } else {
@ -102,15 +109,17 @@ class _AttachmentListState extends State<AttachmentList> {
} }
} }
Widget _buildEntry(Attachment? element, int idx) { Widget _buildEntry(Attachment? element, int idx, {double? width}) {
return AttachmentListEntry( return AttachmentListEntry(
item: element, item: element,
parentId: widget.parentId, parentId: widget.parentId,
width: widget.width, width: width ?? widget.width,
badgeContent: '${idx + 1}/${_attachmentsMeta.length}', badgeContent: '${idx + 1}/${_attachmentsMeta.length}',
showBadge: _attachmentsMeta.length > 1 && !widget.isGrid, showBadge:
_attachmentsMeta.length > 1 && !widget.isGrid && !widget.isColumn,
showBorder: widget.attachmentsId.length > 1, showBorder: widget.attachmentsId.length > 1,
showMature: _showMature, showMature: _showMature,
autoload: widget.autoload,
onReveal: (value) { onReveal: (value) {
setState(() => _showMature = value); setState(() => _showMature = value);
}, },
@ -138,8 +147,46 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
final isNotPureImage = _attachmentsMeta if (widget.isColumn) {
.any((x) => x?.mimetype.split('/').firstOrNull != 'image'); var idx = 0;
const radius = BorderRadius.all(Radius.circular(8));
return Wrap(
spacing: 8,
runSpacing: 8,
children: widget.attachmentsId.map((x) {
final element = _attachmentsMeta[idx];
idx++;
if (element == null) return const SizedBox();
double ratio = element.metadata!['ratio']?.toDouble() ?? 16 / 9;
return Container(
constraints: BoxConstraints(
maxWidth: widget.columnMaxWidth,
maxHeight: 640,
),
child: AspectRatio(
aspectRatio: ratio,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius,
),
child: ClipRRect(
borderRadius: radius,
child: _buildEntry(element, idx),
),
),
),
);
}).toList(),
);
}
final isNotPureImage = _attachmentsMeta.any(
(x) => x?.mimetype.split('/').firstOrNull != 'image',
);
if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) { if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) {
const radius = BorderRadius.all(Radius.circular(8)); const radius = BorderRadius.all(Radius.circular(8));
return GridView.builder( return GridView.builder(
@ -148,7 +195,7 @@ class _AttachmentListState extends State<AttachmentList> {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: min(3, widget.attachmentsId.length), crossAxisCount: math.min(3, widget.attachmentsId.length),
mainAxisSpacing: 8.0, mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0, crossAxisSpacing: 8.0,
), ),
@ -157,8 +204,10 @@ class _AttachmentListState extends State<AttachmentList> {
final element = _attachmentsMeta[idx]; final element = _attachmentsMeta[idx];
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: border: Border.all(
Border.all(color: Theme.of(context).dividerColor, width: 1), color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius, borderRadius: radius,
), ),
child: ClipRRect( child: ClipRRect(
@ -206,6 +255,7 @@ class AttachmentListEntry extends StatelessWidget {
final Attachment? item; final Attachment? item;
final String? badgeContent; final String? badgeContent;
final double? width; final double? width;
final double? height;
final bool showBorder; final bool showBorder;
final bool showBadge; final bool showBadge;
final bool showMature; final bool showMature;
@ -220,6 +270,7 @@ class AttachmentListEntry extends StatelessWidget {
this.item, this.item,
this.badgeContent, this.badgeContent,
this.width, this.width,
this.height,
this.showBorder = false, this.showBorder = false,
this.showBadge = false, this.showBadge = false,
this.showMature = false, this.showMature = false,
@ -244,6 +295,7 @@ class AttachmentListEntry extends StatelessWidget {
return GestureDetector( return GestureDetector(
child: Container( child: Container(
width: width ?? MediaQuery.of(context).size.width, width: width ?? MediaQuery.of(context).size.width,
height: height,
decoration: BoxDecoration( decoration: BoxDecoration(
border: showBorder border: showBorder
? Border.symmetric( ? Border.symmetric(
@ -328,13 +380,13 @@ class AttachmentListEntry extends StatelessWidget {
} }
class AttachmentSelfContainedEntry extends StatefulWidget { class AttachmentSelfContainedEntry extends StatefulWidget {
final int id; final String rid;
final String parentId; final String parentId;
final bool isDense; final bool isDense;
const AttachmentSelfContainedEntry({ const AttachmentSelfContainedEntry({
super.key, super.key,
required this.id, required this.rid,
required this.parentId, required this.parentId,
this.isDense = false, this.isDense = false,
}); });
@ -353,10 +405,12 @@ class _AttachmentSelfContainedEntryState
final AttachmentProvider attachments = Get.find(); final AttachmentProvider attachments = Get.find();
return FutureBuilder( return FutureBuilder(
future: attachments.getMetadata(widget.id), future: attachments.getMetadata(widget.rid),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const Text('Loading...'); return const Center(
child: CircularProgressIndicator(),
);
} }
return AttachmentListEntry( return AttachmentListEntry(
@ -364,7 +418,6 @@ class _AttachmentSelfContainedEntryState
isDense: widget.isDense, isDense: widget.isDense,
parentId: widget.parentId, parentId: widget.parentId,
showMature: _showMature, showMature: _showMature,
showBorder: true,
onReveal: (value) { onReveal: (value) {
setState(() => _showMature = value); setState(() => _showMature = value);
}, },

View File

@ -9,14 +9,20 @@ import 'package:solian/models/call.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/call.dart'; import 'package:solian/providers/call.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/chat/call/call_prejoin.dart'; import 'package:solian/widgets/chat/call/call_prejoin.dart';
class ChannelCallIndicator extends StatelessWidget { class ChannelCallIndicator extends StatelessWidget {
final Channel channel; final Channel channel;
final Call ongoingCall; final Call ongoingCall;
final Function onJoin;
const ChannelCallIndicator( const ChannelCallIndicator({
{super.key, required this.channel, required this.ongoingCall}); super.key,
required this.channel,
required this.ongoingCall,
required this.onJoin,
});
void _showCallPrejoin(BuildContext context) { void _showCallPrejoin(BuildContext context) {
showModalBottomSheet( showModalBottomSheet(
@ -25,6 +31,7 @@ class ChannelCallIndicator extends StatelessWidget {
builder: (context) => ChatCallPrejoinPopup( builder: (context) => ChatCallPrejoinPopup(
ongoingCall: ongoingCall, ongoingCall: ongoingCall,
channel: channel, channel: channel,
onJoin: onJoin,
), ),
); );
} }
@ -40,48 +47,72 @@ class ChannelCallIndicator extends StatelessWidget {
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
content: Row( content: Row(
children: [ children: [
if (ongoingCall.participants.isEmpty) Text('callOngoingEmpty'.tr), Obx(() {
if (ongoingCall.participants.isNotEmpty) if (call.isInitialized.value) {
Text('callOngoingParticipants'.trParams({ return Text('callOngoingJoined'.trParams({
'count': ongoingCall.participants.length.toString(), 'duration': call.lastDuration.value,
})), }));
} else if (ongoingCall.participants.isEmpty) {
return Text('callOngoingEmpty'.tr);
} else {
return Text('callOngoingParticipants'.trParams({
'count': ongoingCall.participants.length.toString(),
}));
}
}),
const SizedBox(width: 6), const SizedBox(width: 6),
if (ongoingCall.participants.isNotEmpty) Obx(() {
Container( if (call.isInitialized.value) {
height: 28, return const SizedBox();
constraints: const BoxConstraints(maxWidth: 120), }
child: AvatarStack( if (ongoingCall.participants.isNotEmpty) {
return Container(
height: 28, height: 28,
borderWidth: 0, constraints: const BoxConstraints(maxWidth: 120),
avatars: ongoingCall.participants.map((x) { child: AvatarStack(
final userinfo = Account.fromJson(jsonDecode(x['metadata'])); height: 28,
return PlatformInfo.canCacheImage borderWidth: 0,
? CachedNetworkImageProvider(userinfo.avatar) avatars: ongoingCall.participants.map((x) {
as ImageProvider final userinfo =
: NetworkImage(userinfo.avatar) as ImageProvider; Account.fromJson(jsonDecode(x['metadata']));
}).toList(), return PlatformInfo.canCacheImage
), ? CachedNetworkImageProvider(userinfo.avatar)
), as ImageProvider
: NetworkImage(userinfo.avatar) as ImageProvider;
}).toList(),
),
);
}
return const SizedBox();
})
], ],
), ),
actions: [ actions: [
Obx(() { Obx(() {
if (call.current.value == null) { if (call.isBusy.value) {
return const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 3),
).paddingAll(16);
} else if (call.current.value == null) {
return TextButton( return TextButton(
onPressed: () => _showCallPrejoin(context), onPressed: () => _showCallPrejoin(context),
child: Text('callJoin'.tr), child: Text('callJoin'.tr),
); );
} else if (call.channel.value?.id == channel.id) { } else if (call.channel.value?.id == channel.id &&
!SolianTheme.isLargeScreen(context)) {
return TextButton( return TextButton(
onPressed: () => call.gotoScreen(context), onPressed: () => onJoin(),
child: Text('callResume'.tr), child: Text('callResume'.tr),
); );
} else { } else if (!SolianTheme.isLargeScreen(context)) {
return TextButton( return TextButton(
onPressed: null, onPressed: null,
child: Text('callJoin'.tr), child: Text('callJoin'.tr),
); );
} }
return const SizedBox();
}) })
], ],
); );

View File

@ -88,10 +88,12 @@ class _ControlsWidgetState extends State<ControlsWidget> {
void _disconnect() async { void _disconnect() async {
if (await showDisconnectDialog() != true) return; if (await showDisconnectDialog() != true) return;
final ChatCallProvider provider = Get.find(); final ChatCallProvider call = Get.find();
if (provider.current.value != null) { if (call.current.value != null) {
provider.disposeRoom(); call.disposeRoom();
Navigator.pop(context); if (Navigator.canPop(context)) {
Navigator.pop(context);
}
} }
} }
@ -209,8 +211,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
runSpacing: 5, runSpacing: 5,
children: [ children: [
IconButton( IconButton(
icon: Transform.flip( icon: const Icon(Icons.exit_to_app),
flipX: true, child: const Icon(Icons.exit_to_app)),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
onPressed: _disconnect, onPressed: _disconnect,
), ),

View File

@ -11,11 +11,13 @@ import 'package:solian/providers/call.dart';
class ChatCallPrejoinPopup extends StatefulWidget { class ChatCallPrejoinPopup extends StatefulWidget {
final Call ongoingCall; final Call ongoingCall;
final Channel channel; final Channel channel;
final Function onJoin;
const ChatCallPrejoinPopup({ const ChatCallPrejoinPopup({
super.key, super.key,
required this.ongoingCall, required this.ongoingCall,
required this.channel, required this.channel,
required this.onJoin,
}); });
@override @override
@ -25,22 +27,23 @@ class ChatCallPrejoinPopup extends StatefulWidget {
class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> { class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> {
bool _isBusy = false; bool _isBusy = false;
void performJoin() async { void _performJoin() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
final ChatCallProvider provider = Get.find(); final ChatCallProvider call = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
provider.setCall(widget.ongoingCall, widget.channel); call.setCall(widget.ongoingCall, widget.channel);
call.isBusy.value = true;
try { try {
final resp = await provider.getRoomToken(); final resp = await call.getRoomToken();
final token = resp.$1; final token = resp.$1;
final endpoint = resp.$2; final endpoint = resp.$2;
provider.initRoom(); call.initRoom();
provider.setupRoomListeners( call.setupRoomListeners(
onDisconnected: (reason) { onDisconnected: (reason) {
context.showSnackbar( context.showSnackbar(
'callDisconnected'.trParams({'reason': reason.toString()}), 'callDisconnected'.trParams({'reason': reason.toString()}),
@ -48,23 +51,22 @@ class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> {
}, },
); );
provider.joinRoom(endpoint, token); call.joinRoom(endpoint, token);
provider.gotoScreen(context).then((_) { Navigator.pop(context);
Navigator.pop(context);
});
} catch (e) { } catch (e) {
context.showErrorDialog(e); context.showErrorDialog(e);
} }
widget.onJoin();
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
@override @override
void initState() { void initState() {
final ChatCallProvider provider = Get.find(); final ChatCallProvider call = Get.find();
provider.checkPermissions().then((_) { call.checkPermissions().then((_) {
provider.initHardware(); call.initHardware();
}); });
super.initState(); super.initState();
@ -169,7 +171,7 @@ class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> {
backgroundColor: backgroundColor:
Theme.of(context).colorScheme.primaryContainer, Theme.of(context).colorScheme.primaryContainer,
), ),
onPressed: _isBusy ? null : performJoin, onPressed: _isBusy ? null : _performJoin,
child: Text('callJoin'.tr), child: Text('callJoin'.tr),
), ),
], ],

View File

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

View File

@ -8,6 +8,7 @@ import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/chat/chat_event_action_log.dart'; import 'package:solian/widgets/chat/chat_event_action_log.dart';
import 'package:solian/widgets/chat/chat_event_message.dart'; import 'package:solian/widgets/chat/chat_event_message.dart';
import 'package:solian/widgets/link_expansion.dart';
import 'package:timeago/timeago.dart' show format; import 'package:timeago/timeago.dart' show format;
class ChatEvent extends StatelessWidget { class ChatEvent extends StatelessWidget {
@ -37,10 +38,15 @@ class ChatEvent extends StatelessWidget {
return '$negativeSign${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds'; return '$negativeSign${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds';
} }
Widget _buildLinkExpansion() {
if (item.body['text'] == null) return const SizedBox();
return LinkExpansion(content: item.body['text']);
}
Widget _buildAttachment(BuildContext context, {bool isMinimal = false}) { Widget _buildAttachment(BuildContext context, {bool isMinimal = false}) {
final attachments = item.body['attachments'] != null final attachments = item.body['attachments'] != null
? List<int>.from(item.body['attachments'].map((x) => x)) ? List<String>.from(item.body['attachments']?.whereType<String>())
: List<int>.empty(); : List<String>.empty();
if (attachments.isEmpty) return const SizedBox(); if (attachments.isEmpty) return const SizedBox();
@ -67,7 +73,7 @@ class ChatEvent extends StatelessWidget {
return Container( return Container(
key: Key('m${item.uuid}attachments-box'), key: Key('m${item.uuid}attachments-box'),
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
padding: EdgeInsets.only(top: isMerged ? 0 : 4), padding: EdgeInsets.only(top: isMerged ? 0 : 4, bottom: 4),
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxHeight: 720, maxHeight: 720,
), ),
@ -75,7 +81,7 @@ class ChatEvent extends StatelessWidget {
key: Key('m${item.uuid}attachments'), key: Key('m${item.uuid}attachments'),
parentId: item.uuid, parentId: item.uuid,
attachmentsId: attachments, attachmentsId: attachments,
viewport: 1, isColumn: true,
), ),
); );
} }
@ -90,10 +96,13 @@ class ChatEvent extends StatelessWidget {
return const SizedBox(); return const SizedBox();
} }
return ChatEvent( return Container(
item: snapshot.data!.data, constraints: const BoxConstraints(maxWidth: 480),
isMerged: false, child: ChatEvent(
isQuote: true, item: snapshot.data!.data,
isMerged: false,
isQuote: true,
),
).paddingOnly(left: isMerged ? 52 : 0); ).paddingOnly(left: isMerged ? 52 : 0);
}, },
); );
@ -188,8 +197,9 @@ class ChatEvent extends StatelessWidget {
), ),
], ],
).paddingOnly(right: 12), ).paddingOnly(right: 12),
_buildAttachment(context, isMinimal: isContentPreviewing) _buildAttachment(context, isMinimal: isContentPreviewing).paddingOnly(
.paddingOnly(left: isContentPreviewing ? 12 : 0), left: isContentPreviewing ? 12 : 56,
),
], ],
); );
} else if (isQuote) { } else if (isQuote) {
@ -210,7 +220,9 @@ class ChatEvent extends StatelessWidget {
Row( Row(
children: [ children: [
AccountAvatar( AccountAvatar(
content: item.sender.account.avatar, radius: 9), content: item.sender.account.avatar,
radius: 9,
),
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
item.sender.account.nick, item.sender.account.nick,
@ -221,8 +233,7 @@ class ChatEvent extends StatelessWidget {
], ],
), ),
_buildContent().paddingOnly(left: 0.5), _buildContent().paddingOnly(left: 0.5),
_buildAttachment(context, isMinimal: true) _buildAttachment(context, isMinimal: true),
.paddingOnly(left: 0),
], ],
), ),
), ),
@ -231,6 +242,7 @@ class ChatEvent extends StatelessWidget {
).paddingOnly(left: isMerged ? 52 : 0, right: 4); ).paddingOnly(left: isMerged ? 52 : 0, right: 4);
} else { } else {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start,
key: Key('m${item.uuid}'), key: Key('m${item.uuid}'),
children: [ children: [
Row( Row(
@ -284,7 +296,8 @@ class ChatEvent extends StatelessWidget {
), ),
], ],
).paddingSymmetric(horizontal: 12), ).paddingSymmetric(horizontal: 12),
_buildAttachment(context), _buildLinkExpansion().paddingOnly(left: 52, right: 8),
_buildAttachment(context).paddingOnly(left: 56, right: 8),
], ],
); );
} }

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/controllers/chat_events_controller.dart'; import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
@ -83,12 +82,7 @@ class ChatEventList extends StatelessWidget {
), ),
); );
}, },
).animate(key: Key('m-animation${item.uuid}')).slideY( );
duration: 250.ms,
curve: Curves.fastEaseInToSlowEaseOut,
end: 0,
begin: 0.5,
);
}, },
); );
}), }),

View File

@ -59,7 +59,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
final TextEditingController _textController = TextEditingController(); final TextEditingController _textController = TextEditingController();
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
final List<int> _attachments = List.empty(growable: true); final List<String> _attachments = List.empty(growable: true);
Event? _editTo; Event? _editTo;
Event? _replyTo; Event? _replyTo;
@ -68,7 +68,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => AttachmentEditorPopup( builder: (context) => AttachmentEditorPopup(
usage: 'm.attachment', pool: 'messaging',
initialAttachments: _attachments, initialAttachments: _attachments,
onAdd: (value) { onAdd: (value) {
setState(() { setState(() {
@ -103,7 +103,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
final AttachmentUploaderController uploader = Get.find(); final AttachmentUploaderController uploader = Get.find();
if (uploader.queueOfUpload.any( if (uploader.queueOfUpload.any(
((x) => x.usage == 'm.attachment' && x.isUploading), ((x) => x.isUploading),
)) { )) {
context.showErrorDialog('attachmentUploadInProgress'.tr); context.showErrorDialog('attachmentUploadInProgress'.tr);
return; return;

View File

@ -0,0 +1,123 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:get/get.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/link_expander.dart';
import 'package:url_launcher/url_launcher_string.dart';
class LinkExpansion extends StatelessWidget {
final String content;
const LinkExpansion({super.key, required this.content});
Widget _buildImage(String url, {double? width, double? height}) {
return PlatformInfo.canCacheImage
? CachedNetworkImage(imageUrl: url, width: width, height: height)
: Image.network(url, width: width, height: height);
}
@override
Widget build(BuildContext context) {
final linkRegex = RegExp(
r'(?:(?:https?|ftp):\/\/|www\.)'
r'(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)'
r'[^\s<]*'
r'[^\s<?!.,:*_~]',
);
final matches = linkRegex.allMatches(content);
if (matches.isEmpty) {
return const SizedBox();
}
final LinkExpandController expandController = Get.find();
return Wrap(
children: matches.map((x) {
return Container(
constraints: BoxConstraints(
maxWidth: matches.length == 1 ? 480 : 340,
),
child: FutureBuilder(
future: expandController.expandLink(x.group(0)!),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
final isRichDescription = [
"solsynth.dev",
].contains(Uri.parse(snapshot.data!.url).host);
return GestureDetector(
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if ([
(snapshot.data!.icon?.isNotEmpty ?? false),
snapshot.data!.siteName != null
].any((x) => x))
Row(
children: [
if (snapshot.data!.icon?.isNotEmpty ?? false)
ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: _buildImage(
snapshot.data!.icon!,
width: 32,
height: 32,
),
).paddingOnly(right: 8),
if (snapshot.data!.siteName != null)
Text(
snapshot.data!.siteName!,
style: Theme.of(context).textTheme.labelLarge,
),
],
).paddingOnly(
bottom: (snapshot.data!.icon?.isNotEmpty ?? false)
? 8
: 4,
),
if (snapshot.data!.image != null &&
(snapshot.data!.image?.startsWith('http') ?? false))
ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: _buildImage(
snapshot.data!.image!,
),
).paddingOnly(bottom: 8),
Text(
snapshot.data!.title ?? 'No Title',
maxLines: 1,
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.bodyLarge,
),
if (snapshot.data!.description != null &&
isRichDescription)
MarkdownBody(data: snapshot.data!.description!)
else if (snapshot.data!.description != null)
Text(
snapshot.data!.description!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
).paddingAll(12),
),
onTap: () {
launchUrlString(x.group(0)!);
},
);
},
),
);
}).toList(),
);
}
}

View File

@ -131,7 +131,7 @@ class MarkdownTextContent extends StatelessWidget {
child: AttachmentSelfContainedEntry( child: AttachmentSelfContainedEntry(
isDense: true, isDense: true,
parentId: parentId, parentId: parentId,
id: int.parse(segments[1]), rid: segments[1],
), ),
), ),
).paddingSymmetric(vertical: 4); ).paddingSymmetric(vertical: 4);

View File

@ -33,9 +33,11 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
final resp = await provider.getCurrentStatus(); final resp = await provider.getCurrentStatus();
final status = AccountStatus.fromJson(resp.body); final status = AccountStatus.fromJson(resp.body);
setState(() { if (mounted) {
_accountStatus = status; setState(() {
}); _accountStatus = status;
});
}
} }
void _closeDrawer() { void _closeDrawer() {

View File

@ -19,7 +19,7 @@ class PostEditorCategoriesDialog extends StatelessWidget {
initialTags: controller.tags, initialTags: controller.tags,
hintText: 'postTagsPlaceholder'.tr, hintText: 'postTagsPlaceholder'.tr,
onUpdate: (value) { onUpdate: (value) {
controller.tags.value = value; controller.tags.value = List.from(value, growable: true);
controller.tags.refresh(); controller.tags.refresh();
}, },
), ),

View File

@ -14,12 +14,25 @@ class PostEditorOverviewDialog extends StatelessWidget {
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextField(
autofocus: true,
autocorrect: true,
controller: controller.aliasController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
hintText: 'alias'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextField( TextField(
autofocus: true, autofocus: true,
autocorrect: true, autocorrect: true,
controller: controller.titleController, controller: controller.titleController,
decoration: InputDecoration( decoration: InputDecoration(
border: const UnderlineInputBorder(), isDense: true,
border: const OutlineInputBorder(),
hintText: 'title'.tr, hintText: 'title'.tr,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
@ -33,7 +46,8 @@ class PostEditorOverviewDialog extends StatelessWidget {
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
controller: controller.descriptionController, controller: controller.descriptionController,
decoration: InputDecoration( decoration: InputDecoration(
border: const UnderlineInputBorder(), isDense: true,
border: const OutlineInputBorder(),
hintText: 'description'.tr, hintText: 'description'.tr,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),

View File

@ -20,7 +20,7 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => AttachmentEditorPopup( builder: (context) => AttachmentEditorPopup(
usage: 'i.attachment', pool: 'interactive',
singleMode: true, singleMode: true,
imageOnly: true, imageOnly: true,
autoUpload: true, autoUpload: true,
@ -84,8 +84,7 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
widget.controller.thumbnail.value = widget.controller.thumbnail.value = _attachmentController.text;
int.tryParse(_attachmentController.text);
Navigator.pop(context); Navigator.pop(context);
}, },
child: Text('confirm'.tr), child: Text('confirm'.tr),

View File

@ -42,24 +42,30 @@ class _PostActionState extends State<PostAction> {
Future<void> _doShare({bool noUri = false}) async { Future<void> _doShare({bool noUri = false}) async {
ShareResult result; ShareResult result;
String id;
final box = context.findRenderObject() as RenderBox?; final box = context.findRenderObject() as RenderBox?;
if (widget.item.alias?.isNotEmpty ?? false) {
id = '${widget.item.areaAlias}/${widget.item.alias}';
} else {
id = '${widget.item.id}';
}
if ((PlatformInfo.isAndroid || PlatformInfo.isIOS) && !noUri) { if ((PlatformInfo.isAndroid || PlatformInfo.isIOS) && !noUri) {
result = await Share.shareUri( result = await Share.shareUri(
Uri.parse('https://solsynth.dev/posts/${widget.item.id}'), Uri.parse('https://solsynth.dev/posts/$id'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
); );
} else { } else {
final extraContent = [ final extraContent = <String?>[
widget.item.body['title'], widget.item.body['title'],
widget.item.body['description'], widget.item.body['description'],
]; ].where((x) => x != null && x.isNotEmpty).toList();
final isExtraNotEmpty = extraContent.any((x) => x != null); final isExtraNotEmpty = extraContent.any((x) => x != null);
result = await Share.share( result = await Share.share(
'postShareContent'.trParams({ 'postShareContent'.trParams({
'username': widget.item.author.nick, 'username': widget.item.author.nick,
'content': 'content':
'${extraContent.join('\n')}${isExtraNotEmpty ? '\n\n' : ''}${widget.item.body['content'] ?? 'no content'}', '${extraContent.join('\n')}${isExtraNotEmpty ? '\n\n' : ''}${widget.item.body['content'] ?? 'no content'}',
'link': 'https://solsynth.dev/posts/${widget.item.id}', 'link': 'https://solsynth.dev/posts/$id',
}), }),
subject: 'postShareSubject'.trParams({ subject: 'postShareSubject'.trParams({
'username': widget.item.author.nick, 'username': widget.item.author.nick,
@ -96,9 +102,27 @@ class _PostActionState extends State<PostAction> {
'postActionList'.tr, 'postActionList'.tr,
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
), ),
Text( Row(
'#${widget.item.id.toString().padLeft(8, '0')}', children: [
style: Theme.of(context).textTheme.bodySmall, Text(
'#${widget.item.id.toString().padLeft(8, '0')}',
style: Theme.of(context).textTheme.bodySmall,
),
if (widget.item.alias?.isNotEmpty ?? false)
Text(
'·',
style: Theme.of(context).textTheme.bodySmall,
).paddingSymmetric(horizontal: 6),
if (widget.item.alias?.isNotEmpty ?? false)
Expanded(
child: Text(
'${widget.item.areaAlias}:${widget.item.alias}',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
), ),
], ],
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),

View File

@ -10,6 +10,7 @@ import 'package:solian/shells/title_shell.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart'; import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/link_expansion.dart';
import 'package:solian/widgets/markdown_text_content.dart'; import 'package:solian/widgets/markdown_text_content.dart';
import 'package:solian/widgets/posts/post_tags.dart'; import 'package:solian/widgets/posts/post_tags.dart';
import 'package:solian/widgets/posts/post_quick_action.dart'; import 'package:solian/widgets/posts/post_quick_action.dart';
@ -78,23 +79,17 @@ class _PostItemState extends State<PostItem> {
Widget _buildThumbnail() { Widget _buildThumbnail() {
if (widget.item.body['thumbnail'] == null) return const SizedBox(); if (widget.item.body['thumbnail'] == null) return const SizedBox();
const radius = BorderRadius.all(Radius.circular(8)); final border = BorderSide(
return AspectRatio( color: Theme.of(context).dividerColor,
aspectRatio: 16 / 9, width: 0.3,
child: Container( );
decoration: BoxDecoration( return Container(
border: Border.all( decoration: BoxDecoration(border: Border(top: border, bottom: border)),
color: Theme.of(context).dividerColor, child: AspectRatio(
width: 0.3, aspectRatio: 16 / 9,
), child: AttachmentSelfContainedEntry(
borderRadius: radius, rid: widget.item.body['thumbnail'],
), parentId: 'p${item.id}-thumbnail',
child: ClipRRect(
borderRadius: radius,
child: AttachmentSelfContainedEntry(
id: widget.item.body['thumbnail'],
parentId: 'p${item.id}-thumbnail',
),
), ),
), ),
); );
@ -294,12 +289,41 @@ class _PostItemState extends State<PostItem> {
); );
} }
Widget _buildAttachments() {
final List<String> attachments = item.body['attachments'] is List
? List.from(item.body['attachments']?.whereType<String>())
: List.empty();
if (attachments.length > 3) {
return AttachmentList(
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: true,
isGrid: true,
).paddingOnly(left: 36, top: 4, bottom: 4);
} else if (attachments.length > 1) {
return AttachmentList(
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: true,
isColumn: true,
).paddingOnly(left: 60, right: 24);
} else {
return AttachmentList(
flatMaxHeight: MediaQuery.of(context).size.width,
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: true,
);
}
}
double _contentHeight = 0; double _contentHeight = 0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<int> attachments = item.body['attachments'] is List final List<String> attachments = item.body['attachments'] is List
? item.body['attachments']?.cast<int>() ? List.from(item.body['attachments']?.whereType<String>())
: List.empty(); : List.empty();
final hasAttachment = attachments.isNotEmpty; final hasAttachment = attachments.isNotEmpty;
@ -307,7 +331,7 @@ class _PostItemState extends State<PostItem> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildThumbnail().paddingSymmetric(horizontal: 12, vertical: 4), _buildThumbnail().paddingOnly(bottom: 8),
_buildHeader().paddingSymmetric(horizontal: 12), _buildHeader().paddingSymmetric(horizontal: 12),
_buildHeaderDivider().paddingSymmetric(horizontal: 12), _buildHeaderDivider().paddingSymmetric(horizontal: 12),
Stack( Stack(
@ -355,6 +379,11 @@ class _PostItemState extends State<PostItem> {
), ),
], ],
), ),
LinkExpansion(content: item.body['content']).paddingOnly(
left: 8,
right: 8,
top: 4,
),
_buildFooter().paddingOnly(left: 16), _buildFooter().paddingOnly(left: 16),
if (attachments.isNotEmpty) if (attachments.isNotEmpty)
Row( Row(
@ -381,7 +410,7 @@ class _PostItemState extends State<PostItem> {
closedBuilder: (_, openContainer) => Column( closedBuilder: (_, openContainer) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildThumbnail().paddingSymmetric(horizontal: 12, vertical: 4), _buildThumbnail().paddingOnly(bottom: 4),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -447,27 +476,31 @@ class _PostItemState extends State<PostItem> {
], ],
), ),
if (widget.item.replyTo != null && widget.isShowEmbed) if (widget.item.replyTo != null && widget.isShowEmbed)
_buildReply(context).paddingOnly(top: 4), Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 4),
child: _buildReply(context),
),
if (widget.item.repostTo != null && widget.isShowEmbed) if (widget.item.repostTo != null && widget.isShowEmbed)
_buildRepost(context).paddingOnly(top: 4), Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 4),
child: _buildRepost(context),
),
_buildFooter().paddingOnly(left: 12), _buildFooter().paddingOnly(left: 12),
LinkExpansion(content: item.body['content'])
.paddingOnly(top: 4),
], ],
), ),
), ),
], ],
).paddingOnly( ).paddingOnly(
top: 10, top: 10,
bottom: hasAttachment ? 10 : 0, bottom: attachments.length == 1 ? 10 : 0,
right: 16, right: 16,
left: 16, left: 16,
), ),
AttachmentList( _buildAttachments(),
flatMaxHeight: MediaQuery.of(context).size.width,
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: true,
isGrid: attachments.length > 1,
),
if (widget.isShowReply || widget.isReactable) if (widget.isShowReply || widget.isReactable)
PostQuickAction( PostQuickAction(
isShowReply: widget.isShowReply, isShowReply: widget.isShowReply,
@ -480,8 +513,8 @@ class _PostItemState extends State<PostItem> {
}); });
}, },
).paddingOnly( ).paddingOnly(
top: hasAttachment ? 10 : 6, top: attachments.length == 1 ? 10 : 6,
left: hasAttachment ? 24 : 60, left: attachments.length == 1 ? 24 : 60,
right: 16, right: 16,
bottom: 10, bottom: 10,
) )

View File

@ -29,7 +29,7 @@ class _StickerUploadDialogState extends State<StickerUploadDialog> {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => AttachmentEditorPopup( builder: (context) => AttachmentEditorPopup(
usage: 'sticker', pool: 'sticker',
singleMode: true, singleMode: true,
imageOnly: true, imageOnly: true,
autoUpload: true, autoUpload: true,

View File

@ -163,12 +163,6 @@ PODS:
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
- macos_window_utils (1.0.0): - macos_window_utils (1.0.0):
- FlutterMacOS - FlutterMacOS
- media_kit_libs_macos_video (1.0.4):
- FlutterMacOS
- media_kit_native_event_loop (1.0.0):
- FlutterMacOS
- media_kit_video (0.0.1):
- FlutterMacOS
- nanopb (2.30910.0): - nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0) - nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0) - nanopb/encode (= 2.30910.0)
@ -186,8 +180,6 @@ PODS:
- PromisesObjC (= 2.4.0) - PromisesObjC (= 2.4.0)
- protocol_handler_macos (0.0.1): - protocol_handler_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- screen_brightness_macos (0.1.0):
- FlutterMacOS
- share_plus (0.0.1): - share_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
@ -198,6 +190,9 @@ PODS:
- FlutterMacOS - FlutterMacOS
- url_launcher_macos (0.0.1): - url_launcher_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (125.6422.04) - WebRTC-SDK (125.6422.04)
@ -217,18 +212,15 @@ DEPENDENCIES:
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`) - macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`)
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- protocol_handler_macos (from `Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos`) - protocol_handler_macos (from `Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos`)
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
SPEC REPOS: SPEC REPOS:
@ -280,12 +272,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos :path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
macos_window_utils: macos_window_utils:
:path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos :path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos
media_kit_libs_macos_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
media_kit_native_event_loop:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos
media_kit_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
package_info_plus: package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
pasteboard: pasteboard:
@ -294,8 +280,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
protocol_handler_macos: protocol_handler_macos:
:path: Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos
screen_brightness_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos
share_plus: share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation: shared_preferences_foundation:
@ -304,6 +288,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
url_launcher_macos: url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
video_player_avfoundation:
:path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin
wakelock_plus: wakelock_plus:
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
@ -335,9 +321,6 @@ SPEC CHECKSUMS:
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
livekit_client: 95f3b71e6545845aa658a6df0a3a62dcc3471d7c livekit_client: 95f3b71e6545845aa658a6df0a3a62dcc3471d7c
macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663 macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
nanopb: 438bc412db1928dac798aa6fd75726007be04262 nanopb: 438bc412db1928dac798aa6fd75726007be04262
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
@ -345,11 +328,11 @@ SPEC CHECKSUMS:
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3 WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3

View File

@ -534,6 +534,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.8.12" version: "3.8.12"
firebase_performance:
dependency: "direct main"
description:
name: firebase_performance
sha256: "6d17133458b9627f15f278d6f71bebbbce885d393f3462b690e55deeb5c36b90"
url: "https://pub.dev"
source: hosted
version: "0.10.0+4"
firebase_performance_platform_interface:
dependency: transitive
description:
name: firebase_performance_platform_interface
sha256: "28dc0a70a3459fe51d1c1be5754803a9a0db0e210322ec7526f6ce42bf6ad83e"
url: "https://pub.dev"
source: hosted
version: "0.1.4+40"
firebase_performance_web:
dependency: transitive
description:
name: firebase_performance_web
sha256: db91d86b34280f5253d2913945fdd51d7114486584a298a7bedf1c4b2ab08f79
url: "https://pub.dev"
source: hosted
version: "0.1.6+12"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -1734,10 +1758,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
sha256: c5e5b2a142a893a752cb36ae5888680248686725a54afceff31f9a3a76bc53c2 sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.4+1" version: "2.5.4+2"
sqflite_common_ffi: sqflite_common_ffi:
dependency: "direct dev" dependency: "direct dev"
description: description:

View File

@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App" description: "The Solar Network App"
publish_to: "none" publish_to: "none"
version: 1.2.1+14 version: 1.2.1+19
environment: environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
@ -45,9 +45,6 @@ dependencies:
sqflite: ^2.3.3+1 sqflite: ^2.3.3+1
protocol_handler: ^0.2.0 protocol_handler: ^0.2.0
markdown: ^7.2.2 markdown: ^7.2.2
media_kit: ^1.1.10+1
media_kit_video: ^1.2.4
media_kit_libs_video: ^1.0.4
pasteboard: ^0.2.0 pasteboard: ^0.2.0
desktop_drop: ^0.4.4 desktop_drop: ^0.4.4
badges: ^3.1.2 badges: ^3.1.2
@ -70,6 +67,10 @@ dependencies:
collection: ^1.18.0 collection: ^1.18.0
firebase_crashlytics: ^4.0.4 firebase_crashlytics: ^4.0.4
firebase_analytics: ^11.2.1 firebase_analytics: ^11.2.1
firebase_performance: ^0.10.0+4
media_kit: ^1.1.10+1
media_kit_video: ^1.2.4
media_kit_libs_video: ^1.0.4
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: