diff --git a/lib/core/config/app_fonts.dart b/lib/core/config/app_fonts.dart new file mode 100644 index 0000000..6eb1ff8 --- /dev/null +++ b/lib/core/config/app_fonts.dart @@ -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 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); + } +} diff --git a/lib/frontend/screens/profile/customization_screen.dart b/lib/frontend/screens/profile/customization_screen.dart new file mode 100644 index 0000000..e746620 --- /dev/null +++ b/lib/frontend/screens/profile/customization_screen.dart @@ -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 createState() => _CustomizationScreenState(); +} + +class _CustomizationScreenState extends State { + 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), + ), + ), + ); + } +} diff --git a/lib/frontend/screens/profile/settings_tab.dart b/lib/frontend/screens/profile/settings_tab.dart index 313abef..0d22199 100644 --- a/lib/frontend/screens/profile/settings_tab.dart +++ b/lib/frontend/screens/profile/settings_tab.dart @@ -9,6 +9,7 @@ import '../../../core/utils/haptics.dart'; import '../../../l10n/app_localizations.dart'; import '../../../main.dart'; import '../auth/proxy_settings_sheet.dart'; +import 'customization_screen.dart'; import 'debug_menu_screen.dart'; import 'devices_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( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), diff --git a/lib/main.dart b/lib/main.dart index 8596491..5513b1f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,11 +2,12 @@ import 'dart:async'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.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:shared_preferences/shared_preferences.dart'; import 'backend/api.dart'; +import 'core/config/app_fonts.dart'; import 'backend/modules/account.dart'; import 'backend/modules/contacts.dart'; import 'backend/modules/messages.dart'; @@ -59,11 +60,14 @@ void main() async { final prefs = await SharedPreferences.getInstance(); final initialFpsOverlay = prefs.getBool('dev_fps_overlay') ?? false; final initialVpnBypass = prefs.getBool(VpnBypassService.prefKey) ?? false; + final initialFontId = + prefs.getString(AppFonts.prefKey) ?? AppFonts.fallback.id; runApp( KometApp( initialLocale: initialLocale, initialFpsOverlay: initialFpsOverlay, initialVpnBypass: initialVpnBypass, + initialFontId: initialFontId, ), ); } @@ -74,11 +78,13 @@ class KometApp extends StatefulWidget { required this.initialLocale, this.initialFpsOverlay = false, this.initialVpnBypass = false, + required this.initialFontId, }); final Locale initialLocale; final bool initialFpsOverlay; final bool initialVpnBypass; + final String initialFontId; static final navigatorKey = GlobalKey(); static KometAppState? stateOf(BuildContext context) { @@ -93,6 +99,7 @@ class KometAppState extends State { static const _fallbackSeed = Color(0xFFC1C4FF); late Locale _locale; + late String _fontId; bool _isLoggingOut = false; StreamSubscription? _sessionExpiredSub; StreamSubscription? _loginStatusSub; @@ -112,6 +119,7 @@ class KometAppState extends State { void initState() { super.initState(); _locale = widget.initialLocale; + _fontId = widget.initialFontId; api.setReconnectCallback(() async { try { @@ -217,6 +225,17 @@ class KometAppState extends State { } } + String get fontId => _fontId; + + Future 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() { _profileUpdateController.add(null); } @@ -282,18 +301,24 @@ class KometAppState extends State { themeMode: ThemeMode.system, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - theme: ThemeData( - useMaterial3: true, - colorScheme: lightScheme, - textTheme: GoogleFonts.interTextTheme( - ThemeData(brightness: Brightness.light).textTheme, + theme: withM3ETheme( + ThemeData( + useMaterial3: true, + colorScheme: lightScheme, + textTheme: AppFonts.textTheme( + _fontId, + ThemeData(brightness: Brightness.light).textTheme, + ), ), ), - darkTheme: ThemeData( - useMaterial3: true, - colorScheme: darkScheme, - textTheme: GoogleFonts.interTextTheme( - ThemeData(brightness: Brightness.dark).textTheme, + darkTheme: withM3ETheme( + ThemeData( + useMaterial3: true, + colorScheme: darkScheme, + textTheme: AppFonts.textTheme( + _fontId, + ThemeData(brightness: Brightness.dark).textTheme, + ), ), ), navigatorKey: KometApp.navigatorKey, diff --git a/pubspec.lock b/pubspec.lock index bac0404..2501c4e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -33,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -161,6 +185,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -421,6 +461,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -469,6 +517,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -485,6 +541,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -501,6 +573,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -541,6 +621,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -653,6 +749,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -730,6 +834,14 @@ packages: description: flutter source: sdk 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: dependency: transitive description: @@ -738,6 +850,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -850,6 +970,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2b922da..a0d984f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,7 @@ dependencies: sqflite_common_ffi: ^2.4.0+2 path: ^1.9.1 google_fonts: ^6.2.1 + m3e_collection: ^0.3.7 material_symbols_icons: ^4.2906.0 dynamic_color: ^1.8.1 shared_preferences: ^2.5.4