Skip to content

Commit 134bde1

Browse files
committed
smart folders use id of folders instead of name, smart folder migration on startup ViennaRSS#1904
1 parent c516ee1 commit 134bde1

File tree

9 files changed

+136
-16
lines changed

9 files changed

+136
-16
lines changed

Vienna.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
03A131B31AA54EAC0037471F /* Database+Migration.m in Sources */ = {isa = PBXBuildFile; fileRef = 03A131B21AA54EAC0037471F /* Database+Migration.m */; };
5656
03F33D961DF2A2F900B04FAF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 03F33D951DF2A2F900B04FAF /* Assets.xcassets */; };
5757
2F06504C2506B5B500468A0E /* LoadingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F06504B2506B5B500468A0E /* LoadingIndicator.swift */; };
58+
2F1248372DE3447E00C96C40 /* Database+CriteriaMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F1248362DE3447E00C96C40 /* Database+CriteriaMigration.swift */; };
5859
2F125EDE24D0638600868E11 /* BrowserTabWithLegacyAddressBar.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2F125EDD24D0638600868E11 /* BrowserTabWithLegacyAddressBar.xib */; };
5960
2F125EE024D066ED00868E11 /* AsyncHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F125EDF24D066ED00868E11 /* AsyncHelper.swift */; };
6061
2F20FF8F25618627000F68C2 /* WebKitArticleTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F20FF8E25618627000F68C2 /* WebKitArticleTab.swift */; };
@@ -312,6 +313,7 @@
312313
03F33D951DF2A2F900B04FAF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
313314
2A37F4B0FDCFA73011CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = main.m; sourceTree = "<group>"; };
314315
2F06504B2506B5B500468A0E /* LoadingIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIndicator.swift; sourceTree = "<group>"; };
316+
2F1248362DE3447E00C96C40 /* Database+CriteriaMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Database+CriteriaMigration.swift"; sourceTree = "<group>"; };
315317
2F125EDD24D0638600868E11 /* BrowserTabWithLegacyAddressBar.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BrowserTabWithLegacyAddressBar.xib; sourceTree = "<group>"; };
316318
2F125EDF24D066ED00868E11 /* AsyncHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHelper.swift; sourceTree = "<group>"; };
317319
2F20FF8E25618627000F68C2 /* WebKitArticleTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitArticleTab.swift; sourceTree = "<group>"; };
@@ -1319,6 +1321,7 @@
13191321
AA26F4D50604927300FE7994 /* Database.m */,
13201322
03A131B11AA54EAC0037471F /* Database+Migration.h */,
13211323
03A131B21AA54EAC0037471F /* Database+Migration.m */,
1324+
2F1248362DE3447E00C96C40 /* Database+CriteriaMigration.swift */,
13221325
AA36CD7906100692001E33A4 /* Field.h */,
13231326
AA36CD7A06100692001E33A4 /* Field.m */,
13241327
43502865165DE9DF0018EDB7 /* Export.h */,
@@ -1880,6 +1883,7 @@
18801883
435028A8165DE9E00018EDB7 /* Import.m in Sources */,
18811884
435028A9165DE9E00018EDB7 /* InfoPanelController.m in Sources */,
18821885
435028AA165DE9E00018EDB7 /* MessageListView.m in Sources */,
1886+
2F1248372DE3447E00C96C40 /* Database+CriteriaMigration.swift in Sources */,
18831887
435028AB165DE9E00018EDB7 /* NewGroupFolder.m in Sources */,
18841888
F6A4E8481F040D58001C9191 /* PlugInToolbarItemButton.swift in Sources */,
18851889
2F33DB3229244ADB00A9716A /* DoesNotContainPredicateEditorRowTemplate.swift in Sources */,

Vienna/Sources/Alerts/SmartFolder.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ - (IBAction)doSave:(id)sender
321321
[controller selectFolder:self.smartFolderId];
322322
} else {
323323
[Database.sharedManager updateSearchFolder:self.smartFolderId
324-
withFolder:folderName
324+
withNewFolderName:folderName
325325
withQuery:criteriaTree];
326326
}
327327

Vienna/Sources/Criteria/Criteria+NSPredicate.swift

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,11 @@ extension Criteria: PredicateConvertible {
146146

147147
convenience init?(predicate: NSComparisonPredicate, notContains: Bool) {
148148
let field = predicate.leftExpression.constantValue as? String ?? predicate.leftExpression.keyPath
149-
let value = predicate.rightExpression.constantValue as? String ?? predicate.rightExpression.keyPath
149+
let value = if field != MA_Field_Folder {
150+
predicate.rightExpression.constantValue as? String ?? predicate.rightExpression.keyPath
151+
} else {
152+
Criteria.convertIndentedFolderNameToFolderId(predicate.rightExpression.constantValue as? String ?? "", on: Database.shared)
153+
}
150154
var fallback = false
151155

152156
let criteriaOperator: CriteriaOperator
@@ -225,6 +229,16 @@ extension Criteria: PredicateConvertible {
225229
return buildPredicate()
226230
}
227231

232+
class func convertIndentedFolderNameToFolderId(_ value: String, on database: Database) -> String {
233+
// indentation of subfolders that was added for visual purposes has to be removed
234+
let folderName = String(value.drop { $0 == " " })
235+
guard let folder = database.folder(fromName: folderName) else {
236+
// no error, since it is possible that a folder was deleted without considering the smart folder referencing it
237+
return ""
238+
}
239+
return "\(folder.itemId)"
240+
}
241+
228242
private func buildPredicate() -> NSPredicate {
229243

230244
let field = self.field
@@ -283,25 +297,31 @@ extension Criteria: PredicateConvertible {
283297

284298
private func buildComparisonPredicate(_ field: String, _ value: String, _ operatorType: CriteriaOperator) -> NSPredicate {
285299
let left = NSExpression(forConstantValue: field)
286-
let right = NSExpression(forConstantValue: value)
287300

288-
let type: NSComparisonPredicate.Operator = convertOperatorTypeForComparisonPredicate(operatorType)
301+
let expressionValue: String
302+
if field == MA_Field_Folder {
303+
expressionValue = convertFolderIdToIndentedName(value)
304+
} else {
305+
expressionValue = value
306+
}
289307

290308
let comparisonPredicate: NSComparisonPredicate
291309

292310
// TODO: constants for fixed criteria values also for Criteria+SQL,
293311
// e.g. YES, NO, yesterday, today, last week, ...
294312

295313
if (field == MA_Field_LastUpdate || field == MA_Field_PublicationDate)
296-
&& operatorType == .after && value == DateOffset.yesterday.rawValue {
314+
&& operatorType == .after && expressionValue == DateOffset.yesterday.rawValue {
297315
// Use canonical "is today" instead of "is after yesterday"
298316
comparisonPredicate = NSComparisonPredicate(leftExpression: left, rightExpression: NSExpression(forConstantValue: DateOffset.today), modifier: .direct, type: .equalTo)
299-
} else if operatorType == .notEqualTo && (value == "No" || value == "Yes") {
317+
} else if operatorType == .notEqualTo && (expressionValue == "No" || expressionValue == "Yes") {
300318
// Use canonical "is yes / is no" representation instead of allowing
301319
// ambiguous "is not yes - is no / is not no - is yes"
302-
let invertedRight = NSExpression(forConstantValue: value == "No" ? "Yes" : "No")
320+
let invertedRight = NSExpression(forConstantValue: expressionValue == "No" ? "Yes" : "No")
303321
comparisonPredicate = NSComparisonPredicate(leftExpression: left, rightExpression: invertedRight, modifier: .direct, type: .equalTo)
304322
} else {
323+
let right = NSExpression(forConstantValue: expressionValue)
324+
let type: NSComparisonPredicate.Operator = convertOperatorTypeForComparisonPredicate(operatorType)
305325
comparisonPredicate = NSComparisonPredicate(leftExpression: left, rightExpression: right, modifier: .direct, type: type)
306326
}
307327

@@ -313,4 +333,22 @@ extension Criteria: PredicateConvertible {
313333
return comparisonPredicate
314334
}
315335
}
336+
337+
private func convertFolderIdToIndentedName(_ value: String) -> String {
338+
guard let database = Database.shared, let folderId = Int(value) else {
339+
fatalError("Database not available or folder name was not converted to id")
340+
}
341+
guard let folder = Database.shared.folder(fromID: folderId) else {
342+
// no error, since it is possible that a folder was deleted without considering the smart folder referencing it
343+
return ""
344+
}
345+
346+
var parentFolder = database.folder(fromID: folder.parentId)
347+
var indent = 0
348+
while parentFolder != nil {
349+
indent += 1
350+
parentFolder = database.folder(fromID: parentFolder?.parentId ?? 0)
351+
}
352+
return String(repeating: " ", count: indent) + folder.name
353+
}
316354
}

Vienna/Sources/Criteria/Criteria+SQL.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,11 @@ extension Criteria: SQLConversion {
149149
}
150150

151151
func folderSqlString(sqlFieldName: String, database: Database) -> String {
152-
let folder = database.folder(fromName: value)
152+
guard let value = Int(value) else {
153+
fatalError("Value should have been migrated to folder id")
154+
}
155+
let folder = database.folder(fromID: Int(value))
156+
153157
let scope: QueryScope
154158
switch operatorType {
155159
case .under:

Vienna/Sources/Criteria/Criteria.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ protocol CriteriaElement: PredicateConvertible {
9090

9191
// Workaround while this variable is needed for objc, due to generics in traverse function
9292
protocol Traversable: CriteriaElement {
93+
/// traversal for conversion
9394
func traverse<ResultType>(treeConversion: (_ element: CriteriaTree, _ subresult: [ResultType]) -> ResultType, criteriaConversion: (_ element: Criteria) -> ResultType) -> ResultType
95+
/// traversal for modification
96+
func traverse(treeModifier: ((_ element: CriteriaTree) -> Void)?, criteriaModifier: ((_ element: Criteria) -> Void)?)
9497
}
9598

9699
@objc
@@ -130,6 +133,17 @@ class CriteriaTree: NSObject, Traversable {
130133
}
131134
return treeConversion(self, subresult)
132135
}
136+
137+
func traverse(treeModifier: ((CriteriaTree) -> Void)?, criteriaModifier: ((Criteria) -> Void)?) {
138+
//traversion to modify is the same as traversion for conversion with implicit modification while discarding conversion result
139+
_ = traverse { criteriaTree, _ in
140+
treeModifier?(criteriaTree)
141+
return ""
142+
} criteriaConversion: { criteria in
143+
criteriaModifier?(criteria)
144+
return ""
145+
}
146+
}
133147
}
134148

135149
@objc
@@ -149,4 +163,8 @@ class Criteria: NSObject, Traversable {
149163
func traverse<ResultType>(treeConversion: (_ element: CriteriaTree, _ subresult: [ResultType]) -> ResultType, criteriaConversion: (_ element: Criteria) -> ResultType) -> ResultType {
150164
return criteriaConversion(self)
151165
}
166+
167+
func traverse(treeModifier: ((CriteriaTree) -> Void)?, criteriaModifier: ((Criteria) -> Void)?) {
168+
criteriaModifier?(self)
169+
}
152170
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// Database+CriteriaMigration.swift
3+
// Vienna
4+
//
5+
// Copyright 2025 Tassilo Karge
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// https://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import Foundation
21+
22+
extension Database {
23+
@objc
24+
func migrateCriteria(from version: Int) {
25+
switch version + 1 {
26+
case 0..<27: //xml migration did not exist yet, but we could hit this case if someone jumps a major version
27+
NSLog("Migration of xml for version < 27")
28+
fallthrough
29+
case 27: //migrate from folder names to folder ids
30+
guard let smartfoldersDict = self.smartfoldersDict as? [Int: CriteriaTree] else {
31+
NSLog("Criteria conversion to version 27 failed")
32+
return
33+
}
34+
for (folderId, criteriaTree) in smartfoldersDict {
35+
36+
criteriaTree.traverse(treeModifier: { _ in }, criteriaModifier: { criteria in
37+
if criteria.field == MA_Field_Folder {
38+
criteria.value = Criteria.convertIndentedFolderNameToFolderId(criteria.value, on: self)
39+
}
40+
})
41+
self.updateSearchFolder(folderId, withNewFolderName: nil, withQuery: criteriaTree)
42+
}
43+
44+
NSLog("Updated smart folder xmls to version 27")
45+
fallthrough
46+
default:
47+
break
48+
}
49+
}
50+
}

Vienna/Sources/Database/Database+Migration.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ + (void)migrateDatabase:(FMDatabase *)database
259259
database.userVersion = (uint32_t)26;
260260
NSLog(@"Updated database schema to version 26.");
261261
}
262+
case 27: //change smart folder search strings from name to id, happens in Database+CriteriaMigration
263+
database.userVersion = (uint32_t)27;
264+
NSLog(@"Updated database schema to version 27.");
262265
}
263266
}
264267

Vienna/Sources/Database/Database.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ extern NSNotificationName const VNADatabaseDidDeleteFolderNotification;
8181
* @return An OpenReaderFolder that corresponds
8282
*/
8383
-(Folder *)folderFromRemoteId:(NSString *)wantedRemoteId;
84-
-(Folder *)folderFromName:(NSString *)wantedName;
84+
-(Folder * _Nullable)folderFromName:(NSString *)wantedName;
8585
/*!
8686
* folderForPredicateFormat
8787
* Returns a smart folder for the predicate format string.
@@ -124,8 +124,9 @@ extern NSNotificationName const VNADatabaseDidDeleteFolderNotification;
124124
subscriptionURL:(NSString *)url remoteId:(NSString *)remoteId;
125125

126126
// Smart folder functions
127+
@property (nonatomic) NSMutableDictionary<NSNumber *, CriteriaTree *> *smartfoldersDict;
127128
-(NSInteger)addSmartFolder:(NSString *)folderName underParent:(NSInteger)parentId withQuery:(CriteriaTree *)criteriaTree;
128-
-(void)updateSearchFolder:(NSInteger)folderId withFolder:(NSString *)folderName withQuery:(CriteriaTree *)criteriaTree;
129+
-(void)updateSearchFolder:(NSInteger)folderId withNewFolderName:(NSString *)folderName withQuery:(CriteriaTree *)criteriaTree;
129130
-(CriteriaTree *)searchStringForSmartFolder:(NSInteger)folderId;
130131

131132
// Article functions

Vienna/Sources/Database/Database.m

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ @interface Database ()
4141
@property (nonatomic) NSMutableArray *fieldsOrdered;
4242
@property (nonatomic) NSMutableDictionary *fieldsByName;
4343
@property (nonatomic) NSMutableDictionary *foldersDict;
44-
@property (nonatomic) NSMutableDictionary<NSNumber *, CriteriaTree *> *smartfoldersDict;
4544
@property (readwrite, nonatomic) BOOL readOnly;
4645
@property (readwrite, nonatomic) NSInteger countOfUnread;
4746

@@ -57,7 +56,7 @@ + (NSString *)databasePath;
5756

5857
// The current database version number
5958
static NSInteger const VNAMinimumSupportedDatabaseVersion = 12;
60-
static NSInteger const VNACurrentDatabaseVersion = 26;
59+
static NSInteger const VNACurrentDatabaseVersion = 27;
6160

6261
@implementation Database
6362

@@ -178,7 +177,9 @@ - (BOOL)initialiseDatabase {
178177
// TODO: move this into transaction so we can rollback on failure
179178
[Database migrateDatabase:db fromVersion:databaseVersion];
180179
}];
181-
180+
181+
[self migrateCriteriaFrom:databaseVersion];
182+
182183
// Confirm the database is now at the correct version
183184
if (self.databaseVersion == VNACurrentDatabaseVersion) {
184185
return YES;
@@ -1431,8 +1432,9 @@ -(Folder *)folderFromID:(NSInteger)wantedId
14311432
/* folderFromName
14321433
* Retrieve a Folder given its name.
14331434
*/
1434-
-(Folder *)folderFromName:(NSString *)wantedName
1435+
-(Folder * _Nullable)folderFromName:(NSString *)wantedName
14351436
{
1437+
[self initFolderArray];
14361438
Folder * folder;
14371439
for (folder in [self.foldersDict objectEnumerator]) {
14381440
if ([folder.name isEqualToString:wantedName]) {
@@ -1856,10 +1858,10 @@ -(NSInteger)addSmartFolder:(NSString *)folderName underParent:(NSInteger)parentI
18561858
/* updateSearchFolder
18571859
* Updates the search string for the specified folder.
18581860
*/
1859-
-(void)updateSearchFolder:(NSInteger)folderId withFolder:(NSString *)folderName withQuery:(CriteriaTree *)criteriaTree
1861+
-(void)updateSearchFolder:(NSInteger)folderId withNewFolderName:(nullable NSString *)folderName withQuery:(CriteriaTree *)criteriaTree
18601862
{
18611863
Folder * folder = [self folderFromID:folderId];
1862-
if (![folder.name isEqualToString:folderName]) {
1864+
if (folderName && ![folder.name isEqualToString:folderName]) {
18631865
[self setName:folderName forFolder:folderId];
18641866
}
18651867

0 commit comments

Comments
 (0)