diff --git a/assets/linux/DEBIAN/postinst b/assets/linux/DEBIAN/postinst index 7d0aff839..e017701ae 100644 --- a/assets/linux/DEBIAN/postinst +++ b/assets/linux/DEBIAN/postinst @@ -3,18 +3,18 @@ ln -sf /opt/PiliPlus/piliplus /usr/bin/piliplus chmod +x /usr/bin/piliplus -if [ $1 == "config" ] && [ -x /usr/binupdate-mime-database ]; then +if [ $1 == "configure" ] && [ -x /usr/bin/update-mime-database ]; then echo "updating mime database..." update-mime-database /usr/share/mime || true fi -if [ $1 == "config" ] && [ -x /usr/bin/gtk-update-icon-cache ]; then +if [ $1 == "configure" ] && [ -x /usr/bin/gtk-update-icon-cache ]; then echo "updating icon cache..." gtk-update-icon-cache -q -f -t /usr/share/icons/hicolor || true fi -if [ $1 == "config" ] && [ -x /usr/bin/update-desktop-database ]; then - echo "updating desktop database..." +if [ $1 == "configure" ] && [ -x /usr/bin/update-desktop-database ]; then + echo "configure desktop database..." update-desktop-database -q /usr/share/applications || true fi diff --git a/assets/linux/piliplus.desktop b/assets/linux/piliplus.desktop index 92fd6c956..3623ad66b 100644 --- a/assets/linux/piliplus.desktop +++ b/assets/linux/piliplus.desktop @@ -6,4 +6,5 @@ Comment[zh_CN]=使用 Flutter 开发的 BiliBili 第三方客户端 Exec=piliplus Icon=piliplus Terminal=false +StartupWMClass=com.example.piliplus Categories=Video;AudioVideo;Player; diff --git a/lib/common/widgets/stateful_builder.dart b/lib/common/widgets/stateful_builder.dart new file mode 100644 index 000000000..9c9d3cfa2 --- /dev/null +++ b/lib/common/widgets/stateful_builder.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class StatefulBuilder extends StatefulWidget { + const StatefulBuilder({ + super.key, + this.onInit, + this.onDispose, + required this.builder, + }); + + final VoidCallback? onInit; + + final VoidCallback? onDispose; + + final StatefulWidgetBuilder builder; + + @override + State createState() => _StatefulBuilderState(); +} + +class _StatefulBuilderState extends State { + @override + void initState() { + super.initState(); + widget.onInit?.call(); + } + + @override + void dispose() { + widget.onDispose?.call(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.builder(context, setState); +} diff --git a/lib/main.dart b/lib/main.dart index 6f279c31f..85a0acdc8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -292,15 +292,43 @@ class MyApp extends StatelessWidget { getPages: Routes.getPages, defaultTransition: Pref.pageTransition, builder: FlutterSmartDialog.init( - toastBuilder: (String msg) => CustomToast(msg: msg), + toastBuilder: (msg) => CustomToast(msg: msg), loadingBuilder: (msg) => LoadingWidget(msg: msg), builder: (context, child) { - child = MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear(Pref.defaultTextScale), - ), - child: child!, - ); + final uiScale = Pref.uiScale; + final mediaQuery = MediaQuery.of(context); + final textScaler = TextScaler.linear(Pref.defaultTextScale); + if (uiScale != 1.0) { + // Apply full UI scaling for desktop + final actualSize = mediaQuery.size; + final scaledSize = actualSize / uiScale; + child = MediaQuery( + data: mediaQuery.copyWith( + // Tell child the logical size it should layout to + size: scaledSize, + padding: mediaQuery.padding / uiScale, + viewPadding: mediaQuery.viewPadding / uiScale, + viewInsets: mediaQuery.viewInsets / uiScale, + textScaler: textScaler, + ), + // Use OverflowBox to let child layout to scaledSize, + // then FittedBox scales it to fit actualSize + child: FittedBox( + fit: BoxFit.fill, + alignment: Alignment.topLeft, + child: SizedBox( + width: scaledSize.width, + height: scaledSize.height, + child: child, + ), + ), + ); + } else { + child = MediaQuery( + data: mediaQuery.copyWith(textScaler: textScaler), + child: child!, + ); + } if (PlatformUtils.isDesktop) { return Focus( canRequestFocus: false, diff --git a/lib/pages/setting/models/model.dart b/lib/pages/setting/models/model.dart index 06ec1d5f3..fd6e318cd 100644 --- a/lib/pages/setting/models/model.dart +++ b/lib/pages/setting/models/model.dart @@ -34,7 +34,7 @@ class NormalModel extends SettingsModel { final ValueGetter? getTitle; final ValueGetter? getSubtitle; final Widget Function()? getTrailing; - final void Function(BuildContext context, void Function() setState)? onTap; + final void Function(BuildContext context, VoidCallback setState)? onTap; const NormalModel({ super.subtitle, diff --git a/lib/pages/setting/models/style_settings.dart b/lib/pages/setting/models/style_settings.dart index 0668ecd0d..1c4b27e7b 100644 --- a/lib/pages/setting/models/style_settings.dart +++ b/lib/pages/setting/models/style_settings.dart @@ -5,6 +5,7 @@ import 'package:PiliPlus/common/widgets/color_palette.dart'; import 'package:PiliPlus/common/widgets/custom_toast.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/stateful_builder.dart'; import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/models/common/dynamic/dynamic_badge_mode.dart'; import 'package:PiliPlus/models/common/dynamic/up_panel_position.dart'; @@ -33,7 +34,7 @@ import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:auto_orientation/auto_orientation.dart'; import 'package:flex_seed_scheme/flex_seed_scheme.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide StatefulBuilder; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -56,6 +57,12 @@ List get styleSettings => [ needReboot: true, ), ], + NormalModel( + title: '界面缩放', + getSubtitle: () => '当前缩放比例:${Pref.uiScale.toStringAsFixed(2)}', + leading: const Icon(Icons.zoom_in_outlined), + onTap: _showUiScaleDialog, + ), SwitchModel( title: '横屏适配', subtitle: '启用横屏布局与逻辑,平板、折叠屏等可开启;建议全屏方向设为【不改变当前方向】', @@ -774,3 +781,107 @@ void _showQualityDialog({ } }); } + +const _minUiScale = 0.5; +const _maxUiScale = 2.0; + +void _showUiScaleDialog( + BuildContext context, + VoidCallback setState, +) { + double uiScale = Pref.uiScale; + final textController = TextEditingController( + text: uiScale.toStringAsFixed(2), + ); + + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('界面缩放'), + contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 12), + content: StatefulBuilder( + onDispose: textController.dispose, + builder: (context, setDialogState) { + return Column( + spacing: 20, + mainAxisSize: MainAxisSize.min, + children: [ + Slider( + padding: .zero, + value: uiScale, + min: _minUiScale, + max: _maxUiScale, + divisions: ((_maxUiScale - _minUiScale) * 20).toInt(), + label: textController.text, + onChanged: (value) => setDialogState(() { + uiScale = value.toPrecision(2); + textController.text = uiScale.toStringAsFixed(2); + }), + ), + TextFormField( + controller: textController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + inputFormatters: [ + LengthLimitingTextInputFormatter(4), + FilteringTextInputFormatter.allow(RegExp(r'[\d.]+')), + ], + decoration: const InputDecoration( + labelText: '缩放比例', + hintText: '0.50 - 2.00', + border: OutlineInputBorder(), + ), + onChanged: (value) { + final parsed = double.tryParse(value); + if (parsed != null && + parsed >= _minUiScale && + parsed <= _maxUiScale) { + setDialogState(() { + uiScale = parsed; + }); + } + }, + ), + ], + ); + }, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + GStorage.setting.delete(SettingBoxKey.uiScale).whenComplete(() { + setState(); + Get.appUpdate(); + }); + }, + child: const Text('重置'), + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + '取消', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + GStorage.setting.put(SettingBoxKey.uiScale, uiScale).whenComplete( + () { + setState(); + Get.appUpdate(); + }, + ); + }, + child: const Text('确定'), + ), + ], + ); + }, + ); +} diff --git a/lib/pages/setting/widgets/normal_item.dart b/lib/pages/setting/widgets/normal_item.dart index 0076b465e..c0e8c0f4d 100644 --- a/lib/pages/setting/widgets/normal_item.dart +++ b/lib/pages/setting/widgets/normal_item.dart @@ -8,7 +8,7 @@ class NormalItem extends StatefulWidget { final ValueGetter? getSubtitle; final Widget? leading; final ValueGetter? getTrailing; - final void Function(BuildContext context, void Function() setState)? onTap; + final void Function(BuildContext context, VoidCallback setState)? onTap; final EdgeInsetsGeometry? contentPadding; final TextStyle? titleStyle; diff --git a/lib/utils/storage_key.dart b/lib/utils/storage_key.dart index 9568301c1..07580d003 100644 --- a/lib/utils/storage_key.dart +++ b/lib/utils/storage_key.dart @@ -152,7 +152,8 @@ abstract final class SettingBoxKey { isWindowMaximized = 'isWindowMaximized', showWindowTitleBar = 'showWindowTitleBar', desktopVolume = 'desktopVolume', - showTrayIcon = 'showTrayIcon'; + showTrayIcon = 'showTrayIcon', + uiScale = 'uiScale'; static const String subtitlePreferenceV2 = 'subtitlePreferenceV2', enableDragSubtitle = 'enableDragSubtitle', diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index ed83e1c4a..21c0882a1 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -628,6 +628,9 @@ abstract final class Pref { static double get defaultTextScale => _setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0); + static double get uiScale => + _setting.get(SettingBoxKey.uiScale, defaultValue: 1.0); + static bool get dynamicsWaterfallFlow => _setting.get(SettingBoxKey.dynamicsWaterfallFlow, defaultValue: true);