fix/feat: мелкие фиксы, и доп. инфа в info
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
@@ -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'];
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 чата';
|
||||
|
||||
|
||||
@@ -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
@@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user