diff --git a/.github/workflows/build-android-fcm.yml b/.github/workflows/build-android-fcm.yml index f3f8718..7a7eafe 100644 --- a/.github/workflows/build-android-fcm.yml +++ b/.github/workflows/build-android-fcm.yml @@ -67,10 +67,10 @@ jobs: echo "Release signing configured." - name: Build Universal APK - run: flutter build apk --release --flavor oneme + run: flutter build apk --release --flavor oneme --obfuscate --split-debug-info=build/symbols - name: Build Split APKs - run: flutter build apk --release --split-per-abi --flavor oneme + run: flutter build apk --release --split-per-abi --flavor oneme --obfuscate --split-debug-info=build/symbols - name: Upload Universal APK uses: actions/upload-artifact@v4 @@ -105,7 +105,7 @@ jobs: retention-days: 30 - name: Build App Bundle - run: flutter build appbundle --release --flavor oneme + run: flutter build appbundle --release --flavor oneme --obfuscate --split-debug-info=build/symbols - name: Upload App Bundle artifact uses: actions/upload-artifact@v4 @@ -114,3 +114,11 @@ jobs: path: build/app/outputs/bundle/onemeRelease/app-oneme-release.aab if-no-files-found: error retention-days: 30 + + - name: Upload debug symbols + uses: actions/upload-artifact@v4 + with: + name: komet-android-fcm-symbols + path: build/symbols + if-no-files-found: error + retention-days: 90 diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 3d7f042..f0acd04 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -67,10 +67,10 @@ jobs: echo "Release signing configured." - name: Build Universal APK - run: flutter build apk --release --flavor komet + run: flutter build apk --release --flavor komet --obfuscate --split-debug-info=build/symbols - name: Build Split APKs - run: flutter build apk --release --split-per-abi --flavor komet + run: flutter build apk --release --split-per-abi --flavor komet --obfuscate --split-debug-info=build/symbols - name: Upload Universal APK uses: actions/upload-artifact@v4 @@ -105,7 +105,7 @@ jobs: retention-days: 30 - name: Build App Bundle - run: flutter build appbundle --release --flavor komet + run: flutter build appbundle --release --flavor komet --obfuscate --split-debug-info=build/symbols - name: Upload App Bundle artifact uses: actions/upload-artifact@v4 @@ -114,3 +114,11 @@ jobs: path: build/app/outputs/bundle/kometRelease/app-komet-release.aab if-no-files-found: error retention-days: 30 + + - name: Upload debug symbols + uses: actions/upload-artifact@v4 + with: + name: komet-android-symbols + path: build/symbols + if-no-files-found: error + retention-days: 90 diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index a5de1e7..4eedc68 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -64,9 +64,9 @@ jobs: - name: Build APKs and App Bundle (${{ matrix.flavor }}) run: | - flutter build apk --release --flavor ${{ matrix.flavor }} - flutter build apk --release --split-per-abi --flavor ${{ matrix.flavor }} - flutter build appbundle --release --flavor ${{ matrix.flavor }} + flutter build apk --release --flavor ${{ matrix.flavor }} --obfuscate --split-debug-info=build/symbols + flutter build apk --release --split-per-abi --flavor ${{ matrix.flavor }} --obfuscate --split-debug-info=build/symbols + flutter build appbundle --release --flavor ${{ matrix.flavor }} --obfuscate --split-debug-info=build/symbols - name: Collect Android artifacts (${{ matrix.flavor }}) run: | @@ -77,6 +77,7 @@ jobs: cp build/app/outputs/flutter-apk/app-armeabi-v7a-$F-release.apk dist/Komet-android-$F-armeabi-v7a.apk cp build/app/outputs/flutter-apk/app-x86_64-$F-release.apk dist/Komet-android-$F-x86_64.apk cp build/app/outputs/bundle/${F}Release/app-$F-release.aab dist/Komet-android-$F.aab + tar -czf dist/Komet-android-$F-symbols.tar.gz -C build/symbols . - name: Upload Android artifacts (${{ matrix.flavor }}) uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index adc94f8..34dc87b 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -64,9 +64,9 @@ jobs: - name: Build APKs and App Bundle (${{ matrix.flavor }}) run: | - flutter build apk --release --flavor ${{ matrix.flavor }} - flutter build apk --release --split-per-abi --flavor ${{ matrix.flavor }} - flutter build appbundle --release --flavor ${{ matrix.flavor }} + flutter build apk --release --flavor ${{ matrix.flavor }} --obfuscate --split-debug-info=build/symbols + flutter build apk --release --split-per-abi --flavor ${{ matrix.flavor }} --obfuscate --split-debug-info=build/symbols + flutter build appbundle --release --flavor ${{ matrix.flavor }} --obfuscate --split-debug-info=build/symbols - name: Collect Android artifacts (${{ matrix.flavor }}) run: | @@ -77,6 +77,7 @@ jobs: cp build/app/outputs/flutter-apk/app-armeabi-v7a-$F-release.apk dist/Komet-android-$F-armeabi-v7a.apk cp build/app/outputs/flutter-apk/app-x86_64-$F-release.apk dist/Komet-android-$F-x86_64.apk cp build/app/outputs/bundle/${F}Release/app-$F-release.aab dist/Komet-android-$F.aab + tar -czf dist/Komet-android-$F-symbols.tar.gz -C build/symbols . - name: Upload Android artifacts (${{ matrix.flavor }}) uses: actions/upload-artifact@v4 diff --git a/CLAUDE.md b/CLAUDE.md index 00500dd..fa0a52d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,10 +14,10 @@ flutter analyze # lint / static analysis flutter run # run on connected device (default: komet flavor) flutter run --flavor oneme -t lib/main.dart # run oneme flavor (FCM) -# Android builds -flutter build apk --release --flavor komet -flutter build apk --release --split-per-abi --flavor komet -flutter build appbundle --release --flavor komet +# Android builds (release builds use obfuscation; keep symbols to de-obfuscate crashes) +flutter build apk --release --flavor komet --obfuscate --split-debug-info=build/symbols +flutter build apk --release --split-per-abi --flavor komet --obfuscate --split-debug-info=build/symbols +flutter build appbundle --release --flavor komet --obfuscate --split-debug-info=build/symbols # Other platforms flutter build ios --release --no-codesign diff --git a/assets/fonts/Inter.ttf b/assets/fonts/Inter.ttf new file mode 100644 index 0000000..047c92f Binary files /dev/null and b/assets/fonts/Inter.ttf differ diff --git a/assets/fonts/Outfit.ttf b/assets/fonts/Outfit.ttf new file mode 100644 index 0000000..466d624 Binary files /dev/null and b/assets/fonts/Outfit.ttf differ diff --git a/assets/fonts/Unbounded.ttf b/assets/fonts/Unbounded.ttf new file mode 100644 index 0000000..e3c064d Binary files /dev/null and b/assets/fonts/Unbounded.ttf differ diff --git a/lib/core/config/app_fonts.dart b/lib/core/config/app_fonts.dart index f99d1f9..636ef54 100644 --- a/lib/core/config/app_fonts.dart +++ b/lib/core/config/app_fonts.dart @@ -1,26 +1,23 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:shared_preferences/shared_preferences.dart'; class AppFont { final String id; final String label; - final String? googleFamily; + final String? fontFamily; const AppFont({ required this.id, required this.label, - this.googleFamily, + this.fontFamily, }); - bool get isSystem => googleFamily == null; + bool get isSystem => fontFamily == null; bool get isCustom => id.startsWith(AppFonts.customPrefix); } class AppFonts { static const String prefKey = 'app_font'; static const String scalePrefKey = 'app_font_scale'; - static const String customPrefKey = 'app_custom_fonts'; static const String customPrefix = 'g:'; static const double minScale = 0.60; @@ -29,8 +26,8 @@ class AppFonts { static const List builtIn = [ AppFont(id: 'system', label: 'Системный'), - AppFont(id: 'inter', label: 'Inter', googleFamily: 'Inter'), - AppFont(id: 'unbounded', label: 'Unbounded', googleFamily: 'Unbounded'), + AppFont(id: 'inter', label: 'Inter', fontFamily: 'Inter'), + AppFont(id: 'unbounded', label: 'Unbounded', fontFamily: 'Unbounded'), ]; static AppFont get fallback => builtIn.first; @@ -40,29 +37,20 @@ class AppFonts { static AppFont resolve(String id) { if (id.startsWith(customPrefix)) { final family = id.substring(customPrefix.length); - return AppFont(id: id, label: family, googleFamily: family); + return AppFont(id: id, label: family, fontFamily: family); } return builtIn.firstWhere((f) => f.id == id, orElse: () => fallback); } static TextTheme textTheme(String id, TextTheme base) { - final family = resolve(id).googleFamily; + final family = resolve(id).fontFamily; if (family == null) return base; - try { - return GoogleFonts.getTextTheme(family, base); - } catch (_) { - return base; - } + return base.apply(fontFamily: family); } static TextStyle sample(String id, {required double fontSize}) { - final family = resolve(id).googleFamily; - if (family == null) return TextStyle(fontSize: fontSize); - try { - return GoogleFonts.getFont(family, fontSize: fontSize); - } catch (_) { - return TextStyle(fontSize: fontSize); - } + final family = resolve(id).fontFamily; + return TextStyle(fontFamily: family, fontSize: fontSize); } static double clampScale(double scale) => @@ -86,35 +74,4 @@ class AppFonts { value = value.replaceAll('+', ' ').trim(); return value.isEmpty ? null : value; } - - static String? matchGoogleFamily(String family) { - final map = GoogleFonts.asMap(); - if (map.containsKey(family)) return family; - final lower = family.toLowerCase(); - for (final key in map.keys) { - if (key.toLowerCase() == lower) return key; - } - return null; - } - - static Future> loadCustomFamilies() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getStringList(customPrefKey) ?? const []; - } - - static Future addCustomFamily(String family) async { - final prefs = await SharedPreferences.getInstance(); - final list = prefs.getStringList(customPrefKey) ?? []; - if (!list.contains(family)) { - list.add(family); - await prefs.setStringList(customPrefKey, list); - } - } - - static Future removeCustomFamily(String family) async { - final prefs = await SharedPreferences.getInstance(); - final list = prefs.getStringList(customPrefKey) ?? []; - list.remove(family); - await prefs.setStringList(customPrefKey, list); - } } diff --git a/lib/core/config/custom_font_service.dart b/lib/core/config/custom_font_service.dart new file mode 100644 index 0000000..151d31f --- /dev/null +++ b/lib/core/config/custom_font_service.dart @@ -0,0 +1,152 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class CustomFontService { + static const String prefKey = 'app_custom_fonts'; + static const String _userAgent = 'Mozilla/5.0 (X11; Linux x86_64) Chrome/120'; + + static final Set _loaded = {}; + + static Future> families() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getStringList(prefKey) ?? const []; + } + + static Future preloadCached() async { + final dir = await _cacheDir(); + for (final family in await families()) { + if (_loaded.contains(family)) continue; + final file = _fileFor(dir, family); + if (!await file.exists()) continue; + try { + await _register(family, await file.readAsBytes()); + } catch (_) {} + } + } + + static Future addFamily(String family) async { + final dir = await _cacheDir(); + final file = _fileFor(dir, family); + Uint8List? bytes; + if (await file.exists()) { + bytes = await file.readAsBytes(); + } else { + bytes = await _download(family); + if (bytes == null) return null; + await file.writeAsBytes(bytes); + } + try { + await _register(family, bytes); + } catch (_) { + if (await file.exists()) await file.delete(); + return null; + } + await _persist(family); + return family; + } + + static Future removeFamily(String family) async { + final prefs = await SharedPreferences.getInstance(); + final list = prefs.getStringList(prefKey) ?? []; + list.remove(family); + await prefs.setStringList(prefKey, list); + final file = _fileFor(await _cacheDir(), family); + if (await file.exists()) await file.delete(); + } + + static Future _cacheDir() async { + final base = await getApplicationSupportDirectory(); + final dir = Directory('${base.path}/custom_fonts'); + if (!await dir.exists()) await dir.create(recursive: true); + return dir; + } + + static File _fileFor(Directory dir, String family) { + final safe = family.replaceAll(RegExp(r'[^A-Za-z0-9_-]'), '_'); + return File('${dir.path}/$safe.ttf'); + } + + static Future _register(String family, Uint8List bytes) async { + if (!_isSfnt(bytes)) { + throw const FormatException('downloaded data is not a ttf/otf font'); + } + final loader = FontLoader(family) + ..addFont(Future.value(ByteData.sublistView(bytes))); + await loader.load(); + _loaded.add(family); + } + + static bool _isSfnt(Uint8List b) { + if (b.length < 4) return false; + final tag = (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]; + return tag == 0x00010000 || + tag == 0x4F54544F || + tag == 0x74727565 || + tag == 0x74746366; + } + + static Future _persist(String family) async { + final prefs = await SharedPreferences.getInstance(); + final list = prefs.getStringList(prefKey) ?? []; + if (!list.contains(family)) { + list.add(family); + await prefs.setStringList(prefKey, list); + } + } + + static Future _download(String family) async { + final encoded = Uri.encodeQueryComponent(family); + final variants = [ + 'https://fonts.googleapis.com/css2?family=$encoded:wght@100..900', + 'https://fonts.googleapis.com/css2?family=$encoded', + ]; + final client = HttpClient()..connectionTimeout = const Duration(seconds: 15); + try { + for (final url in variants) { + final css = await _fetchText(client, Uri.parse(url)); + if (css == null) continue; + final ttf = RegExp(r'url\((https://[^)]+\.ttf)\)').firstMatch(css); + final ttfUrl = ttf?.group(1); + if (ttfUrl == null) continue; + final bytes = await _fetchBytes(client, Uri.parse(ttfUrl)); + if (bytes != null && _isSfnt(bytes)) return bytes; + } + return null; + } catch (_) { + return null; + } finally { + client.close(force: true); + } + } + + static Future _fetchText(HttpClient client, Uri uri) async { + final req = await client.getUrl(uri); + req.headers.set(HttpHeaders.userAgentHeader, _userAgent); + final resp = await req.close(); + if (resp.statusCode != HttpStatus.ok) { + await resp.drain(); + return null; + } + return resp.transform(const Utf8Decoder()).join(); + } + + static Future _fetchBytes(HttpClient client, Uri uri) async { + final req = await client.getUrl(uri); + req.headers.set(HttpHeaders.userAgentHeader, _userAgent); + final resp = await req.close(); + if (resp.statusCode != HttpStatus.ok) { + await resp.drain(); + return null; + } + final builder = BytesBuilder(copy: false); + await for (final chunk in resp) { + builder.add(chunk); + } + return builder.takeBytes(); + } +} diff --git a/lib/frontend/screens/auth/code_confirmation_screen.dart b/lib/frontend/screens/auth/code_confirmation_screen.dart index c8dbb99..fe64747 100644 --- a/lib/frontend/screens/auth/code_confirmation_screen.dart +++ b/lib/frontend/screens/auth/code_confirmation_screen.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:komet/l10n/app_localizations.dart'; import 'package:flutter/services.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'password_2fa_screen.dart'; import 'registration_screen.dart'; import '../../../main.dart'; @@ -234,7 +233,7 @@ class _CodeConfirmationScreenState extends State const SizedBox(height: 16), Text( widget.phoneNumber, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 22, fontWeight: FontWeight.w500, diff --git a/lib/frontend/screens/auth/login_screen.dart b/lib/frontend/screens/auth/login_screen.dart index ad18758..4dd45b9 100644 --- a/lib/frontend/screens/auth/login_screen.dart +++ b/lib/frontend/screens/auth/login_screen.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:komet/core/config/countries.dart'; import 'package:komet/l10n/app_localizations.dart'; @@ -172,7 +171,7 @@ class _LoginScreenState extends State { padding: const EdgeInsets.only(left: 8, bottom: 8), child: Text( l10n.loginLanguage, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w600, @@ -182,7 +181,7 @@ class _LoginScreenState extends State { ListTile( title: Text( l10n.languageNameRu, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, @@ -198,7 +197,7 @@ class _LoginScreenState extends State { ListTile( title: Text( l10n.languageNameEn, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, @@ -249,7 +248,7 @@ class _LoginScreenState extends State { children: [ Text( AppLocalizations.of(context)!.loginTermsOfUse, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w500, @@ -420,7 +419,7 @@ class _LoginScreenState extends State { children: [ Text( l10n.loginConfirmPhoneTitle, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurfaceVariant, fontSize: 14, fontWeight: FontWeight.w500, @@ -429,7 +428,7 @@ class _LoginScreenState extends State { const SizedBox(height: 12), Text( '${_selectedCountry.phoneCode} $formattedPhone', - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w500, @@ -445,7 +444,7 @@ class _LoginScreenState extends State { onPressed: () => Navigator.pop(context), child: Text( l10n.loginEdit, - style: GoogleFonts.inter( + style: TextStyle( color: cs.primary, fontSize: 15, fontWeight: FontWeight.w500, @@ -482,7 +481,7 @@ class _LoginScreenState extends State { }, child: Text( l10n.loginDone, - style: GoogleFonts.inter( + style: TextStyle( color: cs.primary, fontSize: 15, fontWeight: FontWeight.w600, @@ -558,7 +557,7 @@ class _LoginScreenState extends State { leading: Icon(Symbols.security, color: cs.onSurface), title: Text( l10n.loginSpoofRedacted, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, @@ -578,7 +577,7 @@ class _LoginScreenState extends State { leading: Icon(Symbols.vpn_lock, color: cs.onSurface), title: Text( l10n.loginProxy, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, @@ -593,7 +592,7 @@ class _LoginScreenState extends State { leading: Icon(Symbols.dns, color: cs.onSurface), title: Text( l10n.loginChangeServer, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, @@ -633,7 +632,7 @@ class _LoginScreenState extends State { leading: Icon(Symbols.qr_code_2, color: cs.onSurface), title: Text( l10n.loginSignInWithQr, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, @@ -647,7 +646,7 @@ class _LoginScreenState extends State { leading: Icon(Symbols.key, color: cs.onSurface), title: Text( l10n.loginSignInWithToken, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, @@ -669,7 +668,7 @@ class _LoginScreenState extends State { leading: Icon(Symbols.description, color: cs.onSurface), title: Text( l10n.loginSignInWithSessionFile, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, @@ -771,7 +770,7 @@ class _LoginScreenState extends State { const SizedBox(height: 16), Text( l10n.loginTitle, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 32, fontWeight: FontWeight.w500, @@ -801,7 +800,7 @@ class _LoginScreenState extends State { children: [ Text( _countryDisplayName(_selectedCountry), - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w400, @@ -823,7 +822,7 @@ class _LoginScreenState extends State { children: [ Text( _selectedCountry.phoneCode, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w400, @@ -905,7 +904,7 @@ class _LoginScreenState extends State { onPressed: () => _showOtherLoginMethods(context), child: Text( l10n.loginOtherSignInMethods, - style: GoogleFonts.inter( + style: TextStyle( color: cs.primary, fontSize: 14, fontWeight: FontWeight.w400, @@ -922,7 +921,7 @@ class _LoginScreenState extends State { padding: const EdgeInsets.only(bottom: 24.0), child: RichText( text: TextSpan( - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w500, @@ -931,7 +930,7 @@ class _LoginScreenState extends State { children: [ TextSpan( text: l10n.loginTermsIntro, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 14, height: 1.4, @@ -940,7 +939,7 @@ class _LoginScreenState extends State { ), TextSpan( text: l10n.loginTermsLink, - style: GoogleFonts.inter( + style: TextStyle( color: cs.primary, fontSize: 14, height: 1.4, @@ -1016,7 +1015,7 @@ class _LoginScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( label, - style: GoogleFonts.inter( + style: TextStyle( color: cs.primary, fontSize: 14, fontWeight: FontWeight.w400, diff --git a/lib/frontend/screens/auth/password_2fa_screen.dart b/lib/frontend/screens/auth/password_2fa_screen.dart index f64b970..6e1d1d6 100644 --- a/lib/frontend/screens/auth/password_2fa_screen.dart +++ b/lib/frontend/screens/auth/password_2fa_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import '../../../main.dart'; import '../../widgets/custom_notification.dart'; import '../../widgets/login_success_screen.dart'; @@ -94,7 +93,7 @@ class _Password2FAScreenState extends State { const SizedBox(height: 16), Text( 'Двухфакторная аутентификация', - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 22, fontWeight: FontWeight.w500, diff --git a/lib/frontend/screens/auth/proxy_settings_sheet.dart b/lib/frontend/screens/auth/proxy_settings_sheet.dart index 2c20811..2e920b1 100644 --- a/lib/frontend/screens/auth/proxy_settings_sheet.dart +++ b/lib/frontend/screens/auth/proxy_settings_sheet.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:komet/backend/api.dart'; import 'package:komet/core/config/proxy_config.dart'; import 'package:komet/l10n/app_localizations.dart'; @@ -128,7 +127,7 @@ class _ProxySettingsSheetState extends State { ), Text( l10n.proxySettingsTitle, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w600, @@ -230,7 +229,7 @@ class _ProxySettingsSheetState extends State { alignment: Alignment.center, child: Text( labels[type]!, - style: GoogleFonts.inter( + style: TextStyle( color: selected ? cs.onPrimary : cs.onSurfaceVariant, fontSize: 13, fontWeight: selected ? FontWeight.w600 : FontWeight.w500, @@ -258,7 +257,7 @@ class _ProxySettingsSheetState extends State { children: [ Text( label, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurfaceVariant, fontWeight: FontWeight.w500, fontSize: 14, @@ -271,10 +270,10 @@ class _ProxySettingsSheetState extends State { inputFormatters: inputFormatters, enabled: !_busy, obscureText: obscureText, - style: GoogleFonts.inter(color: cs.onSurface, fontSize: 15), + style: TextStyle(color: cs.onSurface, fontSize: 15), decoration: InputDecoration( hintText: hintText, - hintStyle: GoogleFonts.inter( + hintStyle: TextStyle( color: cs.onSurfaceVariant.withValues(alpha: 0.6), fontSize: 15, ), diff --git a/lib/frontend/screens/auth/registration_screen.dart b/lib/frontend/screens/auth/registration_screen.dart index 6347c3b..ee67f55 100644 --- a/lib/frontend/screens/auth/registration_screen.dart +++ b/lib/frontend/screens/auth/registration_screen.dart @@ -1,6 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:komet/l10n/app_localizations.dart'; import '../../../backend/modules/account.dart'; @@ -134,7 +133,7 @@ class _RegistrationScreenState extends State { children: [ Text( l10n.registrationTitle, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 26, fontWeight: FontWeight.w600, @@ -203,7 +202,7 @@ class _RegistrationScreenState extends State { const SizedBox(height: 28), Text( l10n.registrationChooseAvatar, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600, diff --git a/lib/frontend/screens/auth/select_country_screen.dart b/lib/frontend/screens/auth/select_country_screen.dart index bf4bd03..a74dfe0 100644 --- a/lib/frontend/screens/auth/select_country_screen.dart +++ b/lib/frontend/screens/auth/select_country_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:komet/core/config/countries.dart'; import 'package:komet/l10n/app_localizations.dart'; @@ -69,14 +68,14 @@ class _SelectCountryScreenState extends State { ? TextField( controller: _searchController, autofocus: true, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w400, ), decoration: InputDecoration( hintText: l10n.selectCountrySearchHint, - hintStyle: GoogleFonts.inter( + hintStyle: TextStyle( color: cs.onSurfaceVariant, fontSize: 18, fontWeight: FontWeight.w400, @@ -87,7 +86,7 @@ class _SelectCountryScreenState extends State { ) : Text( l10n.selectCountryTitle, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w500, @@ -121,7 +120,7 @@ class _SelectCountryScreenState extends State { return ListTile( leading: Text( country.phoneCode, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurfaceVariant, fontSize: 14, fontWeight: FontWeight.w400, @@ -129,7 +128,7 @@ class _SelectCountryScreenState extends State { ), title: Text( lang == 'ru' ? country.ru : country.en, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, diff --git a/lib/frontend/screens/auth/server_settings_sheet.dart b/lib/frontend/screens/auth/server_settings_sheet.dart index f071f3b..20aad75 100644 --- a/lib/frontend/screens/auth/server_settings_sheet.dart +++ b/lib/frontend/screens/auth/server_settings_sheet.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:komet/backend/api.dart'; import 'package:komet/core/config/config.dart'; import 'package:komet/l10n/app_localizations.dart'; @@ -131,7 +130,7 @@ class _ServerSettingsSheetState extends State { ), Text( l10n.serverSettingsTitle, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w600, @@ -184,7 +183,7 @@ class _ServerSettingsSheetState extends State { children: [ Text( label, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurfaceVariant, fontWeight: FontWeight.w500, fontSize: 14, @@ -196,10 +195,10 @@ class _ServerSettingsSheetState extends State { keyboardType: keyboardType, inputFormatters: inputFormatters, enabled: !_busy, - style: GoogleFonts.inter(color: cs.onSurface, fontSize: 15), + style: TextStyle(color: cs.onSurface, fontSize: 15), decoration: InputDecoration( hintText: hintText, - hintStyle: GoogleFonts.inter( + hintStyle: TextStyle( color: cs.onSurfaceVariant.withValues(alpha: 0.6), fontSize: 15, ), diff --git a/lib/frontend/screens/profile/devices_screen.dart b/lib/frontend/screens/profile/devices_screen.dart index 60ef135..44a05db 100644 --- a/lib/frontend/screens/profile/devices_screen.dart +++ b/lib/frontend/screens/profile/devices_screen.dart @@ -4,7 +4,6 @@ import 'dart:math'; import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../core/utils/format.dart'; import '../../../main.dart' show accountModule; @@ -75,7 +74,7 @@ class _DevicesScreenState extends State backgroundColor: cs.surfaceContainerHigh, title: Text( 'Ссылка из QR', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontWeight: FontWeight.w600, fontSize: 18, color: cs.onSurface, @@ -230,7 +229,7 @@ class _DevicesScreenState extends State ), title: ConnectionTitleText( 'Устройства', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 20, fontWeight: FontWeight.w600, color: cs.onSurface, @@ -280,7 +279,7 @@ class _DevicesScreenState extends State const SizedBox(height: 16), Text( 'Устройства в KOMET', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 18, fontWeight: FontWeight.w700, color: cs.onSurface, @@ -302,7 +301,7 @@ class _DevicesScreenState extends State icon: const Icon(Symbols.qr_code_scanner, size: 22), label: Text( 'Сканировать QR', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 15, fontWeight: FontWeight.w600, ), @@ -473,7 +472,7 @@ class _DevicesScreenState extends State children: [ Text( title, - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 16, fontWeight: FontWeight.w700, color: cs.onSurface, diff --git a/lib/frontend/screens/profile/font_settings_screen.dart b/lib/frontend/screens/profile/font_settings_screen.dart index 84d908e..367df42 100644 --- a/lib/frontend/screens/profile/font_settings_screen.dart +++ b/lib/frontend/screens/profile/font_settings_screen.dart @@ -3,6 +3,7 @@ import 'package:m3e_collection/m3e_collection.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../core/config/app_fonts.dart'; +import '../../../core/config/custom_font_service.dart'; import '../../../core/utils/haptics.dart'; import '../../../main.dart'; import '../../widgets/connection_status.dart'; @@ -18,6 +19,7 @@ class FontSettingsScreen extends StatefulWidget { class _FontSettingsScreenState extends State { List _custom = const []; + bool _adding = false; @override void initState() { @@ -26,7 +28,7 @@ class _FontSettingsScreenState extends State { } Future _reloadCustom() async { - final list = await AppFonts.loadCustomFamilies(); + final list = await CustomFontService.families(); if (mounted) setState(() => _custom = list); } @@ -45,26 +47,26 @@ class _FontSettingsScreenState extends State { } return; } - final canonical = AppFonts.matchGoogleFamily(parsed); - if (canonical == null) { - if (mounted) { - showCustomNotification( - context, - 'Шрифт «$parsed» не найден в Google Fonts', - ); - } + setState(() => _adding = true); + final family = await CustomFontService.addFamily(parsed); + if (!mounted) return; + setState(() => _adding = false); + if (family == null) { + showCustomNotification( + context, + 'Шрифт «$parsed» не найден или нет сети', + ); return; } - await AppFonts.addCustomFamily(canonical); await _reloadCustom(); if (!mounted) return; - KometApp.stateOf(context)?.applyAppFont(AppFonts.customId(canonical)); + KometApp.stateOf(context)?.applyAppFont(AppFonts.customId(family)); Haptics.success(); - showCustomNotification(context, 'Шрифт «$canonical» добавлен'); + showCustomNotification(context, 'Шрифт «$family» добавлен'); } Future _removeFont(String family) async { - await AppFonts.removeCustomFamily(family); + await CustomFontService.removeFamily(family); await _reloadCustom(); if (!mounted) return; final app = KometApp.stateOf(context); @@ -175,11 +177,11 @@ class _FontSettingsScreenState extends State { SizedBox( width: double.infinity, child: ButtonM3E( - onPressed: _showAddFontDialog, + onPressed: _adding ? null : _showAddFontDialog, style: ButtonM3EStyle.outlined, size: ButtonM3ESize.md, - icon: const Icon(Symbols.add), - label: const Text('Добавить шрифт'), + icon: Icon(_adding ? Symbols.hourglass_top : Symbols.add), + label: Text(_adding ? 'Загрузка…' : 'Добавить шрифт'), ), ), const SizedBox(height: 30), diff --git a/lib/frontend/screens/profile/web_qr_scan_screen.dart b/lib/frontend/screens/profile/web_qr_scan_screen.dart index ab893ff..1d1822e 100644 --- a/lib/frontend/screens/profile/web_qr_scan_screen.dart +++ b/lib/frontend/screens/profile/web_qr_scan_screen.dart @@ -3,7 +3,6 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; @@ -72,7 +71,7 @@ class _WebQrScanScreenState extends State { ), title: Text( 'QR для веба и ПК', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 20, fontWeight: FontWeight.w600, color: Colors.white, @@ -134,7 +133,7 @@ class _WebQrScanScreenState extends State { child: Text( 'Наведите камеру на QR-код на экране компьютера', textAlign: TextAlign.center, - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 15, fontWeight: FontWeight.w500, color: Colors.white, diff --git a/lib/frontend/widgets/custom_notification.dart b/lib/frontend/widgets/custom_notification.dart index 786d3ec..9e1e760 100644 --- a/lib/frontend/widgets/custom_notification.dart +++ b/lib/frontend/widgets/custom_notification.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; const Duration _defaultNotificationDuration = Duration(milliseconds: 2600); @@ -90,7 +89,7 @@ class _CustomNotificationState extends State ), child: Text( widget.message, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w500, diff --git a/lib/frontend/widgets/info_action_sheet.dart b/lib/frontend/widgets/info_action_sheet.dart index d6c6dce..494cf02 100644 --- a/lib/frontend/widgets/info_action_sheet.dart +++ b/lib/frontend/widgets/info_action_sheet.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:shared_preferences/shared_preferences.dart'; class InfoActionSheetItem { @@ -150,7 +149,7 @@ class _InfoActionSheetState extends State<_InfoActionSheet> { Text( widget.title, textAlign: TextAlign.center, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurface, fontSize: 22, fontWeight: FontWeight.w700, @@ -161,7 +160,7 @@ class _InfoActionSheetState extends State<_InfoActionSheet> { Text( widget.subtitle!, textAlign: TextAlign.center, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurfaceVariant, fontSize: 13, fontWeight: FontWeight.w400, @@ -195,7 +194,7 @@ class _InfoActionSheetState extends State<_InfoActionSheet> { ), child: Text( buttonText, - style: GoogleFonts.inter( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), @@ -248,7 +247,7 @@ class _InfoActionSheetState extends State<_InfoActionSheet> { children: [ Text( item.title, - style: GoogleFonts.inter( + style: TextStyle( color: titleColor, fontSize: 16, fontWeight: FontWeight.w700, @@ -258,7 +257,7 @@ class _InfoActionSheetState extends State<_InfoActionSheet> { const SizedBox(height: 4), Text( item.body, - style: GoogleFonts.inter( + style: TextStyle( color: cs.onSurfaceVariant, fontSize: 14, fontWeight: FontWeight.w400, diff --git a/lib/frontend/widgets/web_qr_login.dart b/lib/frontend/widgets/web_qr_login.dart index 3972632..3f763ab 100644 --- a/lib/frontend/widgets/web_qr_login.dart +++ b/lib/frontend/widgets/web_qr_login.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import '../../main.dart' show accountModule; import 'custom_notification.dart'; @@ -25,7 +24,7 @@ Future showWebQrLoginConfirmSheet(BuildContext context) async { const SizedBox(height: 20), Text( 'Вход по QR', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 20, fontWeight: FontWeight.w700, color: cs.onSurface, diff --git a/lib/main.dart b/lib/main.dart index b6291ac..6a79473 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,6 +22,7 @@ import 'core/config/komet_settings.dart'; import 'core/config/app_bubble_shape.dart'; import 'core/config/app_cache_extent.dart'; import 'core/config/app_fonts.dart'; +import 'core/config/custom_font_service.dart'; import 'core/config/app_message_actions_style.dart'; import 'core/config/app_swipe_back_desktop.dart'; import 'core/config/app_pranks.dart'; @@ -147,6 +148,11 @@ void main() async { final initialFontScale = AppFonts.clampScale( prefs.getDouble(AppFonts.scalePrefKey) ?? AppFonts.defaultScale, ); + if (AppFonts.resolve(initialFontId).isCustom) { + await CustomFontService.preloadCached(); + } else { + unawaited(CustomFontService.preloadCached()); + } final initialAccentSeed = await accentFuture; AppBubbleShape.current.value = await bubbleShapeFuture; AppBubbleBehavior.current.value = await bubbleBehaviorFuture; diff --git a/pubspec.lock b/pubspec.lock index 69e9882..d29aaa7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -121,46 +121,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" - camera: - dependency: "direct main" - description: - name: camera - sha256: "4142a19a38e388d3bab444227636610ba88982e36dff4552d5191a86f65dc437" - url: "https://pub.dev" - source: hosted - version: "0.11.4" - camera_android_camerax: - dependency: transitive - description: - name: camera_android_camerax - sha256: "8516fe308bc341a5067fb1a48edff0ddfa57c0d3cdcc9dbe7ceca3ba119e2577" - url: "https://pub.dev" - source: hosted - version: "0.6.30" - camera_avfoundation: - dependency: transitive - description: - name: camera_avfoundation - sha256: "11b4aee2f5e5e038982e152b4a342c749b414aa27857899d20f4323e94cb5f0b" - url: "https://pub.dev" - source: hosted - version: "0.9.23+2" - camera_platform_interface: - dependency: transitive - description: - name: camera_platform_interface - sha256: "7ac852d77699acee79f0d438b793feee26721841e50973576419ff5c6d95e9b7" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - camera_web: - dependency: transitive - description: - name: camera_web - sha256: "1245a480a113437f8d46d19c0fb90cea9db921436d9cf2ba5fb11854a1312693" - url: "https://pub.dev" - source: hosted - version: "0.3.5+4" characters: dependency: transitive description: @@ -669,14 +629,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" - google_fonts: - dependency: "direct main" - description: - name: google_fonts - sha256: "4e9391085e524954a51e3625b7c9c7e9851dc3f376603208bb45c24b9a66255d" - url: "https://pub.dev" - source: hosted - version: "8.1.0" gtk: dependency: transitive description: @@ -893,24 +845,8 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.6" - media_kit_libs_android_video: - dependency: transitive - description: - name: media_kit_libs_android_video - sha256: "3f6274e5ab2de512c286a25c327288601ee445ed8ac319e0ef0b66148bd8f76c" - url: "https://pub.dev" - source: hosted - version: "1.3.8" - media_kit_libs_ios_video: - dependency: transitive - description: - name: media_kit_libs_ios_video - sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991 - url: "https://pub.dev" - source: hosted - version: "1.1.4" media_kit_libs_linux: - dependency: transitive + dependency: "direct main" description: name: media_kit_libs_linux sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf" @@ -918,23 +854,15 @@ packages: source: hosted version: "1.2.1" media_kit_libs_macos_video: - dependency: transitive + dependency: "direct main" description: name: media_kit_libs_macos_video sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d url: "https://pub.dev" source: hosted version: "1.1.4" - media_kit_libs_video: - dependency: "direct main" - description: - name: media_kit_libs_video - sha256: "2b235b5dac79c6020e01eef5022c6cc85fedc0df1738aadc6ea489daa12a92a9" - url: "https://pub.dev" - source: hosted - version: "1.0.7" media_kit_libs_windows_video: - dependency: transitive + dependency: "direct main" description: name: media_kit_libs_windows_video sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab @@ -1434,14 +1362,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" - source: hosted - version: "2.1.1" string_scanner: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5870329..03a36e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,6 @@ dependencies: sqflite: ^2.4.2 sqflite_common_ffi: ^2.4.0+2 path: ^1.9.1 - google_fonts: ^8.1.0 m3e_collection: ^0.3.7 material_symbols_icons: ^4.2906.0 dynamic_color: ^1.8.1 @@ -74,9 +73,10 @@ dependencies: opus_dart: ^3.0.1 opus_flutter_android: ^3.0.1 ogg_opus_player: ^0.8.0 - camera: ^0.11.0 video_player_media_kit: ^2.0.0 - media_kit_libs_video: ^1.0.7 + media_kit_libs_windows_video: ^1.0.11 + media_kit_libs_linux: ^1.2.1 + media_kit_libs_macos_video: ^1.1.4 dev_dependencies: flutter_test: @@ -122,6 +122,17 @@ flutter: - assets/komet_icon.png - assets/meteor_icon.png + fonts: + - family: Inter + fonts: + - asset: assets/fonts/Inter.ttf + - family: Outfit + fonts: + - asset: assets/fonts/Outfit.ttf + - family: Unbounded + fonts: + - asset: assets/fonts/Unbounded.ttf + # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg