Skip to content

Commit dc94c44

Browse files
committed
feat: add visual density settings and custom font preview function + fix settings apply
1 parent 588996a commit dc94c44

File tree

15 files changed

+537
-139
lines changed

15 files changed

+537
-139
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import 'package:flutter/services.dart';
2+
import 'package:flutter/widgets.dart';
3+
4+
/// Следит за lifecycle и сбрасывает состояние клавиатуры при смене активности.
5+
///
6+
/// На desktop иногда теряются KeyUp события при потере фокуса/сворачивании окна,
7+
/// из‑за чего следующий KeyDown может вызвать assert в `HardwareKeyboard`.
8+
class KeyboardStateResetter extends StatefulWidget {
9+
const KeyboardStateResetter({required this.child, super.key});
10+
11+
final Widget child;
12+
13+
@override
14+
State<KeyboardStateResetter> createState() => _KeyboardStateResetterState();
15+
}
16+
17+
class _KeyboardStateResetterState extends State<KeyboardStateResetter>
18+
with WidgetsBindingObserver {
19+
static const bool _isFlutterTest = bool.fromEnvironment('FLUTTER_TEST');
20+
21+
@override
22+
void initState() {
23+
super.initState();
24+
if (_isFlutterTest) return;
25+
WidgetsBinding.instance.addObserver(this);
26+
}
27+
28+
@override
29+
void didChangeAppLifecycleState(AppLifecycleState state) {
30+
switch (state) {
31+
case AppLifecycleState.resumed:
32+
case AppLifecycleState.inactive:
33+
case AppLifecycleState.paused:
34+
case AppLifecycleState.detached:
35+
case AppLifecycleState.hidden:
36+
_clearKeyboardState();
37+
break;
38+
}
39+
}
40+
41+
void _clearKeyboardState() {
42+
// HardwareKeyboard API отличается между версиями Flutter — вызываем мягко.
43+
try {
44+
final dynamic hw = HardwareKeyboard.instance;
45+
hw.clearState();
46+
} catch (_) {}
47+
}
48+
49+
@override
50+
void dispose() {
51+
if (_isFlutterTest) {
52+
super.dispose();
53+
return;
54+
}
55+
WidgetsBinding.instance.removeObserver(this);
56+
super.dispose();
57+
}
58+
59+
@override
60+
Widget build(BuildContext context) => widget.child;
61+
}

frontend/lib/features/custom_fonts/application/font_service.dart

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import 'package:flutter/foundation.dart';
2+
import 'package:flutter/services.dart';
3+
import 'package:uuid/uuid.dart';
24
import '../domain/entities/custom_font.dart';
35
import '../domain/repositories/font_repository.dart';
46
import '../domain/usecases/load_custom_font.dart';
@@ -11,6 +13,7 @@ import '../infrastructure/repositories/font_repository_impl.dart';
1113
/// Following Facade Pattern and Single Responsibility Principle
1214
class FontService {
1315
static FontService? _instance;
16+
static const _uuid = Uuid();
1417
late final FontRepository _repository;
1518
late final LoadCustomFontUseCase _loadUseCase;
1619
late final SaveCustomFontUseCase _saveUseCase;
@@ -19,6 +22,11 @@ class FontService {
1922
/// Current loaded custom font (null if using system font)
2023
final ValueNotifier<CustomFont?> currentFont = ValueNotifier(null);
2124

25+
/// Pending font for preview (not yet saved)
26+
CustomFont? _pendingFont;
27+
Uint8List? _pendingFontData;
28+
bool _pendingRemove = false;
29+
2230
/// Factory constructor for singleton
2331
factory FontService() {
2432
_instance ??= FontService._internal();
@@ -45,8 +53,90 @@ class FontService {
4553
}
4654
}
4755

56+
/// Load font for preview only (register in memory, don't save to storage)
57+
/// Call commitPendingFont() to save permanently
58+
Future<CustomFont> loadFontForPreview({
59+
required Uint8List fontData,
60+
required String fileName,
61+
required String familyName,
62+
}) async {
63+
if (fontData.isEmpty) {
64+
throw ArgumentError('Font data cannot be empty');
65+
}
66+
if (familyName.isEmpty) {
67+
throw ArgumentError('Font family name cannot be empty');
68+
}
69+
70+
// Создаём entity для превью
71+
final font = CustomFont(
72+
id: _uuid.v4(),
73+
name: fileName,
74+
familyName: familyName,
75+
uploadedAt: DateTime.now(),
76+
sizeInBytes: fontData.length,
77+
);
78+
79+
// Регистрируем шрифт в Flutter (только в памяти)
80+
final loader = FontLoader(familyName);
81+
loader.addFont(Future.value(ByteData.sublistView(fontData)));
82+
await loader.load();
83+
84+
// Сохраняем как pending
85+
_pendingFont = font;
86+
_pendingFontData = fontData;
87+
_pendingRemove = false;
88+
89+
return font;
90+
}
91+
92+
/// Get pending font (for preview in settings)
93+
CustomFont? get pendingFont => _pendingFont;
94+
95+
/// Check if there's a pending remove operation
96+
bool get hasPendingRemove => _pendingRemove;
97+
98+
/// Check if there are any pending changes
99+
bool get hasPendingChanges => _pendingFont != null || _pendingRemove;
100+
101+
/// Mark current font for removal (will be removed on commit)
102+
void markFontForRemoval() {
103+
_pendingRemove = true;
104+
_pendingFont = null;
105+
_pendingFontData = null;
106+
}
107+
108+
/// Commit pending changes (save font or remove)
109+
Future<void> commitPendingChanges() async {
110+
if (_pendingRemove) {
111+
// Удаляем текущий шрифт
112+
await _removeUseCase.execute();
113+
currentFont.value = null;
114+
_pendingRemove = false;
115+
} else if (_pendingFont != null && _pendingFontData != null) {
116+
// Удаляем старый шрифт если есть
117+
if (currentFont.value != null) {
118+
await _removeUseCase.execute();
119+
}
120+
// Сохраняем новый шрифт
121+
await _repository.saveFontData(_pendingFont!.id, _pendingFontData!);
122+
await _repository.saveCustomFont(_pendingFont!);
123+
currentFont.value = _pendingFont;
124+
}
125+
// Очищаем pending состояние
126+
_pendingFont = null;
127+
_pendingFontData = null;
128+
}
129+
130+
/// Cancel pending changes
131+
void cancelPendingChanges() {
132+
_pendingFont = null;
133+
_pendingFontData = null;
134+
_pendingRemove = false;
135+
}
136+
48137
/// Save and load a new custom font
49138
/// Returns the saved font metadata
139+
@Deprecated('Use loadFontForPreview + commitPendingChanges instead')
50140
Future<CustomFont> saveCustomFont({
51141
required Uint8List fontData,
52142
required String fileName,
@@ -69,6 +159,7 @@ class FontService {
69159
}
70160

71161
/// Remove custom font and revert to system font
162+
@Deprecated('Use markFontForRemoval + commitPendingChanges instead')
72163
Future<void> removeCustomFont() async {
73164
await _removeUseCase.execute();
74165
currentFont.value = null;

frontend/lib/features/custom_fonts/presentation/providers/font_provider.dart

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,43 @@ class FontProvider extends ChangeNotifier {
1616
bool get hasCustomFont => _fontService.hasCustomFont();
1717
String? get fontFamily => _fontService.getCurrentFontFamily();
1818

19+
/// Pending font for preview (not yet saved)
20+
CustomFont? get pendingFont => _fontService.pendingFont;
21+
22+
/// Check if font marked for removal
23+
bool get hasPendingRemove => _fontService.hasPendingRemove;
24+
25+
/// Check if there are pending changes
26+
bool get hasPendingChanges => _fontService.hasPendingChanges;
27+
28+
/// Effective font family for preview (pending or current)
29+
String? get previewFontFamily {
30+
if (_fontService.hasPendingRemove) return null;
31+
return _fontService.pendingFont?.familyName ?? currentFont?.familyName;
32+
}
33+
34+
/// Effective font for display (pending or current)
35+
CustomFont? get effectiveFont {
36+
if (_fontService.hasPendingRemove) return null;
37+
return _fontService.pendingFont ?? currentFont;
38+
}
39+
40+
/// Check if effectively has a custom font (for preview)
41+
bool get effectivelyHasCustomFont {
42+
if (_fontService.hasPendingRemove) return false;
43+
return _fontService.pendingFont != null || hasCustomFont;
44+
}
45+
1946
FontProvider() {
20-
// Listen to font changes from service
2147
_fontService.currentFont.addListener(_onFontChanged);
2248
}
2349

2450
void _onFontChanged() {
2551
notifyListeners();
2652
}
2753

28-
/// Load a custom font from file
29-
Future<void> loadCustomFont({
54+
/// Load font for preview (not saved yet)
55+
Future<void> loadFontForPreview({
3056
required Uint8List fontData,
3157
required String fileName,
3258
required String familyName,
@@ -35,11 +61,12 @@ class FontProvider extends ChangeNotifier {
3561
_clearError();
3662

3763
try {
38-
await _fontService.saveCustomFont(
64+
await _fontService.loadFontForPreview(
3965
fontData: fontData,
4066
fileName: fileName,
4167
familyName: familyName,
4268
);
69+
notifyListeners();
4370
} catch (e) {
4471
_setError('Failed to load font: $e');
4572
rethrow;
@@ -48,21 +75,34 @@ class FontProvider extends ChangeNotifier {
4875
}
4976
}
5077

51-
/// Remove custom font and revert to system font
52-
Future<void> removeCustomFont() async {
78+
/// Mark font for removal (will be removed on commit)
79+
void markFontForRemoval() {
80+
_fontService.markFontForRemoval();
81+
notifyListeners();
82+
}
83+
84+
/// Commit pending changes (save or remove font)
85+
Future<void> commitPendingChanges() async {
5386
_setLoading(true);
5487
_clearError();
5588

5689
try {
57-
await _fontService.removeCustomFont();
90+
await _fontService.commitPendingChanges();
91+
notifyListeners();
5892
} catch (e) {
59-
_setError('Failed to remove font: $e');
93+
_setError('Failed to save font changes: $e');
6094
rethrow;
6195
} finally {
6296
_setLoading(false);
6397
}
6498
}
6599

100+
/// Cancel pending changes
101+
void cancelPendingChanges() {
102+
_fontService.cancelPendingChanges();
103+
notifyListeners();
104+
}
105+
66106
void _setLoading(bool value) {
67107
_isLoading = value;
68108
notifyListeners();

0 commit comments

Comments
 (0)