Improve cache settings view
This commit is contained in:
parent
d5edf360fb
commit
f192e1c29d
@ -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 */,
|
||||||
|
Binary file not shown.
@ -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)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
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)
|
.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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user