fix/feat: мелкие фиксы, и доп. инфа в info

This commit is contained in:
Jganenokk
2026-08-08 23:55:10 +07:00
parent 474923328b
commit 45297e4aff
92 changed files with 2407 additions and 1195 deletions
+2 -110
View File
@@ -11,6 +11,7 @@ import '../../core/storage/app_database.dart';
import '../../core/storage/spoofing_service.dart';
import '../../core/storage/token_storage.dart';
import '../../core/utils/logger.dart';
import '../../models/login_info.dart';
import 'banners.dart';
import 'chats.dart';
import 'complaints.dart';
@@ -693,41 +694,12 @@ class AccountModule {
}
Future<void> _saveLoginInfo(Map<dynamic, dynamic> data, int accountId) async {
final contact = data['profile']?['contact'] as Map?;
final videoChatHistory = data['videoChatHistory'];
final chats = data['chats'] as List?;
final config = data['config'] as Map?;
final serverConfig = config?['server'] as Map?;
final userConfig = config?['user'] as Map?;
if (serverConfig != null) {
await _persistEntryBannerApps(accountId, serverConfig);
}
final yMap = serverConfig?['y-map'] as Map?;
final whiteListLinks = serverConfig?['white-list-links'] as List?;
final fileUploadUnsupported =
serverConfig?['file-upload-unsupported-types'] as List?;
final time = data['time'] as int?;
final info = {
'registrationTime': contact?['registrationTime'],
'country': contact?['country'],
'videoChatHistory': videoChatHistory,
'updateTime': contact?['updateTime'],
'id': contact?['id'],
'chatMarker': chats != null && chats.isNotEmpty
? _extractChatMarker(chats.cast<Map>())
: null,
'time': time,
'server': serverConfig != null
? _extractServerInfo(
serverConfig,
yMap,
whiteListLinks,
fileUploadUnsupported,
)
: null,
'user': userConfig != null ? _extractUserConfig(userConfig) : null,
};
final info = LoginInfo.fromPayload(data);
await AppDatabase.saveLoginInfo(accountId, jsonEncode(info));
}
@@ -760,86 +732,6 @@ class AccountModule {
}
}
Map<String, dynamic> _extractChatMarker(List<Map> chats) {
int? latestTime;
for (final chat in chats) {
final lastEventTime = chat['lastEventTime'] as int?;
if (lastEventTime != null &&
(latestTime == null || lastEventTime > latestTime)) {
latestTime = lastEventTime;
}
}
return {'chatMarker': latestTime};
}
Map<String, dynamic> _extractServerInfo(
Map serverConfig,
Map? yMap,
List? whiteListLinks,
List? fileUploadUnsupported,
) {
return {
'account-removal-enabled': serverConfig['account-removal-enabled'],
'image-size': serverConfig['image-size'],
'gce': serverConfig['gce'],
'gcce': serverConfig['gcce'],
'max-msg-length': serverConfig['max-msg-length'],
'quotes-enabled': serverConfig['quotes-enabled'],
'calls-endpoint': serverConfig['calls-endpoint'],
'send-location-enabled': serverConfig['send-location-enabled'],
'lgce': serverConfig['lgce'],
'wud': serverConfig['wud'],
'video-msg-enabled': serverConfig['video-msg-enabled'],
'grse': serverConfig['grse'],
'edit-timeout': serverConfig['edit-timeout'],
'image-quality': serverConfig['image-quality'],
'unsafe-files-alert': serverConfig['unsafe-files-alert'],
'account-nickname-enabled': serverConfig['account-nickname-enabled'],
'mentions_entity_names_limit':
serverConfig['mentions_entity_names_limit'],
'reactions-enabled': serverConfig['reactions-enabled'],
'y-map': yMap != null
? {
'tile': yMap['tile'],
'geocoder': yMap['geocoder'],
'static': yMap['static'],
}
: null,
'white-list-links': whiteListLinks,
'file-upload-unsupported-types': fileUploadUnsupported,
};
}
Map<String, dynamic> _extractUserConfig(Map userConfig) {
return {
'CHATS_PUSH_NOTIFICATION': userConfig['CHATS_PUSH_NOTIFICATION'],
'PUSH_DETAILS': userConfig['PUSH_DETAILS'],
'PUSH_SOUND': userConfig['PUSH_SOUND'],
'PHONE_NUMBER_PRIVACY': userConfig['PHONE_NUMBER_PRIVACY'],
'INACTIVE_TTL': userConfig['INACTIVE_TTL'],
'SHOW_READ_MARK': userConfig['SHOW_READ_MARK'],
'AUDIO_TRANSCRIPTION_ENABLED': userConfig['AUDIO_TRANSCRIPTION_ENABLED'],
'SEARCH_BY_PHONE': userConfig['SEARCH_BY_PHONE'],
'INCOMING_CALL': userConfig['INCOMING_CALL'],
'DOUBLE_TAP_REACTION_DISABLED':
userConfig['DOUBLE_TAP_REACTION_DISABLED'],
'SAFE_MODE_NO_PIN': userConfig['SAFE_MODE_NO_PIN'],
'CHATS_PUSH_SOUND': userConfig['CHATS_PUSH_SOUND'],
'DOUBLE_TAP_REACTION_VALUE': userConfig['DOUBLE_TAP_REACTION_VALUE'],
'FAMILY_PROTECTION': userConfig['FAMILY_PROTECTION'],
'HIDDEN': userConfig['HIDDEN'],
'CHATS_INVITE': userConfig['CHATS_INVITE'],
'PUSH_NEW_CONTACTS': userConfig['PUSH_NEW_CONTACTS'],
'UNSAFE_FILES': userConfig['UNSAFE_FILES'],
'DONT_DISTURB_UNTIL': userConfig['DONT_DISTURB_UNTIL'],
'ALT_KEYBOARD': userConfig['ALT_KEYBOARD'],
'CONTENT_LEVEL_ACCESS': userConfig['CONTENT_LEVEL_ACCESS'],
'STICKERS_SUGGEST': userConfig['STICKERS_SUGGEST'],
'SAFE_MODE': userConfig['SAFE_MODE'],
'M_CALL_PUSH_NOTIFICATION': userConfig['M_CALL_PUSH_NOTIFICATION'],
};
}
Future<RequestCodeResult> _requestCodeInternal(
String phone,
AuthRequestType type,
+16 -3
View File
@@ -146,6 +146,12 @@ class CachedChat {
return iAmAdmin(myId) || options.contains('ALL_CAN_PIN_MESSAGE');
}
bool get forwardDisabled => options.contains('DISABLE_FORWARD');
bool get copyDisabled => options.contains('MESSAGE_COPY_NOT_ALLOWED');
bool get confirmBeforeSend => options.contains('CONFIRM_BEFORE_SEND');
bool get isMuted {
if (dontDisturbUntil == ChatsModule.muteOff) return false;
if (dontDisturbUntil < 0) return true;
@@ -502,7 +508,10 @@ class ChatsModule {
if (cached.unreadCount == next && nextMark == currentMark) return;
final participants = Map<int, int>.from(cached.participants)
..[accountId] = nextMark;
final updated = cached.copyWith(unreadCount: next, participants: participants);
final updated = cached.copyWith(
unreadCount: next,
participants: participants,
);
await AppDatabase.saveChats([updated.toDbRow()]);
_bump();
}
@@ -1571,7 +1580,9 @@ class ChatsModule {
};
final packet = await api.sendRequest(Opcode.msgSend, payload);
if (!packet.isOk) {
logger.w('_createChat($chatType): server error payload=${packet.payload}');
logger.w(
'_createChat($chatType): server error payload=${packet.payload}',
);
return null;
}
final data = packet.payload;
@@ -1882,7 +1893,9 @@ class ChatsModule {
'operation': 'add',
});
if (!packet.isOk) {
logger.w('addMembers $chatId: ${messageFromErrorPayload(packet.payload)}');
logger.w(
'addMembers $chatId: ${messageFromErrorPayload(packet.payload)}',
);
return false;
}
final data = packet.payload;
+18 -2
View File
@@ -1,5 +1,6 @@
import 'dart:async';
import '../../core/protocol/packet.dart';
import '../../core/storage/app_database.dart';
import '../../core/storage/token_storage.dart';
import '../../core/utils/logger.dart';
@@ -88,8 +89,23 @@ class OutboxService {
elements: elements.isEmpty ? null : elements,
);
} catch (e) {
logger.w('Outbox: отправка ${pending.id} не удалась: $e');
continue;
if (!isPermanentSendFailure(e)) {
logger.w('Outbox: отправка ${pending.id} не удалась: $e');
continue;
}
logger.w('Outbox: ${pending.id} отклонено сервером: $e');
final failed = pending.copyWith(status: 'error');
await AppDatabase.saveMessages([failed.toDbRow()]);
chats.emitMessageSent(pending.chatId, pending.id, failed);
await chats.applyOutgoing(
accountId,
pending.chatId,
messageId: failed.id,
time: failed.time,
text: text,
status: 'error',
elements: elements.isEmpty ? null : elements,
);
}
}
} catch (e) {
+21 -2
View File
@@ -6,6 +6,25 @@ extension AppColorTokens on ColorScheme {
const int kAvatarThumbSize = 144;
const Color kSuccessGreen = Color(0xFF2EC36B);
const Color kDangerRed = Color(0xFFE5484D);
const Color kReadReceiptBlue = Color(0xFF4FC3F7);
const Color kOnlineGreen = Color(0xFF34C759);
const Color kEditorAccent = Color(0xFF2F8FFF);
class MediaAccent {
static Color? _seed;
static ColorScheme? _scheme;
static ColorScheme schemeOf(BuildContext context) {
final seed = Theme.of(context).colorScheme.primary;
if (_seed != seed || _scheme == null) {
_seed = seed;
_scheme = ColorScheme.fromSeed(
seedColor: seed,
brightness: Brightness.dark,
);
}
return _scheme!;
}
static Color of(BuildContext context) => schemeOf(context).primary;
}
+12 -3
View File
@@ -1,15 +1,24 @@
import 'package:flutter/foundation.dart';
import 'app_visual_style.dart';
import 'persisted_setting.dart';
enum ComposerStyle { glossy, materialYou }
enum ComposerStyle { auto, glossy, materialYou }
class ComposerChrome {
static bool isGlossy(ComposerStyle style) => switch (style) {
ComposerStyle.auto => AppVisualStyle.current.value.glossyChrome,
ComposerStyle.glossy => true,
ComposerStyle.materialYou => false,
};
}
class AppComposerStyle {
static const prefKey = 'app_composer_style';
static final _setting = PersistedEnum<ComposerStyle>(
prefKey: prefKey,
defaultValue: ComposerStyle.materialYou,
defaultValue: ComposerStyle.auto,
encode: (value) => value.name,
decode: _parse,
);
@@ -21,5 +30,5 @@ class AppComposerStyle {
static Future<void> save(ComposerStyle value) => _setting.save(value);
static ComposerStyle _parse(String? val) =>
enumFromName(ComposerStyle.values, val, ComposerStyle.materialYou);
enumFromName(ComposerStyle.values, val, ComposerStyle.auto);
}
+25
View File
@@ -1,5 +1,25 @@
import 'package:flutter/material.dart';
const String kDisplayFontFamily = 'Outfit';
@immutable
class AppDisplayFont extends ThemeExtension<AppDisplayFont> {
final String? family;
const AppDisplayFont(this.family);
@override
AppDisplayFont copyWith({String? family}) =>
AppDisplayFont(family ?? this.family);
@override
AppDisplayFont lerp(ThemeExtension<AppDisplayFont>? other, double t) =>
t < 0.5 ? this : (other as AppDisplayFont? ?? this);
}
String? displayFontOf(BuildContext context) =>
Theme.of(context).extension<AppDisplayFont>()?.family ?? kDisplayFontFamily;
class AppFont {
final String id;
final String label;
@@ -38,6 +58,11 @@ class AppFonts {
return builtIn.firstWhere((f) => f.id == id, orElse: () => fallback);
}
static String? displayFamily(String id) {
final font = resolve(id);
return font.isSystem ? kDisplayFontFamily : font.fontFamily;
}
static TextTheme textTheme(String id, TextTheme base) {
final family = resolve(id).fontFamily;
if (family == null) return base;
+6
View File
@@ -3,14 +3,20 @@ import 'package:flutter/material.dart';
class AppFrost {
static const double sigma = 34;
static const double panelSigma = 24;
static const double overlaySigma = 18;
static const double mediaBackdropSigma = 30;
static const double glassAlpha = 0.28;
static const double blurPanelAlpha = 0.55;
static const double scrimAlpha = 0.4;
static Color glassTint(ColorScheme cs, [double alpha = glassAlpha]) =>
cs.surfaceContainerHigh.withValues(alpha: alpha);
static Color blurPanelTint(ColorScheme cs) => glassTint(cs, blurPanelAlpha);
static Color scrim([double alpha = scrimAlpha]) =>
Colors.black.withValues(alpha: alpha);
static BorderSide hairline(ColorScheme cs) =>
BorderSide(color: cs.outlineVariant.withValues(alpha: 0.4), width: 0.5);
}
+18 -8
View File
@@ -1,17 +1,27 @@
import 'package:flutter/foundation.dart';
import '../../frontend/widgets/liquid_glass.dart';
import 'app_visual_style.dart';
import 'persisted_setting.dart';
enum NavPillStyle { glossy, frostBlur, liquidGlass }
enum NavPillStyle { auto, glossy, frostBlur, liquidGlass }
class NavPillMaterial {
static bool isLiquid(NavPillStyle style) =>
style == NavPillStyle.liquidGlass && LiquidGlass.isSupported;
static NavPillStyle resolve(NavPillStyle style) {
if (style != NavPillStyle.auto) return style;
return AppVisualStyle.current.value == VisualStyle.liquidGlass
? NavPillStyle.liquidGlass
: NavPillStyle.glossy;
}
static bool isFrost(NavPillStyle style) =>
style == NavPillStyle.frostBlur ||
(style == NavPillStyle.liquidGlass && !LiquidGlass.isSupported);
static bool isLiquid(NavPillStyle style) =>
resolve(style) == NavPillStyle.liquidGlass && LiquidGlass.isSupported;
static bool isFrost(NavPillStyle style) {
final resolved = resolve(style);
return resolved == NavPillStyle.frostBlur ||
(resolved == NavPillStyle.liquidGlass && !LiquidGlass.isSupported);
}
}
class AppNavPillStyle {
@@ -19,7 +29,7 @@ class AppNavPillStyle {
static final _setting = PersistedEnum<NavPillStyle>(
prefKey: prefKey,
defaultValue: NavPillStyle.frostBlur,
defaultValue: NavPillStyle.auto,
encode: (value) => value.name,
decode: _parse,
);
@@ -31,5 +41,5 @@ class AppNavPillStyle {
static Future<void> save(NavPillStyle value) => _setting.save(value);
static NavPillStyle _parse(String? val) =>
enumFromName(NavPillStyle.values, val, NavPillStyle.frostBlur);
enumFromName(NavPillStyle.values, val, NavPillStyle.auto);
}
+29
View File
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
class AppShape {
static const double card = 20;
static const double button = 14;
static const double sheet = 24;
static const double dialog = 24;
static const double pill = 100;
static const BorderRadius cardRadius = BorderRadius.all(
Radius.circular(card),
);
static const BorderRadius buttonRadius = BorderRadius.all(
Radius.circular(button),
);
static const BorderRadius pillRadius = BorderRadius.all(
Radius.circular(pill),
);
static const RoundedRectangleBorder buttonBorder = RoundedRectangleBorder(
borderRadius: buttonRadius,
);
static const RoundedRectangleBorder dialogBorder = RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(dialog)),
);
static const RoundedRectangleBorder sheetBorder = RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(sheet)),
);
}
+6
View File
@@ -50,6 +50,12 @@ class SessionExpiredException extends PacketError {
const SessionExpiredException(super.message);
}
bool isPermanentSendFailure(Object error) {
if (error is! PacketError) return false;
if (error is SessionExpiredException) return false;
return !(error.errorKey?.contains('not.ready') ?? false);
}
String messageFromErrorPayload(dynamic payload) {
if (payload is Map) {
final msg = payload['message'];
+2 -1
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../core/config/app_fonts.dart';
class DebugHeaderSection extends StatelessWidget {
const DebugHeaderSection({super.key});
@@ -28,7 +29,7 @@ class DebugHeaderSection extends StatelessWidget {
color: cs.onSurface,
fontSize: 20,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
+2 -3
View File
@@ -7,6 +7,7 @@ import '../../core/protocol/packet.dart';
import '../../main.dart';
import '../widgets/glossy_pill.dart';
import '../widgets/small_spinner.dart';
import '../../core/config/app_shape.dart';
class DebugSyncProbeSection extends StatefulWidget {
const DebugSyncProbeSection({super.key});
@@ -143,9 +144,7 @@ class _DebugSyncProbeSectionState extends State<DebugSyncProbeSection> {
onPressed: _loading ? null : _send,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
shape: AppShape.buttonBorder,
),
child: _loading
? const SmallSpinner(size: 20)
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:komet/l10n/app_localizations.dart';
import 'package:flutter/services.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'password_2fa_screen.dart';
import 'registration_screen.dart';
import 'session_stale_recovery.dart';
@@ -314,7 +315,7 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: cs.onSurfaceVariant),
icon: Icon(Symbols.arrow_back, color: cs.onSurfaceVariant),
onPressed: () => Navigator.pop(context),
),
),
@@ -528,7 +529,7 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
child: recovering
? SmallSpinner(size: 24, color: cs.onPrimaryContainer)
: Icon(
Icons.arrow_forward,
Symbols.arrow_forward,
color: _codeController.text.length == 6
? cs.onPrimaryContainer
: cs.onSurfaceVariant,
+6 -7
View File
@@ -23,6 +23,8 @@ import '../../widgets/small_spinner.dart';
import '../../../backend/api.dart';
import '../../../core/protocol/packet.dart';
import '../../../main.dart';
import '../../../core/config/app_frost.dart';
import '../../../core/config/app_shape.dart';
class LoginScreen extends StatefulWidget {
final int? returnToAccountId;
@@ -409,7 +411,7 @@ class _LoginScreenState extends State<LoginScreen> {
context: screenContext,
barrierDismissible: true,
barrierLabel: '',
barrierColor: Colors.black54,
barrierColor: AppFrost.scrim(),
transitionDuration: const Duration(milliseconds: 250),
pageBuilder: (context, anim1, anim2) => const SizedBox.shrink(),
transitionBuilder: (context, anim1, anim2, child) {
@@ -424,9 +426,7 @@ class _LoginScreenState extends State<LoginScreen> {
child: AlertDialog(
backgroundColor: cs.surfaceContainerHigh,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
shape: AppShape.dialogBorder,
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
actionsPadding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
content: Column(
@@ -840,7 +840,7 @@ class _LoginScreenState extends State<LoginScreen> {
),
const Spacer(),
Icon(
Icons.keyboard_arrow_down,
Symbols.keyboard_arrow_down,
color: cs.onSurfaceVariant,
),
],
@@ -1005,7 +1005,7 @@ class _LoginScreenState extends State<LoginScreen> {
color: cs.onPrimaryContainer,
)
: Icon(
Icons.arrow_forward,
Symbols.arrow_forward,
color: _isPhoneValid
? cs.onPrimaryContainer
: cs.onSurfaceVariant,
@@ -1068,4 +1068,3 @@ class _LoginScreenState extends State<LoginScreen> {
);
}
}
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../core/protocol/packet.dart';
import '../../../main.dart';
import '../../widgets/animated_slash_icon.dart';
@@ -117,7 +118,7 @@ class _Password2FAScreenState extends State<Password2FAScreen>
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: cs.onSurfaceVariant),
icon: Icon(Symbols.arrow_back, color: cs.onSurfaceVariant),
onPressed: () => Navigator.pop(context),
),
),
@@ -173,8 +174,8 @@ class _Password2FAScreenState extends State<Password2FAScreen>
),
suffixIcon: IconButton(
icon: AnimatedSlashIcon(
icon: Icons.visibility,
slashedIcon: Icons.visibility_off,
icon: Symbols.visibility,
slashedIcon: Symbols.visibility_off,
slashed: _isPasswordVisible,
color: cs.onSurfaceVariant,
),
@@ -203,7 +204,7 @@ class _Password2FAScreenState extends State<Password2FAScreen>
child: _isLoading
? SmallSpinner(size: 24, color: cs.onPrimaryContainer)
: Icon(
Icons.arrow_forward,
Symbols.arrow_forward,
color: _passwordController.text.isNotEmpty
? cs.onPrimaryContainer
: cs.onSurfaceVariant,
@@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:komet/l10n/app_localizations.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../backend/modules/account.dart';
import '../../../main.dart';
@@ -99,7 +100,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: cs.onSurfaceVariant),
icon: Icon(Symbols.arrow_back, color: cs.onSurfaceVariant),
onPressed: _isSubmitting ? null : () => Navigator.pop(context),
),
),
@@ -113,7 +114,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
child: _isSubmitting
? SmallSpinner(size: 22, color: cs.onSurfaceVariant)
: Icon(
Icons.arrow_forward,
Symbols.arrow_forward,
color: _canSubmit ? cs.onPrimaryContainer : cs.onSurfaceVariant,
),
),
@@ -9,6 +9,7 @@ import '../../widgets/adaptive_shell.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/section_header.dart';
import '../../widgets/small_spinner.dart';
import '../../../core/config/app_shape.dart';
class TokenLoginScreen extends StatefulWidget {
final int? returnToAccountId;
@@ -141,7 +142,7 @@ class _TokenLoginScreenState extends State<TokenLoginScreen> {
onPressed: _isLoading ? null : _login,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
shape: const StadiumBorder(),
shape: AppShape.buttonBorder,
),
child: _isLoading
? const SmallSpinner(size: 22)
@@ -346,7 +347,7 @@ class _TokenLoginScreenState extends State<TokenLoginScreen> {
return ChoiceChip(
label: Text(opt.label),
avatar: isSelected
? Icon(Icons.check, size: 18, color: cs.onSecondaryContainer)
? Icon(Symbols.check, size: 18, color: cs.onSecondaryContainer)
: Icon(opt.icon, size: 18, color: cs.onSurfaceVariant),
selected: isSelected,
showCheckmark: false,
@@ -9,6 +9,7 @@ import 'package:komet/frontend/widgets/custom_notification.dart';
import 'package:komet/frontend/widgets/small_spinner.dart';
import 'package:komet/l10n/app_localizations.dart';
import 'package:komet/main.dart' show messagesModule;
import '../../../core/config/app_shape.dart';
Future<bool> showCreatedCallSheet(
BuildContext context, {
@@ -196,9 +197,7 @@ class _CreatedCallCardState extends State<_CreatedCallCard> {
child: FilledButton(
onPressed: () => Navigator.of(context).pop(true),
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
shape: AppShape.buttonBorder,
),
child: Text(
l10n.callLinkStart,
@@ -10,6 +10,7 @@ import '../../widgets/custom_notification.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/prompt_dialog.dart';
import '../../widgets/sheet_helpers.dart';
import '../../../core/config/app_fonts.dart';
class CallParticipantView {
final String name;
@@ -118,7 +119,7 @@ class _ParticipantsSheetState extends State<_ParticipantsSheet> {
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -358,7 +359,7 @@ class _ParticipantsSheetState extends State<_ParticipantsSheet> {
color: cs.onSurface,
fontSize: 20,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
);
+10 -11
View File
@@ -20,6 +20,7 @@ import '../../../core/cache/info_cache.dart';
import '../../../core/calls/call_controller.dart';
import '../../../core/calls/call_info.dart';
import '../../../core/calls/call_session.dart';
import '../../../core/config/app_colors.dart';
import '../../../core/utils/format.dart';
import '../../../core/utils/logger.dart';
import '../../../l10n/app_localizations.dart';
@@ -30,9 +31,7 @@ import '../../widgets/sheet_helpers.dart';
import '../../widgets/small_spinner.dart';
import 'call_participants_sheet.dart';
import 'komet_hub.dart';
const Color _kEndRed = Color(0xFFE5484D);
const Color _kAcceptGreen = Color(0xFF2EC36B);
import '../../../core/config/app_fonts.dart';
class CallScreen extends StatefulWidget {
final String name;
@@ -603,7 +602,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
color: cs.onSurface,
fontSize: 24,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
const SizedBox(height: 2),
@@ -673,7 +672,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
borderRadius: BorderRadius.circular(20),
depth: 6,
borderSide: speaking
? const BorderSide(color: _kAcceptGreen, width: 2.5)
? const BorderSide(color: kSuccessGreen, width: 2.5)
: null,
padding: EdgeInsets.all(showVideo ? 0 : 12),
child: showVideo
@@ -1136,7 +1135,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
color: cs.onPrimaryContainer,
fontSize: size * 0.38,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
);
@@ -1154,7 +1153,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
color: cs.onSurface,
fontSize: 30,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
height: 1.1,
),
),
@@ -1228,14 +1227,14 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
_CallButton(
icon: Symbols.call_end,
label: l10n.callDecline,
background: _kEndRed,
background: kDangerRed,
foreground: Colors.white,
onTap: _decline,
),
_CallButton(
icon: Symbols.call,
label: l10n.callAccept,
background: _kAcceptGreen,
background: kSuccessGreen,
foreground: Colors.white,
onTap: _accept,
),
@@ -1290,7 +1289,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
_CallButton(
icon: Symbols.call_end,
label: l10n.callEndButton,
background: _kEndRed,
background: kDangerRed,
foreground: Colors.white,
onTap: _hangup,
),
@@ -1550,7 +1549,7 @@ class _CallInfoSheet extends StatelessWidget {
color: cs.onSurface,
fontSize: 20,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
const SizedBox(height: 2),
+2 -1
View File
@@ -20,6 +20,7 @@ import '../../widgets/spectrum_tint.dart';
import '../../../l10n/app_localizations.dart';
import 'call_link_sheet.dart';
import 'call_screen.dart';
import '../../../core/config/app_fonts.dart';
class CallsTab extends StatefulWidget {
const CallsTab({super.key});
@@ -490,7 +491,7 @@ class _CallsTabState extends State<CallsTab>
color: cs.onSurface,
fontSize: 24,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
const ConnectionStatusLine(),
+2 -1
View File
@@ -7,6 +7,7 @@ import '../../../core/calls/call_session.dart';
import '../../../core/games/checkers.dart';
import '../../../l10n/app_localizations.dart';
import '../../widgets/sheet_helpers.dart';
import '../../../core/config/app_fonts.dart';
Future<void> showKometHub(
BuildContext context, {
@@ -115,7 +116,7 @@ class _KometHubState extends State<_KometHub> {
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
],
@@ -19,6 +19,7 @@ import '../../../../core/utils/logger.dart';
import '../../../widgets/custom_notification.dart';
import '../../../widgets/lottie_slash_icon.dart';
import 'voice_record_controller.dart';
import '../../../../core/config/app_frost.dart';
const String _flashIcon = 'assets/lottie/ic_flash_on_to_off.json';
@@ -304,7 +305,7 @@ class VideoNoteRecordingLayer extends StatefulWidget {
class _VideoNoteRecordingLayerState extends State<VideoNoteRecordingLayer>
with SingleTickerProviderStateMixin {
static const double _circle = 260;
static const double _maxBlur = 18;
static const double _maxBlur = AppFrost.overlaySigma;
late final AnimationController _reveal = AnimationController(
vsync: this,
@@ -16,6 +16,7 @@ import 'package:komet/frontend/widgets/online_dot.dart';
import 'package:komet/frontend/widgets/profile_hero.dart';
import 'package:komet/main.dart' show storiesModule;
import 'package:komet/models/story.dart';
import '../../../../../core/config/app_fonts.dart';
class ChatHeaderRow extends StatelessWidget {
final bool glossy;
@@ -85,7 +86,7 @@ class ChatHeaderRow extends StatelessWidget {
color: cs.onSurface,
fontSize: 17,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
);
return Padding(
padding: const EdgeInsets.fromLTRB(10, 4, 10, 8),
@@ -152,7 +153,7 @@ class ChatHeaderRow extends StatelessWidget {
color: cs.onPrimaryContainer,
fontSize: d * 0.36,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -270,7 +271,7 @@ class ChatHeaderRow extends StatelessWidget {
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
);
return Row(
children: [
@@ -398,118 +398,126 @@ class ComposerInputBar extends StatelessWidget {
},
child: ValueListenableBuilder<bool>(
valueListenable: hasText,
builder: (context, hasText, _) => ValueListenableBuilder<bool>(
valueListenable: voiceRec.locked,
builder: (context, voiceLocked, _) =>
ValueListenableBuilder<bool>(
valueListenable: voiceRec.isRecording,
builder: (context, voiceRecording, _) =>
AnimatedBuilder(
animation: Listenable.merge([
note.videoNoteMode,
note.isRecording,
note.locked,
]),
builder: (context, _) {
final videoMode =
note.videoNoteMode.value;
final noteRecording =
note.isRecording.value;
final recording =
voiceRecording || noteRecording;
final locked = noteRecording
? note.locked.value
: voiceLocked;
final sendMode =
hasText ||
hasForward ||
locked ||
forceSend;
final pill = _actionSurface(
color: _flat
? Colors.transparent
: recording
? cs.error
: _frost
? AppFrost.glassTint(cs)
: cs.surfaceContainerHighest,
onTap:
(hasText ||
hasForward ||
forceSend)
? onSendText
: locked
? () => noteRecording
? note.stop(cancel: false)
: voiceRec.stop(
cancel: false,
)
: null,
onLongPress:
(hasText &&
!forceSend &&
!hasForward)
? onScheduleMessage
: null,
child: SizedBox(
width: 54,
height: 54,
child: Center(
child: ComposerMorphIcon(
action: sendMode
? ComposerAction.send
: videoMode
? ComposerAction.videocam
: ComposerAction.mic,
color: recording
? (_flat
? cs.error
: cs.onError)
: sendMode
? cs.primary
: _flat
? cs.onSurfaceVariant
: cs.onSurface,
builder: (context, hasText, _) =>
ValueListenableBuilder<bool>(
valueListenable: voiceRec.locked,
builder: (context, voiceLocked, _) =>
ValueListenableBuilder<bool>(
valueListenable: voiceRec.isRecording,
builder: (context, voiceRecording, _) =>
AnimatedBuilder(
animation: Listenable.merge([
note.videoNoteMode,
note.isRecording,
note.locked,
]),
builder: (context, _) {
final videoMode =
note.videoNoteMode.value;
final noteRecording =
note.isRecording.value;
final recording =
voiceRecording ||
noteRecording;
final locked = noteRecording
? note.locked.value
: voiceLocked;
final sendMode =
hasText ||
hasForward ||
locked ||
forceSend;
final pill = _actionSurface(
color: _flat
? Colors.transparent
: recording
? cs.error
: _frost
? AppFrost.glassTint(cs)
: cs.surfaceContainerHighest,
onTap:
(hasText ||
hasForward ||
forceSend)
? onSendText
: locked
? () => noteRecording
? note.stop(
cancel: false,
)
: voiceRec.stop(
cancel: false,
)
: null,
onLongPress:
(hasText &&
!forceSend &&
!hasForward)
? onScheduleMessage
: null,
child: SizedBox(
width: 54,
height: 54,
child: Center(
child: ComposerMorphIcon(
action: sendMode
? ComposerAction.send
: videoMode
? ComposerAction
.videocam
: ComposerAction.mic,
color: recording
? (_flat
? cs.error
: cs.onError)
: sendMode
? cs.primary
: _flat
? cs.onSurfaceVariant
: cs.onSurface,
),
),
),
),
),
);
final visual = _recordingButtonVisual(
pill: pill,
cs: cs,
active: recording && !locked,
);
final voiceEnabled =
!sendMode && !forceSend;
return GestureDetector(
onTap: voiceEnabled
? note.toggleMode
: null,
onLongPressStart: voiceEnabled
? (_) => videoMode
? note.start()
: voiceRec.start()
: null,
onLongPressMoveUpdate: voiceEnabled
? (d) => videoMode
? note.handleDrag(
d.offsetFromOrigin,
)
: voiceRec.handleDrag(
d.offsetFromOrigin,
)
: null,
onLongPressEnd: voiceEnabled
? (_) => videoMode
? note.handleEnd()
: voiceRec.handleEnd()
: null,
child: visual,
);
},
),
),
),
);
final visual =
_recordingButtonVisual(
pill: pill,
cs: cs,
active:
recording && !locked,
);
final voiceEnabled =
!sendMode && !forceSend;
return GestureDetector(
onTap: voiceEnabled
? note.toggleMode
: null,
onLongPressStart: voiceEnabled
? (_) => videoMode
? note.start()
: voiceRec.start()
: null,
onLongPressMoveUpdate:
voiceEnabled
? (d) => videoMode
? note.handleDrag(
d.offsetFromOrigin,
)
: voiceRec.handleDrag(
d.offsetFromOrigin,
)
: null,
onLongPressEnd: voiceEnabled
? (_) => videoMode
? note.handleEnd()
: voiceRec.handleEnd()
: null,
child: visual,
);
},
),
),
),
),
),
],
@@ -525,7 +533,7 @@ class ComposerInputBar extends StatelessWidget {
return _barSurface(cs, bar);
}
bool get _flat => style == ComposerStyle.materialYou;
bool get _flat => !ComposerChrome.isGlossy(style);
bool get _frost => ComposerMaterial.isFrost(background);
@@ -12,6 +12,7 @@ import 'package:komet/frontend/widgets/komet_avatar.dart';
import 'package:komet/frontend/widgets/small_spinner.dart';
import 'package:komet/frontend/screens/chats/chat/chat_search_controller.dart';
import 'package:komet/frontend/screens/chats/chat/message_search_result.dart';
import '../../../../../core/config/app_fonts.dart';
class SearchTopBar extends StatelessWidget {
const SearchTopBar({
@@ -37,13 +38,17 @@ class SearchTopBar extends StatelessWidget {
textInputAction: TextInputAction.search,
onSubmitted: search.submit,
cursorColor: cs.primary,
style: TextStyle(color: cs.onSurface, fontSize: 16, fontFamily: 'Outfit'),
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontFamily: displayFontOf(context),
),
decoration: InputDecoration(
hintText: 'Поиск...',
hintStyle: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 16,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
border: InputBorder.none,
isDense: true,
@@ -164,7 +169,8 @@ class SearchOverlay extends StatelessWidget {
bottom: MediaQuery.paddingOf(context).bottom + 16,
),
itemCount: results.length,
itemBuilder: (context, index) => _tile(results[index]),
itemBuilder: (context, index) =>
_tile(context, results[index]),
);
}
if (loading) {
@@ -196,7 +202,7 @@ class SearchOverlay extends StatelessWidget {
);
}
Widget _tile(MessageSearchResult r) {
Widget _tile(BuildContext context, MessageSearchResult r) {
final name = senderName(r.senderId);
final date = formatDateWords(DateTime.fromMillisecondsSinceEpoch(r.time));
return InkWell(
@@ -228,7 +234,7 @@ class SearchOverlay extends StatelessWidget {
color: cs.primary,
fontSize: 15,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:komet/backend/modules/messages.dart';
import 'package:komet/frontend/widgets/glossy_pill.dart';
import '../../../../../core/config/app_fonts.dart';
class SelectionTopBar extends StatelessWidget {
final ColorScheme cs;
@@ -51,7 +52,7 @@ class SelectionTopBar extends StatelessWidget {
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -114,7 +115,7 @@ class SelectionTopBar extends StatelessWidget {
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -149,6 +150,7 @@ class SelectionBottomBar extends StatelessWidget {
final Set<String> selected;
final VoidCallback onReply;
final VoidCallback onForward;
final bool allowForward;
const SelectionBottomBar({
super.key,
@@ -156,6 +158,7 @@ class SelectionBottomBar extends StatelessWidget {
required this.selected,
required this.onReply,
required this.onForward,
this.allowForward = true,
});
@override
@@ -169,6 +172,7 @@ class SelectionBottomBar extends StatelessWidget {
if (single) ...[
Expanded(
child: _pill(
context,
cs,
icon: Symbols.reply,
label: 'Ответить',
@@ -176,18 +180,22 @@ class SelectionBottomBar extends StatelessWidget {
onTap: onReply,
),
),
const SizedBox(width: 12),
if (allowForward) const SizedBox(width: 12),
] else
const Spacer(),
Expanded(
child: _pill(
cs,
icon: Symbols.forward,
label: 'Переслать',
iconLeading: true,
onTap: onForward,
),
),
if (allowForward)
Expanded(
child: _pill(
context,
cs,
icon: Symbols.forward,
label: 'Переслать',
iconLeading: true,
onTap: onForward,
),
)
else if (!single)
const Spacer(),
],
),
),
@@ -195,6 +203,7 @@ class SelectionBottomBar extends StatelessWidget {
}
Widget _pill(
BuildContext context,
ColorScheme cs, {
required IconData icon,
required String label,
@@ -207,7 +216,7 @@ class SelectionBottomBar extends StatelessWidget {
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
);
final iconWidget = Icon(icon, color: cs.onSurface, size: 22, weight: 500);
@@ -8,6 +8,7 @@ import '../../widgets/glossy_pill.dart';
import '../../widgets/primary_loading_button.dart';
import '../../widgets/settings_card.dart';
import '../../widgets/small_spinner.dart';
import '../../../core/config/app_fonts.dart';
class ChatEncryptionScreen extends StatefulWidget {
final int accountId;
@@ -101,7 +102,7 @@ class _ChatEncryptionScreenState extends State<ChatEncryptionScreen> {
color: cs.onSurface,
fontSize: 20,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
+146 -60
View File
@@ -1,4 +1,6 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
@@ -48,6 +50,7 @@ import '../stories/story_viewer_screen.dart';
import 'chat_screen.dart';
import 'group_invite_sheets.dart';
import 'profile_action_sheets.dart';
import '../../../core/config/app_fonts.dart';
class _MemberInfo {
final int id;
@@ -588,9 +591,9 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
: collapsedH;
final delta = expandedH - collapsedH;
_syncHeaderDelta(delta);
final controller = _bodyScrollController ??=
(ScrollController(initialScrollOffset: delta)
..addListener(_onBodyScroll));
final controller = _bodyScrollController ??= (ScrollController(
initialScrollOffset: delta,
)..addListener(_onBodyScroll));
return NotificationListener<ScrollNotification>(
onNotification: (n) => _onHeaderScrollNotification(n, delta),
@@ -794,7 +797,11 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
: SegmentedRingPainter(
total: _storyPreview!.totalCount,
read: _storyPreview!.readCount,
unreadColors: [cs.primary, cs.tertiary, cs.primary],
unreadColors: [
cs.primary,
cs.tertiary,
cs.primary,
],
readColor: cs.outlineVariant,
strokeWidth: 3.4,
),
@@ -809,7 +816,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
child: Row(
children: [
IconButton(
icon: Icon(Icons.arrow_back, color: iconColor),
icon: Icon(Symbols.arrow_back, color: iconColor),
onPressed: () => Navigator.pop(context),
),
Expanded(
@@ -958,13 +965,13 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
if (interactive && _avatarHover) ...[
_avatarArrow(
alignment: Alignment.centerLeft,
icon: Icons.chevron_left,
icon: Symbols.chevron_left,
enabled: _avatarIndex > 0,
onTap: () => _stepAvatar(-1),
),
_avatarArrow(
alignment: Alignment.centerRight,
icon: Icons.chevron_right,
icon: Symbols.chevron_right,
enabled: _avatarIndex < pages.length - 1,
onTap: () => _stepAvatar(1),
),
@@ -1085,11 +1092,11 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
'${pluralRu(unread.length, 'история', 'истории', 'историй')}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
style: TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -1147,12 +1154,12 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
final color = iconColor ?? cs.onSurface;
if (entries.isEmpty) {
return IconButton(
icon: Icon(Icons.more_vert, color: color),
icon: Icon(Symbols.more_vert, color: color),
onPressed: null,
);
}
return PopupMenuButton<VoidCallback>(
icon: Icon(Icons.more_vert, color: color),
icon: Icon(Symbols.more_vert, color: color),
onSelected: (action) => action(),
itemBuilder: (_) => [
for (final entry in entries)
@@ -1181,7 +1188,9 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
_moreMenuEntries() {
if (_isLoading) return const [];
final entries =
<({IconData icon, String label, bool destructive, VoidCallback onTap})>[];
<
({IconData icon, String label, bool destructive, VoidCallback onTap})
>[];
if (widget.chatType == 'DIALOG') {
if (_isContact) {
@@ -1270,7 +1279,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
color: textColor,
fontSize: lerpDouble(22, 25, t)!,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
);
final custom = _customName;
final real = _realName;
@@ -1377,8 +1386,8 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
Widget _buildActions(ColorScheme cs) {
final muteBtn = (
icon: Icons.notifications,
slashedIcon: Icons.notifications_off,
icon: Symbols.notifications,
slashedIcon: Symbols.notifications_off,
slashed: _isMuted,
label: _isMuted
? l10n.chatInfoActionMuted
@@ -1386,14 +1395,14 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
onTap: _muteBusy ? null : _toggleMute,
);
final chatBtn = (
icon: Icons.chat_bubble,
icon: Symbols.chat_bubble,
slashedIcon: null,
slashed: false,
label: l10n.contactProfileActionChat,
onTap: _openChat,
);
final leaveBtn = (
icon: Icons.exit_to_app,
icon: Symbols.exit_to_app,
slashedIcon: null,
slashed: false,
label: l10n.chatInfoActionLeave,
@@ -1416,7 +1425,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
muteBtn,
if (!_isBot)
(
icon: Icons.call,
icon: Symbols.call,
slashedIcon: null,
slashed: false,
label: l10n.contactProfileActionCall,
@@ -1526,7 +1535,9 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
showCustomNotification(
context,
error ??
(muted ? l10n.chatInfoNotificationsOn : l10n.chatInfoNotificationsOff),
(muted
? l10n.chatInfoNotificationsOn
: l10n.chatInfoNotificationsOff),
);
}
@@ -1549,8 +1560,11 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
if (active != null) {
await navigator.push(
MaterialPageRoute(
builder: (_) =>
CallScreen(name: _customName, avatarUrl: avatarUrl, session: active),
builder: (_) => CallScreen(
name: _customName,
avatarUrl: avatarUrl,
session: active,
),
),
);
return;
@@ -1819,8 +1833,11 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
}
}
} else {
final link = _chatInfo?.link;
if (link != null && link.isNotEmpty) {
final info = _chatInfo;
final link = info?.link;
if (link != null &&
link.isNotEmpty &&
(info?.canSeeInviteLink(_myId) ?? false)) {
items.add(_linkCard(cs, link));
}
final desc = _chatInfo?.description;
@@ -1903,7 +1920,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
),
),
IconButton(
icon: Icon(Icons.qr_code_2, color: cs.primary, size: 22),
icon: Icon(Symbols.qr_code_2, color: cs.primary, size: 22),
onPressed: () {},
),
],
@@ -1911,6 +1928,9 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
);
}
static const Duration _descRevealDuration = Duration(milliseconds: 260);
static const Curve _descRevealCurve = Curves.easeOutCubic;
Widget _collapsibleDescCard(ColorScheme cs, String desc) {
const int collapsedLines = 3;
final isLong = desc.length > 120;
@@ -1930,23 +1950,45 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
const SizedBox(height: 4),
FormattedMessageText(
text: desc,
ranges: const [],
entityMode: TextEntityMode.copy,
style: TextStyle(color: cs.onSurface, fontSize: 15, height: 1.4),
maxLines: (_descExpanded || !isLong) ? null : collapsedLines,
overflow: (_descExpanded || !isLong)
? null
: TextOverflow.ellipsis,
AnimatedSize(
duration: _descRevealDuration,
curve: _descRevealCurve,
alignment: Alignment.topLeft,
child: FormattedMessageText(
text: desc,
ranges: const [],
entityMode: TextEntityMode.copy,
style: TextStyle(
color: cs.onSurface,
fontSize: 15,
height: 1.4,
),
maxLines: (_descExpanded || !isLong) ? null : collapsedLines,
overflow: (_descExpanded || !isLong)
? null
: TextOverflow.ellipsis,
),
),
if (isLong) ...[
const SizedBox(height: 6),
GestureDetector(
onTap: () => setState(() => _descExpanded = !_descExpanded),
child: Text(
_descExpanded ? l10n.chatInfoCollapse : l10n.chatInfoShowMore,
style: TextStyle(color: cs.primary, fontSize: 13),
behavior: HitTestBehavior.opaque,
onTap: () {
Haptics.tap();
setState(() => _descExpanded = !_descExpanded);
},
child: AnimatedTextSwap(
showAlternate: _descExpanded,
duration: _descRevealDuration,
curve: _descRevealCurve,
alternate: Text(
l10n.chatInfoCollapse,
style: TextStyle(color: cs.primary, fontSize: 13),
),
child: Text(
l10n.chatInfoShowMore,
style: TextStyle(color: cs.primary, fontSize: 13),
),
),
),
],
@@ -2046,7 +2088,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
return _buildPlaceholder(
cs,
l10n.chatInfoEmptyGeneralChats,
Icons.group,
Symbols.group,
);
}
return CommonChatsTab(
@@ -2060,7 +2102,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
cs,
SharedContentKind.media,
l10n.chatInfoEmptyMedia,
Icons.photo_library,
Symbols.photo_library,
);
}
if (_selectedTab == l10n.chatInfoTabFiles) {
@@ -2068,7 +2110,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
cs,
SharedContentKind.files,
l10n.chatInfoEmptyFiles,
Icons.description,
Symbols.description,
);
}
if (_selectedTab == l10n.chatInfoTabVoice) {
@@ -2076,7 +2118,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
cs,
SharedContentKind.voice,
l10n.chatInfoEmptyVoice,
Icons.mic,
Symbols.mic,
);
}
if (_selectedTab == l10n.chatInfoTabLinks) {
@@ -2084,7 +2126,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
cs,
SharedContentKind.links,
l10n.chatInfoEmptyLinks,
Icons.link,
Symbols.link,
);
}
return const SizedBox.shrink();
@@ -2187,7 +2229,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
children: [
_memberAction(
cs,
Icons.person_add,
Symbols.person_add,
l10n.chatInfoAddMember,
_openAddMembers,
),
@@ -2195,7 +2237,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
_listDivider(cs),
_memberAction(
cs,
Icons.link,
Symbols.link,
l10n.chatInfoInviteByLink,
() => _openInviteLink(_inviteLink!),
),
@@ -2232,7 +2274,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Icon(Icons.expand_more, color: cs.primary, size: 26),
Icon(Symbols.expand_more, color: cs.primary, size: 26),
const SizedBox(width: 14),
Text(
l10n.chatInfoShowMore,
@@ -2480,6 +2522,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
add(l10n.chatInfoRowId, chat['id']);
if (type == 'DIALOG') {
add(l10n.chatInfoRowUserId, _contactData?.raw['id'] ?? _otherId);
add(l10n.chatInfoRowCreated, chat['created'], tsFormat: true);
add(l10n.chatInfoRowModified, chat['modified'], tsFormat: true);
add(l10n.callInfoStatus, chat['status']);
@@ -2508,6 +2551,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
final opts = chat['options'] as Map?;
add(l10n.chatInfoRowOfficialGroup, opts?['OFFICIAL'] as bool?);
add(l10n.chatInfoRowSignAdmin, opts?['SIGN_ADMIN'] as bool?);
_addChatOptions(add, opts);
add(l10n.callInfoStatus, chat['status']);
}
@@ -2524,6 +2568,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
l10n.chatInfoRowOnlyAdmin,
opts?['ONLY_ADMIN_CAN_ADD_MEMBER'] as bool?,
);
_addChatOptions(add, opts);
add(l10n.callInfoStatus, chat['status']);
}
@@ -2572,6 +2617,36 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
);
}
void _addChatOptions(
void Function(String label, dynamic val, {bool tsFormat}) add,
Map? opts,
) {
if (opts == null) return;
add(l10n.chatInfoRowDisableForward, opts['DISABLE_FORWARD'] as bool?);
add(
l10n.chatInfoRowCopyDisabled,
opts['MESSAGE_COPY_NOT_ALLOWED'] as bool?,
);
add(l10n.chatInfoRowOnlyAdminCall, opts['ONLY_ADMIN_CAN_CALL'] as bool?);
add(l10n.chatInfoRowAllCanPin, opts['ALL_CAN_PIN_MESSAGE'] as bool?);
add(
l10n.chatInfoRowMembersSeeLink,
opts['MEMBERS_CAN_SEE_PRIVATE_LINK'] as bool?,
);
add(
l10n.chatInfoRowConfirmBeforeSend,
opts['CONFIRM_BEFORE_SEND'] as bool?,
);
add(
l10n.chatInfoRowOnlyOwnerIconTitle,
opts['ONLY_OWNER_CAN_CHANGE_ICON_TITLE'] as bool?,
);
add(
l10n.chatInfoRowPromotedDisabled,
opts['PROMOTED_CONTENT_DISABLED'] as bool?,
);
}
List<({String label, String value})> _buildExtraContactRows() {
final c = _contactData;
if (c == null) return const [];
@@ -2646,6 +2721,13 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
);
}
Future<void> _copyInfoValue(String value) async {
await Clipboard.setData(ClipboardData(text: value));
if (!mounted) return;
Haptics.tap();
showCustomNotification(context, l10n.msgActionsCopied);
}
Widget _infoRow(
ColorScheme cs,
String label,
@@ -2658,22 +2740,26 @@ class _ChatInfoScreenState extends State<ChatInfoScreen>
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10),
),
Text(
value,
style: TextStyle(
color: cs.onSurface,
fontSize: 12,
fontWeight: FontWeight.w500,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onLongPress: () => unawaited(_copyInfoValue(value)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10),
),
),
],
Text(
value,
style: TextStyle(
color: cs.onSurface,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
?trailing,
@@ -84,10 +84,12 @@ import '../../../backend/modules/webapp.dart';
import '../../../models/story.dart';
import '../webapp/open_mini_app.dart';
import '../stories/story_ring.dart';
import '../../widgets/attachment/bubbles/bubble_context.dart';
import '../../widgets/sending_clock_icon.dart';
import '../stories/story_viewer_screen.dart';
import '../downloads_screen.dart';
import '../../widgets/media_playback_pill.dart';
import '../../../core/config/app_fonts.dart';
class _StoriesScrollPhysics extends BouncingScrollPhysics {
final bool Function() blockPositive;
@@ -1545,7 +1547,8 @@ class _ChatListScreenState extends State<ChatListScreen>
) +
8) *
(1.0 - _pullRatio),
height: FoldedStoryStack.outerSize,
height:
FoldedStoryStack.outerSize,
child: OverflowBox(
alignment: Alignment.centerLeft,
maxWidth:
@@ -1575,7 +1578,7 @@ class _ChatListScreenState extends State<ChatListScreen>
color: cs.onSurface,
fontSize: 20,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -1753,9 +1756,7 @@ class _ChatListScreenState extends State<ChatListScreen>
),
),
if (!widget.forwardMode)
const MediaPlaybackPill(
margin: EdgeInsets.fromLTRB(20, 6, 20, 2),
),
const MediaPlaybackPill(margin: EdgeInsets.fromLTRB(20, 6, 20, 2)),
if (!widget.forwardMode) _buildInformerBanner(),
],
),
@@ -2407,7 +2408,7 @@ class _ChatListScreenState extends State<ChatListScreen>
color: cs.onSurface,
fontSize: 20,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
],
@@ -2687,6 +2688,8 @@ class _ChatListScreenState extends State<ChatListScreen>
return oneLine.isEmpty ? null : oneLine;
}
static const double _ownStatusIconSize = 14;
String? _ownStatusFor(CachedChat chat, bool isPlaceholder) {
if (isPlaceholder || chat.id == 0) return null;
final me = _profile?.id;
@@ -2695,31 +2698,19 @@ class _ChatListScreenState extends State<ChatListScreen>
}
Widget _ownStatusIcon(ColorScheme cs, String status, bool read) {
if (isSendingStatus(status)) {
return SendingClockIcon(color: cs.outline, size: 14);
}
IconData icon;
Color color;
switch (status) {
case 'sending':
case 'pending':
icon = Symbols.schedule;
color = cs.outline;
case 'error':
icon = Symbols.error;
color = Colors.redAccent;
default:
if (read) {
icon = Symbols.done_all;
color = kReadReceiptBlue;
} else {
icon = Symbols.check;
color = cs.outline;
}
}
final sending = isSendingStatus(status);
final effective = (read && !sending && status != 'error') ? 'read' : status;
final visual = messageStatusVisual(effective, dimColor: cs.outline);
return Padding(
padding: const EdgeInsets.only(left: 6),
child: Icon(icon, size: 16, color: color, fill: 1),
child: sending
? SendingClockIcon(color: visual.color, size: _ownStatusIconSize)
: Icon(
visual.icon,
size: _ownStatusIconSize,
color: visual.color,
weight: 400,
),
);
}
+57 -19
View File
@@ -118,6 +118,8 @@ import 'chat/retain_offset_physics.dart';
import 'profile_action_sheets.dart';
import '../../../core/media/media_playback.dart';
import '../../widgets/media_playback_pill.dart';
import '../../../core/config/app_fonts.dart';
import '../../../core/config/app_shape.dart';
class _DateSeparatorItem {
final DateTime date;
@@ -598,7 +600,9 @@ class _ChatScreenState extends State<ChatScreen>
ChatChromeStyle get _effectiveChrome {
final chrome = AppChatChrome.current.value;
if (chrome == ChatChromeStyle.liquidGlass) return ChatChromeStyle.transparent;
if (chrome == ChatChromeStyle.liquidGlass) {
return ChatChromeStyle.transparent;
}
if (_wallpaper != null && chrome == ChatChromeStyle.none) {
return ChatChromeStyle.blur;
}
@@ -1612,7 +1616,9 @@ class _ChatScreenState extends State<ChatScreen>
if (_historyAutoloadSuppressed) return;
if (_isLoading) return;
if (_commentsMode) {
if (_commentsLoadingMore || !_commentsHasMore || _messages.isEmpty) return;
if (_commentsLoadingMore || !_commentsHasMore || _messages.isEmpty) {
return;
}
final pos = _scrollController.position;
if (pos.pixels - pos.minScrollExtent <= _historyPrefetchExtent) {
unawaited(_loadMoreComments());
@@ -1692,7 +1698,10 @@ class _ChatScreenState extends State<ChatScreen>
return null;
}
Future<void> _holdScrollAfterAppend(String? anchorId, double? beforeDy) async {
Future<void> _holdScrollAfterAppend(
String? anchorId,
double? beforeDy,
) async {
if (anchorId == null || beforeDy == null) return;
await WidgetsBinding.instance.endOfFrame;
if (!mounted || !_scrollController.hasClients) return;
@@ -2218,7 +2227,11 @@ class _ChatScreenState extends State<ChatScreen>
String? _effectiveStatus(CachedMessage msg) {
if (msg.senderId != _myId) return null;
if (msg.status == 'sending' || msg.status == 'error') return msg.status;
if (msg.status == 'sending' ||
msg.status == 'pending' ||
msg.status == 'error') {
return msg.status;
}
return 'sent';
}
@@ -2692,6 +2705,7 @@ class _ChatScreenState extends State<ChatScreen>
selected: selected,
onReply: _replySelected,
onForward: _forwardSelected,
allowForward: !(chat?.forwardDisabled ?? false),
),
),
),
@@ -2699,7 +2713,7 @@ class _ChatScreenState extends State<ChatScreen>
);
Widget wrapChrome(Widget child) {
if (_composerFrosted) {
if (AppComposerStyle.current.value != ComposerStyle.materialYou) {
if (ComposerChrome.isGlossy(AppComposerStyle.current.value)) {
return child;
}
return _FrostedPanel(
@@ -2778,7 +2792,7 @@ class _ChatScreenState extends State<ChatScreen>
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
const SizedBox(height: 16),
@@ -2927,6 +2941,7 @@ class _ChatScreenState extends State<ChatScreen>
builder: (ctx, setLocalState) {
return AlertDialog(
backgroundColor: cs.surfaceContainerHigh,
shape: AppShape.dialogBorder,
title: const Text('Удалить сообщение'),
content: Column(
mainAxisSize: MainAxisSize.min,
@@ -3183,7 +3198,8 @@ class _ChatScreenState extends State<ChatScreen>
headerStatus: _headerStatusNotifier,
scheduledCount: _scheduledCount,
otherUnread: _otherUnread,
showCall: !_commentsMode &&
showCall:
!_commentsMode &&
widget.chatType == 'DIALOG' &&
!_peerIsBot,
onClose: widget.onClose,
@@ -3868,6 +3884,16 @@ class _ChatScreenState extends State<ChatScreen>
}
}
if (chat?.confirmBeforeSend ?? false) {
final l10n = AppLocalizations.of(context)!;
final confirmed = await showConfirmDialog(
context,
message: l10n.chatSendConfirmMessage,
confirmLabel: l10n.chatSendConfirmAction,
);
if (!confirmed || !mounted) return;
}
final wireText = await _encryptOutgoing(text);
if (wireText == null || !mounted) return;
final encrypted = wireText != text;
@@ -4028,6 +4054,9 @@ class _ChatScreenState extends State<ChatScreen>
}
return;
}
final failed = isPermanentSendFailure(e);
final status = failed ? 'error' : 'pending';
if (failed) logger.w('Отправка отклонена сервером: $e');
final index = _messages.indexWhere((m) => m.id == tempId);
if (index != -1 && mounted) {
final queued = CachedMessage(
@@ -4037,7 +4066,7 @@ class _ChatScreenState extends State<ChatScreen>
senderId: _myId,
text: text,
time: now,
status: 'pending',
status: status,
payload: composedPayload,
);
_messages[index] = queued;
@@ -4051,7 +4080,7 @@ class _ChatScreenState extends State<ChatScreen>
messageId: tempId,
time: now,
text: text,
status: 'pending',
status: status,
elements: elements,
),
);
@@ -5586,9 +5615,7 @@ class _ChatScreenState extends State<ChatScreen>
final cs = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Center(
child: SmallSpinner(size: 22, color: cs.onSurfaceVariant),
),
child: Center(child: SmallSpinner(size: 22, color: cs.onSurfaceVariant)),
);
}
@@ -5670,7 +5697,8 @@ class _ChatScreenState extends State<ChatScreen>
final bool isChannelPost =
!_commentsMode &&
(chat?.type ?? widget.chatType) == 'CHANNEL' &&
(chat?.type ?? widget.chatType) ==
'CHANNEL' &&
!message.isControl;
final bool isCommentedPost =
_commentsMode &&
@@ -5746,9 +5774,12 @@ class _ChatScreenState extends State<ChatScreen>
onReply: message.isControl
? null
: () => _startReply(message),
onForward: message.isControl
onForward:
message.isControl ||
(chat?.forwardDisabled ?? false)
? null
: () => _forwardMessages([message]),
allowCopy: !(chat?.copyDisabled ?? false),
onMarkUnread: message.isControl
? null
: () => _markMessageUnread(message),
@@ -6135,7 +6166,10 @@ class _ChatScreenState extends State<ChatScreen>
FileAttachment(fileId: fileId),
).id;
try {
final realId = await messagesModule.sendFileMessage(widget.chatId, fileId);
final realId = await messagesModule.sendFileMessage(
widget.chatId,
fileId,
);
final ok = realId != null;
if (!mounted) return ok;
if (ok) {
@@ -6271,7 +6305,9 @@ class _ChatScreenState extends State<ChatScreen>
var durationMs = video.item.duration?.inMilliseconds;
if (durationMs == null && DesktopVideoProbe.supported) {
durationMs = (await DesktopVideoProbe.duration(file.path))?.inMilliseconds;
durationMs = (await DesktopVideoProbe.duration(
file.path,
))?.inMilliseconds;
}
final dims = await video.item.dimensions();
Uint8List? thumbBytes;
@@ -6949,9 +6985,7 @@ class _PinnedMessageBanner extends StatelessWidget {
if (frosted) {
return GlassSurface(
liquid: liquid,
borderRadius: floating
? BorderRadius.circular(16)
: BorderRadius.zero,
borderRadius: floating ? BorderRadius.circular(16) : BorderRadius.zero,
frostTint: Colors.transparent,
border: floating ? null : bottomBorder,
backdropKey: backdropKey,
@@ -7059,6 +7093,7 @@ class _SelectableMessageRow extends StatefulWidget {
final VoidCallback? onEdit;
final VoidCallback? onReply;
final VoidCallback? onForward;
final bool allowCopy;
final VoidCallback? onMarkUnread;
final VoidCallback? onPin;
final bool Function() isPinned;
@@ -7083,6 +7118,7 @@ class _SelectableMessageRow extends StatefulWidget {
this.onEdit,
this.onReply,
this.onForward,
this.allowCopy = true,
this.onMarkUnread,
this.onPin,
required this.isPinned,
@@ -7153,6 +7189,7 @@ class _SelectableMessageRowState extends State<_SelectableMessageRow> {
onEdit: widget.onEdit,
onReply: widget.onReply,
onForward: widget.onForward,
allowCopy: widget.allowCopy,
onMarkUnread: widget.onMarkUnread,
onPin: widget.onPin,
isPinned: _isPinnedNow(),
@@ -7221,6 +7258,7 @@ class _SelectableMessageRowState extends State<_SelectableMessageRow> {
onEdit: widget.onEdit,
onReply: widget.onReply,
onForward: widget.onForward,
allowCopy: widget.allowCopy,
onMarkUnread: widget.onMarkUnread,
onPin: widget.onPin,
isPinned: _isPinnedNow(),
@@ -6,6 +6,8 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:komet/core/storage/chat_wallpaper_store.dart';
import 'package:komet/frontend/widgets/chat_wallpaper_view.dart';
import '../../../core/config/app_frost.dart';
import '../../../core/config/app_fonts.dart';
class ChatWallpaperPreviewScreen extends StatefulWidget {
final Uint8List imageBytes;
@@ -100,13 +102,13 @@ class _ChatWallpaperPreviewScreenState
icon: const Icon(Symbols.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
const Text(
Text(
'Обои',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
],
@@ -158,7 +160,10 @@ class _Frosted extends StatelessWidget {
return ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 24, sigmaY: 24),
filter: ui.ImageFilter.blur(
sigmaX: AppFrost.panelSigma,
sigmaY: AppFrost.panelSigma,
),
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.14),
@@ -258,7 +263,7 @@ class _DimLabel extends StatelessWidget {
color: color,
fontSize: 16,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -279,7 +284,8 @@ class _RevealClipper extends CustomClipper<Rect> {
const _RevealClipper(this.fraction);
@override
Rect getClip(Size size) => Rect.fromLTWH(0, 0, size.width * fraction, size.height);
Rect getClip(Size size) =>
Rect.fromLTWH(0, 0, size.width * fraction, size.height);
@override
bool shouldReclip(_RevealClipper oldClipper) =>
@@ -324,11 +330,11 @@ class _ToggleChip extends StatelessWidget {
const SizedBox(width: 10),
Text(
label,
style: const TextStyle(
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
],
@@ -350,7 +356,7 @@ class _ApplyButton extends StatelessWidget {
onTap: onTap,
child: _Frosted(
radius: 26,
child: const SizedBox(
child: SizedBox(
height: 52,
child: Center(
child: Text(
@@ -359,7 +365,7 @@ class _ApplyButton extends StatelessWidget {
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -384,6 +390,7 @@ class _SamplePreview extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_bubble(
context,
text: 'Как насчёт новых обоев для этого чата?',
color: cs.surfaceContainerHighest.withValues(alpha: 0.92),
textColor: cs.onSurface,
@@ -391,6 +398,7 @@ class _SamplePreview extends StatelessWidget {
),
const SizedBox(height: 8),
_bubble(
context,
text: 'Отличная идея.',
color: cs.primary,
textColor: cs.onPrimary,
@@ -402,7 +410,8 @@ class _SamplePreview extends StatelessWidget {
);
}
Widget _bubble({
Widget _bubble(
BuildContext context, {
required String text,
required Color color,
required Color textColor,
@@ -423,7 +432,7 @@ class _SamplePreview extends StatelessWidget {
style: TextStyle(
color: textColor,
fontSize: 15,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../contacts/contact_sheet_common.dart';
import '../../../core/config/app_fonts.dart';
class ConfirmChoice {
final bool confirmed;
@@ -135,7 +136,7 @@ class _ConfirmCardState extends State<_ConfirmCard> {
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
const SizedBox(height: 8),
@@ -159,8 +160,7 @@ class _ConfirmCardState extends State<_ConfirmCard> {
Checkbox(
value: _checked,
visualDensity: VisualDensity.compact,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onChanged: (v) => setState(() => _checked = v ?? false),
),
const SizedBox(width: 8),
@@ -195,9 +195,9 @@ class _ConfirmCardState extends State<_ConfirmCard> {
foregroundColor: cs.onErrorContainer,
)
: null,
onPressed: () => Navigator.of(context).pop(
ConfirmChoice(confirmed: true, checked: _checked),
),
onPressed: () => Navigator.of(
context,
).pop(ConfirmChoice(confirmed: true, checked: _checked)),
child: Text(widget.confirmLabel),
),
],
@@ -282,7 +282,7 @@ class _ComplaintCardState extends State<_ComplaintCard> {
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
const SizedBox(height: 6),
@@ -19,6 +19,7 @@ import '../../widgets/schedule_time_picker.dart';
import '../../widgets/sheet_helpers.dart';
import '../../widgets/small_spinner.dart';
import '../../widgets/reload_on_reconnect.dart';
import '../../../core/config/app_fonts.dart';
class ScheduledMessagesScreen extends StatefulWidget {
final int chatId;
@@ -117,7 +118,7 @@ class _ScheduledMessagesScreenState extends State<ScheduledMessagesScreen>
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
const SizedBox(height: 16),
@@ -249,7 +250,7 @@ class _ScheduledMessagesScreenState extends State<ScheduledMessagesScreen>
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
Text(
@@ -10,6 +10,7 @@ import 'package:komet/frontend/screens/contacts/open_contact_profile.dart';
import 'package:komet/frontend/widgets/custom_notification.dart';
import 'package:komet/l10n/app_localizations.dart';
import 'package:komet/main.dart';
import 'package:material_symbols_icons/symbols.dart';
Future<void> showAddContactSheet(BuildContext context) {
return showBlurredCard<void>(
@@ -209,7 +210,7 @@ class _AddContactCardState extends State<_AddContactCard> {
),
),
Icon(
Icons.keyboard_arrow_down,
Symbols.keyboard_arrow_down,
size: 20,
color: cs.onSurfaceVariant,
),
@@ -297,7 +298,10 @@ class _AddContactCardState extends State<_AddContactCard> {
)
: Text(
l10n.addContactSave,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
);
@@ -339,5 +343,4 @@ class _AddContactCardState extends State<_AddContactCard> {
),
);
}
}
@@ -2,6 +2,8 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import '../../../core/config/app_frost.dart';
Future<T?> showBlurredCard<T>(
BuildContext context,
Widget Function(BuildContext hostContext) builder,
@@ -10,13 +12,16 @@ Future<T?> showBlurredCard<T>(
context: context,
barrierDismissible: true,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: Colors.black.withValues(alpha: 0.28),
barrierColor: AppFrost.scrim(),
transitionDuration: const Duration(milliseconds: 260),
pageBuilder: (_, _, _) => builder(context),
transitionBuilder: (_, anim, _, child) {
final t = Curves.easeOutCubic.transform(anim.value);
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 14 * t, sigmaY: 14 * t),
filter: ImageFilter.blur(
sigmaX: AppFrost.overlaySigma * t,
sigmaY: AppFrost.overlaySigma * t,
),
child: Opacity(
opacity: anim.value,
child: Transform.scale(scale: 0.94 + 0.06 * t, child: child),
@@ -19,6 +19,9 @@ import '../../widgets/springy_tap.dart';
import '../chats/chat_info_screen.dart';
import 'nfc_exchange_sheet.dart';
import 'open_contact_profile.dart';
import '../../../core/config/app_frost.dart';
import '../../../core/config/app_fonts.dart';
import '../../../core/config/app_shape.dart';
enum _SearchMode { phone, id }
@@ -57,7 +60,7 @@ class _ContactsTabState extends State<ContactsTab> with SpectrumSurface {
context: context,
barrierDismissible: true,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: Colors.black54,
barrierColor: AppFrost.scrim(),
transitionDuration: const Duration(milliseconds: 320),
pageBuilder: (_, _, _) => const Align(
alignment: Alignment.topCenter,
@@ -240,7 +243,7 @@ class _ContactsTabState extends State<ContactsTab> with SpectrumSurface {
color: cs.onSurface,
fontSize: 24,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
const ConnectionStatusLine(),
@@ -495,9 +498,7 @@ class _SearchContactSheetState extends State<_SearchContactSheet> {
selected: {_mode},
onSelectionChanged: (s) => _setMode(s.first),
showSelectedIcon: false,
style: ButtonStyle(
visualDensity: VisualDensity.compact,
),
style: ButtonStyle(visualDensity: VisualDensity.compact),
),
const SizedBox(height: 12),
TextField(
@@ -570,9 +571,7 @@ class _SearchContactSheetState extends State<_SearchContactSheet> {
FilledButton(
onPressed: _loading ? null : _submit,
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
shape: AppShape.buttonBorder,
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: _loading
@@ -6,6 +6,8 @@ import 'package:komet/frontend/widgets/custom_notification.dart';
import 'package:komet/frontend/widgets/komet_avatar.dart';
import 'package:komet/l10n/app_localizations.dart';
import 'package:komet/main.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../core/config/app_shape.dart';
enum EditContactAction { updated, removed }
@@ -132,6 +134,7 @@ class _EditContactCardState extends State<_EditContactCard> {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape: AppShape.dialogBorder,
title: Text(l10n.editContactDeleteConfirmTitle),
content: Text(l10n.editContactDeleteConfirmBody),
actions: [
@@ -158,9 +161,9 @@ class _EditContactCardState extends State<_EditContactCard> {
return;
}
Navigator.of(context).pop(
const EditContactResult(EditContactAction.removed),
);
Navigator.of(
context,
).pop(const EditContactResult(EditContactAction.removed));
}
@override
@@ -260,14 +263,12 @@ class _EditContactCardState extends State<_EditContactCard> {
),
if (hasText)
InkWell(
onTap: _busy
? null
: () => setState(() => controller.clear()),
onTap: _busy ? null : () => setState(() => controller.clear()),
borderRadius: BorderRadius.circular(20),
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(
Icons.close,
Symbols.close,
size: 18,
color: cs.onSurfaceVariant,
),
@@ -294,7 +295,10 @@ class _EditContactCardState extends State<_EditContactCard> {
)
: Text(
l10n.editContactSave,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
);
@@ -313,14 +317,19 @@ class _EditContactCardState extends State<_EditContactCard> {
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: cs.error),
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.error,
),
)
: Text(
l10n.editContactDelete,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
);
}
}
@@ -17,6 +17,7 @@ import '../../../models/contact_info.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/small_spinner.dart';
import '../../../core/config/app_shape.dart';
enum _Stage {
checking,
@@ -159,7 +160,10 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
);
if (!mounted) return;
setState(() => _stage = _Stage.added);
showCustomNotification(context, AppLocalizations.of(context)!.nfcContactAdded);
showCustomNotification(
context,
AppLocalizations.of(context)!.nfcContactAdded,
);
await Future.delayed(const Duration(milliseconds: 700));
if (mounted) Navigator.pop(context);
} catch (e) {
@@ -412,7 +416,8 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
),
const SizedBox(height: 4),
Text(
formatPhone(_peerPhone) ?? l10n.nfcPeerIdFallback('${_peerId ?? ''}'),
formatPhone(_peerPhone) ??
l10n.nfcPeerIdFallback('${_peerId ?? ''}'),
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
const SizedBox(height: 24),
@@ -421,9 +426,7 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
child: FilledButton(
onPressed: (_stage == _Stage.adding || loading) ? null : _add,
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
shape: AppShape.buttonBorder,
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: _stage == _Stage.adding
+2 -3
View File
@@ -14,6 +14,7 @@ import '../widgets/chat_menu_overlay.dart';
import '../widgets/confirm_dialog.dart';
import '../widgets/custom_notification.dart';
import '../widgets/small_spinner.dart';
import '../widgets/sheet_helpers.dart';
class DownloadsScreen extends StatefulWidget {
const DownloadsScreen({super.key});
@@ -116,9 +117,7 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
final clear = await showModalBottomSheet<bool>(
context: context,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(22)),
),
shape: kSheetShape,
builder: (sheetContext) => SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
@@ -5,8 +5,8 @@ import '../../widgets/connection_status.dart';
import '../../../core/config/app_icon.dart';
import '../../../core/utils/haptics.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/settings_radio_tile.dart';
import '../../widgets/settings_card.dart';
class AppIconScreen extends StatefulWidget {
const AppIconScreen({super.key});
@@ -57,11 +57,8 @@ class _AppIconScreenState extends State<AppIconScreen> {
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 120),
children: [
GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
SettingsPanel(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 12),
depth: 6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -19,8 +19,9 @@ import '../../../core/utils/debouncer.dart';
import '../../../core/utils/haptics.dart';
import '../../../l10n/app_localizations.dart';
import '../../../main.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/liquid_glass.dart';
import '../../widgets/settings_card.dart';
import '../../../core/config/app_shape.dart';
class AppearanceScreen extends StatefulWidget {
const AppearanceScreen({super.key});
@@ -141,7 +142,9 @@ class _AppearanceScreenState extends State<AppearanceScreen> {
void _applyVisualStyle(VisualStyle style) {
AppVisualStyle.save(style);
if (style == VisualStyle.liquidGlass) {
AppNavPillStyle.save(NavPillStyle.liquidGlass);
if (AppNavPillStyle.current.value != NavPillStyle.auto) {
AppNavPillStyle.save(NavPillStyle.liquidGlass);
}
AppComposerBackground.save(ComposerBackground.liquidGlass);
AppChatChrome.save(ChatChromeStyle.liquidGlass);
return;
@@ -164,11 +167,7 @@ class _VisualStyleCard extends StatelessWidget {
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
depth: 6,
return SettingsPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -233,11 +232,7 @@ class _ChatChromeCard extends StatelessWidget {
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
depth: 6,
return SettingsPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -314,11 +309,7 @@ class _ComposerBarCard extends StatelessWidget {
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
depth: 6,
return SettingsPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -339,24 +330,32 @@ class _ComposerBarCard extends StatelessWidget {
ValueListenableBuilder<ComposerStyle>(
valueListenable: AppComposerStyle.current,
builder: (context, current, _) {
return SegmentedButton<ComposerStyle>(
segments: [
ButtonSegment(
value: ComposerStyle.glossy,
label: Text(l10n.appearanceVisualStyleGlossy),
),
ButtonSegment(
value: ComposerStyle.materialYou,
label: Text(l10n.appearanceVisualStyleMaterialYou),
),
],
selected: {current},
onSelectionChanged: (set) {
if (set.isNotEmpty) {
Haptics.selection();
AppComposerStyle.save(set.first);
}
},
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SegmentedButton<ComposerStyle>(
showSelectedIcon: false,
segments: [
ButtonSegment(
value: ComposerStyle.auto,
label: Text(l10n.appearanceStyleAuto),
),
ButtonSegment(
value: ComposerStyle.glossy,
label: Text(l10n.appearanceVisualStyleGlossy),
),
ButtonSegment(
value: ComposerStyle.materialYou,
label: Text(l10n.appearanceVisualStyleMaterialYou),
),
],
selected: {current},
onSelectionChanged: (set) {
if (set.isNotEmpty) {
Haptics.selection();
AppComposerStyle.save(set.first);
}
},
),
);
},
),
@@ -409,11 +408,7 @@ class _NavPillStyleCard extends StatelessWidget {
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
depth: 6,
return SettingsPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -439,30 +434,37 @@ class _NavPillStyleCard extends StatelessWidget {
!LiquidGlass.isSupported
? NavPillStyle.frostBlur
: current;
return SegmentedButton<NavPillStyle>(
showSelectedIcon: false,
segments: [
ButtonSegment(
value: NavPillStyle.glossy,
label: Text(l10n.appearanceNavPillGlossy),
),
ButtonSegment(
value: NavPillStyle.frostBlur,
label: Text(l10n.appearanceNavPillFrost),
),
if (LiquidGlass.isSupported)
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SegmentedButton<NavPillStyle>(
showSelectedIcon: false,
segments: [
ButtonSegment(
value: NavPillStyle.liquidGlass,
label: Text(l10n.appearanceGlassMaterial),
value: NavPillStyle.auto,
label: Text(l10n.appearanceStyleAuto),
),
],
selected: {selectable},
onSelectionChanged: (set) {
if (set.isNotEmpty) {
Haptics.selection();
AppNavPillStyle.save(set.first);
}
},
ButtonSegment(
value: NavPillStyle.glossy,
label: Text(l10n.appearanceNavPillGlossy),
),
ButtonSegment(
value: NavPillStyle.frostBlur,
label: Text(l10n.appearanceNavPillFrost),
),
if (LiquidGlass.isSupported)
ButtonSegment(
value: NavPillStyle.liquidGlass,
label: Text(l10n.appearanceGlassMaterial),
),
],
selected: {selectable},
onSelectionChanged: (set) {
if (set.isNotEmpty) {
Haptics.selection();
AppNavPillStyle.save(set.first);
}
},
),
);
},
),
@@ -479,11 +481,8 @@ class _GradientToggleCard extends StatelessWidget {
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
return SettingsPanel(
padding: const EdgeInsets.fromLTRB(20, 14, 12, 14),
depth: 6,
child: Row(
children: [
Icon(Symbols.blur_on, color: cs.onSurface, size: 24, weight: 500),
@@ -531,11 +530,8 @@ class _SpectrumToggleCard extends StatelessWidget {
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
return SettingsPanel(
padding: const EdgeInsets.fromLTRB(20, 14, 12, 14),
depth: 6,
child: Row(
children: [
Icon(Symbols.graphic_eq, color: cs.onSurface, size: 24, weight: 500),
@@ -675,11 +671,9 @@ class _ChatPreview extends StatelessWidget {
builder: (context, _) {
final style = AppBubbleShape.current.value;
final behavior = AppBubbleBehavior.current.value;
return GlossyPill(
return SettingsPanel(
color: cs.surfaceContainerLow,
borderRadius: BorderRadius.circular(28),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
depth: 6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -781,10 +775,7 @@ class _ColorPickerCard extends StatelessWidget {
) {
final swatchColor = sys ? cs.primary : col;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
depth: 6,
return SettingsPanel(
child: Column(
children: [
InkWell(
@@ -863,9 +854,7 @@ class _ColorPickerCard extends StatelessWidget {
child: FilledButton.tonal(
onPressed: sys ? null : onReset,
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
shape: AppShape.buttonBorder,
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -906,11 +895,7 @@ class _BubbleShapeCard extends StatelessWidget {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
depth: 6,
return SettingsPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -967,11 +952,7 @@ class _BubbleBehaviorCard extends StatelessWidget {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
depth: 6,
return SettingsPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -8,6 +8,8 @@ import '../../widgets/chat_wallpaper_sheet.dart';
import '../../widgets/chat_wallpaper_view.dart';
import '../../widgets/custom_notification.dart';
import '../chats/chat_wallpaper_preview_screen.dart';
import '../../../core/config/app_fonts.dart';
import '../../../core/config/app_shape.dart';
class ChatBackgroundScreen extends StatefulWidget {
const ChatBackgroundScreen({super.key});
@@ -33,8 +35,10 @@ class _ChatBackgroundScreenState extends State<ChatBackgroundScreen> {
if (!mounted) return;
setState(() {
_accountId = profile?.id ?? 0;
_wallpaper = ChatWallpaperStore.instance
.get(_accountId, kGlobalWallpaperChatId);
_wallpaper = ChatWallpaperStore.instance.get(
_accountId,
kGlobalWallpaperChatId,
);
_ready = true;
});
}
@@ -42,8 +46,10 @@ class _ChatBackgroundScreenState extends State<ChatBackgroundScreen> {
void _refresh() {
if (!mounted) return;
setState(() {
_wallpaper = ChatWallpaperStore.instance
.get(_accountId, kGlobalWallpaperChatId);
_wallpaper = ChatWallpaperStore.instance.get(
_accountId,
kGlobalWallpaperChatId,
);
});
}
@@ -109,12 +115,12 @@ class _ChatBackgroundScreenState extends State<ChatBackgroundScreen> {
appBar: AppBar(
backgroundColor: cs.surface,
surfaceTintColor: Colors.transparent,
title: const Text(
title: Text(
'Фон чатов',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -134,7 +140,7 @@ class _ChatBackgroundScreenState extends State<ChatBackgroundScreen> {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
child: ClipRRect(
borderRadius: BorderRadius.circular(28),
borderRadius: AppShape.cardRadius,
child: Stack(
fit: StackFit.expand,
children: [
@@ -222,7 +228,7 @@ class _ChatBackgroundScreenState extends State<ChatBackgroundScreen> {
color: cs.onPrimary,
fontSize: 16,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -250,6 +256,7 @@ class _SampleBubbles extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_bubble(
context,
text: 'Единый фон для всех чатов',
color: cs.surfaceContainerHighest.withValues(alpha: 0.94),
textColor: cs.onSurface,
@@ -257,6 +264,7 @@ class _SampleBubbles extends StatelessWidget {
),
const SizedBox(height: 8),
_bubble(
context,
text: 'Красиво ✨',
color: cs.primary,
textColor: cs.onPrimary,
@@ -268,7 +276,8 @@ class _SampleBubbles extends StatelessWidget {
);
}
Widget _bubble({
Widget _bubble(
BuildContext context, {
required String text,
required Color color,
required Color textColor,
@@ -289,7 +298,7 @@ class _SampleBubbles extends StatelessWidget {
style: TextStyle(
color: textColor,
fontSize: 15,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -20,6 +20,7 @@ import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/sheet_helpers.dart';
import '../../widgets/small_spinner.dart';
import '../../../core/config/app_shape.dart';
enum _EnvState { loading, notConfigured, ready }
@@ -416,9 +417,7 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
horizontal: 32,
vertical: 14,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
shape: AppShape.buttonBorder,
),
child: _isCreatingEnv
? SmallSpinner(size: 18, color: cs.onPrimary)
@@ -608,9 +607,7 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
horizontal: 32,
vertical: 14,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
shape: AppShape.buttonBorder,
),
child: Text(
l10n.cloudStorageUpload,
@@ -1037,7 +1034,9 @@ class _FileDetailsSheetState extends State<_FileDetailsSheet> {
return Container(
decoration: BoxDecoration(
color: cs.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppShape.sheet),
),
),
padding: EdgeInsets.fromLTRB(
24,
@@ -1199,7 +1198,9 @@ class _SendByIdSheetState extends State<_SendByIdSheet> {
return Container(
decoration: BoxDecoration(
color: cs.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppShape.sheet),
),
),
padding: EdgeInsets.fromLTRB(
24,
@@ -1248,9 +1249,7 @@ class _SendByIdSheetState extends State<_SendByIdSheet> {
onPressed: _sending ? null : _submit,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
shape: AppShape.buttonBorder,
),
child: _sending
? SmallSpinner(size: 18, color: cs.onPrimary)
@@ -18,6 +18,7 @@ import '../../widgets/prompt_dialog.dart';
import '../../widgets/small_spinner.dart';
import '../../widgets/web_qr_login.dart';
import 'web_qr_scan_screen.dart';
import '../../../core/config/app_fonts.dart';
class DevicesScreen extends StatefulWidget {
const DevicesScreen({super.key});
@@ -212,7 +213,7 @@ class _DevicesScreenState extends State<DevicesScreen>
title: ConnectionTitleText(
l10n.devicesTitle,
style: TextStyle(
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
fontSize: 20,
fontWeight: FontWeight.w600,
color: cs.onSurface,
@@ -264,7 +265,7 @@ class _DevicesScreenState extends State<DevicesScreen>
Text(
l10n.devicesPromoTitle,
style: TextStyle(
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
fontSize: 18,
fontWeight: FontWeight.w700,
color: cs.onSurface,
@@ -287,7 +288,7 @@ class _DevicesScreenState extends State<DevicesScreen>
label: Text(
l10n.devicesScanQrButton,
style: TextStyle(
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
fontSize: 15,
fontWeight: FontWeight.w600,
),
@@ -463,7 +464,7 @@ class _DevicesScreenState extends State<DevicesScreen>
Text(
title,
style: TextStyle(
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
fontSize: 16,
fontWeight: FontWeight.w700,
color: cs.onSurface,
@@ -9,8 +9,8 @@ import '../../../l10n/app_localizations.dart';
import '../../../main.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/prompt_dialog.dart';
import '../../widgets/settings_card.dart';
class FontSettingsScreen extends StatefulWidget {
const FontSettingsScreen({super.key});
@@ -198,11 +198,8 @@ class _PreviewCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
return SettingsPanel(
padding: const EdgeInsets.all(24),
depth: 6,
child: SizedBox(
width: double.infinity,
child: Column(
@@ -337,11 +334,8 @@ class _FontSizeControl extends StatelessWidget {
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final isDefault = (scale - AppFonts.defaultScale).abs() < 0.001;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
return SettingsPanel(
padding: const EdgeInsets.fromLTRB(20, 16, 12, 16),
depth: 6,
child: Column(
children: [
Row(
+237 -79
View File
@@ -83,9 +83,12 @@ class _InfoScreenState extends State<InfoScreen> {
Widget _buildContent(ColorScheme cs, AppLocalizations l10n) {
final info = _info!;
final server = info['server'] as Map<String, dynamic>?;
final user = info['user'] as Map<String, dynamic>?;
final yMap = server?['y-map'] as Map<String, dynamic>?;
final chats = _asStringMap(info['chats']);
final server = _asStringMap(info['server']);
final user = _asStringMap(info['user']);
final experiments = _asStringMap(info['experiments']);
final chatSettings = _asStringMap(info['chatSettings']);
final yMap = _asStringMap(server?['y-map']);
final accountKeys = <String, String>{
'registrationTime': l10n.infoRegistrationTime,
@@ -93,7 +96,36 @@ class _InfoScreenState extends State<InfoScreen> {
'videoChatHistory': l10n.infoVideoChatHistory,
'updateTime': l10n.infoUpdateTime,
'id': l10n.infoId,
'phone': l10n.infoPhone,
'photoId': l10n.infoPhotoId,
'accountStatus': l10n.infoAccountStatus,
'contactOptions': l10n.infoContactOptions,
'profileOptions': l10n.infoProfileOptions,
'names': l10n.infoNames,
'baseUrl': l10n.infoBaseUrl,
'baseRawUrl': l10n.infoBaseRawUrl,
};
final packetKeys = <String, String>{
'chatMarker': l10n.infoChatMarker,
'time': l10n.infoServerTime,
'updates': l10n.infoUpdates,
'messagesCount': l10n.infoMessagesCount,
'contactsCount': l10n.infoContactsCount,
'presenceCount': l10n.infoPresenceCount,
'configHash': l10n.infoConfigHash,
};
final chatKeys = <String, String>{
'count': l10n.infoChatsCount,
'active': l10n.infoChatsActive,
'hidden': l10n.infoChatsHidden,
'dialogs': l10n.infoChatsDialogs,
'groups': l10n.infoChatsGroups,
'channels': l10n.infoChatsChannels,
'unread': l10n.infoChatsUnread,
'newMessages': l10n.infoChatsNewMessages,
'messages': l10n.infoChatsMessages,
};
final serverKeys = <String, String>{
@@ -117,66 +149,153 @@ class _InfoScreenState extends State<InfoScreen> {
'reactions-enabled': l10n.infoReactionsEnabled,
};
return ListView(
padding: const EdgeInsets.all(16),
children: [
SectionHeader(l10n.infoAccountSection),
...accountKeys.entries.map(
(e) =>
_buildRow(e.key, e.value, _formatValue(info[e.key], e.key), cs),
),
return SelectionArea(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
SectionHeader(l10n.infoAccountSection),
...accountKeys.entries.map(
(entry) =>
_buildDataRow(entry.key, entry.value, info[entry.key], cs),
),
const SizedBox(height: 16),
SectionHeader(l10n.infoServerSection),
...serverKeys.entries.map(
(e) => _buildRow(
e.key,
e.value,
_formatValue(server?[e.key], e.key),
const SizedBox(height: 16),
SectionHeader(l10n.infoPacketSection),
...packetKeys.entries.map(
(entry) =>
_buildDataRow(entry.key, entry.value, info[entry.key], cs),
),
const SizedBox(height: 16),
SectionHeader(l10n.infoChatsSection),
...chatKeys.entries.map(
(entry) => _buildDataRow(
'chats.${entry.key}',
entry.value,
chats?[entry.key],
cs,
),
),
const SizedBox(height: 16),
SectionHeader(l10n.infoServerSection),
...serverKeys.entries.map(
(entry) =>
_buildDataRow(entry.key, entry.value, server?[entry.key], cs),
),
..._buildDynamicRows(
server,
cs,
excludedKeys: {
...serverKeys.keys,
'y-map',
'file-upload-unsupported-types',
'white-list-links',
},
),
const SizedBox(height: 8),
SectionHeader(l10n.infoYMapSection),
_buildDataRow('y-map.tile', l10n.infoTile, yMap?['tile'], cs),
_buildDataRow(
'y-map.geocoder',
l10n.infoGeocoder,
yMap?['geocoder'],
cs,
),
),
_buildDataRow('y-map.static', l10n.infoStatic, yMap?['static'], cs),
const SizedBox(height: 8),
SectionHeader(l10n.infoYMapSection),
_buildRow('tile', l10n.infoTile, yMap?['tile']?.toString() ?? '-', cs),
_buildRow(
'geocoder',
l10n.infoGeocoder,
yMap?['geocoder']?.toString() ?? '-',
cs,
),
_buildRow(
'static',
l10n.infoStatic,
yMap?['static']?.toString() ?? '-',
cs,
),
const SizedBox(height: 8),
SectionHeader(l10n.infoFileUploadTypes),
_buildListValueRow(
'file-upload-unsupported-types',
l10n.infoFileUploadTypes,
server?['file-upload-unsupported-types'] as List?,
cs,
showLabel: false,
),
const SizedBox(height: 8),
SectionHeader(l10n.infoFileUploadTypes),
_buildListRow(server?['file-upload-unsupported-types'] as List?, cs),
const SizedBox(height: 8),
SectionHeader(l10n.infoWhiteListLinks),
_buildListValueRow(
'white-list-links',
l10n.infoWhiteListLinks,
server?['white-list-links'] as List?,
cs,
showLabel: false,
),
const SizedBox(height: 8),
SectionHeader(l10n.infoWhiteListLinks),
_buildListRow(server?['white-list-links'] as List?, cs),
if (chatSettings != null && chatSettings.isNotEmpty) ...[
const SizedBox(height: 8),
SectionHeader(l10n.infoChatSettingsSection),
..._buildDynamicRows(chatSettings, cs),
],
const SizedBox(height: 8),
SectionHeader(l10n.infoUserSection),
if (user != null)
...user.entries
.where((e) => e.value != null)
.map((e) => _buildRow(e.key, e.key, e.value.toString(), cs)),
if (experiments != null && experiments.isNotEmpty) ...[
const SizedBox(height: 8),
SectionHeader(l10n.infoExperimentsSection),
..._buildDynamicRows(experiments, cs),
],
const SizedBox(height: 120),
],
const SizedBox(height: 8),
SectionHeader(l10n.infoUserSection),
..._buildDynamicRows(user, cs),
const SizedBox(height: 120),
],
),
);
}
List<Widget> _buildDynamicRows(
Map<String, dynamic>? values,
ColorScheme cs, {
Set<String> excludedKeys = const {},
}) {
if (values == null) return [];
final entries = <MapEntry<String, dynamic>>[];
for (final entry in values.entries) {
if (excludedKeys.contains(entry.key) || entry.value == null) continue;
_flattenEntry(entry.key, entry.value, entries);
}
entries.sort((a, b) => a.key.compareTo(b.key));
return entries
.map((entry) => _buildDataRow(entry.key, entry.key, entry.value, cs))
.toList();
}
void _flattenEntry(
String key,
dynamic value,
List<MapEntry<String, dynamic>> target,
) {
final map = _asStringMap(value);
if (map == null || map.isEmpty) {
target.add(MapEntry(key, value));
return;
}
for (final entry in map.entries) {
_flattenEntry('$key.${entry.key}', entry.value, target);
}
}
Widget _buildDataRow(
String key,
String label,
dynamic value,
ColorScheme cs,
) {
if (value is List) {
return _buildListValueRow(key, label, value, cs);
}
return _buildRow(key, label, _formatValue(value, key), cs);
}
Widget _buildRow(String key, String label, String value, ColorScheme cs) {
return Padding(
padding: const EdgeInsets.only(bottom: 1),
child: GlossyPill(
key: ValueKey('info-$key'),
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
@@ -213,42 +332,75 @@ class _InfoScreenState extends State<InfoScreen> {
);
}
Widget _buildListRow(List? items, ColorScheme cs) {
if (items == null || items.isEmpty) {
return GlossyPill(
Widget _buildListValueRow(
String key,
String label,
List? items,
ColorScheme cs, {
bool showLabel = true,
}) {
final values = items ?? const [];
final simple = values.every(
(item) => item == null || item is String || item is num || item is bool,
);
return Padding(
padding: const EdgeInsets.only(bottom: 1),
child: GlossyPill(
key: ValueKey('info-$key'),
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
padding: const EdgeInsets.all(16),
depth: 6,
child: Text('-', style: TextStyle(color: cs.onSurfaceVariant)),
);
}
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
padding: const EdgeInsets.all(16),
depth: 6,
child: Wrap(
spacing: 8,
runSpacing: 4,
children: items
.map(
(item) => Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Text(
item.toString(),
style: TextStyle(fontSize: 13, color: cs.onSurface),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showLabel) ...[
Text(
label,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
)
.toList(),
const SizedBox(height: 8),
],
if (values.isEmpty)
Text('-', style: TextStyle(color: cs.onSurfaceVariant))
else if (simple)
Wrap(
spacing: 8,
runSpacing: 4,
children: values
.map(
(item) => Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Text(
_formatValue(item, key),
style: TextStyle(fontSize: 13, color: cs.onSurface),
),
),
)
.toList(),
)
else
Text(
const JsonEncoder.withIndent(' ').convert(values),
style: TextStyle(
color: cs.onSurface,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
@@ -259,6 +411,7 @@ class _InfoScreenState extends State<InfoScreen> {
final ts = value['chatMarker'] as int?;
return ts != null ? _formatTs(ts) : '-';
}
if (key == 'phone' && value is num && value > 0) return '+$value';
if (value is int && value > 1000000000000) return _formatTs(value);
if (key == 'edit-timeout' && value is int && value > 0) {
final weeks = value ~/ 604800;
@@ -280,4 +433,9 @@ class _InfoScreenState extends State<InfoScreen> {
return '${dt.year}-${pad2(dt.month)}-${pad2(dt.day)} '
'${pad2(dt.hour)}:${pad2(dt.minute)}:${pad2(dt.second)}';
}
Map<String, dynamic>? _asStringMap(dynamic value) {
if (value is! Map) return null;
return value.map((key, item) => MapEntry(key.toString(), item));
}
}
@@ -3,6 +3,7 @@ import 'package:lottie/lottie.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../core/config/app_animations.dart';
import '../../../core/config/app_fonts.dart';
class LottiePolygonScreen extends StatelessWidget {
const LottiePolygonScreen({super.key});
@@ -32,7 +33,7 @@ class LottiePolygonScreen extends StatelessWidget {
title: Text(
'Lottie полигон',
style: TextStyle(
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
fontSize: 20,
fontWeight: FontWeight.w600,
color: cs.onSurface,
@@ -47,9 +48,7 @@ class LottiePolygonScreen extends StatelessWidget {
crossAxisCount: 3,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
children: [
for (final entry in _entries) _PolygonTile(entry: entry),
],
children: [for (final entry in _entries) _PolygonTile(entry: entry)],
),
),
);
@@ -113,14 +112,10 @@ class _PolygonTileState extends State<_PolygonTile>
fit: BoxFit.contain,
delegates: LottieDelegates(
values: [
ValueDelegate.color(
const ['**'],
value: cs.onSurface,
),
ValueDelegate.strokeColor(
const ['**'],
value: cs.onSurface,
),
ValueDelegate.color(const ['**'], value: cs.onSurface),
ValueDelegate.strokeColor(const [
'**',
], value: cs.onSurface),
],
),
onLoaded: (composition) {
@@ -5,8 +5,8 @@ import '../../widgets/connection_status.dart';
import '../../../core/config/app_message_actions_style.dart';
import '../../../core/utils/haptics.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/settings_radio_tile.dart';
import '../../widgets/settings_card.dart';
class MessageActionsScreen extends StatelessWidget {
const MessageActionsScreen({super.key});
@@ -53,11 +53,8 @@ class _StyleCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
return SettingsPanel(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 12),
depth: 6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -9,6 +9,8 @@ import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/primary_loading_button.dart';
import '../../widgets/small_spinner.dart';
import '../../../core/config/app_fonts.dart';
import '../../../core/config/app_shape.dart';
class PasswordEntryScreen extends StatefulWidget {
const PasswordEntryScreen({super.key});
@@ -74,6 +76,7 @@ class _PasswordEntryScreenState extends State<PasswordEntryScreen> {
return await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
shape: AppShape.dialogBorder,
title: Text(l10n.passwordEntryConfirmTitle),
content: TextField(
controller: controller,
@@ -189,7 +192,7 @@ class _PasswordEntryScreenState extends State<PasswordEntryScreen> {
color: cs.onSurface,
fontSize: 20,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
],
@@ -390,7 +393,7 @@ class _PasswordEntryScreenState extends State<PasswordEntryScreen> {
),
_buildActionRow(
cs,
icon: Icons.email_outlined,
icon: Symbols.mail,
label: l10n.passwordEntryChangeEmailAction,
isLast: false,
onTap: () => _openWithPassword(
@@ -403,7 +406,7 @@ class _PasswordEntryScreenState extends State<PasswordEntryScreen> {
),
_buildActionRow(
cs,
icon: Icons.delete_outline,
icon: Symbols.delete,
label: l10n.passwordEntryDeleteAction,
isLast: true,
textColor: cs.error,
@@ -478,7 +481,9 @@ class _PasswordEntryScreenState extends State<PasswordEntryScreen> {
child: InkWell(
onTap: onTap,
borderRadius: isLast
? const BorderRadius.vertical(bottom: Radius.circular(20))
? const BorderRadius.vertical(
bottom: Radius.circular(AppShape.card),
)
: BorderRadius.zero,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17),
@@ -4,7 +4,7 @@ import '../../widgets/connection_status.dart';
import '../../../core/config/app_cache_extent.dart';
import '../../../core/utils/haptics.dart';
import '../../widgets/confirm_dialog.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/settings_card.dart';
class PerformanceScreen extends StatefulWidget {
const PerformanceScreen({super.key});
@@ -103,11 +103,7 @@ class _PerformanceScreenState extends State<PerformanceScreen> {
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 120),
children: [
GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
depth: 6,
SettingsPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -15,6 +15,8 @@ import '../../widgets/glossy_pill.dart';
import '../../widgets/sheet_helpers.dart';
import '../../widgets/small_spinner.dart';
import 'password_entry_screen.dart';
import '../../../core/config/app_fonts.dart';
import '../../../core/config/app_shape.dart';
class SecurityScreen extends StatefulWidget {
const SecurityScreen({super.key});
@@ -217,7 +219,7 @@ class _SecurityScreenState extends State<SecurityScreen>
color: cs.onSurface,
fontSize: 20,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
const Spacer(),
@@ -250,7 +252,7 @@ class _SecurityScreenState extends State<SecurityScreen>
final l10n = AppLocalizations.of(context)!;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
borderRadius: AppShape.cardRadius,
depth: 6,
child: Column(
children: [
@@ -351,7 +353,7 @@ class _SecurityScreenState extends State<SecurityScreen>
final isSafeMode = _privacyConfig?.safeMode ?? false;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
borderRadius: AppShape.cardRadius,
depth: 6,
child: Column(
children: [
@@ -532,7 +534,7 @@ class _SecurityScreenState extends State<SecurityScreen>
),
_settingsRow(
cs,
icon: Icons.visibility_off_outlined,
icon: Symbols.visibility_off,
label: l10n.securityShowOnlineStatus,
trailingText: _privacyConfig?.hidden == true
? l10n.securityPrivacyNobody
@@ -713,7 +715,7 @@ class _SecurityScreenState extends State<SecurityScreen>
_privacyConfig?.audioTranscriptionEnabled ?? true;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
borderRadius: AppShape.cardRadius,
depth: 6,
child: Column(
children: [
@@ -758,7 +760,7 @@ class _SecurityScreenState extends State<SecurityScreen>
),
_settingsRow(
cs,
icon: Icons.mic_none_outlined,
icon: Symbols.mic,
label: l10n.securityAudioTranscription,
trailingWidget: Switch(
value: audioTranscription,
@@ -783,7 +785,7 @@ class _SecurityScreenState extends State<SecurityScreen>
final count = _blockedContacts.length;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
borderRadius: AppShape.cardRadius,
depth: 6,
child: Material(
color: Colors.transparent,
@@ -873,7 +875,9 @@ class _SecurityScreenState extends State<SecurityScreen>
child: InkWell(
onTap: onTap ?? () => showCustomNotification(context, label),
borderRadius: isLast
? const BorderRadius.vertical(bottom: Radius.circular(20))
? const BorderRadius.vertical(
bottom: Radius.circular(AppShape.card),
)
: BorderRadius.zero,
child: Padding(
padding: EdgeInsets.symmetric(
@@ -46,6 +46,8 @@ import 'notifications_screen.dart';
import 'security_screen.dart';
import 'spoof_screen.dart';
import '../../widgets/media_playback_pill.dart';
import '../../../core/config/app_fonts.dart';
import '../../../core/config/app_shape.dart';
class SettingsTab extends StatefulWidget {
const SettingsTab({super.key});
@@ -290,9 +292,7 @@ class _SettingsTabState extends State<SettingsTab> with SpectrumSurface {
backgroundColor: cs.error,
foregroundColor: cs.onError,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
shape: AppShape.buttonBorder,
),
child: const Text('Выйти'),
),
@@ -825,7 +825,7 @@ class _SettingsTabState extends State<SettingsTab> with SpectrumSurface {
color: nameColor,
fontSize: lerpDouble(20, 26, pt),
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -1025,7 +1025,7 @@ class _SettingsTabState extends State<SettingsTab> with SpectrumSurface {
Symbols.check_circle,
fill: 1,
size: 15,
color: online ? kOnlineGreen : cs.mutedText,
color: online ? kSuccessGreen : cs.mutedText,
),
const SizedBox(width: 5),
Text(
+280 -308
View File
@@ -6,6 +6,7 @@ import 'dart:math';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../core/config/device_presets.dart';
import '../../../core/storage/device_identity.dart';
@@ -17,7 +18,10 @@ import '../../../main.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/info_action_sheet.dart';
import '../../../core/config/app_colors.dart';
import '../../../core/config/app_shape.dart';
import '../../widgets/section_header.dart';
import '../../widgets/settings_card.dart';
import '../../widgets/small_spinner.dart';
import '../auth/login_screen.dart';
@@ -74,7 +78,7 @@ class _SpoofScreenState extends State<SpoofScreen> {
Future<bool> _confirmFullSpoofing() {
return showInfoActionSheet(
context,
headerIcon: Icons.warning_amber_rounded,
headerIcon: Symbols.warning,
title: 'Могут быть последствия.',
subtitle: 'Меняй, только если знаешь что делаешь.',
confirmLabel: 'ОК',
@@ -328,6 +332,7 @@ class _SpoofScreenState extends State<SpoofScreen> {
final confirmed = await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
shape: AppShape.dialogBorder,
title: Text(l10n.spoofDialogApplyTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
@@ -436,31 +441,39 @@ class _SpoofScreenState extends State<SpoofScreen> {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: ConnectionTitleText(l10n.spoofScreenTitle),
centerTitle: true,
backgroundColor: cs.surface,
appBar: ConnectionTitleBar(
titleText: l10n.spoofScreenTitle,
backgroundColor: cs.surface,
),
body: _isLoading
? const Center(child: SmallSpinner(size: 36))
: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 120),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
: SafeArea(
top: false,
child: ListView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 120),
children: [
_buildEnableCard(),
const SizedBox(height: 16),
const SizedBox(height: 12),
_buildInfoCard(),
const SizedBox(height: 16),
const SizedBox(height: 20),
_sectionHeader(l10n.spoofMethodTitle),
_buildSpoofingMethodCard(),
const SizedBox(height: 16),
const SizedBox(height: 20),
_sectionHeader(l10n.spoofDeviceTypeTitle),
_buildDeviceTypeCard(),
const SizedBox(height: 24),
const SizedBox(height: 20),
_sectionHeader(l10n.spoofMainSectionTitle),
_buildMainDataCard(),
const SizedBox(height: 16),
const SizedBox(height: 20),
_sectionHeader(l10n.spoofRegionalSectionTitle),
_buildRegionalDataCard(),
const SizedBox(height: 16),
const SizedBox(height: 20),
_sectionHeader(l10n.spoofIdentifiersSectionTitle),
_buildIdentifiersCard(),
],
),
@@ -470,62 +483,61 @@ class _SpoofScreenState extends State<SpoofScreen> {
);
}
Widget _sectionHeader(String title) => SectionHeader(
title,
padding: const EdgeInsets.fromLTRB(8, 0, 8, 8),
fontSize: 14,
);
Widget _buildEnableCard() {
final l10n = AppLocalizations.of(context)!;
return Card(
child: SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
title: Text(
l10n.spoofEnableTitle,
style: Theme.of(context).textTheme.titleMedium,
),
subtitle: Text(
_spoofingEnabled
return SettingsCard(
children: [
SettingsToggleTile(
icon: Symbols.security,
label: l10n.spoofEnableTitle,
subtitle: _spoofingEnabled
? l10n.spoofEnableSubtitleOn
: l10n.spoofEnableSubtitleOff,
value: _spoofingEnabled,
onChanged: (value) async {
if (value) {
await _applyGeneratedData();
} else {
await _loadDeviceData();
}
},
),
value: _spoofingEnabled,
onChanged: (value) async {
if (value) {
await _applyGeneratedData();
} else {
await _loadDeviceData();
}
},
),
],
);
}
Widget _buildInfoCard() {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return Card(
color: Theme.of(
context,
).colorScheme.secondaryContainer.withValues(alpha: 0.5),
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.touch_app,
size: 18,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
const SizedBox(width: 8),
Flexible(
child: Text(
l10n.spoofInfoHint,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
return SettingsPanel(
color: cs.secondaryContainer.withValues(alpha: 0.5),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
child: Row(
children: [
Icon(
Symbols.touch_app,
size: 20,
weight: 400,
color: cs.onSecondaryContainer,
),
const SizedBox(width: 16),
Expanded(
child: Text(
l10n.spoofInfoHint,
style: TextStyle(
fontSize: 13,
height: 1.3,
color: cs.onSecondaryContainer,
),
),
],
),
),
],
),
);
}
@@ -537,37 +549,35 @@ class _SpoofScreenState extends State<SpoofScreen> {
if (_selectedMethod == SpoofingMethod.partial) {
descriptionWidget = _buildDescriptionTile(
icon: Icons.check_circle_outline,
color: Colors.green.shade700,
icon: Symbols.check_circle,
color: kSuccessGreen,
text: l10n.spoofMethodPartialDescription,
);
} else {
descriptionWidget = _buildDescriptionTile(
icon: Icons.warning_amber_rounded,
icon: Symbols.warning,
color: theme.colorScheme.error,
text: l10n.spoofMethodFullDescription,
);
}
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text(l10n.spoofMethodTitle, style: theme.textTheme.titleMedium),
const SizedBox(height: 12),
SegmentedButton<SpoofingMethod>(
style: SegmentedButton.styleFrom(shape: const StadiumBorder()),
return SettingsPanel(
child: Column(
children: [
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SegmentedButton<SpoofingMethod>(
showSelectedIcon: false,
segments: [
ButtonSegment(
value: SpoofingMethod.partial,
label: Text(l10n.spoofMethodPartial),
icon: const Icon(Icons.security_outlined),
icon: const Icon(Symbols.security),
),
ButtonSegment(
value: SpoofingMethod.full,
label: Text(l10n.spoofMethodFull),
icon: const Icon(Icons.public_outlined),
icon: const Icon(Symbols.public),
),
],
selected: {_selectedMethod},
@@ -586,10 +596,10 @@ class _SpoofScreenState extends State<SpoofScreen> {
_syncDeviceLocale();
},
),
const SizedBox(height: 12),
descriptionWidget,
],
),
),
const SizedBox(height: 12),
descriptionWidget,
],
),
);
}
@@ -597,54 +607,41 @@ class _SpoofScreenState extends State<SpoofScreen> {
Widget _buildDeviceTypeCard() {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.spoofDeviceTypeTitle, style: theme.textTheme.titleMedium),
const SizedBox(height: 12),
_buildDescriptionTile(
icon: Icons.info_outline,
color: theme.colorScheme.primary,
text: l10n.spoofDeviceTypeDescription,
return SettingsPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDescriptionTile(
icon: Symbols.info,
color: theme.colorScheme.primary,
text: l10n.spoofDeviceTypeDescription,
),
const SizedBox(height: 12),
if (_selectedMethod == SpoofingMethod.full)
_buildChipSelector<String>(
options: const [
_ChipOption('ANDROID', 'Android', Symbols.android),
_ChipOption('DESKTOP', 'Desktop', Symbols.desktop_windows),
],
selected: _selectedDeviceType,
onSelected: _onDeviceTypeChanged,
trailing: [
_buildDisabledChip('iOS', Symbols.phone_iphone, theme),
],
)
else
_buildChipSelector<String>(
options: const [
_ChipOption('ANDROID', 'Android', Symbols.android),
],
selected: 'ANDROID',
onSelected: _onDeviceTypeChanged,
trailing: [
_buildDisabledChip('iOS', Symbols.phone_iphone, theme),
_buildDisabledChip('Desktop', Symbols.desktop_windows, theme),
],
),
const SizedBox(height: 12),
if (_selectedMethod == SpoofingMethod.full)
_buildChipSelector<String>(
options: const [
_ChipOption('ANDROID', 'Android', Icons.android_outlined),
_ChipOption(
'DESKTOP',
'Desktop',
Icons.desktop_windows_outlined,
),
],
selected: _selectedDeviceType,
onSelected: _onDeviceTypeChanged,
trailing: [
_buildDisabledChip('iOS', Icons.phone_iphone_outlined, theme),
],
)
else
_buildChipSelector<String>(
options: const [
_ChipOption('ANDROID', 'Android', Icons.android_outlined),
],
selected: 'ANDROID',
onSelected: _onDeviceTypeChanged,
trailing: [
_buildDisabledChip('iOS', Icons.phone_iphone_outlined, theme),
_buildDisabledChip(
'Desktop',
Icons.desktop_windows_outlined,
theme,
),
],
),
],
),
],
),
);
}
@@ -663,214 +660,189 @@ class _SpoofScreenState extends State<SpoofScreen> {
required Color color,
required String text,
}) {
return ListTile(
leading: Icon(icon, color: color),
contentPadding: EdgeInsets.zero,
title: Text(
text,
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.onSurfaceVariant,
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color, size: 20, weight: 400),
const SizedBox(width: 16),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: 13,
height: 1.3,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
],
);
}
Widget _buildMainDataCard() {
final l10n = AppLocalizations.of(context)!;
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
l10n.spoofMainSectionTitle,
padding: const EdgeInsets.only(bottom: 16.0, top: 8.0),
fontSize: 22,
return SettingsPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _deviceNameController,
decoration: _inputDecoration(
l10n.spoofFieldDeviceName,
Symbols.smartphone,
),
TextField(
controller: _deviceNameController,
decoration: _inputDecoration(
l10n.spoofFieldDeviceName,
Icons.smartphone_outlined,
),
),
const SizedBox(height: 16),
TextField(
controller: _osVersionController,
decoration: _inputDecoration(
l10n.spoofFieldOsVersion,
Symbols.layers,
),
const SizedBox(height: 16),
TextField(
controller: _osVersionController,
decoration: _inputDecoration(
l10n.spoofFieldOsVersion,
Icons.layers_outlined,
),
),
],
),
),
],
),
);
}
Widget _buildRegionalDataCard() {
final l10n = AppLocalizations.of(context)!;
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
l10n.spoofRegionalSectionTitle,
padding: const EdgeInsets.only(bottom: 16.0, top: 8.0),
fontSize: 22,
return SettingsPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _screenController,
decoration: _inputDecoration(
l10n.spoofFieldScreen,
Symbols.fullscreen,
),
TextField(
controller: _screenController,
decoration: _inputDecoration(
l10n.spoofFieldScreen,
Icons.fullscreen_outlined,
),
),
const SizedBox(height: 16),
TextField(
controller: _timezoneController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldTimezone,
Symbols.public,
),
const SizedBox(height: 16),
TextField(
controller: _timezoneController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldTimezone,
Icons.public_outlined,
),
),
const SizedBox(height: 16),
TextField(
controller: _localeController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldLocale,
Symbols.language,
),
const SizedBox(height: 16),
TextField(
controller: _localeController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldLocale,
Icons.language_outlined,
),
),
const SizedBox(height: 16),
TextField(
controller: _deviceLocaleController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldDeviceLocale,
Symbols.translate,
),
const SizedBox(height: 16),
TextField(
controller: _deviceLocaleController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldDeviceLocale,
Icons.translate_outlined,
),
),
],
),
),
],
),
);
}
Widget _buildIdentifiersCard() {
final l10n = AppLocalizations.of(context)!;
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
l10n.spoofIdentifiersSectionTitle,
padding: const EdgeInsets.only(bottom: 16.0, top: 8.0),
fontSize: 22,
return SettingsPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDescriptionTile(
icon: Symbols.info,
color: Theme.of(context).colorScheme.tertiary,
text: l10n.spoofIdentifiersDescription,
),
const SizedBox(height: 12),
TextField(
controller: _instanceIdController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldInstanceId,
Symbols.fingerprint,
),
_buildDescriptionTile(
icon: Icons.info_outline,
color: Theme.of(context).colorScheme.tertiary,
text: l10n.spoofIdentifiersDescription,
),
const SizedBox(height: 16),
TextField(
controller: _clientSessionIdController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldClientSessionId,
Symbols.vpn_key,
),
const SizedBox(height: 12),
TextField(
controller: _instanceIdController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldInstanceId,
Icons.fingerprint_outlined,
),
),
const SizedBox(height: 16),
TextField(
controller: _clientSessionIdController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldClientSessionId,
Icons.vpn_key_outlined,
),
),
const SizedBox(height: 16),
TextField(
controller: _deviceIdController,
decoration:
_inputDecoration(
l10n.spoofFieldDeviceId,
Icons.tag_outlined,
).copyWith(
suffixIcon: IconButton(
icon: const Icon(Icons.autorenew_outlined),
tooltip: l10n.spoofRegenerateIdTooltip,
onPressed: _generateNewDeviceId,
),
),
const SizedBox(height: 16),
TextField(
controller: _deviceIdController,
decoration: _inputDecoration(l10n.spoofFieldDeviceId, Symbols.tag)
.copyWith(
suffixIcon: IconButton(
icon: const Icon(Symbols.autorenew),
tooltip: l10n.spoofRegenerateIdTooltip,
onPressed: _generateNewDeviceId,
),
),
const SizedBox(height: 16),
TextField(
controller: _appVersionController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldAppVersion,
Icons.info_outline_rounded,
),
),
const SizedBox(height: 16),
TextField(
controller: _buildNumberController,
enabled: _selectedMethod == SpoofingMethod.full,
keyboardType: TextInputType.number,
decoration: _inputDecoration(
l10n.spoofFieldBuildNumber,
Icons.numbers_outlined,
),
),
const SizedBox(height: 16),
TextField(
controller: _pushDeviceTypeController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldPushDeviceType,
Icons.notifications_outlined,
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Text(
l10n.spoofFieldArchitecture,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
TextField(
controller: _appVersionController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldAppVersion,
Symbols.info,
),
),
const SizedBox(height: 16),
TextField(
controller: _buildNumberController,
enabled: _selectedMethod == SpoofingMethod.full,
keyboardType: TextInputType.number,
decoration: _inputDecoration(
l10n.spoofFieldBuildNumber,
Symbols.numbers,
),
),
const SizedBox(height: 16),
TextField(
controller: _pushDeviceTypeController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldPushDeviceType,
Symbols.notifications,
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Text(
l10n.spoofFieldArchitecture,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
_buildChipSelector<String>(
options: const [
_ChipOption('arm64-v8a', 'arm64-v8a', Icons.memory_outlined),
_ChipOption(
'armeabi-v7a',
'armeabi-v7a',
Icons.memory_outlined,
),
_ChipOption('x86', 'x86', Icons.memory_outlined),
_ChipOption('x86_64', 'x86_64', Icons.memory_outlined),
_ChipOption('arm64', 'arm64', Icons.memory_outlined),
],
selected: _selectedArch,
onSelected: (value) => setState(() => _selectedArch = value),
),
],
),
),
_buildChipSelector<String>(
options: const [
_ChipOption('arm64-v8a', 'arm64-v8a', Symbols.memory),
_ChipOption('armeabi-v7a', 'armeabi-v7a', Symbols.memory),
_ChipOption('x86', 'x86', Symbols.memory),
_ChipOption('x86_64', 'x86_64', Symbols.memory),
_ChipOption('arm64', 'arm64', Symbols.memory),
],
selected: _selectedArch,
onSelected: (value) => setState(() => _selectedArch = value),
),
],
),
);
}
@@ -879,7 +851,7 @@ class _SpoofScreenState extends State<SpoofScreen> {
return InputDecoration(
labelText: label,
prefixIcon: Icon(icon),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)),
border: const OutlineInputBorder(borderRadius: AppShape.buttonRadius),
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceContainerHighest,
);
@@ -901,7 +873,7 @@ class _SpoofScreenState extends State<SpoofScreen> {
return ChoiceChip(
label: Text(opt.label),
avatar: isSelected
? Icon(Icons.check, size: 18, color: cs.onSecondaryContainer)
? Icon(Symbols.check, size: 18, color: cs.onSecondaryContainer)
: (opt.icon != null
? Icon(opt.icon, size: 18, color: cs.onSurfaceVariant)
: null),
@@ -918,8 +890,8 @@ class _SpoofScreenState extends State<SpoofScreen> {
side: BorderSide(
color: isSelected ? Colors.transparent : cs.outlineVariant,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
shape: const RoundedRectangleBorder(
borderRadius: AppShape.buttonRadius,
),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
@@ -946,7 +918,7 @@ class _SpoofScreenState extends State<SpoofScreen> {
vertical: 16,
horizontal: 16,
),
shape: const StadiumBorder(),
shape: AppShape.buttonBorder,
),
child: Text(l10n.spoofButtonGenerate),
),
@@ -961,12 +933,12 @@ class _SpoofScreenState extends State<SpoofScreen> {
vertical: 16,
horizontal: 16,
),
shape: const StadiumBorder(),
shape: AppShape.buttonBorder,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.save_alt_outlined),
const Icon(Symbols.save_alt),
const SizedBox(width: 8),
Text(l10n.spoofButtonApply),
],
@@ -11,6 +11,7 @@ import '../../../l10n/app_localizations.dart';
import '../../../main.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/settings_radio_tile.dart';
import '../../widgets/settings_card.dart';
class ThemeSettingsScreen extends StatelessWidget {
const ThemeSettingsScreen({super.key});
@@ -71,11 +72,8 @@ class _ThemeModeCard extends StatelessWidget {
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
return SettingsPanel(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 12),
depth: 6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -171,11 +169,8 @@ class _AmoledCardState extends State<_AmoledCard> {
return Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (e) => _lastPointerPosition = e.position,
child: GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
child: SettingsPanel(
padding: const EdgeInsets.fromLTRB(20, 14, 12, 14),
depth: 6,
child: Row(
children: [
Icon(Symbols.contrast, color: cs.onSurface, size: 24, weight: 500),
@@ -235,11 +230,7 @@ class _ScheduleCard extends StatelessWidget {
return AnimatedOpacity(
opacity: enabled ? 1 : 0.5,
duration: const Duration(milliseconds: 200),
child: GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
depth: 6,
child: SettingsPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -12,6 +12,8 @@ import '../../../core/protocol/packet.dart';
import '../../../core/transport/traffic_monitor.dart';
import '../../../core/utils/format.dart';
import '../../widgets/custom_notification.dart';
import '../../../core/config/app_fonts.dart';
import '../../../core/config/app_shape.dart';
class TrafficMonitorScreen extends StatefulWidget {
const TrafficMonitorScreen({super.key});
@@ -153,7 +155,7 @@ class _TrafficMonitorScreenState extends State<TrafficMonitorScreen> {
color: cs.onSurface,
fontSize: 20,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -202,7 +204,7 @@ class _TrafficMonitorScreenState extends State<TrafficMonitorScreen> {
padding: const EdgeInsets.fromLTRB(16, 10, 12, 10),
decoration: BoxDecoration(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
borderRadius: AppShape.cardRadius,
),
child: AnimatedBuilder(
animation: _monitor,
@@ -216,7 +218,7 @@ class _TrafficMonitorScreenState extends State<TrafficMonitorScreen> {
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: on ? kOnlineGreen : cs.outline,
color: on ? kSuccessGreen : cs.outline,
),
),
const SizedBox(width: 12),
@@ -5,9 +5,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import '../../../core/config/app_colors.dart';
import '../../widgets/animated_slash_icon.dart';
import '../../widgets/connection_status.dart';
import '../../../core/config/app_fonts.dart';
class WebQrScanScreen extends StatefulWidget {
const WebQrScanScreen({super.key});
@@ -73,7 +75,7 @@ class _WebQrScanScreenState extends State<WebQrScanScreen> {
title: Text(
'QR для веба и ПК',
style: TextStyle(
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
@@ -138,7 +140,7 @@ class _WebQrScanScreenState extends State<WebQrScanScreen> {
'Наведите камеру на QR-код на экране компьютера',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.white,
@@ -360,7 +362,7 @@ class _TelegramStyleFinderOverlayState
final finderRect = _interpolatedFinderRect();
final rrect = _finderRRect(finderRect, _frameCornerRadius);
final frameColor = _qrInView ? const Color(0xFF4ADE80) : Colors.white;
final frameColor = _qrInView ? kSuccessGreen : Colors.white;
return IgnorePointer(
child: Stack(
fit: StackFit.expand,
@@ -9,6 +9,7 @@ import '../../../core/utils/haptics.dart';
import '../../../main.dart' show fileUploader, messagesModule, storiesModule;
import '../../widgets/custom_notification.dart';
import '../../widgets/primary_loading_button.dart';
import '../../../core/config/app_frost.dart';
const int _storyExpiration = 86400000;
@@ -177,7 +178,10 @@ class _StoryComposerScreenState extends State<StoryComposerScreen> {
children: [
Positioned.fill(
child: ImageFiltered(
imageFilter: ui.ImageFilter.blur(sigmaX: 30, sigmaY: 30),
imageFilter: ui.ImageFilter.blur(
sigmaX: AppFrost.mediaBackdropSigma,
sigmaY: AppFrost.mediaBackdropSigma,
),
child: backdrop,
),
),
+11 -3
View File
@@ -2,6 +2,7 @@ import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../core/utils/haptics.dart';
import '../../../models/story.dart';
@@ -110,7 +111,8 @@ class _StoryRingState extends State<StoryRing> {
owner: widget.preview.owner,
overrideInfo: widget.ownerOverride,
builder: (context, info) {
final name = widget.selfLabel ??
final name =
widget.selfLabel ??
(info?.name.isNotEmpty == true ? info!.name : '');
return Padding(
padding: const EdgeInsets.only(right: 16),
@@ -212,7 +214,13 @@ class SegmentedRingPainter extends CustomPainter {
for (var i = 0; i < n; i++) {
final start = -math.pi / 2 + gap / 2 + i * segment;
canvas.drawArc(rect, start, sweep, false, i < read ? readPaint : unreadPaint);
canvas.drawArc(
rect,
start,
sweep,
false,
i < read ? readPaint : unreadPaint,
);
}
}
@@ -355,7 +363,7 @@ class _StorySelfTileState extends State<StorySelfTile> {
color: cs.primary,
),
child: Icon(
Icons.add,
Symbols.add,
size: 14,
color: cs.onPrimary,
),
@@ -18,6 +18,8 @@ import '../../widgets/liquid_glass.dart';
import '../../widgets/lottie_image.dart';
import '../../widgets/small_spinner.dart';
import 'story_owner_info.dart';
import '../../../core/config/app_frost.dart';
import '../../../core/config/app_fonts.dart';
const _quickReactions = ['❤️', '🔥', '😍', '👏', '😂', '😮'];
const Duration _photoDuration = Duration(seconds: 5);
@@ -64,7 +66,8 @@ Future<void> openStoryViewer(
animation: animation,
child: child,
builder: (context, child) {
final closing = animation.status == AnimationStatus.reverse ||
final closing =
animation.status == AnimationStatus.reverse ||
animation.status == AnimationStatus.dismissed;
// Круговое раскрытие — только на открытии; закрытие всегда
// мягким fade + scale (круг «схлопыванием» резал кадр).
@@ -214,7 +217,9 @@ class _StoryViewerScreenState extends State<StoryViewerScreen>
return;
}
setState(() => _loading[ownerId] = true);
final stories = await storiesModule.getByOwner(widget.previews[index].owner);
final stories = await storiesModule.getByOwner(
widget.previews[index].owner,
);
if (!mounted) return;
setState(() {
_stories[ownerId] = stories;
@@ -434,8 +439,11 @@ class _StoryViewerScreenState extends State<StoryViewerScreen>
? _buildActiveOwner()
: _OwnerCover(
preview: widget.previews[index],
overrideInfo: widget.ownerOverrides[
widget.previews[index].owner.ownerId],
overrideInfo:
widget.ownerOverrides[widget
.previews[index]
.owner
.ownerId],
);
return _CubePage(
controller: _ownerController,
@@ -530,9 +538,7 @@ class _StoryViewerScreenState extends State<StoryViewerScreen>
),
const _TopScrim(),
if (loading)
const Center(
child: SmallSpinner(size: 28, color: Colors.white),
),
const Center(child: SmallSpinner(size: 28, color: Colors.white)),
SafeArea(
child: Column(
children: [
@@ -612,14 +618,12 @@ class _StoryViewerScreenState extends State<StoryViewerScreen>
info?.name.isNotEmpty == true ? info!.name : '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
style: TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
shadows: [
Shadow(color: Colors.black54, blurRadius: 4),
],
fontFamily: displayFontOf(context),
shadows: [Shadow(color: Colors.black54, blurRadius: 4)],
),
),
if (story != null && story.time > 0)
@@ -1034,7 +1038,10 @@ class _StoryMediaView extends StatelessWidget {
final Widget blurBg = preview != null
? Positioned.fill(
child: ImageFiltered(
imageFilter: ui.ImageFilter.blur(sigmaX: 30, sigmaY: 30),
imageFilter: ui.ImageFilter.blur(
sigmaX: AppFrost.mediaBackdropSigma,
sigmaY: AppFrost.mediaBackdropSigma,
),
child: Image(image: preview, fit: BoxFit.cover),
),
)
@@ -1056,24 +1063,22 @@ class _StoryMediaView extends StatelessWidget {
fit: BoxFit.contain,
);
} else if (preview != null) {
fg = Center(child: Image(image: preview, fit: BoxFit.contain));
fg = Center(
child: Image(image: preview, fit: BoxFit.contain),
);
} else {
fg = const SizedBox.shrink();
}
return Stack(
fit: StackFit.expand,
children: [
blurBg,
fg,
],
);
return Stack(fit: StackFit.expand, children: [blurBg, fg]);
}
final url = media.url;
Widget fg;
if (url == null || url.isEmpty) {
fg = preview != null
? Center(child: Image(image: preview, fit: BoxFit.contain))
? Center(
child: Image(image: preview, fit: BoxFit.contain),
)
: const SizedBox.shrink();
} else {
fg = CachedNetworkImage(
@@ -1081,22 +1086,24 @@ class _StoryMediaView extends StatelessWidget {
fit: BoxFit.contain,
fadeInDuration: const Duration(milliseconds: 200),
placeholder: preview != null
? (context, _) => Center(child: Image(image: preview, fit: BoxFit.contain))
? (context, _) => Center(
child: Image(image: preview, fit: BoxFit.contain),
)
: null,
errorWidget: (context, _, _) => preview != null
? Center(child: Image(image: preview, fit: BoxFit.contain))
? Center(
child: Image(image: preview, fit: BoxFit.contain),
)
: const Center(
child: Icon(Symbols.broken_image, color: Colors.white54, size: 48),
child: Icon(
Symbols.broken_image,
color: Colors.white54,
size: 48,
),
),
);
}
return Stack(
fit: StackFit.expand,
children: [
blurBg,
fg,
],
);
return Stack(fit: StackFit.expand, children: [blurBg, fg]);
}
}
@@ -9,6 +9,7 @@ import '../../core/storage/token_storage.dart';
import '../../core/utils/haptics.dart';
import 'animated_overlay_popup.dart';
import 'komet_avatar.dart';
import '../../core/config/app_frost.dart';
class AccountSwitcherController extends ChangeNotifier {
Offset? pointer;
@@ -239,7 +240,7 @@ class _AccountSwitcherLayerState extends State<_AccountSwitcherLayer>
animation: overlayAnimation,
builder: (ctx, _) {
final t = overlayAnimation.value.clamp(0.0, 1.0);
final blurSigma = 14.0 * t;
final blurSigma = AppFrost.overlaySigma * t;
return GestureDetector(
onTap: closeOverlay,
behavior: HitTestBehavior.opaque,
@@ -45,7 +45,7 @@ Future<void> showAttachmentSheet(
isScrollControlled: true,
requestFocus: false,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withValues(alpha: 0.45),
barrierColor: AppFrost.scrim(),
builder: (_) => AttachmentSheet(
title: title,
onSend: onSend,
@@ -1093,7 +1093,9 @@ class _ThumbnailState extends State<_Thumbnail> {
}
void _resolveProvider() {
final file = widget.editedFile ?? (widget.item.isVideo ? null : widget.item.localFile);
final file =
widget.editedFile ??
(widget.item.isVideo ? null : widget.item.localFile);
if (file != null) {
_provider = ResizeImage(
FileImage(file),
@@ -356,7 +356,7 @@ class _SelectionToggle extends StatelessWidget {
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected ? kEditorAccent : Colors.transparent,
color: isSelected ? MediaAccent.of(context) : Colors.transparent,
border: Border.all(color: Colors.white, width: 2),
),
child: isSelected
@@ -466,7 +466,7 @@ class _FileToggleState extends State<_FileToggle> {
builder: (context, t, _) {
final color = Color.lerp(
Colors.white54,
Color.lerp(Colors.white, kEditorAccent, 0.4),
Color.lerp(Colors.white, MediaAccent.of(context), 0.4),
t,
);
return Icon(Symbols.description, color: color, size: 24);
@@ -484,7 +484,7 @@ class _SendButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
color: kEditorAccent,
color: MediaAccent.of(context),
shape: const CircleBorder(),
child: InkWell(
customBorder: const CircleBorder(),
@@ -13,9 +13,20 @@ import 'package:komet/frontend/widgets/custom_notification.dart';
import '../../../core/config/app_colors.dart';
import '../../../l10n/app_localizations.dart';
import '../small_spinner.dart';
import '../../../core/config/app_shape.dart';
const Color _kPanel = Color(0xFF0A0A0A);
const List<Color> _kPenWheel = [
Color(0xFFFF3B30),
Color(0xFFFFCC00),
Color(0xFF34C759),
Color(0xFF00C7BE),
Color(0xFF2F8FFF),
Color(0xFFAF52DE),
Color(0xFFFF3B30),
];
class CropState {
final int quarterTurns;
final bool flipH;
@@ -404,9 +415,7 @@ class _PhotoCropEditorState extends State<PhotoCropEditor> {
Widget _buildViewport() {
final img = _image;
if (img == null) {
return const Center(
child: SmallSpinner(size: 36, color: Colors.white),
);
return const Center(child: SmallSpinner(size: 36, color: Colors.white));
}
return LayoutBuilder(
builder: (context, constraints) {
@@ -445,7 +454,7 @@ class _PhotoCropEditorState extends State<PhotoCropEditor> {
onPressed: _flip,
icon: Icon(
Symbols.flip,
color: _flipH ? kEditorAccent : Colors.white,
color: _flipH ? MediaAccent.of(context) : Colors.white,
),
tooltip: l10n.photoEditorFlipTooltip,
),
@@ -501,7 +510,7 @@ class _PhotoCropEditorState extends State<PhotoCropEditor> {
child: Text(
l10n.photoEditorDone,
style: TextStyle(
color: _baking ? Colors.white38 : kEditorAccent,
color: _baking ? Colors.white38 : MediaAccent.of(context),
fontSize: 15,
fontWeight: FontWeight.w600,
),
@@ -594,7 +603,9 @@ class _StraightenRuler extends StatelessWidget {
onDoubleTap: () => onChanged(0),
child: SizedBox(
height: 56,
child: CustomPaint(painter: _RulerPainter(value)),
child: CustomPaint(
painter: _RulerPainter(value, MediaAccent.of(context)),
),
),
);
}
@@ -602,8 +613,9 @@ class _StraightenRuler extends StatelessWidget {
class _RulerPainter extends CustomPainter {
final double value;
final Color accent;
_RulerPainter(this.value);
_RulerPainter(this.value, this.accent);
@override
void paint(Canvas canvas, Size size) {
@@ -638,14 +650,15 @@ class _RulerPainter extends CustomPainter {
Offset(cx, baseY - 18),
Offset(cx, baseY + 2),
Paint()
..color = kEditorAccent
..color = accent
..strokeWidth = 2
..strokeCap = StrokeCap.round,
);
}
@override
bool shouldRepaint(covariant _RulerPainter old) => old.value != value;
bool shouldRepaint(covariant _RulerPainter old) =>
old.value != value || old.accent != accent;
}
const Color _kDrawPanel = Color(0xFF101010);
@@ -897,6 +910,7 @@ class _PhotoDrawEditorState extends State<PhotoDrawEditor> {
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1E1E1E),
shape: AppShape.dialogBorder,
title: Text(
l10n.photoEditorTextDialogTitle,
style: const TextStyle(color: Colors.white),
@@ -1094,7 +1108,12 @@ class _PhotoDrawEditorState extends State<PhotoDrawEditor> {
if (selected != null)
Positioned.fill(
child: IgnorePointer(
child: CustomPaint(painter: _SelectionPainter(selected)),
child: CustomPaint(
painter: _SelectionPainter(
selected,
MediaAccent.of(context),
),
),
),
),
],
@@ -1235,17 +1254,7 @@ class _PhotoDrawEditorState extends State<PhotoDrawEditor> {
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: SweepGradient(
colors: [
Color(0xFFFF3B30),
Color(0xFFFFCC00),
kOnlineGreen,
Color(0xFF00C7BE),
kEditorAccent,
Color(0xFFAF52DE),
Color(0xFFFF3B30),
],
),
gradient: SweepGradient(colors: _kPenWheel),
),
child: Container(
decoration: BoxDecoration(
@@ -1650,8 +1659,9 @@ Size textMarkSize(TextMark t) {
class _SelectionPainter extends CustomPainter {
final TextMark text;
final Color accent;
_SelectionPainter(this.text);
_SelectionPainter(this.text, this.accent);
@override
void paint(Canvas canvas, Size size) {
@@ -1676,7 +1686,7 @@ class _SelectionPainter extends CustomPainter {
_dashedLine(canvas, bl, tl, border);
final fill = Paint()
..color = kEditorAccent
..color = accent
..style = PaintingStyle.fill;
final ring = Paint()
..color = Colors.white
@@ -2349,9 +2359,7 @@ class _PhotoAdjustEditorState extends State<PhotoAdjustEditor> {
Widget _buildPreview() {
final img = _image;
if (img == null) {
return const Center(
child: SmallSpinner(size: 36, color: Colors.white),
);
return const Center(child: SmallSpinner(size: 36, color: Colors.white));
}
return LayoutBuilder(
builder: (context, constraints) {
@@ -2664,12 +2672,16 @@ class _PhotoAdjustEditorState extends State<PhotoAdjustEditor> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: selected ? kEditorAccent : Colors.white, size: 30),
Icon(
icon,
color: selected ? MediaAccent.of(context) : Colors.white,
size: 30,
),
const SizedBox(height: 6),
Text(
label,
style: TextStyle(
color: selected ? kEditorAccent : Colors.white70,
color: selected ? MediaAccent.of(context) : Colors.white70,
fontSize: 12,
),
),
@@ -2704,7 +2716,7 @@ class _PhotoAdjustEditorState extends State<PhotoAdjustEditor> {
child: Text(
l10n.photoEditorDone,
style: TextStyle(
color: _baking ? Colors.white38 : kEditorAccent,
color: _baking ? Colors.white38 : MediaAccent.of(context),
fontSize: 15,
fontWeight: FontWeight.w600,
),
@@ -2720,7 +2732,7 @@ class _PhotoAdjustEditorState extends State<PhotoAdjustEditor> {
return IconButton(
onPressed: disabled ? null : () => setState(() => _tab = tab),
icon: Icon(icon),
color: selected ? kEditorAccent : Colors.white,
color: selected ? MediaAccent.of(context) : Colors.white,
disabledColor: Colors.white24,
);
}
@@ -27,6 +27,7 @@ import '../photo_viewer.dart';
import '../reload_on_reconnect.dart';
import '../small_spinner.dart';
import '../swipe_route.dart';
import '../sheet_helpers.dart';
enum SharedContentKind { media, files, voice, links }
@@ -153,9 +154,7 @@ Future<void> _showItemMenu(BuildContext context, List<_MenuAction> actions) {
return showModalBottomSheet<void>(
context: context,
backgroundColor: cs.surfaceContainerHigh,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
shape: kSheetShape,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -419,7 +418,7 @@ class _CommonChatsTabState extends State<CommonChatsTab>
final cs = Theme.of(context).colorScheme;
if (_loading) return _loadingState(cs);
if (_chats.isEmpty) {
return _emptyState(cs, widget.emptyLabel, Icons.group);
return _emptyState(cs, widget.emptyLabel, Symbols.group);
}
return Container(
+19 -14
View File
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:komet/core/config/chat_wallpaper_themes.dart';
import 'package:komet/core/config/app_colors.dart';
import 'package:komet/core/storage/chat_wallpaper_store.dart';
import '../../core/config/app_fonts.dart';
enum WallpaperPickType { none, theme, gallery }
@@ -10,12 +12,10 @@ class WallpaperPick {
final WallpaperPickType type;
final ChatWallpaperTheme? theme;
const WallpaperPick.none()
: type = WallpaperPickType.none,
theme = null;
const WallpaperPick.none() : type = WallpaperPickType.none, theme = null;
const WallpaperPick.gallery()
: type = WallpaperPickType.gallery,
theme = null;
: type = WallpaperPickType.gallery,
theme = null;
const WallpaperPick.theme(this.theme) : type = WallpaperPickType.theme;
}
@@ -81,12 +81,12 @@ class _ChatWallpaperGalleryScreenState
icon: const Icon(Symbols.arrow_back),
onPressed: () => Navigator.pop(context),
),
title: const Text(
title: Text(
'Обои',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -169,7 +169,9 @@ class _ChatWallpaperGalleryScreenState
),
),
const SizedBox(width: 12),
Expanded(child: _ApplyButton(enabled: _changed, onTap: _apply)),
Expanded(
child: _ApplyButton(enabled: _changed, onTap: _apply),
),
],
),
),
@@ -216,6 +218,7 @@ class _SampleBubbles extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_bubble(
context,
text: 'Как насчёт новых обоев для этого чата?',
color: cs.surfaceContainerHighest.withValues(alpha: 0.94),
textColor: cs.onSurface,
@@ -223,6 +226,7 @@ class _SampleBubbles extends StatelessWidget {
),
const SizedBox(height: 8),
_bubble(
context,
text: 'Выглядит отлично 🔥',
color: cs.primary,
textColor: cs.onPrimary,
@@ -234,7 +238,8 @@ class _SampleBubbles extends StatelessWidget {
);
}
Widget _bubble({
Widget _bubble(
BuildContext context, {
required String text,
required Color color,
required Color textColor,
@@ -255,7 +260,7 @@ class _SampleBubbles extends StatelessWidget {
style: TextStyle(
color: textColor,
fontSize: 15,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
@@ -336,7 +341,7 @@ class _TileFrame extends StatelessWidget {
color: selected ? cs.primary : cs.onSurfaceVariant,
fontSize: 12,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
],
@@ -363,7 +368,7 @@ class _NoneTile extends StatelessWidget {
child: ColoredBox(
color: cs.surfaceContainerHighest,
child: const Center(
child: Icon(Symbols.block, color: Color(0xFFFF3B30), size: 34),
child: Icon(Symbols.block, color: kDangerRed, size: 34),
),
),
);
@@ -419,7 +424,7 @@ class _GalleryButton extends StatelessWidget {
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
],
@@ -456,7 +461,7 @@ class _ApplyButton extends StatelessWidget {
color: cs.onPrimary,
fontSize: 16,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
),
+2 -1
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../core/config/app_shape.dart';
/// Shared confirmation dialog. Returns true if confirmed, false otherwise.
Future<bool> showConfirmDialog(
@@ -14,7 +15,7 @@ Future<bool> showConfirmDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: cs.surfaceContainerHigh,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
shape: AppShape.dialogBorder,
title: title == null
? null
: Text(title, style: TextStyle(color: cs.onSurface)),
+2 -3
View File
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'sheet_helpers.dart';
import '../../core/config/app_shape.dart';
class InfoActionSheetItem {
final IconData icon;
@@ -188,9 +189,7 @@ class _InfoActionSheetState extends State<_InfoActionSheet> {
disabledBackgroundColor: cs.primary.withValues(alpha: 0.45),
disabledForegroundColor: cs.onPrimary.withValues(alpha: 0.85),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
shape: AppShape.buttonBorder,
),
child: Text(
buttonText,
@@ -21,11 +21,7 @@ class ReactionEmoji {
final String? animationUrl;
final String? staticUrl;
const ReactionEmoji({
required this.emoji,
this.animationUrl,
this.staticUrl,
});
const ReactionEmoji({required this.emoji, this.animationUrl, this.staticUrl});
}
class MessageReader {
@@ -114,6 +110,7 @@ void showMessageActions({
Future<bool> Function(int reasonId)? onReport,
VoidCallback? onDelete,
bool allowDelete = true,
bool allowCopy = true,
VoidCallback? onEdit,
VoidCallback? onReply,
VoidCallback? onForward,
@@ -153,6 +150,7 @@ void showMessageActions({
onReport: onReport,
onDelete: onDelete,
allowDelete: allowDelete,
allowCopy: allowCopy,
onEdit: onEdit,
onReply: onReply,
onForward: onForward,
@@ -189,6 +187,7 @@ class _MessageActionsLayer extends StatefulWidget {
final Future<bool> Function(int reasonId)? onReport;
final VoidCallback? onDelete;
final bool allowDelete;
final bool allowCopy;
final VoidCallback? onEdit;
final VoidCallback? onReply;
final VoidCallback? onForward;
@@ -217,6 +216,7 @@ class _MessageActionsLayer extends StatefulWidget {
this.onReport,
this.onDelete,
this.allowDelete = true,
this.allowCopy = true,
this.onEdit,
this.onReply,
this.onForward,
@@ -503,7 +503,8 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
_Action(Symbols.reply, l10n.msgActionsReply, _reply),
if (widget.onForward != null)
_Action(Symbols.forward, l10n.msgActionsForward, _forward),
if (hasText) _Action(Symbols.content_copy, l10n.msgActionsCopy, _copy),
if (hasText && widget.allowCopy)
_Action(Symbols.content_copy, l10n.msgActionsCopy, _copy),
if (widget.isMe && widget.onEdit != null)
_Action(Symbols.edit, l10n.msgActionsEdit, _edit),
if (widget.onPin != null)
@@ -1004,7 +1005,11 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
);
}
Widget _buildQuickRow(ColorScheme cs, double cell, List<ReactionEmoji> quick) {
Widget _buildQuickRow(
ColorScheme cs,
double cell,
List<ReactionEmoji> quick,
) {
return Center(
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -1232,9 +1237,7 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
if (_readByLoading) {
body = const Padding(
padding: EdgeInsets.symmetric(vertical: 28),
child: Center(
child: SmallSpinner(size: 24),
),
child: Center(child: SmallSpinner(size: 24)),
);
} else {
final readers = _readers ?? const <MessageReader>[];
@@ -1315,9 +1318,7 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
if (_reportLoading) {
body = const Padding(
padding: EdgeInsets.symmetric(vertical: 28),
child: Center(
child: SmallSpinner(size: 24),
),
child: Center(child: SmallSpinner(size: 24)),
);
} else {
final reasons = _reasons ?? const <({int id, String title})>[];
@@ -1644,9 +1645,7 @@ class _ReactionEmojiPickerState extends State<_ReactionEmojiPicker> {
_buildSearchField(cs),
Expanded(
child: !_loaded
? const Center(
child: SmallSpinner(size: 26),
)
? const Center(child: SmallSpinner(size: 26))
: _results.isEmpty
? const SizedBox.shrink()
: LottieScrollScope(
@@ -1761,7 +1760,8 @@ class _ReactionGlyph extends StatelessWidget {
final anim = reaction.animationUrl;
final still = reaction.staticUrl;
final hasAsset =
(anim != null && anim.isNotEmpty) || (still != null && still.isNotEmpty);
(anim != null && anim.isNotEmpty) ||
(still != null && still.isNotEmpty);
if (!hasAsset) {
return Center(
child: Text(reaction.emoji, style: TextStyle(fontSize: size * 0.9)),
+2 -1
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../../core/cache/info_cache.dart';
import '../../core/config/app_colors.dart';
class OnlineDot extends StatelessWidget {
final int userId;
@@ -14,7 +15,7 @@ class OnlineDot extends StatelessWidget {
required this.userId,
required this.borderColor,
this.size = 12,
this.color = const Color(0xFF2EC36B),
this.color = kSuccessGreen,
this.borderWidth = 2,
});
+3 -2
View File
@@ -18,6 +18,7 @@ import '../../core/utils/media_cache.dart';
import '../../core/utils/media_saver.dart';
import '../../core/utils/save_file_as.dart';
import '../../l10n/app_localizations.dart';
import '../../core/config/app_colors.dart';
import '../../main.dart';
import '../../models/attachment.dart';
import 'attachment/photo_hero.dart';
@@ -1498,7 +1499,7 @@ class _VideoSettingsButton extends StatelessWidget {
final l10n = AppLocalizations.of(context)!;
return PopupMenuButton<String>(
key: const ValueKey('video-settings'),
color: const Color(0xFF292326),
color: MediaAccent.schemeOf(context).surfaceContainerHigh,
tooltip: l10n.videoViewerSettings,
icon: const Icon(Symbols.settings, color: Colors.white),
onSelected: (value) {
@@ -1564,7 +1565,7 @@ class _SettingChoice extends StatelessWidget {
child: Text(label, style: const TextStyle(color: Colors.white)),
),
if (selected)
const Icon(Symbols.check, color: Color(0xFFE68ABA), size: 18),
Icon(Symbols.check, color: MediaAccent.of(context), size: 18),
],
);
}
+2 -3
View File
@@ -7,6 +7,7 @@ import '../../core/utils/haptics.dart';
import '../../models/poll.dart';
import 'custom_notification.dart';
import 'small_spinner.dart';
import '../../core/config/app_shape.dart';
class PollView extends StatefulWidget {
final int chatId;
@@ -242,9 +243,7 @@ class _PollViewState extends State<PollView>
style: TextButton.styleFrom(
foregroundColor: widget.accentColor,
backgroundColor: widget.dimColor.withValues(alpha: 0.12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
shape: AppShape.buttonBorder,
),
child: _voting
? SmallSpinner(size: 16, color: widget.accentColor)
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'small_spinner.dart';
import 'springy_tap.dart';
import '../../core/config/app_shape.dart';
class PrimaryLoadingButton extends StatelessWidget {
final ValueListenable<bool> loading;
@@ -34,13 +35,9 @@ class PrimaryLoadingButton extends StatelessWidget {
backgroundColor: background ?? cs.primary,
foregroundColor: fg,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
shape: AppShape.buttonBorder,
),
child: isLoading
? SmallSpinner(size: 20, color: fg)
: child,
child: isLoading ? SmallSpinner(size: 20, color: fg) : child,
),
),
);
+4 -1
View File
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import '../../core/config/app_fonts.dart';
import '../../core/config/app_shape.dart';
Future<String?> showTextInputDialog(
BuildContext context, {
@@ -20,12 +22,13 @@ Future<String?> showTextInputDialog(
final cs = Theme.of(dialogContext).colorScheme;
return AlertDialog(
backgroundColor: cs.surfaceContainerHigh,
shape: AppShape.dialogBorder,
title: title == null
? null
: Text(
title,
style: TextStyle(
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
fontWeight: FontWeight.w600,
fontSize: 18,
color: cs.onSurface,
@@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
import '../../core/utils/format.dart';
import 'custom_notification.dart';
import 'sheet_helpers.dart';
import '../../core/config/app_fonts.dart';
import '../../core/config/app_shape.dart';
const List<String> _weekdayShort = ['пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'вс'];
@@ -132,7 +134,7 @@ class _ScheduleSheetState extends State<_ScheduleSheet> {
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
),
),
const SizedBox(height: 8),
@@ -172,9 +174,7 @@ class _ScheduleSheetState extends State<_ScheduleSheet> {
child: FilledButton(
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: AppShape.buttonBorder,
),
onPressed: _confirm,
child: Text(
+30 -2
View File
@@ -1,8 +1,34 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../core/config/app_shape.dart';
import 'glossy_pill.dart';
class SettingsPanel extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry padding;
final Color? color;
const SettingsPanel({
super.key,
required this.child,
this.padding = const EdgeInsets.fromLTRB(20, 18, 20, 20),
this.color,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return GlossyPill(
color: color ?? cs.surfaceContainerHigh,
borderRadius: AppShape.cardRadius,
padding: padding,
depth: 6,
child: child,
);
}
}
class SettingsCard extends StatelessWidget {
final List<Widget> children;
@@ -13,7 +39,7 @@ class SettingsCard extends StatelessWidget {
final cs = Theme.of(context).colorScheme;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
borderRadius: AppShape.cardRadius,
depth: 6,
child: Column(
children: [
@@ -135,7 +161,9 @@ class SettingsNavTile extends StatelessWidget {
child: InkWell(
onTap: onTap ?? () {},
borderRadius: isLast
? const BorderRadius.vertical(bottom: Radius.circular(20))
? const BorderRadius.vertical(
bottom: Radius.circular(AppShape.card),
)
: null,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17),
+3 -3
View File
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import '../../core/config/app_shape.dart';
/// Standard rounded top shape for modal bottom sheets.
const RoundedRectangleBorder kSheetShape = RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
);
const RoundedRectangleBorder kSheetShape = AppShape.sheetBorder;
/// The little drag "grabber" pill shown at the top of a bottom sheet.
class SheetGrabber extends StatelessWidget {
+2 -3
View File
@@ -10,6 +10,7 @@ import 'custom_notification.dart';
import 'small_spinner.dart';
import 'lottie_image.dart';
import 'sticker_peek.dart';
import '../../core/config/app_shape.dart';
enum _PackAction { forward, copyLink }
@@ -304,9 +305,7 @@ class _StickerPackSheetState extends State<_StickerPackSheet> {
? cs.surfaceContainerHighest
: cs.primary,
foregroundColor: _isFavorite ? cs.onSurface : cs.onPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
shape: AppShape.buttonBorder,
),
onPressed: _busy ? null : _toggle,
child: _busy
+5 -1
View File
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import '../../core/utils/haptics.dart';
import 'lottie_image.dart';
import '../../core/config/app_frost.dart';
class _PeekData {
final String? url;
@@ -210,7 +211,10 @@ class _PeekOverlay extends StatelessWidget {
children: [
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20 * t, sigmaY: 20 * t),
filter: ImageFilter.blur(
sigmaX: AppFrost.overlaySigma * t,
sigmaY: AppFrost.overlaySigma * t,
),
child: ColoredBox(
color: Colors.black.withValues(alpha: 0.3 * t),
),
+6 -5
View File
@@ -5,11 +5,10 @@ import '../../core/utils/update_installer.dart';
import '../../core/utils/link_opener.dart';
import '../../l10n/app_localizations.dart';
import 'custom_notification.dart';
import '../../core/config/app_fonts.dart';
import '../../core/config/app_shape.dart';
Future<void> showUpdateDialog(
BuildContext context,
AppUpdateInfo info,
) async {
Future<void> showUpdateDialog(BuildContext context, AppUpdateInfo info) async {
final l10n = AppLocalizations.of(context)!;
await showDialog<void>(
context: context,
@@ -18,10 +17,11 @@ Future<void> showUpdateDialog(
final notes = info.notes;
return AlertDialog(
backgroundColor: cs.surfaceContainerHigh,
shape: AppShape.dialogBorder,
title: Text(
l10n.updateAvailableTitle,
style: TextStyle(
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
fontWeight: FontWeight.w600,
fontSize: 18,
color: cs.onSurface,
@@ -152,6 +152,7 @@ class _UpdateProgressDialogState extends State<_UpdateProgressDialog> {
final percent = (_progress * 100).round();
return AlertDialog(
backgroundColor: cs.surfaceContainerHigh,
shape: AppShape.dialogBorder,
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
+2 -1
View File
@@ -4,6 +4,7 @@ import '../../main.dart' show accountModule;
import 'custom_notification.dart';
import 'sheet_helpers.dart';
import 'small_spinner.dart';
import '../../core/config/app_fonts.dart';
Future<bool> showWebQrLoginConfirmSheet(BuildContext context) async {
final agreed = await showModalBottomSheet<bool>(
@@ -24,7 +25,7 @@ Future<bool> showWebQrLoginConfirmSheet(BuildContext context) async {
Text(
'Вход по QR',
style: TextStyle(
fontFamily: 'Outfit',
fontFamily: displayFontOf(context),
fontSize: 20,
fontWeight: FontWeight.w700,
color: cs.onSurface,
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import '../../core/config/app_shape.dart';
String _resourceLabel(PermissionResourceType type) {
if (type == PermissionResourceType.CAMERA) return 'камера';
@@ -16,9 +17,9 @@ Future<PermissionResponse> askWebViewPermission(
PermissionRequest request,
) async {
PermissionResponse deny() => PermissionResponse(
resources: request.resources,
action: PermissionResponseAction.DENY,
);
resources: request.resources,
action: PermissionResponseAction.DENY,
);
if (!context.mounted) return deny();
@@ -32,6 +33,7 @@ Future<PermissionResponse> askWebViewPermission(
final granted = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape: AppShape.dialogBorder,
title: const Text('Запрос доступа'),
content: Text('$host запрашивает доступ к: $labels.'),
actions: [
+39
View File
@@ -123,8 +123,12 @@
"profileMenuSpoof": "Spoofing",
"infoTitle": "Info",
"infoAccountSection": "Account",
"infoPacketSection": "Login packet",
"infoChatsSection": "Chats in login packet",
"infoChatSettingsSection": "Per-chat settings",
"infoServerSection": "Server",
"infoUserSection": "User",
"infoExperimentsSection": "Experiments",
"infoYMapSection": "Y-Map",
"infoFileUploadTypes": "file-upload-unsupported-types",
"infoWhiteListLinks": "white-list-links",
@@ -133,7 +137,30 @@
"infoVideoChatHistory": "videoChatHistory",
"infoUpdateTime": "updateTime",
"infoId": "id",
"infoPhone": "phone",
"infoPhotoId": "photoId",
"infoAccountStatus": "accountStatus",
"infoContactOptions": "contact options",
"infoProfileOptions": "profile options",
"infoNames": "names",
"infoBaseUrl": "baseUrl",
"infoBaseRawUrl": "baseRawUrl",
"infoChatMarker": "chatMarker",
"infoServerTime": "server time",
"infoUpdates": "updates",
"infoMessagesCount": "messages in packet",
"infoContactsCount": "contacts in packet",
"infoPresenceCount": "presence records",
"infoConfigHash": "config hash",
"infoChatsCount": "chats loaded",
"infoChatsActive": "active",
"infoChatsHidden": "hidden",
"infoChatsDialogs": "dialogs",
"infoChatsGroups": "groups",
"infoChatsChannels": "channels",
"infoChatsUnread": "unread chats",
"infoChatsNewMessages": "new messages",
"infoChatsMessages": "messages in loaded chats",
"infoAccountRemovalEnabled": "account-removal-enabled",
"infoImageSize": "image-size",
"infoGce": "gce",
@@ -300,6 +327,7 @@
"appearanceTitle": "Appearance",
"appearanceVisualStyleTitle": "Visual style",
"appearanceVisualStyleSubtitle": "Material You or dimensional Glossy capsules",
"appearanceStyleAuto": "Match theme",
"appearanceVisualStyleMaterialYou": "Material You",
"appearanceVisualStyleGlossy": "Glossy",
"appearanceVisualStyleLiquidGlass": "Liquid Glass",
@@ -683,6 +711,17 @@
"chatInfoNoData": "No data",
"chatInfoHideExtra": "Hide",
"chatInfoShowMoreExtra": "Details",
"chatSendConfirmMessage": "Send this message to the chat?",
"chatSendConfirmAction": "Send",
"chatInfoRowDisableForward": "Forwarding disabled",
"chatInfoRowCopyDisabled": "Copying disabled",
"chatInfoRowOnlyAdminCall": "Admins can call",
"chatInfoRowAllCanPin": "Anyone can pin",
"chatInfoRowMembersSeeLink": "Members see the link",
"chatInfoRowConfirmBeforeSend": "Confirm before sending",
"chatInfoRowOnlyOwnerIconTitle": "Owner edits title and icon",
"chatInfoRowPromotedDisabled": "Promoted content off",
"chatInfoRowUserId": "User ID",
"chatInfoRowId": "Chat ID",
"chatInfoRowCreated": "Created",
"chatInfoRowModified": "Modified",
+234
View File
@@ -740,6 +740,24 @@ abstract class AppLocalizations {
/// **'Account'**
String get infoAccountSection;
/// No description provided for @infoPacketSection.
///
/// In en, this message translates to:
/// **'Login packet'**
String get infoPacketSection;
/// No description provided for @infoChatsSection.
///
/// In en, this message translates to:
/// **'Chats in login packet'**
String get infoChatsSection;
/// No description provided for @infoChatSettingsSection.
///
/// In en, this message translates to:
/// **'Per-chat settings'**
String get infoChatSettingsSection;
/// No description provided for @infoServerSection.
///
/// In en, this message translates to:
@@ -752,6 +770,12 @@ abstract class AppLocalizations {
/// **'User'**
String get infoUserSection;
/// No description provided for @infoExperimentsSection.
///
/// In en, this message translates to:
/// **'Experiments'**
String get infoExperimentsSection;
/// No description provided for @infoYMapSection.
///
/// In en, this message translates to:
@@ -800,12 +824,150 @@ abstract class AppLocalizations {
/// **'id'**
String get infoId;
/// No description provided for @infoPhone.
///
/// In en, this message translates to:
/// **'phone'**
String get infoPhone;
/// No description provided for @infoPhotoId.
///
/// In en, this message translates to:
/// **'photoId'**
String get infoPhotoId;
/// No description provided for @infoAccountStatus.
///
/// In en, this message translates to:
/// **'accountStatus'**
String get infoAccountStatus;
/// No description provided for @infoContactOptions.
///
/// In en, this message translates to:
/// **'contact options'**
String get infoContactOptions;
/// No description provided for @infoProfileOptions.
///
/// In en, this message translates to:
/// **'profile options'**
String get infoProfileOptions;
/// No description provided for @infoNames.
///
/// In en, this message translates to:
/// **'names'**
String get infoNames;
/// No description provided for @infoBaseUrl.
///
/// In en, this message translates to:
/// **'baseUrl'**
String get infoBaseUrl;
/// No description provided for @infoBaseRawUrl.
///
/// In en, this message translates to:
/// **'baseRawUrl'**
String get infoBaseRawUrl;
/// No description provided for @infoChatMarker.
///
/// In en, this message translates to:
/// **'chatMarker'**
String get infoChatMarker;
/// No description provided for @infoServerTime.
///
/// In en, this message translates to:
/// **'server time'**
String get infoServerTime;
/// No description provided for @infoUpdates.
///
/// In en, this message translates to:
/// **'updates'**
String get infoUpdates;
/// No description provided for @infoMessagesCount.
///
/// In en, this message translates to:
/// **'messages in packet'**
String get infoMessagesCount;
/// No description provided for @infoContactsCount.
///
/// In en, this message translates to:
/// **'contacts in packet'**
String get infoContactsCount;
/// No description provided for @infoPresenceCount.
///
/// In en, this message translates to:
/// **'presence records'**
String get infoPresenceCount;
/// No description provided for @infoConfigHash.
///
/// In en, this message translates to:
/// **'config hash'**
String get infoConfigHash;
/// No description provided for @infoChatsCount.
///
/// In en, this message translates to:
/// **'chats loaded'**
String get infoChatsCount;
/// No description provided for @infoChatsActive.
///
/// In en, this message translates to:
/// **'active'**
String get infoChatsActive;
/// No description provided for @infoChatsHidden.
///
/// In en, this message translates to:
/// **'hidden'**
String get infoChatsHidden;
/// No description provided for @infoChatsDialogs.
///
/// In en, this message translates to:
/// **'dialogs'**
String get infoChatsDialogs;
/// No description provided for @infoChatsGroups.
///
/// In en, this message translates to:
/// **'groups'**
String get infoChatsGroups;
/// No description provided for @infoChatsChannels.
///
/// In en, this message translates to:
/// **'channels'**
String get infoChatsChannels;
/// No description provided for @infoChatsUnread.
///
/// In en, this message translates to:
/// **'unread chats'**
String get infoChatsUnread;
/// No description provided for @infoChatsNewMessages.
///
/// In en, this message translates to:
/// **'new messages'**
String get infoChatsNewMessages;
/// No description provided for @infoChatsMessages.
///
/// In en, this message translates to:
/// **'messages in loaded chats'**
String get infoChatsMessages;
/// No description provided for @infoAccountRemovalEnabled.
///
/// In en, this message translates to:
@@ -1544,6 +1706,12 @@ abstract class AppLocalizations {
/// **'Material You or dimensional Glossy capsules'**
String get appearanceVisualStyleSubtitle;
/// No description provided for @appearanceStyleAuto.
///
/// In en, this message translates to:
/// **'Match theme'**
String get appearanceStyleAuto;
/// No description provided for @appearanceVisualStyleMaterialYou.
///
/// In en, this message translates to:
@@ -3110,6 +3278,72 @@ abstract class AppLocalizations {
/// **'Details'**
String get chatInfoShowMoreExtra;
/// No description provided for @chatSendConfirmMessage.
///
/// In en, this message translates to:
/// **'Send this message to the chat?'**
String get chatSendConfirmMessage;
/// No description provided for @chatSendConfirmAction.
///
/// In en, this message translates to:
/// **'Send'**
String get chatSendConfirmAction;
/// No description provided for @chatInfoRowDisableForward.
///
/// In en, this message translates to:
/// **'Forwarding disabled'**
String get chatInfoRowDisableForward;
/// No description provided for @chatInfoRowCopyDisabled.
///
/// In en, this message translates to:
/// **'Copying disabled'**
String get chatInfoRowCopyDisabled;
/// No description provided for @chatInfoRowOnlyAdminCall.
///
/// In en, this message translates to:
/// **'Admins can call'**
String get chatInfoRowOnlyAdminCall;
/// No description provided for @chatInfoRowAllCanPin.
///
/// In en, this message translates to:
/// **'Anyone can pin'**
String get chatInfoRowAllCanPin;
/// No description provided for @chatInfoRowMembersSeeLink.
///
/// In en, this message translates to:
/// **'Members see the link'**
String get chatInfoRowMembersSeeLink;
/// No description provided for @chatInfoRowConfirmBeforeSend.
///
/// In en, this message translates to:
/// **'Confirm before sending'**
String get chatInfoRowConfirmBeforeSend;
/// No description provided for @chatInfoRowOnlyOwnerIconTitle.
///
/// In en, this message translates to:
/// **'Owner edits title and icon'**
String get chatInfoRowOnlyOwnerIconTitle;
/// No description provided for @chatInfoRowPromotedDisabled.
///
/// In en, this message translates to:
/// **'Promoted content off'**
String get chatInfoRowPromotedDisabled;
/// No description provided for @chatInfoRowUserId.
///
/// In en, this message translates to:
/// **'User ID'**
String get chatInfoRowUserId;
/// No description provided for @chatInfoRowId.
///
/// In en, this message translates to:
+117
View File
@@ -348,12 +348,24 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get infoAccountSection => 'Account';
@override
String get infoPacketSection => 'Login packet';
@override
String get infoChatsSection => 'Chats in login packet';
@override
String get infoChatSettingsSection => 'Per-chat settings';
@override
String get infoServerSection => 'Server';
@override
String get infoUserSection => 'User';
@override
String get infoExperimentsSection => 'Experiments';
@override
String get infoYMapSection => 'Y-Map';
@@ -378,9 +390,78 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get infoId => 'id';
@override
String get infoPhone => 'phone';
@override
String get infoPhotoId => 'photoId';
@override
String get infoAccountStatus => 'accountStatus';
@override
String get infoContactOptions => 'contact options';
@override
String get infoProfileOptions => 'profile options';
@override
String get infoNames => 'names';
@override
String get infoBaseUrl => 'baseUrl';
@override
String get infoBaseRawUrl => 'baseRawUrl';
@override
String get infoChatMarker => 'chatMarker';
@override
String get infoServerTime => 'server time';
@override
String get infoUpdates => 'updates';
@override
String get infoMessagesCount => 'messages in packet';
@override
String get infoContactsCount => 'contacts in packet';
@override
String get infoPresenceCount => 'presence records';
@override
String get infoConfigHash => 'config hash';
@override
String get infoChatsCount => 'chats loaded';
@override
String get infoChatsActive => 'active';
@override
String get infoChatsHidden => 'hidden';
@override
String get infoChatsDialogs => 'dialogs';
@override
String get infoChatsGroups => 'groups';
@override
String get infoChatsChannels => 'channels';
@override
String get infoChatsUnread => 'unread chats';
@override
String get infoChatsNewMessages => 'new messages';
@override
String get infoChatsMessages => 'messages in loaded chats';
@override
String get infoAccountRemovalEnabled => 'account-removal-enabled';
@@ -776,6 +857,9 @@ class AppLocalizationsEn extends AppLocalizations {
String get appearanceVisualStyleSubtitle =>
'Material You or dimensional Glossy capsules';
@override
String get appearanceStyleAuto => 'Match theme';
@override
String get appearanceVisualStyleMaterialYou => 'Material You';
@@ -1606,6 +1690,39 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get chatInfoShowMoreExtra => 'Details';
@override
String get chatSendConfirmMessage => 'Send this message to the chat?';
@override
String get chatSendConfirmAction => 'Send';
@override
String get chatInfoRowDisableForward => 'Forwarding disabled';
@override
String get chatInfoRowCopyDisabled => 'Copying disabled';
@override
String get chatInfoRowOnlyAdminCall => 'Admins can call';
@override
String get chatInfoRowAllCanPin => 'Anyone can pin';
@override
String get chatInfoRowMembersSeeLink => 'Members see the link';
@override
String get chatInfoRowConfirmBeforeSend => 'Confirm before sending';
@override
String get chatInfoRowOnlyOwnerIconTitle => 'Owner edits title and icon';
@override
String get chatInfoRowPromotedDisabled => 'Promoted content off';
@override
String get chatInfoRowUserId => 'User ID';
@override
String get chatInfoRowId => 'Chat ID';
+117
View File
@@ -350,12 +350,24 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get infoAccountSection => 'Аккаунт';
@override
String get infoPacketSection => 'Пакет входа';
@override
String get infoChatsSection => 'Чаты в пакете входа';
@override
String get infoChatSettingsSection => 'Настройки отдельных чатов';
@override
String get infoServerSection => 'Сервер';
@override
String get infoUserSection => 'Пользователь';
@override
String get infoExperimentsSection => 'Эксперименты';
@override
String get infoYMapSection => 'Y-Map';
@@ -380,9 +392,78 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get infoId => 'id аккаунта:';
@override
String get infoPhone => 'Телефон:';
@override
String get infoPhotoId => 'id аватарки:';
@override
String get infoAccountStatus => 'Статус аккаунта:';
@override
String get infoContactOptions => 'Опции контакта:';
@override
String get infoProfileOptions => 'Опции профиля:';
@override
String get infoNames => 'Имена:';
@override
String get infoBaseUrl => 'Ссылка на аватарку:';
@override
String get infoBaseRawUrl => 'Исходная аватарка:';
@override
String get infoChatMarker => 'chatMarker';
@override
String get infoServerTime => 'Время сервера:';
@override
String get infoUpdates => 'Количество обновлений:';
@override
String get infoMessagesCount => 'Сообщений в пакете:';
@override
String get infoContactsCount => 'Контактов в пакете:';
@override
String get infoPresenceCount => 'Статусов присутствия:';
@override
String get infoConfigHash => 'Хеш конфигурации:';
@override
String get infoChatsCount => 'Загружено чатов:';
@override
String get infoChatsActive => 'Активных:';
@override
String get infoChatsHidden => 'Скрытых:';
@override
String get infoChatsDialogs => 'Диалогов:';
@override
String get infoChatsGroups => 'Групп:';
@override
String get infoChatsChannels => 'Каналов:';
@override
String get infoChatsUnread => 'Непрочитанных чатов:';
@override
String get infoChatsNewMessages => 'Новых сообщений:';
@override
String get infoChatsMessages => 'Сообщений в загруженных чатах:';
@override
String get infoAccountRemovalEnabled => 'Мгновенное удаление аккаунта:';
@@ -781,6 +862,9 @@ class AppLocalizationsRu extends AppLocalizations {
String get appearanceVisualStyleSubtitle =>
'Material You или объёмные Glossy-капсулы';
@override
String get appearanceStyleAuto => 'Как в теме';
@override
String get appearanceVisualStyleMaterialYou => 'Material You';
@@ -1615,6 +1699,39 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get chatInfoShowMoreExtra => 'Подробнее';
@override
String get chatSendConfirmMessage => 'Отправить это сообщение в чат?';
@override
String get chatSendConfirmAction => 'Отправить';
@override
String get chatInfoRowDisableForward => 'Пересылка запрещена';
@override
String get chatInfoRowCopyDisabled => 'Копирование запрещено';
@override
String get chatInfoRowOnlyAdminCall => 'Звонить могут админы';
@override
String get chatInfoRowAllCanPin => 'Все могут закреплять';
@override
String get chatInfoRowMembersSeeLink => 'Ссылка видна участникам';
@override
String get chatInfoRowConfirmBeforeSend => 'Подтверждать отправку';
@override
String get chatInfoRowOnlyOwnerIconTitle => 'Название меняет владелец';
@override
String get chatInfoRowPromotedDisabled => 'Реклама отключена';
@override
String get chatInfoRowUserId => 'ID пользователя';
@override
String get chatInfoRowId => 'ID чата';
+39
View File
@@ -123,8 +123,12 @@
"profileMenuSpoof": "Подмена данных",
"infoTitle": "Info",
"infoAccountSection": "Аккаунт",
"infoPacketSection": "Пакет входа",
"infoChatsSection": "Чаты в пакете входа",
"infoChatSettingsSection": "Настройки отдельных чатов",
"infoServerSection": "Сервер",
"infoUserSection": "Пользователь",
"infoExperimentsSection": "Эксперименты",
"infoYMapSection": "Y-Map",
"infoFileUploadTypes": "запрещённые типы файлов",
"infoWhiteListLinks": "безопасные ссылки",
@@ -133,7 +137,30 @@
"infoVideoChatHistory": "videoChatHistory",
"infoUpdateTime": "Последнее обновление аватарки:",
"infoId": "id аккаунта:",
"infoPhone": "Телефон:",
"infoPhotoId": "id аватарки:",
"infoAccountStatus": "Статус аккаунта:",
"infoContactOptions": "Опции контакта:",
"infoProfileOptions": "Опции профиля:",
"infoNames": "Имена:",
"infoBaseUrl": "Ссылка на аватарку:",
"infoBaseRawUrl": "Исходная аватарка:",
"infoChatMarker": "chatMarker",
"infoServerTime": "Время сервера:",
"infoUpdates": "Количество обновлений:",
"infoMessagesCount": "Сообщений в пакете:",
"infoContactsCount": "Контактов в пакете:",
"infoPresenceCount": "Статусов присутствия:",
"infoConfigHash": "Хеш конфигурации:",
"infoChatsCount": "Загружено чатов:",
"infoChatsActive": "Активных:",
"infoChatsHidden": "Скрытых:",
"infoChatsDialogs": "Диалогов:",
"infoChatsGroups": "Групп:",
"infoChatsChannels": "Каналов:",
"infoChatsUnread": "Непрочитанных чатов:",
"infoChatsNewMessages": "Новых сообщений:",
"infoChatsMessages": "Сообщений в загруженных чатах:",
"infoAccountRemovalEnabled": "Мгновенное удаление аккаунта:",
"infoImageSize": "image-size",
"infoGce": "gce",
@@ -258,6 +285,7 @@
"appearanceTitle": "Внешний вид",
"appearanceVisualStyleTitle": "Визуал",
"appearanceVisualStyleSubtitle": "Material You или объёмные Glossy-капсулы",
"appearanceStyleAuto": "Как в теме",
"appearanceVisualStyleMaterialYou": "Material You",
"appearanceVisualStyleGlossy": "Glossy",
"appearanceVisualStyleLiquidGlass": "Liquid Glass",
@@ -525,6 +553,17 @@
"chatInfoNoData": "Нет данных",
"chatInfoHideExtra": "Скрыть",
"chatInfoShowMoreExtra": "Подробнее",
"chatSendConfirmMessage": "Отправить это сообщение в чат?",
"chatSendConfirmAction": "Отправить",
"chatInfoRowDisableForward": "Пересылка запрещена",
"chatInfoRowCopyDisabled": "Копирование запрещено",
"chatInfoRowOnlyAdminCall": "Звонить могут админы",
"chatInfoRowAllCanPin": "Все могут закреплять",
"chatInfoRowMembersSeeLink": "Ссылка видна участникам",
"chatInfoRowConfirmBeforeSend": "Подтверждать отправку",
"chatInfoRowOnlyOwnerIconTitle": "Название меняет владелец",
"chatInfoRowPromotedDisabled": "Реклама отключена",
"chatInfoRowUserId": "ID пользователя",
"chatInfoRowId": "ID чата",
"chatInfoRowCreated": "Создан",
"chatInfoRowModified": "Изменён",
+4 -3
View File
@@ -871,12 +871,14 @@ class KometAppState extends State<KometApp>
_themeCacheFontId = _fontId;
_themeCacheLight = light;
_themeCacheDark = dark;
final displayFont = AppDisplayFont(AppFonts.displayFamily(_fontId));
_lightTheme = withM3ETheme(
ThemeData(
useMaterial3: true,
colorScheme: light,
pageTransitionsTheme: _appPageTransitions,
progressIndicatorTheme: _expressiveProgressTheme,
extensions: [displayFont],
textTheme: AppFonts.textTheme(
_fontId,
ThemeData(brightness: Brightness.light).textTheme,
@@ -889,6 +891,7 @@ class KometAppState extends State<KometApp>
colorScheme: dark,
pageTransitionsTheme: _appPageTransitions,
progressIndicatorTheme: _expressiveProgressTheme,
extensions: [displayFont],
textTheme: AppFonts.textTheme(
_fontId,
ThemeData(brightness: Brightness.dark).textTheme,
@@ -1109,9 +1112,7 @@ class _StartupScreenState extends State<_StartupScreen> {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
body: Center(
child: SmallSpinner(size: 36, color: cs.primary),
),
body: Center(child: SmallSpinner(size: 36, color: cs.primary)),
);
}
}
+8
View File
@@ -23,6 +23,14 @@ class ChatInfo {
bool isAdmin(int id) => adminIds.contains(id);
bool isOwner(int id) => owner != null && id == owner;
bool option(String name) {
final opts = raw['options'];
return opts is Map && opts[name] == true;
}
bool canSeeInviteLink(int id) =>
isAdmin(id) || isOwner(id) || option('MEMBERS_CAN_SEE_PRIVATE_LINK');
String? adminAlias(int id) {
final source = raw['adminParticipants'];
if (source is! Map) return null;
+141
View File
@@ -0,0 +1,141 @@
class LoginInfo {
const LoginInfo._();
static Map<String, dynamic> fromPayload(Map<dynamic, dynamic> payload) {
final profile = _asMap(payload['profile']);
final contact = _asMap(profile?['contact']);
final chats = _asList(payload['chats']);
final config = _asMap(payload['config']);
final server = _asMap(config?['server']);
final user = _asMap(config?['user']);
final experiments = _asMap(config?['experiments']);
final chatSettings = _asMap(config?['chats']);
return {
'registrationTime': contact?['registrationTime'],
'country': contact?['country'],
'videoChatHistory': payload['videoChatHistory'],
'updateTime': contact?['updateTime'],
'id': contact?['id'],
'phone': contact?['phone'],
'photoId': contact?['photoId'],
'accountStatus': contact?['accountStatus'],
'contactOptions': _copyJsonValue(contact?['options']),
'profileOptions': _copyJsonValue(profile?['profileOptions']),
'names': _copyJsonValue(contact?['names']),
'baseUrl': contact?['baseUrl'],
'baseRawUrl': contact?['baseRawUrl'],
'chatMarker': payload['chatMarker'] ?? _latestChatEventTime(chats),
'time': payload['time'],
'updates': payload['updates'],
'messagesCount': _collectionLength(payload['messages']),
'contactsCount': _collectionLength(payload['contacts']),
'presenceCount': _collectionLength(payload['presence']),
'configHash': config?['hash'],
'chats': _buildChatsSummary(chats),
'server': _copyMap(server),
'user': _copyMap(user),
'experiments': _copyMap(experiments),
'chatSettings': _copyMap(chatSettings),
};
}
static Map<String, dynamic> _buildChatsSummary(List<dynamic> chats) {
var active = 0;
var hidden = 0;
var dialogs = 0;
var groups = 0;
var channels = 0;
var unread = 0;
var newMessages = 0;
var messages = 0;
for (final rawChat in chats) {
final chat = _asMap(rawChat);
if (chat == null) continue;
switch (chat['status']) {
case 'ACTIVE':
active++;
case 'HIDDEN':
hidden++;
}
switch (chat['type']) {
case 'DIALOG':
dialogs++;
case 'CHAT':
groups++;
case 'CHANNEL':
channels++;
}
final chatNewMessages = _asInt(chat['newMessages']) ?? 0;
if (chatNewMessages > 0) unread++;
newMessages += chatNewMessages;
messages += _asInt(chat['messagesCount']) ?? 0;
}
return {
'count': chats.length,
'active': active,
'hidden': hidden,
'dialogs': dialogs,
'groups': groups,
'channels': channels,
'unread': unread,
'newMessages': newMessages,
'messages': messages,
};
}
static int? _latestChatEventTime(List<dynamic> chats) {
int? latest;
for (final rawChat in chats) {
final chat = _asMap(rawChat);
final value = _asInt(chat?['lastEventTime']);
if (value != null && (latest == null || value > latest)) {
latest = value;
}
}
return latest;
}
static Map<dynamic, dynamic>? _asMap(dynamic value) {
return value is Map ? value : null;
}
static List<dynamic> _asList(dynamic value) {
return value is List ? value : const [];
}
static int? _asInt(dynamic value) {
return switch (value) {
int number => number,
num number => number.toInt(),
String text => int.tryParse(text),
_ => null,
};
}
static int _collectionLength(dynamic value) {
return switch (value) {
Map items => items.length,
List items => items.length,
_ => 0,
};
}
static Map<String, dynamic>? _copyMap(Map<dynamic, dynamic>? value) {
if (value == null) return null;
return value.map(
(key, item) => MapEntry(key.toString(), _copyJsonValue(item)),
);
}
static dynamic _copyJsonValue(dynamic value) {
if (value is Map) return _copyMap(value);
if (value is List) return value.map(_copyJsonValue).toList();
if (value == null || value is String || value is num || value is bool) {
return value;
}
return value.toString();
}
}
+141
View File
@@ -0,0 +1,141 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:komet/models/login_info.dart';
void main() {
group('LoginInfo', () {
test('extracts profile, packet, chat, and config data', () {
final info = LoginInfo.fromPayload({
'profile': {
'contact': {
'id': 100200300,
'updateTime': 1700000002000,
'registrationTime': 1700000001000,
'baseUrl': 'https://example.invalid/avatar',
'baseRawUrl': 'https://example.invalid/avatar/raw',
'photoId': 400500600,
'phone': 70000000000,
'names': [
{
'name': 'Тест Пользователь',
'firstName': 'Тест',
'lastName': 'Пользователь',
'type': 'SYNTHETIC',
},
],
'options': ['SYNTHETIC'],
'accountStatus': 2,
'country': 'ZZ',
},
'profileOptions': [7],
},
'chats': [
{
'id': -1001,
'type': 'CHAT',
'status': 'ACTIVE',
'lastEventTime': 1700000008000,
'newMessages': 3,
'messagesCount': 12,
},
{
'id': -1002,
'type': 'CHANNEL',
'status': 'HIDDEN',
'lastEventTime': 1700000007000,
},
{
'id': 1003,
'type': 'DIALOG',
'status': 'ACTIVE',
'lastEventTime': 1700000006000,
},
],
'chatMarker': 1700000009000,
'contacts': [
{'id': 2001},
{'id': 2002},
],
'presence': {'synthetic-peer': 1700000003000},
'messages': {'synthetic-message': {}},
'config': {
'hash': 'synthetic-config-hash',
'server': {
'known-flag': true,
'nested': {'limit': 4},
},
'user': {'SYNTHETIC_SETTING': 'ON'},
'chats': {
'synthetic-chat': {'sound': false},
},
'experiments': {
'synthetic-experiment': {'enabled': true},
},
},
'videoChatHistory': true,
'time': 1700000010000,
'updates': 4,
});
expect(info['id'], 100200300);
expect(info['phone'], 70000000000);
expect(info['photoId'], 400500600);
expect(info['accountStatus'], 2);
expect(info['country'], 'ZZ');
expect(info['profileOptions'], [7]);
expect(info['contactOptions'], ['SYNTHETIC']);
expect(info['chatMarker'], 1700000009000);
expect(info['contactsCount'], 2);
expect(info['presenceCount'], 1);
expect(info['messagesCount'], 1);
expect(info['configHash'], 'synthetic-config-hash');
expect(info['chats'], {
'count': 3,
'active': 2,
'hidden': 1,
'dialogs': 1,
'groups': 1,
'channels': 1,
'unread': 1,
'newMessages': 3,
'messages': 12,
});
expect(info['server'], {
'known-flag': true,
'nested': {'limit': 4},
});
expect(info['user'], {'SYNTHETIC_SETTING': 'ON'});
expect(info['chatSettings'], {
'synthetic-chat': {'sound': false},
});
expect(info['experiments'], {
'synthetic-experiment': {'enabled': true},
});
expect(() => jsonEncode(info), returnsNormally);
});
test('falls back to latest chat event when marker is absent', () {
final info = LoginInfo.fromPayload({
'chats': [
{'lastEventTime': 1700000004000},
{'lastEventTime': 1700000006000},
{'lastEventTime': 1700000005000},
],
});
expect(info['chatMarker'], 1700000006000);
expect(info['chats'], {
'count': 3,
'active': 0,
'hidden': 0,
'dialogs': 0,
'groups': 0,
'channels': 0,
'unread': 0,
'newMessages': 0,
'messages': 0,
});
});
});
}
+9 -9
View File
@@ -101,18 +101,18 @@ void main() {
test('message links carry the message id', () {
final byName =
MaxLink.parse('max.ru/somechannel/117008613873053494')
MaxLink.parse('max.ru/somechannel/900000000000000001')
as MaxContentLink;
expect(byName.kind, MaxContentKind.public);
expect(byName.messageId, 117008613873053494);
expect(byName.messageId, 900000000000000001);
expect(byName.baseUrl, 'https://max.ru/somechannel');
final byId =
MaxLink.parse('max.ru/c/1673760/117008613873053494')
MaxLink.parse('max.ru/c/424242/900000000000000001')
as MaxContentLink;
expect(byId.kind, MaxContentKind.content);
expect(byId.messageId, 117008613873053494);
expect(byId.baseUrl, 'https://max.ru/c/1673760');
expect(byId.messageId, 900000000000000001);
expect(byId.baseUrl, 'https://max.ru/c/424242');
});
test('invite, call and sticker links keep their own types', () {
@@ -129,12 +129,12 @@ void main() {
test('uid and cid open a contact and a chat', () {
expect(
(MaxLink.parse('max://max.ru/?uid=105587131') as MaxContactIdLink)
(MaxLink.parse('max://max.ru/?uid=434343') as MaxContactIdLink)
.userId,
105587131,
434343,
);
final chat = MaxLink.parse('max://max.ru/?cid=1673760') as MaxChatIdLink;
expect(chat.chatId, 1673760);
final chat = MaxLink.parse('max://max.ru/?cid=424242') as MaxChatIdLink;
expect(chat.chatId, 424242);
expect(chat.messageId, isNull);
});
});