Compare commits

...

13 Commits

24 changed files with 214 additions and 143 deletions

View File

@ -181,6 +181,8 @@
"settingsAppearance": "Appearance",
"settingsAppBarTransparent": "Transparent 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",
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
"settingsBackgroundImageClear": "Clear Existing Background Image",

View File

@ -185,6 +185,8 @@
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
"settingsAppBarTransparent": "透明顶栏",
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
"settingsDrawerPreferCollapse": "侧边栏偏好折叠",
"settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。",
"settingsColorScheme": "主题色",
"settingsColorSchemeDescription": "设置应用主题色。",
"settingsColorSeed": "预设色彩主题",

View File

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

View File

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

View File

@ -83,7 +83,6 @@ class ChatMessageController extends ChangeNotifier {
if (member.id == profile?.id) break;
if (!typingMembers.any((x) => x.id == member.id)) {
typingMembers.add(member);
print('Typing member: ${typingMembers.map((ele) => member.id)}');
notifyListeners();
}
typingInactiveTimer[member.id]?.cancel();

View File

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

View File

@ -258,6 +258,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
Future<void> _initialize() async {
try {
final cfg = context.read<ConfigProvider>();
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
final home = context.read<HomeWidgetProvider>();
await home.initialize();
if (!mounted) return;
@ -298,6 +302,17 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
@override
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:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/providers/widget.dart';
@ -12,6 +13,7 @@ const kNetworkServerStoreKey = 'app_server_url';
const kAppbarTransparentStoreKey = 'app_bar_transparent';
const kAppBackgroundStoreKey = 'app_has_background';
const kAppColorSchemeStoreKey = 'app_color_scheme';
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none,
@ -33,6 +35,24 @@ class ConfigProvider extends ChangeNotifier {
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 {
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 {
if (!_ua.isAuthorized) return;
if (isConnected) {
if (isConnected || conn != null) {
disconnect();
}
@ -97,7 +97,7 @@ class WebSocketProvider extends ChangeNotifier {
onError: (err) {
isConnected = false;
notifyListeners();
Future.delayed(const Duration(seconds: 11), () => connect());
Future.delayed(const Duration(seconds: 1), () => connect());
},
);
}

View File

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

View File

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

View File

@ -54,7 +54,9 @@ class PostEditorScreen extends StatefulWidget {
}
class _PostEditorScreenState extends State<PostEditorScreen> {
final PostWriteController _writeController = PostWriteController();
late final PostWriteController _writeController = PostWriteController(
doLoadFromTemporary: widget.postEditId == null,
);
bool _isFetching = false;

View File

@ -240,6 +240,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
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(

View File

@ -48,7 +48,7 @@ Future<ThemeData> createAppTheme(
appBarTheme: AppBarTheme(
centerTitle: true,
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,
),
scaffoldBackgroundColor: Colors.transparent,

View File

@ -329,7 +329,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
)
else
Text(
'${item.size} Bytes',
item.size.formatBytes(),
style: metaTextStyle,
),
if (item.metadata['width'] != null && item.metadata['height'] != null)

View File

@ -124,7 +124,7 @@ class ChatMessage extends StatelessWidget {
dateFormatter.format(data.createdAt.toLocal()),
).fontSize(13),
],
),
).height(21),
if (isCompact) const Gap(8),
if (data.preload?.quoteEvent != null)
StyledWidget(Container(
@ -150,7 +150,12 @@ class ChatMessage extends StatelessWidget {
),
)).padding(bottom: 4, top: 4),
switch (data.type) {
'messages.new' => _ChatMessageText(data: data),
'messages.new' => _ChatMessageText(
data: data,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
_ => _ChatMessageSystemNotify(data: data),
},
],
@ -181,20 +186,73 @@ class ChatMessage extends StatelessWidget {
class _ChatMessageText extends StatelessWidget {
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
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) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownTextContent(
SelectionArea(
contextMenuBuilder: (context, editableTextState) {
final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
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: MarkdownTextContent(
content: data.body['text'],
isSelectable: true,
isAutoWarp: true,
),
),
if (data.updatedAt != data.createdAt)
Text(
'messageEditedHint'.tr(),

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.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 {
final ContextMenu contextMenu;
@ -22,11 +23,10 @@ class ContextMenuArea extends StatelessWidget {
return Listener(
onPointerDown: (event) {
mousePosition = event.position;
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
if (!isCollapseDrawer) {
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
final cfg = context.read<ConfigProvider>();
if (!cfg.drawerIsCollapsed) {
// Leave padding for side navigation
mousePosition = isExpandDrawer
mousePosition = cfg.drawerIsExpanded
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 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:flutter/material.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:markdown/markdown.dart' as markdown;
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_item.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:path/path.dart';
import 'package:uuid/uuid.dart';
import 'attachment/attachment_zoom.dart';
class MarkdownTextContent extends StatelessWidget {
final String content;
final bool isSelectable;
final bool isAutoWarp;
final bool isEnlargeSticker;
final TextScaler? textScaler;
@ -31,14 +25,14 @@ class MarkdownTextContent extends StatelessWidget {
const MarkdownTextContent({
super.key,
required this.content,
this.isSelectable = false,
this.isAutoWarp = false,
this.isEnlargeSticker = false,
this.textScaler,
this.attachments,
});
Widget _buildContent(BuildContext context) {
@override
Widget build(BuildContext context) {
return Markdown(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@ -70,10 +64,10 @@ class MarkdownTextContent extends StatelessWidget {
),
borderRadius: const BorderRadius.all(Radius.circular(4)),
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
)),
builders: {
'code': _MarkdownTextCodeElement(),
},
),
code: GoogleFonts.robotoMono(height: 1),
),
builders: {},
softLineBreak: true,
extensionSet: markdown.ExtensionSet(
<markdown.BlockSyntax>[
@ -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 {
@ -253,46 +239,3 @@ class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
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

@ -6,8 +6,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/navigation.dart';
import 'package:surface/widgets/connection_indicator.dart';
import 'package:surface/widgets/dialog.dart';
@ -57,10 +59,11 @@ class AppRootScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cfg = context.watch<ConfigProvider>();
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
final isCollapseDrawer = cfg.drawerIsCollapsed;
final isExpandedDrawer = cfg.drawerIsExpanded;
final routeName = GoRouter.of(context).routerDelegate.currentConfiguration.last.route.name;
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),
],
@ -147,7 +150,7 @@ class AppRootScaffold extends StatelessWidget {
Expanded(child: innerWidget),
],
),
drawer: !isExpandDrawer ? AppNavigationDrawer() : null,
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : 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/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart';
import 'package:surface/types/reaction.dart';
import 'package:surface/widgets/account/account_image.dart';
@ -112,7 +113,7 @@ class PostItem extends StatelessWidget {
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
} 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();
@ -198,6 +199,10 @@ class PostItem extends StatelessWidget {
).center();
}
final displayableAttachments = data.preload?.attachments
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -247,9 +252,9 @@ class PostItem extends StatelessWidget {
],
),
),
if ((data.preload?.attachments?.isNotEmpty ?? false) && data.type != 'article')
if (displayableAttachments?.isNotEmpty ?? false)
AttachmentList(
data: data.preload!.attachments!,
data: displayableAttachments!,
bordered: true,
gridded: true,
maxHeight: showFullPost ? null : 480,
@ -874,13 +879,17 @@ class _PostContentBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (data.body['content'] == null) return const SizedBox.shrink();
return MarkdownTextContent(
isSelectable: isSelectable,
final content = MarkdownTextContent(
isEnlargeSticker: true,
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: data.body['content'],
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> {
final PostWriteController _writeController = PostWriteController();
final PostWriteController _writeController = PostWriteController(doLoadFromTemporary: false);
bool _isFetching = false;

View File

@ -55,7 +55,9 @@ class UniversalImage extends StatelessWidget {
? null
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
return Container(
constraints: BoxConstraints(maxHeight: 80),
child: Center(
child: TweenAnimationBuilder(
tween: Tween(
begin: 0,
@ -68,6 +70,7 @@ class UniversalImage extends StatelessWidget {
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
),
),
),
);
},
errorBuilder: noErrorWidget

View File

@ -700,10 +700,10 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
sha256: "255b00afa1a7bad19727da6a7780cf3db6c3c12e68d302d85e0ff1fdf173db9e"
sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487
url: "https://pub.dev"
source: hosted
version: "0.7.4+3"
version: "0.7.5"
flutter_native_splash:
dependency: "direct dev"
description:
@ -1863,14 +1863,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
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
# 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.
version: 2.2.1+47
version: 2.2.2+50
environment:
sdk: ^3.5.4
@ -54,7 +54,6 @@ dependencies:
flutter_markdown: ^0.7.4+1
url_launcher: ^6.3.1
flutter_animate: ^4.5.0
syntax_highlight: ^0.4.0
google_fonts: ^6.2.1
path: ^1.9.0
relative_time: ^5.0.0