изменение профиля
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart' show Locale;
|
||||
import '../api.dart';
|
||||
import '../../core/protocol/opcode_map.dart';
|
||||
import '../../core/protocol/packet.dart';
|
||||
@@ -431,6 +432,82 @@ class AccountModule {
|
||||
return config;
|
||||
}
|
||||
|
||||
Future<ProfileData> updateProfileName(String firstName, String? lastName) async {
|
||||
_ensureOnline();
|
||||
final payload = <dynamic, dynamic>{
|
||||
'firstName': firstName,
|
||||
};
|
||||
if (lastName != null) payload['lastName'] = lastName;
|
||||
final packet = await _api.sendRequest(Opcode.profile, payload);
|
||||
if (packet.isError) {
|
||||
throw Exception(packet.payload?.toString() ?? 'Server error');
|
||||
}
|
||||
final data = packet.payload as Map?;
|
||||
if (data == null) throw Exception('Empty response');
|
||||
final profile = data['profile'] as Map?;
|
||||
if (profile == null) throw Exception('No profile in response');
|
||||
final contact = profile['contact'] as Map?;
|
||||
if (contact == null) throw Exception('No contact in response');
|
||||
final newProfile = ProfileData.fromServerMap(contact.cast<dynamic, dynamic>());
|
||||
await AppDatabase.saveProfile(newProfile, isActive: true);
|
||||
return newProfile;
|
||||
}
|
||||
|
||||
Future<ProfileData> updateProfileAvatar(String photoToken, String avatarType) async {
|
||||
_ensureOnline();
|
||||
final packet = await _api.sendRequest(Opcode.profile, {
|
||||
'photoToken': photoToken,
|
||||
'avatarType': avatarType,
|
||||
});
|
||||
if (packet.isError) {
|
||||
throw Exception(packet.payload?.toString() ?? 'Server error');
|
||||
}
|
||||
final data = packet.payload as Map?;
|
||||
if (data == null) throw Exception('Empty response');
|
||||
final profile = data['profile'] as Map?;
|
||||
if (profile == null) throw Exception('No profile in response');
|
||||
final contact = profile['contact'] as Map?;
|
||||
if (contact == null) throw Exception('No contact in response');
|
||||
final newProfile = ProfileData.fromServerMap(contact.cast<dynamic, dynamic>());
|
||||
await AppDatabase.saveProfile(newProfile, isActive: true);
|
||||
return newProfile;
|
||||
}
|
||||
|
||||
Future<String> getAvatarUploadUrl() async {
|
||||
_ensureOnline();
|
||||
final packet = await _api.sendRequest(Opcode.photoUpload, {
|
||||
'count': 1,
|
||||
'profile': true,
|
||||
});
|
||||
if (packet.isError) {
|
||||
throw Exception(packet.payload?.toString() ?? 'Server error');
|
||||
}
|
||||
final data = packet.payload as Map?;
|
||||
if (data == null) throw Exception('Empty response');
|
||||
final url = data['url'] as String?;
|
||||
if (url == null) throw Exception('No url in response');
|
||||
return url;
|
||||
}
|
||||
|
||||
Future<ProfileData> removeProfilePhoto(int photoId) async {
|
||||
_ensureOnline();
|
||||
final packet = await _api.sendRequest(Opcode.removeContactPhoto, {
|
||||
'photoId': photoId,
|
||||
});
|
||||
if (packet.isError) {
|
||||
throw Exception(packet.payload?.toString() ?? 'Server error');
|
||||
}
|
||||
final data = packet.payload as Map?;
|
||||
if (data == null) throw Exception('Empty response');
|
||||
final profile = data['profile'] as Map?;
|
||||
if (profile == null) throw Exception('No profile in response');
|
||||
final contact = profile['contact'] as Map?;
|
||||
if (contact == null) throw Exception('No contact in response');
|
||||
final newProfile = ProfileData.fromServerMap(contact.cast<dynamic, dynamic>());
|
||||
await AppDatabase.saveProfile(newProfile, isActive: true);
|
||||
return newProfile;
|
||||
}
|
||||
|
||||
// 2FA Creation (when not set)
|
||||
Future<String> create2faTrack() async {
|
||||
_ensureOnline();
|
||||
@@ -931,7 +1008,7 @@ class AccountModule {
|
||||
throw Exception('login: отсутствует profile.contact в ответе');
|
||||
}
|
||||
final profile = ProfileData.fromServerMap(contact.cast<dynamic, dynamic>());
|
||||
await AppDatabase.saveProfile(profile);
|
||||
await AppDatabase.saveProfile(profile, isActive: true);
|
||||
await AppDatabase.setActiveAccount(profile.id);
|
||||
|
||||
await _saveSyncState(data, serverTime, profile.id);
|
||||
|
||||
@@ -98,7 +98,7 @@ class ProfileData {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toDbRow() => {
|
||||
Map<String, dynamic> toDbRow({bool isActive = false}) => {
|
||||
'id': id,
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
@@ -109,6 +109,7 @@ class ProfileData {
|
||||
'country': country,
|
||||
'account_status': accountStatus,
|
||||
'update_time': updateTime,
|
||||
'is_active': isActive ? 1 : 0,
|
||||
'profile_options': profileOptions?.join(','),
|
||||
};
|
||||
}
|
||||
@@ -280,11 +281,11 @@ class AppDatabase {
|
||||
)
|
||||
''';
|
||||
|
||||
static Future<void> saveProfile(ProfileData profile) async {
|
||||
static Future<void> saveProfile(ProfileData profile, {bool isActive = true}) async {
|
||||
final db = await _instance;
|
||||
await db.insert(
|
||||
'profile',
|
||||
profile.toDbRow(),
|
||||
profile.toDbRow(isActive: isActive),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../../core/storage/app_database.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../../main.dart' show accountModule, KometApp;
|
||||
import '../../widgets/custom_notification.dart';
|
||||
|
||||
class EditProfileScreen extends StatefulWidget {
|
||||
const EditProfileScreen({super.key});
|
||||
|
||||
@override
|
||||
State<EditProfileScreen> createState() => _EditProfileScreenState();
|
||||
}
|
||||
|
||||
class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
bool _isLoading = true;
|
||||
bool _isSaving = false;
|
||||
String? _avatarUrl;
|
||||
int? _photoId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProfile();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadProfile() async {
|
||||
final profile = await AppDatabase.loadActiveProfile();
|
||||
if (!mounted) return;
|
||||
if (profile != null) {
|
||||
_firstNameController.text = profile.firstName;
|
||||
_lastNameController.text = profile.lastName ?? '';
|
||||
_avatarUrl = profile.baseUrl;
|
||||
_photoId = profile.photoId;
|
||||
setState(() => _isLoading = false);
|
||||
} else {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveName() async {
|
||||
if (_isSaving) return;
|
||||
final firstName = _firstNameController.text.trim();
|
||||
if (firstName.isEmpty) {
|
||||
if (mounted) showCustomNotification(context, 'Имя не может быть пустым');
|
||||
return;
|
||||
}
|
||||
setState(() => _isSaving = true);
|
||||
try {
|
||||
final newProfile = await accountModule.updateProfileName(
|
||||
firstName,
|
||||
_lastNameController.text.trim().isEmpty ? null : _lastNameController.text.trim(),
|
||||
);
|
||||
_avatarUrl = newProfile.baseUrl;
|
||||
_photoId = newProfile.photoId;
|
||||
KometApp.stateOf(context)?.notifyProfileUpdate();
|
||||
if (mounted) {
|
||||
showCustomNotification(context, 'Имя сохранено');
|
||||
setState(() => _isSaving = false);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
showCustomNotification(context, 'Ошибка: $e');
|
||||
setState(() => _isSaving = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _changeAvatar() async {
|
||||
if (_isSaving) return;
|
||||
try {
|
||||
final uploadUrl = await accountModule.getAvatarUploadUrl();
|
||||
if (!mounted) return;
|
||||
showCustomNotification(context, 'Загрузка аватарки: $uploadUrl (пока нет)');
|
||||
} catch (e) {
|
||||
if (mounted) showCustomNotification(context, 'Ошибка: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _removeAvatar() async {
|
||||
if (_isSaving || _photoId == null) return;
|
||||
setState(() => _isSaving = true);
|
||||
try {
|
||||
final newProfile = await accountModule.removeProfilePhoto(_photoId!);
|
||||
_avatarUrl = newProfile.baseUrl;
|
||||
_photoId = newProfile.photoId;
|
||||
KometApp.stateOf(context)?.notifyProfileUpdate();
|
||||
if (mounted) {
|
||||
showCustomNotification(context, 'Фото удалено');
|
||||
setState(() => _isSaving = false);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
showCustomNotification(context, 'Ошибка: $e');
|
||||
setState(() => _isSaving = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Symbols.arrow_back, color: cs.onSurface),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
l10n?.editProfileTitle ?? 'Edit Profile',
|
||||
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isLoading || _isSaving ? null : _saveName,
|
||||
child: _isSaving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(
|
||||
l10n?.editProfileSave ?? 'Save',
|
||||
style: TextStyle(color: cs.primary, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 88,
|
||||
height: 88,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: cs.primary.withValues(alpha: 0.5),
|
||||
width: 2.5,
|
||||
),
|
||||
),
|
||||
child: ClipOval(
|
||||
child: _avatarUrl != null && _avatarUrl!.isNotEmpty
|
||||
? Image.network(_avatarUrl!, fit: BoxFit.cover)
|
||||
: Container(
|
||||
color: cs.primaryContainer,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
_firstNameController.text.isNotEmpty
|
||||
? _firstNameController.text[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color: cs.onPrimaryContainer,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(Symbols.camera_alt, color: cs.onPrimary, size: 20),
|
||||
onPressed: _changeAvatar,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_photoId != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: _removeAvatar,
|
||||
child: Text(
|
||||
l10n?.editProfileRemovePhoto ?? 'Remove photo',
|
||||
style: TextStyle(color: cs.error),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
_buildTextField(
|
||||
l10n?.editProfileFirstName ?? 'First name',
|
||||
_firstNameController,
|
||||
cs,
|
||||
enabled: !_isSaving,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
l10n?.editProfileLastName ?? 'Last name',
|
||||
_lastNameController,
|
||||
cs,
|
||||
enabled: !_isSaving,
|
||||
),
|
||||
const SizedBox(height: 120),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField(String label, TextEditingController controller, ColorScheme cs, {bool enabled = true}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 6),
|
||||
child: Text(label, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
|
||||
),
|
||||
TextField(
|
||||
controller: controller,
|
||||
enabled: enabled,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: cs.surfaceContainerHigh,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -115,11 +115,11 @@ class _InfoScreenState extends State<InfoScreen> {
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildSectionTitle(l10n.infoAccountSection, cs),
|
||||
...accountKeys.entries.map((e) => _buildRow(e.key, e.value, _formatValue(info[e.key]), cs)),
|
||||
...accountKeys.entries.map((e) => _buildRow(e.key, e.value, _formatValue(info[e.key], e.key), cs)),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
_buildSectionTitle(l10n.infoServerSection, cs),
|
||||
...serverKeys.entries.map((e) => _buildRow(e.key, e.value, _formatValue(server?[e.key]), cs)),
|
||||
...serverKeys.entries.map((e) => _buildRow(e.key, e.value, _formatValue(server?[e.key], e.key), cs)),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
_buildSectionTitle(l10n.infoYMapSection, cs),
|
||||
@@ -240,28 +240,17 @@ class _InfoScreenState extends State<InfoScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
String _formatValue(dynamic value) {
|
||||
String _formatValue(dynamic value, String key) {
|
||||
if (value == null) return '-';
|
||||
if (value is Map && value.containsKey('chatMarker')) {
|
||||
final ts = value['chatMarker'] as int?;
|
||||
if (ts != null) {
|
||||
final dt = DateTime.fromMillisecondsSinceEpoch(ts);
|
||||
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
|
||||
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
return '-';
|
||||
return ts != null ? _formatTs(ts) : '-';
|
||||
}
|
||||
if (value is int && value > 1000000000000) {
|
||||
final dt = DateTime.fromMillisecondsSinceEpoch(value);
|
||||
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
|
||||
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
if (value is int && value > 86400) {
|
||||
if (value is int && value > 1000000000000) return _formatTs(value);
|
||||
if (key == 'edit-timeout' && value is int && value > 0) {
|
||||
final weeks = value ~/ 604800;
|
||||
final days = (value % 604800) ~/ 86400;
|
||||
if (weeks > 0) {
|
||||
return '$weeks ${_w(weeks)} ${days > 0 ? '$days ${_d(days)}' : ''}'.trim();
|
||||
}
|
||||
if (weeks > 0) return '$weeks ${_w(weeks)} ${days > 0 ? '$days ${_d(days)}' : ''}'.trim();
|
||||
final h = value ~/ 3600;
|
||||
final m = (value % 3600) ~/ 60;
|
||||
if (h > 0) return '${h}h ${m}m';
|
||||
@@ -270,6 +259,13 @@ class _InfoScreenState extends State<InfoScreen> {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
String _formatTs(int ts) {
|
||||
if (ts < 1000000000000) return ts.toString();
|
||||
final dt = DateTime.fromMillisecondsSinceEpoch(ts);
|
||||
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
|
||||
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String _w(int n) {
|
||||
final m = n % 10;
|
||||
if (m == 1 && n != 11) return 'нед';
|
||||
|
||||
@@ -6,9 +6,11 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import '../../../core/storage/app_database.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../../main.dart';
|
||||
import '../auth/proxy_settings_sheet.dart';
|
||||
import 'debug_menu_screen.dart';
|
||||
import 'devices_screen.dart';
|
||||
import 'edit_profile_screen.dart';
|
||||
import 'info_screen.dart';
|
||||
import 'security_screen.dart';
|
||||
import 'spoof_screen.dart';
|
||||
@@ -27,17 +29,25 @@ class _SettingsTabState extends State<SettingsTab> {
|
||||
bool _debugMenuVisible = false;
|
||||
int _versionSecretTapCount = 0;
|
||||
Timer? _versionSecretTapResetTimer;
|
||||
StreamSubscription? _profileUpdateSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProfile();
|
||||
_loadAppVersion();
|
||||
final appState = KometApp.stateOf(context);
|
||||
if (appState != null) {
|
||||
_profileUpdateSub = appState.profileUpdateStream.listen((_) {
|
||||
if (mounted) _loadProfile();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_versionSecretTapResetTimer?.cancel();
|
||||
_profileUpdateSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -317,7 +327,14 @@ child: _buildSection(
|
||||
size: 22,
|
||||
weight: 400,
|
||||
),
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const EditProfileScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
+6
-1
@@ -159,5 +159,10 @@
|
||||
"chatInfoJoined": "joined:",
|
||||
"chatInfoGroupCreated": "group created:",
|
||||
"chatInfoGroupOwner": "group owner:",
|
||||
"chatInfoDialogStarted": "dialog started:"
|
||||
"chatInfoDialogStarted": "dialog started:",
|
||||
"editProfileTitle": "Edit Profile",
|
||||
"editProfileSave": "Save",
|
||||
"editProfileFirstName": "First name",
|
||||
"editProfileLastName": "Last name",
|
||||
"editProfileRemovePhoto": "Remove photo"
|
||||
}
|
||||
|
||||
@@ -955,6 +955,36 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'dialog started:'**
|
||||
String get chatInfoDialogStarted;
|
||||
|
||||
/// No description provided for @editProfileTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit Profile'**
|
||||
String get editProfileTitle;
|
||||
|
||||
/// No description provided for @editProfileSave.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save'**
|
||||
String get editProfileSave;
|
||||
|
||||
/// No description provided for @editProfileFirstName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'First name'**
|
||||
String get editProfileFirstName;
|
||||
|
||||
/// No description provided for @editProfileLastName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last name'**
|
||||
String get editProfileLastName;
|
||||
|
||||
/// No description provided for @editProfileRemovePhoto.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove photo'**
|
||||
String get editProfileRemovePhoto;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -451,4 +451,19 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get chatInfoDialogStarted => 'dialog started:';
|
||||
|
||||
@override
|
||||
String get editProfileTitle => 'Edit Profile';
|
||||
|
||||
@override
|
||||
String get editProfileSave => 'Save';
|
||||
|
||||
@override
|
||||
String get editProfileFirstName => 'First name';
|
||||
|
||||
@override
|
||||
String get editProfileLastName => 'Last name';
|
||||
|
||||
@override
|
||||
String get editProfileRemovePhoto => 'Remove photo';
|
||||
}
|
||||
|
||||
@@ -453,4 +453,19 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get chatInfoDialogStarted => 'ЛС начат в:';
|
||||
|
||||
@override
|
||||
String get editProfileTitle => 'Редактирование профиля';
|
||||
|
||||
@override
|
||||
String get editProfileSave => 'Сохранить';
|
||||
|
||||
@override
|
||||
String get editProfileFirstName => 'Имя';
|
||||
|
||||
@override
|
||||
String get editProfileLastName => 'Фамилия';
|
||||
|
||||
@override
|
||||
String get editProfileRemovePhoto => 'Удалить фото';
|
||||
}
|
||||
|
||||
+6
-1
@@ -159,5 +159,10 @@
|
||||
"chatInfoJoined": "Зашли в:",
|
||||
"chatInfoGroupCreated": "Группа создана в:",
|
||||
"chatInfoGroupOwner": "Создатель группы:",
|
||||
"chatInfoDialogStarted": "ЛС начат в:"
|
||||
"chatInfoDialogStarted": "ЛС начат в:",
|
||||
"editProfileTitle": "Редактирование профиля",
|
||||
"editProfileSave": "Сохранить",
|
||||
"editProfileFirstName": "Имя",
|
||||
"editProfileLastName": "Фамилия",
|
||||
"editProfileRemovePhoto": "Удалить фото"
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ class KometAppState extends State<KometApp> {
|
||||
late final ValueNotifier<bool> fpsOverlayEnabled = ValueNotifier(
|
||||
widget.initialFpsOverlay,
|
||||
);
|
||||
final _profileUpdateController = StreamController<void>.broadcast();
|
||||
Stream<void> get profileUpdateStream => _profileUpdateController.stream;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -123,6 +125,7 @@ class KometAppState extends State<KometApp> {
|
||||
@override
|
||||
void dispose() {
|
||||
_sessionExpiredSub?.cancel();
|
||||
_profileUpdateController.close();
|
||||
fpsOverlayEnabled.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -147,6 +150,10 @@ class KometAppState extends State<KometApp> {
|
||||
}
|
||||
}
|
||||
|
||||
void notifyProfileUpdate() {
|
||||
_profileUpdateController.add(null);
|
||||
}
|
||||
|
||||
ColorScheme _adjustDarkScheme(ColorScheme base) {
|
||||
return base.copyWith(
|
||||
surface: Color.alphaBlend(
|
||||
|
||||
Reference in New Issue
Block a user