Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:PiliPlus/services/download/download_service.dart';
import 'package:PiliPlus/services/service_locator.dart';
import 'package:PiliPlus/utils/cache_manager.dart';
import 'package:PiliPlus/utils/calc_window_position.dart';
import 'package:PiliPlus/utils/app_font.dart';
import 'package:PiliPlus/utils/date_utils.dart';
import 'package:PiliPlus/utils/extension/iterable_ext.dart';
import 'package:PiliPlus/utils/extension/theme_ext.dart';
Expand Down Expand Up @@ -96,6 +97,7 @@ void main() async {
if (kDebugMode) debugPrint('GStorage init error: $e');
exit(0);
}
await AppFont.init();
ScaledWidgetsFlutterBinding.instance.scaleFactor = Pref.uiScale;
await Future.wait([_initDownPath(), _initTmpPath()]);
Get
Expand Down
67 changes: 67 additions & 0 deletions lib/pages/setting/models/style_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import 'package:PiliPlus/pages/setting/widgets/multi_select_dialog.dart';
import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart';
import 'package:PiliPlus/pages/setting/widgets/slider_dialog.dart';
import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart';
import 'package:PiliPlus/utils/app_font.dart';
import 'package:PiliPlus/utils/extension/file_ext.dart';
import 'package:PiliPlus/utils/extension/num_ext.dart';
import 'package:PiliPlus/utils/extension/theme_ext.dart';
Expand Down Expand Up @@ -156,6 +157,12 @@ List<SettingsModel> get styleSettings => [
onChanged: (value) => Get.forceAppUpdate(),
onTap: _showFontWeightDialog,
),
NormalModel(
title: '应用字体',
leading: const Icon(Icons.font_download_outlined),
getSubtitle: () => AppFont.currentFontName ?? '系统字体',
onTap: _showCustomFontDialog,
),
NormalModel(
title: '界面缩放',
getSubtitle: () => '当前缩放比例:${Pref.uiScale.toStringAsFixed(2)}',
Expand Down Expand Up @@ -488,6 +495,66 @@ void _showQualityDialog({
});
}

Future<void> _showCustomFontDialog(
BuildContext context,
VoidCallback setState,
) async {
final pageContext = context;
await showDialog<void>(
context: pageContext,
builder: (dialogContext) => AlertDialog(
title: const Text('应用字体'),
content: Text(
AppFont.currentFontName == null
? '当前使用系统字体。'
: '当前字体:${AppFont.currentFontName}',
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('取消'),
),
TextButton(
onPressed: () async {
Navigator.of(dialogContext).pop();
final cleared = await AppFont.clear();
if (!pageContext.mounted) {
return;
}
if (cleared) {
setState();
Get.forceAppUpdate();
SmartDialog.showToast('已恢复为系统字体');
} else {
SmartDialog.showToast('当前已经是系统字体');
}
},
child: const Text('系统字体'),
),
FilledButton(
onPressed: () async {
Navigator.of(dialogContext).pop();
try {
final changed = await AppFont.pickAndApply();
if (!pageContext.mounted) {
return;
}
if (changed) {
setState();
Get.forceAppUpdate();
SmartDialog.showToast('自定义字体已应用');
}
} catch (e) {
SmartDialog.showToast('字体加载失败: $e');
}
},
child: const Text('选择字体'),
),
],
),
);
}

void _showUiScaleDialog(
BuildContext context,
VoidCallback setState,
Expand Down
158 changes: 158 additions & 0 deletions lib/utils/app_font.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import 'dart:io';

import 'package:PiliPlus/utils/path_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_key.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as path;

abstract final class AppFont {
static const List<String> allowedExtensions = ['ttf', 'otf'];
static const String _fontDirName = 'fonts';

static String? get currentFontName => Pref.customFontName;

static Future<void> init() async {
final fontPath = Pref.customFontPath;
final fontFamily = Pref.customFontFamily;
if (fontPath == null || fontFamily == null) {
await _cleanupFontDir();
return;
}

final file = File(fontPath);
if (!file.existsSync()) {
await clear();
return;
}

try {
await _loadFont(fontPath: fontPath, fontFamily: fontFamily);
await _cleanupFontDir(excludePath: fontPath);
} catch (_) {
await clear();
}
}

static Future<bool> pickAndApply() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: allowedExtensions,
withData: true,
);
if (result == null || result.files.isEmpty) {
return false;
}

final picked = result.files.single;
final extension = path
.extension(picked.path ?? picked.name)
.replaceFirst('.', '')
.toLowerCase();
if (!allowedExtensions.contains(extension)) {
throw UnsupportedError('unsupported font file: $extension');
}

final fontDir = Directory(path.join(appSupportDirPath, _fontDirName));
if (!fontDir.existsSync()) {
await fontDir.create(recursive: true);
}

final timestamp = DateTime.now().millisecondsSinceEpoch;
final targetPath = path.join(fontDir.path, 'custom_font_$timestamp.$extension');
final targetFile = File(targetPath);
if (picked.bytes case final Uint8List bytes) {
await targetFile.writeAsBytes(bytes, flush: true);
} else if (picked.path case final String sourcePath) {
await File(sourcePath).copy(targetPath);
} else {
throw StateError('missing font bytes');
}

final fontFamily = 'custom_font_$timestamp';
try {
await _loadFont(fontPath: targetPath, fontFamily: fontFamily);
final previousFontPath = Pref.customFontPath;
await GStorage.setting.put(SettingBoxKey.customFontPath, targetPath);
await GStorage.setting.put(SettingBoxKey.customFontFamily, fontFamily);
await GStorage.setting.put(
SettingBoxKey.customFontName,
path.basename(picked.path ?? picked.name),
);
if (previousFontPath != null && previousFontPath != targetPath) {
final previousFile = File(previousFontPath);
if (previousFile.existsSync()) {
try {
await previousFile.delete();
} catch (_) {}
}
}
await _cleanupFontDir(excludePath: targetPath);
return true;
} catch (_) {
if (targetFile.existsSync()) {
await targetFile.delete();
}
rethrow;
}
}

static Future<bool> clear() async {
final fontPath = Pref.customFontPath;
final hadCustomFont =
(fontPath != null && fontPath.isNotEmpty) ||
Pref.customFontFamily != null ||
Pref.customFontName != null;
await GStorage.setting.delete(SettingBoxKey.customFontPath);
await GStorage.setting.delete(SettingBoxKey.customFontFamily);
await GStorage.setting.delete(SettingBoxKey.customFontName);
if (fontPath != null && fontPath.isNotEmpty) {
final file = File(fontPath);
if (file.existsSync()) {
try {
await file.delete();
} catch (_) {}
}
}
final deletedFiles = await _cleanupFontDir();
return hadCustomFont || deletedFiles;
}

static Future<void> _loadFont({
required String fontPath,
required String fontFamily,
}) async {
final bytes = await File(fontPath).readAsBytes();
await (FontLoader(fontFamily)
..addFont(Future.value(ByteData.sublistView(bytes))))
.load();
}

static Future<bool> _cleanupFontDir({String? excludePath}) async {
final fontDir = Directory(path.join(appSupportDirPath, _fontDirName));
if (!fontDir.existsSync()) {
return false;
}

var deletedAny = false;
await for (final entity in fontDir.list()) {
if (entity is! File) {
continue;
}
if (excludePath != null && path.equals(entity.path, excludePath)) {
continue;
}
final extension = path.extension(entity.path).replaceFirst('.', '').toLowerCase();
if (!allowedExtensions.contains(extension)) {
continue;
}
try {
await entity.delete();
deletedAny = true;
} catch (_) {}
}
return deletedAny;
}
}
3 changes: 3 additions & 0 deletions lib/utils/storage_key.dart
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ abstract final class SettingBoxKey {

static const String themeMode = 'themeMode',
defaultTextScale = 'textScale',
customFontPath = 'customFontPath',
customFontFamily = 'customFontFamily',
customFontName = 'customFontName',
dynamicColor = 'dynamicColor',
customColor = 'customColor',
displayMode = 'displayMode',
Expand Down
13 changes: 13 additions & 0 deletions lib/utils/storage_pref.dart
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,19 @@ abstract final class Pref {
static int get appFontWeight =>
_setting.get(SettingBoxKey.appFontWeight, defaultValue: -1);

static String? get customFontPath =>
_setting.get(SettingBoxKey.customFontPath);

static String? get customFontFamily {
final value = _setting.get(SettingBoxKey.customFontFamily, defaultValue: '');
return value is String && value.isNotEmpty ? value : null;
}

static String? get customFontName {
final value = _setting.get(SettingBoxKey.customFontName, defaultValue: '');
return value is String && value.isNotEmpty ? value : null;
}

static bool get enableDragSubtitle =>
_setting.get(SettingBoxKey.enableDragSubtitle, defaultValue: false);

Expand Down
21 changes: 18 additions & 3 deletions lib/utils/theme_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,23 @@ abstract final class ThemeUtils {
required bool isDynamic,
bool isDark = false,
}) {
final customFontFamily = Pref.customFontFamily;
final appFontWeight = Pref.appFontWeight.clamp(
-1,
FontWeight.values.length - 1,
);
final fontWeight = appFontWeight == -1
? null
: FontWeight.values[appFontWeight];
late final textStyle = TextStyle(fontWeight: fontWeight);
late final textStyle = TextStyle(
fontWeight: fontWeight,
fontFamily: customFontFamily,
);
ThemeData themeData = ThemeData(
colorScheme: colorScheme,
fontFamily: customFontFamily,
useMaterial3: true,
textTheme: fontWeight == null
textTheme: fontWeight == null && customFontFamily == null
? null
: TextTheme(
displayLarge: textStyle,
Expand All @@ -41,7 +46,7 @@ abstract final class ThemeUtils {
labelMedium: textStyle,
labelSmall: textStyle,
),
tabBarTheme: fontWeight == null
tabBarTheme: fontWeight == null && customFontFamily == null
? null
: TabBarThemeData(labelStyle: textStyle),
appBarTheme: AppBarTheme(
Expand All @@ -53,6 +58,7 @@ abstract final class ThemeUtils {
titleTextStyle: TextStyle(
fontSize: 16,
color: colorScheme.onSurface,
fontFamily: customFontFamily,
fontWeight: fontWeight,
),
),
Expand Down Expand Up @@ -88,6 +94,7 @@ abstract final class ThemeUtils {
titleTextStyle: TextStyle(
fontSize: 18,
color: colorScheme.onSurface,
fontFamily: customFontFamily,
fontWeight: fontWeight,
),
backgroundColor: colorScheme.surface,
Expand Down Expand Up @@ -130,6 +137,14 @@ abstract final class ThemeUtils {
},
),
);
if (customFontFamily != null) {
themeData = themeData.copyWith(
textTheme: themeData.textTheme.apply(fontFamily: customFontFamily),
primaryTextTheme: themeData.primaryTextTheme.apply(
fontFamily: customFontFamily,
),
);
}
if (isDark) {
if (Pref.isPureBlackTheme) {
themeData = darkenTheme(themeData);
Expand Down
Loading