Compare commits

..

14 Commits

Author SHA1 Message Date
69d5e95565 🚀 Launch 2.4.2+86 March Update 2025-03-31 23:05:36 +08:00
3e3442fc89 💄 Enable new sfx on special days 2025-03-31 23:00:13 +08:00
8181010b0b 💄 New desktop loading animation 2025-03-31 22:50:08 +08:00
269caf7555 💄 Some improvements
🐛 Bug fixes
 The heart reaction
2025-03-31 01:27:45 +08:00
ae0809ad35 💄 Optimize background color 2025-03-31 00:51:37 +08:00
4005f03cf8 🐛 Fix notification 2025-03-30 23:25:38 +08:00
4bd8ec54f1 Optimize initialization 2025-03-30 20:43:47 +08:00
51a387851f 🐛 Fix infinite starting up 2025-03-30 20:37:04 +08:00
8ed847d870 ♻️ Use API Version 2 to load post 2025-03-30 15:31:02 +08:00
dfe13de220 Program Badges 2025-03-29 17:00:17 +08:00
b02a54c1e9 🐛 Fix sound mode 2025-03-29 16:41:23 +08:00
55a7e7d900 🐛 Fix app drawer did not close after selected 2025-03-29 01:04:37 +08:00
3585941ccb 🐛 Optimize noise cancellation 2025-03-29 01:03:11 +08:00
7c6f2cc4ab ♻️ Refactored call view 2025-03-29 00:58:13 +08:00
59 changed files with 1305 additions and 809 deletions

View File

@ -941,5 +941,12 @@
"settingsResetMemorizedWindowSize": "Reset Window Size", "settingsResetMemorizedWindowSize": "Reset Window Size",
"settingsResetMemorizedWindowSizeDescription": "Reset the memorized window size, and set it to the default size.", "settingsResetMemorizedWindowSizeDescription": "Reset the memorized window size, and set it to the default size.",
"chatDirect": "Direct Messages", "chatDirect": "Direct Messages",
"back": "返回" "back": "Back",
"badgeProgramDeveloper": "Developer Program Member",
"badgeProgramStellar": "A Stellar",
"badgeProgramModerator": "Community Moderator",
"postEditedHint": "edited",
"splashScreenServer": "Server",
"splashScreenServerName": "Potato",
"splashScreenCaption": "Trying to establishing connection with HyperNet™"
} }

View File

@ -938,5 +938,12 @@
"settingsResetMemorizedWindowSize": "重置窗口大小", "settingsResetMemorizedWindowSize": "重置窗口大小",
"settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。", "settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。",
"chatDirect": "私信", "chatDirect": "私信",
"back": "返回" "back": "返回",
"badgeProgramDeveloper": "开发者计划成员",
"badgeProgramStellar": "一颗恒星",
"badgeProgramModerator": "社区管理员",
"postEditedHint": "已编辑",
"splashScreenServer": "服务器",
"splashScreenServerName": "土豆",
"splashScreenCaption": "正在尝试与 HyperNet™ 取得太阳链连接"
} }

View File

@ -189,6 +189,11 @@ PODS:
- Flutter - Flutter
- flutter_webrtc - flutter_webrtc
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
- livekit_noise_filter (0.0.1):
- Flutter
- flutter_webrtc
- LiveKitKrispNoiseFilter (= 0.0.7)
- LiveKitKrispNoiseFilter (0.0.7)
- media_kit_libs_ios_video (1.0.4): - media_kit_libs_ios_video (1.0.4):
- Flutter - Flutter
- media_kit_video (0.0.1): - media_kit_video (0.0.1):
@ -283,6 +288,7 @@ DEPENDENCIES:
- in_app_review (from `.symlinks/plugins/in_app_review/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- Kingfisher (~> 8.0) - Kingfisher (~> 8.0)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`) - livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- livekit_noise_filter (from `.symlinks/plugins/livekit_noise_filter/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
@ -315,6 +321,7 @@ SPEC REPOS:
- GoogleDataTransport - GoogleDataTransport
- GoogleUtilities - GoogleUtilities
- Kingfisher - Kingfisher
- LiveKitKrispNoiseFilter
- nanopb - nanopb
- OrderedSet - OrderedSet
- PromisesObjC - PromisesObjC
@ -369,6 +376,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/in_app_review/ios" :path: ".symlinks/plugins/in_app_review/ios"
livekit_client: livekit_client:
:path: ".symlinks/plugins/livekit_client/ios" :path: ".symlinks/plugins/livekit_client/ios"
livekit_noise_filter:
:path: ".symlinks/plugins/livekit_noise_filter/ios"
media_kit_libs_ios_video: media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios" :path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_video: media_kit_video:
@ -438,6 +447,8 @@ SPEC CHECKSUMS:
in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457 in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
livekit_client: 08755cabfa4da4ed455642f460cfbb39bc518070 livekit_client: 08755cabfa4da4ed455642f460cfbb39bc518070
livekit_noise_filter: a26aeb1c1eae6db0a023fd2f6ea3ff108c3ecbb0
LiveKitKrispNoiseFilter: efe418ceca28163ace0ff222bd2cc02384645d84
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275

View File

@ -241,7 +241,7 @@ class PostWriteController extends ChangeNotifier {
contentController.text = post.body['content'] ?? ''; contentController.text = post.body['content'] ?? '';
aliasController.text = post.alias ?? ''; aliasController.text = post.alias ?? '';
rewardController.text = post.body['reward']?.toString() ?? ''; rewardController.text = post.body['reward']?.toString() ?? '';
videoAttachment = post.preload?.video; videoAttachment = SnAttachment.fromJson(post.body['video']);
publishedAt = post.publishedAt; publishedAt = post.publishedAt;
publishedUntil = post.publishedUntil; publishedUntil = post.publishedUntil;
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true); visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
@ -252,17 +252,21 @@ class PostWriteController extends ChangeNotifier {
categories = categories =
List.from(post.categories.map((ele) => ele.alias), growable: true); List.from(post.categories.map((ele) => ele.alias), growable: true);
attachments.addAll( attachments.addAll(
post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []); post.body['attachments']
poll = post.preload?.poll; .where(SnAttachment.fromJson)
?.map(PostWriteMedia) ??
[],
);
poll = post.poll;
editingDraft = post.isDraft; editingDraft = post.isDraft;
if (post.preload?.thumbnail != null && if (post.body['thumbnail'] != null) {
(post.preload?.thumbnail?.rid.isNotEmpty ?? false)) { thumbnail =
thumbnail = PostWriteMedia(post.preload!.thumbnail); PostWriteMedia(SnAttachment.fromJson(post.body['thumbnail']));
} }
if (post.preload?.realm != null) { if (post.realm != null) {
realm = post.preload!.realm!; realm = post.realm!;
} }
editingPost = post; editingPost = post;

View File

@ -15,6 +15,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -49,6 +50,7 @@ import 'package:surface/router.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/menu_bar.dart'; import 'package:surface/widgets/menu_bar.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/version_label.dart'; import 'package:surface/widgets/version_label.dart';
import 'package:tray_manager/tray_manager.dart'; import 'package:tray_manager/tray_manager.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
@ -57,6 +59,7 @@ import 'package:in_app_review/in_app_review.dart';
import 'package:image_picker_android/image_picker_android.dart'; import 'package:image_picker_android/image_picker_android.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:local_notifier/local_notifier.dart'; import 'package:local_notifier/local_notifier.dart';
import 'package:flutter_animate/flutter_animate.dart';
@pragma('vm:entry-point') @pragma('vm:entry-point')
void appBackgroundDispatcher() { void appBackgroundDispatcher() {
@ -257,6 +260,7 @@ class _AppSplashScreen extends StatefulWidget {
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
bool _isBusy = false; bool _isBusy = false;
double _initPercentage = 0;
String _phaseText = 'appInitStarting'; String _phaseText = 'appInitStarting';
void _tryRequestRating() async { void _tryRequestRating() async {
@ -331,20 +335,24 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
// The Network initialization must be done after the HomeWidget initialization // The Network initialization must be done after the HomeWidget initialization
// The Network initialization will save the server url to the HomeWidget // The Network initialization will save the server url to the HomeWidget
// The Network initialization will also save initialize the Config, so it not need to be initialized again // The Network initialization will also save initialize the Config, so it not need to be initialized again
_initPercentage = 0.1;
_setPhaseText('network'); _setPhaseText('network');
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.initializeUserAgent(); await sn.initializeUserAgent();
await sn.setConfigWithNative(); await sn.setConfigWithNative();
if (!mounted) return; if (!mounted) return;
_initPercentage = 0.2;
_setPhaseText('userdata'); _setPhaseText('userdata');
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
await ua.initialize(); await ua.initialize();
if (!mounted) return; if (!mounted) return;
_initPercentage = 0.3;
_setPhaseText('websocket'); _setPhaseText('websocket');
final ws = context.read<WebSocketProvider>(); final ws = context.read<WebSocketProvider>();
await ws.tryConnect(); await ws.tryConnect();
try { try {
if (!mounted) return; if (!mounted) return;
_initPercentage = 0.9;
_setPhaseText('keyPair'); _setPhaseText('keyPair');
final kp = context.read<KeyPairProvider>(); final kp = context.read<KeyPairProvider>();
kp.reloadActive(); kp.reloadActive();
@ -357,24 +365,25 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
notify.listen(); notify.listen();
try { try {
notify.registerPushNotifications(); notify.registerPushNotifications();
if (!mounted) return;
_setPhaseText('stickers');
final sticker = context.read<SnStickerProvider>();
await sticker.listSticker();
if (!mounted) return;
_setPhaseText('userDirectory');
final ud = context.read<UserDirectoryProvider>();
await ud.loadAccountCache();
if (!mounted) return;
_setPhaseText('realm');
final rm = context.read<SnRealmProvider>();
await rm.refreshAvailableRealms();
if (!mounted) return;
_setPhaseText('chat');
final ct = context.read<ChatChannelProvider>();
await ct.refreshAvailableChannels();
_initPercentage = 1;
_setPhaseText('done');
} catch (_) {} } catch (_) {}
if (!mounted) return;
_setPhaseText('stickers');
final sticker = context.read<SnStickerProvider>();
await sticker.listSticker();
if (!mounted) return;
_setPhaseText('userDirectory');
final ud = context.read<UserDirectoryProvider>();
await ud.loadAccountCache();
if (!mounted) return;
_setPhaseText('realm');
final rm = context.read<SnRealmProvider>();
await rm.refreshAvailableRealms();
if (!mounted) return;
_setPhaseText('chat');
final ct = context.read<ChatChannelProvider>();
await ct.refreshAvailableChannels();
_setPhaseText('done');
_playIntro(); _playIntro();
} }
} catch (err) { } catch (err) {
@ -396,8 +405,22 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
final cfg = context.read<ConfigProvider>(); final cfg = context.read<ConfigProvider>();
if (!cfg.soundEffects) return; if (!cfg.soundEffects) return;
final date = DateTime.now();
final player = AudioPlayer(playerId: 'launch-done-player'); final player = AudioPlayer(playerId: 'launch-done-player');
await player.play(AssetSource('audio/sfx/launch-done.mp3'), volume: 0.8); await player.play(
(cfg.aprilFoolFeatures && date.month == 4 && date.day == 1)
? AssetSource('audio/sfx/launch-intro.mp3')
: AssetSource('audio/sfx/launch-done.mp3'),
volume: 0.8,
ctx: AudioContext(
android: AudioContextAndroid(
contentType: AndroidContentType.sonification,
usageType: AndroidUsageType.notificationEvent,
),
iOS: AudioContextIOS(category: AVAudioSessionCategory.ambient),
),
mode: PlayerMode.lowLatency,
);
player.onPlayerComplete.listen((_) { player.onPlayerComplete.listen((_) {
player.dispose(); player.dispose();
}); });
@ -456,15 +479,22 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
AppLifecycleListener(onExitRequested: _onExitRequested); AppLifecycleListener(onExitRequested: _onExitRequested);
} }
_trayInitialization(); try {
_hotkeyInitialization(); _trayInitialization();
_notifyInitialization(); _hotkeyInitialization();
_initialize().then((_) { _notifyInitialization();
_postInitialization(); _initialize().then((_) {
_tryRequestRating(); _postInitialization();
_checkForUpdate(); _tryRequestRating();
setState(() => _isBusy = false); _checkForUpdate();
}); setState(() => _isBusy = false);
}).catchError((err) {
logging.error('[Bootstrap] Unable to initialize app', err);
setState(() => _isBusy = false);
});
} catch (err) {
logging.error('[Bootstrap] Unable to initialize (pre-stage) app', err);
}
} }
Future<AppExitResponse> _onExitRequested() async { Future<AppExitResponse> _onExitRequested() async {
@ -555,46 +585,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
}); });
return SizeChangedLayoutNotifier( return SizeChangedLayoutNotifier(
child: _isBusy child: _isBusy
? Material( ? _AppLoadingScreen(
key: Key('app-splash-screen-$_isBusy'), isBusy: _isBusy,
child: Stack( initPercentage: _initPercentage,
children: [ phaseText: _phaseText,
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/icon/kanban-1st.jpg'),
fit: BoxFit.cover,
opacity: 0.1,
),
color: Theme.of(context).colorScheme.surface,
backgroundBlendMode: BlendMode.darken,
),
),
Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 240),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/icon/icon.png',
width: 64,
height: 64,
color:
Theme.of(context).colorScheme.onSurface,
),
Text('Solar Network').bold(),
AppVersionLabel(),
Gap(8),
Text(_phaseText, textAlign: TextAlign.center),
Gap(16),
const LinearProgressIndicator(),
],
),
),
),
],
),
) )
: widget.child, : widget.child,
); );
@ -604,3 +598,234 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
); );
} }
} }
class _AppLoadingScreen extends StatelessWidget {
const _AppLoadingScreen({
required this.isBusy,
required this.initPercentage,
required this.phaseText,
});
final bool isBusy;
final double initPercentage;
final String phaseText;
@override
Widget build(BuildContext context) {
if (ResponsiveScaffold.getIsExpand(context)) {
return Material(
key: Key('app-splash-screen-$isBusy'),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/icon/kanban-1st.jpg'),
fit: BoxFit.cover,
opacity: 0.1,
),
color: Theme.of(context).colorScheme.surface,
backgroundBlendMode: BlendMode.darken,
),
),
Center(
child: Row(
children: [
Expanded(
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: initPercentage),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${(value * 100).toStringAsFixed(0)}%')
.padding(left: 32, bottom: 4),
LinearProgressIndicator(
value: value,
borderRadius: const BorderRadius.all(
Radius.circular(0),
),
stopIndicatorColor: Colors.transparent,
backgroundColor: Colors.transparent,
),
const Gap(24),
],
),
),
),
Expanded(
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: initPercentage),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('${(value * 100).toStringAsFixed(0)}%')
.padding(right: 32, bottom: 4),
Transform.flip(
flipX: true,
child: LinearProgressIndicator(
value: value,
borderRadius: const BorderRadius.all(
Radius.circular(0),
),
stopIndicatorColor: Colors.transparent,
backgroundColor: Colors.transparent,
),
),
const Gap(24),
],
),
),
),
],
),
),
Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 240, minWidth: 160),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.surface.withOpacity(0.85),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 3,
),
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'splashScreenServer',
style: GoogleFonts.notoSerifHk(height: 1, fontSize: 11),
textAlign: TextAlign.center,
).tr().opacity(0.85),
Text(
'splashScreenServerName',
style: GoogleFonts.notoSerifHk(
fontSize: 24,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
).tr().opacity(0.85),
Text.rich(
TextSpan(
text: '#',
style: GoogleFonts.notoSerifHk(),
children: [
TextSpan(
text: '0',
style: GoogleFonts.notoSerifHk(
fontSize: 80,
fontWeight: FontWeight.bold,
),
),
],
),
textAlign: TextAlign.center,
).padding(vertical: 16),
],
),
),
),
Positioned(
left: 0,
right: 0,
bottom: MediaQuery.of(context).size.height * 0.2,
child: Column(
children: [
Text(
phaseText,
textAlign: TextAlign.center,
),
AnimateWidgetExtensions(Text(
'splashScreenCaption',
textAlign: TextAlign.center,
).tr())
.animate(onPlay: (e) => e.repeat())
.fadeIn(duration: 500.ms, curve: Curves.easeOut)
.then()
.fadeOut(
duration: 500.ms,
delay: 1000.ms,
curve: Curves.easeIn,
),
],
),
),
Positioned(
bottom: 8,
left: 16,
right: 16,
child: Row(
children: [
Image.asset(
'assets/icon/icon.png',
width: 40,
height: 40,
color: Theme.of(context).colorScheme.onSurface,
).padding(all: 4),
const Gap(4),
Text('Solar Network').bold(),
Expanded(child: const SizedBox()),
AppVersionLabel(),
const Gap(12),
],
),
),
],
),
);
}
return Material(
key: Key('app-splash-screen-$isBusy'),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/icon/kanban-1st.jpg'),
fit: BoxFit.cover,
opacity: 0.1,
),
color: Theme.of(context).colorScheme.surface,
backgroundBlendMode: BlendMode.darken,
),
),
Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 240),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/icon/icon.png',
width: 64,
height: 64,
color: Theme.of(context).colorScheme.onSurface,
),
Text('Solar Network').bold(),
AppVersionLabel(),
Gap(8),
Text(phaseText, textAlign: TextAlign.center),
Gap(16),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: initPercentage),
duration: Duration(milliseconds: 300),
builder: (context, value, _) =>
LinearProgressIndicator(value: value),
),
],
),
),
),
],
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
import 'package:livekit_noise_filter/livekit_noise_filter.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
@ -131,10 +132,14 @@ class ChatCallProvider extends ChangeNotifier {
void initRoom() { void initRoom() {
initHardware(); initHardware();
final timeout = const Duration(seconds: 60);
_room = Room( _room = Room(
roomOptions: const RoomOptions( roomOptions: RoomOptions(
dynacast: true, dynacast: true,
adaptiveStream: true, adaptiveStream: true,
defaultAudioCaptureOptions: AudioCaptureOptions(
processor: LiveKitNoiseFilter(),
),
defaultAudioPublishOptions: AudioPublishOptions( defaultAudioPublishOptions: AudioPublishOptions(
name: 'call_voice', name: 'call_voice',
stream: 'call_stream', stream: 'call_stream',
@ -154,6 +159,16 @@ class ChatCallProvider extends ChangeNotifier {
params: VideoParametersPresets.h1080_169, params: VideoParametersPresets.h1080_169,
), ),
), ),
connectOptions: ConnectOptions(
autoSubscribe: true,
timeouts: Timeouts(
connection: timeout,
debounce: timeout,
publish: timeout,
peerConnection: timeout,
iceRestart: timeout,
),
),
); );
_listener = _room.createListener(); _listener = _room.createListener();
WakelockPlus.enable(); WakelockPlus.enable();

View File

@ -106,6 +106,14 @@ class NotificationProvider extends ChangeNotifier {
_notifySoundPlayer.play( _notifySoundPlayer.play(
AssetSource('audio/notify/metal-pipe.mp3'), AssetSource('audio/notify/metal-pipe.mp3'),
volume: 0.6, volume: 0.6,
ctx: AudioContext(
android: AudioContextAndroid(
contentType: AndroidContentType.sonification,
usageType: AndroidUsageType.notificationEvent,
),
iOS: AudioContextIOS(category: AVAudioSessionCategory.ambient),
),
mode: PlayerMode.lowLatency,
); );
} }
} }

View File

@ -1,144 +1,31 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/poll.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/types/realm.dart';
class SnPostContentProvider { class SnPostContentProvider {
late final SnNetworkProvider _sn; late final SnNetworkProvider _sn;
late final UserDirectoryProvider _ud;
late final SnAttachmentProvider _attach;
late final SnRealmProvider _realm;
SnPostContentProvider(BuildContext context) { SnPostContentProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>(); _sn = context.read<SnNetworkProvider>();
_ud = context.read<UserDirectoryProvider>();
_attach = context.read<SnAttachmentProvider>();
_realm = context.read<SnRealmProvider>();
}
Future<SnPoll> _fetchPoll(int id) async {
final resp = await _sn.client.get('/cgi/co/polls/$id');
return SnPoll.fromJson(resp.data);
} }
Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async { Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async {
Set<String> rids = {};
Set<int> uids = {};
for (var i = 0; i < out.length; i++) {
rids.addAll(out[i].body['attachments']?.cast<String>() ?? []);
if (out[i].body['thumbnail'] != null) {
rids.add(out[i].body['thumbnail']);
}
if (out[i].body['video'] != null) {
rids.add(out[i].body['video']);
}
if (out[i].repostTo != null) {
out[i] = out[i].copyWith(
repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
);
}
if (out[i].publisher.type == 0) {
uids.add(out[i].publisher.accountId);
}
}
final attachments = await _attach.getMultiple(rids.toList());
for (var i = 0; i < out.length; i++) {
SnPoll? poll;
SnRealm? realm;
if (out[i].pollId != null) {
poll = await _fetchPoll(out[i].pollId!);
}
if (out[i].realmId != null) {
realm = await _realm.getRealm(out[i].realmId!);
}
out[i] = out[i].copyWith(
preload: SnPostPreload(
thumbnail: attachments
.where((ele) => ele?.rid == out[i].body['thumbnail'])
.firstOrNull,
attachments: attachments
.where((ele) =>
out[i].body['attachments']?.contains(ele?.rid) ?? false)
.toList(),
video: attachments
.where((ele) => ele?.rid == out[i].body['video'])
.firstOrNull,
poll: poll,
realm: realm,
),
);
}
uids.addAll(
attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
await _ud.listAccount(uids);
return out; return out;
} }
Future<SnPost> _preloadRelatedDataSingle(SnPost out) async { Future<SnPost> _preloadRelatedDataSingle(SnPost out) async {
Set<String> rids = {};
Set<int> uids = {};
rids.addAll(out.body['attachments']?.cast<String>() ?? []);
if (out.body['thumbnail'] != null) {
rids.add(out.body['thumbnail']);
}
if (out.body['video'] != null) {
rids.add(out.body['video']);
}
if (out.repostTo != null) {
out = out.copyWith(
repostTo: await _preloadRelatedDataSingle(out.repostTo!),
);
}
if (out.publisher.type == 0) {
uids.add(out.publisher.accountId);
}
final attachments = await _attach.getMultiple(rids.toList());
SnPoll? poll;
SnRealm? realm;
if (out.pollId != null) {
poll = await _fetchPoll(out.pollId!);
}
if (out.realmId != null) {
realm = await _realm.getRealm(out.realmId!);
}
out = out.copyWith(
preload: SnPostPreload(
thumbnail: attachments
.where((ele) => ele?.rid == out.body['thumbnail'])
.firstOrNull,
attachments: attachments
.where(
(ele) => out.body['attachments']?.contains(ele?.rid) ?? false)
.toList(),
video: attachments
.where((ele) => ele?.rid == out.body['video'])
.firstOrNull,
poll: poll,
realm: realm,
),
);
uids.addAll(
attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
await _ud.listAccount(uids);
return out; return out;
} }
Future<List<SnPost>> listRecommendations() async { Future<List<SnPost>> listRecommendations() async {
final resp = await _sn.client.get('/cgi/co/recommendations'); final resp = await _sn.client.get(
'/cgi/co/recommendations',
options: Options(headers: {
'X-API-Version': '2',
}),
);
final out = _preloadRelatedDataInBatch( final out = _preloadRelatedDataInBatch(
List.from(resp.data.map((ele) => SnPost.fromJson(ele))), List.from(resp.data.map((ele) => SnPost.fromJson(ele))),
); );
@ -202,6 +89,9 @@ class SnPostContentProvider {
if (realm != null) 'realm': realm, if (realm != null) 'realm': realm,
if (channel != null) 'channel': channel, if (channel != null) 'channel': channel,
}, },
options: Options(headers: {
'X-API-Version': '2',
}),
); );
final List<SnPost> out = await _preloadRelatedDataInBatch( final List<SnPost> out = await _preloadRelatedDataInBatch(
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
@ -215,11 +105,16 @@ class SnPostContentProvider {
int take = 10, int take = 10,
int offset = 0, int offset = 0,
}) async { }) async {
final resp = await _sn.client final resp = await _sn.client.get(
.get('/cgi/co/posts/$parentId/replies', queryParameters: { '/cgi/co/posts/$parentId/replies',
'take': take, queryParameters: {
'offset': offset, 'take': take,
}); 'offset': offset,
},
options: Options(headers: {
'X-API-Version': '2',
}),
);
final List<SnPost> out = await _preloadRelatedDataInBatch( final List<SnPost> out = await _preloadRelatedDataInBatch(
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
); );
@ -234,13 +129,20 @@ class SnPostContentProvider {
Iterable<String>? tags, Iterable<String>? tags,
Iterable<String>? categories, Iterable<String>? categories,
}) async { }) async {
final resp = await _sn.client.get('/cgi/co/posts/search', queryParameters: { final resp = await _sn.client.get(
'take': take, '/cgi/co/posts/search',
'offset': offset, queryParameters: {
'probe': searchTerm, 'take': take,
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','), 'offset': offset,
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','), 'probe': searchTerm,
}); if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
if (categories?.isNotEmpty ?? false)
'categories': categories!.join(','),
},
options: Options(headers: {
'X-API-Version': '2',
}),
);
final List<SnPost> out = await _preloadRelatedDataInBatch( final List<SnPost> out = await _preloadRelatedDataInBatch(
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
); );
@ -249,7 +151,12 @@ class SnPostContentProvider {
} }
Future<SnPost> getPost(dynamic id) async { Future<SnPost> getPost(dynamic id) async {
final resp = await _sn.client.get('/cgi/co/posts/$id'); final resp = await _sn.client.get(
'/cgi/co/posts/$id',
options: Options(headers: {
'X-API-Version': '2',
}),
);
final out = _preloadRelatedDataSingle( final out = _preloadRelatedDataSingle(
SnPost.fromJson(resp.data), SnPost.fromJson(resp.data),
); );

View File

@ -1,4 +1,3 @@
import 'package:animations/animations.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:surface/screens/abuse_report.dart'; import 'package:surface/screens/abuse_report.dart';
@ -54,16 +53,6 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/about.dart'; import 'package:surface/widgets/about.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
Widget _fadeThroughTransition(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: child,
);
}
final _appRoutes = [ final _appRoutes = [
GoRoute( GoRoute(
path: '/', path: '/',
@ -305,10 +294,7 @@ final _appRoutes = [
GoRoute( GoRoute(
path: '/realm', path: '/realm',
name: 'realm', name: 'realm',
pageBuilder: (context, state) => CustomTransitionPage( builder: (context, state) => const RealmScreen(),
transitionsBuilder: _fadeThroughTransition,
child: const RealmScreen(),
),
routes: [ routes: [
GoRoute( GoRoute(
path: '/:alias/community', path: '/:alias/community',

View File

@ -110,7 +110,7 @@ class AccountScreen extends StatelessWidget {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text("screenAccount").tr(), title: Text("screenAccount").tr(),

View File

@ -59,7 +59,7 @@ class _ActionEventScreenState extends State<ActionEventScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountActionEvent').tr(), title: Text('accountActionEvent').tr(),

View File

@ -91,7 +91,7 @@ class _AccountAuthTicketState extends State<AccountAuthTicket> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountAuthTickets').tr(), title: Text('accountAuthTickets').tr(),

View File

@ -70,7 +70,7 @@ class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
title: Text('screenAccountBadges').tr(), title: Text('screenAccountBadges').tr(),
), ),

View File

@ -69,7 +69,7 @@ class _AccountContactMethodState extends State<AccountContactMethod> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountContactMethods').tr(), title: Text('accountContactMethods').tr(),

View File

@ -62,7 +62,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: PageBackButton(), leading: PageBackButton(),
title: Text('screenFactorSettings').tr(), title: Text('screenFactorSettings').tr(),

View File

@ -37,7 +37,7 @@ class _KeyPairScreenState extends State<KeyPairScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
title: Text('screenKeyPairs').tr(), title: Text('screenKeyPairs').tr(),
), ),

View File

@ -75,7 +75,7 @@ class _AccountNotifyPrefsScreenState extends State<AccountNotifyPrefsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountSettingsNotify').tr(), title: Text('accountSettingsNotify').tr(),

View File

@ -70,7 +70,7 @@ class _AccountSecurityPrefsScreenState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountSettingsSecurity').tr(), title: Text('accountSettingsSecurity').tr(),

View File

@ -244,7 +244,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('screenAccountProfileEdit').tr()), title: Text('screenAccountProfileEdit').tr()),
@ -263,7 +263,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 7,
child: Container( child: Container(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme

View File

@ -61,6 +61,21 @@ final Map<String, (String, IconData, Color)> kBadgesMeta = {
Symbols.thumb_up, Symbols.thumb_up,
Colors.lightGreen, Colors.lightGreen,
), ),
'programs.developers': (
'badgeProgramDeveloper',
Symbols.code,
Colors.blue,
),
'programs.stellar': (
'badgeProgramStellar',
Symbols.family_star,
Colors.orange,
),
'programs.moderator': (
'badgeProgramModerator',
Symbols.sword_rose,
Colors.blue,
),
}; };
class UserScreen extends StatefulWidget { class UserScreen extends StatefulWidget {

View File

@ -70,7 +70,7 @@ class _AccountProgramScreenState extends State<AccountProgramScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
title: Text('accountProgram').tr(), title: Text('accountProgram').tr(),
), ),

View File

@ -196,7 +196,7 @@ class _AccountPublisherEditScreenState
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: PageBackButton(), leading: PageBackButton(),
title: Text('screenAccountPublisherEdit').tr()), title: Text('screenAccountPublisherEdit').tr()),
@ -214,7 +214,7 @@ class _AccountPublisherEditScreenState
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 7,
child: Container( child: Container(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme

View File

@ -26,7 +26,7 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('screenAccountPublisherNew').tr(), title: Text('screenAccountPublisherNew').tr(),

View File

@ -82,7 +82,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('screenAccountPublishers').tr(), title: Text('screenAccountPublishers').tr(),

View File

@ -55,7 +55,7 @@ class _PunishmentsScreenState extends State<PunishmentsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
title: Text('accountPunishments').tr(), title: Text('accountPunishments').tr(),
leading: PageBackButton(), leading: PageBackButton(),

View File

@ -37,7 +37,7 @@ class AccountSettingsScreen extends StatelessWidget {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: PageBackButton(), leading: PageBackButton(),
title: Text('screenAccountSettings').tr(), title: Text('screenAccountSettings').tr(),

View File

@ -8,7 +8,6 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_zoom.dart'; import 'package:surface/widgets/attachment/attachment_zoom.dart';
import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/attachment_item.dart';
@ -53,7 +52,6 @@ class _AlbumScreenState extends State<AlbumScreen> {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ud = context.read<UserDirectoryProvider>();
final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: { final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: {
'take': 10, 'take': 10,
'offset': _attachments.length, 'offset': _attachments.length,
@ -64,8 +62,6 @@ class _AlbumScreenState extends State<AlbumScreen> {
_attachments.addAll(attachments); _attachments.addAll(attachments);
_heroTags.addAll(_attachments.map((_) => uuid.v4())); _heroTags.addAll(_attachments.map((_) => uuid.v4()));
await ud.listAccount(attachments.map((e) => e.accountId).toSet());
_totalCount = resp.data['count'] as int?; _totalCount = resp.data['count'] as int?;
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;

View File

@ -223,7 +223,7 @@ class _ChatScreenState extends State<ChatScreen> {
} }
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenChat').tr(), title: Text('screenChat').tr(),
@ -389,7 +389,7 @@ class _ChatScreenState extends State<ChatScreen> {
children: [ children: [
if (_focusedRealm!.banner != null) if (_focusedRealm!.banner != null)
AspectRatio( AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 7,
child: AutoResizeUniversalImage( child: AutoResizeUniversalImage(
sn.getAttachmentUrl( sn.getAttachmentUrl(
_focusedRealm!.banner!, _focusedRealm!.banner!,

View File

@ -32,7 +32,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
} }
} }
Widget _buildListLayout() { Widget _buildMeetLayout() {
final call = context.read<ChatCallProvider>(); final call = context.read<ChatCallProvider>();
return Stack( return Stack(
children: [ children: [
@ -41,9 +41,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75), Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
child: call.focusTrack != null child: call.focusTrack != null
? InteractiveParticipantWidget( ? InteractiveParticipantWidget(
isFixedAvatar: false,
participant: call.focusTrack!, participant: call.focusTrack!,
onTap: () {},
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
@ -62,23 +60,18 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
return Container(); return Container();
} }
return Padding( return SizedBox(
padding: const EdgeInsets.only(top: 8, left: 8), height: 128,
child: ClipRRect( width: 128,
borderRadius: const BorderRadius.all(Radius.circular(8)), child: InteractiveParticipantWidget(
child: InteractiveParticipantWidget( participant: track,
isFixedAvatar: true, avatarSize: 32,
width: 120, onTap: () {
height: 120, if (track.participant.sid !=
color: Theme.of(context).cardColor, call.focusTrack?.participant.sid) {
participant: track, call.setFocusTrack(track);
onTap: () { }
if (track.participant.sid != },
call.focusTrack?.participant.sid) {
call.setFocusTrack(track);
}
},
),
), ),
); );
}, },
@ -89,50 +82,26 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
); );
} }
Widget _buildGridLayout() { Widget _buildListLayout() {
final call = context.read<ChatCallProvider>(); final call = context.read<ChatCallProvider>();
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(
double screenWidth = constraints.maxWidth; builder: (context, constraints) {
double screenHeight = constraints.maxHeight; return ListView.builder(
padding: EdgeInsets.zero,
int columns = (math.sqrt(call.participantTracks.length)).ceil(); itemCount: math.max(0, call.participantTracks.length),
int rows = (call.participantTracks.length / columns).ceil(); itemBuilder: (BuildContext context, int index) {
final track = call.participantTracks[index];
double tileWidth = screenWidth / columns; return InteractiveParticipantWidget(
double tileHeight = screenHeight / rows; padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
isList: true,
return StyledWidget(GridView.builder( avatarSize: 24,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( participant: track,
crossAxisCount: columns, );
childAspectRatio: tileWidth / tileHeight, },
crossAxisSpacing: 8, );
mainAxisSpacing: 8, },
), );
itemCount: math.max(0, call.participantTracks.length),
itemBuilder: (BuildContext context, int index) {
final track = call.participantTracks[index];
return Card(
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget(
color: Theme.of(context)
.colorScheme
.surfaceContainerHigh
.withOpacity(0.75),
participant: track,
onTap: () {
if (track.participant.sid !=
call.focusTrack?.participant.sid) {
call.setFocusTrack(track);
}
},
),
),
);
},
)).padding(all: 8);
});
} }
@override @override
@ -155,7 +124,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
listenable: call, listenable: call,
builder: (context, _) { builder: (context, _) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
title: RichText( title: RichText(
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -176,131 +145,129 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
]), ]),
), ),
), ),
body: GestureDetector( body: Column(
behavior: HitTestBehavior.translucent, children: [
child: Column( SizedBox(
children: [ width: MediaQuery.of(context).size.width,
height: 64,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Builder(builder: (context) {
final call = context.read<ChatCallProvider>();
final connectionQuality =
call.room.localParticipant?.connectionQuality ??
livekit.ConnectionQuality.unknown;
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
call.channel?.name ?? 'unknown'.tr(),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const Gap(6),
Text(call.lastDuration.toString())
],
),
Row(
children: [
Text(
{
livekit.ConnectionState.disconnected:
'callStatusDisconnected'.tr(),
livekit.ConnectionState.connected:
'callStatusConnected'.tr(),
livekit.ConnectionState.connecting:
'callStatusConnecting'.tr(),
livekit.ConnectionState.reconnecting:
'callStatusReconnecting'.tr(),
}[call.room.connectionState]!,
),
const Gap(6),
if (connectionQuality !=
livekit.ConnectionQuality.unknown)
Icon(
{
livekit.ConnectionQuality.excellent:
Icons.signal_cellular_alt,
livekit.ConnectionQuality.good:
Icons.signal_cellular_alt_2_bar,
livekit.ConnectionQuality.poor:
Icons.signal_cellular_alt_1_bar,
}[connectionQuality],
color: {
livekit.ConnectionQuality.excellent:
Colors.green,
livekit.ConnectionQuality.good:
Colors.orange,
livekit.ConnectionQuality.poor:
Colors.red,
}[connectionQuality],
size: 16,
)
else
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
padding: EdgeInsets.zero,
),
).padding(all: 3),
],
),
],
),
);
}),
Row(
children: [
IconButton(
icon: _layoutMode == 0
? const Icon(Icons.view_list)
: const Icon(Icons.grid_view),
onPressed: () {
_switchLayout();
},
),
],
),
],
).padding(left: 20, right: 16),
),
Expanded(
child: Material(
color: Theme.of(context).colorScheme.surfaceContainerLow,
child: Builder(
builder: (context) {
switch (_layoutMode) {
case 1:
return _buildListLayout();
default:
return _buildMeetLayout();
}
},
),
),
),
if (call.room.localParticipant != null)
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
height: 64, child: ControlsWidget(
child: Row( call.room,
mainAxisAlignment: MainAxisAlignment.spaceBetween, call.room.localParticipant!,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Builder(builder: (context) {
final call = context.read<ChatCallProvider>();
final connectionQuality =
call.room.localParticipant?.connectionQuality ??
livekit.ConnectionQuality.unknown;
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
call.channel?.name ?? 'unknown'.tr(),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const Gap(6),
Text(call.lastDuration.toString())
],
),
Row(
children: [
Text(
{
livekit.ConnectionState.disconnected:
'callStatusDisconnected'.tr(),
livekit.ConnectionState.connected:
'callStatusConnected'.tr(),
livekit.ConnectionState.connecting:
'callStatusConnecting'.tr(),
livekit.ConnectionState.reconnecting:
'callStatusReconnecting'.tr(),
}[call.room.connectionState]!,
),
const Gap(6),
if (connectionQuality !=
livekit.ConnectionQuality.unknown)
Icon(
{
livekit.ConnectionQuality.excellent:
Icons.signal_cellular_alt,
livekit.ConnectionQuality.good:
Icons.signal_cellular_alt_2_bar,
livekit.ConnectionQuality.poor:
Icons.signal_cellular_alt_1_bar,
}[connectionQuality],
color: {
livekit.ConnectionQuality.excellent:
Colors.green,
livekit.ConnectionQuality.good:
Colors.orange,
livekit.ConnectionQuality.poor:
Colors.red,
}[connectionQuality],
size: 16,
)
else
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
).padding(all: 3),
],
),
],
),
);
}),
Row(
children: [
IconButton(
icon: _layoutMode == 0
? const Icon(Icons.view_list)
: const Icon(Icons.grid_view),
onPressed: () {
_switchLayout();
},
),
],
),
],
).padding(left: 20, right: 16),
),
Expanded(
child: Material(
color: Theme.of(context).colorScheme.surfaceContainerLow,
child: Builder(
builder: (context) {
switch (_layoutMode) {
case 1:
return _buildGridLayout();
default:
return _buildListLayout();
}
},
),
), ),
), ),
if (call.room.localParticipant != null) Gap(MediaQuery.of(context).padding.bottom),
SizedBox( ],
width: MediaQuery.of(context).size.width,
child: ControlsWidget(
call.room,
call.room.localParticipant!,
),
),
],
),
onTap: () {},
), ),
); );
}); });

View File

@ -220,7 +220,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id; final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(), title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
), ),

View File

@ -141,7 +141,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
title: widget.editingChannelAlias != null title: widget.editingChannelAlias != null
? Text('screenChatManage').tr() ? Text('screenChatManage').tr()

View File

@ -304,7 +304,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
final ud = context.read<UserDirectoryProvider>(); final ud = context.read<UserDirectoryProvider>();
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
_channel?.type == 1 _channel?.type == 1

View File

@ -157,7 +157,7 @@ class _ExploreScreenState extends State<ExploreScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.watch<ConfigProvider>(); final cfg = context.watch<ConfigProvider>();
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
floatingActionButtonLocation: ExpandableFab.location, floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab( floatingActionButton: ExpandableFab(
key: _fabKey, key: _fabKey,

View File

@ -11,13 +11,11 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/notification.dart'; import 'package:surface/providers/notification.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/notification.dart'; import 'package:surface/types/notification.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/markdown_content.dart'; import 'package:surface/widgets/markdown_content.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../providers/userinfo.dart'; import '../providers/userinfo.dart';
@ -219,34 +217,24 @@ class _NotificationScreenState extends State<NotificationScreen> {
'interactive.subscription', 'interactive.subscription',
].contains(nty.topic) && ].contains(nty.topic) &&
nty.metadata['related_post'] != null) nty.metadata['related_post'] != null)
GestureDetector( TextButton(
child: Container( style: ButtonStyle(
decoration: BoxDecoration( padding: WidgetStatePropertyAll(
borderRadius: const BorderRadius.all( EdgeInsets.zero,
Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1),
), ),
child: PostItem( visualDensity: VisualDensity.compact,
data: SnPost.fromJson(
nty.metadata['related_post']!),
showComments: false,
showReactions: false,
showMenu: false,
).padding(vertical: 4),
), ),
onTap: () { child: Text('postReadMore').tr(),
onPressed: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postDetail', 'postDetail',
pathParameters: { pathParameters: {
'slug': nty 'slug': nty.metadata['related_post']['id']
.metadata['related_post']!['id'] .toString(),
.toString()
}, },
); );
}, },
).padding(top: 8), ),
const Gap(8), const Gap(8),
Row( Row(
children: [ children: [

View File

@ -66,7 +66,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
final double maxWidth = _data?.type == 'video' ? double.infinity : 640; final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: BackButton( leading: BackButton(
onPressed: () { onPressed: () {

View File

@ -79,6 +79,7 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
key: ValueKey(ele), key: ValueKey(ele),
data: ele, data: ele,
maxWidth: 640, maxWidth: 640,
useReplace: true,
onChanged: (ele) { onChanged: (ele) {
_posts[idx] = ele; _posts[idx] = ele;
setState(() {}); setState(() {});

View File

@ -286,7 +286,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
body: NestedScrollView( body: NestedScrollView(
controller: _scrollController, controller: _scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {

View File

@ -45,7 +45,7 @@ class _WalletScreenState extends State<WalletScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: PageBackButton(), title: Text('screenAccountWallet').tr()), leading: PageBackButton(), title: Text('screenAccountWallet').tr()),
body: Column( body: Column(

View File

@ -1,4 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:surface/types/account.dart';
part 'attachment.freezed.dart'; part 'attachment.freezed.dart';
@ -39,6 +40,7 @@ abstract class SnAttachment with _$SnAttachment {
required int? refId, required int? refId,
required SnAttachmentPool? pool, required SnAttachmentPool? pool,
required int? poolId, required int? poolId,
required SnAccount? account,
required int accountId, required int accountId,
int? thumbnailId, int? thumbnailId,
SnAttachment? thumbnail, SnAttachment? thumbnail,
@ -49,7 +51,8 @@ abstract class SnAttachment with _$SnAttachment {
@Default({}) Map<String, dynamic> metadata, @Default({}) Map<String, dynamic> metadata,
}) = _SnAttachment; }) = _SnAttachment;
factory SnAttachment.fromJson(Map<String, Object?> json) => _$SnAttachmentFromJson(json); factory SnAttachment.fromJson(Map<String, Object?> json) =>
_$SnAttachmentFromJson(json);
Map<String, dynamic> get data => { Map<String, dynamic> get data => {
...metadata, ...metadata,
@ -85,7 +88,8 @@ abstract class SnAttachmentFragment with _$SnAttachmentFragment {
@Default([]) List<String> fileChunksMissing, @Default([]) List<String> fileChunksMissing,
}) = _SnAttachmentFragment; }) = _SnAttachmentFragment;
factory SnAttachmentFragment.fromJson(Map<String, Object?> json) => _$SnAttachmentFragmentFromJson(json); factory SnAttachmentFragment.fromJson(Map<String, Object?> json) =>
_$SnAttachmentFragmentFromJson(json);
SnMediaType get mediaType => switch (mimetype.split('/').firstOrNull) { SnMediaType get mediaType => switch (mimetype.split('/').firstOrNull) {
'image' => SnMediaType.image, 'image' => SnMediaType.image,
@ -109,7 +113,8 @@ abstract class SnAttachmentPool with _$SnAttachmentPool {
required int? accountId, required int? accountId,
}) = _SnAttachmentPool; }) = _SnAttachmentPool;
factory SnAttachmentPool.fromJson(Map<String, Object?> json) => _$SnAttachmentPoolFromJson(json); factory SnAttachmentPool.fromJson(Map<String, Object?> json) =>
_$SnAttachmentPoolFromJson(json);
} }
@freezed @freezed
@ -122,7 +127,8 @@ abstract class SnAttachmentDestination with _$SnAttachmentDestination {
required bool isBoost, required bool isBoost,
}) = _SnAttachmentDestination; }) = _SnAttachmentDestination;
factory SnAttachmentDestination.fromJson(Map<String, Object?> json) => _$SnAttachmentDestinationFromJson(json); factory SnAttachmentDestination.fromJson(Map<String, Object?> json) =>
_$SnAttachmentDestinationFromJson(json);
} }
@freezed @freezed
@ -139,7 +145,8 @@ abstract class SnAttachmentBoost with _$SnAttachmentBoost {
required int account, required int account,
}) = _SnAttachmentBoost; }) = _SnAttachmentBoost;
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) => _$SnAttachmentBoostFromJson(json); factory SnAttachmentBoost.fromJson(Map<String, Object?> json) =>
_$SnAttachmentBoostFromJson(json);
} }
@freezed @freezed
@ -158,7 +165,8 @@ abstract class SnSticker with _$SnSticker {
required int accountId, required int accountId,
}) = _SnSticker; }) = _SnSticker;
factory SnSticker.fromJson(Map<String, Object?> json) => _$SnStickerFromJson(json); factory SnSticker.fromJson(Map<String, Object?> json) =>
_$SnStickerFromJson(json);
} }
@freezed @freezed
@ -175,7 +183,8 @@ abstract class SnStickerPack with _$SnStickerPack {
required int accountId, required int accountId,
}) = _SnStickerPack; }) = _SnStickerPack;
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json); factory SnStickerPack.fromJson(Map<String, Object?> json) =>
_$SnStickerPackFromJson(json);
} }
@freezed @freezed
@ -186,5 +195,6 @@ abstract class SnAttachmentBilling with _$SnAttachmentBilling {
required double includedRatio, required double includedRatio,
}) = _SnAttachmentBilling; }) = _SnAttachmentBilling;
factory SnAttachmentBilling.fromJson(Map<String, Object?> json) => _$SnAttachmentBillingFromJson(json); factory SnAttachmentBilling.fromJson(Map<String, Object?> json) =>
_$SnAttachmentBillingFromJson(json);
} }

View File

@ -38,6 +38,7 @@ mixin _$SnAttachment {
int? get refId; int? get refId;
SnAttachmentPool? get pool; SnAttachmentPool? get pool;
int? get poolId; int? get poolId;
SnAccount? get account;
int get accountId; int get accountId;
int? get thumbnailId; int? get thumbnailId;
SnAttachment? get thumbnail; SnAttachment? get thumbnail;
@ -98,6 +99,7 @@ mixin _$SnAttachment {
(identical(other.refId, refId) || other.refId == refId) && (identical(other.refId, refId) || other.refId == refId) &&
(identical(other.pool, pool) || other.pool == pool) && (identical(other.pool, pool) || other.pool == pool) &&
(identical(other.poolId, poolId) || other.poolId == poolId) && (identical(other.poolId, poolId) || other.poolId == poolId) &&
(identical(other.account, account) || other.account == account) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
other.accountId == accountId) && other.accountId == accountId) &&
(identical(other.thumbnailId, thumbnailId) || (identical(other.thumbnailId, thumbnailId) ||
@ -140,6 +142,7 @@ mixin _$SnAttachment {
refId, refId,
pool, pool,
poolId, poolId,
account,
accountId, accountId,
thumbnailId, thumbnailId,
thumbnail, thumbnail,
@ -152,7 +155,7 @@ mixin _$SnAttachment {
@override @override
String toString() { String toString() {
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)'; return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
} }
} }
@ -186,6 +189,7 @@ abstract mixin class $SnAttachmentCopyWith<$Res> {
int? refId, int? refId,
SnAttachmentPool? pool, SnAttachmentPool? pool,
int? poolId, int? poolId,
SnAccount? account,
int accountId, int accountId,
int? thumbnailId, int? thumbnailId,
SnAttachment? thumbnail, SnAttachment? thumbnail,
@ -197,6 +201,7 @@ abstract mixin class $SnAttachmentCopyWith<$Res> {
$SnAttachmentCopyWith<$Res>? get ref; $SnAttachmentCopyWith<$Res>? get ref;
$SnAttachmentPoolCopyWith<$Res>? get pool; $SnAttachmentPoolCopyWith<$Res>? get pool;
$SnAccountCopyWith<$Res>? get account;
$SnAttachmentCopyWith<$Res>? get thumbnail; $SnAttachmentCopyWith<$Res>? get thumbnail;
$SnAttachmentCopyWith<$Res>? get compressed; $SnAttachmentCopyWith<$Res>? get compressed;
} }
@ -236,6 +241,7 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
Object? refId = freezed, Object? refId = freezed,
Object? pool = freezed, Object? pool = freezed,
Object? poolId = freezed, Object? poolId = freezed,
Object? account = freezed,
Object? accountId = null, Object? accountId = null,
Object? thumbnailId = freezed, Object? thumbnailId = freezed,
Object? thumbnail = freezed, Object? thumbnail = freezed,
@ -338,6 +344,10 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
? _self.poolId ? _self.poolId
: poolId // ignore: cast_nullable_to_non_nullable : poolId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
account: freezed == account
? _self.account
: account // ignore: cast_nullable_to_non_nullable
as SnAccount?,
accountId: null == accountId accountId: null == accountId
? _self.accountId ? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
@ -401,6 +411,20 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
}); });
} }
/// Create a copy of SnAttachment
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
/// Create a copy of SnAttachment /// Create a copy of SnAttachment
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@ -457,6 +481,7 @@ class _SnAttachment extends SnAttachment {
required this.refId, required this.refId,
required this.pool, required this.pool,
required this.poolId, required this.poolId,
required this.account,
required this.accountId, required this.accountId,
this.thumbnailId, this.thumbnailId,
this.thumbnail, this.thumbnail,
@ -521,6 +546,8 @@ class _SnAttachment extends SnAttachment {
@override @override
final int? poolId; final int? poolId;
@override @override
final SnAccount? account;
@override
final int accountId; final int accountId;
@override @override
final int? thumbnailId; final int? thumbnailId;
@ -612,6 +639,7 @@ class _SnAttachment extends SnAttachment {
(identical(other.refId, refId) || other.refId == refId) && (identical(other.refId, refId) || other.refId == refId) &&
(identical(other.pool, pool) || other.pool == pool) && (identical(other.pool, pool) || other.pool == pool) &&
(identical(other.poolId, poolId) || other.poolId == poolId) && (identical(other.poolId, poolId) || other.poolId == poolId) &&
(identical(other.account, account) || other.account == account) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
other.accountId == accountId) && other.accountId == accountId) &&
(identical(other.thumbnailId, thumbnailId) || (identical(other.thumbnailId, thumbnailId) ||
@ -654,6 +682,7 @@ class _SnAttachment extends SnAttachment {
refId, refId,
pool, pool,
poolId, poolId,
account,
accountId, accountId,
thumbnailId, thumbnailId,
thumbnail, thumbnail,
@ -666,7 +695,7 @@ class _SnAttachment extends SnAttachment {
@override @override
String toString() { String toString() {
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)'; return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
} }
} }
@ -702,6 +731,7 @@ abstract mixin class _$SnAttachmentCopyWith<$Res>
int? refId, int? refId,
SnAttachmentPool? pool, SnAttachmentPool? pool,
int? poolId, int? poolId,
SnAccount? account,
int accountId, int accountId,
int? thumbnailId, int? thumbnailId,
SnAttachment? thumbnail, SnAttachment? thumbnail,
@ -716,6 +746,8 @@ abstract mixin class _$SnAttachmentCopyWith<$Res>
@override @override
$SnAttachmentPoolCopyWith<$Res>? get pool; $SnAttachmentPoolCopyWith<$Res>? get pool;
@override @override
$SnAccountCopyWith<$Res>? get account;
@override
$SnAttachmentCopyWith<$Res>? get thumbnail; $SnAttachmentCopyWith<$Res>? get thumbnail;
@override @override
$SnAttachmentCopyWith<$Res>? get compressed; $SnAttachmentCopyWith<$Res>? get compressed;
@ -757,6 +789,7 @@ class __$SnAttachmentCopyWithImpl<$Res>
Object? refId = freezed, Object? refId = freezed,
Object? pool = freezed, Object? pool = freezed,
Object? poolId = freezed, Object? poolId = freezed,
Object? account = freezed,
Object? accountId = null, Object? accountId = null,
Object? thumbnailId = freezed, Object? thumbnailId = freezed,
Object? thumbnail = freezed, Object? thumbnail = freezed,
@ -859,6 +892,10 @@ class __$SnAttachmentCopyWithImpl<$Res>
? _self.poolId ? _self.poolId
: poolId // ignore: cast_nullable_to_non_nullable : poolId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
account: freezed == account
? _self.account
: account // ignore: cast_nullable_to_non_nullable
as SnAccount?,
accountId: null == accountId accountId: null == accountId
? _self.accountId ? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
@ -922,6 +959,20 @@ class __$SnAttachmentCopyWithImpl<$Res>
}); });
} }
/// Create a copy of SnAttachment
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
/// Create a copy of SnAttachment /// Create a copy of SnAttachment
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override

View File

@ -39,6 +39,9 @@ _SnAttachment _$SnAttachmentFromJson(Map<String, dynamic> json) =>
? null ? null
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>), : SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
poolId: (json['pool_id'] as num?)?.toInt(), poolId: (json['pool_id'] as num?)?.toInt(),
account: json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(), thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
thumbnail: json['thumbnail'] == null thumbnail: json['thumbnail'] == null
@ -82,6 +85,7 @@ Map<String, dynamic> _$SnAttachmentToJson(_SnAttachment instance) =>
'ref_id': instance.refId, 'ref_id': instance.refId,
'pool': instance.pool?.toJson(), 'pool': instance.pool?.toJson(),
'pool_id': instance.poolId, 'pool_id': instance.poolId,
'account': instance.account?.toJson(),
'account_id': instance.accountId, 'account_id': instance.accountId,
'thumbnail_id': instance.thumbnailId, 'thumbnail_id': instance.thumbnailId,
'thumbnail': instance.thumbnail?.toJson(), 'thumbnail': instance.thumbnail?.toJson(),

View File

@ -1,4 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:surface/types/account.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/types/poll.dart'; import 'package:surface/types/poll.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
@ -26,6 +27,7 @@ abstract class SnPost with _$SnPost {
required int? replyId, required int? replyId,
required int? repostId, required int? repostId,
required int? realmId, required int? realmId,
required SnRealm? realm,
required SnPost? replyTo, required SnPost? replyTo,
required SnPost? repostTo, required SnPost? repostTo,
required List<int>? visibleUsersList, required List<int>? visibleUsersList,
@ -43,9 +45,9 @@ abstract class SnPost with _$SnPost {
@Default(0) int totalAggregatedViews, @Default(0) int totalAggregatedViews,
required int publisherId, required int publisherId,
required int? pollId, required int? pollId,
required SnPoll? poll,
required SnPublisher publisher, required SnPublisher publisher,
required SnMetric metric, required SnMetric metric,
SnPostPreload? preload,
}) = _SnPost; }) = _SnPost;
factory SnPost.fromJson(Map<String, Object?> json) => _$SnPostFromJson(json); factory SnPost.fromJson(Map<String, Object?> json) => _$SnPostFromJson(json);
@ -146,6 +148,7 @@ abstract class SnPublisher with _$SnPublisher {
required int totalDownvote, required int totalDownvote,
required int? realmId, required int? realmId,
required int accountId, required int accountId,
required SnAccount? account,
}) = _SnPublisher; }) = _SnPublisher;
factory SnPublisher.fromJson(Map<String, Object?> json) => factory SnPublisher.fromJson(Map<String, Object?> json) =>

View File

@ -30,6 +30,7 @@ mixin _$SnPost {
int? get replyId; int? get replyId;
int? get repostId; int? get repostId;
int? get realmId; int? get realmId;
SnRealm? get realm;
SnPost? get replyTo; SnPost? get replyTo;
SnPost? get repostTo; SnPost? get repostTo;
List<int>? get visibleUsersList; List<int>? get visibleUsersList;
@ -47,9 +48,9 @@ mixin _$SnPost {
int get totalAggregatedViews; int get totalAggregatedViews;
int get publisherId; int get publisherId;
int? get pollId; int? get pollId;
SnPoll? get poll;
SnPublisher get publisher; SnPublisher get publisher;
SnMetric get metric; SnMetric get metric;
SnPostPreload? get preload;
/// Create a copy of SnPost /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -88,6 +89,7 @@ mixin _$SnPost {
(identical(other.repostId, repostId) || (identical(other.repostId, repostId) ||
other.repostId == repostId) && other.repostId == repostId) &&
(identical(other.realmId, realmId) || other.realmId == realmId) && (identical(other.realmId, realmId) || other.realmId == realmId) &&
(identical(other.realm, realm) || other.realm == realm) &&
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) && (identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
(identical(other.repostTo, repostTo) || (identical(other.repostTo, repostTo) ||
other.repostTo == repostTo) && other.repostTo == repostTo) &&
@ -119,10 +121,10 @@ mixin _$SnPost {
(identical(other.publisherId, publisherId) || (identical(other.publisherId, publisherId) ||
other.publisherId == publisherId) && other.publisherId == publisherId) &&
(identical(other.pollId, pollId) || other.pollId == pollId) && (identical(other.pollId, pollId) || other.pollId == pollId) &&
(identical(other.poll, poll) || other.poll == poll) &&
(identical(other.publisher, publisher) || (identical(other.publisher, publisher) ||
other.publisher == publisher) && other.publisher == publisher) &&
(identical(other.metric, metric) || other.metric == metric) && (identical(other.metric, metric) || other.metric == metric));
(identical(other.preload, preload) || other.preload == preload));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -144,6 +146,7 @@ mixin _$SnPost {
replyId, replyId,
repostId, repostId,
realmId, realmId,
realm,
replyTo, replyTo,
repostTo, repostTo,
const DeepCollectionEquality().hash(visibleUsersList), const DeepCollectionEquality().hash(visibleUsersList),
@ -161,14 +164,14 @@ mixin _$SnPost {
totalAggregatedViews, totalAggregatedViews,
publisherId, publisherId,
pollId, pollId,
poll,
publisher, publisher,
metric, metric
preload
]); ]);
@override @override
String toString() { String toString() {
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)'; return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, realm: $realm, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, poll: $poll, publisher: $publisher, metric: $metric)';
} }
} }
@ -193,6 +196,7 @@ abstract mixin class $SnPostCopyWith<$Res> {
int? replyId, int? replyId,
int? repostId, int? repostId,
int? realmId, int? realmId,
SnRealm? realm,
SnPost? replyTo, SnPost? replyTo,
SnPost? repostTo, SnPost? repostTo,
List<int>? visibleUsersList, List<int>? visibleUsersList,
@ -210,15 +214,16 @@ abstract mixin class $SnPostCopyWith<$Res> {
int totalAggregatedViews, int totalAggregatedViews,
int publisherId, int publisherId,
int? pollId, int? pollId,
SnPoll? poll,
SnPublisher publisher, SnPublisher publisher,
SnMetric metric, SnMetric metric});
SnPostPreload? preload});
$SnRealmCopyWith<$Res>? get realm;
$SnPostCopyWith<$Res>? get replyTo; $SnPostCopyWith<$Res>? get replyTo;
$SnPostCopyWith<$Res>? get repostTo; $SnPostCopyWith<$Res>? get repostTo;
$SnPollCopyWith<$Res>? get poll;
$SnPublisherCopyWith<$Res> get publisher; $SnPublisherCopyWith<$Res> get publisher;
$SnMetricCopyWith<$Res> get metric; $SnMetricCopyWith<$Res> get metric;
$SnPostPreloadCopyWith<$Res>? get preload;
} }
/// @nodoc /// @nodoc
@ -248,6 +253,7 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
Object? replyId = freezed, Object? replyId = freezed,
Object? repostId = freezed, Object? repostId = freezed,
Object? realmId = freezed, Object? realmId = freezed,
Object? realm = freezed,
Object? replyTo = freezed, Object? replyTo = freezed,
Object? repostTo = freezed, Object? repostTo = freezed,
Object? visibleUsersList = freezed, Object? visibleUsersList = freezed,
@ -265,9 +271,9 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
Object? totalAggregatedViews = null, Object? totalAggregatedViews = null,
Object? publisherId = null, Object? publisherId = null,
Object? pollId = freezed, Object? pollId = freezed,
Object? poll = freezed,
Object? publisher = null, Object? publisher = null,
Object? metric = null, Object? metric = null,
Object? preload = freezed,
}) { }) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id id: null == id
@ -330,6 +336,10 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
? _self.realmId ? _self.realmId
: realmId // ignore: cast_nullable_to_non_nullable : realmId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
realm: freezed == realm
? _self.realm
: realm // ignore: cast_nullable_to_non_nullable
as SnRealm?,
replyTo: freezed == replyTo replyTo: freezed == replyTo
? _self.replyTo ? _self.replyTo
: replyTo // ignore: cast_nullable_to_non_nullable : replyTo // ignore: cast_nullable_to_non_nullable
@ -398,6 +408,10 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
? _self.pollId ? _self.pollId
: pollId // ignore: cast_nullable_to_non_nullable : pollId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
poll: freezed == poll
? _self.poll
: poll // ignore: cast_nullable_to_non_nullable
as SnPoll?,
publisher: null == publisher publisher: null == publisher
? _self.publisher ? _self.publisher
: publisher // ignore: cast_nullable_to_non_nullable : publisher // ignore: cast_nullable_to_non_nullable
@ -406,13 +420,23 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
? _self.metric ? _self.metric
: metric // ignore: cast_nullable_to_non_nullable : metric // ignore: cast_nullable_to_non_nullable
as SnMetric, as SnMetric,
preload: freezed == preload
? _self.preload
: preload // ignore: cast_nullable_to_non_nullable
as SnPostPreload?,
)); ));
} }
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnRealmCopyWith<$Res>? get realm {
if (_self.realm == null) {
return null;
}
return $SnRealmCopyWith<$Res>(_self.realm!, (value) {
return _then(_self.copyWith(realm: value));
});
}
/// Create a copy of SnPost /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@ -441,6 +465,20 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
}); });
} }
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPollCopyWith<$Res>? get poll {
if (_self.poll == null) {
return null;
}
return $SnPollCopyWith<$Res>(_self.poll!, (value) {
return _then(_self.copyWith(poll: value));
});
}
/// Create a copy of SnPost /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@ -460,20 +498,6 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
return _then(_self.copyWith(metric: value)); return _then(_self.copyWith(metric: value));
}); });
} }
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostPreloadCopyWith<$Res>? get preload {
if (_self.preload == null) {
return null;
}
return $SnPostPreloadCopyWith<$Res>(_self.preload!, (value) {
return _then(_self.copyWith(preload: value));
});
}
} }
/// @nodoc /// @nodoc
@ -495,6 +519,7 @@ class _SnPost extends SnPost {
required this.replyId, required this.replyId,
required this.repostId, required this.repostId,
required this.realmId, required this.realmId,
required this.realm,
required this.replyTo, required this.replyTo,
required this.repostTo, required this.repostTo,
required final List<int>? visibleUsersList, required final List<int>? visibleUsersList,
@ -512,9 +537,9 @@ class _SnPost extends SnPost {
this.totalAggregatedViews = 0, this.totalAggregatedViews = 0,
required this.publisherId, required this.publisherId,
required this.pollId, required this.pollId,
required this.poll,
required this.publisher, required this.publisher,
required this.metric, required this.metric})
this.preload})
: _body = body, : _body = body,
_tags = tags, _tags = tags,
_categories = categories, _categories = categories,
@ -583,6 +608,8 @@ class _SnPost extends SnPost {
@override @override
final int? realmId; final int? realmId;
@override @override
final SnRealm? realm;
@override
final SnPost? replyTo; final SnPost? replyTo;
@override @override
final SnPost? repostTo; final SnPost? repostTo;
@ -637,11 +664,11 @@ class _SnPost extends SnPost {
@override @override
final int? pollId; final int? pollId;
@override @override
final SnPoll? poll;
@override
final SnPublisher publisher; final SnPublisher publisher;
@override @override
final SnMetric metric; final SnMetric metric;
@override
final SnPostPreload? preload;
/// Create a copy of SnPost /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -685,6 +712,7 @@ class _SnPost extends SnPost {
(identical(other.repostId, repostId) || (identical(other.repostId, repostId) ||
other.repostId == repostId) && other.repostId == repostId) &&
(identical(other.realmId, realmId) || other.realmId == realmId) && (identical(other.realmId, realmId) || other.realmId == realmId) &&
(identical(other.realm, realm) || other.realm == realm) &&
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) && (identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
(identical(other.repostTo, repostTo) || (identical(other.repostTo, repostTo) ||
other.repostTo == repostTo) && other.repostTo == repostTo) &&
@ -716,10 +744,10 @@ class _SnPost extends SnPost {
(identical(other.publisherId, publisherId) || (identical(other.publisherId, publisherId) ||
other.publisherId == publisherId) && other.publisherId == publisherId) &&
(identical(other.pollId, pollId) || other.pollId == pollId) && (identical(other.pollId, pollId) || other.pollId == pollId) &&
(identical(other.poll, poll) || other.poll == poll) &&
(identical(other.publisher, publisher) || (identical(other.publisher, publisher) ||
other.publisher == publisher) && other.publisher == publisher) &&
(identical(other.metric, metric) || other.metric == metric) && (identical(other.metric, metric) || other.metric == metric));
(identical(other.preload, preload) || other.preload == preload));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -741,6 +769,7 @@ class _SnPost extends SnPost {
replyId, replyId,
repostId, repostId,
realmId, realmId,
realm,
replyTo, replyTo,
repostTo, repostTo,
const DeepCollectionEquality().hash(_visibleUsersList), const DeepCollectionEquality().hash(_visibleUsersList),
@ -758,14 +787,14 @@ class _SnPost extends SnPost {
totalAggregatedViews, totalAggregatedViews,
publisherId, publisherId,
pollId, pollId,
poll,
publisher, publisher,
metric, metric
preload
]); ]);
@override @override
String toString() { String toString() {
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)'; return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, realm: $realm, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, poll: $poll, publisher: $publisher, metric: $metric)';
} }
} }
@ -791,6 +820,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
int? replyId, int? replyId,
int? repostId, int? repostId,
int? realmId, int? realmId,
SnRealm? realm,
SnPost? replyTo, SnPost? replyTo,
SnPost? repostTo, SnPost? repostTo,
List<int>? visibleUsersList, List<int>? visibleUsersList,
@ -808,20 +838,22 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
int totalAggregatedViews, int totalAggregatedViews,
int publisherId, int publisherId,
int? pollId, int? pollId,
SnPoll? poll,
SnPublisher publisher, SnPublisher publisher,
SnMetric metric, SnMetric metric});
SnPostPreload? preload});
@override
$SnRealmCopyWith<$Res>? get realm;
@override @override
$SnPostCopyWith<$Res>? get replyTo; $SnPostCopyWith<$Res>? get replyTo;
@override @override
$SnPostCopyWith<$Res>? get repostTo; $SnPostCopyWith<$Res>? get repostTo;
@override @override
$SnPollCopyWith<$Res>? get poll;
@override
$SnPublisherCopyWith<$Res> get publisher; $SnPublisherCopyWith<$Res> get publisher;
@override @override
$SnMetricCopyWith<$Res> get metric; $SnMetricCopyWith<$Res> get metric;
@override
$SnPostPreloadCopyWith<$Res>? get preload;
} }
/// @nodoc /// @nodoc
@ -851,6 +883,7 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
Object? replyId = freezed, Object? replyId = freezed,
Object? repostId = freezed, Object? repostId = freezed,
Object? realmId = freezed, Object? realmId = freezed,
Object? realm = freezed,
Object? replyTo = freezed, Object? replyTo = freezed,
Object? repostTo = freezed, Object? repostTo = freezed,
Object? visibleUsersList = freezed, Object? visibleUsersList = freezed,
@ -868,9 +901,9 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
Object? totalAggregatedViews = null, Object? totalAggregatedViews = null,
Object? publisherId = null, Object? publisherId = null,
Object? pollId = freezed, Object? pollId = freezed,
Object? poll = freezed,
Object? publisher = null, Object? publisher = null,
Object? metric = null, Object? metric = null,
Object? preload = freezed,
}) { }) {
return _then(_SnPost( return _then(_SnPost(
id: null == id id: null == id
@ -933,6 +966,10 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
? _self.realmId ? _self.realmId
: realmId // ignore: cast_nullable_to_non_nullable : realmId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
realm: freezed == realm
? _self.realm
: realm // ignore: cast_nullable_to_non_nullable
as SnRealm?,
replyTo: freezed == replyTo replyTo: freezed == replyTo
? _self.replyTo ? _self.replyTo
: replyTo // ignore: cast_nullable_to_non_nullable : replyTo // ignore: cast_nullable_to_non_nullable
@ -1001,6 +1038,10 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
? _self.pollId ? _self.pollId
: pollId // ignore: cast_nullable_to_non_nullable : pollId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
poll: freezed == poll
? _self.poll
: poll // ignore: cast_nullable_to_non_nullable
as SnPoll?,
publisher: null == publisher publisher: null == publisher
? _self.publisher ? _self.publisher
: publisher // ignore: cast_nullable_to_non_nullable : publisher // ignore: cast_nullable_to_non_nullable
@ -1009,13 +1050,23 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
? _self.metric ? _self.metric
: metric // ignore: cast_nullable_to_non_nullable : metric // ignore: cast_nullable_to_non_nullable
as SnMetric, as SnMetric,
preload: freezed == preload
? _self.preload
: preload // ignore: cast_nullable_to_non_nullable
as SnPostPreload?,
)); ));
} }
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnRealmCopyWith<$Res>? get realm {
if (_self.realm == null) {
return null;
}
return $SnRealmCopyWith<$Res>(_self.realm!, (value) {
return _then(_self.copyWith(realm: value));
});
}
/// Create a copy of SnPost /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@ -1044,6 +1095,20 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
}); });
} }
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPollCopyWith<$Res>? get poll {
if (_self.poll == null) {
return null;
}
return $SnPollCopyWith<$Res>(_self.poll!, (value) {
return _then(_self.copyWith(poll: value));
});
}
/// Create a copy of SnPost /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@ -1063,20 +1128,6 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
return _then(_self.copyWith(metric: value)); return _then(_self.copyWith(metric: value));
}); });
} }
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostPreloadCopyWith<$Res>? get preload {
if (_self.preload == null) {
return null;
}
return $SnPostPreloadCopyWith<$Res>(_self.preload!, (value) {
return _then(_self.copyWith(preload: value));
});
}
} }
/// @nodoc /// @nodoc
@ -2465,6 +2516,7 @@ mixin _$SnPublisher {
int get totalDownvote; int get totalDownvote;
int? get realmId; int? get realmId;
int get accountId; int get accountId;
SnAccount? get account;
/// Create a copy of SnPublisher /// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -2501,7 +2553,8 @@ mixin _$SnPublisher {
other.totalDownvote == totalDownvote) && other.totalDownvote == totalDownvote) &&
(identical(other.realmId, realmId) || other.realmId == realmId) && (identical(other.realmId, realmId) || other.realmId == realmId) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
other.accountId == accountId)); other.accountId == accountId) &&
(identical(other.account, account) || other.account == account));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -2521,11 +2574,12 @@ mixin _$SnPublisher {
totalUpvote, totalUpvote,
totalDownvote, totalDownvote,
realmId, realmId,
accountId); accountId,
account);
@override @override
String toString() { String toString() {
return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId)'; return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId, account: $account)';
} }
} }
@ -2549,7 +2603,10 @@ abstract mixin class $SnPublisherCopyWith<$Res> {
int totalUpvote, int totalUpvote,
int totalDownvote, int totalDownvote,
int? realmId, int? realmId,
int accountId}); int accountId,
SnAccount? account});
$SnAccountCopyWith<$Res>? get account;
} }
/// @nodoc /// @nodoc
@ -2578,6 +2635,7 @@ class _$SnPublisherCopyWithImpl<$Res> implements $SnPublisherCopyWith<$Res> {
Object? totalDownvote = null, Object? totalDownvote = null,
Object? realmId = freezed, Object? realmId = freezed,
Object? accountId = null, Object? accountId = null,
Object? account = freezed,
}) { }) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id id: null == id
@ -2636,8 +2694,26 @@ class _$SnPublisherCopyWithImpl<$Res> implements $SnPublisherCopyWith<$Res> {
? _self.accountId ? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
as int, as int,
account: freezed == account
? _self.account
: account // ignore: cast_nullable_to_non_nullable
as SnAccount?,
)); ));
} }
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
} }
/// @nodoc /// @nodoc
@ -2657,7 +2733,8 @@ class _SnPublisher implements SnPublisher {
required this.totalUpvote, required this.totalUpvote,
required this.totalDownvote, required this.totalDownvote,
required this.realmId, required this.realmId,
required this.accountId}); required this.accountId,
required this.account});
factory _SnPublisher.fromJson(Map<String, dynamic> json) => factory _SnPublisher.fromJson(Map<String, dynamic> json) =>
_$SnPublisherFromJson(json); _$SnPublisherFromJson(json);
@ -2689,6 +2766,8 @@ class _SnPublisher implements SnPublisher {
final int? realmId; final int? realmId;
@override @override
final int accountId; final int accountId;
@override
final SnAccount? account;
/// Create a copy of SnPublisher /// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -2730,7 +2809,8 @@ class _SnPublisher implements SnPublisher {
other.totalDownvote == totalDownvote) && other.totalDownvote == totalDownvote) &&
(identical(other.realmId, realmId) || other.realmId == realmId) && (identical(other.realmId, realmId) || other.realmId == realmId) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
other.accountId == accountId)); other.accountId == accountId) &&
(identical(other.account, account) || other.account == account));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -2750,11 +2830,12 @@ class _SnPublisher implements SnPublisher {
totalUpvote, totalUpvote,
totalDownvote, totalDownvote,
realmId, realmId,
accountId); accountId,
account);
@override @override
String toString() { String toString() {
return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId)'; return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId, account: $account)';
} }
} }
@ -2780,7 +2861,11 @@ abstract mixin class _$SnPublisherCopyWith<$Res>
int totalUpvote, int totalUpvote,
int totalDownvote, int totalDownvote,
int? realmId, int? realmId,
int accountId}); int accountId,
SnAccount? account});
@override
$SnAccountCopyWith<$Res>? get account;
} }
/// @nodoc /// @nodoc
@ -2809,6 +2894,7 @@ class __$SnPublisherCopyWithImpl<$Res> implements _$SnPublisherCopyWith<$Res> {
Object? totalDownvote = null, Object? totalDownvote = null,
Object? realmId = freezed, Object? realmId = freezed,
Object? accountId = null, Object? accountId = null,
Object? account = freezed,
}) { }) {
return _then(_SnPublisher( return _then(_SnPublisher(
id: null == id id: null == id
@ -2867,8 +2953,26 @@ class __$SnPublisherCopyWithImpl<$Res> implements _$SnPublisherCopyWith<$Res> {
? _self.accountId ? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
as int, as int,
account: freezed == account
? _self.account
: account // ignore: cast_nullable_to_non_nullable
as SnAccount?,
)); ));
} }
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
} }
/// @nodoc /// @nodoc

View File

@ -32,6 +32,9 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
replyId: (json['reply_id'] as num?)?.toInt(), replyId: (json['reply_id'] as num?)?.toInt(),
repostId: (json['repost_id'] as num?)?.toInt(), repostId: (json['repost_id'] as num?)?.toInt(),
realmId: (json['realm_id'] as num?)?.toInt(), realmId: (json['realm_id'] as num?)?.toInt(),
realm: json['realm'] == null
? null
: SnRealm.fromJson(json['realm'] as Map<String, dynamic>),
replyTo: json['reply_to'] == null replyTo: json['reply_to'] == null
? null ? null
: SnPost.fromJson(json['reply_to'] as Map<String, dynamic>), : SnPost.fromJson(json['reply_to'] as Map<String, dynamic>),
@ -68,12 +71,12 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
(json['total_aggregated_views'] as num?)?.toInt() ?? 0, (json['total_aggregated_views'] as num?)?.toInt() ?? 0,
publisherId: (json['publisher_id'] as num).toInt(), publisherId: (json['publisher_id'] as num).toInt(),
pollId: (json['poll_id'] as num?)?.toInt(), pollId: (json['poll_id'] as num?)?.toInt(),
poll: json['poll'] == null
? null
: SnPoll.fromJson(json['poll'] as Map<String, dynamic>),
publisher: publisher:
SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>), SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
metric: SnMetric.fromJson(json['metric'] as Map<String, dynamic>), metric: SnMetric.fromJson(json['metric'] as Map<String, dynamic>),
preload: json['preload'] == null
? null
: SnPostPreload.fromJson(json['preload'] as Map<String, dynamic>),
); );
Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
@ -92,6 +95,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'reply_id': instance.replyId, 'reply_id': instance.replyId,
'repost_id': instance.repostId, 'repost_id': instance.repostId,
'realm_id': instance.realmId, 'realm_id': instance.realmId,
'realm': instance.realm?.toJson(),
'reply_to': instance.replyTo?.toJson(), 'reply_to': instance.replyTo?.toJson(),
'repost_to': instance.repostTo?.toJson(), 'repost_to': instance.repostTo?.toJson(),
'visible_users_list': instance.visibleUsersList, 'visible_users_list': instance.visibleUsersList,
@ -109,9 +113,9 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'total_aggregated_views': instance.totalAggregatedViews, 'total_aggregated_views': instance.totalAggregatedViews,
'publisher_id': instance.publisherId, 'publisher_id': instance.publisherId,
'poll_id': instance.pollId, 'poll_id': instance.pollId,
'poll': instance.poll?.toJson(),
'publisher': instance.publisher.toJson(), 'publisher': instance.publisher.toJson(),
'metric': instance.metric.toJson(), 'metric': instance.metric.toJson(),
'preload': instance.preload?.toJson(),
}; };
_SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) => _SnPostTag( _SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) => _SnPostTag(
@ -241,6 +245,9 @@ _SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
totalDownvote: (json['total_downvote'] as num).toInt(), totalDownvote: (json['total_downvote'] as num).toInt(),
realmId: (json['realm_id'] as num?)?.toInt(), realmId: (json['realm_id'] as num?)?.toInt(),
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
account: json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
); );
Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) => Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
@ -259,6 +266,7 @@ Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
'total_downvote': instance.totalDownvote, 'total_downvote': instance.totalDownvote,
'realm_id': instance.realmId, 'realm_id': instance.realmId,
'account_id': instance.accountId, 'account_id': instance.accountId,
'account': instance.account?.toJson(),
}; };
_SnSubscription _$SnSubscriptionFromJson(Map<String, dynamic> json) => _SnSubscription _$SnSubscriptionFromJson(Map<String, dynamic> json) =>

View File

@ -17,4 +17,5 @@ const Map<String, ReactInfo> kTemplateReactions = {
'party': ReactInfo(icon: '🎉', attitude: 1), 'party': ReactInfo(icon: '🎉', attitude: 1),
'joy': ReactInfo(icon: '🤣', attitude: 1), 'joy': ReactInfo(icon: '🤣', attitude: 1),
'pray': ReactInfo(icon: '🙏', attitude: 1), 'pray': ReactInfo(icon: '🙏', attitude: 1),
'heart': ReactInfo(icon: '❤️', attitude: 1),
}; };

View File

@ -54,11 +54,15 @@ class AccountImage extends StatelessWidget {
)) ))
.center(), .center(),
) )
: AutoResizeUniversalImage( : UniversalImage(
sn.getAttachmentUrl(url), sn.getAttachmentUrl(url),
filterQuality: filterQuality, filterQuality: filterQuality,
key: Key('attachment-${content.hashCode}'), key: Key('attachment-${content.hashCode}'),
fit: BoxFit.cover, fit: BoxFit.cover,
width: (radius != null ? radius! : 20) * 2,
height: (radius != null ? radius! : 20) * 2,
cacheWidth: (radius != null ? radius! : 20) * 2,
cacheHeight: (radius != null ? radius! : 20) * 2,
), ),
), ),
), ),

View File

@ -16,7 +16,6 @@ import 'package:photo_view/photo_view_gallery.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
@ -418,8 +417,7 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ud = context.read<UserDirectoryProvider>(); final account = data.account!;
final account = ud.getFromCache(data.accountId);
const tableGap = TableRow( const tableGap = TableRow(
children: [ children: [
@ -461,12 +459,12 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
children: [ children: [
if (data.accountId > 0) if (data.accountId > 0)
AccountImage( AccountImage(
content: account?.avatar, content: account.avatar,
radius: 8, radius: 8,
), ),
const Gap(8), const Gap(8),
Text(data.accountId > 0 Text(data.accountId > 0
? account?.nick ?? 'unknown'.tr() ? account.nick
: 'unknown'.tr()), : 'unknown'.tr()),
const Gap(8), const Gap(8),
Text('#${data.accountId}', Text('#${data.accountId}',

View File

@ -8,12 +8,12 @@ import 'package:surface/widgets/account/account_image.dart';
class NoContentWidget extends StatefulWidget { class NoContentWidget extends StatefulWidget {
final SnAccount? userinfo; final SnAccount? userinfo;
final bool isSpeaking; final bool isSpeaking;
final bool isFixed; final double? avatarSize;
const NoContentWidget({ const NoContentWidget({
super.key, super.key,
this.userinfo, this.userinfo,
this.isFixed = false, this.avatarSize,
required this.isSpeaking, required this.isSpeaking,
}); });
@ -45,41 +45,35 @@ class _NoContentWidgetState extends State<NoContentWidget>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double radius = widget.isFixed final double radius = widget.avatarSize ??
? 32 math.min(
: math.min( MediaQuery.of(context).size.width * 0.1,
MediaQuery.of(context).size.width * 0.1, MediaQuery.of(context).size.height * 0.1,
MediaQuery.of(context).size.height * 0.1, );
);
return Container( return Animate(
alignment: Alignment.center, autoPlay: false,
child: Center( controller: _animationController,
child: Animate( effects: [
autoPlay: false, CustomEffect(
controller: _animationController, begin: widget.isSpeaking ? 2 : 0,
effects: [ end: 8,
CustomEffect( curve: Curves.easeInOut,
begin: widget.isSpeaking ? 2 : 0, duration: 1250.ms,
end: 8, builder: (context, value, child) => Container(
curve: Curves.easeInOut, decoration: BoxDecoration(
duration: 1250.ms, borderRadius: BorderRadius.all(Radius.circular(radius + 8)),
builder: (context, value, child) => Container( border: value > 0
decoration: BoxDecoration( ? Border.all(color: Colors.green, width: value)
borderRadius: BorderRadius.all(Radius.circular(radius + 8)), : null,
border: value > 0 ),
? Border.all(color: Colors.green, width: value) child: child,
: null,
),
child: child,
),
)
],
child: AccountImage(
content: widget.userinfo?.avatar,
radius: radius,
), ),
), )
],
child: AccountImage(
content: widget.userinfo?.avatar,
radius: radius,
), ),
); );
} }

View File

@ -2,7 +2,9 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:gap/gap.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/types/account.dart'; import 'package:surface/types/account.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/widgets/chat/call/call_no_content.dart'; import 'package:surface/widgets/chat/call/call_no_content.dart';
@ -11,23 +13,32 @@ import 'package:surface/widgets/chat/call/call_participant_menu.dart';
import 'package:surface/widgets/chat/call/call_participant_stats.dart'; import 'package:surface/widgets/chat/call/call_participant_stats.dart';
abstract class ParticipantWidget extends StatefulWidget { abstract class ParticipantWidget extends StatefulWidget {
static ParticipantWidget widgetFor(ParticipantTrack participantTrack, static ParticipantWidget widgetFor(
{bool isFixed = false, bool showStatsLayer = false}) { ParticipantTrack participantTrack, {
double? avatarSize,
EdgeInsets? padding,
bool showStatsLayer = false,
bool isList = false,
}) {
if (participantTrack.participant is LocalParticipant) { if (participantTrack.participant is LocalParticipant) {
return LocalParticipantWidget( return LocalParticipantWidget(
participantTrack.participant as LocalParticipant, participantTrack.participant as LocalParticipant,
participantTrack.videoTrack, participantTrack.videoTrack,
isFixed, avatarSize,
participantTrack.isScreenShare, participantTrack.isScreenShare,
showStatsLayer, showStatsLayer,
isList,
padding,
); );
} else if (participantTrack.participant is RemoteParticipant) { } else if (participantTrack.participant is RemoteParticipant) {
return RemoteParticipantWidget( return RemoteParticipantWidget(
participantTrack.participant as RemoteParticipant, participantTrack.participant as RemoteParticipant,
participantTrack.videoTrack, participantTrack.videoTrack,
isFixed, avatarSize,
participantTrack.isScreenShare, participantTrack.isScreenShare,
showStatsLayer, showStatsLayer,
isList,
padding,
); );
} }
throw UnimplementedError('Unknown participant type'); throw UnimplementedError('Unknown participant type');
@ -36,8 +47,10 @@ abstract class ParticipantWidget extends StatefulWidget {
abstract final Participant participant; abstract final Participant participant;
abstract final VideoTrack? videoTrack; abstract final VideoTrack? videoTrack;
abstract final bool isScreenShare; abstract final bool isScreenShare;
abstract final bool isFixed; abstract final double? avatarSize;
abstract final bool showStatsLayer; abstract final bool showStatsLayer;
abstract final bool isList;
abstract final EdgeInsets? padding;
final VideoQuality quality; final VideoQuality quality;
const ParticipantWidget({ const ParticipantWidget({
@ -52,18 +65,24 @@ class LocalParticipantWidget extends ParticipantWidget {
@override @override
final VideoTrack? videoTrack; final VideoTrack? videoTrack;
@override @override
final bool isFixed; final double? avatarSize;
@override @override
final bool isScreenShare; final bool isScreenShare;
@override @override
final bool showStatsLayer; final bool showStatsLayer;
@override
final bool isList;
@override
final EdgeInsets? padding;
const LocalParticipantWidget( const LocalParticipantWidget(
this.participant, this.participant,
this.videoTrack, this.videoTrack,
this.isFixed, this.avatarSize,
this.isScreenShare, this.isScreenShare,
this.showStatsLayer, { this.showStatsLayer,
this.isList,
this.padding, {
super.key, super.key,
}); });
@ -77,18 +96,24 @@ class RemoteParticipantWidget extends ParticipantWidget {
@override @override
final VideoTrack? videoTrack; final VideoTrack? videoTrack;
@override @override
final bool isFixed; final double? avatarSize;
@override @override
final bool isScreenShare; final bool isScreenShare;
@override @override
final bool showStatsLayer; final bool showStatsLayer;
@override
final bool isList;
@override
final EdgeInsets? padding;
const RemoteParticipantWidget( const RemoteParticipantWidget(
this.participant, this.participant,
this.videoTrack, this.videoTrack,
this.isFixed, this.avatarSize,
this.isScreenShare, this.isScreenShare,
this.showStatsLayer, { this.showStatsLayer,
this.isList,
this.padding, {
super.key, super.key,
}); });
@ -136,19 +161,82 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
} }
@override @override
Widget build(BuildContext ctx) { Widget build(BuildContext context) {
if (widget.isList) {
return Padding(
padding: widget.padding ?? EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
SizedBox(
width: (widget.avatarSize ?? 32) * 2,
height: (widget.avatarSize ?? 32) * 2,
child: Center(
child: NoContentWidget(
userinfo: _userinfoMetadata,
avatarSize: widget.avatarSize,
isSpeaking: widget.participant.isSpeaking,
),
),
),
const Gap(8),
Expanded(
child: SizedBox(
height: (widget.avatarSize ?? 32) * 2,
child: ParticipantInfoWidget(
isList: true,
title: widget.participant.name.isNotEmpty
? widget.participant.name
: widget.participant.identity,
audioAvailable: _firstAudioPublication?.muted == false &&
_firstAudioPublication?.subscribed == true,
connectionQuality: widget.participant.connectionQuality,
isScreenShare: widget.isScreenShare,
),
),
),
],
),
if (_activeVideoTrack != null && !_activeVideoTrack!.muted)
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Material(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.75),
child: VideoTrackRenderer(
_activeVideoTrack!,
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
),
),
).padding(top: 8),
),
],
),
);
}
return Stack( return Stack(
children: [ children: [
_activeVideoTrack != null && !_activeVideoTrack!.muted if (_activeVideoTrack != null && !_activeVideoTrack!.muted)
? VideoTrackRenderer( VideoTrackRenderer(
_activeVideoTrack!, _activeVideoTrack!,
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
) )
: NoContentWidget( else
userinfo: _userinfoMetadata, Center(
isFixed: widget.isFixed, child: NoContentWidget(
isSpeaking: widget.participant.isSpeaking, userinfo: _userinfoMetadata,
), avatarSize: widget.avatarSize,
isSpeaking: widget.participant.isSpeaking,
),
),
if (widget.showStatsLayer) if (widget.showStatsLayer)
Positioned( Positioned(
top: 30, top: 30,
@ -199,44 +287,51 @@ class _RemoteParticipantWidgetState
} }
class InteractiveParticipantWidget extends StatelessWidget { class InteractiveParticipantWidget extends StatelessWidget {
final double? width; final double? avatarSize;
final double? height; final bool isList;
final Color? color;
final bool isFixedAvatar;
final ParticipantTrack participant; final ParticipantTrack participant;
final Function() onTap; final Function? onTap;
final EdgeInsets? padding;
const InteractiveParticipantWidget({ const InteractiveParticipantWidget({
super.key, super.key,
this.width, this.avatarSize,
this.height, this.isList = false,
this.color, this.padding,
this.isFixedAvatar = false,
required this.participant, required this.participant,
required this.onTap, this.onTap,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return Material(
child: Container( color: Colors.transparent,
width: width, child: InkWell(
height: height, onTap: onTap != null
color: color, ? () {
child: ParticipantWidget.widgetFor(participant, isFixed: isFixedAvatar), onTap?.call();
), }
onTap: () => onTap(), : null,
onLongPress: () { onLongPress: () {
if (participant.participant is LocalParticipant) return; if (participant.participant is LocalParticipant) return;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => ParticipantMenu( builder: (context) => ParticipantMenu(
participant: participant.participant as RemoteParticipant, participant: participant.participant as RemoteParticipant,
videoTrack: participant.videoTrack, videoTrack: participant.videoTrack,
isScreenShare: participant.isScreenShare, isScreenShare: participant.isScreenShare,
),
);
},
child: Container(
child: ParticipantWidget.widgetFor(
participant,
avatarSize: avatarSize,
isList: isList,
padding: padding,
), ),
); ),
}, ),
); );
} }
} }

View File

@ -9,6 +9,7 @@ class ParticipantInfoWidget extends StatelessWidget {
final bool audioAvailable; final bool audioAvailable;
final ConnectionQuality connectionQuality; final ConnectionQuality connectionQuality;
final bool isScreenShare; final bool isScreenShare;
final bool isList;
const ParticipantInfoWidget({ const ParticipantInfoWidget({
super.key, super.key,
@ -16,64 +17,124 @@ class ParticipantInfoWidget extends StatelessWidget {
this.audioAvailable = true, this.audioAvailable = true,
this.connectionQuality = ConnectionQuality.unknown, this.connectionQuality = ConnectionQuality.unknown,
this.isScreenShare = false, this.isScreenShare = false,
this.isList = false,
}); });
@override @override
Widget build(BuildContext context) => Container( Widget build(BuildContext context) {
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75), if (isList) {
padding: const EdgeInsets.symmetric( return Column(
vertical: 7, mainAxisAlignment: MainAxisAlignment.center,
horizontal: 10, crossAxisAlignment: CrossAxisAlignment.start,
), children: [
child: Row( if (title != null)
mainAxisAlignment: MainAxisAlignment.end, Text(
crossAxisAlignment: CrossAxisAlignment.center, title!,
children: [ overflow: TextOverflow.ellipsis,
if (title != null) style: const TextStyle(
Flexible( color: Colors.white,
child: Text( fontWeight: FontWeight.bold,
title!,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.white),
),
), ),
const Gap(5), ).padding(left: 2),
isScreenShare Row(
? const Icon( children: [
Symbols.monitor, isScreenShare
? const Icon(
Symbols.monitor,
color: Colors.white,
size: 16,
)
: Icon(
audioAvailable ? Symbols.mic : Symbols.mic_off,
color: audioAvailable ? Colors.white : Colors.red,
size: 16,
),
const Gap(3),
if (connectionQuality != ConnectionQuality.unknown)
Icon(
{
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
}[connectionQuality],
color: {
ConnectionQuality.excellent: Colors.green,
ConnectionQuality.good: Colors.orange,
ConnectionQuality.poor: Colors.red,
}[connectionQuality],
size: 16,
)
else
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
color: Colors.white, color: Colors.white,
size: 16, strokeWidth: 2,
)
: Icon(
audioAvailable ? Symbols.mic : Symbols.mic_off,
color: audioAvailable ? Colors.white : Colors.red,
size: 16,
), ),
const Gap(3), ).padding(all: 3),
if (connectionQuality != ConnectionQuality.unknown) ],
Icon( )
{ ],
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
}[connectionQuality],
color: {
ConnectionQuality.excellent: Colors.green,
ConnectionQuality.good: Colors.orange,
ConnectionQuality.poor: Colors.red,
}[connectionQuality],
size: 16,
)
else
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
).padding(all: 3),
],
),
); );
}
return Container(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
padding: const EdgeInsets.symmetric(
vertical: 7,
horizontal: 10,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (title != null)
Flexible(
child: Text(
title!,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.white),
),
),
const Gap(5),
isScreenShare
? const Icon(
Symbols.monitor,
color: Colors.white,
size: 16,
)
: Icon(
audioAvailable ? Symbols.mic : Symbols.mic_off,
color: audioAvailable ? Colors.white : Colors.red,
size: 16,
),
const Gap(3),
if (connectionQuality != ConnectionQuality.unknown)
Icon(
{
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
}[connectionQuality],
color: {
ConnectionQuality.excellent: Colors.green,
ConnectionQuality.good: Colors.orange,
ConnectionQuality.poor: Colors.red,
}[connectionQuality],
size: 16,
)
else
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
).padding(all: 3),
],
),
);
}
} }

View File

@ -26,11 +26,13 @@ class _LinkPreviewWidgetState extends State<LinkPreviewWidget> {
Future<void> _getLinkMeta() async { Future<void> _getLinkMeta() async {
final linkRegex = RegExp(r'https?:\/\/[^\s/$.?#].[^\s]*'); final linkRegex = RegExp(r'https?:\/\/[^\s/$.?#].[^\s]*');
final links = linkRegex.allMatches(widget.text).map((e) => e.group(0)).toSet(); final links =
linkRegex.allMatches(widget.text).map((e) => e.group(0)).toSet();
final lp = context.read<SnLinkPreviewProvider>(); final lp = context.read<SnLinkPreviewProvider>();
final List<Future<SnLinkMeta?>> futures = links.where((e) => e != null).map((e) => lp.getLinkMeta(e!)).toList(); final List<Future<SnLinkMeta?>> futures =
links.where((e) => e != null).map((e) => lp.getLinkMeta(e!)).toList();
final results = await Future.wait(futures); final results = await Future.wait(futures);
_links.addAll(results.where((e) => e != null).map((e) => e!).toList()); _links.addAll(results.where((e) => e != null).map((e) => e!).toList());
@ -66,7 +68,9 @@ class _LinkPreviewEntry extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE) ? double.infinity : 480, maxWidth: ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
? double.infinity
: 480,
), ),
child: GestureDetector( child: GestureDetector(
child: Card( child: Card(
@ -74,16 +78,25 @@ class _LinkPreviewEntry extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (meta.image != null) if (meta.image != null)
Container( ClipRRect(
margin: const EdgeInsets.only(bottom: 4), borderRadius: BorderRadius.only(
color: Theme.of(context).colorScheme.surfaceContainer, topLeft: Radius.circular(8),
child: AspectRatio( topRight: Radius.circular(8),
aspectRatio: 16 / 9, ),
child: ClipRRect( child: Container(
borderRadius: const BorderRadius.all(Radius.circular(8)), margin: const EdgeInsets.only(bottom: 4),
child: AutoResizeUniversalImage( color: Theme.of(context).colorScheme.surfaceContainer,
meta.image!.startsWith('//') ? 'https:${meta.image}' : meta.image!, child: AspectRatio(
fit: BoxFit.contain, aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: AutoResizeUniversalImage(
meta.image!.startsWith('//')
? 'https:${meta.image}'
: meta.image!,
fit: BoxFit.contain,
),
), ),
), ),
), ),
@ -98,7 +111,8 @@ class _LinkPreviewEntry extends StatelessWidget {
width: 36, width: 36,
height: 36, height: 36,
child: meta.icon!.endsWith('.svg') child: meta.icon!.endsWith('.svg')
? SvgPicture.network(meta.icon!, width: 36, height: 36) ? SvgPicture.network(meta.icon!,
width: 36, height: 36)
: UniversalImage( : UniversalImage(
meta.icon!, meta.icon!,
noErrorWidget: true, noErrorWidget: true,

View File

@ -87,6 +87,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
onTap: () { onTap: () {
GoRouter.of(context).pushNamed(ele.screen); GoRouter.of(context).pushNamed(ele.screen);
nav.setIndex(idx); nav.setIndex(idx);
Scaffold.of(context).closeDrawer();
}, },
); );
}) })

View File

@ -282,7 +282,7 @@ class ResponsiveScaffoldLanding extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (ResponsiveScaffold.getIsExpand(context) || child == null) { if (ResponsiveScaffold.getIsExpand(context) || child == null) {
return AppScaffold( return AppScaffold(
noBackground: true, noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar(), appBar: AppBar(),
body: const SizedBox.shrink(), body: const SizedBox.shrink(),
); );

View File

@ -179,57 +179,54 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return SizedBox( return Column(
height: MediaQuery.of(context).size.height * 0.85, crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: [ crossAxisAlignment: CrossAxisAlignment.center,
Row( children: [
crossAxisAlignment: CrossAxisAlignment.center, const Icon(Symbols.comment, size: 24),
children: [ const Gap(16),
const Icon(Symbols.comment, size: 24), Text('postCommentsDetailed')
const Gap(16), .plural(widget.commentCount)
Text('postCommentsDetailed') .textStyle(Theme.of(context).textTheme.titleLarge!),
.plural(widget.commentCount) ],
.textStyle(Theme.of(context).textTheme.titleLarge!), ).padding(horizontal: 20, top: 16, bottom: 12),
], Expanded(
).padding(horizontal: 20, top: 16, bottom: 12), child: CustomScrollView(
Expanded( slivers: [
child: CustomScrollView( if (ua.isAuthorized)
slivers: [ SliverToBoxAdapter(
if (ua.isAuthorized) child: Container(
SliverToBoxAdapter( margin: const EdgeInsets.only(bottom: 8),
child: Container( height: 240,
margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration(
height: 240, border: Border.symmetric(
decoration: BoxDecoration( horizontal: BorderSide(
border: Border.symmetric( color: Theme.of(context).dividerColor,
horizontal: BorderSide( width: 1 / devicePixelRatio,
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
),
), ),
), ),
child: PostMiniEditor( ),
postReplyId: widget.post.id, child: PostMiniEditor(
onPost: () { postReplyId: widget.post.id,
_childListKey.currentState!.refresh(); onPost: () {
}, _childListKey.currentState!.refresh();
onExpand: () { },
Navigator.pop(context); onExpand: () {
}, Navigator.pop(context);
), },
), ),
), ),
PostCommentSliverList(
parentPost: widget.post,
key: _childListKey,
), ),
], PostCommentSliverList(
), parentPost: widget.post,
key: _childListKey,
),
],
), ),
], ),
), ],
); );
} }
} }

View File

@ -274,8 +274,10 @@ class _PostItemState extends State<PostItem> {
final isParentAuthor = ua.isAuthorized && final isParentAuthor = ua.isAuthorized &&
widget.data.replyTo?.publisher.accountId == ua.user?.id; widget.data.replyTo?.publisher.accountId == ua.user?.id;
final displayableAttachments = widget.data.preload?.attachments final displayableAttachments = widget.data.body['attachments']
?.where((ele) => ?.map((e) => SnAttachment.fromJson(e))
.cast<SnAttachment>()
.where((ele) =>
ele?.mediaType != SnMediaType.image || ele?.mediaType != SnMediaType.image ||
widget.data.type != 'article') widget.data.type != 'article')
.toList(); .toList();
@ -284,7 +286,7 @@ class _PostItemState extends State<PostItem> {
var attachmentSize = math.min( var attachmentSize = math.min(
MediaQuery.of(context).size.width, widget.maxWidth ?? double.infinity); MediaQuery.of(context).size.width, widget.maxWidth ?? double.infinity);
if ((widget.data.preload?.attachments?.length ?? 0) > 1) { if ((widget.data.body['attachments']?.length ?? 0) > 1) {
attachmentSize -= 80; attachmentSize -= 80;
} }
@ -341,7 +343,7 @@ class _PostItemState extends State<PostItem> {
], ],
), ),
const Gap(8), const Gap(8),
if (widget.data.preload?.thumbnail != null) if (widget.data.body['thumbnail'] != null)
Container( Container(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -361,14 +363,14 @@ class _PostItemState extends State<PostItem> {
), ),
child: AutoResizeUniversalImage( child: AutoResizeUniversalImage(
sn.getAttachmentUrl( sn.getAttachmentUrl(
widget.data.preload!.thumbnail!.rid, widget.data.body['thumbnail']['rid'],
), ),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
), ),
), ),
if (widget.data.preload?.video != null) if (widget.data.body['video'] != null)
_PostVideoPlayer(data: widget.data).padding(bottom: 8), _PostVideoPlayer(data: widget.data).padding(bottom: 8),
if (widget.data.type == 'question') if (widget.data.type == 'question')
_PostQuestionHint(data: widget.data).padding(bottom: 8), _PostQuestionHint(data: widget.data).padding(bottom: 8),
@ -455,10 +457,10 @@ class _PostItemState extends State<PostItem> {
if (widget.data.repostTo != null) if (widget.data.repostTo != null)
_PostQuoteContent(child: widget.data.repostTo!).padding( _PostQuoteContent(child: widget.data.repostTo!).padding(
top: 4, top: 4,
bottom: widget.data.preload?.attachments?.isNotEmpty ?? bottom:
false widget.data.body['attachments'].isNotEmpty ?? false
? 12 ? 12
: 0, : 0,
), ),
], ],
).padding( ).padding(
@ -479,11 +481,11 @@ class _PostItemState extends State<PostItem> {
fit: widget.showFullPost ? BoxFit.cover : BoxFit.contain, fit: widget.showFullPost ? BoxFit.cover : BoxFit.contain,
padding: EdgeInsets.only(left: 12, right: 12), padding: EdgeInsets.only(left: 12, right: 12),
), ),
if (widget.data.preload?.poll != null) if (widget.data.poll != null)
StyledWidget(Container( StyledWidget(Container(
constraints: constraints:
BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity), BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
child: PostPoll(poll: widget.data.preload!.poll!), child: PostPoll(poll: widget.data.poll!),
)) ))
.padding( .padding(
left: 12, left: 12,
@ -585,7 +587,7 @@ class _PostItemState extends State<PostItem> {
), ),
], ],
).padding(bottom: widget.showCompactAvatar ? 4 : 0), ).padding(bottom: widget.showCompactAvatar ? 4 : 0),
if (widget.data.preload?.thumbnail != null) if (widget.data.body['thumbnail'] != null)
Container( Container(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -605,14 +607,14 @@ class _PostItemState extends State<PostItem> {
), ),
child: AutoResizeUniversalImage( child: AutoResizeUniversalImage(
sn.getAttachmentUrl( sn.getAttachmentUrl(
widget.data.preload!.thumbnail!.rid, widget.data.body['thumbnail']['rid'],
), ),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
), ),
), ),
if (widget.data.preload?.video != null) if (widget.data.body['video'] != null)
_PostVideoPlayer(data: widget.data) _PostVideoPlayer(data: widget.data)
.padding(bottom: 8), .padding(bottom: 8),
if (widget.data.type == 'question') if (widget.data.type == 'question')
@ -712,7 +714,7 @@ class _PostItemState extends State<PostItem> {
_isTranslated || _isTranslated ||
_isTranslating) && _isTranslating) &&
(widget.data.repostTo != null || (widget.data.repostTo != null ||
(widget.data.preload?.attachments (widget.data.body['attachments']
?.isNotEmpty ?? ?.isNotEmpty ??
false)) false))
? 8 ? 8
@ -722,7 +724,7 @@ class _PostItemState extends State<PostItem> {
_PostQuoteContent(child: widget.data.repostTo!) _PostQuoteContent(child: widget.data.repostTo!)
.padding( .padding(
bottom: bottom:
(widget.data.preload?.attachments?.isNotEmpty ?? (widget.data.body['attachments']?.isNotEmpty ??
false) false)
? 8 ? 8
: 0, : 0,
@ -746,8 +748,8 @@ class _PostItemState extends State<PostItem> {
padding: padding:
EdgeInsets.only(left: widget.showAvatar ? 60 : 12, right: 12), EdgeInsets.only(left: widget.showAvatar ? 60 : 12, right: 12),
), ),
if (widget.data.preload?.poll != null) if (widget.data.poll != null)
PostPoll(poll: widget.data.preload!.poll!).padding( PostPoll(poll: widget.data.poll!).padding(
left: widget.showAvatar ? 60 : 12, left: widget.showAvatar ? 60 : 12,
right: 12, right: 12,
top: 12, top: 12,
@ -808,7 +810,7 @@ class PostShareImageWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (data.preload?.thumbnail != null) if (data.body['thumbnail'] != null)
AspectRatio( AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: ClipRRect( child: ClipRRect(
@ -817,7 +819,7 @@ class PostShareImageWidget extends StatelessWidget {
topRight: Radius.circular(8), topRight: Radius.circular(8),
), ),
child: AutoResizeUniversalImage( child: AutoResizeUniversalImage(
sn.getAttachmentUrl(data.preload!.thumbnail!.rid), sn.getAttachmentUrl(data.body['thumbnail']['rid']),
fit: BoxFit.cover, fit: BoxFit.cover,
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
), ),
@ -855,9 +857,13 @@ class PostShareImageWidget extends StatelessWidget {
isRelativeDate: false, isRelativeDate: false,
).padding(horizontal: 16, bottom: 8), ).padding(horizontal: 16, bottom: 8),
if (data.type != 'article' && if (data.type != 'article' &&
(data.preload?.attachments?.isNotEmpty ?? false)) (data.body['attachments']?.isNotEmpty ?? false))
StyledWidget(AttachmentList( StyledWidget(AttachmentList(
data: data.preload!.attachments!, data: data.body['attachments']
?.map((e) => SnAttachment.fromJson(e))
.cast<SnAttachment>()
.toList() ??
[],
columned: true, columned: true,
fit: BoxFit.contain, fit: BoxFit.contain,
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
@ -1146,31 +1152,9 @@ class _PostHeadline extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isEnlarge) { if (isEnlarge) {
final sn = context.read<SnNetworkProvider>();
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (data.preload?.thumbnail != null)
Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(data.preload!.thumbnail!.rid),
fit: BoxFit.cover,
),
),
),
),
if (data.body['title'] != null || (title?.isNotEmpty ?? false)) if (data.body['title'] != null || (title?.isNotEmpty ?? false))
Text( Text(
title ?? data.body['title'], title ?? data.body['title'],
@ -1255,7 +1239,7 @@ class _PostAvatar extends StatelessWidget {
: null; : null;
return GestureDetector( return GestureDetector(
child: data.preload?.realm == null child: data.realm == null
? AccountImage( ? AccountImage(
filterQuality: filterQuality, filterQuality: filterQuality,
content: data.publisher.avatar, content: data.publisher.avatar,
@ -1271,7 +1255,7 @@ class _PostAvatar extends StatelessWidget {
) )
: AccountImage( : AccountImage(
filterQuality: filterQuality, filterQuality: filterQuality,
content: data.preload!.realm!.avatar, content: data.realm!.avatar,
radius: isCompact ? 12 : 20, radius: isCompact ? 12 : 20,
borderRadius: isCompact ? 4 : 8, borderRadius: isCompact ? 4 : 8,
badgeOffset: Offset(-6, -4), badgeOffset: Offset(-6, -4),
@ -1568,6 +1552,7 @@ class _PostContentHeader extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isCompact) { if (isCompact) {
return Row( return Row(
spacing: 4,
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
@ -1575,7 +1560,6 @@ class _PostContentHeader extends StatelessWidget {
maxLines: 1, maxLines: 1,
).bold(), ).bold(),
), ),
const Gap(4),
Flexible( Flexible(
child: Text( child: Text(
isRelativeDate isRelativeDate
@ -1587,6 +1571,10 @@ class _PostContentHeader extends StatelessWidget {
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
).fontSize(13).opacity(0.8), ).fontSize(13).opacity(0.8),
), ),
if (data.editedAt != null)
Flexible(
child: Text('postEditedHint').tr().fontSize(13).opacity(0.8),
)
], ],
); );
} else { } else {
@ -1596,20 +1584,20 @@ class _PostContentHeader extends StatelessWidget {
Row( Row(
children: [ children: [
Text(data.publisher.nick).bold(), Text(data.publisher.nick).bold(),
if (data.preload?.realm != null) if (data.realm != null)
const Icon(Symbols.arrow_right, size: 16) const Icon(Symbols.arrow_right, size: 16)
.padding(horizontal: 2) .padding(horizontal: 2)
.opacity(0.5), .opacity(0.5),
if (data.preload?.realm != null) Text(data.preload!.realm!.name), if (data.realm != null) Text(data.realm!.name),
], ],
), ),
Row( Row(
spacing: 4,
children: [ children: [
Text( Text(
'@${data.publisher.name}', '@${data.publisher.name}',
maxLines: 1, maxLines: 1,
).fontSize(13), ).fontSize(13),
const Gap(4),
Text( Text(
isRelativeDate isRelativeDate
? RelativeTime(context) ? RelativeTime(context)
@ -1619,6 +1607,8 @@ class _PostContentHeader extends StatelessWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
).fontSize(13), ).fontSize(13),
if (data.editedAt != null)
Text('postEditedHint').tr().fontSize(13),
], ],
).opacity(0.8), ).opacity(0.8),
], ],
@ -1648,7 +1638,11 @@ class _PostContentBody extends StatelessWidget {
RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''), RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''),
textScaler: isEnlarge ? TextScaler.linear(1.1) : null, textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: text, content: text,
attachments: data.preload?.attachments, attachments: data.body['attachments']
?.map((e) => SnAttachment.fromJson(e))
.cast<SnAttachment>()
.toList() ??
[],
); );
if (isSelectable) { if (isSelectable) {
@ -1706,14 +1700,14 @@ class _PostQuoteContent extends StatelessWidget {
], ],
).padding(horizontal: 16), ).padding(horizontal: 16),
if (child.type != 'article' && if (child.type != 'article' &&
(child.preload?.attachments?.isNotEmpty ?? false)) (child.body['attachments']?.isNotEmpty ?? false))
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8), bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8), bottomRight: Radius.circular(8),
), ),
child: AttachmentList( child: AttachmentList(
data: child.preload!.attachments!, data: child.body['attachments']!,
maxHeight: 360, maxHeight: 360,
minWidth: 640, minWidth: 640,
fit: BoxFit.contain, fit: BoxFit.contain,
@ -2062,8 +2056,6 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) => PostCommentListPopup( builder: (context) => PostCommentListPopup(
post: widget.data, post: widget.data,
commentCount: widget.data.metric.replyCount, commentCount: widget.data.metric.replyCount,
@ -2352,7 +2344,7 @@ class _PostVideoPlayer extends StatelessWidget {
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AttachmentItem( child: AttachmentItem(
data: data.preload!.video!, data: data.body['video'],
heroTag: 'post-video-${data.id}', heroTag: 'post-video-${data.id}',
), ),
), ),

View File

@ -24,6 +24,7 @@ import gal
import hotkey_manager_macos import hotkey_manager_macos
import in_app_review import in_app_review
import livekit_client import livekit_client
import livekit_noise_filter
import local_notifier import local_notifier
import media_kit_libs_macos_video import media_kit_libs_macos_video
import media_kit_video import media_kit_video
@ -60,6 +61,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin")) HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
LiveKitKrispNoiseFilterPlugin.register(with: registry.registrar(forPlugin: "LiveKitKrispNoiseFilterPlugin"))
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin")) LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))

View File

@ -152,6 +152,11 @@ PODS:
- flutter_webrtc - flutter_webrtc
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
- livekit_noise_filter (0.0.1):
- flutter_webrtc
- FlutterMacOS
- LiveKitKrispNoiseFilter (= 0.0.7)
- LiveKitKrispNoiseFilter (0.0.7)
- local_notifier (0.1.0): - local_notifier (0.1.0):
- FlutterMacOS - FlutterMacOS
- media_kit_libs_macos_video (1.0.4): - media_kit_libs_macos_video (1.0.4):
@ -237,6 +242,7 @@ DEPENDENCIES:
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
- livekit_noise_filter (from `Flutter/ephemeral/.symlinks/plugins/livekit_noise_filter/macos`)
- local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`) - local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`) - media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
@ -265,6 +271,7 @@ SPEC REPOS:
- GoogleDataTransport - GoogleDataTransport
- GoogleUtilities - GoogleUtilities
- HotKey - HotKey
- LiveKitKrispNoiseFilter
- nanopb - nanopb
- OrderedSet - OrderedSet
- PromisesObjC - PromisesObjC
@ -315,6 +322,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
livekit_client: livekit_client:
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos :path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
livekit_noise_filter:
:path: Flutter/ephemeral/.symlinks/plugins/livekit_noise_filter/macos
local_notifier: local_notifier:
:path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos :path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos
media_kit_libs_macos_video: media_kit_libs_macos_video:
@ -378,6 +387,8 @@ SPEC CHECKSUMS:
hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe
in_app_review: 0599bccaed5e02f6bed2b0d30d16f86b63ed8638 in_app_review: 0599bccaed5e02f6bed2b0d30d16f86b63ed8638
livekit_client: 35690bf9861be6325a6f7d11bb38d50c7c9fed80 livekit_client: 35690bf9861be6325a6f7d11bb38d50c7c9fed80
livekit_noise_filter: c5710c0871ef3621b48c0b44d3c3ff938ba414b2
LiveKitKrispNoiseFilter: efe418ceca28163ace0ff222bd2cc02384645d84
local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758

View File

@ -1381,6 +1381,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" version: "2.4.1"
livekit_noise_filter:
dependency: "direct main"
description:
name: livekit_noise_filter
sha256: "398bfd1cc63ada9dee9fd7ea415e2fc1e51e091a6d217aad3649b882c35c7fcb"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
local_notifier: local_notifier:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 2.4.2+85 version: 2.4.2+86
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@ -144,6 +144,7 @@ dependencies:
latlong2: ^0.9.1 latlong2: ^0.9.1
crypto: ^3.0.6 crypto: ^3.0.6
audioplayers: ^6.4.0 audioplayers: ^6.4.0
livekit_noise_filter: ^0.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: