mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
clear cache button
This commit is contained in:
@@ -449,6 +449,9 @@
|
|||||||
"admin_password": "Admin Password",
|
"admin_password": "Admin Password",
|
||||||
"administration": "Administration",
|
"administration": "Administration",
|
||||||
"advanced": "Advanced",
|
"advanced": "Advanced",
|
||||||
|
"advanced_settings_clear_image_cache": "Clear Image Cache",
|
||||||
|
"advanced_settings_clear_image_cache_error": "Failed to clear image cache",
|
||||||
|
"advanced_settings_clear_image_cache_success": "Successfully cleared {size}",
|
||||||
"advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.",
|
"advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.",
|
||||||
"advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter",
|
"advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter",
|
||||||
"advanced_settings_log_level_title": "Log level: {level}",
|
"advanced_settings_log_level_title": "Log level: {level}",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ private open class RemoteImagesPigeonCodec : StandardMessageCodec() {
|
|||||||
interface RemoteImageApi {
|
interface RemoteImageApi {
|
||||||
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, callback: (Result<Map<String, Long>?>) -> Unit)
|
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, callback: (Result<Map<String, Long>?>) -> Unit)
|
||||||
fun cancelRequest(requestId: Long)
|
fun cancelRequest(requestId: Long)
|
||||||
|
fun clearCache(callback: (Result<Long>) -> Unit)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by RemoteImageApi. */
|
/** The codec used by RemoteImageApi. */
|
||||||
@@ -99,6 +100,24 @@ interface RemoteImageApi {
|
|||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
api.clearCache{ result: Result<Long> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(RemoteImagesPigeonUtils.wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(RemoteImagesPigeonUtils.wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+106
-18
@@ -8,6 +8,7 @@ import app.alextran.immich.INITIAL_BUFFER_SIZE
|
|||||||
import app.alextran.immich.NativeBuffer
|
import app.alextran.immich.NativeBuffer
|
||||||
import app.alextran.immich.NativeByteBuffer
|
import app.alextran.immich.NativeByteBuffer
|
||||||
import app.alextran.immich.core.SSLConfig
|
import app.alextran.immich.core.SSLConfig
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
import okhttp3.Callback
|
||||||
@@ -24,6 +25,11 @@ import java.io.EOFException
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.file.FileVisitResult
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.SimpleFileVisitor
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -90,6 +96,16 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
|||||||
override fun cancelRequest(requestId: Long) {
|
override fun cancelRequest(requestId: Long) {
|
||||||
requestMap.remove(requestId)?.cancellationSignal?.cancel()
|
requestMap.remove(requestId)?.cancellationSignal?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun clearCache(callback: (Result<Long>) -> Unit) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
ImageFetcherManager.clearCache(callback)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
callback(Result.failure(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object ImageFetcherManager {
|
private object ImageFetcherManager {
|
||||||
@@ -120,6 +136,10 @@ private object ImageFetcherManager {
|
|||||||
fetcher.fetch(url, headers, signal, onSuccess, onFailure)
|
fetcher.fetch(url, headers, signal, onSuccess, onFailure)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearCache(onCleared: (Result<Long>) -> Unit) {
|
||||||
|
fetcher.clearCache(onCleared)
|
||||||
|
}
|
||||||
|
|
||||||
private fun invalidate() {
|
private fun invalidate() {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
val oldFetcher = fetcher
|
val oldFetcher = fetcher
|
||||||
@@ -151,25 +171,22 @@ private sealed interface ImageFetcher {
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun drain()
|
fun drain()
|
||||||
|
|
||||||
|
fun clearCache(onCleared: (Result<Long>) -> Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CronetImageFetcher(context: Context, cacheDir: File) : ImageFetcher {
|
private class CronetImageFetcher(context: Context, cacheDir: File) : ImageFetcher {
|
||||||
private val engine: CronetEngine
|
private val ctx = context
|
||||||
|
private var engine: CronetEngine
|
||||||
private val executor = Executors.newFixedThreadPool(4)
|
private val executor = Executors.newFixedThreadPool(4)
|
||||||
private val stateLock = Any()
|
private val stateLock = Any()
|
||||||
private var activeCount = 0
|
private var activeCount = 0
|
||||||
private var draining = false
|
private var draining = false
|
||||||
|
private var onCacheCleared: ((Result<Long>) -> Unit)? = null
|
||||||
|
private val storageDir = File(cacheDir, "cronet").apply { mkdirs() }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val storageDir = File(cacheDir, "cronet").apply { mkdirs() }
|
engine = build(context)
|
||||||
engine = CronetEngine.Builder(context)
|
|
||||||
.enableHttp2(true)
|
|
||||||
.enableQuic(true)
|
|
||||||
.enableBrotli(true)
|
|
||||||
.setStoragePath(storageDir.absolutePath)
|
|
||||||
.setUserAgent(USER_AGENT)
|
|
||||||
.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, CACHE_SIZE_BYTES)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetch(
|
override fun fetch(
|
||||||
@@ -195,29 +212,68 @@ private class CronetImageFetcher(context: Context, cacheDir: File) : ImageFetche
|
|||||||
request.start()
|
request.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun build(ctx: Context): CronetEngine {
|
||||||
|
return CronetEngine.Builder(ctx)
|
||||||
|
.enableHttp2(true)
|
||||||
|
.enableQuic(true)
|
||||||
|
.enableBrotli(true)
|
||||||
|
.setStoragePath(storageDir.absolutePath)
|
||||||
|
.setUserAgent(USER_AGENT)
|
||||||
|
.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, CACHE_SIZE_BYTES)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
private fun onComplete() {
|
private fun onComplete() {
|
||||||
val shouldShutdown = synchronized(stateLock) {
|
val didDrain = synchronized(stateLock) {
|
||||||
activeCount--
|
activeCount--
|
||||||
draining && activeCount == 0
|
draining && activeCount == 0
|
||||||
}
|
}
|
||||||
if (shouldShutdown) {
|
if (didDrain) {
|
||||||
engine.shutdown()
|
onDrained()
|
||||||
executor.shutdown()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun drain() {
|
override fun drain() {
|
||||||
val shouldShutdown = synchronized(stateLock) {
|
val didDrain = synchronized(stateLock) {
|
||||||
if (draining) return
|
if (draining) return
|
||||||
draining = true
|
draining = true
|
||||||
activeCount == 0
|
activeCount == 0
|
||||||
}
|
}
|
||||||
if (shouldShutdown) {
|
if (didDrain) {
|
||||||
engine.shutdown()
|
onDrained()
|
||||||
executor.shutdown()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onDrained() {
|
||||||
|
engine.shutdown()
|
||||||
|
val onCacheCleared = synchronized(stateLock) {
|
||||||
|
val onCacheCleared = onCacheCleared
|
||||||
|
this.onCacheCleared = null
|
||||||
|
onCacheCleared
|
||||||
|
}
|
||||||
|
if (onCacheCleared == null) {
|
||||||
|
executor.shutdown()
|
||||||
|
} else {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val result = runCatching { deleteFolderAndGetSize(storageDir.toPath()) }
|
||||||
|
// Cronet is very good at self-repair, so it shouldn't fail here regardless of clear result
|
||||||
|
engine = build(ctx)
|
||||||
|
synchronized(stateLock) { draining = false }
|
||||||
|
onCacheCleared(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearCache(onCleared: (Result<Long>) -> Unit) {
|
||||||
|
synchronized(stateLock) {
|
||||||
|
if (onCacheCleared != null) {
|
||||||
|
return onCleared(Result.success(-1))
|
||||||
|
}
|
||||||
|
onCacheCleared = onCleared
|
||||||
|
}
|
||||||
|
drain()
|
||||||
|
}
|
||||||
|
|
||||||
private class FetchCallback(
|
private class FetchCallback(
|
||||||
private val onSuccess: (NativeByteBuffer) -> Unit,
|
private val onSuccess: (NativeByteBuffer) -> Unit,
|
||||||
private val onFailure: (Exception) -> Unit,
|
private val onFailure: (Exception) -> Unit,
|
||||||
@@ -283,6 +339,27 @@ private class CronetImageFetcher(context: Context, cacheDir: File) : ImageFetche
|
|||||||
onComplete()
|
onComplete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun deleteFolderAndGetSize(root: Path): Long = withContext(Dispatchers.IO) {
|
||||||
|
var totalSize = 0L
|
||||||
|
|
||||||
|
Files.walkFileTree(root, object : SimpleFileVisitor<Path>() {
|
||||||
|
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
|
||||||
|
totalSize += attrs.size()
|
||||||
|
Files.delete(file)
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
|
||||||
|
if (dir != root) {
|
||||||
|
Files.delete(dir)
|
||||||
|
}
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
totalSize
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class OkHttpImageFetcher private constructor(
|
private class OkHttpImageFetcher private constructor(
|
||||||
@@ -412,6 +489,7 @@ private class OkHttpImageFetcher private constructor(
|
|||||||
buffer.free()
|
buffer.free()
|
||||||
onFailure(e)
|
onFailure(e)
|
||||||
}
|
}
|
||||||
|
onComplete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -429,4 +507,14 @@ private class OkHttpImageFetcher private constructor(
|
|||||||
client.cache?.close()
|
client.cache?.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun clearCache(onCleared: (Result<Long>) -> Unit) {
|
||||||
|
try {
|
||||||
|
val size = client.cache!!.size()
|
||||||
|
client.cache!!.evictAll()
|
||||||
|
onCleared(Result.success(size))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onCleared(Result.failure(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class RemoteImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
|
|||||||
protocol RemoteImageApi {
|
protocol RemoteImageApi {
|
||||||
func requestImage(url: String, headers: [String: String], requestId: Int64, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
|
func requestImage(url: String, headers: [String: String], requestId: Int64, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
|
||||||
func cancelRequest(requestId: Int64) throws
|
func cancelRequest(requestId: Int64) throws
|
||||||
|
func clearCache(completion: @escaping (Result<Int64, Error>) -> Void)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
@@ -114,5 +115,20 @@ class RemoteImageApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
cancelRequestChannel.setMessageHandler(nil)
|
cancelRequestChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
|
let clearCacheChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
clearCacheChannel.setMessageHandler { _, reply in
|
||||||
|
api.clearCache { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clearCacheChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,18 +20,19 @@ class RemoteImageRequest {
|
|||||||
|
|
||||||
class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
||||||
private static let delegate = RemoteImageApiDelegate()
|
private static let delegate = RemoteImageApiDelegate()
|
||||||
|
static let cacheDir = FileManager.default.temporaryDirectory.appendingPathComponent(
|
||||||
|
"thumbnails", isDirectory: true)
|
||||||
static let session = {
|
static let session = {
|
||||||
let config = URLSessionConfiguration.default
|
let config = URLSessionConfiguration.default
|
||||||
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
|
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
|
||||||
config.httpAdditionalHeaders = ["User-Agent": "Immich_iOS_\(version)"]
|
config.httpAdditionalHeaders = ["User-Agent": "Immich_iOS_\(version)"]
|
||||||
let thumbnailPath = FileManager.default.temporaryDirectory.appendingPathComponent("thumbnails", isDirectory: true)
|
try! FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
|
||||||
try! FileManager.default.createDirectory(at: thumbnailPath, withIntermediateDirectories: true)
|
|
||||||
config.urlCache = URLCache(
|
config.urlCache = URLCache(
|
||||||
memoryCapacity: 0,
|
memoryCapacity: 0,
|
||||||
diskCapacity: 1 << 30,
|
diskCapacity: 1 << 30,
|
||||||
directory: thumbnailPath
|
directory: cacheDir
|
||||||
)
|
)
|
||||||
config.httpMaximumConnectionsPerHost = 64
|
config.httpMaximumConnectionsPerHost = 16
|
||||||
return URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
|
return URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -51,6 +52,15 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
|||||||
func cancelRequest(requestId: Int64) {
|
func cancelRequest(requestId: Int64) {
|
||||||
Self.delegate.cancel(requestId: requestId)
|
Self.delegate.cancel(requestId: requestId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clearCache(completion: @escaping (Result<Int64, any Error>) -> Void) {
|
||||||
|
Task {
|
||||||
|
let cache = Self.session.configuration.urlCache!
|
||||||
|
let cacheSize = Int64(cache.currentDiskUsage)
|
||||||
|
cache.removeAllCachedResponses()
|
||||||
|
completion(.success(cacheSize))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
|
class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
|
||||||
|
|||||||
+28
@@ -98,4 +98,32 @@ class RemoteImageApi {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> clearCache() async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else if (pigeonVar_replyList[0] == null) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'null-error',
|
||||||
|
message: 'Host platform returned null value for non-null return value.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (pigeonVar_replyList[0] as int?)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ String formatBytes(int bytes) {
|
|||||||
|
|
||||||
String formatHumanReadableBytes(int bytes, int decimals) {
|
String formatHumanReadableBytes(int bytes, int decimals) {
|
||||||
if (bytes <= 0) return "0 B";
|
if (bytes <= 0) return "0 B";
|
||||||
const suffixes = ["B", "KB", "MB", "GB", "TB"];
|
const suffixes = ["B", "KiB", "MiB", "GiB", "TiB"];
|
||||||
var i = (log(bytes) / log(1024)).floor();
|
var i = (log(bytes) / log(1024)).floor();
|
||||||
return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}';
|
return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
|
||||||
@@ -153,6 +155,43 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text("advanced_settings_clear_image_cache".tr(), style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
leading: const Icon(Icons.playlist_remove_rounded),
|
||||||
|
onTap: () async {
|
||||||
|
final int clearedBytes;
|
||||||
|
try {
|
||||||
|
clearedBytes = await remoteImageApi.clearCache();
|
||||||
|
} catch (e) {
|
||||||
|
context.scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
content: Text(
|
||||||
|
"advanced_settings_clear_image_cache_error".tr(),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(color: context.themeData.colorScheme.error),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearedBytes < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS always returns a small non-zero value
|
||||||
|
final clearedMB = clearedBytes < (256 * 1024) ? "0 MiB" : formatHumanReadableBytes(clearedBytes, 2);
|
||||||
|
context.scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
content: Text(
|
||||||
|
"advanced_settings_clear_image_cache_success".tr(namedArgs: {'size': clearedMB}),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return SettingsSubPageScaffold(settings: advancedSettings);
|
return SettingsSubPageScaffold(settings: advancedSettings);
|
||||||
|
|||||||
@@ -22,4 +22,7 @@ abstract class RemoteImageApi {
|
|||||||
});
|
});
|
||||||
|
|
||||||
void cancelRequest(int requestId);
|
void cancelRequest(int requestId);
|
||||||
|
|
||||||
|
@async
|
||||||
|
int clearCache();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user