Skip to content

Commit

Permalink
Support for collections in trash (#1013)
Browse files Browse the repository at this point in the history
  • Loading branch information
michalrentka authored Oct 29, 2024
1 parent 2ab1a41 commit b1e9d01
Show file tree
Hide file tree
Showing 50 changed files with 3,152 additions and 1,308 deletions.
94 changes: 86 additions & 8 deletions Zotero.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

61 changes: 0 additions & 61 deletions Zotero/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,44 +58,6 @@ final class AppDelegate: UIResponder {
UserDefaults.standard.removeObject(forKey: "ItemsSortType")
}

/// This migration was created to move from "old" file structure (before build 120) to "new" one, where items are stored with their proper filenames.
/// In `DidMigrateFileStructure` all downloaded items were moved. Items which were up for upload were forgotten, so `DidMigrateFileStructure2` was added to migrate also these items.
/// TODO: - Remove after beta
private func migrateFileStructure(queue: DispatchQueue) {
let didMigrateFileStructure = UserDefaults.standard.bool(forKey: "DidMigrateFileStructure")
let didMigrateFileStructure2 = UserDefaults.standard.bool(forKey: "DidMigrateFileStructure2")

guard !didMigrateFileStructure || !didMigrateFileStructure2 else { return }

guard let dbStorage = self.controllers.userControllers?.dbStorage else {
// If user is logget out, no need to migrate, DB is empty and files should be gone.
UserDefaults.standard.setValue(true, forKey: "DidMigrateFileStructure")
UserDefaults.standard.setValue(true, forKey: "DidMigrateFileStructure2")
return
}

// Migrate file structure
if !didMigrateFileStructure && !didMigrateFileStructure2 {
if let items = try? self.readAttachmentTypes(for: ReadAllDownloadedAndForUploadItemsDbRequest(), dbStorage: dbStorage, queue: queue) {
self.migrateFileStructure(for: items)
}
UserDefaults.standard.setValue(true, forKey: "DidMigrateFileStructure")
UserDefaults.standard.setValue(true, forKey: "DidMigrateFileStructure2")
} else if !didMigrateFileStructure {
if let items = try? self.readAttachmentTypes(for: ReadAllDownloadedItemsDbRequest(), dbStorage: dbStorage, queue: queue) {
self.migrateFileStructure(for: items)
}
UserDefaults.standard.setValue(true, forKey: "DidMigrateFileStructure")
} else if !didMigrateFileStructure2 {
if let items = try? self.readAttachmentTypes(for: ReadAllItemsForUploadDbRequest(), dbStorage: dbStorage, queue: queue) {
self.migrateFileStructure(for: items)
}
UserDefaults.standard.setValue(true, forKey: "DidMigrateFileStructure2")
}

NotificationCenter.default.post(name: .forceReloadItems, object: nil)
}

private func readAttachmentTypes<Request: DbResponseRequest>(for request: Request, dbStorage: DbStorage, queue: DispatchQueue) throws -> [(String, LibraryIdentifier, Attachment.Kind)] where Request.Response == Results<RItem> {
var types: [(String, LibraryIdentifier, Attachment.Kind)] = []

Expand All @@ -113,28 +75,6 @@ final class AppDelegate: UIResponder {
return types
}

private func migrateFileStructure(for items: [(String, LibraryIdentifier, Attachment.Kind)]) {
for (key, libraryId, type) in items {
switch type {
case .url: break
case .file(_, _, _, let linkType, _) where (linkType == .embeddedImage || linkType == .linkedFile): break // Embedded images and linked files don't need to be checked.
case .file(let filename, let contentType, _, let linkType, _):
// Snapshots were stored based on new structure, no need to do anything.
guard linkType != .importedUrl || contentType != "text/html" else { continue }

let filenameParts = filename.split(separator: ".")
let oldFile: File
if filenameParts.count > 1, let ext = filenameParts.last.flatMap(String.init) {
oldFile = FileData(rootPath: Files.appGroupPath, relativeComponents: ["downloads", libraryId.folderName], name: key, ext: ext)
} else {
oldFile = FileData(rootPath: Files.appGroupPath, relativeComponents: ["downloads", libraryId.folderName], name: key, contentType: contentType)
}
let newFile = Files.attachmentFile(in: libraryId, key: key, filename: filename, contentType: contentType)
try? self.controllers.fileStorage.move(from: oldFile, to: newFile)
}
}
}

private func removeFinishedUploadFiles(queue: DispatchQueue) {
let didDeleteFiles = UserDefaults.standard.bool(forKey: "DidDeleteFinishedUploadFiles")

Expand Down Expand Up @@ -287,7 +227,6 @@ extension AppDelegate: UIApplicationDelegate {

let queue = DispatchQueue(label: "org.zotero.AppDelegateMigration", qos: .userInitiated)
queue.async {
self.migrateFileStructure(queue: queue)
self.removeFinishedUploadFiles(queue: queue)
self.updateCreatorSummaryFormat(queue: queue)
}
Expand Down
1 change: 1 addition & 0 deletions Zotero/Assets/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@
"accessibility.items.share" = "Share selected items";
"accessibility.items.download_attachments" = "Download attachments for selected items";
"accessibility.items.remove_downloads" = "Remove downloads for selected items";
"accessibility.items.collection" = "Collection";
"accessibility.item_detail.download_and_open" = "Double tap to download and open";
"accessibility.item_detail.open" = "Double tap to open";
"accessibility.pdf.sidebar_open" = "Open sidebar";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ final class AttachmentDownloader: NSObject {
case ready(compressed: Bool?)
case failed(Swift.Error)
case cancelled

var isProgress: Bool {
switch self {
case .progress:
return true

case .ready, .failed, .cancelled:
return false
}
}
}

let key: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct CreateCollectionDbRequest: DbRequest {
collection.name = self.name
collection.syncState = .synced
collection.libraryId = self.libraryId
collection.updateSortName()

var changes: RCollectionChanges = .name

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ struct EditCollectionDbRequest: DbRequest {

if collection.name != self.name {
collection.name = self.name
collection.updateSortName()
changes.insert(.name)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ struct EmptyTrashDbRequest: DbRequest {
var needsWrite: Bool { return true }

func process(in database: Realm) throws {
database.objects(RItem.self).filter(.items(for: .custom(.trash), libraryId: self.libraryId)).forEach {
database.objects(RItem.self).filter(.items(for: .custom(.trash), libraryId: libraryId)).forEach {
$0.deleted = true
$0.changeType = .user
}
database.objects(RCollection.self).filter(.trashedCollections(in: libraryId)).forEach {
$0.deleted = true
$0.changeType = .user
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// MarkCollectionsAsTrashedDbRequest.swift
// Zotero
//
// Created by Michal Rentka on 18.07.2024.
// Copyright © 2024 Corporation for Digital Scholarship. All rights reserved.
//

import Foundation

import RealmSwift

struct MarkCollectionsAsTrashedDbRequest: DbRequest {
let keys: [String]
let libraryId: LibraryIdentifier
let trashed: Bool

var needsWrite: Bool { return true }

func process(in database: Realm) throws {
let collections = database.objects(RCollection.self).filter(.keys(self.keys, in: self.libraryId))
collections.forEach { item in
item.trash = trashed
item.changeType = .user
item.changes.append(RObjectChange.create(changes: RCollectionChanges.trash))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,30 @@ struct ReadCollectionsDbRequest: DbResponseRequest {

let libraryId: LibraryIdentifier
let excludedKeys: Set<String>
let trash: Bool
let searchTextComponents: [String]

var needsWrite: Bool { return false }

init(libraryId: LibraryIdentifier, excludedKeys: Set<String> = []) {
init(libraryId: LibraryIdentifier, trash: Bool = false, searchTextComponents: [String] = [], excludedKeys: Set<String> = []) {
self.libraryId = libraryId
self.trash = trash
self.excludedKeys = excludedKeys
self.searchTextComponents = searchTextComponents
}

func process(in database: Realm) throws -> Results<RCollection> {
let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [.notSyncState(.dirty, in: self.libraryId),
.deleted(false),
.isTrash(false),
.key(notIn: self.excludedKeys)])
return database.objects(RCollection.self).filter(predicate)
var predicates: [NSPredicate] = [
.notSyncState(.dirty, in: libraryId),
.deleted(false),
.isTrash(trash),
.key(notIn: excludedKeys)
]
if !searchTextComponents.isEmpty {
for component in searchTextComponents {
predicates.append(NSPredicate(format: "name contains[c] %@", component))
}
}
return database.objects(RCollection.self).filter(NSCompoundPredicate(andPredicateWithSubpredicates: predicates))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ struct StoreCollectionsDbRequest: DbRequest {

static func update(collection: RCollection, response: CollectionResponse, libraryId: LibraryIdentifier, database: Realm) {
collection.key = response.key
collection.name = response.data.name
if collection.name != response.data.name {
collection.name = response.data.name
collection.updateSortName()
}
collection.version = response.version
collection.syncState = .synced
collection.syncRetries = 0
Expand Down
2 changes: 1 addition & 1 deletion Zotero/Controllers/IdentifierLookupController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ final class IdentifierLookupController {

setupObservers()
}

// MARK: Actions
func initialize(libraryId: LibraryIdentifier, collectionKeys: Set<String>, completion: @escaping ([LookupData]?) -> Void) {
accessQueue.async(flags: .barrier) { [weak self] in
Expand Down
2 changes: 2 additions & 0 deletions Zotero/Extensions/Localizable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ internal enum L10n {
internal enum Items {
/// Add selected items to collection
internal static let addToCollection = L10n.tr("Localizable", "accessibility.items.add_to_collection", fallback: "Add selected items to collection")
/// Collection
internal static let collection = L10n.tr("Localizable", "accessibility.items.collection", fallback: "Collection")
/// Delete selected items
internal static let delete = L10n.tr("Localizable", "accessibility.items.delete", fallback: "Delete selected items")
/// Deselect All Items
Expand Down
23 changes: 22 additions & 1 deletion Zotero/Models/Database/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import RealmSwift
import Network

struct Database {
private static let schemaVersion: UInt64 = 47
private static let schemaVersion: UInt64 = 49

static func mainConfiguration(url: URL, fileStorage: FileStorage) -> Realm.Configuration {
var config = Realm.Configuration(
Expand Down Expand Up @@ -92,6 +92,27 @@ struct Database {
if schemaVersion < 47 {
setTrashDates(migration: migration)
}
if schemaVersion < 49 {
migrateCollectionsSortName(migration: migration)
migrateTagNames(migration: migration)
migrateItemSortTitle(migration: migration)
}
}
}

private static func migrateItemSortTitle(migration: Migration) {
migration.enumerateObjects(ofType: RItem.className()) { oldObject, newObject in
if let title = oldObject?["displayTitle"] as? String, !title.isEmpty {
newObject?["sortTitle"] = RItem.sortTitle(from: title)
}
}
}

private static func migrateCollectionsSortName(migration: Migration) {
migration.enumerateObjects(ofType: RCollection.className()) { oldObject, newObject in
if let name = oldObject?["name"] as? String, !name.isEmpty {
newObject?["sortName"] = RCollection.sortName(from: name)
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions Zotero/Models/Database/RCollection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ final class RCollection: Object {

@Persisted(indexed: true) var key: String
@Persisted var name: String
@Persisted var sortName: String
@Persisted var dateModified: Date
@Persisted var parentKey: String?
@Persisted var collapsed: Bool = true
Expand Down Expand Up @@ -60,6 +61,17 @@ final class RCollection: Object {
/// Indicates whether the object is trashed locally and needs to be synced with backend
@Persisted var trash: Bool

static func sortName(from name: String) -> String {
return name.folding(options: .diacriticInsensitive, locale: .current).trimmingCharacters(in: CharacterSet(charactersIn: "[]'\"")).lowercased()
}

func updateSortName() {
let newName = RCollection.sortName(from: name)
if newName != sortName {
sortName = newName
}
}

// MARK: - Sync properties

var changedFields: RCollectionChanges {
Expand Down
10 changes: 7 additions & 3 deletions Zotero/Models/Database/RItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,14 @@ final class RItem: Object {
self.updateSortTitle()
}

static func sortTitle(from title: String) -> String {
return title.strippedRichTextTags.folding(options: .diacriticInsensitive, locale: .current).trimmingCharacters(in: CharacterSet(charactersIn: "[]'\"")).lowercased()
}

private func updateSortTitle() {
let newTitle = self.displayTitle.strippedRichTextTags.trimmingCharacters(in: CharacterSet(charactersIn: "[]'\"")).lowercased()
if newTitle != self.sortTitle {
self.sortTitle = newTitle
let newTitle = RItem.sortTitle(from: displayTitle)
if newTitle != sortTitle {
sortTitle = newTitle
}
}

Expand Down
2 changes: 1 addition & 1 deletion Zotero/Models/Database/RTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ final class RTag: Object {
}

static func sortName(from name: String) -> String {
return name.trimmingCharacters(in: CharacterSet(charactersIn: "[]'\"")).lowercased()
return name.folding(options: .diacriticInsensitive, locale: .current).trimmingCharacters(in: CharacterSet(charactersIn: "[]'\"")).lowercased()
}

static func create(name: String, color: String? = nil, libraryId: LibraryIdentifier, order: Int? = nil) -> RTag {
Expand Down
1 change: 0 additions & 1 deletion Zotero/Models/Notifications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,4 @@ extension Notification.Name {
static let attachmentFileDeleted = Notification.Name("org.zotero.AttachmentFileDeleted")
// Sent when attachment (`RItem`) is completely removed from the app (not just trashed). Used to remove attachment files of deleted attachments.
static let attachmentDeleted = Notification.Name(rawValue: "org.zotero.AttachmentsDeleted")
static let forceReloadItems = Notification.Name(rawValue: "org.zotero.ForceReloadItems")
}
16 changes: 9 additions & 7 deletions Zotero/Models/UpdatableObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,28 +51,30 @@ extension Updatable {

extension RCollection: Updatable {
var updateParameters: [String: Any]? {
guard self.isChanged else { return nil }
guard isChanged else { return nil }

var parameters: [String: Any] = ["key": self.key,
"version": self.version]
var parameters: [String: Any] = ["key": key, "version": version]

let changes = self.changedFields
let changes = changedFields
if changes.contains(.name) {
parameters["name"] = self.name
parameters["name"] = name
}
if changes.contains(.parent) {
if let key = self.parentKey {
if let key = parentKey {
parameters["parentCollection"] = key
} else {
parameters["parentCollection"] = false
}
}
if changes.contains(.trash) {
parameters["deleted"] = trash
}

return parameters
}

var selfOrChildChanged: Bool {
return self.isChanged
return isChanged
}

func markAsChanged(in database: Realm) {
Expand Down
Loading

0 comments on commit b1e9d01

Please sign in to comment.