Improve cache settings view
This commit is contained in:
parent
d5edf360fb
commit
f192e1c29d
@ -8,6 +8,7 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
882C955E2AE7F0DE00657886 /* ClassifierDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882C955D2AE7F0DE00657886 /* ClassifierDownloadView.swift */; };
|
||||
882C95602AE7FA7100657886 /* CachedImageCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882C955F2AE7FA7100657886 /* CachedImageCountView.swift */; };
|
||||
88C1511C29A11ADF0080EF4F /* CapImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C1511B29A11ADF0080EF4F /* CapImagesView.swift */; };
|
||||
88DBE72E285495B100D1573B /* FancyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBE72D285495B100D1573B /* FancyTextField.swift */; };
|
||||
E20D104A285612AF0019BD91 /* ImageGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20D1049285612AF0019BD91 /* ImageGrid.swift */; };
|
||||
@ -54,6 +55,7 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
882C955D2AE7F0DE00657886 /* ClassifierDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassifierDownloadView.swift; sourceTree = "<group>"; };
|
||||
882C955F2AE7FA7100657886 /* CachedImageCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedImageCountView.swift; sourceTree = "<group>"; };
|
||||
88C1511B29A11ADF0080EF4F /* CapImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapImagesView.swift; sourceTree = "<group>"; };
|
||||
88DBE72D285495B100D1573B /* FancyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyTextField.swift; sourceTree = "<group>"; };
|
||||
E20D1049285612AF0019BD91 /* ImageGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrid.swift; sourceTree = "<group>"; };
|
||||
@ -195,6 +197,7 @@
|
||||
E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */,
|
||||
E2EA00E0283F658E00F7B269 /* SettingsView.swift */,
|
||||
882C955D2AE7F0DE00657886 /* ClassifierDownloadView.swift */,
|
||||
882C955F2AE7FA7100657886 /* CachedImageCountView.swift */,
|
||||
E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */,
|
||||
E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */,
|
||||
88C1511B29A11ADF0080EF4F /* CapImagesView.swift */,
|
||||
@ -306,6 +309,7 @@
|
||||
E2EA00EB284109CC00F7B269 /* CGImage+Extensions.swift in Sources */,
|
||||
E20D104E28574C7C0019BD91 /* CachedCapImage.swift in Sources */,
|
||||
E2EA00DF283F5CA000F7B269 /* ErrorView.swift in Sources */,
|
||||
882C95602AE7FA7100657886 /* CachedImageCountView.swift in Sources */,
|
||||
E20D104A285612AF0019BD91 /* ImageGrid.swift in Sources */,
|
||||
E2EA00D5283EDFA200F7B269 /* FrameManager.swift in Sources */,
|
||||
E25AAC90283D871E006E9E7F /* CGImagePropertyOrientation+Extensions.swift in Sources */,
|
||||
|
Binary file not shown.
@ -91,8 +91,12 @@ final class Database: ObservableObject {
|
||||
pendingImageUploadStorage = newValue.map { cap, images in
|
||||
"\(cap)-\(images.map { "\($0)" }.joined(separator: ":"))"
|
||||
}.joined(separator: ";")
|
||||
pendingImageUploadCount = newValue.values.reduce(0) { $0 + $1.count }
|
||||
}
|
||||
}
|
||||
|
||||
@Published
|
||||
private(set) var pendingImageUploadCount = 0
|
||||
|
||||
private var uploadTimer: Timer?
|
||||
|
||||
@ -569,28 +573,10 @@ final class Database: ObservableObject {
|
||||
changedCaps.contains(cap) || imageUploads[cap] != nil
|
||||
}
|
||||
|
||||
var pendingImageUploadCount: Int {
|
||||
imageUploads.values.reduce(0) { $0 + $1.count }
|
||||
}
|
||||
|
||||
private func capId(from url: URL) -> Int? {
|
||||
Int(url.lastPathComponent.components(separatedBy: "-").first!)
|
||||
}
|
||||
|
||||
private func imageId(from url: URL) -> CapImage? {
|
||||
let parts = url.deletingPathExtension().lastPathComponent.components(separatedBy: "-")
|
||||
guard parts.count == 2 else {
|
||||
log("File \(url.lastPathComponent) is not a cap image")
|
||||
return nil
|
||||
}
|
||||
guard let capId = Int(parts.first!),
|
||||
let version = Int(parts.last!) else {
|
||||
log("File \(url.lastPathComponent) is not a cap image")
|
||||
return nil
|
||||
}
|
||||
return .init(cap: capId, version: version)
|
||||
}
|
||||
|
||||
private func uploadAllImages() async {
|
||||
guard hasServerAuthentication else {
|
||||
log("No server authentication to upload to server")
|
||||
@ -980,36 +966,10 @@ final class Database: ObservableObject {
|
||||
var classifierSize: Int {
|
||||
localClassifierUrl.fileSize
|
||||
}
|
||||
|
||||
private var cachedImages: [URL] {
|
||||
do {
|
||||
return try fm.contentsOfDirectory(at: images.folder, includingPropertiesForKeys: nil)
|
||||
} catch {
|
||||
log("Failed to get cached images: \(error)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func clearImageCache() {
|
||||
let allImages = cachedImages
|
||||
let unnecessaryImages = allImages
|
||||
.filter {
|
||||
guard let id = imageId(from: $0) else {
|
||||
return true
|
||||
}
|
||||
guard let cap = caps[id.cap] else {
|
||||
return true
|
||||
}
|
||||
return cap.mainImage != id.version
|
||||
}
|
||||
log("Deleting \(unnecessaryImages.count) of \(allImages.count) cached images")
|
||||
for cachedImage in unnecessaryImages {
|
||||
do {
|
||||
try fm.removeItem(at: cachedImage)
|
||||
} catch {
|
||||
log("Failed to delete cached image \(cachedImage.lastPathComponent): \(error)")
|
||||
}
|
||||
}
|
||||
let imagesToKeep = caps.values.map { $0.image }
|
||||
images.clearImageCache(keeping: Set(imagesToKeep))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class ImageCache {
|
||||
final class ImageCache: ObservableObject {
|
||||
|
||||
let folder: URL
|
||||
|
||||
@ -16,6 +16,9 @@ final class ImageCache {
|
||||
private let thumbnailQuality: CGFloat = 0.7
|
||||
|
||||
private let imageQuality: CGFloat = 0.3
|
||||
|
||||
@Published
|
||||
private(set) var imageCount = 0
|
||||
|
||||
init(folder: URL, server: URL, thumbnailSize: CGFloat) throws {
|
||||
self.folder = folder
|
||||
@ -25,6 +28,7 @@ final class ImageCache {
|
||||
if !fm.fileExists(atPath: folder.path) {
|
||||
try fm.createDirectory(at: folder, withIntermediateDirectories: true)
|
||||
}
|
||||
self.imageCount = countLocalImages()
|
||||
}
|
||||
|
||||
private func localImageUrl(_ image: CapImage) -> URL {
|
||||
@ -92,12 +96,6 @@ final class ImageCache {
|
||||
return UIImage(at: localUrl)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func removeImage(_ image: CapImage) -> Bool {
|
||||
let localUrl = localImageUrl(image)
|
||||
return removePossibleFile(localUrl)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func refreshImage(_ image: CapImage) async -> Bool {
|
||||
guard let downloadedImageUrl = await loadRemoteImage(image) else {
|
||||
@ -122,6 +120,9 @@ final class ImageCache {
|
||||
for url in files {
|
||||
do {
|
||||
try fm.removeItem(at: url)
|
||||
DispatchQueue.main.async {
|
||||
self.imageCount -= 1
|
||||
}
|
||||
} catch {
|
||||
isSuccessful = false
|
||||
log("Failed to remove image \(url.lastPathComponent) from cache: \(error)")
|
||||
@ -157,14 +158,20 @@ final class ImageCache {
|
||||
|
||||
private func saveImage(_ image: CapImage, at tempUrl: URL) -> Bool {
|
||||
let localUrl = localImageUrl(image)
|
||||
let isOverwrite = exists(localUrl)
|
||||
guard removePossibleFile(localUrl) else {
|
||||
return false
|
||||
}
|
||||
do {
|
||||
try fm.moveItem(at: tempUrl, to: localUrl)
|
||||
if !isOverwrite {
|
||||
DispatchQueue.main.async {
|
||||
self.imageCount += 1
|
||||
}
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
print("failed to save image \(localUrl.lastPathComponent): \(error)")
|
||||
print("Failed to save image \(localUrl.lastPathComponent): \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -250,12 +257,64 @@ final class ImageCache {
|
||||
return false
|
||||
}
|
||||
let localUrl = localThumbnailUrl(cap: cap)
|
||||
let isOverwrite = exists(localUrl)
|
||||
do {
|
||||
try data.write(to: localUrl)
|
||||
if !isOverwrite {
|
||||
DispatchQueue.main.async {
|
||||
self.imageCount += 1
|
||||
}
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
print("Failed to save thumbnail \(cap): \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func countLocalImages() -> Int {
|
||||
cachedImages.count
|
||||
}
|
||||
|
||||
private var cachedImages: [URL] {
|
||||
do {
|
||||
return try fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil)
|
||||
} catch {
|
||||
log("Failed to get cached images: \(error)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private func imageId(from url: URL) -> CapImage? {
|
||||
let parts = url.deletingPathExtension().lastPathComponent.components(separatedBy: "-")
|
||||
guard parts.count == 2 else {
|
||||
log("File \(url.lastPathComponent) is not a cap image")
|
||||
return nil
|
||||
}
|
||||
guard let capId = Int(parts.first!),
|
||||
let version = Int(parts.last!) else {
|
||||
log("File \(url.lastPathComponent) is not a cap image")
|
||||
return nil
|
||||
}
|
||||
return .init(cap: capId, version: version)
|
||||
}
|
||||
|
||||
func clearImageCache(keeping: Set<CapImage>) {
|
||||
let allImages = cachedImages
|
||||
for url in allImages {
|
||||
if let id = imageId(from: url), keeping.contains(id) {
|
||||
continue // Skip image
|
||||
}
|
||||
do {
|
||||
try fm.removeItem(at: url)
|
||||
} catch {
|
||||
log("Failed to delete cached image \(url.lastPathComponent): \(error)")
|
||||
}
|
||||
}
|
||||
let newCount = countLocalImages()
|
||||
log("Deleted \(allImages.count - newCount) of \(allImages.count) cached images, leaving \(newCount)")
|
||||
DispatchQueue.main.async {
|
||||
self.imageCount = newCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
Caps/Views/CachedImageCountView.swift
Normal file
15
Caps/Views/CachedImageCountView.swift
Normal file
@ -0,0 +1,15 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CachedImageCountView: View {
|
||||
|
||||
@ObservedObject
|
||||
var cache: ImageCache
|
||||
|
||||
var body: some View {
|
||||
SettingsStatisticRow(label: "Downloaded images", value: "\(cache.imageCount)")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CachedImageCountView(cache: Database.mock.images)
|
||||
}
|
@ -33,8 +33,22 @@ struct SettingsView: View {
|
||||
.padding(.top)
|
||||
Group {
|
||||
SettingsStatisticRow(label: "Caps", value: "\(database.numberOfCaps)")
|
||||
SettingsStatisticRow(label: "Total images", value: "\(database.numberOfImages)")
|
||||
SettingsStatisticRow(label: "Images per cap", value: String(format: "%.1f", database.averageImageCount))
|
||||
SettingsStatisticRow(label: "Total images", value: "\(database.numberOfImages)")
|
||||
CachedImageCountView(cache: database.images)
|
||||
SettingsStatisticRow(label: "Cache size", value: byteString(imageCacheSize))
|
||||
SettingsStatisticRow(label: "Database", value: byteString(database.databaseSize))
|
||||
if database.pendingImageUploadCount > 0 {
|
||||
SettingsStatisticRow(label: "Images to upload", value: "\(database.pendingImageUploadCount)")
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: clearImageCache) {
|
||||
Label("Clear image cache", systemSymbol: .trash)
|
||||
}
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
}.padding(.horizontal)
|
||||
Text("Classifier")
|
||||
.font(.footnote)
|
||||
@ -44,6 +58,7 @@ struct SettingsView: View {
|
||||
Group {
|
||||
SettingsStatisticRow(label: "Server Version", value: "\(database.serverClassifierVersion)")
|
||||
SettingsStatisticRow(label: "Local Version", value: "\(database.localClassifierVersion)")
|
||||
SettingsStatisticRow(label: "Size", value: byteString(database.classifierSize))
|
||||
SettingsStatisticRow(label: "Recognized caps", value: "\(database.classifierClassCount)")
|
||||
HStack {
|
||||
Spacer()
|
||||
@ -65,24 +80,6 @@ struct SettingsView: View {
|
||||
ClassifierDownloadView(progress: progress)
|
||||
}
|
||||
}.padding(.horizontal)
|
||||
Text("Storage")
|
||||
.font(.footnote)
|
||||
.textCase(.uppercase)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top)
|
||||
Group {
|
||||
SettingsStatisticRow(label: "Image cache", value: byteString(imageCacheSize))
|
||||
SettingsStatisticRow(label: "Database", value: byteString(database.databaseSize))
|
||||
SettingsStatisticRow(label: "Classifier", value: byteString(database.classifierSize))
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: clearImageCache) {
|
||||
Label("Clear image cache", systemSymbol: .trash)
|
||||
}
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
}.padding(.horizontal)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal)
|
||||
@ -109,6 +106,7 @@ struct SettingsView: View {
|
||||
// Ensure that correct version is saved
|
||||
await database.updateServerClassifierVersion()
|
||||
await database.downloadClassifier()
|
||||
await database.downloadClassifierClasses()
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user