Compare commits

...

29 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
LittleSheep
61dbf92909 🌐 Merge pull request #19 from Texas0295/master
Add AppImage build tools & Update workflow
2025-03-28 18:37:55 +08:00
Texas0295
b69e4002e0 Add AppImage build tools & Update workflow 2025-03-28 02:01:45 +08:00
Texas0295
49aa24b79d Merge branch 'Solsynth:master' into master 2025-03-28 01:59:19 +08:00
ceb5c53229 🚀 Launch 2.4.2+85 2025-03-28 01:01:34 +08:00
908f0cb59e 🐛 Fix some issues 2025-03-28 00:54:51 +08:00
7c2b8de931 Desktop chat list
🍱 Update launch sfx
2025-03-28 00:52:19 +08:00
Texas0295
ddd0a4c3d3 remove cache=true in build-linux 2025-03-28 00:41:58 +08:00
Texas0295
99e07de243 upload appimage file 2025-03-28 00:04:44 +08:00
6bb9c21759 Rollback drawer style on mobile
🗑️ Remove drawer prefer collapse & expand
2025-03-28 00:00:39 +08:00
8f2fc55608 User ear healthy 2025-03-27 23:52:37 +08:00
a1c4e5eca0 ♻️ Refactored large screen user experience 2025-03-27 23:18:40 +08:00
Texas0295
10bf0883e5 add appimage build 2025-03-27 23:11:15 +08:00
595050f89f ♻️ Explore two column 2025-03-27 22:58:06 +08:00
0722c99f21 ♻️ Openable Post Item now push pages 2025-03-27 22:46:36 +08:00
12d03836f9 ♻️ Updated nav & account page two column design 2025-03-27 22:42:44 +08:00
70 changed files with 2383 additions and 1623 deletions

View File

@@ -48,7 +48,6 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: stable channel: stable
cache: true
- run: | - run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev sudo apt-get install -y ninja-build libgtk-3-dev
@@ -65,3 +64,18 @@ jobs:
with: with:
name: build-output-linux name: build-output-linux
path: build/linux/x64/release/bundle path: build/linux/x64/release/bundle
- name: Build AppImage
run: |
rm -r Solian.AppDir | true
mkdir Solian.AppDir
cp -r build/linux/x64/release/bundle/* Solian.AppDir
cp -r buildtools/appimage_config/* Solian.AppDir
cp assets/icon/icon-light-radius.png Solian.AppDir
sudo chmod +x buildtools/appimagetool-x86_64.AppImage
sudo chmod +x Solian.AppDir/AppRun
./buildtools/appimagetool-x86_64.AppImage Solian.AppDir
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-linux-appimage
path: './*.AppImage*'

Binary file not shown.

View File

@@ -939,5 +939,14 @@
"settingsSoundEffects": "Sound Effects", "settingsSoundEffects": "Sound Effects",
"settingsSoundEffectsDescription": "Enable the sound effects around the app.", "settingsSoundEffectsDescription": "Enable the sound effects around the app.",
"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",
"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

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

View File

@@ -0,0 +1,4 @@
#!/bin/sh
cd "$(dirname "$0")"
exec ./surface

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Type=Application
Terminal=false
Name=Solian
Exec=surface %u
Icon=icon-light-radius
Categories=Network;

Binary file not shown.

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,23 +335,27 @@ 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>();
await kp.reloadActive(); kp.reloadActive();
kp.listen(); kp.listen();
} catch (_) {} } catch (_) {}
if (ua.isAuthorized) { if (ua.isAuthorized) {
@@ -357,7 +365,6 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
notify.listen(); notify.listen();
try { try {
notify.registerPushNotifications(); notify.registerPushNotifications();
} catch (_) {}
if (!mounted) return; if (!mounted) return;
_setPhaseText('stickers'); _setPhaseText('stickers');
final sticker = context.read<SnStickerProvider>(); final sticker = context.read<SnStickerProvider>();
@@ -374,7 +381,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
_setPhaseText('chat'); _setPhaseText('chat');
final ct = context.read<ChatChannelProvider>(); final ct = context.read<ChatChannelProvider>();
await ct.refreshAvailableChannels(); await ct.refreshAvailableChannels();
_initPercentage = 1;
_setPhaseText('done'); _setPhaseText('done');
} catch (_) {}
_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 player = AudioPlayer(playerId: 'launch-intro-player'); final date = DateTime.now();
await player.play(AssetSource('audio/sfx/launch-intro.mp3'), volume: 0.5); final player = AudioPlayer(playerId: 'launch-done-player');
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,6 +479,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
AppLifecycleListener(onExitRequested: _onExitRequested); AppLifecycleListener(onExitRequested: _onExitRequested);
} }
try {
_trayInitialization(); _trayInitialization();
_hotkeyInitialization(); _hotkeyInitialization();
_notifyInitialization(); _notifyInitialization();
@@ -464,7 +488,13 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
_tryRequestRating(); _tryRequestRating();
_checkForUpdate(); _checkForUpdate();
setState(() => _isBusy = false); 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,8 +585,205 @@ 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,
initPercentage: _initPercentage,
phaseText: _phaseText,
)
: widget.child,
);
},
),
),
);
}
}
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( child: Stack(
children: [ children: [
Container( Container(
@@ -580,27 +807,25 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
'assets/icon/icon.png', 'assets/icon/icon.png',
width: 64, width: 64,
height: 64, height: 64,
color: color: Theme.of(context).colorScheme.onSurface,
Theme.of(context).colorScheme.onSurface,
), ),
Text('Solar Network').bold(), Text('Solar Network').bold(),
AppVersionLabel(), AppVersionLabel(),
Gap(8), Gap(8),
Text(_phaseText, textAlign: TextAlign.center), Text(phaseText, textAlign: TextAlign.center),
Gap(16), Gap(16),
const LinearProgressIndicator(), TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: initPercentage),
duration: Duration(milliseconds: 300),
builder: (context, value, _) =>
LinearProgressIndicator(value: value),
),
], ],
), ),
), ),
), ),
], ],
), ),
)
: widget.child,
);
},
),
),
); );
} }
} }

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

@@ -13,7 +13,6 @@ const kNetworkServerStoreKey = 'app_server_url';
const kAppbarTransparentStoreKey = 'app_bar_transparent'; const kAppbarTransparentStoreKey = 'app_bar_transparent';
const kAppBackgroundStoreKey = 'app_has_background'; const kAppBackgroundStoreKey = 'app_has_background';
const kAppColorSchemeStoreKey = 'app_color_scheme'; const kAppColorSchemeStoreKey = 'app_color_scheme';
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
const kAppNotifyWithHaptic = 'app_notify_with_haptic'; const kAppNotifyWithHaptic = 'app_notify_with_haptic';
const kAppExpandPostLink = 'app_expand_post_link'; const kAppExpandPostLink = 'app_expand_post_link';
const kAppExpandChatLink = 'app_expand_chat_link'; const kAppExpandChatLink = 'app_expand_chat_link';
@@ -47,27 +46,17 @@ class ConfigProvider extends ChangeNotifier {
} }
bool drawerIsCollapsed = false; bool drawerIsCollapsed = false;
bool drawerIsExpanded = false;
void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) { void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
bool newDrawerIsCollapsed = false; bool newDrawerIsCollapsed = false;
bool newDrawerIsExpanded = false;
if (withMediaQuery) { if (withMediaQuery) {
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600; newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600;
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 601;
} else { } else {
final rpb = ResponsiveBreakpoints.of(context); final rpb = ResponsiveBreakpoints.of(context);
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE); newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
newDrawerIsExpanded = rpb.largerThan(TABLET)
? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
? false
: true
: false;
} }
if (newDrawerIsExpanded != drawerIsExpanded || if (newDrawerIsCollapsed != drawerIsCollapsed) {
newDrawerIsCollapsed != drawerIsCollapsed) {
drawerIsExpanded = newDrawerIsExpanded;
drawerIsCollapsed = newDrawerIsCollapsed; drawerIsCollapsed = newDrawerIsCollapsed;
notifyListeners(); notifyListeners();
} }

View File

@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/types/realm.dart';
class AppNavListItem { class AppNavListItem {
final String title; final String title;
@@ -60,11 +59,6 @@ class NavigationProvider extends ChangeNotifier {
screen: 'chat', screen: 'chat',
label: 'screenChat', label: 'screenChat',
), ),
AppNavDestination(
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
screen: 'account',
label: 'screenAccount',
),
AppNavDestination( AppNavDestination(
icon: Icon(Symbols.group, weight: 400, opticalSize: 20), icon: Icon(Symbols.group, weight: 400, opticalSize: 20),
screen: 'realm', screen: 'realm',
@@ -75,6 +69,11 @@ class NavigationProvider extends ChangeNotifier {
screen: 'news', screen: 'news',
label: 'screenNews', label: 'screenNews',
), ),
AppNavDestination(
icon: Icon(Symbols.settings, weight: 400, opticalSize: 20),
screen: 'settings',
label: 'screenSettings',
),
]; ];
static const List<String> kDefaultPinnedDestination = [ static const List<String> kDefaultPinnedDestination = [
'home', 'home',
@@ -135,11 +134,4 @@ class NavigationProvider extends ChangeNotifier {
_currentIndex = idx; _currentIndex = idx;
notifyListeners(); notifyListeners();
} }
SnRealm? focusedRealm;
void setFocusedRealm(SnRealm? realm) {
focusedRealm = realm;
notifyListeners();
}
} }

View File

@@ -105,6 +105,15 @@ class NotificationProvider extends ChangeNotifier {
if (now.day == 1 && now.month == 4) { if (now.day == 1 && now.month == 4) {
_notifySoundPlayer.play( _notifySoundPlayer.play(
AssetSource('audio/notify/metal-pipe.mp3'), AssetSource('audio/notify/metal-pipe.mp3'),
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',
queryParameters: {
'take': take, 'take': take,
'offset': offset, '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(
'/cgi/co/posts/search',
queryParameters: {
'take': take, 'take': take,
'offset': offset, 'offset': offset,
'probe': searchTerm, 'probe': searchTerm,
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','), if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
if (categories?.isNotEmpty ?? false) 'categories': categories!.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: '/',
@@ -72,8 +61,8 @@ final _appRoutes = [
), ),
GoRoute( GoRoute(
path: '/posts', path: '/posts',
name: 'explore', name: 'posts',
builder: (context, state) => const ExploreScreen(), builder: (_, __) => const SizedBox.shrink(),
routes: [ routes: [
GoRoute( GoRoute(
path: '/draft', path: '/draft',
@@ -111,26 +100,51 @@ final _appRoutes = [
state.uri.queryParameters['categories']?.split(','), state.uri.queryParameters['categories']?.split(','),
), ),
), ),
],
),
ShellRoute(
builder: (context, state, child) => ResponsiveScaffold(
asideFlex: 2,
contentFlex: 3,
aside: const ExploreScreen(),
child: child,
),
routes: [
GoRoute(
path: '/explore',
name: 'explore',
builder: (context, state) => const ResponsiveScaffoldLanding(
child: ExploreScreen(),
),
),
GoRoute(
path: '/posts/:slug',
name: 'postDetail',
builder: (context, state) => PostDetailScreen(
key: ValueKey(state.pathParameters['slug']!),
slug: state.pathParameters['slug']!,
preload: state.extra as SnPost?,
),
),
GoRoute( GoRoute(
path: '/publishers/:name', path: '/publishers/:name',
name: 'postPublisher', name: 'postPublisher',
builder: (context, state) => builder: (context, state) =>
PostPublisherScreen(name: state.pathParameters['name']!), PostPublisherScreen(name: state.pathParameters['name']!),
), ),
GoRoute(
path: '/:slug',
name: 'postDetail',
builder: (context, state) => PostDetailScreen(
slug: state.pathParameters['slug']!,
preload: state.extra as SnPost?,
),
),
], ],
), ),
ShellRoute(
builder: (context, state, child) => ResponsiveScaffold(
aside: const AccountScreen(),
child: child,
),
routes: [
GoRoute( GoRoute(
path: '/account', path: '/account',
name: 'account', name: 'account',
builder: (context, state) => const AccountScreen(), builder: (context, state) =>
const ResponsiveScaffoldLanding(child: AccountScreen()),
routes: [ routes: [
GoRoute( GoRoute(
path: '/punishments', path: '/punishments',
@@ -216,24 +230,35 @@ final _appRoutes = [
name: state.pathParameters['name']!, name: state.pathParameters['name']!,
), ),
), ),
],
),
],
),
GoRoute( GoRoute(
path: '/profile/:name', path: '/accounts/:name',
name: 'accountProfilePage', name: 'accountProfilePage',
pageBuilder: (context, state) => NoTransitionPage( pageBuilder: (context, state) => NoTransitionPage(
child: UserScreen(name: state.pathParameters['name']!), child: UserScreen(name: state.pathParameters['name']!),
), ),
), ),
], ShellRoute(
), builder: (context, state, child) =>
ResponsiveScaffold(aside: const ChatScreen(), child: child),
routes: [
GoRoute( GoRoute(
path: '/chat', path: '/chat',
name: 'chat', name: 'chat',
builder: (context, state) => const ChatScreen(), builder: (context, state) => const ResponsiveScaffoldLanding(
child: ChatScreen(),
),
routes: [ routes: [
GoRoute( GoRoute(
path: '/:scope/:alias', path: '/:scope/:alias',
name: 'chatRoom', name: 'chatRoom',
builder: (context, state) => ChatRoomScreen( builder: (context, state) => ChatRoomScreen(
key: ValueKey(
'${state.pathParameters['scope']!}:${state.pathParameters['alias']!}',
),
scope: state.pathParameters['scope']!, scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!, alias: state.pathParameters['alias']!,
extra: state.extra as ChatRoomScreenExtra?, extra: state.extra as ChatRoomScreenExtra?,
@@ -264,13 +289,12 @@ 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,6 +110,7 @@ class AccountScreen extends StatelessWidget {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text("screenAccount").tr(), title: Text("screenAccount").tr(),
@@ -141,15 +142,6 @@ class AccountScreen extends StatelessWidget {
], ],
) )
: null, : null,
actions: [
IconButton(
icon: const Icon(Symbols.settings, fill: 1),
onPressed: () {
GoRouter.of(context).pushNamed('settings');
},
),
const Gap(8),
],
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: ua.isAuthorized child: ua.isAuthorized

View File

@@ -59,6 +59,7 @@ class _ActionEventScreenState extends State<ActionEventScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
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,6 +91,7 @@ class _AccountAuthTicketState extends State<AccountAuthTicket> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
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,6 +70,7 @@ class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
title: Text('screenAccountBadges').tr(), title: Text('screenAccountBadges').tr(),
), ),

View File

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

View File

@@ -16,7 +16,11 @@ final Map<int, (String, String, IconData)> kFactorTypes = {
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password), 0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email), 1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer), 2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active), 3: (
'authFactorInAppNotify',
'authFactorInAppNotifyDescription',
Symbols.notifications_active
),
}; };
class FactorSettingsScreen extends StatefulWidget { class FactorSettingsScreen extends StatefulWidget {
@@ -36,7 +40,10 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/me/factors'); final resp = await sn.client.get('/cgi/id/users/me/factors');
_factors = List<SnAuthFactor>.from( _factors = List<SnAuthFactor>.from(
resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [], resp.data
?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
); );
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
@@ -55,6 +62,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: PageBackButton(), leading: PageBackButton(),
title: Text('screenFactorSettings').tr(), title: Text('screenFactorSettings').tr(),
@@ -96,7 +104,8 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
return ListTile( return ListTile(
title: Text(kFactorTypes[ele.type]!.$1).tr(), title: Text(kFactorTypes[ele.type]!.$1).tr(),
subtitle: Text(kFactorTypes[ele.type]!.$2).tr(), subtitle: Text(kFactorTypes[ele.type]!.$2).tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 12), contentPadding:
const EdgeInsets.only(left: 24, right: 12),
leading: Icon(kFactorTypes[ele.type]!.$3), leading: Icon(kFactorTypes[ele.type]!.$3),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Symbols.close), icon: const Icon(Symbols.close),
@@ -105,14 +114,17 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
context context
.showConfirmDialog( .showConfirmDialog(
'authFactorDelete'.tr(), 'authFactorDelete'.tr(),
'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]), 'authFactorDeleteDescription'.tr(
args: [kFactorTypes[ele.type]!.$1.tr()]),
) )
.then((val) async { .then((val) async {
if (!val) return; if (!val) return;
try { try {
if (!context.mounted) return; if (!context.mounted) return;
final sn = context.read<SnNetworkProvider>(); final sn =
await sn.client.delete('/cgi/id/users/me/factors/${ele.id}'); context.read<SnNetworkProvider>();
await sn.client.delete(
'/cgi/id/users/me/factors/${ele.id}');
_fetchFactors(); _fetchFactors();
} catch (err) { } catch (err) {
if (!context.mounted) return; if (!context.mounted) return;
@@ -191,7 +203,9 @@ class _FactorNewDialogState extends State<_FactorNewDialog> {
value: _factorType, value: _factorType,
items: kFactorTypes.entries.map( items: kFactorTypes.entries.map(
(ele) { (ele) {
final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key); final contains = widget.currentlyHave
.map((ele) => ele.type)
.contains(ele.key);
return DropdownMenuItem<int>( return DropdownMenuItem<int>(
enabled: !contains, enabled: !contains,
value: ele.key, value: ele.key,

View File

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

View File

@@ -75,6 +75,7 @@ class _AccountNotifyPrefsScreenState extends State<AccountNotifyPrefsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
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,6 +70,7 @@ class _AccountSecurityPrefsScreenState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountSettingsSecurity').tr(), title: Text('accountSettingsSecurity').tr(),

View File

@@ -66,21 +66,23 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
_locationController.text = prof.profile!.location; _locationController.text = prof.profile!.location;
_avatar = prof.avatar; _avatar = prof.avatar;
_banner = prof.banner; _banner = prof.banner;
_links = prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList(); _links =
prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList();
_birthday = prof.profile!.birthday?.toLocal(); _birthday = prof.profile!.birthday?.toLocal();
if (_birthday != null) { if (_birthday != null) {
_birthdayController.text = DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal()); _birthdayController.text =
DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal());
} }
} }
void _selectBirthday() async { void _selectBirthday() async {
await showCupertinoModalPopup<DateTime?>( await showCupertinoModalPopup<DateTime?>(
context: context, context: context,
builder: builder: (BuildContext context) => Container(
(BuildContext context) => Container(
height: 216, height: 216,
padding: const EdgeInsets.only(top: 6.0), padding: const EdgeInsets.only(top: 6.0),
margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), margin:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: SafeArea( child: SafeArea(
top: false, top: false,
@@ -91,7 +93,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
onDateTimeChanged: (DateTime newDate) { onDateTimeChanged: (DateTime newDate) {
setState(() { setState(() {
_birthday = newDate; _birthday = newDate;
_birthdayController.text = DateFormat(_kDateFormat).format(_birthday!); _birthdayController.text =
DateFormat(_kDateFormat).format(_birthday!);
}); });
}, },
), ),
@@ -109,11 +112,12 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
Uint8List? rawBytes; Uint8List? rawBytes;
if (!skipCrop) { if (!skipCrop) {
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); final ImageProvider imageProvider =
final aspectRatios = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; final aspectRatios = place == 'banner'
final result = ? [CropAspectRatio(width: 16, height: 7)]
(!kIsWeb && (Platform.isIOS || Platform.isMacOS)) : [CropAspectRatio(width: 1, height: 1)];
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
? await showCupertinoImageCropper( ? await showCupertinoImageCropper(
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
context, context,
@@ -131,7 +135,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!
.buffer
.asUint8List();
} else { } else {
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
@@ -152,7 +158,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
if (!mounted) return; if (!mounted) return;
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.client.put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid}); await sn.client
.put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid});
if (!mounted) return; if (!mounted) return;
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
@@ -188,7 +195,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
'location': _locationController.value.text, 'location': _locationController.value.text,
'birthday': _birthday?.toUtc().toIso8601String(), 'birthday': _birthday?.toUtc().toIso8601String(),
'links': { 'links': {
for (final link in _links!.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty)) link.$1: link.$2, for (final link in _links!
.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty))
link.$1: link.$2,
}, },
}, },
); );
@@ -235,7 +244,10 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
appBar: AppBar(leading: const PageBackButton(), title: Text('screenAccountProfileEdit').tr()), noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAccountProfileEdit').tr()),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -251,12 +263,15 @@ 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).colorScheme.surfaceContainerHigh, color: Theme.of(context)
child: .colorScheme
_banner != null .surfaceContainerHigh,
? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover) child: _banner != null
? AutoResizeUniversalImage(
sn.getAttachmentUrl(_banner!),
fit: BoxFit.cover)
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
), ),
@@ -294,12 +309,16 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
labelText: 'fieldUsername'.tr(), labelText: 'fieldUsername'.tr(),
helperText: 'fieldUsernameCannotEditHint'.tr(), helperText: 'fieldUsernameCannotEditHint'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextField( TextField(
controller: _nicknameController, controller: _nicknameController,
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldNickname'.tr()), decoration: InputDecoration(
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr()),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
Row( Row(
children: [ children: [
@@ -311,7 +330,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldFirstName'.tr(), labelText: 'fieldFirstName'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
const Gap(8), const Gap(8),
@@ -323,7 +343,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldLastName'.tr(), labelText: 'fieldLastName'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
], ],
@@ -338,7 +359,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldGender'.tr(), labelText: 'fieldGender'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
const Gap(4), const Gap(4),
@@ -350,7 +372,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldPronouns'.tr(), labelText: 'fieldPronouns'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
], ],
@@ -360,8 +383,11 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
maxLines: null, maxLines: null,
minLines: 3, minLines: 3,
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldDescription'.tr()), decoration: InputDecoration(
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), border: const UnderlineInputBorder(),
labelText: 'fieldDescription'.tr()),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -373,18 +399,21 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldTimeZone'.tr(), labelText: 'fieldTimeZone'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
const Gap(4), const Gap(4),
StyledWidget( StyledWidget(
IconButton( IconButton(
icon: const Icon(Symbols.calendar_month), icon: const Icon(Symbols.calendar_month),
visualDensity: VisualDensity(horizontal: -4, vertical: -4), visualDensity:
VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
onPressed: () async { onPressed: () async {
_timezoneController.text = await FlutterTimezone.getLocalTimezone(); _timezoneController.text =
await FlutterTimezone.getLocalTimezone();
}, },
), ),
).padding(top: 6), ).padding(top: 6),
@@ -392,7 +421,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
StyledWidget( StyledWidget(
IconButton( IconButton(
icon: const Icon(Symbols.clear), icon: const Icon(Symbols.clear),
visualDensity: VisualDensity(horizontal: -4, vertical: -4), visualDensity:
VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
onPressed: () { onPressed: () {
@@ -404,13 +434,18 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
), ),
TextField( TextField(
controller: _locationController, controller: _locationController,
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldLocation'.tr()), decoration: InputDecoration(
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), border: const UnderlineInputBorder(),
labelText: 'fieldLocation'.tr()),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextField( TextField(
controller: _birthdayController, controller: _birthdayController,
readOnly: true, readOnly: true,
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldBirthday'.tr()), decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldBirthday'.tr()),
onTap: () => _selectBirthday(), onTap: () => _selectBirthday(),
), ),
if (_links != null) if (_links != null)
@@ -418,7 +453,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
margin: const EdgeInsets.only(top: 16, bottom: 4), margin: const EdgeInsets.only(top: 16, bottom: 4),
child: Container( child: Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -427,13 +463,17 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
Expanded( Expanded(
child: Text( child: Text(
'fieldLinks'.tr(), 'fieldLinks'.tr(),
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 17), style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 17),
), ),
), ),
IconButton( IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
visualDensity: VisualDensity(horizontal: -4, vertical: -4), visualDensity:
VisualDensity(horizontal: -4, vertical: -4),
icon: const Icon(Symbols.add), icon: const Icon(Symbols.add),
onPressed: () { onPressed: () {
setState(() => _links!.add(('', ''))); setState(() => _links!.add(('', '')));
@@ -457,7 +497,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
onChanged: (value) { onChanged: (value) {
_links![idx] = (value, _links![idx].$2); _links![idx] = (value, _links![idx].$2);
}, },
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager
.instance.primaryFocus
?.unfocus(),
), ),
), ),
const Gap(8), const Gap(8),
@@ -473,7 +515,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
onChanged: (value) { onChanged: (value) {
_links![idx] = (_links![idx].$1, value); _links![idx] = (_links![idx].$1, value);
}, },
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager
.instance.primaryFocus
?.unfocus(),
), ),
), ),
], ],

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

View File

@@ -27,10 +27,12 @@ class AccountPublisherEditScreen extends StatefulWidget {
const AccountPublisherEditScreen({super.key, required this.name}); const AccountPublisherEditScreen({super.key, required this.name});
@override @override
State<AccountPublisherEditScreen> createState() => _AccountPublisherEditScreenState(); State<AccountPublisherEditScreen> createState() =>
_AccountPublisherEditScreenState();
} }
class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> { class _AccountPublisherEditScreenState
extends State<AccountPublisherEditScreen> {
bool _isBusy = false; bool _isBusy = false;
SnPublisher? _publisher; SnPublisher? _publisher;
@@ -115,11 +117,12 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
Uint8List? rawBytes; Uint8List? rawBytes;
if (!skipCrop) { if (!skipCrop) {
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); final ImageProvider imageProvider =
final aspectRatios = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; final aspectRatios = place == 'banner'
final result = ? [CropAspectRatio(width: 16, height: 7)]
(!kIsWeb && (Platform.isIOS || Platform.isMacOS)) : [CropAspectRatio(width: 1, height: 1)];
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
? await showCupertinoImageCropper( ? await showCupertinoImageCropper(
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
context, context,
@@ -137,7 +140,9 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!
.buffer
.asUint8List();
} else { } else {
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
@@ -191,7 +196,10 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
appBar: AppBar(leading: PageBackButton(), title: Text('screenAccountPublisherEdit').tr()), noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar(
leading: PageBackButton(),
title: Text('screenAccountPublisherEdit').tr()),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
@@ -206,12 +214,15 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
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).colorScheme.surfaceContainerHigh, color: Theme.of(context)
child: .colorScheme
_banner != null .surfaceContainerHigh,
? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover) child: _banner != null
? AutoResizeUniversalImage(
sn.getAttachmentUrl(_banner!),
fit: BoxFit.cover)
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
), ),
@@ -245,13 +256,15 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
labelText: 'fieldUsername'.tr(), labelText: 'fieldUsername'.tr(),
helperText: 'fieldUsernameCannotEditHint'.tr(), helperText: 'fieldUsernameCannotEditHint'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(4), const Gap(4),
TextField( TextField(
controller: _nickController, controller: _nickController,
decoration: InputDecoration(labelText: 'fieldNickname'.tr()), decoration: InputDecoration(labelText: 'fieldNickname'.tr()),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(4), const Gap(4),
TextField( TextField(
@@ -259,7 +272,8 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
maxLines: null, maxLines: null,
minLines: 3, minLines: 3,
decoration: InputDecoration(labelText: 'fieldDescription'.tr()), decoration: InputDecoration(labelText: 'fieldDescription'.tr()),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), const Gap(12),
Row( Row(

View File

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

View File

@@ -33,7 +33,8 @@ class _PublisherScreenState extends State<PublisherScreen> {
try { try {
final resp = await sn.client.get('/cgi/co/publishers/me'); final resp = await sn.client.get('/cgi/co/publishers/me');
final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []); final List<SnPublisher> out = List<SnPublisher>.from(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
if (!mounted) return; if (!mounted) return;
@@ -81,6 +82,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('screenAccountPublishers').tr(), title: Text('screenAccountPublishers').tr(),
@@ -93,7 +95,9 @@ class _PublisherScreenState extends State<PublisherScreen> {
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.add_circle), leading: const Icon(Symbols.add_circle),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) { GoRouter.of(context)
.pushNamed('accountPublisherNew')
.then((value) {
if (value == true) { if (value == true) {
_publishers.clear(); _publishers.clear();
_fetchPublishers(); _fetchPublishers();
@@ -119,7 +123,8 @@ class _PublisherScreenState extends State<PublisherScreen> {
return ListTile( return ListTile(
title: Text(publisher.nick), title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'), subtitle: Text('@${publisher.name}'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16), contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(content: publisher.avatar), leading: AccountImage(content: publisher.avatar),
trailing: PopupMenuButton( trailing: PopupMenuButton(
itemBuilder: (BuildContext context) => [ itemBuilder: (BuildContext context) => [

View File

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

View File

@@ -37,6 +37,7 @@ class AccountSettingsScreen extends StatelessWidget {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
return AppScaffold( return AppScaffold(
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

@@ -1,3 +1,5 @@
import 'package:animations/animations.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
@@ -6,21 +8,22 @@ import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.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/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/chat/room.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_select.dart'; import 'package:surface/widgets/account/account_select.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/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
@@ -38,6 +41,7 @@ class _ChatScreenState extends State<ChatScreen> {
List<SnChannel>? _channels; List<SnChannel>? _channels;
Map<int, SnChatMessage>? _lastMessages; Map<int, SnChatMessage>? _lastMessages;
Map<int, int>? _unreadCounts; Map<int, int>? _unreadCounts;
Map<int, int>? _unreadCountsGrouped;
Future<void> _fetchWhatsNew() async { Future<void> _fetchWhatsNew() async {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
@@ -45,19 +49,48 @@ class _ChatScreenState extends State<ChatScreen> {
if (resp.data == null) return; if (resp.data == null) return;
final List<dynamic> out = resp.data; final List<dynamic> out = resp.data;
setState(() { setState(() {
_unreadCounts = {for (var v in out) v['channel_id']: v['count']}; _unreadCounts ??= {};
_unreadCountsGrouped ??= {};
for (var v in out) {
_unreadCounts![v['channel_id']] = v['count'];
final channel =
_channels?.firstWhereOrNull((ele) => ele.id == v['channel_id']);
if (channel != null) {
if (channel.realmId != null) {
_unreadCountsGrouped![channel.realmId!] ??= 0;
_unreadCountsGrouped![channel.realmId!] =
(_unreadCountsGrouped![channel.realmId!]! + v['count']).toInt();
}
if (channel.type == 1) {
_unreadCountsGrouped![0] ??= 0;
_unreadCountsGrouped![0] =
(_unreadCountsGrouped![0]! + v['count']).toInt();
}
}
}
}); });
} }
void _refreshChannels({bool noRemote = false}) { void _refreshChannels({bool withBoost = false, bool noRemote = false}) {
final ct = context.read<ChatChannelProvider>();
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
setState(() => _isBusy = false); setState(() => _isBusy = false);
return; return;
} }
if (!withBoost) {
if (!noRemote) {
ct.refreshAvailableChannels();
}
} else {
setState(() {
_channels = ct.availableChannels;
});
}
final chan = context.read<ChatChannelProvider>(); final chan = context.read<ChatChannelProvider>();
chan.fetchChannels(noRemote: noRemote).listen((channels) async { chan.fetchChannels(noRemote: true).listen((channels) async {
final lastMessages = await chan.getLastMessages(channels); final lastMessages = await chan.getLastMessages(channels);
_lastMessages = {for (final val in lastMessages) val.channelId: val}; _lastMessages = {for (final val in lastMessages) val.channelId: val};
channels.sort((a, b) { channels.sort((a, b) {
@@ -99,6 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
..onDone(() { ..onDone(() {
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = false); setState(() => _isBusy = false);
_fetchWhatsNew();
}); });
} }
@@ -130,22 +164,28 @@ class _ChatScreenState extends State<ChatScreen> {
} }
} }
SnChannel? _focusChannel;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_refreshChannels(); _refreshChannels(withBoost: true);
_fetchWhatsNew();
} }
void _onTapChannel(SnChannel channel) { void _onTapChannel(SnChannel channel) {
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP); setState(() => _unreadCounts?[channel.id] = 0);
if (ResponsiveScaffold.getIsExpand(context)) {
if (doExpand) { GoRouter.of(context).pushReplacementNamed(
setState(() => _focusChannel = channel); 'chatRoom',
return; pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
).then((value) {
if (mounted) {
setState(() => _unreadCounts?[channel.id] = 0);
_refreshChannels(noRemote: true);
} }
});
} else {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'chatRoom', 'chatRoom',
pathParameters: { pathParameters: {
@@ -154,16 +194,21 @@ class _ChatScreenState extends State<ChatScreen> {
}, },
).then((value) { ).then((value) {
if (mounted) { if (mounted) {
_unreadCounts?[channel.id] = 0;
setState(() => _unreadCounts?[channel.id] = 0); setState(() => _unreadCounts?[channel.id] = 0);
_refreshChannels(noRemote: true); _refreshChannels(noRemote: true);
} }
}); });
} }
}
SnRealm? _focusedRealm;
bool _isDirect = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final sn = context.read<SnNetworkProvider>();
final rel = context.read<SnRealmProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return AppScaffold( return AppScaffold(
@@ -177,10 +222,8 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP); return AppScaffold(
noBackground: ResponsiveScaffold.getIsExpand(context),
final chatList = AppScaffold(
noBackground: doExpand,
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenChat').tr(), title: Text('screenChat').tr(),
@@ -248,65 +291,199 @@ class _ChatScreenState extends State<ChatScreen> {
body: Column( body: Column(
children: [ children: [
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
if (_channels != null && ResponsiveScaffold.getIsExpand(context))
Expanded( Expanded(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () => Future.wait([ onRefresh: () => Future.sync(() => _refreshChannels()),
Future.sync(() => _refreshChannels()), child: Builder(builder: (context) {
_fetchWhatsNew(), final scopeList = ListView(
]), key: const Key('realm-list-view'),
child: ListView.builder( padding: EdgeInsets.zero,
itemCount: _channels?.length ?? 0,
itemBuilder: (context, idx) {
final channel = _channels![idx];
final lastMessage = _lastMessages?[channel.id];
return _ChatChannelEntry(
channel: channel,
lastMessage: lastMessage,
unreadCount: _unreadCounts?[channel.id],
onTap: () {
if (doExpand) {
_unreadCounts?[channel.id] = 0;
setState(() => _focusChannel = channel);
return;
}
_onTapChannel(channel);
},
);
},
),
),
),
),
],
),
);
if (doExpand) {
return AppBackground(
isRoot: true,
child: Row(
children: [ children: [
SizedBox(width: 340, child: chatList), ListTile(
const VerticalDivider(width: 1), minTileHeight: 48,
if (_focusChannel != null) leading:
const Icon(Symbols.inbox_text).padding(right: 4),
contentPadding: EdgeInsets.only(left: 24, right: 24),
title: Text('chatDirect').tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (_unreadCountsGrouped?[0] != null &&
(_unreadCountsGrouped?[0] ?? 0) > 0)
Badge(
label: Text(
_unreadCountsGrouped![0].toString(),
),
),
],
),
onTap: () {
setState(() => _isDirect = true);
},
),
...rel.availableRealms.map((ele) {
return ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.only(left: 20, right: 24),
leading: AccountImage(
content: ele.avatar,
radius: 16,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (_unreadCountsGrouped?[ele.id] != null &&
(_unreadCountsGrouped?[ele.id] ?? 0) > 0)
Badge(
label: Text(
_unreadCountsGrouped![ele.id].toString(),
),
),
],
),
title: Text(ele.name),
onTap: () {
setState(() => _focusedRealm = ele);
},
);
}),
],
);
final directChatList = ListView(
key: Key('direct-chat-list-view'),
padding: EdgeInsets.zero,
children: [
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.arrow_left_alt),
contentPadding: EdgeInsets.only(left: 24),
title: Text('back').tr(),
onTap: () {
setState(() => _isDirect = false);
},
),
const Divider(height: 1),
..._channels!.where((ele) => ele.type == 1).map(
(ele) {
return _ChatChannelEntry(
channel: ele,
unreadCount: _unreadCounts?[ele.id],
lastMessage: _lastMessages?[ele.id],
isCompact: true,
onTap: () => _onTapChannel(ele),
);
},
)
],
);
final realmScopedChatList = _focusedRealm == null
? const SizedBox.shrink()
: ListView(
key: ValueKey(_focusedRealm),
padding: EdgeInsets.zero,
children: [
if (_focusedRealm!.banner != null)
AspectRatio(
aspectRatio: 16 / 7,
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(
_focusedRealm!.banner!,
),
fit: BoxFit.cover,
),
),
ListTile(
minTileHeight: 48,
tileColor: Theme.of(context)
.colorScheme
.surfaceContainer,
leading: AccountImage(
content: _focusedRealm!.avatar,
radius: 16,
),
contentPadding: EdgeInsets.only(
left: 20,
right: 16,
),
trailing: IconButton(
icon: const Icon(Symbols.close),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
visualDensity: VisualDensity.compact,
onPressed: () {
setState(() => _focusedRealm = null);
},
),
title: Text(_focusedRealm!.name),
),
...(_channels!
.where(
(ele) => ele.realmId == _focusedRealm?.id)
.map(
(ele) {
return _ChatChannelEntry(
channel: ele,
unreadCount: _unreadCounts?[ele.id],
lastMessage: _lastMessages?[ele.id],
onTap: () => _onTapChannel(ele),
isCompact: true,
);
},
))
],
);
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation) {
return SharedAxisTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
transitionType: SharedAxisTransitionType.horizontal,
child: child,
);
},
child: (_focusedRealm == null && !_isDirect)
? scopeList
: _isDirect
? directChatList
: realmScopedChatList,
);
}),
),
)
else if (_channels != null)
Expanded( Expanded(
child: ChatRoomScreen( child: RefreshIndicator(
key: ValueKey(_focusChannel!.id), onRefresh: () => Future.sync(() => _refreshChannels()),
scope: _focusChannel!.realm?.alias ?? 'global', child: ListView(
alias: _focusChannel!.alias, key: const Key('chat-list-view'),
padding: EdgeInsets.zero,
children: [
...(_channels!.map((ele) {
return _ChatChannelEntry(
channel: ele,
unreadCount: _unreadCounts?[ele.id],
lastMessage: _lastMessages?[ele.id],
onTap: () => _onTapChannel(ele),
);
}))
],
),
), ),
), ),
], ],
), ),
); );
} }
return chatList;
}
} }
class _ChatChannelEntry extends StatelessWidget { class _ChatChannelEntry extends StatelessWidget {
@@ -314,11 +491,13 @@ class _ChatChannelEntry extends StatelessWidget {
final int? unreadCount; final int? unreadCount;
final SnChatMessage? lastMessage; final SnChatMessage? lastMessage;
final Function? onTap; final Function? onTap;
final bool isCompact;
const _ChatChannelEntry({ const _ChatChannelEntry({
required this.channel, required this.channel,
this.unreadCount, this.unreadCount,
this.lastMessage, this.lastMessage,
this.onTap, this.onTap,
this.isCompact = false,
}); });
@override @override
@@ -337,6 +516,34 @@ class _ChatChannelEntry extends StatelessWidget {
? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name ? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name
: channel.name; : channel.name;
if (isCompact) {
return ListTile(
minTileHeight: 48,
contentPadding:
EdgeInsets.only(left: otherMember != null ? 20 : 24, right: 24),
leading: otherMember != null
? AccountImage(
content: ud.getFromCache(otherMember.accountId)?.avatar,
radius: 16,
)
: const Icon(Symbols.tag),
trailing: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (unreadCount != null && (unreadCount ?? 0) > 0)
Badge(
label: Text(unreadCount.toString()),
),
],
),
title: Text(title),
onTap: () {
onTap?.call();
},
);
}
return ListTile( return ListTile(
title: Row( title: Row(
children: [ children: [
@@ -399,7 +606,7 @@ class _ChatChannelEntry extends StatelessWidget {
content: otherMember != null content: otherMember != null
? ud.getFromCache(otherMember.accountId)?.avatar ? ud.getFromCache(otherMember.accountId)?.avatar
: channel.realm?.avatar, : channel.realm?.avatar,
fallbackWidget: const Icon(Symbols.chat, size: 20), fallbackWidget: const Icon(Symbols.tag, size: 20),
), ),
onTap: () => onTap?.call(), onTap: () => onTap?.call(),
); );

View File

@@ -32,17 +32,16 @@ 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: [
Container( Container(
color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75), color:
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(),
), ),
@@ -61,23 +60,19 @@ 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(
isFixedAvatar: true,
width: 120,
height: 120,
color: Theme.of(context).cardColor,
participant: track, participant: track,
avatarSize: 32,
onTap: () { onTap: () {
if (track.participant.sid != call.focusTrack?.participant.sid) { if (track.participant.sid !=
call.focusTrack?.participant.sid) {
call.setFocusTrack(track); call.setFocusTrack(track);
} }
}, },
), ),
),
); );
}, },
), ),
@@ -87,46 +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();
int rows = (call.participantTracks.length / columns).ceil();
double tileWidth = screenWidth / columns;
double tileHeight = screenHeight / rows;
return StyledWidget(GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
childAspectRatio: tileWidth / tileHeight,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: math.max(0, call.participantTracks.length), itemCount: math.max(0, call.participantTracks.length),
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final track = call.participantTracks[index]; final track = call.participantTracks[index];
return Card( return InteractiveParticipantWidget(
child: ClipRRect( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
borderRadius: const BorderRadius.all(Radius.circular(8)), isList: true,
child: InteractiveParticipantWidget( avatarSize: 24,
color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
participant: track, participant: track,
onTap: () {
if (track.participant.sid != call.focusTrack?.participant.sid) {
call.setFocusTrack(track);
}
},
),
),
); );
}, },
)).padding(all: 8); );
}); },
);
} }
@override @override
@@ -149,6 +124,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
listenable: call, listenable: call,
builder: (context, _) { builder: (context, _) {
return AppScaffold( return AppScaffold(
noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
title: RichText( title: RichText(
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -169,9 +145,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
]), ]),
), ),
), ),
body: GestureDetector( body: Column(
behavior: HitTestBehavior.translucent,
child: Column(
children: [ children: [
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
@@ -183,7 +157,8 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
Builder(builder: (context) { Builder(builder: (context) {
final call = context.read<ChatCallProvider>(); final call = context.read<ChatCallProvider>();
final connectionQuality = final connectionQuality =
call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown; call.room.localParticipant?.connectionQuality ??
livekit.ConnectionQuality.unknown;
return Expanded( return Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -205,24 +180,35 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
children: [ children: [
Text( Text(
{ {
livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(), livekit.ConnectionState.disconnected:
livekit.ConnectionState.connected: 'callStatusConnected'.tr(), 'callStatusDisconnected'.tr(),
livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(), livekit.ConnectionState.connected:
livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(), 'callStatusConnected'.tr(),
livekit.ConnectionState.connecting:
'callStatusConnecting'.tr(),
livekit.ConnectionState.reconnecting:
'callStatusReconnecting'.tr(),
}[call.room.connectionState]!, }[call.room.connectionState]!,
), ),
const Gap(6), const Gap(6),
if (connectionQuality != livekit.ConnectionQuality.unknown) if (connectionQuality !=
livekit.ConnectionQuality.unknown)
Icon( Icon(
{ {
livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt, livekit.ConnectionQuality.excellent:
livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar, Icons.signal_cellular_alt,
livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar, livekit.ConnectionQuality.good:
Icons.signal_cellular_alt_2_bar,
livekit.ConnectionQuality.poor:
Icons.signal_cellular_alt_1_bar,
}[connectionQuality], }[connectionQuality],
color: { color: {
livekit.ConnectionQuality.excellent: Colors.green, livekit.ConnectionQuality.excellent:
livekit.ConnectionQuality.good: Colors.orange, Colors.green,
livekit.ConnectionQuality.poor: Colors.red, livekit.ConnectionQuality.good:
Colors.orange,
livekit.ConnectionQuality.poor:
Colors.red,
}[connectionQuality], }[connectionQuality],
size: 16, size: 16,
) )
@@ -233,6 +219,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: Colors.white, color: Colors.white,
strokeWidth: 2, strokeWidth: 2,
padding: EdgeInsets.zero,
), ),
).padding(all: 3), ).padding(all: 3),
], ],
@@ -244,7 +231,9 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
Row( Row(
children: [ children: [
IconButton( IconButton(
icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view), icon: _layoutMode == 0
? const Icon(Icons.view_list)
: const Icon(Icons.grid_view),
onPressed: () { onPressed: () {
_switchLayout(); _switchLayout();
}, },
@@ -261,9 +250,9 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
builder: (context) { builder: (context) {
switch (_layoutMode) { switch (_layoutMode) {
case 1: case 1:
return _buildGridLayout();
default:
return _buildListLayout(); return _buildListLayout();
default:
return _buildMeetLayout();
} }
}, },
), ),
@@ -277,10 +266,9 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
call.room.localParticipant!, call.room.localParticipant!,
), ),
), ),
Gap(MediaQuery.of(context).padding.bottom),
], ],
), ),
onTap: () {},
),
); );
}); });
} }

View File

@@ -220,6 +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: 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

@@ -49,7 +49,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [], resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
); );
if (_editingChannel != null) { if (_editingChannel != null) {
_belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId); _belongToRealm =
_realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
} }
} catch (err) { } catch (err) {
if (mounted) context.showErrorDialog(err); if (mounted) context.showErrorDialog(err);
@@ -97,7 +98,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
'is_community': _isCommunity, 'is_community': _isCommunity,
if (_editingChannel != null && _belongToRealm == null) if (_editingChannel != null && _belongToRealm == null)
'new_belongs_realm': 'global' 'new_belongs_realm': 'global'
else if (_editingChannel != null && _belongToRealm?.id != _editingChannel?.realm?.id) else if (_editingChannel != null &&
_belongToRealm?.id != _editingChannel?.realm?.id)
'new_belongs_realm': _belongToRealm!.alias, 'new_belongs_realm': _belongToRealm!.alias,
}; };
@@ -139,8 +141,11 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(), title: widget.editingChannelAlias != null
? Text('screenChatManage').tr()
: Text('screenChatNew').tr(),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
@@ -152,7 +157,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
leadingPadding: const EdgeInsets.only(left: 10, right: 20), leadingPadding: const EdgeInsets.only(left: 10, right: 20),
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
content: Text( content: Text(
'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']), 'channelEditingNotice'
.tr(args: ['#${_editingChannel!.alias}']),
), ),
actions: [ actions: [
TextButton( TextButton(
@@ -192,12 +198,15 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!), Text(item.name).textStyle(Theme.of(context)
.textTheme
.bodyMedium!),
Text( Text(
item.description, item.description,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
).textStyle(Theme.of(context).textTheme.bodySmall!), ).textStyle(
Theme.of(context).textTheme.bodySmall!),
], ],
), ),
), ),
@@ -213,7 +222,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
CircleAvatar( CircleAvatar(
radius: 16, radius: 16,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
foregroundColor: Theme.of(context).colorScheme.onSurface, foregroundColor:
Theme.of(context).colorScheme.onSurface,
child: const Icon(Symbols.clear), child: const Icon(Symbols.clear),
), ),
const Gap(12), const Gap(12),
@@ -222,7 +232,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('fieldChatBelongToRealmUnset').tr().textStyle( Text('fieldChatBelongToRealmUnset')
.tr()
.textStyle(
Theme.of(context).textTheme.bodyMedium!, Theme.of(context).textTheme.bodyMedium!,
), ),
], ],
@@ -257,7 +269,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
helperText: 'fieldChatAliasHint'.tr(), helperText: 'fieldChatAliasHint'.tr(),
helperMaxLines: 2, helperMaxLines: 2,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(4), const Gap(4),
TextField( TextField(
@@ -266,7 +279,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldChatName'.tr(), labelText: 'fieldChatName'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(4), const Gap(4),
TextField( TextField(
@@ -277,7 +291,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldChatDescription'.tr(), labelText: 'fieldChatDescription'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), const Gap(12),
CheckboxListTile( CheckboxListTile(

View File

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

View File

@@ -157,6 +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: ResponsiveScaffold.getIsExpand(context),
floatingActionButtonLocation: ExpandableFab.location, floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab( floatingActionButton: ExpandableFab(
key: _fabKey, key: _fabKey,
@@ -243,6 +244,8 @@ class _ExploreScreenState extends State<ExploreScreen>
GoRouter.of(context).pushNamed('postShuffle'); GoRouter.of(context).pushNamed('postShuffle');
}, },
), ),
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE))
const Gap(48),
Expanded( Expanded(
child: Center( child: Center(
child: IconButton( child: IconButton(
@@ -534,6 +537,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
switch (ele.type) { switch (ele.type) {
case 'interactive.post': case 'interactive.post':
return OpenablePostItem( return OpenablePostItem(
useReplace: true,
data: SnPost.fromJson(ele.data), data: SnPost.fromJson(ele.data),
maxWidth: 640, maxWidth: 640,
onChanged: (data) { onChanged: (data) {

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

@@ -12,7 +12,6 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.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/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_comment_list.dart'; import 'package:surface/widgets/post/post_comment_list.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
@@ -66,9 +65,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
final double maxWidth = _data?.type == 'video' ? double.infinity : 640; final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
return AppBackground( return AppScaffold(
isRoot: widget.onBack != null, noBackground: ResponsiveScaffold.getIsExpand(context),
child: AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: BackButton( leading: BackButton(
onPressed: () { onPressed: () {
@@ -89,16 +87,14 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
TextSpan( TextSpan(
text: _data?.body['title'] ?? 'postNoun'.tr(), text: _data?.body['title'] ?? 'postNoun'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith( style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: color: Theme.of(context).appBarTheme.foregroundColor!,
Theme.of(context).appBarTheme.foregroundColor!,
), ),
), ),
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: 'postDetail'.tr(), text: 'postDetail'.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: color: Theme.of(context).appBarTheme.foregroundColor!,
Theme.of(context).appBarTheme.foregroundColor!,
), ),
), ),
]), ]),
@@ -175,7 +171,6 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)), SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
], ],
), ),
),
); );
} }
} }

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

View File

@@ -325,20 +325,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {}); setState(() {});
}, },
), ),
CheckboxListTile(
secondary: const Icon(Symbols.left_panel_close),
title: Text('settingsDrawerPreferCollapse').tr(),
subtitle:
Text('settingsDrawerPreferCollapseDescription').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
onChanged: (value) {
_prefs.setBool(kAppDrawerPreferCollapse, value ?? false);
final cfg = context.read<ConfigProvider>();
cfg.calcDrawerSize(context);
setState(() {});
},
),
CheckboxListTile( CheckboxListTile(
secondary: const Icon(Symbols.hide), secondary: const Icon(Symbols.hide),
title: Text('settingsHideBottomNav').tr(), title: Text('settingsHideBottomNav').tr(),

View File

@@ -45,7 +45,9 @@ class _WalletScreenState extends State<WalletScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
appBar: AppBar(leading: PageBackButton(), title: Text('screenAccountWallet').tr()), noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar(
leading: PageBackButton(), title: Text('screenAccountWallet').tr()),
body: Column( body: Column(
children: [ children: [
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
@@ -66,7 +68,9 @@ class _WalletScreenState extends State<WalletScreen> {
SizedBox(width: double.infinity), SizedBox(width: double.infinity),
Text( Text(
NumberFormat.compactCurrency( NumberFormat.compactCurrency(
locale: EasyLocalization.of(context)!.currentLocale.toString(), locale: EasyLocalization.of(context)!
.currentLocale
.toString(),
symbol: '${'walletCurrencyShort'.tr()} ', symbol: '${'walletCurrencyShort'.tr()} ',
decimalDigits: 2, decimalDigits: 2,
).format(double.parse(_wallet!.balance)), ).format(double.parse(_wallet!.balance)),
@@ -76,17 +80,21 @@ class _WalletScreenState extends State<WalletScreen> {
const Gap(16), const Gap(16),
Text( Text(
NumberFormat.compactCurrency( NumberFormat.compactCurrency(
locale: EasyLocalization.of(context)!.currentLocale.toString(), locale: EasyLocalization.of(context)!
.currentLocale
.toString(),
symbol: '${'walletCurrencyGoldenShort'.tr()} ', symbol: '${'walletCurrencyGoldenShort'.tr()} ',
decimalDigits: 2, decimalDigits: 2,
).format(double.parse(_wallet!.goldenBalance)), ).format(double.parse(_wallet!.goldenBalance)),
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
Text('walletCurrencyGolden'.plural(double.parse(_wallet!.goldenBalance))), Text('walletCurrencyGolden'
.plural(double.parse(_wallet!.goldenBalance))),
], ],
).padding(horizontal: 20, vertical: 24), ).padding(horizontal: 20, vertical: 24),
).padding(horizontal: 8, top: 16, bottom: 4), ).padding(horizontal: 8, top: 16, bottom: 4),
if (_wallet != null) Expanded(child: _WalletTransactionList(myself: _wallet!)), if (_wallet != null)
Expanded(child: _WalletTransactionList(myself: _wallet!)),
], ],
), ),
); );
@@ -116,7 +124,10 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
queryParameters: {'take': 10, 'offset': _transactions.length}, queryParameters: {'take': 10, 'offset': _transactions.length},
); );
_totalCount = resp.data['count']; _totalCount = resp.data['count'];
_transactions.addAll(resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? []); _transactions.addAll(resp.data['data']
?.map((e) => SnTransaction.fromJson(e))
.cast<SnTransaction>() ??
[]);
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@@ -141,7 +152,8 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
child: InfiniteList( child: InfiniteList(
itemCount: _transactions.length, itemCount: _transactions.length,
isLoading: _isBusy, isLoading: _isBusy,
hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!, hasReachedMax:
_totalCount != null && _transactions.length >= _totalCount!,
onFetchData: () { onFetchData: () {
_fetchTransactions(); _fetchTransactions();
}, },
@@ -149,7 +161,9 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
final ele = _transactions[idx]; final ele = _transactions[idx];
final isIncoming = ele.payeeId == widget.myself.id; final isIncoming = ele.payeeId == widget.myself.id;
return ListTile( return ListTile(
leading: isIncoming ? const Icon(Symbols.call_received) : const Icon(Symbols.call_made), leading: isIncoming
? const Icon(Symbols.call_received)
: const Icon(Symbols.call_made),
title: Text( title: Text(
'${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}', '${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}',
style: TextStyle(color: isIncoming ? Colors.green : Colors.red), style: TextStyle(color: isIncoming ? Colors.green : Colors.red),
@@ -162,12 +176,20 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
Row( Row(
children: [ children: [
Text( Text(
'walletTransactionType${ele.currency.capitalize()}'.tr(), 'walletTransactionType${ele.currency.capitalize()}'
.tr(),
style: Theme.of(context).textTheme.labelSmall, style: Theme.of(context).textTheme.labelSmall,
), ),
Text(' · ').textStyle(Theme.of(context).textTheme.labelSmall!).padding(right: 4), Text(' · ')
.textStyle(Theme.of(context).textTheme.labelSmall!)
.padding(right: 4),
Text( Text(
DateFormat(null, EasyLocalization.of(context)!.currentLocale.toString()).format(ele.createdAt), DateFormat(
null,
EasyLocalization.of(context)!
.currentLocale
.toString())
.format(ele.createdAt),
style: Theme.of(context).textTheme.labelSmall, style: Theme.of(context).textTheme.labelSmall,
), ),
], ],
@@ -199,8 +221,7 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
final TextEditingController passwordController = TextEditingController(); final TextEditingController passwordController = TextEditingController();
final password = await showDialog<String?>( final password = await showDialog<String?>(
context: context, context: context,
builder: builder: (ctx) => AlertDialog(
(ctx) => AlertDialog(
title: Text('walletCreate').tr(), title: Text('walletCreate').tr(),
content: Column( content: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -217,7 +238,9 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
], ],
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text('cancel').tr()), TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text('cancel').tr()),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(ctx).pop(passwordController.text); Navigator.of(ctx).pop(passwordController.text);
@@ -257,12 +280,18 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
children: [ children: [
CircleAvatar(radius: 28, child: Icon(Symbols.add, size: 28)), CircleAvatar(radius: 28, child: Icon(Symbols.add, size: 28)),
const Gap(12), const Gap(12),
Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(), Text('walletCreate',
Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(), style: Theme.of(context).textTheme.titleLarge)
.tr(),
Text('walletCreateSubtitle',
style: Theme.of(context).textTheme.bodyMedium)
.tr(),
const Gap(8), const Gap(8),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: TextButton(onPressed: _isBusy ? null : () => _createWallet(), child: Text('next').tr()), child: TextButton(
onPressed: _isBusy ? null : () => _createWallet(),
child: Text('next').tr()),
), ),
], ],
).padding(horizontal: 20, vertical: 24), ).padding(horizontal: 20, vertical: 24),

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,17 +45,13 @@ 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,
child: Center(
child: Animate(
autoPlay: false, autoPlay: false,
controller: _animationController, controller: _animationController,
effects: [ effects: [
@@ -79,8 +75,6 @@ class _NoContentWidgetState extends State<NoContentWidget>
content: widget.userinfo?.avatar, content: widget.userinfo?.avatar,
radius: radius, 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
Center(
child: NoContentWidget(
userinfo: _userinfoMetadata, userinfo: _userinfoMetadata,
isFixed: widget.isFixed, avatarSize: widget.avatarSize,
isSpeaking: widget.participant.isSpeaking, isSpeaking: widget.participant.isSpeaking,
), ),
),
if (widget.showStatsLayer) if (widget.showStatsLayer)
Positioned( Positioned(
top: 30, top: 30,
@@ -199,33 +287,31 @@ 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(
@@ -237,6 +323,15 @@ class InteractiveParticipantWidget extends StatelessWidget {
), ),
); );
}, },
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,10 +17,69 @@ 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) {
if (isList) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null)
Text(
title!,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
).padding(left: 2),
Row(
children: [
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),
],
)
],
);
}
return Container(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75), color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 7, vertical: 7,
@@ -77,3 +137,4 @@ class ParticipantInfoWidget extends StatelessWidget {
), ),
); );
} }
}

View File

@@ -16,12 +16,7 @@ class ConnectionIndicator extends StatelessWidget {
final ws = context.watch<WebSocketProvider>(); final ws = context.watch<WebSocketProvider>();
final cfg = context.watch<ConfigProvider>(); final cfg = context.watch<ConfigProvider>();
final marginLeft = final marginLeft = cfg.drawerIsCollapsed ? 0.0 : 80.0;
cfg.drawerIsCollapsed
? 0.0
: cfg.drawerIsExpanded
? 304.0
: 80.0;
return ListenableBuilder( return ListenableBuilder(
listenable: ws, listenable: ws,
@@ -35,10 +30,10 @@ class ConnectionIndicator extends StatelessWidget {
child: GestureDetector( child: GestureDetector(
child: Material( child: Material(
elevation: 2, elevation: 2,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16))),
color: Theme.of(context).colorScheme.secondaryContainer, color: Theme.of(context).colorScheme.secondaryContainer,
child: child: ua.isAuthorized
ua.isAuthorized
? Row( ? Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -47,21 +42,30 @@ class ConnectionIndicator extends StatelessWidget {
if (ws.isBusy) if (ws.isBusy)
Text( Text(
'serverConnecting', 'serverConnecting',
).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) ).tr().textColor(Theme.of(context)
.colorScheme
.onSecondaryContainer)
else if (!ws.isConnected) else if (!ws.isConnected)
Text( Text(
'serverDisconnected', 'serverDisconnected',
).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) ).tr().textColor(Theme.of(context)
.colorScheme
.onSecondaryContainer)
else else
Text( Text(
'serverConnected', 'serverConnected',
).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer), ).tr().textColor(Theme.of(context)
.colorScheme
.onSecondaryContainer),
const Gap(8), const Gap(8),
if (ws.isBusy) if (ws.isBusy)
const CircularProgressIndicator( const CircularProgressIndicator(
strokeWidth: 2.5, strokeWidth: 2.5,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
).width(12).height(12).padding(horizontal: 4, right: 4) )
.width(12)
.height(12)
.padding(horizontal: 4, right: 4)
else if (!ws.isConnected) else if (!ws.isConnected)
const Icon(Symbols.power_off, size: 18) const Icon(Symbols.power_off, size: 18)
else else
@@ -69,7 +73,9 @@ class ConnectionIndicator extends StatelessWidget {
], ],
).padding(horizontal: 8, vertical: 4) ).padding(horizontal: 8, vertical: 4)
: const SizedBox.shrink(), : const SizedBox.shrink(),
).opacity(show ? 1 : 0, animate: true).animate(const Duration(milliseconds: 300), Curves.easeInOut), )
.opacity(show ? 1 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
onTap: () { onTap: () {
if (!ws.isConnected && !ws.isBusy) { if (!ws.isConnected && !ws.isBusy) {
ws.connect(); ws.connect();

View File

@@ -26,9 +26,7 @@ class ContextMenuArea extends StatelessWidget {
final cfg = context.read<ConfigProvider>(); final cfg = context.read<ConfigProvider>();
if (!cfg.drawerIsCollapsed) { if (!cfg.drawerIsCollapsed) {
// Leave padding for side navigation // Leave padding for side navigation
mousePosition = cfg.drawerIsExpanded mousePosition = mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
: mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
} }
}, },
child: GestureDetector( child: GestureDetector(
@@ -40,7 +38,8 @@ class ContextMenuArea extends StatelessWidget {
} }
void _showMenu(BuildContext context, Offset mousePosition) async { void _showMenu(BuildContext context, Offset mousePosition) async {
final menu = contextMenu.copyWith(position: contextMenu.position ?? mousePosition); final menu =
contextMenu.copyWith(position: contextMenu.position ?? mousePosition);
final value = await showContextMenu(context, contextMenu: menu); final value = await showContextMenu(context, contextMenu: menu);
onItemSelected?.call(value); onItemSelected?.call(value);
} }

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,20 +78,29 @@ class _LinkPreviewEntry extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (meta.image != null) if (meta.image != null)
Container( ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: Container(
margin: const EdgeInsets.only(bottom: 4), margin: const EdgeInsets.only(bottom: 4),
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: AutoResizeUniversalImage( child: AutoResizeUniversalImage(
meta.image!.startsWith('//') ? 'https:${meta.image}' : meta.image!, meta.image!.startsWith('//')
? 'https:${meta.image}'
: meta.image!,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
), ),
), ),
), ),
),
SizedBox( SizedBox(
height: 48, height: 48,
child: Row( child: Row(
@@ -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

@@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:animations/animations.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@@ -11,14 +10,9 @@ import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; 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/channel.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/navigation.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:surface/widgets/version_label.dart'; import 'package:surface/widgets/version_label.dart';
class AppNavigationDrawer extends StatefulWidget { class AppNavigationDrawer extends StatefulWidget {
@@ -45,27 +39,18 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final nav = context.watch<NavigationProvider>(); final nav = context.watch<NavigationProvider>();
final cfg = context.watch<ConfigProvider>();
final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
return ListenableBuilder( return ListenableBuilder(
listenable: nav, listenable: nav,
builder: (context, _) { builder: (context, _) {
return Drawer( return Drawer(
elevation: widget.elevation, elevation: widget.elevation,
backgroundColor: backgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(0))),
child: Column( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (!kIsWeb && if (!kIsWeb &&
(Platform.isWindows || (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
Platform.isLinux ||
Platform.isMacOS) &&
!cfg.drawerIsExpanded)
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
@@ -78,42 +63,37 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
child: WindowTitleBarBox(), child: WindowTitleBarBox(),
), ),
Gap(MediaQuery.of(context).padding.top), Gap(MediaQuery.of(context).padding.top),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Solar Network').bold(),
AppVersionLabel(),
],
).padding(
horizontal: 32,
vertical: 12,
),
Expanded( Expanded(
child: _DrawerContentList(), child: ListView(
), padding: EdgeInsets.zero,
Row( children: [
spacing: 8, ...nav.destinations.mapIndexed((idx, ele) {
children: return ListTile(
nav.destinations.where((ele) => ele.isPinned).mapIndexed( leading: ele.icon,
(idx, ele) { title: Text(ele.label).tr(),
return Expanded( contentPadding: EdgeInsets.symmetric(horizontal: 24),
child: Tooltip( selected: nav.currentIndex == idx,
message: ele.label.tr(), onTap: () {
child: IconButton( GoRouter.of(context).pushNamed(ele.screen);
icon: ele.icon,
color: nav.currentIndex == idx
? Theme.of(context).colorScheme.onPrimaryContainer
: Theme.of(context).colorScheme.onSurface,
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
nav.currentIndex == idx
? Theme.of(context)
.colorScheme
.primaryContainer
: Colors.transparent,
),
),
onPressed: () {
GoRouter.of(context).goNamed(ele.screen);
Scaffold.of(context).closeDrawer();
nav.setIndex(idx); nav.setIndex(idx);
Scaffold.of(context).closeDrawer();
}, },
),
),
); );
}, })
).toList(), ],
).padding(horizontal: 16, bottom: 8), ),
),
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: ListTile( child: ListTile(
@@ -167,163 +147,3 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
); );
} }
} }
class _DrawerContentList extends StatelessWidget {
const _DrawerContentList();
@override
Widget build(BuildContext context) {
final ct = context.read<ChatChannelProvider>();
final sn = context.read<SnNetworkProvider>();
final nav = context.watch<NavigationProvider>();
final rel = context.watch<SnRealmProvider>();
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> primaryAnimation,
Animation<double> secondaryAnimation) {
return SharedAxisTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
transitionType: SharedAxisTransitionType.horizontal,
child: child,
);
},
child: nav.focusedRealm == null
? ListView(
key: const Key('realm-list-view'),
padding: EdgeInsets.zero,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Solar Network').bold(),
AppVersionLabel(),
],
).padding(
horizontal: 32,
vertical: 12,
),
...rel.availableRealms.map((ele) {
return ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.symmetric(horizontal: 24),
leading: AccountImage(
content: ele.avatar,
radius: 16,
),
title: Text(ele.name),
onTap: () {
nav.setFocusedRealm(ele);
},
);
}),
ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.only(left: 28, right: 16),
leading: const Icon(Symbols.globe).padding(right: 4),
title: Text('screenRealmDiscovery').tr(),
onTap: () {
GoRouter.of(context).pushNamed('realmDiscovery');
Scaffold.of(context).closeDrawer();
},
),
],
)
: ListView(
key: ValueKey(nav.focusedRealm),
padding: EdgeInsets.zero,
children: [
if (nav.focusedRealm!.banner != null)
AspectRatio(
aspectRatio: 16 / 9,
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(
nav.focusedRealm!.banner!,
),
fit: BoxFit.cover,
),
),
ListTile(
minTileHeight: 48,
tileColor: Theme.of(context).colorScheme.surfaceContainer,
contentPadding: EdgeInsets.only(
left: 24,
right: 16,
),
leading: AccountImage(
content: nav.focusedRealm!.avatar,
radius: 16,
),
trailing: IconButton(
icon: const Icon(Symbols.close),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
visualDensity: VisualDensity.compact,
onPressed: () {
nav.setFocusedRealm(null);
},
),
title: Text(nav.focusedRealm!.name),
onTap: () {
GoRouter.of(context).goNamed(
'realmDetail',
pathParameters: {
'alias': nav.focusedRealm!.alias,
},
);
Scaffold.of(context).closeDrawer();
},
),
ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.only(
left: 28,
right: 8,
),
leading: const Icon(Symbols.globe),
title: Text('community').tr(),
onTap: () {
GoRouter.of(context).goNamed(
'realmCommunity',
pathParameters: {
'alias': nav.focusedRealm!.alias,
},
);
Scaffold.of(context).closeDrawer();
},
),
if (ct.availableChannels
.where((ele) => ele.realmId == nav.focusedRealm?.id)
.isNotEmpty)
const Divider(height: 1),
...(ct.availableChannels
.where((ele) => ele.realmId == nav.focusedRealm?.id)
.map((ele) {
return ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.only(
left: 28,
right: 8,
),
leading: const Icon(Symbols.tag),
title: Text(ele.name),
onTap: () {
GoRouter.of(context).goNamed(
'chatRoom',
pathParameters: {
'scope': ele.realm?.alias ?? 'global',
'alias': ele.alias,
},
);
Scaffold.of(context).closeDrawer();
},
);
}))
],
),
);
}
}

View File

@@ -1,10 +1,12 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; 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:surface/providers/navigation.dart'; import 'package:surface/providers/navigation.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/widgets/account/account_image.dart';
class AppRailNavigation extends StatefulWidget { class AppRailNavigation extends StatefulWidget {
const AppRailNavigation({super.key}); const AppRailNavigation({super.key});
@@ -18,43 +20,59 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context)); context
.read<NavigationProvider>()
.autoDetectIndex(GoRouter.maybeOf(context));
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.watch<UserProvider>();
final nav = context.watch<NavigationProvider>(); final nav = context.watch<NavigationProvider>();
return ListenableBuilder( return ListenableBuilder(
listenable: nav, listenable: nav,
builder: (context, _) { builder: (context, _) {
final destinations = nav.destinations.where((ele) => ele.isPinned).toList(); final destinations = nav.destinations.toList();
return SizedBox( return SizedBox(
width: 80, width: 80,
child: NavigationRail( child: NavigationRail(
selectedIndex: labelType: NavigationRailLabelType.selected,
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null, backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainerLow
.withOpacity(0.5),
selectedIndex: nav.currentIndex != null &&
nav.currentIndex! < nav.destinations.length
? nav.currentIndex
: null,
destinations: [ destinations: [
...destinations.where((ele) => ele.isPinned).map((ele) { ...destinations.map((ele) {
return NavigationRailDestination( return NavigationRailDestination(
icon: ele.icon, icon: ele.icon,
label: Text(ele.label).tr(), label: Text(ele.label).tr(),
); );
}), }),
], ],
leading: const Gap(4),
trailing: Expanded( trailing: Expanded(
child: Align( child: Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: StyledWidget( child: Padding(
IconButton( padding: EdgeInsets.only(bottom: 24),
icon: const Icon(Symbols.menu), child: GestureDetector(
onPressed: () { child: AccountImage(
Scaffold.of(context).openDrawer(); content: ua.user?.avatar,
fallbackWidget:
ua.isAuthorized ? null : const Icon(Symbols.login),
),
onTap: () {
GoRouter.of(context).goNamed('account');
}, },
), ),
).padding(bottom: 16), ),
), ),
), ),
onDestinationSelected: (idx) { onDestinationSelected: (idx) {

View File

@@ -66,7 +66,9 @@ class AppScaffold extends StatelessWidget {
return Scaffold( return Scaffold(
extendBody: true, extendBody: true,
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: noBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor,
body: SizedBox.expand( body: SizedBox.expand(
child: noBackground child: noBackground
? content ? content
@@ -111,7 +113,6 @@ class AppRootScaffold extends StatelessWidget {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final isCollapseDrawer = cfg.drawerIsCollapsed; final isCollapseDrawer = cfg.drawerIsCollapsed;
final isExpandedDrawer = cfg.drawerIsExpanded;
final routeName = GoRouter.of(context) final routeName = GoRouter.of(context)
.routerDelegate .routerDelegate
@@ -132,19 +133,7 @@ class AppRootScaffold extends StatelessWidget {
? body ? body
: Row( : Row(
children: [ children: [
Container( AppRailNavigation(),
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
),
),
),
child: isExpandedDrawer
? AppNavigationDrawer(elevation: 0)
: AppRailNavigation(),
),
Expanded(child: body), Expanded(child: body),
], ],
); );
@@ -232,10 +221,72 @@ class AppRootScaffold extends StatelessWidget {
), ),
], ],
), ),
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null, drawerEdgeDragWidth: isPopable ? 0 : null,
drawer: isCollapseDrawer ? const AppNavigationDrawer() : null,
bottomNavigationBar: bottomNavigationBar:
isShowBottomNavigation ? AppBottomNavigationBar() : null, isShowBottomNavigation ? AppBottomNavigationBar() : null,
); );
} }
} }
class ResponsiveScaffold extends StatelessWidget {
final Widget aside;
final Widget? child;
final int asideFlex;
final int contentFlex;
const ResponsiveScaffold({
super.key,
required this.aside,
required this.child,
this.asideFlex = 1,
this.contentFlex = 2,
});
static bool getIsExpand(BuildContext context) {
return ResponsiveBreakpoints.of(context).largerOrEqualTo(TABLET);
}
@override
Widget build(BuildContext context) {
if (getIsExpand(context)) {
return AppBackground(
isRoot: true,
child: Row(
children: [
Flexible(
flex: asideFlex,
child: aside,
),
VerticalDivider(width: 1),
if (child != null && child != aside)
Flexible(flex: contentFlex, child: child!)
else
Flexible(
flex: contentFlex,
child: ResponsiveScaffoldLanding(child: null),
),
],
),
);
}
return AppBackground(isRoot: true, child: child ?? aside);
}
}
class ResponsiveScaffoldLanding extends StatelessWidget {
final Widget? child;
const ResponsiveScaffoldLanding({super.key, required this.child});
@override
Widget build(BuildContext context) {
if (ResponsiveScaffold.getIsExpand(context) || child == null) {
return AppScaffold(
noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar(),
body: const SizedBox.shrink(),
);
}
return child!;
}
}

View File

@@ -4,7 +4,6 @@ import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
@@ -30,19 +29,9 @@ class PostCommentQuickAction extends StatelessWidget {
return Container( return Container(
height: 240, height: 240,
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? const EdgeInsets.symmetric(vertical: 8)
: EdgeInsets.zero,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE) borderRadius: BorderRadius.zero,
? const BorderRadius.all(Radius.circular(8)) border: Border.symmetric(
: BorderRadius.zero,
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? Border.all(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
)
: Border.symmetric(
horizontal: BorderSide( horizontal: BorderSide(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio, width: 1 / devicePixelRatio,
@@ -190,9 +179,7 @@ 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,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
@@ -240,7 +227,6 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
), ),
), ),
], ],
),
); );
} }
} }

View File

@@ -1,7 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:animations/animations.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:file_saver/file_saver.dart'; import 'package:file_saver/file_saver.dart';
@@ -26,7 +25,6 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/translation.dart'; import 'package:surface/providers/translation.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/post/post_detail.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/types/reaction.dart'; import 'package:surface/types/reaction.dart';
@@ -53,6 +51,7 @@ class OpenablePostItem extends StatelessWidget {
final bool showMenu; final bool showMenu;
final bool showFullPost; final bool showFullPost;
final bool showExpandableComments; final bool showExpandableComments;
final bool useReplace;
final double? maxWidth; final double? maxWidth;
final Function(SnPost data)? onChanged; final Function(SnPost data)? onChanged;
final Function()? onDeleted; final Function()? onDeleted;
@@ -66,6 +65,7 @@ class OpenablePostItem extends StatelessWidget {
this.showMenu = true, this.showMenu = true,
this.showFullPost = false, this.showFullPost = false,
this.showExpandableComments = false, this.showExpandableComments = false,
this.useReplace = false,
this.maxWidth, this.maxWidth,
this.onChanged, this.onChanged,
this.onDeleted, this.onDeleted,
@@ -74,14 +74,10 @@ class OpenablePostItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>();
return Container( return Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Center( child: Center(
child: OpenContainer( child: GestureDetector(
closedBuilder: (_, __) => Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: PostItem( child: PostItem(
data: data, data: data,
maxWidth: maxWidth, maxWidth: maxWidth,
@@ -92,22 +88,18 @@ class OpenablePostItem extends StatelessWidget {
onDeleted: onDeleted, onDeleted: onDeleted,
onSelectAnswer: onSelectAnswer, onSelectAnswer: onSelectAnswer,
), ),
), onTap: () {
openBuilder: (_, close) => PostDetailScreen( if (useReplace) {
slug: data.id.toString(), GoRouter.of(context)
preload: data, .pushReplacementNamed('postDetail', pathParameters: {
onBack: close, 'slug': data.id.toString(),
), });
openColor: Colors.transparent, } else {
openElevation: 0, GoRouter.of(context).pushNamed('postDetail', pathParameters: {
transitionType: ContainerTransitionType.fade, 'slug': data.id.toString(),
closedElevation: 0, });
closedColor: Theme.of(context).colorScheme.surface.withOpacity( }
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0 : 1, },
),
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
), ),
), ),
); );
@@ -282,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();
@@ -292,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;
} }
@@ -349,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(
@@ -369,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),
@@ -463,8 +457,8 @@ 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,
), ),
@@ -487,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,
@@ -593,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(
@@ -613,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')
@@ -720,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
@@ -730,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,
@@ -754,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,
@@ -816,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(
@@ -825,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,
), ),
@@ -863,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,
@@ -1154,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'],
@@ -1263,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,
@@ -1279,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),
@@ -1576,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(
@@ -1583,7 +1560,6 @@ class _PostContentHeader extends StatelessWidget {
maxLines: 1, maxLines: 1,
).bold(), ).bold(),
), ),
const Gap(4),
Flexible( Flexible(
child: Text( child: Text(
isRelativeDate isRelativeDate
@@ -1595,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 {
@@ -1604,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)
@@ -1627,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),
], ],
@@ -1656,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) {
@@ -1714,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,
@@ -2070,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,
@@ -2360,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+84 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: