Improve cache settings view

This commit is contained in:
Christoph Hagen 2023-10-24 15:44:13 +02:00
parent d5edf360fb
commit f192e1c29d
6 changed files with 110 additions and 74 deletions

View File

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
882C955E2AE7F0DE00657886 /* ClassifierDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882C955D2AE7F0DE00657886 /* ClassifierDownloadView.swift */; }; 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 */; }; 88C1511C29A11ADF0080EF4F /* CapImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C1511B29A11ADF0080EF4F /* CapImagesView.swift */; };
88DBE72E285495B100D1573B /* FancyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBE72D285495B100D1573B /* FancyTextField.swift */; }; 88DBE72E285495B100D1573B /* FancyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBE72D285495B100D1573B /* FancyTextField.swift */; };
E20D104A285612AF0019BD91 /* ImageGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20D1049285612AF0019BD91 /* ImageGrid.swift */; }; E20D104A285612AF0019BD91 /* ImageGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20D1049285612AF0019BD91 /* ImageGrid.swift */; };
@ -54,6 +55,7 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
882C955D2AE7F0DE00657886 /* ClassifierDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassifierDownloadView.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; E20D1049285612AF0019BD91 /* ImageGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrid.swift; sourceTree = "<group>"; };
@ -195,6 +197,7 @@
E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */, E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */,
E2EA00E0283F658E00F7B269 /* SettingsView.swift */, E2EA00E0283F658E00F7B269 /* SettingsView.swift */,
882C955D2AE7F0DE00657886 /* ClassifierDownloadView.swift */, 882C955D2AE7F0DE00657886 /* ClassifierDownloadView.swift */,
882C955F2AE7FA7100657886 /* CachedImageCountView.swift */,
E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */, E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */,
E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */, E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */,
88C1511B29A11ADF0080EF4F /* CapImagesView.swift */, 88C1511B29A11ADF0080EF4F /* CapImagesView.swift */,
@ -306,6 +309,7 @@
E2EA00EB284109CC00F7B269 /* CGImage+Extensions.swift in Sources */, E2EA00EB284109CC00F7B269 /* CGImage+Extensions.swift in Sources */,
E20D104E28574C7C0019BD91 /* CachedCapImage.swift in Sources */, E20D104E28574C7C0019BD91 /* CachedCapImage.swift in Sources */,
E2EA00DF283F5CA000F7B269 /* ErrorView.swift in Sources */, E2EA00DF283F5CA000F7B269 /* ErrorView.swift in Sources */,
882C95602AE7FA7100657886 /* CachedImageCountView.swift in Sources */,
E20D104A285612AF0019BD91 /* ImageGrid.swift in Sources */, E20D104A285612AF0019BD91 /* ImageGrid.swift in Sources */,
E2EA00D5283EDFA200F7B269 /* FrameManager.swift in Sources */, E2EA00D5283EDFA200F7B269 /* FrameManager.swift in Sources */,
E25AAC90283D871E006E9E7F /* CGImagePropertyOrientation+Extensions.swift in Sources */, E25AAC90283D871E006E9E7F /* CGImagePropertyOrientation+Extensions.swift in Sources */,

View File

@ -91,9 +91,13 @@ final class Database: ObservableObject {
pendingImageUploadStorage = newValue.map { cap, images in pendingImageUploadStorage = newValue.map { cap, images in
"\(cap)-\(images.map { "\($0)" }.joined(separator: ":"))" "\(cap)-\(images.map { "\($0)" }.joined(separator: ":"))"
}.joined(separator: ";") }.joined(separator: ";")
pendingImageUploadCount = newValue.values.reduce(0) { $0 + $1.count }
} }
} }
@Published
private(set) var pendingImageUploadCount = 0
private var uploadTimer: Timer? private var uploadTimer: Timer?
/// The classifications for all caps from the classifier /// The classifications for all caps from the classifier
@ -569,28 +573,10 @@ final class Database: ObservableObject {
changedCaps.contains(cap) || imageUploads[cap] != nil changedCaps.contains(cap) || imageUploads[cap] != nil
} }
var pendingImageUploadCount: Int {
imageUploads.values.reduce(0) { $0 + $1.count }
}
private func capId(from url: URL) -> Int? { private func capId(from url: URL) -> Int? {
Int(url.lastPathComponent.components(separatedBy: "-").first!) 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 { private func uploadAllImages() async {
guard hasServerAuthentication else { guard hasServerAuthentication else {
log("No server authentication to upload to server") log("No server authentication to upload to server")
@ -981,35 +967,9 @@ final class Database: ObservableObject {
localClassifierUrl.fileSize 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() { func clearImageCache() {
let allImages = cachedImages let imagesToKeep = caps.values.map { $0.image }
let unnecessaryImages = allImages images.clearImageCache(keeping: Set(imagesToKeep))
.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)")
}
}
} }
} }

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
import UIKit import UIKit
final class ImageCache { final class ImageCache: ObservableObject {
let folder: URL let folder: URL
@ -17,6 +17,9 @@ final class ImageCache {
private let imageQuality: CGFloat = 0.3 private let imageQuality: CGFloat = 0.3
@Published
private(set) var imageCount = 0
init(folder: URL, server: URL, thumbnailSize: CGFloat) throws { init(folder: URL, server: URL, thumbnailSize: CGFloat) throws {
self.folder = folder self.folder = folder
self.server = server self.server = server
@ -25,6 +28,7 @@ final class ImageCache {
if !fm.fileExists(atPath: folder.path) { if !fm.fileExists(atPath: folder.path) {
try fm.createDirectory(at: folder, withIntermediateDirectories: true) try fm.createDirectory(at: folder, withIntermediateDirectories: true)
} }
self.imageCount = countLocalImages()
} }
private func localImageUrl(_ image: CapImage) -> URL { private func localImageUrl(_ image: CapImage) -> URL {
@ -92,12 +96,6 @@ final class ImageCache {
return UIImage(at: localUrl) return UIImage(at: localUrl)
} }
@discardableResult
func removeImage(_ image: CapImage) -> Bool {
let localUrl = localImageUrl(image)
return removePossibleFile(localUrl)
}
@discardableResult @discardableResult
func refreshImage(_ image: CapImage) async -> Bool { func refreshImage(_ image: CapImage) async -> Bool {
guard let downloadedImageUrl = await loadRemoteImage(image) else { guard let downloadedImageUrl = await loadRemoteImage(image) else {
@ -122,6 +120,9 @@ final class ImageCache {
for url in files { for url in files {
do { do {
try fm.removeItem(at: url) try fm.removeItem(at: url)
DispatchQueue.main.async {
self.imageCount -= 1
}
} catch { } catch {
isSuccessful = false isSuccessful = false
log("Failed to remove image \(url.lastPathComponent) from cache: \(error)") 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 { private func saveImage(_ image: CapImage, at tempUrl: URL) -> Bool {
let localUrl = localImageUrl(image) let localUrl = localImageUrl(image)
let isOverwrite = exists(localUrl)
guard removePossibleFile(localUrl) else { guard removePossibleFile(localUrl) else {
return false return false
} }
do { do {
try fm.moveItem(at: tempUrl, to: localUrl) try fm.moveItem(at: tempUrl, to: localUrl)
if !isOverwrite {
DispatchQueue.main.async {
self.imageCount += 1
}
}
return true return true
} catch { } catch {
print("failed to save image \(localUrl.lastPathComponent): \(error)") print("Failed to save image \(localUrl.lastPathComponent): \(error)")
return false return false
} }
} }
@ -250,12 +257,64 @@ final class ImageCache {
return false return false
} }
let localUrl = localThumbnailUrl(cap: cap) let localUrl = localThumbnailUrl(cap: cap)
let isOverwrite = exists(localUrl)
do { do {
try data.write(to: localUrl) try data.write(to: localUrl)
if !isOverwrite {
DispatchQueue.main.async {
self.imageCount += 1
}
}
return true return true
} catch { } catch {
print("Failed to save thumbnail \(cap): \(error)") print("Failed to save thumbnail \(cap): \(error)")
return false 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
}
}
} }

View 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)
}

View File

@ -33,8 +33,22 @@ struct SettingsView: View {
.padding(.top) .padding(.top)
Group { Group {
SettingsStatisticRow(label: "Caps", value: "\(database.numberOfCaps)") 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: "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) }.padding(.horizontal)
Text("Classifier") Text("Classifier")
.font(.footnote) .font(.footnote)
@ -44,6 +58,7 @@ struct SettingsView: View {
Group { Group {
SettingsStatisticRow(label: "Server Version", value: "\(database.serverClassifierVersion)") SettingsStatisticRow(label: "Server Version", value: "\(database.serverClassifierVersion)")
SettingsStatisticRow(label: "Local Version", value: "\(database.localClassifierVersion)") SettingsStatisticRow(label: "Local Version", value: "\(database.localClassifierVersion)")
SettingsStatisticRow(label: "Size", value: byteString(database.classifierSize))
SettingsStatisticRow(label: "Recognized caps", value: "\(database.classifierClassCount)") SettingsStatisticRow(label: "Recognized caps", value: "\(database.classifierClassCount)")
HStack { HStack {
Spacer() Spacer()
@ -65,24 +80,6 @@ struct SettingsView: View {
ClassifierDownloadView(progress: progress) ClassifierDownloadView(progress: progress)
} }
}.padding(.horizontal) }.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() Spacer()
} }
.padding(.horizontal) .padding(.horizontal)
@ -109,6 +106,7 @@ struct SettingsView: View {
// Ensure that correct version is saved // Ensure that correct version is saved
await database.updateServerClassifierVersion() await database.updateServerClassifierVersion()
await database.downloadClassifier() await database.downloadClassifier()
await database.downloadClassifierClasses()
} }
} }