diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidFileLocationsProvider.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidFileLocationsProvider.kt index 2625c59d..639d80b7 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidFileLocationsProvider.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidFileLocationsProvider.kt @@ -20,4 +20,37 @@ class AndroidFileLocationsProvider( override fun setExecutableIfNeeded(path: String) { // No-op on Android } + + override fun getCacheSizeBytes(): Long { + val dir = File(appDownloadsDir()) + return calculateDirSize(dir) + } + + override fun clearCacheFiles(): Boolean { + val dir = File(appDownloadsDir()) + return deleteDirectoryContents(dir) + } + + private fun calculateDirSize(dir: File): Long { + if (!dir.exists()) return 0L + var size = 0L + dir.listFiles()?.forEach { file -> + size += if (file.isDirectory) calculateDirSize(file) else file.length() + } + return size + } + + private fun deleteDirectoryContents(dir: File): Boolean { + if (!dir.exists()) return true + var allDeleted = true + dir.listFiles()?.forEach { file -> + if (file.isDirectory) { + if (!deleteDirectoryContents(file)) allDeleted = false + if (!file.delete()) allDeleted = false + } else { + if (!file.delete()) allDeleted = false + } + } + return allDeleted + } } \ No newline at end of file diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/FileLocationsProvider.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/FileLocationsProvider.kt index 3c5b27da..44caac6f 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/FileLocationsProvider.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/FileLocationsProvider.kt @@ -4,4 +4,6 @@ interface FileLocationsProvider { fun appDownloadsDir(): String fun userDownloadsDir(): String fun setExecutableIfNeeded(path: String) + fun getCacheSizeBytes(): Long + fun clearCacheFiles(): Boolean } diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopFileLocationsProvider.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopFileLocationsProvider.kt index 92a3ecd6..167a1960 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopFileLocationsProvider.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopFileLocationsProvider.kt @@ -95,6 +95,43 @@ class DesktopFileLocationsProvider( return downloadsDir.absolutePath } + override fun getCacheSizeBytes(): Long { + val appDir = File(appDownloadsDir()) + val userDir = File(userDownloadsDir()) + return calculateDirSize(appDir) + calculateDirSize(userDir) + } + + override fun clearCacheFiles(): Boolean { + val appDir = File(appDownloadsDir()) + val userDir = File(userDownloadsDir()) + val appCleared = deleteDirectoryContents(appDir) + val userCleared = deleteDirectoryContents(userDir) + return appCleared && userCleared + } + + private fun calculateDirSize(dir: File): Long { + if (!dir.exists()) return 0L + var size = 0L + dir.listFiles()?.forEach { file -> + size += if (file.isDirectory) calculateDirSize(file) else file.length() + } + return size + } + + private fun deleteDirectoryContents(dir: File): Boolean { + if (!dir.exists()) return true + var allDeleted = true + dir.listFiles()?.forEach { file -> + if (file.isDirectory) { + if (!deleteDirectoryContents(file)) allDeleted = false + if (!file.delete()) allDeleted = false + } else { + if (!file.delete()) allDeleted = false + } + } + return allDeleted + } + private fun getXdgDownloadsDir(): String? { return try { val userDirsFile = File( diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 33255e9b..bac9882a 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -126,6 +126,7 @@ সফলভাবে লগআউট হয়েছে, রিডাইরেক্ট করা হচ্ছে... + ক্যাশ সফলভাবে পরিষ্কার করা হয়েছে সতর্কতা! @@ -279,9 +280,9 @@ ইনস্টলযোগ্য রিলিজ থাকা রিপোজিটরি GitHub-এ স্টার করুন শেষ সিঙ্ক এইমাত্র - %d মিনিট আগে - %d ঘণ্টা আগে - %d দিন আগে + %1$d মিনিট আগে + %1$d ঘণ্টা আগে + %1$d দিন আগে বন্ধ করুন স্টার করা রিপোজিটরি সিঙ্ক করতে ব্যর্থ হয়েছে @@ -453,4 +454,8 @@ অ্যাপে খুলুন ক্লিপবোর্ডে কোনো GitHub লিংক পাওয়া যায়নি + স্টোরেজ + ক্যাশে পরিষ্কার করুন + বর্তমান আকার: + পরিষ্কার করুন diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index da64435b..6049b2df 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -104,6 +104,7 @@ Cerrar sesión Sesión cerrada correctamente, redirigiendo… + Caché borrada con éxito ¡Advertencia! ¿Estás seguro de que deseas cerrar sesión? @@ -418,4 +419,8 @@ Abrir en la app No se encontró enlace de GitHub en el portapapeles + Almacenamiento + Borrar caché + Tamaño actual: + Borrar \ No newline at end of file diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index 25201a19..4fce5704 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -104,6 +104,7 @@ Se déconnecter Déconnexion réussie, redirection… + Cache vidé avec succès Attention ! Voulez-vous vraiment vous déconnecter ? @@ -418,4 +419,8 @@ Ouvrir dans l\'app Aucun lien GitHub trouvé dans le presse-papiers + Stockage + Vider le cache + Taille actuelle : + Vider \ No newline at end of file diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index 157aa9f1..3432c922 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -126,6 +126,7 @@ सफलतापूर्वक लॉग आउट हो गए, रीडायरेक्ट किया जा रहा है... + कैश सफलतापूर्वक साफ़ किया गया चेतावनी! @@ -277,9 +278,9 @@ उन्हें यहां देखने के लिए इंस्टॉलेबल रिलीज़ वाले GitHub पर रिपॉजिटरी को स्टार करें। अंतिम सिंक अभी-अभी - %d मिनट पहले - %d घंटे पहले - %d दिन पहले + %1$d मिनट पहले + %1$d घंटे पहले + %1$d दिन पहले हटाएं तारांकित रिपॉजिटरी को सिंक करने में विफल रहा @@ -453,4 +454,8 @@ ऐप में खोलें क्लिपबोर्ड में कोई GitHub लिंक नहीं मिला + संग्रहण + कैश साफ़ करें + वर्तमान आकार: + साफ़ करें diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 42edc300..d1fafd6b 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -126,6 +126,7 @@ Uscito con successo, reindirizzamento… + Cache cancellata con successo Attenzione! @@ -454,4 +455,8 @@ Apri nell\'app Nessun link GitHub trovato negli appunti + Archiviazione + Pulisci cache + Dimensione attuale: + Pulisci \ No newline at end of file diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 00c71a92..81edb858 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -104,6 +104,7 @@ ログアウト ログアウトしました。リダイレクト中… + キャッシュを正常にクリアしました 警告! ログアウトしてもよろしいですか? @@ -417,4 +418,8 @@ アプリで開く クリップボードにGitHubリンクが見つかりません + ストレージ + キャッシュをクリア + 現在のサイズ: + クリア \ No newline at end of file diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index d93812bb..3a3353a6 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -124,6 +124,7 @@ 성공적으로 로그아웃되었습니다. 이동 중... + 캐시가 성공적으로 삭제되었습니다 경고! @@ -277,9 +278,9 @@ 설치 가능한 릴리스가 있는 저장소에 별표를 추가하세요 마지막 동기화 방금 - %d분 전 - %d시간 전 - %d일 전 + %1$d분 전 + %1$d시간 전 + %1$d일 전 닫기 별표 저장소 동기화에 실패했습니다 @@ -450,4 +451,8 @@ 앱에서 열기 클립보드에서 GitHub 링크를 찾을 수 없습니다 + 저장 공간 + 캐시 지우기 + 현재 크기: + 지우기 \ No newline at end of file diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 48fa3948..6ac85b77 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -105,6 +105,7 @@ Wyloguj się Wylogowano pomyślnie, przekierowywanie... + Pamięć podręczna wyczyszczona pomyślnie Ostrzeżenie! Czy na pewno chcesz się wylogować? @@ -243,9 +244,9 @@ Oznacz repozytoria z instalowalnymi wydaniami na GitHubie Ostatnia synchronizacja Przed chwilą - %d min temu - %d h temu - %d d temu + %1$d min temu + %1$d h temu + %1$d d temu Zamknij Nie udało się zsynchronizować oznaczonych gwiazdką repozytoriów @@ -416,4 +417,8 @@ Otwórz w aplikacji Nie znaleziono linku GitHub w schowku + Przechowywanie + Wyczyść pamięć podręczną + Aktualny rozmiar: + Wyczyść \ No newline at end of file diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index d3879e39..5a405544 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -104,6 +104,7 @@ Выйти Вы успешно вышли, перенаправление... + Кэш успешно очищен Внимание! Вы уверены, что хотите выйти? @@ -244,9 +245,9 @@ Отмечайте репозитории с установочными релизами на GitHub Последняя синхронизация Только что - %d мин назад - %d ч назад - %d д назад + %1$d мин назад + %1$d ч назад + %1$d д назад Закрыть Не удалось синхронизировать избранные репозитории @@ -418,4 +419,8 @@ Открыть в приложении Ссылка GitHub не найдена в буфере обмена + Хранение + Очистить кэш + Текущий размер: + Очистить \ No newline at end of file diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 99ad849c..e857b4f0 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -125,6 +125,7 @@ Başarılı şekilde çıkış yapıldı, yönlendiriliyor... + Önbellek başarıyla temizlendi Uyarı! @@ -278,9 +279,9 @@ Yüklenebilir sürümlü repoları görmek için GitHub'da yıldızlayın Son eşitleme Şimdi - %d dakika önce - %d saat önce - %d gün önce + %1$d dakika önce + %1$d saat önce + %1$d gün önce Kapat Yıldızlı repoları eşitlerken hata @@ -450,4 +451,8 @@ Uygulamada aç Panoda GitHub bağlantısı bulunamadı + Depolama + Önbelleği Temizle + Geçerli boyut: + Temizle diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index bb136c26..e567ed5b 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -106,6 +106,7 @@ 退出登录 已成功退出,正在跳转… + 缓存已成功清除 警告! 确定要退出登录吗? @@ -227,9 +228,9 @@ 在 GitHub 上收藏有可安装版本的仓库以在此查看 上次同步 刚刚 - %d 分钟前 - %d 小时前 - %d 天前 + %1$d 分钟前 + %1$d 小时前 + %1$d 天前 关闭 同步收藏的仓库失败 @@ -418,4 +419,8 @@ 在应用中打开 剪贴板中未找到 GitHub 链接 + 存储 + 清除缓存 + 当前大小: + 清除 \ No newline at end of file diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 5c1a64fe..49cbc82d 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -147,6 +147,7 @@ Logged out successfully, redirecting... + Cache cleared successfully Warning! @@ -313,9 +314,9 @@ Star repositories on GitHub with installable releases to see them here Last synced Just now - %d min ago - %d h ago - %d d ago + %1$d min ago + %1$d h ago + %1$d d ago Dismiss Failed to sync starred repos @@ -453,4 +454,9 @@ Detected Links Open in app No GitHub link found in clipboard + + Storage + Clear Cache + Current size: + Clear \ No newline at end of file diff --git a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesRoot.kt b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesRoot.kt index 7752e9de..9ef108cf 100644 --- a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesRoot.kt +++ b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesRoot.kt @@ -109,7 +109,7 @@ fun FavouritesScreen( onDevProfileClick = { onAction(FavouritesAction.OnDeveloperProfileClick(repo.repoOwner)) }, - modifier = Modifier.Companion.animateItem() + modifier = Modifier.animateItem() ) } } diff --git a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/components/FavouriteRepositoryItem.kt b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/components/FavouriteRepositoryItem.kt index 007bfd68..9b35e61c 100644 --- a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/components/FavouriteRepositoryItem.kt +++ b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/components/FavouriteRepositoryItem.kt @@ -1,7 +1,7 @@ package zed.rainxch.favourites.presentation.components -import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,19 +12,25 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CalendarToday +import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults +import androidx.compose.material.icons.filled.NewReleases +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.toShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,9 +41,11 @@ import androidx.compose.ui.unit.dp import com.skydoves.landscapist.coil3.CoilImage import com.skydoves.landscapist.components.rememberImageComponent import com.skydoves.landscapist.crossfade.CrossfadePlugin -import zed.rainxch.githubstore.core.presentation.res.* import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.ExpressiveCard import zed.rainxch.favourites.presentation.model.FavouriteRepository +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.remove_from_favourites @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -46,15 +54,11 @@ fun FavouriteRepositoryItem( onToggleFavouriteClick: () -> Unit, onItemClick: () -> Unit, onDevProfileClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - Card( - modifier = modifier.fillMaxWidth(), + ExpressiveCard( + modifier = modifier, onClick = onItemClick, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer - ), - shape = RoundedCornerShape(24.dp) ) { Column( modifier = Modifier @@ -63,9 +67,9 @@ fun FavouriteRepositoryItem( ) { Row( modifier = Modifier - .clickable(onClick = { - onDevProfileClick() - }), + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .clickable(onClick = onDevProfileClick), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { @@ -92,12 +96,12 @@ fun FavouriteRepositoryItem( style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.outline, maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) ) } - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier.fillMaxWidth(), @@ -113,34 +117,28 @@ fun FavouriteRepositoryItem( style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, - softWrap = false, overflow = TextOverflow.Ellipsis ) favouriteRepository.repoDescription?.let { - Spacer(Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(4.dp)) Text( text = it, fontWeight = FontWeight.Medium, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 2, - softWrap = true, overflow = TextOverflow.Ellipsis ) } } IconButton( - onClick = { - onToggleFavouriteClick() - }, - colors = IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), - shapes = IconButtonDefaults.shapes() + onClick = onToggleFavouriteClick, + colors = IconButtonDefaults.filledTonalIconButtonColors(), + modifier = Modifier.align(Alignment.CenterVertically), + shape = MaterialShapes.Cookie6Sided.toShape() ) { Icon( imageVector = Icons.Default.Favorite, @@ -149,50 +147,80 @@ fun FavouriteRepositoryItem( } } - favouriteRepository.primaryLanguage?.let { - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(12.dp)) - Text( - text = it, - fontWeight = FontWeight.Medium, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .background( - color = MaterialTheme.colorScheme.primaryContainer, - shape = RoundedCornerShape(12.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + favouriteRepository.primaryLanguage?.let { language -> + AssistChip( + onClick = { /* No action */ }, + label = { + Text( + text = language, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Code, + contentDescription = null, + modifier = Modifier.size(AssistChipDefaults.IconSize) + ) + }, + colors = AssistChipDefaults.assistChipColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + labelColor = MaterialTheme.colorScheme.onPrimaryContainer, + leadingIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer ) - .padding(8.dp) - ) - } - favouriteRepository.latestRelease?.let { - Spacer(modifier = Modifier.height(6.dp)) + ) + } - Text( - text = it, - fontWeight = FontWeight.Medium, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis + favouriteRepository.latestRelease?.let { release -> + AssistChip( + onClick = { /* No action */ }, + label = { + Text( + text = release, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + leadingIcon = { + Icon( + imageVector = Icons.Default.NewReleases, + contentDescription = null, + modifier = Modifier.size(AssistChipDefaults.IconSize) + ) + } + ) + } + + AssistChip( + onClick = { /* No action */ }, + label = { + Text( + text = favouriteRepository.addedAtFormatter, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + leadingIcon = { + Icon( + imageVector = Icons.Default.CalendarToday, + contentDescription = null, + modifier = Modifier.size(AssistChipDefaults.IconSize) + ) + } ) } - - Spacer(modifier = Modifier.height(6.dp)) - - Text( - text = favouriteRepository.addedAtFormatter, - fontWeight = FontWeight.Medium, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis - ) } } } \ No newline at end of file diff --git a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt b/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt index 37648651..18bec024 100644 --- a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt +++ b/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt @@ -11,7 +11,8 @@ val settingsModule = module { tokenStore = get(), httpClient = get(), cacheManager = get(), - logger = get() + logger = get(), + fileLocationsProvider = get() ) } } \ No newline at end of file diff --git a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt b/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt index 653bed76..39979bf6 100644 --- a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt +++ b/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt @@ -5,6 +5,7 @@ import io.ktor.client.request.get import io.ktor.client.request.header import io.ktor.http.HttpHeaders import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn @@ -13,6 +14,7 @@ import zed.rainxch.core.data.cache.CacheManager.CacheTtl.USER_PROFILE import zed.rainxch.core.data.data_source.TokenStore import zed.rainxch.core.data.dto.UserProfileNetwork import zed.rainxch.core.data.network.executeRequest +import zed.rainxch.core.data.services.FileLocationsProvider import zed.rainxch.core.domain.logging.GitHubStoreLogger import zed.rainxch.core.domain.repository.AuthenticationState import zed.rainxch.feature.profile.data.BuildKonfig @@ -25,7 +27,8 @@ class ProfileRepositoryImpl( private val tokenStore: TokenStore, private val httpClient: HttpClient, private val cacheManager: CacheManager, - private val logger: GitHubStoreLogger + private val logger: GitHubStoreLogger, + private val fileLocationsProvider: FileLocationsProvider ) : ProfileRepository { companion object { @@ -84,4 +87,15 @@ class ProfileRepositoryImpl( tokenStore.clear() cacheManager.clearAll() } + + override fun observeCacheSize(): Flow = flow { + val sizeBytes = fileLocationsProvider.getCacheSizeBytes() + emit(sizeBytes) + }.flowOn(Dispatchers.IO) + + override suspend fun clearCache() { + fileLocationsProvider.clearCacheFiles() + cacheManager.clearAll() + logger.debug("Cache cleared successfully") + } } diff --git a/feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile/domain/repository/ProfileRepository.kt b/feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile/domain/repository/ProfileRepository.kt index 369fa931..70365aaa 100644 --- a/feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile/domain/repository/ProfileRepository.kt +++ b/feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile/domain/repository/ProfileRepository.kt @@ -8,4 +8,6 @@ interface ProfileRepository { fun getUser(): Flow fun getVersionName(): String suspend fun logout() + fun observeCacheSize(): Flow + suspend fun clearCache() } \ No newline at end of file diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt index 921cef90..5ce3cd85 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt @@ -2,6 +2,7 @@ package zed.rainxch.profile.presentation import zed.rainxch.core.domain.model.AppTheme import zed.rainxch.core.domain.model.FontTheme +import zed.rainxch.profile.presentation.model.ProxyType sealed interface ProfileAction { data object OnNavigateBackClick : ProfileAction @@ -15,6 +16,7 @@ sealed interface ProfileAction { data object OnLogoutDismiss : ProfileAction data object OnHelpClick : ProfileAction data object OnLoginClick : ProfileAction + data object OnClearCacheClick : ProfileAction data class OnFontThemeSelected(val fontTheme: FontTheme) : ProfileAction data class OnProxyTypeSelected(val type: ProxyType) : ProfileAction data class OnProxyHostChanged(val host: String) : ProfileAction diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileEvent.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileEvent.kt index 9bb5f0ae..45438522 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileEvent.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileEvent.kt @@ -5,4 +5,6 @@ sealed interface ProfileEvent { data class OnLogoutError(val message: String) : ProfileEvent data object OnProxySaved : ProfileEvent data class OnProxySaveError(val message: String) : ProfileEvent + data object OnCacheCleared : ProfileEvent + data class OnCacheClearError(val message: String) : ProfileEvent } \ No newline at end of file diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt index 33e72f53..56928c01 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt @@ -36,6 +36,7 @@ import zed.rainxch.profile.presentation.components.LogoutDialog import zed.rainxch.profile.presentation.components.sections.about import zed.rainxch.profile.presentation.components.sections.logout import zed.rainxch.profile.presentation.components.sections.networkSection +import zed.rainxch.profile.presentation.components.sections.othersSection import zed.rainxch.profile.presentation.components.sections.profile import zed.rainxch.profile.presentation.components.sections.settings @@ -79,6 +80,18 @@ fun ProfileRoot( snackbarState.showSnackbar(event.message) } } + + ProfileEvent.OnCacheCleared -> { + coroutineScope.launch { + snackbarState.showSnackbar(getString(Res.string.cache_cleared)) + } + } + + is ProfileEvent.OnCacheClearError -> { + coroutineScope.launch { + snackbarState.showSnackbar(event.message) + } + } } } @@ -160,7 +173,7 @@ fun ProfileScreen( ) item { - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(32.dp)) } settings( @@ -169,7 +182,7 @@ fun ProfileScreen( ) item { - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(32.dp)) } networkSection( @@ -178,7 +191,16 @@ fun ProfileScreen( ) item { - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(32.dp)) + } + + othersSection( + state = state, + onAction = onAction + ) + + item { + Spacer(Modifier.height(32.dp)) } about( @@ -187,7 +209,7 @@ fun ProfileScreen( ) item { - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(32.dp)) } if (state.isUserLoggedIn) { diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt index c78f4b7c..66d24075 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt @@ -4,6 +4,7 @@ import zed.rainxch.core.domain.model.AppTheme import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.core.domain.model.ProxyConfig import zed.rainxch.profile.domain.model.UserProfile +import zed.rainxch.profile.presentation.model.ProxyType data class ProfileState( val userProfile: UserProfile? = null, @@ -21,17 +22,5 @@ data class ProfileState( val proxyPassword: String = "", val isProxyPasswordVisible: Boolean = false, val autoDetectClipboardLinks: Boolean = true, -) - -enum class ProxyType { - NONE, SYSTEM, HTTP, SOCKS; - - companion object { - fun fromConfig(config: ProxyConfig): ProxyType = when (config) { - is ProxyConfig.None -> NONE - is ProxyConfig.System -> SYSTEM - is ProxyConfig.Http -> HTTP - is ProxyConfig.Socks -> SOCKS - } - } -} \ No newline at end of file + val cacheSize: String = "" +) \ No newline at end of file diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt index 49d675c7..7d75685e 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt @@ -12,7 +12,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString -import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.ProxyConfig import zed.rainxch.core.domain.repository.ProxyRepository import zed.rainxch.core.domain.repository.ThemesRepository @@ -22,6 +21,7 @@ import zed.rainxch.githubstore.core.presentation.res.failed_to_save_proxy_settin import zed.rainxch.githubstore.core.presentation.res.invalid_proxy_port import zed.rainxch.githubstore.core.presentation.res.proxy_host_required import zed.rainxch.profile.domain.repository.ProfileRepository +import zed.rainxch.profile.presentation.model.ProxyType class ProfileViewModel( private val browserHelper: BrowserHelper, @@ -43,6 +43,7 @@ class ProfileViewModel( loadUserProfile() loadVersionName() loadProxyConfig() + observeCacheSize() hasLoadedInitialData = true } @@ -56,6 +57,32 @@ class ProfileViewModel( private val _events = Channel() val events = _events.receiveAsFlow() + private fun observeCacheSize() { + viewModelScope.launch { + profileRepository.observeCacheSize().collect { sizeBytes -> + _state.update { + it.copy(cacheSize = formatCacheSize(sizeBytes)) + } + } + } + } + + private fun formatCacheSize(bytes: Long): String { + if (bytes <= 0) return "0 B" + val units = arrayOf("B", "KB", "MB", "GB") + var size = bytes.toDouble() + var unitIndex = 0 + while (size >= 1024 && unitIndex < units.lastIndex) { + size /= 1024 + unitIndex++ + } + return if (size == size.toLong().toDouble()) { + "${size.toLong()} ${units[unitIndex]}" + } else { + "${"%.1f".format(size)} ${units[unitIndex]}" + } + } + private fun loadVersionName() { viewModelScope.launch { _state.update { @@ -172,6 +199,23 @@ class ProfileViewModel( ) } + ProfileAction.OnClearCacheClick -> { + viewModelScope.launch { + runCatching { + profileRepository.clearCache() + }.onSuccess { + observeCacheSize() + _events.send(ProfileEvent.OnCacheCleared) + }.onFailure { error -> + _events.send( + ProfileEvent.OnCacheClearError( + error.message ?: "Failed to clear cache" + ) + ) + } + } + } + is ProfileAction.OnThemeColorSelected -> { viewModelScope.launch { themesRepository.setThemeColor(action.themeColor) diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/About.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/About.kt index cfef4968..99db94b3 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/About.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/About.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp import zed.rainxch.githubstore.core.presentation.res.* import org.jetbrains.compose.resources.stringResource import zed.rainxch.profile.presentation.ProfileAction +import zed.rainxch.profile.presentation.components.SectionHeader @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun LazyListScope.about( @@ -38,12 +39,8 @@ fun LazyListScope.about( onAction: (ProfileAction) -> Unit, ) { item { - Text( - text = stringResource(Res.string.section_about), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.outline, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(start = 8.dp) + SectionHeader( + text = stringResource(Res.string.section_about) ) Spacer(Modifier.height(8.dp)) diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Appearance.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Appearance.kt index 651ab587..85369a11 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Appearance.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Appearance.kt @@ -106,7 +106,7 @@ fun LazyListScope.appearanceSection( } ) - VerticalSpacer(16.dp) + VerticalSpacer(8.dp) } ToggleSettingCard( @@ -124,7 +124,7 @@ fun LazyListScope.appearanceSection( } ) - VerticalSpacer(16.dp) + VerticalSpacer(8.dp) ToggleSettingCard( title = stringResource(Res.string.auto_detect_clipboard_links), diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Network.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Network.kt index cec3b1d6..02f95aad 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Network.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Network.kt @@ -45,8 +45,8 @@ import org.jetbrains.compose.resources.stringResource import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.profile.presentation.ProfileAction import zed.rainxch.profile.presentation.ProfileState -import zed.rainxch.profile.presentation.ProxyType import zed.rainxch.profile.presentation.components.SectionHeader +import zed.rainxch.profile.presentation.model.ProxyType @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun LazyListScope.networkSection( diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Others.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Others.kt new file mode 100644 index 00000000..b374b448 --- /dev/null +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Others.kt @@ -0,0 +1,101 @@ +package zed.rainxch.profile.presentation.components.sections + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Storage +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.ExpressiveCard +import zed.rainxch.githubstore.core.presentation.res.* +import zed.rainxch.profile.presentation.ProfileAction +import zed.rainxch.profile.presentation.ProfileState +import zed.rainxch.profile.presentation.components.SectionHeader + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +fun LazyListScope.othersSection( + state: ProfileState, + onAction: (ProfileAction) -> Unit +) { + item { + SectionHeader( + text = stringResource(Res.string.storage).uppercase() + ) + + Spacer(Modifier.height(8.dp)) + + ExpressiveCard { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + imageVector = Icons.Outlined.Storage, + contentDescription = null, + modifier = Modifier + .size(44.dp) + .clip(RoundedCornerShape(36.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerLow) + .padding(8.dp) + ) + + Column ( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + text = stringResource(Res.string.clear_cache), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + + Text( + text = "${stringResource(Res.string.current_size)} ${state.cacheSize}", + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Button( + onClick = { + onAction(ProfileAction.OnClearCacheClick) + }, + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + contentColor = MaterialTheme.colorScheme.onSurface + ) + ) { + Text( + text = stringResource(Res.string.clear), + style = MaterialTheme.typography.titleMediumEmphasized, + fontWeight = FontWeight.Bold + ) + } + } + } + } +} \ No newline at end of file diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/model/ProxyType.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/model/ProxyType.kt new file mode 100644 index 00000000..336cc207 --- /dev/null +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/model/ProxyType.kt @@ -0,0 +1,16 @@ +package zed.rainxch.profile.presentation.model; + +import zed.rainxch.core.domain.model.ProxyConfig + +enum class ProxyType { + NONE, SYSTEM, HTTP, SOCKS; + + companion object { + fun fromConfig(config: ProxyConfig): ProxyType = when (config) { + is ProxyConfig.None -> NONE + is ProxyConfig.System -> SYSTEM + is ProxyConfig.Http -> HTTP + is ProxyConfig.Socks -> SOCKS + } + } +} \ No newline at end of file diff --git a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/components/StarredRepositoryItem.kt b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/components/StarredRepositoryItem.kt index 914fd2ec..1339e5e7 100644 --- a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/components/StarredRepositoryItem.kt +++ b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/components/StarredRepositoryItem.kt @@ -25,9 +25,11 @@ import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledIconToggleButton import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SuggestionChip import androidx.compose.material3.Text +import androidx.compose.material3.toShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -42,6 +44,7 @@ import com.skydoves.landscapist.coil3.CoilImage import zed.rainxch.githubstore.core.presentation.res.* import org.jetbrains.compose.resources.stringResource import androidx.compose.ui.tooling.preview.Preview +import zed.rainxch.core.presentation.components.ExpressiveCard import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.utils.formatCount import zed.rainxch.starred.presentation.model.StarredRepositoryUi @@ -55,9 +58,9 @@ fun StarredRepositoryItem( onDevProfileClick: () -> Unit, modifier: Modifier = Modifier ) { - Card( - modifier = modifier.fillMaxWidth(), - onClick = onItemClick + ExpressiveCard( + onClick = onItemClick, + modifier = modifier.fillMaxWidth() ) { Column( modifier = Modifier @@ -110,7 +113,8 @@ fun StarredRepositoryItem( FilledIconToggleButton( checked = repository.isFavorite, onCheckedChange = { onToggleFavoriteClick() }, - modifier = Modifier.size(40.dp) + modifier = Modifier.size(40.dp), + shape = MaterialShapes.Cookie6Sided.toShape() ) { Icon( imageVector = if (repository.isFavorite) {