From 6c2ad9014624ac0f82b3c4ded76e700335564cd7 Mon Sep 17 00:00:00 2001 From: cheykrym Date: Tue, 9 Dec 2025 23:54:59 +0300 Subject: [PATCH] add clear cache --- yobble/Resources/Localizable.xcstrings | 45 ++++++- yobble/Services/KeychainService.swift | 37 ++++++ yobble/Views/Tab/Settings/SettingsView.swift | 121 ++++++++++++++++++- 3 files changed, 199 insertions(+), 4 deletions(-) diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 258f17f..3b1ea2c 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -432,6 +432,12 @@ }, "Вы можете включить защиту снова в любой момент." : { "comment" : "Сообщение после отключения 2FA" + }, + "Вы уверены, что хотите очистить весь кэш аватаров? Это действие необратимо." : { + + }, + "Вы уверены, что хотите очистить кэш для всех, кроме текущего пользователя?" : { + }, "Выберите оценку — это поможет нам понять настроение." : { "comment" : "feedback: rating hint", @@ -483,6 +489,9 @@ } } } + }, + "Данные и кэш" : { + }, "Двухфакторная аутентификация" : { "comment" : "Заголовок экрана 2FA\nПереход к настройкам двухфакторной аутентификации", @@ -594,9 +603,6 @@ } } } - }, - "Заглушка: Хранилище данных" : { - }, "Загружаем ранние сообщения…" : { @@ -889,6 +895,12 @@ } } } + }, + "Кэш по пользователям" : { + + }, + "Кэш пуст" : { + }, "Лента" : { "localizations" : { @@ -958,6 +970,9 @@ } } } + }, + "Массовая отчистка" : { + }, "Мессенджер-режим сейчас проработан примерно на 50%." : { @@ -1521,6 +1536,12 @@ } } } + }, + "Общая информация" : { + + }, + "Общий размер" : { + }, "Ограничить таймер автоудаления (максимум)" : { "localizations" : { @@ -1613,6 +1634,18 @@ } } } + }, + "Очистить" : { + + }, + "Очистить весь кэш" : { + + }, + "Очистить всё" : { + + }, + "Очистить кэш (кроме текущего)" : { + }, "Ошибка" : { "comment" : "Common error title\nContacts load error title\nProfile update error title\nЗаголовок сообщения об ошибке", @@ -2204,6 +2237,9 @@ }, "Разблокировать" : { "comment" : "Unblock confirmation action" + }, + "Размер кэша аватаров на диске." : { + }, "Разрешить пересылку сообщений" : { "localizations" : { @@ -2578,6 +2614,9 @@ }, "Текущая сессия останется активной" : { "comment" : "Подсказка под кнопкой завершения других сессий" + }, + "Текущий" : { + }, "Тема: %@" : { "comment" : "feedback: success category", diff --git a/yobble/Services/KeychainService.swift b/yobble/Services/KeychainService.swift index aba246a..0e9eb14 100644 --- a/yobble/Services/KeychainService.swift +++ b/yobble/Services/KeychainService.swift @@ -164,6 +164,43 @@ class AvatarCacheService { guard let directory = baseCacheDirectory else { return } try? fileManager.removeItem(at: directory) } + + func getAllCachedUserIds() -> [String] { + guard let baseDir = baseCacheDirectory else { return [] } + do { + let directoryContents = try fileManager.contentsOfDirectory(at: baseDir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) + return directoryContents.map { $0.lastPathComponent } + } catch { + // This can happen if the directory doesn't exist yet, which is not an error. + return [] + } + } + + func sizeOfCache(forUserId userId: String) -> Int64 { + guard let directory = cacheDirectory(for: userId) else { return 0 } + return directorySize(url: directory) + } + + func sizeOfAllCache() -> Int64 { + guard let directory = baseCacheDirectory else { return 0 } + return directorySize(url: directory) + } + + private func directorySize(url: URL) -> Int64 { + let contents: [URL] + do { + contents = try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: [.fileSizeKey], options: .skipsHiddenFiles) + } catch { + return 0 + } + + var totalSize: Int64 = 0 + for url in contents { + let fileSize = (try? url.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? 0 + totalSize += Int64(fileSize) + } + return totalSize + } } class ImageLoader: ObservableObject { diff --git a/yobble/Views/Tab/Settings/SettingsView.swift b/yobble/Views/Tab/Settings/SettingsView.swift index f78df43..9f2649e 100644 --- a/yobble/Views/Tab/Settings/SettingsView.swift +++ b/yobble/Views/Tab/Settings/SettingsView.swift @@ -67,7 +67,7 @@ struct SettingsView: View { Label("Темы", systemImage: "moon.fill") } - NavigationLink(destination: Text("Заглушка: Хранилище данных")) { + NavigationLink(destination: DataSettingsView(currentUserId: viewModel.userId)) { Label("Данные", systemImage: "externaldrive") } @@ -172,3 +172,122 @@ struct SettingsView: View { } } + +struct DataSettingsView: View { + let currentUserId: String + private let cacheService = AvatarCacheService.shared + + @State private var cachedUsers: [CachedUserInfo] = [] + @State private var totalCacheSize: Int64 = 0 + @State private var showClearAllConfirmation = false + @State private var showClearOthersConfirmation = false + + var body: some View { + Form { + Section(header: Text("Общая информация"), footer: Text("Размер кэша аватаров на диске.")) { + HStack { + Text("Общий размер") + Spacer() + Text(format(bytes: totalCacheSize)) + .foregroundColor(.secondary) + } + } + + Section(header: Text("Массовая отчистка")) { + Button("Очистить кэш (кроме текущего)", role: .destructive) { + showClearOthersConfirmation = true + } + .confirmationDialog( + "Вы уверены, что хотите очистить кэш для всех, кроме текущего пользователя?", + isPresented: $showClearOthersConfirmation, + titleVisibility: .visible + ) { + Button("Очистить", role: .destructive, action: clearOtherUsersCache) + } + + Button("Очистить весь кэш", role: .destructive) { + showClearAllConfirmation = true + } + .confirmationDialog( + "Вы уверены, что хотите очистить весь кэш аватаров? Это действие необратимо.", + isPresented: $showClearAllConfirmation, + titleVisibility: .visible + ) { + Button("Очистить всё", role: .destructive, action: clearAllCache) + } + } + + Section(header: Text("Кэш по пользователям")) { + if cachedUsers.isEmpty { + Text("Кэш пуст") + .foregroundColor(.secondary) + } else { + ForEach(cachedUsers) { user in + HStack { + VStack(alignment: .leading) { + Text(user.id) + .font(.system(.body, design: .monospaced)) + .lineLimit(1) + .truncationMode(.middle) + if user.id == currentUserId { + Text("Текущий") + .font(.caption) + .foregroundColor(.accentColor) + } + } + Spacer() + Text(format(bytes: user.size)) + .foregroundColor(.secondary) + Button("Очистить") { + clearCache(for: user.id) + } + .buttonStyle(.borderless) + } + } + } + } + } + .navigationTitle("Данные и кэш") + .onAppear(perform: refreshCacheStats) + } + + private func refreshCacheStats() { + let userIds = cacheService.getAllCachedUserIds() + self.cachedUsers = userIds.map { id in + let size = cacheService.sizeOfCache(forUserId: id) + return CachedUserInfo(id: id, size: size) + }.sorted { $0.size > $1.size } + + self.totalCacheSize = cacheService.sizeOfAllCache() + } + + private func clearCache(for userId: String) { + cacheService.clearCache(forUserId: userId) + refreshCacheStats() + } + + private func clearAllCache() { + cacheService.clearAllCache() + refreshCacheStats() + } + + private func clearOtherUsersCache() { + let otherUsers = cachedUsers.filter { $0.id != currentUserId } + for user in otherUsers { + cacheService.clearCache(forUserId: user.id) + } + refreshCacheStats() + } + + private func format(bytes: Int64) -> String { + let formatter = ByteCountFormatter() + formatter.allowedUnits = [.useAll] + formatter.countStyle = .file + return formatter.string(fromByteCount: bytes) + } +} + +struct CachedUserInfo: Identifiable { + let id: String + let size: Int64 +}