мультиаккаунгтинг(багованный, но он есть), избранное коректно отображается, убрана кнопка ответить в канале, добавлена поддержка видеокружков и голосовых сообщений
This commit is contained in:
173
lib/services/account_manager.dart
Normal file
173
lib/services/account_manager.dart
Normal file
@@ -0,0 +1,173 @@
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:gwid/models/account.dart';
|
||||
import 'package:gwid/models/profile.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class AccountManager {
|
||||
static final AccountManager _instance = AccountManager._internal();
|
||||
factory AccountManager() => _instance;
|
||||
AccountManager._internal();
|
||||
|
||||
static const String _accountsKey = 'multi_accounts';
|
||||
static const String _currentAccountIdKey = 'current_account_id';
|
||||
|
||||
Account? _currentAccount;
|
||||
List<Account> _accounts = [];
|
||||
|
||||
Account? get currentAccount => _currentAccount;
|
||||
List<Account> get accounts => List.unmodifiable(_accounts);
|
||||
|
||||
Future<void> initialize() async {
|
||||
await _loadAccounts();
|
||||
await _loadCurrentAccount();
|
||||
}
|
||||
|
||||
Future<void> _loadAccounts() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final accountsJson = prefs.getString(_accountsKey);
|
||||
if (accountsJson != null) {
|
||||
final List<dynamic> accountsList = jsonDecode(accountsJson);
|
||||
_accounts = accountsList
|
||||
.map((json) => Account.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка загрузки аккаунтов: $e');
|
||||
_accounts = [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadCurrentAccount() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final currentAccountId = prefs.getString(_currentAccountIdKey);
|
||||
|
||||
if (currentAccountId != null) {
|
||||
_currentAccount = _accounts.firstWhere(
|
||||
(account) => account.id == currentAccountId,
|
||||
orElse: () => _accounts.isNotEmpty
|
||||
? _accounts.first
|
||||
: Account(id: '', token: '', createdAt: DateTime.now()),
|
||||
);
|
||||
} else if (_accounts.isNotEmpty) {
|
||||
_currentAccount = _accounts.first;
|
||||
await _saveCurrentAccountId(_currentAccount!.id);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка загрузки текущего аккаунта: $e');
|
||||
if (_accounts.isNotEmpty) {
|
||||
_currentAccount = _accounts.first;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveAccounts() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final accountsJson = jsonEncode(
|
||||
_accounts.map((account) => account.toJson()).toList(),
|
||||
);
|
||||
await prefs.setString(_accountsKey, accountsJson);
|
||||
} catch (e) {
|
||||
print('Ошибка сохранения аккаунтов: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveCurrentAccountId(String accountId) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_currentAccountIdKey, accountId);
|
||||
} catch (e) {
|
||||
print('Ошибка сохранения текущего аккаунта: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Account> addAccount({
|
||||
required String token,
|
||||
String? userId,
|
||||
Profile? profile,
|
||||
}) async {
|
||||
final account = Account(
|
||||
id: const Uuid().v4(),
|
||||
token: token,
|
||||
userId: userId,
|
||||
profile: profile,
|
||||
createdAt: DateTime.now(),
|
||||
lastUsedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final existingIndex = _accounts.indexWhere((acc) => acc.token == token);
|
||||
if (existingIndex != -1) {
|
||||
_accounts[existingIndex] = account.copyWith(
|
||||
id: _accounts[existingIndex].id,
|
||||
);
|
||||
} else {
|
||||
_accounts.add(account);
|
||||
}
|
||||
|
||||
await _saveAccounts();
|
||||
return account;
|
||||
}
|
||||
|
||||
Future<void> switchAccount(String accountId) async {
|
||||
final account = _accounts.firstWhere(
|
||||
(acc) => acc.id == accountId,
|
||||
orElse: () => throw Exception('Аккаунт не найден'),
|
||||
);
|
||||
|
||||
_currentAccount = account;
|
||||
await _saveCurrentAccountId(accountId);
|
||||
|
||||
final index = _accounts.indexWhere((acc) => acc.id == accountId);
|
||||
if (index != -1) {
|
||||
_accounts[index] = _accounts[index].copyWith(lastUsedAt: DateTime.now());
|
||||
await _saveAccounts();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateAccountProfile(String accountId, Profile profile) async {
|
||||
final index = _accounts.indexWhere((acc) => acc.id == accountId);
|
||||
if (index != -1) {
|
||||
_accounts[index] = _accounts[index].copyWith(profile: profile);
|
||||
await _saveAccounts();
|
||||
|
||||
if (_currentAccount?.id == accountId) {
|
||||
_currentAccount = _accounts[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeAccount(String accountId) async {
|
||||
if (_accounts.length <= 1) {
|
||||
throw Exception('Нельзя удалить последний аккаунт');
|
||||
}
|
||||
|
||||
_accounts.removeWhere((acc) => acc.id == accountId);
|
||||
|
||||
if (_currentAccount?.id == accountId) {
|
||||
_currentAccount = _accounts.isNotEmpty ? _accounts.first : null;
|
||||
if (_currentAccount != null) {
|
||||
await _saveCurrentAccountId(_currentAccount!.id);
|
||||
}
|
||||
}
|
||||
|
||||
await _saveAccounts();
|
||||
}
|
||||
|
||||
Future<void> migrateOldAccount() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final oldToken = prefs.getString('authToken');
|
||||
final oldUserId = prefs.getString('userId');
|
||||
|
||||
if (oldToken != null && _accounts.isEmpty) {
|
||||
await addAccount(token: oldToken, userId: oldUserId);
|
||||
print('Старый аккаунт мигрирован в мультиаккаунтинг');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка миграции старого аккаунта: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
@@ -7,42 +5,41 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
|
||||
class CacheService {
|
||||
static final CacheService _instance = CacheService._internal();
|
||||
factory CacheService() => _instance;
|
||||
CacheService._internal();
|
||||
|
||||
|
||||
final Map<String, dynamic> _memoryCache = {};
|
||||
final Map<String, DateTime> _cacheTimestamps = {};
|
||||
|
||||
|
||||
static const Duration _defaultTTL = Duration(hours: 24);
|
||||
static const int _maxMemoryCacheSize = 1000;
|
||||
|
||||
|
||||
SharedPreferences? _prefs;
|
||||
|
||||
|
||||
Directory? _cacheDirectory;
|
||||
|
||||
|
||||
Future<void> initialize() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
_cacheDirectory = await getApplicationCacheDirectory();
|
||||
|
||||
|
||||
await _createCacheDirectories();
|
||||
|
||||
print('CacheService инициализирован');
|
||||
}
|
||||
|
||||
|
||||
Future<void> _createCacheDirectories() async {
|
||||
if (_cacheDirectory == null) return;
|
||||
|
||||
final directories = ['avatars', 'images', 'files', 'chats', 'contacts'];
|
||||
final directories = [
|
||||
'avatars',
|
||||
'images',
|
||||
'files',
|
||||
'chats',
|
||||
'contacts',
|
||||
'audio',
|
||||
];
|
||||
|
||||
for (final dir in directories) {
|
||||
final directory = Directory('${_cacheDirectory!.path}/$dir');
|
||||
@@ -52,21 +49,17 @@ class CacheService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<T?> get<T>(String key, {Duration? ttl}) async {
|
||||
|
||||
if (_memoryCache.containsKey(key)) {
|
||||
final timestamp = _cacheTimestamps[key];
|
||||
if (timestamp != null && !_isExpired(timestamp, ttl ?? _defaultTTL)) {
|
||||
return _memoryCache[key] as T?;
|
||||
} else {
|
||||
|
||||
_memoryCache.remove(key);
|
||||
_cacheTimestamps.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (_prefs != null) {
|
||||
try {
|
||||
final cacheKey = 'cache_$key';
|
||||
@@ -80,7 +73,6 @@ class CacheService {
|
||||
final value = data['value'];
|
||||
|
||||
if (!_isExpired(timestamp, ttl ?? _defaultTTL)) {
|
||||
|
||||
_memoryCache[key] = value;
|
||||
_cacheTimestamps[key] = timestamp;
|
||||
return value as T?;
|
||||
@@ -94,20 +86,16 @@ class CacheService {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Future<void> set<T>(String key, T value, {Duration? ttl}) async {
|
||||
final timestamp = DateTime.now();
|
||||
|
||||
|
||||
_memoryCache[key] = value;
|
||||
_cacheTimestamps[key] = timestamp;
|
||||
|
||||
|
||||
if (_memoryCache.length > _maxMemoryCacheSize) {
|
||||
await _evictOldestMemoryCache();
|
||||
}
|
||||
|
||||
|
||||
if (_prefs != null) {
|
||||
try {
|
||||
final cacheKey = 'cache_$key';
|
||||
@@ -124,7 +112,6 @@ class CacheService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> remove(String key) async {
|
||||
_memoryCache.remove(key);
|
||||
_cacheTimestamps.remove(key);
|
||||
@@ -139,14 +126,12 @@ class CacheService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> clear() async {
|
||||
_memoryCache.clear();
|
||||
_cacheTimestamps.clear();
|
||||
|
||||
if (_prefs != null) {
|
||||
try {
|
||||
|
||||
final keys = _prefs!.getKeys().where((key) => key.startsWith('cache_'));
|
||||
for (final key in keys) {
|
||||
await _prefs!.remove(key);
|
||||
@@ -156,7 +141,6 @@ class CacheService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (_cacheDirectory != null) {
|
||||
try {
|
||||
for (final dir in ['avatars', 'images', 'files', 'chats', 'contacts']) {
|
||||
@@ -172,16 +156,13 @@ class CacheService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool _isExpired(DateTime timestamp, Duration ttl) {
|
||||
return DateTime.now().difference(timestamp) > ttl;
|
||||
}
|
||||
|
||||
|
||||
Future<void> _evictOldestMemoryCache() async {
|
||||
if (_memoryCache.isEmpty) return;
|
||||
|
||||
|
||||
final sortedEntries = _cacheTimestamps.entries.toList()
|
||||
..sort((a, b) => a.value.compareTo(b.value));
|
||||
|
||||
@@ -193,11 +174,9 @@ class CacheService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<Map<String, int>> getCacheSize() async {
|
||||
final memorySize = _memoryCache.length;
|
||||
|
||||
|
||||
int filesSize = 0;
|
||||
if (_cacheDirectory != null) {
|
||||
try {
|
||||
@@ -219,25 +198,20 @@ class CacheService {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Future<String?> cacheFile(String url, {String? customKey}) async {
|
||||
if (_cacheDirectory == null) return null;
|
||||
|
||||
try {
|
||||
|
||||
final fileName = _generateFileName(url, customKey);
|
||||
final filePath = '${_cacheDirectory!.path}/images/$fileName';
|
||||
|
||||
|
||||
final existingFile = File(filePath);
|
||||
if (await existingFile.exists()) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
|
||||
final response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode == 200) {
|
||||
|
||||
await existingFile.writeAsBytes(response.bodyBytes);
|
||||
return filePath;
|
||||
}
|
||||
@@ -248,7 +222,6 @@ class CacheService {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Future<File?> getCachedFile(String url, {String? customKey}) async {
|
||||
if (_cacheDirectory == null) return null;
|
||||
|
||||
@@ -267,33 +240,42 @@ class CacheService {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
String _generateFileName(String url, String? customKey) {
|
||||
final key = customKey ?? url;
|
||||
final hash = key.hashCode.abs().toString().substring(0, 16);
|
||||
final hashString = key.hashCode.abs().toString();
|
||||
final hash = hashString.length >= 16
|
||||
? hashString.substring(0, 16)
|
||||
: hashString.padRight(16, '0');
|
||||
final extension = _getFileExtension(url);
|
||||
return '$hash$extension';
|
||||
}
|
||||
|
||||
|
||||
String _getFileExtension(String url) {
|
||||
try {
|
||||
final uri = Uri.parse(url);
|
||||
final path = uri.path;
|
||||
final extension = path.substring(path.lastIndexOf('.'));
|
||||
return extension.isNotEmpty && extension.length < 10 ? extension : '.jpg';
|
||||
if (extension.isNotEmpty && extension.length < 10) {
|
||||
return extension;
|
||||
}
|
||||
if (url.contains('audio') ||
|
||||
url.contains('voice') ||
|
||||
url.contains('.mp3') ||
|
||||
url.contains('.ogg') ||
|
||||
url.contains('.m4a')) {
|
||||
return '.mp3';
|
||||
}
|
||||
return '.jpg';
|
||||
} catch (e) {
|
||||
return '.jpg';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<bool> hasCachedFile(String url, {String? customKey}) async {
|
||||
final file = await getCachedFile(url, customKey: customKey);
|
||||
return file != null;
|
||||
}
|
||||
|
||||
|
||||
Future<Map<String, dynamic>> getDetailedCacheStats() async {
|
||||
final memorySize = _memoryCache.length;
|
||||
final cacheSize = await getCacheSize();
|
||||
@@ -308,12 +290,7 @@ class CacheService {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Future<void> removeCachedFile(String url, {String? customKey}) async {
|
||||
|
||||
|
||||
}
|
||||
|
||||
Future<void> removeCachedFile(String url, {String? customKey}) async {}
|
||||
|
||||
Future<Map<String, dynamic>> getCacheStats() async {
|
||||
final sizes = await getCacheSize();
|
||||
@@ -329,4 +306,110 @@ class CacheService {
|
||||
'maxMemorySize': _maxMemoryCacheSize,
|
||||
};
|
||||
}
|
||||
|
||||
Future<String?> cacheAudioFile(String url, {String? customKey}) async {
|
||||
if (_cacheDirectory == null) {
|
||||
print('CacheService: _cacheDirectory is null, initializing...');
|
||||
await initialize();
|
||||
if (_cacheDirectory == null) {
|
||||
print('CacheService: Failed to initialize cache directory');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final fileName = _generateFileName(url, customKey);
|
||||
final filePath = '${_cacheDirectory!.path}/audio/$fileName';
|
||||
|
||||
final existingFile = File(filePath);
|
||||
if (await existingFile.exists()) {
|
||||
print('CacheService: Audio file already cached: $filePath');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
print('CacheService: Downloading audio from: $url');
|
||||
print('CacheService: Target file path: $filePath');
|
||||
|
||||
final response = await http
|
||||
.get(
|
||||
Uri.parse(url),
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
},
|
||||
)
|
||||
.timeout(
|
||||
const Duration(seconds: 30),
|
||||
onTimeout: () {
|
||||
print('CacheService: Request timeout');
|
||||
throw TimeoutException('Request timeout');
|
||||
},
|
||||
);
|
||||
|
||||
print(
|
||||
'CacheService: Response status: ${response.statusCode}, content-length: ${response.contentLength}',
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
if (response.bodyBytes.isEmpty) {
|
||||
print('CacheService: Response body is empty');
|
||||
return null;
|
||||
}
|
||||
|
||||
final audioDir = Directory('${_cacheDirectory!.path}/audio');
|
||||
if (!await audioDir.exists()) {
|
||||
await audioDir.create(recursive: true);
|
||||
}
|
||||
|
||||
await existingFile.writeAsBytes(response.bodyBytes);
|
||||
final fileSize = await existingFile.length();
|
||||
print(
|
||||
'CacheService: Audio cached successfully: $filePath (size: $fileSize bytes)',
|
||||
);
|
||||
return filePath;
|
||||
} else {
|
||||
print(
|
||||
'CacheService: Failed to download audio, status code: ${response.statusCode}',
|
||||
);
|
||||
print(
|
||||
'CacheService: Response body: ${response.body.substring(0, response.body.length > 200 ? 200 : response.body.length)}',
|
||||
);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
print('Ошибка кэширования аудио файла $url: $e');
|
||||
print('Stack trace: $stackTrace');
|
||||
if (e is TimeoutException) {
|
||||
print('CacheService: Request timed out');
|
||||
} else if (e is SocketException) {
|
||||
print('CacheService: Network error - ${e.message}');
|
||||
} else if (e is HttpException) {
|
||||
print('CacheService: HTTP error - ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<File?> getCachedAudioFile(String url, {String? customKey}) async {
|
||||
if (_cacheDirectory == null) return null;
|
||||
|
||||
try {
|
||||
final fileName = _generateFileName(url, customKey);
|
||||
final filePath = '${_cacheDirectory!.path}/audio/$fileName';
|
||||
|
||||
final file = File(filePath);
|
||||
if (await file.exists()) {
|
||||
return file;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка получения кэшированного аудио файла: $e');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<bool> hasCachedAudioFile(String url, {String? customKey}) async {
|
||||
final file = await getCachedAudioFile(url, customKey: customKey);
|
||||
return file != null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user