feat(customization): font switcher with Unbounded, styled via m3e

This commit is contained in:
klockky
2026-05-16 10:58:14 +03:00
parent 0729132938
commit 04dda00e65
6 changed files with 397 additions and 11 deletions
+37
View File
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppFont {
final String id;
final String label;
final String googleFamily;
const AppFont({
required this.id,
required this.label,
required this.googleFamily,
});
}
class AppFonts {
static const String prefKey = 'app_font';
static const List<AppFont> all = [
AppFont(id: 'inter', label: 'Inter', googleFamily: 'Inter'),
AppFont(id: 'unbounded', label: 'Unbounded', googleFamily: 'Unbounded'),
];
static AppFont get fallback => all.first;
static AppFont byId(String? id) {
return all.firstWhere((f) => f.id == id, orElse: () => fallback);
}
static TextTheme textTheme(String id, TextTheme base) {
return GoogleFonts.getTextTheme(byId(id).googleFamily, base);
}
static TextStyle sample(String id, {required double fontSize}) {
return GoogleFonts.getFont(byId(id).googleFamily, fontSize: fontSize);
}
}
@@ -0,0 +1,171 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../core/config/app_fonts.dart';
import '../../../core/utils/haptics.dart';
import '../../../main.dart';
class CustomizationScreen extends StatefulWidget {
const CustomizationScreen({super.key});
@override
State<CustomizationScreen> createState() => _CustomizationScreenState();
}
class _CustomizationScreenState extends State<CustomizationScreen> {
void _selectFont(String id) {
final app = KometApp.stateOf(context);
if (app == null || app.fontId == id) return;
Haptics.selection();
app.applyAppFont(id);
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final currentId = KometApp.stateOf(context)?.fontId ?? AppFonts.fallback.id;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBarM3E(
titleText: 'Кастомизация',
backgroundColor: cs.surface,
),
body: SafeArea(
top: false,
child: ListView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 8, 16, 120),
children: [
_PreviewCard(fontId: currentId),
const SizedBox(height: 28),
const _SectionLabel(icon: Symbols.text_fields, text: 'Шрифт'),
const SizedBox(height: 14),
for (final font in AppFonts.all) ...[
_FontOption(
font: font,
selected: font.id == currentId,
onTap: () => _selectFont(font.id),
),
if (font != AppFonts.all.last) const SizedBox(height: 12),
],
],
),
),
);
}
}
class _PreviewCard extends StatelessWidget {
final String fontId;
const _PreviewCard({required this.fontId});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'ПРЕДПРОСМОТР',
style: TextStyle(
color: cs.primary,
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
),
),
const SizedBox(height: 16),
Text(
'Съешь ещё этих мягких булок',
style: AppFonts.sample(fontId, fontSize: 22).copyWith(
color: cs.onSurface,
fontWeight: FontWeight.w600,
height: 1.25,
),
),
const SizedBox(height: 10),
Text(
'The quick brown fox 0123',
style: AppFonts.sample(fontId, fontSize: 15).copyWith(
color: cs.onSurfaceVariant,
),
),
],
),
);
}
}
class _SectionLabel extends StatelessWidget {
final IconData icon;
final String text;
const _SectionLabel({required this.icon, required this.text});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(left: 6),
child: Row(
children: [
Icon(icon, size: 20, color: cs.onSurfaceVariant, weight: 500),
const SizedBox(width: 8),
Text(
text,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 15,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
),
),
],
),
);
}
}
class _FontOption extends StatelessWidget {
final AppFont font;
final bool selected;
final VoidCallback onTap;
const _FontOption({
required this.font,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
child: ButtonM3E(
onPressed: onTap,
style: selected ? ButtonM3EStyle.filled : ButtonM3EStyle.tonal,
size: ButtonM3ESize.xl,
shape: ButtonM3EShape.round,
selected: selected,
icon: Icon(
selected ? Symbols.check_circle : Symbols.font_download,
fill: selected ? 1 : 0,
),
label: Text(
font.label,
style: AppFonts.sample(font.id, fontSize: 18),
),
),
);
}
}
@@ -9,6 +9,7 @@ import '../../../core/utils/haptics.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../auth/proxy_settings_sheet.dart'; import '../auth/proxy_settings_sheet.dart';
import 'customization_screen.dart';
import 'debug_menu_screen.dart'; import 'debug_menu_screen.dart';
import 'devices_screen.dart'; import 'devices_screen.dart';
import 'edit_profile_screen.dart'; import 'edit_profile_screen.dart';
@@ -142,6 +143,29 @@ child: _buildSection(
), ),
), ),
), ),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: _buildSection(
context,
cs,
items: [
_SettingsItem(
icon: Symbols.palette,
label: 'Кастомизация',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CustomizationScreen(),
),
);
},
),
],
),
),
),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
+36 -11
View File
@@ -2,11 +2,12 @@ import 'dart:async';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:komet/l10n/app_localizations.dart'; import 'package:komet/l10n/app_localizations.dart';
import 'package:m3e_collection/m3e_collection.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'backend/api.dart'; import 'backend/api.dart';
import 'core/config/app_fonts.dart';
import 'backend/modules/account.dart'; import 'backend/modules/account.dart';
import 'backend/modules/contacts.dart'; import 'backend/modules/contacts.dart';
import 'backend/modules/messages.dart'; import 'backend/modules/messages.dart';
@@ -59,11 +60,14 @@ void main() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final initialFpsOverlay = prefs.getBool('dev_fps_overlay') ?? false; final initialFpsOverlay = prefs.getBool('dev_fps_overlay') ?? false;
final initialVpnBypass = prefs.getBool(VpnBypassService.prefKey) ?? false; final initialVpnBypass = prefs.getBool(VpnBypassService.prefKey) ?? false;
final initialFontId =
prefs.getString(AppFonts.prefKey) ?? AppFonts.fallback.id;
runApp( runApp(
KometApp( KometApp(
initialLocale: initialLocale, initialLocale: initialLocale,
initialFpsOverlay: initialFpsOverlay, initialFpsOverlay: initialFpsOverlay,
initialVpnBypass: initialVpnBypass, initialVpnBypass: initialVpnBypass,
initialFontId: initialFontId,
), ),
); );
} }
@@ -74,11 +78,13 @@ class KometApp extends StatefulWidget {
required this.initialLocale, required this.initialLocale,
this.initialFpsOverlay = false, this.initialFpsOverlay = false,
this.initialVpnBypass = false, this.initialVpnBypass = false,
required this.initialFontId,
}); });
final Locale initialLocale; final Locale initialLocale;
final bool initialFpsOverlay; final bool initialFpsOverlay;
final bool initialVpnBypass; final bool initialVpnBypass;
final String initialFontId;
static final navigatorKey = GlobalKey<NavigatorState>(); static final navigatorKey = GlobalKey<NavigatorState>();
static KometAppState? stateOf(BuildContext context) { static KometAppState? stateOf(BuildContext context) {
@@ -93,6 +99,7 @@ class KometAppState extends State<KometApp> {
static const _fallbackSeed = Color(0xFFC1C4FF); static const _fallbackSeed = Color(0xFFC1C4FF);
late Locale _locale; late Locale _locale;
late String _fontId;
bool _isLoggingOut = false; bool _isLoggingOut = false;
StreamSubscription<SessionExpiredException>? _sessionExpiredSub; StreamSubscription<SessionExpiredException>? _sessionExpiredSub;
StreamSubscription<LoginStatus>? _loginStatusSub; StreamSubscription<LoginStatus>? _loginStatusSub;
@@ -112,6 +119,7 @@ class KometAppState extends State<KometApp> {
void initState() { void initState() {
super.initState(); super.initState();
_locale = widget.initialLocale; _locale = widget.initialLocale;
_fontId = widget.initialFontId;
api.setReconnectCallback(() async { api.setReconnectCallback(() async {
try { try {
@@ -217,6 +225,17 @@ class KometAppState extends State<KometApp> {
} }
} }
String get fontId => _fontId;
Future<void> applyAppFont(String fontId) async {
if (_fontId == fontId) return;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(AppFonts.prefKey, fontId);
if (mounted) {
setState(() => _fontId = fontId);
}
}
void notifyProfileUpdate() { void notifyProfileUpdate() {
_profileUpdateController.add(null); _profileUpdateController.add(null);
} }
@@ -282,18 +301,24 @@ class KometAppState extends State<KometApp> {
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
theme: ThemeData( theme: withM3ETheme(
useMaterial3: true, ThemeData(
colorScheme: lightScheme, useMaterial3: true,
textTheme: GoogleFonts.interTextTheme( colorScheme: lightScheme,
ThemeData(brightness: Brightness.light).textTheme, textTheme: AppFonts.textTheme(
_fontId,
ThemeData(brightness: Brightness.light).textTheme,
),
), ),
), ),
darkTheme: ThemeData( darkTheme: withM3ETheme(
useMaterial3: true, ThemeData(
colorScheme: darkScheme, useMaterial3: true,
textTheme: GoogleFonts.interTextTheme( colorScheme: darkScheme,
ThemeData(brightness: Brightness.dark).textTheme, textTheme: AppFonts.textTheme(
_fontId,
ThemeData(brightness: Brightness.dark).textTheme,
),
), ),
), ),
navigatorKey: KometApp.navigatorKey, navigatorKey: KometApp.navigatorKey,
+128
View File
@@ -9,6 +9,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.71" version: "1.3.71"
app_bar_m3e:
dependency: transitive
description:
name: app_bar_m3e
sha256: a8cef3d2cfe8d254fac355816ede509e7b437066d9fef8e7ba779a202485bc9f
url: "https://pub.dev"
source: hosted
version: "0.1.2"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -33,6 +41,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
button_group_m3e:
dependency: transitive
description:
name: button_group_m3e
sha256: bb8ce524f87e806c89abb4cd430425de22cd53feeacc122e3b5da45c76ba2246
url: "https://pub.dev"
source: hosted
version: "0.3.1"
button_m3e:
dependency: transitive
description:
name: button_m3e
sha256: "6754ddeb9068ad2005bd26d5ceabc41268029465095686d7d228296c2e706909"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
cached_network_image: cached_network_image:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -161,6 +185,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.8" version: "2.0.8"
expressive_refresh:
dependency: transitive
description:
name: expressive_refresh
sha256: "99bb70ae1719ebdedbaf3b4a49031ef7b916da157f7843b8e83efbe6633b20ad"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
fab_m3e:
dependency: transitive
description:
name: fab_m3e
sha256: e4f5abfa3c8c092005449d56dcac45b85e2dbe9c32789d672c5ed71428e43b59
url: "https://pub.dev"
source: hosted
version: "0.1.1"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -421,6 +461,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
icon_button_m3e:
dependency: transitive
description:
name: icon_button_m3e
sha256: c4524d6141a468679821bbb635b833ac6831925d8a6ae4a4511430b0e4ab9c67
url: "https://pub.dev"
source: hosted
version: "0.2.1"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -469,6 +517,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.0" version: "6.1.0"
loading_indicator_m3e:
dependency: transitive
description:
name: loading_indicator_m3e
sha256: "3fe97385b4f84382c25b901abf88fcee4200e6e78fcd5ed7e037a34d7db53038"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
logger: logger:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -485,6 +541,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
m3e_collection:
dependency: "direct main"
description:
name: m3e_collection
sha256: "623079bd3fe39bac1a3a10656647cdef6a8b2aa8bb8ee5ff99779facd05b18f6"
url: "https://pub.dev"
source: hosted
version: "0.3.7"
m3e_design:
dependency: transitive
description:
name: m3e_design
sha256: "15ff0ef4c43553d855c5e866a9aee8231d44919fe2bb354b1259337bdfd659b4"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -501,6 +573,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.0" version: "0.13.0"
material_new_shapes:
dependency: transitive
description:
name: material_new_shapes
sha256: e4bc375205e187e8fb232573387112dd8c0dd45b03af8aa2b3c79eb4b9e3e0dc
url: "https://pub.dev"
source: hosted
version: "1.0.0"
material_symbols_icons: material_symbols_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -541,6 +621,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.17.6" version: "0.17.6"
navigation_bar_m3e:
dependency: transitive
description:
name: navigation_bar_m3e
sha256: "20d0ce28c783fd7b530e542e1e62c9edc402028303af08667fcbfd89162c83b2"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
navigation_rail_m3e:
dependency: transitive
description:
name: navigation_rail_m3e
sha256: "029a1f556c4ecaf8318ae7d4ad09af117bdb449e483e3d9ec04085a9e341db3d"
url: "https://pub.dev"
source: hosted
version: "0.3.5"
objective_c: objective_c:
dependency: transitive dependency: transitive
description: description:
@@ -653,6 +749,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
progress_indicator_m3e:
dependency: transitive
description:
name: progress_indicator_m3e
sha256: "1a2ca029d14427d31093552a0e916b453f58643b78ef805ce6f4669845ac8f70"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -730,6 +834,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
slider_m3e:
dependency: transitive
description:
name: slider_m3e
sha256: e4c25e94b46ebf3164f407e9db6cce70aa51b92db5143d720f6e55a2a709aa67
url: "https://pub.dev"
source: hosted
version: "0.1.1"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@@ -738,6 +850,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.2" version: "1.10.2"
split_button_m3e:
dependency: transitive
description:
name: split_button_m3e
sha256: "8864b612e2475cf8d070783d5d834c8da98b7489365e4597e5b4f6ef85d50f66"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
sqflite: sqflite:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -850,6 +970,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.0" version: "0.11.0"
toolbar_m3e:
dependency: transitive
description:
name: toolbar_m3e
sha256: "5a02108fc47d5b14dc645fd52d3cbdbd7cc3c2c60c8410c5a69d9cf7ac852cc9"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
+1
View File
@@ -52,6 +52,7 @@ dependencies:
sqflite_common_ffi: ^2.4.0+2 sqflite_common_ffi: ^2.4.0+2
path: ^1.9.1 path: ^1.9.1
google_fonts: ^6.2.1 google_fonts: ^6.2.1
m3e_collection: ^0.3.7
material_symbols_icons: ^4.2906.0 material_symbols_icons: ^4.2906.0
dynamic_color: ^1.8.1 dynamic_color: ^1.8.1
shared_preferences: ^2.5.4 shared_preferences: ^2.5.4