Files
PiliPlus/lib/pages/setting/models/model.dart
s 28b69a06fa feat: Add desktop scaling and fix linux postinst (#1800)
* fix: resolve Linux window close handler to prevent app hang

- Add delete-event callback that properly quits the application when window is closed

* feat: Add desktop scaling and fix linux postinst

- Implement desktop interface scaling in main.dart using FittedBox.
- Add desktop scaling setting UI.
- Add desktopScale to storage preference.
- Fix typos and logic in Linux postinst script.
- Update piliplus.desktop with StartupWMClass.

* update

Signed-off-by: dom <githubaccount56556@proton.me>

---------

Signed-off-by: Shao Guohao <shao.gh.98@gmail.com>
Co-authored-by: dom <githubaccount56556@proton.me>
2026-01-10 10:03:51 +08:00

261 lines
7.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/pages/setting/widgets/normal_item.dart';
import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart';
import 'package:PiliPlus/pages/setting/widgets/switch_item.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show FilteringTextInputFormatter;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
@immutable
sealed class SettingsModel {
final String? subtitle;
final Widget? leading;
final EdgeInsetsGeometry? contentPadding;
final TextStyle? titleStyle;
String? get title;
Widget get widget;
String get effectiveTitle;
String? get effectiveSubtitle;
const SettingsModel({
this.subtitle,
this.leading,
this.contentPadding,
this.titleStyle,
});
}
class NormalModel extends SettingsModel {
@override
final String? title;
final ValueGetter<String>? getTitle;
final ValueGetter<String>? getSubtitle;
final Widget Function()? getTrailing;
final void Function(BuildContext context, VoidCallback setState)? onTap;
const NormalModel({
super.subtitle,
super.leading,
super.contentPadding,
super.titleStyle,
this.title,
this.getTitle,
this.getSubtitle,
this.getTrailing,
this.onTap,
}) : assert(title != null || getTitle != null);
@override
String get effectiveTitle => title ?? getTitle!();
@override
String? get effectiveSubtitle => subtitle ?? getSubtitle?.call();
@override
Widget get widget => NormalItem(
title: title,
getTitle: getTitle,
subtitle: subtitle,
getSubtitle: getSubtitle,
leading: leading,
getTrailing: getTrailing,
onTap: onTap,
contentPadding: contentPadding,
titleStyle: titleStyle,
);
}
class SwitchModel extends SettingsModel {
@override
final String title;
final String setKey;
final bool defaultVal;
final ValueChanged<bool>? onChanged;
final bool needReboot;
final void Function(BuildContext context)? onTap;
const SwitchModel({
super.subtitle,
super.leading,
super.contentPadding,
super.titleStyle,
required this.title,
required this.setKey,
this.defaultVal = false,
this.onChanged,
this.needReboot = false,
this.onTap,
});
@override
String get effectiveTitle => title;
@override
String? get effectiveSubtitle => subtitle;
@override
Widget get widget => SetSwitchItem(
title: title,
subtitle: subtitle,
setKey: setKey,
defaultVal: defaultVal,
onChanged: onChanged,
needReboot: needReboot,
leading: leading,
onTap: onTap,
contentPadding: contentPadding,
titleStyle: titleStyle,
);
}
SettingsModel getBanWordModel({
required String title,
required String key,
required ValueChanged<RegExp> onChanged,
}) {
String banWord = GStorage.setting.get(key, defaultValue: '');
return NormalModel(
leading: const Icon(Icons.filter_alt_outlined),
title: title,
getSubtitle: () => banWord.isEmpty ? "点击添加" : banWord,
onTap: (context, setState) {
String editValue = banWord;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
constraints: StyleString.dialogFixedConstraints,
title: Text(title),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('使用|隔开,如:尝试|测试'),
TextFormField(
autofocus: true,
initialValue: editValue,
textInputAction: TextInputAction.newline,
minLines: 1,
maxLines: 4,
onChanged: (value) => editValue = value,
),
],
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
child: const Text('保存'),
onPressed: () {
Get.back();
banWord = editValue;
setState();
onChanged(RegExp(banWord, caseSensitive: false));
SmartDialog.showToast('已保存');
GStorage.setting.put(key, banWord);
},
),
],
);
},
);
},
);
}
SettingsModel getVideoFilterSelectModel({
required String title,
String? subtitle,
String? suffix,
required String key,
required List<int> values,
int defaultValue = 0,
bool isFilter = true,
ValueChanged<int>? onChanged,
}) {
assert(!isFilter || onChanged != null);
int value = GStorage.setting.get(key, defaultValue: defaultValue);
return NormalModel(
title: '$title${isFilter ? '过滤' : ''}',
leading: const Icon(Icons.timelapse_outlined),
subtitle: subtitle,
getSubtitle: subtitle == null
? () => isFilter
? '过滤掉$title小于$value${suffix ?? ""}」的视频'
: '当前$title:「$value${suffix ?? ""}'
: null,
onTap: (context, setState) async {
var result = await showDialog<int>(
context: context,
builder: (context) {
return SelectDialog<int>(
title: '选择$title${isFilter ? '0即不过滤' : ''}',
value: value,
values:
(values
..addIf(!values.contains(value), value)
..sort())
.map(
(e) => (e, suffix == null ? e.toString() : '$e $suffix'),
)
.toList()
..add((-1, '自定义')),
);
},
);
if (result != null) {
if (result == -1 && context.mounted) {
await showDialog(
context: context,
builder: (context) {
String valueStr = '';
return AlertDialog(
title: Text('自定义$title'),
content: TextField(
autofocus: true,
onChanged: (value) => valueStr = value,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(suffixText: suffix),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
result = int.tryParse(valueStr) ?? 0;
},
child: const Text('确定'),
),
],
);
},
);
}
if (result != -1) {
value = result!;
setState();
onChanged?.call(result!);
GStorage.setting.put(key, result);
}
}
},
);
}