Compare commits

..

34 Commits

Author SHA1 Message Date
3b1e918117 🐛 Fix side nav cause render error 2025-01-20 01:43:11 +08:00
ed7981fdaf 🧪 Post max width 2025-01-19 17:20:24 +08:00
9698ca53e4 Swipe up to view attachment details 2025-01-19 11:44:14 +08:00
ddc1dc7daf 💄 Optimize attachment zoom page 2025-01-19 01:00:00 +08:00
1625a957f8 👔 Use material design 3 by default 2025-01-19 00:39:47 +08:00
2dc50d627e 🧱 Fix roadsign config 2025-01-16 21:51:28 +08:00
2ffde9a3dd 🚀 Launch 2.2.2+53 2025-01-15 16:00:59 +08:00
5967a91ae1 Chat user popover 2025-01-15 15:52:52 +08:00
32c1effcb5 💄 Post editor content max width 2025-01-15 15:19:46 +08:00
9d0e19c56f 🐛 Fix chat messages 2025-01-15 15:14:13 +08:00
acf4e634fe 🐛 Fix websocket will put message in wrong channel 2025-01-14 23:32:02 +08:00
25942c2338 💄 Optimize chat max width 2025-01-14 23:30:35 +08:00
a4f81f6ba1 🐛 Post auto warp 2025-01-14 23:24:11 +08:00
c1b9090e51 💄 Optimized attachment list 2025-01-14 23:17:34 +08:00
f494f70003 🚀 Launch 2.2.2+52 2025-01-08 18:07:51 +08:00
fb2a55a909 🐛 Fix editing message did not load the attachment 2025-01-08 17:48:46 +08:00
4edfa7fd50 🐛 Optimizing styling of chat 2025-01-08 17:37:16 +08:00
d699cac9b1 🚀 Launch 2.2.2+51 2025-01-07 18:10:20 +08:00
c0428e12c1 🐛 Fixed the drawer styling issue 2025-01-07 13:11:45 +08:00
55f434ff05 🚀 Launch 2.2.2+40 2025-01-06 23:39:49 +08:00
f2b3bdda2d 🐛 Add ability check to text selection chat message action 2025-01-06 23:17:24 +08:00
1f6bf33b0e Chat message action on system text selection area 2025-01-06 23:15:18 +08:00
e2027b1a32 Able to prefer sidebar to collapse 2025-01-06 22:57:44 +08:00
2b3a58b55e Optimize temporary save post scenario 2025-01-06 22:12:10 +08:00
6ac536412a 🚀 Launch 2.2.2+49 2025-01-06 22:05:20 +08:00
52f8ffe4e4 💄 Update the app bar color when in transparent mode 2025-01-06 21:57:50 +08:00
aca81431aa 🐛 Fix desktop share post as image do not include file extension name 2025-01-06 21:53:35 +08:00
1fadd850b7 💄 Optimize some styling 2025-01-06 21:46:21 +08:00
ed2a9a21b6 🐛 Fix chat username height difference 2025-01-06 19:18:23 +08:00
57279eb3e4 🚀 Launch 2.2.1+48 2025-01-05 13:41:38 +08:00
c403a2914a 💄 Optimized article attachments displaying strategy 2025-01-05 13:34:37 +08:00
bcb176344c 💄 Optimize some styling 2025-01-05 13:29:39 +08:00
ecf362cffc 🚀 Launch 2.2.1+47 2025-01-05 12:13:43 +08:00
f4ab7671d8 🐛 Bug fixes on resetting post write controller 2025-01-05 12:06:49 +08:00
36 changed files with 895 additions and 401 deletions

View File

@@ -1,12 +1,12 @@
{ {
"sync": { "sync": {
"region": "solian-next", "region": "solian",
"configPath": "roadsign.toml" "configPath": "roadsign.toml"
}, },
"deployments": [ "deployments": [
{ {
"region": "solian-next", "region": "solian",
"site": "solian-next-web", "site": "solian-web",
"path": "build/web" "path": "build/web"
} }
] ]

View File

@@ -17,6 +17,7 @@
android:label="Solian" android:label="Solian"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true"
android:requestLegacyExternalStorage="true"> android:requestLegacyExternalStorage="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View File

@@ -181,6 +181,8 @@
"settingsAppearance": "Appearance", "settingsAppearance": "Appearance",
"settingsAppBarTransparent": "Transparent App Bar", "settingsAppBarTransparent": "Transparent App Bar",
"settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.", "settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
"settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
"settingsDrawerPreferCollapseDescription": "Make the drawer to collapse even when the screen is wide enough.",
"settingsBackgroundImage": "Background Image", "settingsBackgroundImage": "Background Image",
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.", "settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
"settingsBackgroundImageClear": "Clear Existing Background Image", "settingsBackgroundImageClear": "Clear Existing Background Image",
@@ -292,6 +294,7 @@
"addAttachmentFromCameraPhoto": "Take photo", "addAttachmentFromCameraPhoto": "Take photo",
"addAttachmentFromCameraVideo": "Take video", "addAttachmentFromCameraVideo": "Take video",
"addAttachmentFromRandomId": "Link via RID", "addAttachmentFromRandomId": "Link via RID",
"attachmentDetailInfo": "Attachment details",
"attachmentPastedImage": "Pasted Image", "attachmentPastedImage": "Pasted Image",
"attachmentInsertLink": "Insert Link", "attachmentInsertLink": "Insert Link",
"attachmentSetAsPostThumbnail": "Set as post thumbnail", "attachmentSetAsPostThumbnail": "Set as post thumbnail",

View File

@@ -185,6 +185,8 @@
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。", "settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
"settingsAppBarTransparent": "透明顶栏", "settingsAppBarTransparent": "透明顶栏",
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。", "settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
"settingsDrawerPreferCollapse": "侧边栏偏好折叠",
"settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。",
"settingsColorScheme": "主题色", "settingsColorScheme": "主题色",
"settingsColorSchemeDescription": "设置应用主题色。", "settingsColorSchemeDescription": "设置应用主题色。",
"settingsColorSeed": "预设色彩主题", "settingsColorSeed": "预设色彩主题",
@@ -290,6 +292,7 @@
"addAttachmentFromCameraPhoto": "拍摄照片", "addAttachmentFromCameraPhoto": "拍摄照片",
"addAttachmentFromCameraVideo": "拍摄视频", "addAttachmentFromCameraVideo": "拍摄视频",
"addAttachmentFromRandomId": "通过访问 ID 链接", "addAttachmentFromRandomId": "通过访问 ID 链接",
"attachmentDetailInfo": "附件详细信息",
"attachmentPastedImage": "粘贴的图片", "attachmentPastedImage": "粘贴的图片",
"attachmentInsertLink": "插入连接", "attachmentInsertLink": "插入连接",
"attachmentSetAsPostThumbnail": "设置为帖子缩略图", "attachmentSetAsPostThumbnail": "设置为帖子缩略图",

View File

@@ -185,6 +185,8 @@
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。", "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
"settingsAppBarTransparent": "透明頂欄", "settingsAppBarTransparent": "透明頂欄",
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。", "settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
"settingsColorScheme": "主題色", "settingsColorScheme": "主題色",
"settingsColorSchemeDescription": "設置應用主題色。", "settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題", "settingsColorSeed": "預設色彩主題",

View File

@@ -185,6 +185,8 @@
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。", "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
"settingsAppBarTransparent": "透明頂欄", "settingsAppBarTransparent": "透明頂欄",
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。", "settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
"settingsColorScheme": "主題色", "settingsColorScheme": "主題色",
"settingsColorSchemeDescription": "設置應用主題色。", "settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題", "settingsColorSeed": "預設色彩主題",

View File

@@ -74,6 +74,7 @@ class ChatMessageController extends ChangeNotifier {
_wsSubscription = _ws.stream.stream.listen((event) { _wsSubscription = _ws.stream.stream.listen((event) {
switch (event.method) { switch (event.method) {
case 'events.new': case 'events.new':
if (event.payload?['channel_id'] != channel?.id) break;
final payload = SnChatMessage.fromJson(event.payload!); final payload = SnChatMessage.fromJson(event.payload!);
_addMessage(payload); _addMessage(payload);
break; break;
@@ -83,7 +84,6 @@ class ChatMessageController extends ChangeNotifier {
if (member.id == profile?.id) break; if (member.id == profile?.id) break;
if (!typingMembers.any((x) => x.id == member.id)) { if (!typingMembers.any((x) => x.id == member.id)) {
typingMembers.add(member); typingMembers.add(member);
print('Typing member: ${typingMembers.map((ele) => member.id)}');
notifyListeners(); notifyListeners();
} }
typingInactiveTimer[member.id]?.cancel(); typingInactiveTimer[member.id]?.cancel();

View File

@@ -154,7 +154,10 @@ class PostWriteController extends ChangeNotifier {
final TextEditingController descriptionController = TextEditingController(); final TextEditingController descriptionController = TextEditingController();
final TextEditingController aliasController = TextEditingController(); final TextEditingController aliasController = TextEditingController();
PostWriteController() { bool _temporarySaveActive = false;
PostWriteController({bool doLoadFromTemporary = true}) {
_temporarySaveActive = doLoadFromTemporary;
titleController.addListener(() { titleController.addListener(() {
_temporaryPlanSave(); _temporaryPlanSave();
notifyListeners(); notifyListeners();
@@ -166,7 +169,7 @@ class PostWriteController extends ChangeNotifier {
contentController.addListener(() { contentController.addListener(() {
_temporaryPlanSave(); _temporaryPlanSave();
}); });
_temporaryLoad(); if (doLoadFromTemporary) _temporaryLoad();
} }
String mode = kTitleMap.keys.first; String mode = kTitleMap.keys.first;
@@ -317,6 +320,7 @@ class PostWriteController extends ChangeNotifier {
Timer? _temporarySaveTimer; Timer? _temporarySaveTimer;
void _temporaryPlanSave() { void _temporaryPlanSave() {
if (!_temporarySaveActive) return;
_temporarySaveTimer?.cancel(); _temporarySaveTimer?.cancel();
_temporarySaveTimer = Timer(const Duration(seconds: 1), () { _temporarySaveTimer = Timer(const Duration(seconds: 1), () {
_temporarySave(); _temporarySave();
@@ -623,6 +627,8 @@ class PostWriteController extends ChangeNotifier {
void reset() { void reset() {
publishedAt = null; publishedAt = null;
publishedUntil = null; publishedUntil = null;
thumbnail = null;
visibility = 0;
titleController.clear(); titleController.clear();
descriptionController.clear(); descriptionController.clear();
contentController.clear(); contentController.clear();

View File

@@ -10,7 +10,6 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.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:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@@ -18,7 +17,6 @@ import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:responsive_framework/responsive_framework.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/firebase_options.dart'; import 'package:surface/firebase_options.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart'; import 'package:surface/providers/chat_call.dart';
@@ -42,7 +40,6 @@ import 'package:surface/types/chat.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.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/version_label.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:workmanager/workmanager.dart'; import 'package:workmanager/workmanager.dart';
import 'package:in_app_review/in_app_review.dart'; import 'package:in_app_review/in_app_review.dart';
@@ -261,6 +258,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
Future<void> _initialize() async { Future<void> _initialize() async {
try { try {
final cfg = context.read<ConfigProvider>();
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
final home = context.read<HomeWidgetProvider>(); final home = context.read<HomeWidgetProvider>();
await home.initialize(); await home.initialize();
if (!mounted) return; if (!mounted) return;
@@ -301,6 +302,17 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.child; final cfg = context.read<ConfigProvider>();
return NotificationListener<SizeChangedLayoutNotification>(
onNotification: (notification) {
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
return false;
},
child: SizeChangedLayoutNotifier(
child: widget.child,
),
);
} }
} }

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/providers/widget.dart'; import 'package:surface/providers/widget.dart';
@@ -12,6 +13,7 @@ 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 Map<String, FilterQuality> kImageQualityLevel = { const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none, 'settingsImageQualityLowest': FilterQuality.none,
@@ -33,6 +35,24 @@ class ConfigProvider extends ChangeNotifier {
prefs = await SharedPreferences.getInstance(); prefs = await SharedPreferences.getInstance();
} }
bool drawerIsCollapsed = false;
bool drawerIsExpanded = false;
void calcDrawerSize(BuildContext context) {
final rpb = ResponsiveBreakpoints.of(context);
final newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
final newDrawerIsExpanded = rpb.largerThan(TABLET)
? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
? false
: true
: false;
if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) {
drawerIsExpanded = newDrawerIsExpanded;
drawerIsCollapsed = newDrawerIsCollapsed;
notifyListeners();
}
}
FilterQuality get imageQuality { FilterQuality get imageQuality {
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high; return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
} }

View File

@@ -35,7 +35,7 @@ class WebSocketProvider extends ChangeNotifier {
Future<void> connect({noRetry = false}) async { Future<void> connect({noRetry = false}) async {
if (!_ua.isAuthorized) return; if (!_ua.isAuthorized) return;
if (isConnected) { if (isConnected || conn != null) {
disconnect(); disconnect();
} }
@@ -97,7 +97,7 @@ class WebSocketProvider extends ChangeNotifier {
onError: (err) { onError: (err) {
isConnected = false; isConnected = false;
notifyListeners(); notifyListeners();
Future.delayed(const Duration(seconds: 11), () => connect()); Future.delayed(const Duration(seconds: 1), () => connect());
}, },
); );
} }

View File

@@ -3,7 +3,7 @@ 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';
import 'package:surface/screens/account.dart'; import 'package:surface/screens/account.dart';
import 'package:surface/screens/account/pfp.dart'; import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/screens/account/profile_edit.dart'; import 'package:surface/screens/account/profile_edit.dart';
import 'package:surface/screens/account/publishers/publisher_edit.dart'; import 'package:surface/screens/account/publishers/publisher_edit.dart';
import 'package:surface/screens/account/publishers/publisher_new.dart'; import 'package:surface/screens/account/publishers/publisher_new.dart';

View File

@@ -236,7 +236,7 @@ class _ChatScreenState extends State<ChatScreen> {
'alias': channel.alias, 'alias': channel.alias,
}, },
).then((value) { ).then((value) {
if (value == true) _refreshChannels(); if (mounted) _refreshChannels();
}); });
}, },
); );

View File

@@ -217,27 +217,48 @@ class _ExploreScreenState extends State<ExploreScreen> {
hasReachedMax: _postCount != null && _posts.length >= _postCount!, hasReachedMax: _postCount != null && _posts.length >= _postCount!,
onFetchData: _fetchPosts, onFetchData: _fetchPosts,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
return GestureDetector( return Center(
child: PostItem( child: Container(
data: _posts[idx], decoration: BoxDecoration(
maxWidth: 640, border: Border(
onChanged: (data) { left: BorderSide(
setState(() => _posts[idx] = data); color: Theme.of(context).dividerColor,
}, width: 1 / MediaQuery.of(context).devicePixelRatio,
onDeleted: () { ),
_refreshPosts(); right: BorderSide(
}, color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
constraints: const BoxConstraints(maxWidth: 640),
child: Column(
children: [
GestureDetector(
child: PostItem(
data: _posts[idx],
maxWidth: 640,
onChanged: (data) {
setState(() => _posts[idx] = data);
},
onDeleted: () {
_refreshPosts();
},
),
onTap: () {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {'slug': _posts[idx].id.toString()},
extra: _posts[idx],
);
},
),
const Divider(height: 1),
],
),
), ),
onTap: () {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {'slug': _posts[idx].id.toString()},
extra: _posts[idx],
);
},
); );
}, },
separatorBuilder: (context, index) => const Divider(height: 1),
), ),
], ],
), ),

View File

@@ -206,10 +206,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
), ),
if (nty.subtitle != null) const Gap(4), if (nty.subtitle != null) const Gap(4),
MarkdownTextContent( SelectionArea(
content: nty.body, child: MarkdownTextContent(
isAutoWarp: true, content: nty.body,
isSelectable: true, isAutoWarp: true,
),
), ),
if ([ if ([
'interactive.feedback', 'interactive.feedback',

View File

@@ -96,6 +96,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
), ),
), ),
]), ]),
maxLines: 2,
overflow: TextOverflow.ellipsis,
) )
: Text('postDetail').tr(), : Text('postDetail').tr(),
), ),

View File

@@ -54,7 +54,9 @@ class PostEditorScreen extends StatefulWidget {
} }
class _PostEditorScreenState extends State<PostEditorScreen> { class _PostEditorScreenState extends State<PostEditorScreen> {
final PostWriteController _writeController = PostWriteController(); late final PostWriteController _writeController = PostWriteController(
doLoadFromTemporary: widget.postEditId == null,
);
bool _isFetching = false; bool _isFetching = false;
@@ -301,19 +303,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
], ],
), ),
// Content Input Area // Content Input Area
TextField( Container(
controller: _writeController.contentController, constraints: const BoxConstraints(maxWidth: 640),
maxLines: null, child: TextField(
decoration: InputDecoration( controller: _writeController.contentController,
hintText: 'fieldPostContent'.tr(), maxLines: null,
hintStyle: TextStyle(fontSize: 14), decoration: InputDecoration(
isCollapsed: true, hintText: 'fieldPostContent'.tr(),
contentPadding: const EdgeInsets.symmetric( hintStyle: TextStyle(fontSize: 14),
horizontal: 16, isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
),
border: InputBorder.none,
), ),
border: InputBorder.none, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
] ]
.expandIndexed( .expandIndexed(
@@ -364,6 +369,15 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
LoadingIndicator(isActive: _isLoading),
if (_writeController.isBusy && _writeController.progress != null)
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _writeController.progress),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
)
else if (_writeController.isBusy)
const LinearProgressIndicator(value: null, minHeight: 2),
Container( Container(
child: _writeController.temporaryRestored child: _writeController.temporaryRestored
? Container( ? Container(
@@ -394,15 +408,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
) )
.height(_writeController.temporaryRestored ? 32 : 0, animate: true) .height(_writeController.temporaryRestored ? 32 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn), .animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
LoadingIndicator(isActive: _isLoading),
if (_writeController.isBusy && _writeController.progress != null)
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _writeController.progress),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
)
else if (_writeController.isBusy)
const LinearProgressIndicator(value: null, minHeight: 2),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [

View File

@@ -120,7 +120,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
subtitle: Text('settingsThemeMaterial3Description').tr(), subtitle: Text('settingsThemeMaterial3Description').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17), contentPadding: const EdgeInsets.only(left: 24, right: 17),
secondary: const Icon(Symbols.new_releases), secondary: const Icon(Symbols.new_releases),
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false, value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_prefs.setBool( _prefs.setBool(
@@ -240,6 +240,19 @@ 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(() {});
},
),
], ],
), ),
Column( Column(

View File

@@ -36,7 +36,7 @@ Future<ThemeData> createAppTheme(
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false; final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
return ThemeData( return ThemeData(
useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false), useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true),
colorScheme: colorScheme, colorScheme: colorScheme,
brightness: brightness, brightness: brightness,
iconTheme: IconThemeData( iconTheme: IconThemeData(
@@ -48,9 +48,19 @@ Future<ThemeData> createAppTheme(
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
centerTitle: true, centerTitle: true,
elevation: hasAppBarBlurry ? 0 : null, elevation: hasAppBarBlurry ? 0 : null,
backgroundColor: hasAppBarBlurry ? colorScheme.surfaceContainer.withAlpha(200) : colorScheme.primary, backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary, foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
), ),
scaffoldBackgroundColor: Colors.transparent, scaffoldBackgroundColor: Colors.transparent,
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
TargetPlatform.linux: ZoomPageTransitionsBuilder(),
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
},
),
); );
} }

View File

@@ -0,0 +1,164 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/experience.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/types/account.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/universal_image.dart';
class AccountPopoverCard extends StatelessWidget {
final SnAccount data;
const AccountPopoverCard({super.key, required this.data});
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (data.banner.isNotEmpty)
Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: AspectRatio(
aspectRatio: 16 / 7,
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(data.banner),
fit: BoxFit.cover,
),
),
),
// Top padding
Gap(16),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AccountImage(
content: data.avatar,
radius: 20,
),
Gap(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(data.nick).bold(),
Text('@${data.name}').fontSize(13).opacity(0.75),
],
),
),
IconButton(
onPressed: () {
Navigator.pop(context);
GoRouter.of(context).pushNamed(
'accountProfilePage',
pathParameters: {'name': data.name},
);
},
icon: const Icon(Symbols.chevron_right),
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
),
const Gap(8)
],
).padding(horizontal: 16),
const Gap(16),
Wrap(
children: data.badges
.map(
(ele) => Tooltip(
richMessage: TextSpan(
children: [
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
if (ele.metadata['title'] != null)
TextSpan(
text: '\n${ele.metadata['title']}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: '\n'),
TextSpan(
text: DateFormat.yMEd().format(ele.createdAt),
),
],
),
child: Icon(
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
color: kBadgesMeta[ele.type]?.$3,
fill: 1,
),
),
)
.toList(),
).padding(horizontal: 24),
const Gap(8),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.star),
const Gap(8),
Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
const Gap(8),
Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
const Gap(8),
Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 160),
child: LinearProgressIndicator(
value: calcLevelUpProgress(data.profile?.experience ?? 0),
borderRadius: BorderRadius.circular(8),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
).alignment(Alignment.centerLeft),
),
],
).padding(horizontal: 24),
FutureBuilder(
future: sn.client.get('/cgi/id/users/${data.name}/status'),
builder: (context, snapshot) {
final SnAccountStatusInfo? status =
snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null;
return Row(
children: [
Icon(
Symbols.circle,
fill: 1,
size: 16,
color: (status?.isOnline ?? false) ? Colors.green : Colors.grey,
).padding(all: 4),
const Gap(8),
Text(
status != null
? status.isOnline
? 'accountStatusOnline'.tr()
: 'accountStatusOffline'.tr()
: 'loading'.tr(),
),
if (status != null && !status.isOnline && status.lastSeenAt != null)
Text(
'accountStatusLastSeen'.tr(args: [
status.lastSeenAt != null
? RelativeTime(context).format(
status.lastSeenAt!.toLocal(),
)
: 'unknown',
]),
).padding(left: 6).opacity(0.75),
],
).padding(horizontal: 24);
},
),
// Bottom padding
const Gap(16),
],
);
}
}

View File

@@ -106,6 +106,44 @@ class _AttachmentListState extends State<AttachmentList> {
} }
if (widget.gridded) { if (widget.gridded) {
final fullOfImage =
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
if(!fullOfImage) {
return Container(
margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: Column(
spacing: 4,
children: widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: AspectRatio(
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
child: Container(
constraints: constraints,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: BoxFit.cover,
),
),
),
),
)
.toList(),
),
),
);
}
return Container( return Container(
margin: widget.padding ?? EdgeInsets.zero, margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@@ -129,6 +129,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75); Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
bool _showDetail = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
@@ -144,218 +146,348 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
onDismissed: () { onDismissed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
direction: DismissiblePageDismissDirection.down, direction: DismissiblePageDismissDirection.none,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
isFullScreen: true, isFullScreen: true,
child: Scaffold( child: GestureDetector(
body: Stack( behavior: HitTestBehavior.translucent,
children: [ child: Scaffold(
Builder(builder: (context) { body: Stack(
if (widget.data.length == 1) { children: [
final heroTag = widget.heroTags?.first ?? uuid.v4(); Builder(builder: (context) {
return Hero( if (widget.data.length == 1) {
tag: 'attachment-${widget.data.first.rid}-$heroTag', final heroTag = widget.heroTags?.first ?? uuid.v4();
child: PhotoView( return Hero(
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'), tag: 'attachment-${widget.data.first.rid}-$heroTag',
backgroundDecoration: BoxDecoration(color: Colors.transparent), child: PhotoView(
imageProvider: UniversalImage.provider( key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
sn.getAttachmentUrl(widget.data.first.rid), backgroundDecoration: BoxDecoration(color: Colors.transparent),
), imageProvider: UniversalImage.provider(
), sn.getAttachmentUrl(widget.data.first.rid),
); ),
}
return PhotoViewGallery.builder(
pageController: _pageController,
scrollPhysics: const BouncingScrollPhysics(),
builder: (context, idx) {
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
return PhotoViewGalleryPageOptions(
imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
),
heroAttributes: PhotoViewHeroAttributes(
tag: 'attachment-${widget.data.first.rid}-$heroTag',
), ),
); );
}, }
itemCount: widget.data.length,
loadingBuilder: (context, event) => Center( return PhotoViewGallery.builder(
child: SizedBox( pageController: _pageController,
width: 20.0, scrollPhysics: const BouncingScrollPhysics(),
height: 20.0, builder: (context, idx) {
child: CircularProgressIndicator( final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1), return PhotoViewGalleryPageOptions(
imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
),
heroAttributes: PhotoViewHeroAttributes(
tag: 'attachment-${widget.data.first.rid}-$heroTag',
),
);
},
itemCount: widget.data.length,
loadingBuilder: (context, event) => Center(
child: SizedBox(
width: 20.0,
height: 20.0,
child: CircularProgressIndicator(
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
),
), ),
), ),
), backgroundDecoration: BoxDecoration(color: Colors.transparent),
backgroundDecoration: BoxDecoration(color: Colors.transparent), );
); }),
}), Align(
Align( alignment: Alignment.bottomCenter,
alignment: Alignment.bottomCenter, child: IgnorePointer(
child: IgnorePointer( child: Container(
child: Container( height: 300,
height: 300, decoration: BoxDecoration(
decoration: BoxDecoration( gradient: LinearGradient(
gradient: LinearGradient( begin: Alignment.bottomCenter,
begin: Alignment.bottomCenter, end: Alignment.topCenter,
end: Alignment.topCenter, colors: [
colors: [ Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surface, Colors.transparent,
Colors.transparent, ],
], ),
), ),
), ),
), ),
), ),
), Positioned(
Positioned( left: 16,
left: 16, right: 16,
right: 16, bottom: 16 + MediaQuery.of(context).padding.bottom,
bottom: 16 + MediaQuery.of(context).padding.bottom, child: Material(
child: Material( color: Colors.transparent,
color: Colors.transparent, child: Builder(builder: (context) {
child: Builder(builder: (context) { final ud = context.read<UserDirectoryProvider>();
final ud = context.read<UserDirectoryProvider>(); final item = widget.data.elementAt(
final item = widget.data.elementAt( widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0, );
); final account = ud.getAccountFromCache(item.accountId);
final account = ud.getAccountFromCache(item.accountId);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (item.accountId > 0) if (item.accountId > 0)
Row( Row(
children: [ children: [
IgnorePointer( IgnorePointer(
child: AccountImage( child: AccountImage(
content: account?.avatar, content: account?.avatar,
radius: 19, radius: 19,
),
),
const Gap(8),
Expanded(
child: IgnorePointer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'attachmentUploadBy'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
Text(
account?.nick ?? 'unknown'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
), ),
), ),
), const Gap(8),
if (widget.data.length > 1) Expanded(
IgnorePointer( child: IgnorePointer(
child: Text( child: Column(
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}', crossAxisAlignment: CrossAxisAlignment.start,
style: GoogleFonts.robotoMono(fontSize: 13), children: [
).padding(right: 8), Text(
), 'attachmentUploadBy'.tr(),
InkWell( style: Theme.of(context).textTheme.bodySmall,
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: _isDownloading
? null
: () => _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
child: Container(
padding: const EdgeInsets.all(6),
child: !_isDownloading
? !_isCompletedDownload
? const Icon(Symbols.save_alt)
: const Icon(Symbols.download_done)
: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _progressOfDownload,
strokeWidth: 3,
),
), ),
Text(
account?.nick ?? 'unknown'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
), ),
if (widget.data.length > 1)
IgnorePointer(
child: Text(
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}',
style: GoogleFonts.robotoMono(fontSize: 13),
).padding(right: 8),
),
InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: _isDownloading
? null
: () =>
_saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
child: Container(
padding: const EdgeInsets.all(6),
child: !_isDownloading
? !_isCompletedDownload
? const Icon(Symbols.save_alt)
: const Icon(Symbols.download_done)
: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _progressOfDownload,
strokeWidth: 3,
),
),
),
),
],
),
const Gap(4),
IgnorePointer(
child: Text(
item.alt,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
), ),
],
),
const Gap(4),
IgnorePointer(
child: Text(
item.alt,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
), ),
), ),
), const Gap(2),
const Gap(2), IgnorePointer(
IgnorePointer( child: Wrap(
child: Wrap( spacing: 6,
spacing: 6, children: [
children: [ if (item.metadata['exif'] == null)
if (item.metadata['exif'] == null) Text(
'#${item.rid}',
style: metaTextStyle,
),
if (item.metadata['exif']?['Model'] != null)
Text(
'attachmentShotOn'.tr(args: [
item.metadata['exif']?['Model'],
]),
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['ISO'] != null)
Text(
'ISO${item.metadata['exif']?['ISO']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Aperture'] != null)
Text(
'f/${item.metadata['exif']?['Aperture']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Megapixels'] != null &&
item.metadata['exif']?['Model'] != null)
Text(
'${item.metadata['exif']?['Megapixels']}MP',
style: metaTextStyle,
)
else
Text(
item.size.formatBytes(),
style: metaTextStyle,
),
if (item.metadata['width'] != null && item.metadata['height'] != null)
Text(
'${item.metadata['width']}x${item.metadata['height']}',
style: metaTextStyle,
),
if (item.metadata['ratio'] != null)
Text(
(item.metadata['ratio'] as num).toStringAsFixed(2),
style: metaTextStyle,
),
Text( Text(
'#${item.rid}', item.mimetype,
style: metaTextStyle, style: metaTextStyle,
), ),
if (item.metadata['exif']?['Model'] != null) ],
Text( ),
'attachmentShotOn'.tr(args: [
item.metadata['exif']?['Model'],
]),
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['ISO'] != null)
Text(
'ISO${item.metadata['exif']?['ISO']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Aperture'] != null)
Text(
'f/${item.metadata['exif']?['Aperture']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Megapixels'] != null && item.metadata['exif']?['Model'] != null)
Text(
'${item.metadata['exif']?['Megapixels']}MP',
style: metaTextStyle,
)
else
Text(
'${item.size} Bytes',
style: metaTextStyle,
),
if (item.metadata['width'] != null && item.metadata['height'] != null)
Text(
'${item.metadata['width']}x${item.metadata['height']}',
style: metaTextStyle,
),
if (item.metadata['ratio'] != null)
Text(
(item.metadata['ratio'] as num).toStringAsFixed(2),
style: metaTextStyle,
),
Text(
item.mimetype,
style: metaTextStyle,
),
],
), ),
), ],
], );
); }),
}), ),
), ),
), ],
], ),
), ),
onVerticalDragUpdate: (details) {
if (_showDetail) return;
if (details.delta.dy < 0) {
_showDetail = true;
showModalBottomSheet(
context: context,
builder: (context) => _AttachmentZoomDetailPopup(
data: widget.data.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
),
).then((_) {
_showDetail = false;
});
}
},
onTap: () {
Navigator.of(context).pop();
},
),
);
}
}
class _AttachmentZoomDetailPopup extends StatelessWidget {
final SnAttachment data;
const _AttachmentZoomDetailPopup({required this.data});
@override
Widget build(BuildContext context) {
final ud = context.read<UserDirectoryProvider>();
final account = ud.getAccountFromCache(data.accountId);
const tableGap = TableRow(
children: [
TableCell(child: SizedBox(height: 16)),
TableCell(child: SizedBox(height: 16)),
],
);
return SizedBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.info, size: 24),
const Gap(16),
Text('attachmentDetailInfo').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Expanded(
child: Table(
columnWidths: {
0: IntrinsicColumnWidth(),
1: FlexColumnWidth(),
},
children: [
TableRow(
children: [
TableCell(
child: Text('attachmentUploadBy').tr().padding(right: 16),
),
TableCell(
child: Row(
children: [
if (data.accountId > 0)
AccountImage(
content: account?.avatar,
radius: 8,
),
const Gap(8),
Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()),
const Gap(8),
Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75),
],
),
),
],
),
tableGap,
TableRow(
children: [
TableCell(child: Text('Mimetype').padding(right: 16)),
TableCell(child: Text(data.mimetype)),
],
),
TableRow(
children: [
TableCell(child: Text('Size').padding(right: 16)),
TableCell(
child: Row(
children: [
Text(data.size.formatBytes()),
const Gap(12),
Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75),
],
)),
],
),
TableRow(
children: [
TableCell(child: Text('Name').padding(right: 16)),
TableCell(child: Text(data.name)),
],
),
if (data.hash.isNotEmpty)
TableRow(
children: [
TableCell(child: Text('Hash').padding(right: 16)),
TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)),
],
),
tableGap,
...(data.metadata['exif']?.keys.map((k) => TableRow(
children: [
TableCell(child: Text(k).padding(right: 16)),
TableCell(child: Text(data.metadata['exif'][k].toString())),
],
)) ??
[]),
],
).padding(horizontal: 20, vertical: 8),
),
],
), ),
); );
} }

View File

@@ -1,14 +1,18 @@
import 'dart:math' as math;
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_context_menu/flutter_context_menu.dart'; import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:popover/popover.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/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/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_popover.dart';
import 'package:surface/widgets/attachment/attachment_list.dart'; import 'package:surface/widgets/attachment/attachment_list.dart';
import 'package:surface/widgets/context_menu.dart'; import 'package:surface/widgets/context_menu.dart';
import 'package:surface/widgets/link_preview.dart'; import 'package:surface/widgets/link_preview.dart';
@@ -95,8 +99,28 @@ class ChatMessage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (!isMerged && !isCompact) if (!isMerged && !isCompact)
AccountImage( GestureDetector(
content: user?.avatar, child: AccountImage(
content: user?.avatar,
),
onTap: () {
if (user == null) return;
showPopover(
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
transition: PopoverTransition.other,
bodyBuilder: (context) => SizedBox(
width: math.min(400, MediaQuery.of(context).size.width - 10),
child: AccountPopoverCard(
data: user,
),
),
direction: PopoverDirection.bottom,
arrowHeight: 5,
arrowWidth: 15,
arrowDxOffset: -190,
);
},
) )
else if (isMerged) else if (isMerged)
const Gap(40), const Gap(40),
@@ -124,10 +148,13 @@ class ChatMessage extends StatelessWidget {
dateFormatter.format(data.createdAt.toLocal()), dateFormatter.format(data.createdAt.toLocal()),
).fontSize(13), ).fontSize(13),
], ],
), ).height(21),
if (isCompact) const Gap(8), if (isCompact) const Gap(8),
if (data.preload?.quoteEvent != null) if (data.preload?.quoteEvent != null)
StyledWidget(Container( StyledWidget(Container(
constraints: BoxConstraints(
maxWidth: 480,
),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all( border: Border.all(
@@ -150,7 +177,12 @@ class ChatMessage extends StatelessWidget {
), ),
)).padding(bottom: 4, top: 4), )).padding(bottom: 4, top: 4),
switch (data.type) { switch (data.type) {
'messages.new' => _ChatMessageText(data: data), 'messages.new' => _ChatMessageText(
data: data,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
_ => _ChatMessageSystemNotify(data: data), _ => _ChatMessageSystemNotify(data: data),
}, },
], ],
@@ -181,19 +213,75 @@ class ChatMessage extends StatelessWidget {
class _ChatMessageText extends StatelessWidget { class _ChatMessageText extends StatelessWidget {
final SnChatMessage data; final SnChatMessage data;
final Function(SnChatMessage)? onReply;
final Function(SnChatMessage)? onEdit;
final Function(SnChatMessage)? onDelete;
const _ChatMessageText({required this.data}); const _ChatMessageText({required this.data, this.onReply, this.onEdit, this.onDelete});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.read<UserProvider>();
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id;
if (data.body['text'] != null && data.body['text'].isNotEmpty) { if (data.body['text'] != null && data.body['text'].isNotEmpty) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MarkdownTextContent( SelectionArea(
content: data.body['text'], contextMenuBuilder: (context, editableTextState) {
isSelectable: true, final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
isAutoWarp: true,
if (onReply != null) {
items.insert(
0,
ContextMenuButtonItem(
label: 'reply'.tr(),
onPressed: () {
ContextMenuController.removeAny();
onReply?.call(data);
},
),
);
}
if (isOwner && onEdit != null) {
items.insert(
1,
ContextMenuButtonItem(
label: 'edit'.tr(),
onPressed: () {
ContextMenuController.removeAny();
onEdit?.call(data);
},
),
);
}
if (isOwner && onDelete != null) {
items.insert(
2,
ContextMenuButtonItem(
label: 'delete'.tr(),
onPressed: () {
ContextMenuController.removeAny();
onDelete?.call(data);
},
),
);
}
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: editableTextState.contextMenuAnchors,
buttonItems: items,
);
},
child: Container(
constraints: const BoxConstraints(maxWidth: 480),
child: MarkdownTextContent(
content: data.body['text'],
isAutoWarp: true,
),
),
), ),
if (data.updatedAt != data.createdAt) if (data.updatedAt != data.createdAt)
Text( Text(

View File

@@ -48,6 +48,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
void setEdit(SnChatMessage? value) { void setEdit(SnChatMessage? value) {
_contentController.text = value?.body['text'] ?? ''; _contentController.text = value?.body['text'] ?? '';
_attachments.clear();
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
setState(() => _editingMessage = value); setState(() => _editingMessage = value);
} }
@@ -101,7 +103,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
}, },
); );
_attachments[i] = PostWriteMedia(item); setState(() {
_attachments[i] = PostWriteMedia(item);
});
} }
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
@@ -113,7 +117,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
// Send the message // Send the message
// NOTICE This future should not be awaited, so that the message can be sent in the background and the user can continue to type // NOTICE This future should not be awaited, so that the message can be sent in the background and the user can continue to type
widget.controller.sendMessage( widget.controller.sendMessage(
'messages.new', _editingMessage != null ? 'messages.edit' : 'messages.new',
_contentController.text, _contentController.text,
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(), attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
relatedId: _editingMessage?.id, relatedId: _editingMessage?.id,
@@ -197,6 +201,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
InkWell( InkWell(
child: Text('cancel'.tr()), child: Text('cancel'.tr()),
onTap: () { onTap: () {
_attachments.clear();
setState(() => _replyingMessage = null); setState(() => _replyingMessage = null);
}, },
), ),
@@ -236,6 +241,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
InkWell( InkWell(
child: Text('cancel'.tr()), child: Text('cancel'.tr()),
onTap: () { onTap: () {
_attachments.clear();
_contentController.clear(); _contentController.clear();
setState(() => _editingMessage = null); setState(() => _editingMessage = null);
}, },

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/config.dart';
class ContextMenuArea extends StatelessWidget { class ContextMenuArea extends StatelessWidget {
final ContextMenu contextMenu; final ContextMenu contextMenu;
@@ -22,11 +23,10 @@ class ContextMenuArea extends StatelessWidget {
return Listener( return Listener(
onPointerDown: (event) { onPointerDown: (event) {
mousePosition = event.position; mousePosition = event.position;
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE); final cfg = context.read<ConfigProvider>();
if (!isCollapseDrawer) { if (!cfg.drawerIsCollapsed) {
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
// Leave padding for side navigation // Leave padding for side navigation
mousePosition = isExpandDrawer mousePosition = cfg.drawerIsExpanded
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2) ? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2); : mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
} }

View File

@@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
@@ -7,22 +5,18 @@ import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:markdown/markdown.dart' as markdown; import 'package:markdown/markdown.dart' as markdown;
import 'package:provider/provider.dart'; import 'package:provider/provider.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/sn_sticker.dart'; import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:syntax_highlight/syntax_highlight.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:path/path.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'attachment/attachment_zoom.dart'; import 'attachment/attachment_zoom.dart';
class MarkdownTextContent extends StatelessWidget { class MarkdownTextContent extends StatelessWidget {
final String content; final String content;
final bool isSelectable;
final bool isAutoWarp; final bool isAutoWarp;
final bool isEnlargeSticker; final bool isEnlargeSticker;
final TextScaler? textScaler; final TextScaler? textScaler;
@@ -31,14 +25,14 @@ class MarkdownTextContent extends StatelessWidget {
const MarkdownTextContent({ const MarkdownTextContent({
super.key, super.key,
required this.content, required this.content,
this.isSelectable = false,
this.isAutoWarp = false, this.isAutoWarp = false,
this.isEnlargeSticker = false, this.isEnlargeSticker = false,
this.textScaler, this.textScaler,
this.attachments, this.attachments,
}); });
Widget _buildContent(BuildContext context) { @override
Widget build(BuildContext context) {
return Markdown( return Markdown(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@@ -47,33 +41,33 @@ class MarkdownTextContent extends StatelessWidget {
styleSheet: MarkdownStyleSheet.fromTheme( styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context), Theme.of(context),
).copyWith( ).copyWith(
textScaler: textScaler, textScaler: textScaler,
blockquote: TextStyle( blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
blockquoteDecoration: BoxDecoration( blockquoteDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(Radius.circular(4)), borderRadius: const BorderRadius.all(Radius.circular(4)),
), ),
horizontalRuleDecoration: BoxDecoration( horizontalRuleDecoration: BoxDecoration(
border: Border( border: Border(
top: BorderSide( top: BorderSide(
width: 1.0, width: 1.0,
color: Theme.of(context).dividerColor,
),
),
),
codeblockDecoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 0.3,
), ),
borderRadius: const BorderRadius.all(Radius.circular(4)), ),
color: Theme.of(context).colorScheme.surface.withOpacity(0.5), ),
)), codeblockDecoration: BoxDecoration(
builders: { border: Border.all(
'code': _MarkdownTextCodeElement(), color: Theme.of(context).dividerColor,
}, width: 0.3,
),
borderRadius: const BorderRadius.all(Radius.circular(4)),
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
),
code: GoogleFonts.robotoMono(height: 1),
),
builders: {},
softLineBreak: true, softLineBreak: true,
extensionSet: markdown.ExtensionSet( extensionSet: markdown.ExtensionSet(
<markdown.BlockSyntax>[ <markdown.BlockSyntax>[
@@ -148,7 +142,7 @@ class MarkdownTextContent extends StatelessWidget {
); );
case 'attachments': case 'attachments':
final attachment = attachments?.firstWhere( final attachment = attachments?.firstWhere(
(ele) => ele?.rid == segments[1], (ele) => ele?.rid == segments[1],
orElse: () => null, orElse: () => null,
); );
if (attachment != null) { if (attachment != null) {
@@ -206,14 +200,6 @@ class MarkdownTextContent extends StatelessWidget {
}, },
); );
} }
@override
Widget build(BuildContext context) {
if (isSelectable) {
return SelectionArea(child: _buildContent(context));
}
return _buildContent(context);
}
} }
class _UserNameCardInlineSyntax extends markdown.InlineSyntax { class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
@@ -253,46 +239,3 @@ class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
return true; return true;
} }
} }
class _MarkdownTextCodeElement extends MarkdownElementBuilder {
@override
Widget? visitElementAfter(
markdown.Element element,
TextStyle? preferredStyle,
) {
var language = '';
if (element.attributes['class'] != null) {
String lg = element.attributes['class'] as String;
language = lg.substring(9).trim();
}
return SizedBox(
child: FutureBuilder(
future: (() async {
final docPath = '../../../';
final highlightingPath = join(docPath, 'assets/highlighting', language);
await Highlighter.initialize([highlightingPath]);
return Highlighter(
language: highlightingPath,
theme: PlatformDispatcher.instance.platformBrightness == Brightness.light
? await HighlighterTheme.loadLightTheme()
: await HighlighterTheme.loadDarkTheme(),
);
})(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final highlighter = snapshot.data!;
return Text.rich(
highlighter.highlight(element.textContent.trim()),
style: GoogleFonts.robotoMono(),
);
}
return Text(
element.textContent.trim(),
style: GoogleFonts.robotoMono(),
);
},
),
).padding(all: 8);
}
}

View File

@@ -1,9 +1,13 @@
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.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: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/config.dart';
import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/navigation.dart';
import 'package:surface/widgets/version_label.dart'; import 'package:surface/widgets/version_label.dart';
@@ -28,8 +32,9 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final nav = context.watch<NavigationProvider>(); final nav = context.watch<NavigationProvider>();
final cfg = context.watch<ConfigProvider>();
final backgroundColor = ResponsiveBreakpoints.of(context).largerThan(TABLET) ? Colors.transparent : null; final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
return ListenableBuilder( return ListenableBuilder(
listenable: nav, listenable: nav,
@@ -44,6 +49,18 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
selectedIndex: nav.currentIndex, selectedIndex: nav.currentIndex,
children: [ children: [
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS) && !cfg.drawerIsExpanded)
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: WindowTitleBarBox(),
),
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -18,9 +18,7 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context));
.read<NavigationProvider>()
.autoDetectIndex(GoRouter.maybeOf(context));
}); });
} }
@@ -31,11 +29,11 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
return ListenableBuilder( return ListenableBuilder(
listenable: nav, listenable: nav,
builder: (context, _) { builder: (context, _) {
final destinations = final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
nav.destinations.where((ele) => ele.isPinned).toList();
return NavigationRail( return NavigationRail(
selectedIndex: nav.currentIndex, selectedIndex:
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
destinations: [ destinations: [
...destinations.where((ele) => ele.isPinned).map((ele) { ...destinations.where((ele) => ele.isPinned).map((ele) {
return NavigationRailDestination( return NavigationRailDestination(

View File

@@ -6,8 +6,10 @@ import 'package:flutter/foundation.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:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.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/config.dart';
import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/navigation.dart';
import 'package:surface/widgets/connection_indicator.dart'; import 'package:surface/widgets/connection_indicator.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
@@ -57,10 +59,11 @@ class AppRootScaffold extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.watch<ConfigProvider>();
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE); final isCollapseDrawer = cfg.drawerIsCollapsed;
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET); final isExpandedDrawer = cfg.drawerIsExpanded;
final routeName = GoRouter.of(context).routerDelegate.currentConfiguration.last.route.name; final routeName = GoRouter.of(context).routerDelegate.currentConfiguration.last.route.name;
final isShowBottomNavigation = NavigationProvider.kShowBottomNavScreen.contains(routeName) final isShowBottomNavigation = NavigationProvider.kShowBottomNavScreen.contains(routeName)
@@ -81,7 +84,7 @@ class AppRootScaffold extends StatelessWidget {
), ),
), ),
), ),
child: isExpandDrawer ? AppNavigationDrawer(elevation: 0) : AppRailNavigation(), child: isExpandedDrawer ? AppNavigationDrawer(elevation: 0) : AppRailNavigation(),
), ),
Expanded(child: body), Expanded(child: body),
], ],
@@ -147,7 +150,7 @@ class AppRootScaffold extends StatelessWidget {
Expanded(child: innerWidget), Expanded(child: innerWidget),
], ],
), ),
drawer: !isExpandDrawer ? AppNavigationDrawer() : null, drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null, drawerEdgeDragWidth: isPopable ? 0 : null,
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null, bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
), ),

View File

@@ -20,6 +20,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.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';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
@@ -112,7 +113,7 @@ class PostItem extends StatelessWidget {
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
); );
} else { } else {
await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}', file: imageFile); await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}.png', file: imageFile);
} }
await imageFile.delete(); await imageFile.delete();
@@ -198,6 +199,10 @@ class PostItem extends StatelessWidget {
).center(); ).center();
} }
final displayableAttachments = data.preload?.attachments
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
.toList();
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@@ -247,9 +252,9 @@ class PostItem extends StatelessWidget {
], ],
), ),
), ),
if ((data.preload?.attachments?.isNotEmpty ?? false) && data.type != 'article') if (displayableAttachments?.isNotEmpty ?? false)
AttachmentList( AttachmentList(
data: data.preload!.attachments!, data: displayableAttachments!,
bordered: true, bordered: true,
gridded: true, gridded: true,
maxHeight: showFullPost ? null : 480, maxHeight: showFullPost ? null : 480,
@@ -874,13 +879,18 @@ class _PostContentBody extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (data.body['content'] == null) return const SizedBox.shrink(); if (data.body['content'] == null) return const SizedBox.shrink();
return MarkdownTextContent( final content = MarkdownTextContent(
isSelectable: isSelectable, isAutoWarp: data.type == 'story',
isEnlargeSticker: true, isEnlargeSticker: true,
textScaler: isEnlarge ? TextScaler.linear(1.1) : null, textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: data.body['content'], content: data.body['content'],
attachments: data.preload?.attachments, attachments: data.preload?.attachments,
); );
if (isSelectable) {
return SelectionArea(child: content);
}
return content;
} }
} }

View File

@@ -25,7 +25,7 @@ class PostMiniEditor extends StatefulWidget {
} }
class _PostMiniEditorState extends State<PostMiniEditor> { class _PostMiniEditorState extends State<PostMiniEditor> {
final PostWriteController _writeController = PostWriteController(); final PostWriteController _writeController = PostWriteController(doLoadFromTemporary: false);
bool _isFetching = false; bool _isFetching = false;

View File

@@ -55,17 +55,20 @@ class UniversalImage extends StatelessWidget {
? null ? null
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { : (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child; if (loadingProgress == null) return child;
return Center( return Container(
child: TweenAnimationBuilder( constraints: BoxConstraints(maxHeight: 80),
tween: Tween( child: Center(
begin: 0, child: TweenAnimationBuilder(
end: loadingProgress.expectedTotalBytes != null tween: Tween(
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! begin: 0,
: 0, end: loadingProgress.expectedTotalBytes != null
), ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
duration: const Duration(milliseconds: 300), : 0,
builder: (context, value, _) => CircularProgressIndicator( ),
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null, duration: const Duration(milliseconds: 300),
builder: (context, value, _) => CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
),
), ),
), ),
); );

View File

@@ -618,10 +618,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fl_chart name: fl_chart
sha256: c724234b05e378383e958f3e82ca84a3e1e3c06a0898462044dd8a24b1ee9864 sha256: "10ddaf334fe84d59333a12d153043e366f243e0bdfff2df0313e1e249f5bf926"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.70.0" version: "0.70.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -700,10 +700,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_markdown name: flutter_markdown
sha256: "255b00afa1a7bad19727da6a7780cf3db6c3c12e68d302d85e0ff1fdf173db9e" sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.4+3" version: "0.7.5"
flutter_native_splash: flutter_native_splash:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -822,10 +822,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539" sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.6.2" version: "14.6.3"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1863,14 +1863,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.0+3" version: "3.3.0+3"
syntax_highlight:
dependency: "direct main"
description:
name: syntax_highlight
sha256: ee33b6aa82cc722bb9b40152a792181dee222353b486c0255fde666a3e3a4997
url: "https://pub.dev"
source: hosted
version: "0.4.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -2177,4 +2169,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.6.0 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.24.0"

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.2.1+46 version: 2.2.2+53
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@@ -54,7 +54,6 @@ dependencies:
flutter_markdown: ^0.7.4+1 flutter_markdown: ^0.7.4+1
url_launcher: ^6.3.1 url_launcher: ^6.3.1
flutter_animate: ^4.5.0 flutter_animate: ^4.5.0
syntax_highlight: ^0.4.0
google_fonts: ^6.2.1 google_fonts: ^6.2.1
path: ^1.9.0 path: ^1.9.0
relative_time: ^5.0.0 relative_time: ^5.0.0

View File

@@ -1,9 +1,9 @@
id = "solian-next" id = "solian"
[[locations]] [[locations]]
id = "solian-next" id = "solian"
host = ["sn-next.solsynth.dev"] hosts = ["sn.solsynth.dev"]
path = ["/"] paths = ["/"]
[[locations.destinations]] [[locations.destinations]]
id = "solian-next-web" id = "solian-web"
uri = "files:///workdir/solian-next?fallback=index.html&index=index.html" uri = "files:///workdir/solian?fallback=index.html&index=index.html"