diff options
Diffstat (limited to 'QSThingsPlugin')
24 files changed, 852 insertions, 54 deletions
diff --git a/QSThingsPlugin/Core Extensions/NSImage+Badge.h b/QSThingsPlugin/Core Extensions/NSImage+Badge.h new file mode 100644 index 0000000..0882620 --- /dev/null +++ b/QSThingsPlugin/Core Extensions/NSImage+Badge.h @@ -0,0 +1,18 @@ +// +// NSImage+NSImage_Badges.h +// Things Plugin +// +// Created by Ruben Beltran del Rio on 2025-09-12. +// + +#import <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface NSImage (Badge) + +- (NSImage *)imageOfSize:(NSSize)size withSystemBadge:(NSString *)symbolName ofColor:(NSColor *)color; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QSThingsPlugin/Core Extensions/NSImage+Badge.m b/QSThingsPlugin/Core Extensions/NSImage+Badge.m new file mode 100644 index 0000000..e5a2bf1 --- /dev/null +++ b/QSThingsPlugin/Core Extensions/NSImage+Badge.m @@ -0,0 +1,64 @@ +// +// NSImage+NSImage_Badges.h +// Things Plugin +// +// Created by Ruben Beltran del Rio on 2025-09-12. +// + +#import "NSImage+Badge.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation NSImage (Badge) + +- (NSImage *)imageOfSize:(NSSize)size withSystemBadge:(NSString *)symbolName ofColor:(NSColor *)color{ + if (@available(macOS 12.0, *)) { + NSImageRep *baseRep = [self bestRepresentationForRect:NSMakeRect(0, 0, size.width, size.height) + context:nil + hints:nil]; + + NSImage *badgeImage = [NSImage imageWithSystemSymbolName:symbolName + accessibilityDescription:nil]; + [badgeImage setTemplate:YES]; + NSArray *colors = [NSArray arrayWithObjects:color, nil]; + NSImageSymbolConfiguration *iconConfig = [NSImageSymbolConfiguration configurationWithPaletteColors:colors]; + + // Create composite image at the requested size + NSImage *compositImage = [[NSImage alloc] initWithSize:size]; + + [compositImage lockFocus]; + + // Draw the base image + [baseRep drawInRect:NSMakeRect(0, 0, size.width, size.height)]; + + // Calculate badge position (bottom right corner) + CGFloat badgeSize = size.width * 0.4; + CGFloat badgeX = size.width - badgeSize; + CGFloat badgeY = 0; + NSRect badgeRect = NSMakeRect(badgeX, badgeY, badgeSize, badgeSize); + + // Draw white circle background + [[NSColor whiteColor] setFill]; + NSBezierPath *circlePath = [NSBezierPath bezierPathWithOvalInRect:badgeRect]; + [circlePath fill]; + + // Draw the SF Symbol in blue + [[NSColor blueColor] setFill]; + [[badgeImage imageWithSymbolConfiguration:iconConfig] drawInRect:badgeRect]; + + [compositImage unlockFocus]; + + // Preserve template status if applicable + if ([self isTemplate]) { + [compositImage setTemplate:YES]; + } + + return compositImage; + } else { + return nil; + } + +} +@end + +NS_ASSUME_NONNULL_END diff --git a/QSThingsPlugin/Handlers/ThingsAppSectionHandler.h b/QSThingsPlugin/Handlers/ThingsAppSectionHandler.h new file mode 100644 index 0000000..6fc3f35 --- /dev/null +++ b/QSThingsPlugin/Handlers/ThingsAppSectionHandler.h @@ -0,0 +1,14 @@ +#import "Constants.h" +#import "SQLHelper.h" +#import "ObjectHelper.h" +#import "NSImage+Badge.h" +#import <Foundation/Foundation.h> +#import <QSCore/QSCore.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface ThingsAppSectionHandler : NSObject <QSObjectHandler> + +@end + +NS_ASSUME_NONNULL_END diff --git a/QSThingsPlugin/Handlers/ThingsAppSectionHandler.m b/QSThingsPlugin/Handlers/ThingsAppSectionHandler.m new file mode 100644 index 0000000..0f294fc --- /dev/null +++ b/QSThingsPlugin/Handlers/ThingsAppSectionHandler.m @@ -0,0 +1,190 @@ +#import "ThingsAppSectionHandler.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation ThingsAppSectionHandler + +- (BOOL)objectHasChildren:(QSObject *)object { + return YES; +} + +- (void)setQuickIconForObject:(QSObject *)object { + NSImage *baseImage = [QSResourceManager imageNamed:@"com.culturedcode.ThingsMac"]; + + ThingsAppSectionKey identifier = [object identifier]; + NSImage *imageWithBadge = nil; + if (identifier == ThingsAppSectionKeyInbox) { + NSColor *color = [NSColor colorWithRed:0.318 green:0.667 blue:0.949 alpha:1.0]; + imageWithBadge = [baseImage imageOfSize:NSMakeSize(512, 512) withSystemBadge:@"tray.circle" ofColor:color]; + } + if (identifier == ThingsAppSectionKeyToday) { + NSColor *color = [NSColor colorWithRed:0.973 green:0.839 blue:0.282 alpha:1.0]; + imageWithBadge = [baseImage imageOfSize:NSMakeSize(512, 512) withSystemBadge:@"star.circle" ofColor:color]; + } + if (identifier == ThingsAppSectionKeyAnytime) { + NSColor *color = [NSColor colorWithRed:0.353 green:0.651 blue:0.616 alpha:1.0]; + imageWithBadge = [baseImage imageOfSize:NSMakeSize(512, 512) withSystemBadge:@"diamond.circle" ofColor:color]; + } + if (identifier == ThingsAppSectionKeySomeday) { + NSColor *color = [NSColor colorWithRed:0.792 green:0.749 blue:0.525 alpha:1.0]; + imageWithBadge = [baseImage imageOfSize:NSMakeSize(512, 512) withSystemBadge:@"archivebox.circle" ofColor:color]; + } + if (identifier == ThingsAppSectionKeyUpcoming) { + NSColor *color = [NSColor colorWithRed:0.902 green:0.224 blue:0.349 alpha:1.0]; + imageWithBadge = [baseImage imageOfSize:NSMakeSize(512, 512) withSystemBadge:@"calendar.circle" ofColor:color]; + } + + if (imageWithBadge) { + [object setIcon:imageWithBadge]; + } else { + [object setIcon:baseImage]; + } +} + +- (BOOL)loadChildrenForObject:(QSObject *)object { + NSDictionary *contents = [object objectForType:kAppSectionType]; + + if (!contents) { + NSLog(@"Malformed Things Task: No content."); + return NO; + } + + NSString *path = [contents objectForKey:@"path"]; + if (!path) { + NSLog(@"Malformed Things Task: No path."); + return NO; + } + + ThingsAppSectionKey appSection = [object identifier]; + if (!appSection) { + NSLog(@"Malformed Things App Section: No identifier."); + return NO; + } + + NSMutableArray *children = [NSMutableArray array]; + [children addObjectsFromArray:[self tasksForAppSection:appSection usingDatabaseAt:path ]]; + [children addObjectsFromArray:[self projectsForAppSection:appSection usingDatabaseAt:path ]]; + + [object setChildren:children]; + return YES; +} + +- (NSArray *)projectsForAppSection:(ThingsAppSectionKey)appSection usingDatabaseAt:(NSString *)path { + + NSString *conditions = [self conditionsFor:appSection]; + + NSString *query = [NSString stringWithFormat: @"SELECT " + "task.uuid AS uuid, " + "task.creationDate AS creationDate, " + "task.status AS status, " + "task.title AS title, " + "task.notes AS notes, " + "task.area AS area " + "FROM TMTask as task " + "LEFT JOIN TMTask AS project " + "ON task.project = project.uuid " + "LEFT JOIN TMTask AS heading " + "ON task.heading = heading.uuid " + "LEFT JOIN TMTask AS headingProject " + "ON heading.project = headingProject.uuid " + "WHERE task.type = 1 " + "AND task.trashed = 0 " + "AND task.status = 0 " + "%@" + "GROUP BY task.uuid " + "ORDER BY title;", conditions]; + + NSArray *results = [SQLHelper executeSql:query onFile:path]; + NSMutableArray *objects = [NSMutableArray array]; + [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { + NSLog(@"WE PROJECTIN!"); + [objects addObject:[ObjectHelper projectFromSQLResult:dictionary usingPath:path ]]; + }]; + + return objects; +} + +- (NSArray *)tasksForAppSection:(ThingsAppSectionKey)appSection usingDatabaseAt:(NSString *)path { + + NSString *conditions = [self conditionsFor:appSection]; + + NSString *query = [NSString stringWithFormat: @"SELECT " + "task.uuid AS uuid, " + "task.creationDate AS creationDate, " + "task.status AS status, " + "task.title AS title, " + "task.notes AS notes, " + "task.area AS area " + "FROM TMTask as task " + "LEFT JOIN TMTask AS project " + "ON task.project = project.uuid " + "LEFT JOIN TMTask AS heading " + "ON task.heading = heading.uuid " + "LEFT JOIN TMTask AS headingProject " + "ON heading.project = headingProject.uuid " + "WHERE task.type = 0 " + "AND task.trashed = 0 " + "AND task.status = 0 " + "%@" + "GROUP BY task.uuid " + "ORDER BY title;", conditions]; + + NSArray *results = [SQLHelper executeSql:query onFile:path]; + NSMutableArray *objects = [NSMutableArray array]; + [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { + [objects addObject:[ObjectHelper taskFromSQLResult:dictionary usingPath:path ]]; + }]; + + return objects; +} + +- (NSString *)conditionsFor:(ThingsAppSectionKey)appSection { + if (appSection == ThingsAppSectionKeyInbox) { + return @"AND task.startDate IS NULL " + "AND task.area IS NULL " + "AND task.project IS NULL " + "AND task.heading IS NULL "; + } + if (appSection == ThingsAppSectionKeyToday) { + return @"AND task.todayIndexReferenceDate IS NOT NULL " + "AND task.todayIndex <= 0 "; + } + if (appSection == ThingsAppSectionKeyUpcoming) { + return @"AND task.todayIndexReferenceDate IS NOT NULL " + "AND task.todayIndex > 0 "; + } + if (appSection == ThingsAppSectionKeySomeday) { + return @"AND task.start=2 " + "AND task.startDate IS NULL " + "AND task.rt1_instanceCreationStartDate IS NULL "; + } + if (appSection == ThingsAppSectionKeyAnytime) { + return @"AND ( " + "( " + "task.todayIndexReferenceDate IS NOT NULL " + "AND task.todayIndex <= 0 " + ") " + "OR ( " + "task.startDate IS NULL AND " + "task.start=1 AND " + "task.rt1_instanceCreationStartDate IS NULL " + "AND ( " + "task.area IS NOT NULL " + "OR task.project IS NOT NULL " + "OR task.heading IS NOT NULL " + ") " + "AND project.todayIndexReferenceDate IS NULL " + "AND (project.start=1 OR headingProject.start IS NULL) " + "AND heading.todayIndexReferenceDate IS NULL " + "AND (heading.start=1 OR headingProject.start IS NULL) " + "AND headingProject.todayIndexReferenceDate IS NULL " + "AND (headingProject.start=1 OR headingProject.start IS NULL) " + ") " + ") "; + } +} + + +@end + +NS_ASSUME_NONNULL_END diff --git a/QSThingsPlugin/Handlers/ThingsAreaHandler.h b/QSThingsPlugin/Handlers/ThingsAreaHandler.h new file mode 100644 index 0000000..14dd5ad --- /dev/null +++ b/QSThingsPlugin/Handlers/ThingsAreaHandler.h @@ -0,0 +1,13 @@ +#import "Constants.h" +#import "SQLHelper.h" +#import "ObjectHelper.h" +#import "NSImage+Badge.h" +#import <Foundation/Foundation.h> +#import <QSCore/QSCore.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface ThingsAreaHandler : NSObject <QSObjectHandler> +@end + +NS_ASSUME_NONNULL_END diff --git a/QSThingsPlugin/Handlers/ThingsAreaHandler.m b/QSThingsPlugin/Handlers/ThingsAreaHandler.m new file mode 100644 index 0000000..6619a0e --- /dev/null +++ b/QSThingsPlugin/Handlers/ThingsAreaHandler.m @@ -0,0 +1,124 @@ +#import "ThingsAreaHandler.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation ThingsAreaHandler + +- (BOOL)objectHasChildren:(QSObject *)object { + return YES; +} + +- (void)setQuickIconForObject:(QSObject *)object { + NSImage *baseImage = [QSResourceManager imageNamed:@"com.culturedcode.ThingsMac"]; + NSColor *color = [NSColor colorWithRed:0.478 green:0.702 blue:0.584 alpha:1.0]; + NSImage *imageWithBadge = [baseImage imageOfSize:NSMakeSize(512, 512) withSystemBadge:@"cube.circle" ofColor:color]; + + if (imageWithBadge) { + [object setIcon:imageWithBadge]; + } else { + [object setIcon:baseImage]; + } +} + +- (BOOL)loadChildrenForObject:(QSObject *)object { + NSDictionary *contents = [object objectForType:kAreaType]; + + if (!contents) { + NSLog(@"Malformed Things Task: No content."); + return NO; + } + + NSString *path = [contents objectForKey:@"path"]; + if (!path) { + NSLog(@"Malformed Things Task: No path."); + return NO; + } + + NSString *uuid = [contents objectForKey:@"uuid"]; + if (!uuid) { + NSLog(@"Malformed Things Task: No uuid."); + return NO; + } + + NSMutableArray *children = [NSMutableArray array]; + [children addObjectsFromArray:[self tagsForAreaIdentifiedBy:uuid usingDatabaseAt:path ]]; + [children addObjectsFromArray:[self tasksForAreaIdentifiedBy:uuid usingDatabaseAt:path ]]; + [children addObjectsFromArray:[self projectsForAreaIdentifiedBy:uuid usingDatabaseAt:path ]]; + + [object setChildren:children]; + return YES; +} + +- (NSArray *)tagsForAreaIdentifiedBy:(NSString *)uuid usingDatabaseAt:(NSString *)path { + NSString *query = [NSString stringWithFormat: @"SELECT " + "tag.uuid as uuid, " + "tag.title as title " + "FROM TMArea AS area " + "LEFT JOIN TMAreaTag as junction " + "ON junction.areas = area.uuid " + "LEFT JOIN TMTag as tag " + "ON junction.tags = tag.uuid " + "WHERE area.uuid = '%@' " + "AND tag.uuid IS NOT NULL " + "ORDER BY tag.title;", uuid]; + + NSArray *results = [SQLHelper executeSql:query onFile:path]; + NSMutableArray *objects = [NSMutableArray array]; + [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { + [objects addObject:[ObjectHelper tagFromSQLResult:dictionary usingPath:path ]]; + }]; + + return objects; +} + +- (NSArray *)projectsForAreaIdentifiedBy:(NSString *)uuid usingDatabaseAt:(NSString *)path { + NSString *query = [NSString stringWithFormat: @"SELECT " + "uuid, " + "creationDate, " + "status, " + "title, " + "notes, " + "area " + "FROM TMTask " + "WHERE area = '%@' " + "AND type = 1 " + "AND trashed = 0 " + "AND status = 0 " + "ORDER BY title;", uuid]; + + NSArray *results = [SQLHelper executeSql:query onFile:path]; + NSMutableArray *objects = [NSMutableArray array]; + [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { + [objects addObject:[ObjectHelper projectFromSQLResult:dictionary usingPath:path ]]; + }]; + + return objects; +} + +- (NSArray *)tasksForAreaIdentifiedBy:(NSString *)uuid usingDatabaseAt:(NSString *)path { + NSString *query = [NSString stringWithFormat: @"SELECT " + "uuid, " + "creationDate, " + "status, " + "title, " + "notes, " + "area " + "FROM TMTask " + "WHERE area = '%@' " + "AND type = 0 " + "AND trashed = 0 " + "AND status = 0 " + "ORDER BY title;", uuid]; + + NSArray *results = [SQLHelper executeSql:query onFile:path]; + NSMutableArray *objects = [NSMutableArray array]; + [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { + [objects addObject:[ObjectHelper taskFromSQLResult:dictionary usingPath:path ]]; + }]; + + return objects; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/QSThingsPlugin/Handlers/ThingsTagHandler.h b/QSThingsPlugin/Handlers/ThingsTagHandler.h new file mode 100644 index 0000000..6ecf432 --- /dev/null +++ b/QSThingsPlugin/Handlers/ThingsTagHandler.h @@ -0,0 +1,14 @@ +#import "Constants.h" +#import "SQLHelper.h" +#import "ObjectHelper.h" +#import "NSImage+Badge.h" +#import <Foundation/Foundation.h> +#import <QSCore/QSCore.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface ThingsTagHandler : NSObject <QSObjectHandler> + +@end + +NS_ASSUME_NONNULL_END diff --git a/QSThingsPlugin/Handlers/ThingsTagHandler.m b/QSThingsPlugin/Handlers/ThingsTagHandler.m new file mode 100644 index 0000000..27b93a2 --- /dev/null +++ b/QSThingsPlugin/Handlers/ThingsTagHandler.m @@ -0,0 +1,134 @@ +#import "ThingsTagHandler.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation ThingsTagHandler + +- (BOOL)objectHasChildren:(QSObject *)object { + return YES; +} + +- (void)setQuickIconForObject:(QSObject *)object { + NSImage *baseImage = [QSResourceManager imageNamed:@"com.culturedcode.ThingsMac"]; + NSColor *color = [NSColor colorWithRed:0.263 green:0.502 blue:0.376 alpha:1.0]; + NSImage *imageWithBadge = [baseImage imageOfSize:NSMakeSize(512, 512) withSystemBadge:@"tag.circle" ofColor:color]; + + if (imageWithBadge) { + [object setIcon:imageWithBadge]; + } else { + [object setIcon:baseImage]; + } +} + +- (BOOL)loadChildrenForObject:(QSObject *)object { + NSDictionary *contents = [object objectForType:kTagType]; + + if (!contents) { + NSLog(@"Malformed Things Task: No content."); + return NO; + } + + NSString *path = [contents objectForKey:@"path"]; + if (!path) { + NSLog(@"Malformed Things Task: No path."); + return NO; + } + + NSString *uuid = [contents objectForKey:@"uuid"]; + if (!uuid) { + NSLog(@"Malformed Things Task: No uuid."); + return NO; + } + + NSMutableArray *children = [NSMutableArray array]; + [children addObjectsFromArray:[self areasForTagIdentifiedBy:uuid usingDatabaseAt:path ]]; + [children addObjectsFromArray:[self projectsForTagIdentifiedBy:uuid usingDatabaseAt:path ]]; + [children addObjectsFromArray:[self tasksForTagIdentifiedBy:uuid usingDatabaseAt:path ]]; + + [object setChildren:children]; + return YES; +} + +- (NSArray *)projectsForTagIdentifiedBy:(NSString *)uuid usingDatabaseAt:(NSString *)path { + NSString *query = [NSString stringWithFormat: @"SELECT " + "project.uuid as uuid, " + "project.creationDate as creationDate, " + "project.status as status, " + "project.title as title, " + "project.notes as notes, " + "project.area as area " + "FROM TMTag AS tag " + "LEFT JOIN TMTaskTag as junction " + "ON junction.tags = tag.uuid " + "LEFT JOIN TMTask as project " + "ON junction.tasks = project.uuid " + "WHERE tag.uuid = '%@' " + "AND project.uuid IS NOT NULL " + "AND project.type = 1 " + "AND project.status = 0 " + "AND project.trashed = 0 " + "ORDER BY project.title;", uuid]; + + NSArray *results = [SQLHelper executeSql:query onFile:path]; + NSMutableArray *objects = [NSMutableArray array]; + [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { + [objects addObject:[ObjectHelper projectFromSQLResult:dictionary usingPath:path ]]; + }]; + + return objects; +} + +- (NSArray *)tasksForTagIdentifiedBy:(NSString *)uuid usingDatabaseAt:(NSString *)path { + NSString *query = [NSString stringWithFormat: @"SELECT " + "task.uuid as uuid, " + "task.creationDate as creationDate, " + "task.status as status, " + "task.title as title, " + "task.notes as notes, " + "task.area as area " + "FROM TMTag AS tag " + "LEFT JOIN TMTaskTag as junction " + "ON junction.tags = tag.uuid " + "LEFT JOIN TMTask as task " + "ON junction.tasks = task.uuid " + "WHERE tag.uuid = '%@' " + "AND task.uuid IS NOT NULL " + "AND task.type = 0 " + "AND task.status = 0 " + "AND task.trashed = 0 " + "ORDER BY task.title;", uuid]; + + NSArray *results = [SQLHelper executeSql:query onFile:path]; + NSMutableArray *objects = [NSMutableArray array]; + [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { + [objects addObject:[ObjectHelper taskFromSQLResult:dictionary usingPath:path ]]; + }]; + + return objects; +} + +- (NSArray *)areasForTagIdentifiedBy:(NSString *)uuid usingDatabaseAt:(NSString *)path { + NSString *query = [NSString stringWithFormat: @"SELECT " + "area.uuid as uuid, " + "area.title as title " + "FROM TMTag AS tag " + "LEFT JOIN TMAreaTag as junction " + "ON junction.tags = tag.uuid " + "LEFT JOIN TMArea as area " + "ON junction.areas = area.uuid " + "WHERE tag.uuid = '%@' " + "AND area.uuid IS NOT NULL " + "ORDER BY area.title;", uuid]; + + NSArray *results = [SQLHelper executeSql:query onFile:path]; + NSMutableArray *objects = [NSMutableArray array]; + [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { + [objects addObject:[ObjectHelper areaFromSQLResult:dictionary usingPath:path ]]; + }]; + + return objects; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/QSThingsPlugin/Handlers/ThingsTaskHandler.h b/QSThingsPlugin/Handlers/ThingsTaskHandler.h new file mode 100644 index 0000000..b32e527 --- /dev/null +++ b/QSThingsPlugin/Handlers/ThingsTaskHandler.h @@ -0,0 +1,14 @@ +#import "Constants.h" +#import "SQLHelper.h" +#import "ObjectHelper.h" +#import "NSImage+Badge.h" +#import <Foundation/Foundation.h> +#import <QSCore/QSCore.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface ThingsTaskHandler : NSObject <QSObjectHandler> + +@end + +NS_ASSUME_NONNULL_END diff --git a/QSThingsPlugin/Handlers/ThingsTaskHandler.m b/QSThingsPlugin/Handlers/ThingsTaskHandler.m new file mode 100644 index 0000000..5875feb --- /dev/null +++ b/QSThingsPlugin/Handlers/ThingsTaskHandler.m @@ -0,0 +1,165 @@ +#import "ThingsTaskHandler.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation ThingsTaskHandler + +- (BOOL)objectHasChildren:(QSObject *)object { + return YES; +} + +- (void)setQuickIconForObject:(QSObject *)object { + NSImage *baseImage = [QSResourceManager imageNamed:@"com.culturedcode.ThingsMac"]; + NSString *badge = @"checkmark.circle"; + NSColor *color = [NSColor colorWithRed:0.310 green:0.616 blue:0.976 alpha:1.0]; + + if (object.primaryType == kProjectType) { + badge = @"circle"; + color = [NSColor colorWithRed:0.157 green:0.376 blue:0.733 alpha:1.0]; + } + NSImage *imageWithBadge = [baseImage imageOfSize:NSMakeSize(512, 512) withSystemBadge:badge ofColor:color]; + + if (imageWithBadge) { + [object setIcon:imageWithBadge]; + } else { + [object setIcon:baseImage]; + } +} + +- (BOOL)loadChildrenForObject:(QSObject *)object { + NSDictionary *contents = [object objectForType:object.primaryType]; + + if (!contents) { + NSLog(@"Malformed Things Task: No content."); + return NO; + } + + NSString *path = [contents objectForKey:@"path"]; + if (!path) { + NSLog(@"Malformed Things Task: No path."); + return NO; + } + + NSString *uuid = [contents objectForKey:@"uuid"]; + if (!uuid) { + NSLog(@"Malformed Things Task: No uuid."); + return NO; + } + + // Tasks and Projects are basically the same thing in the Things database. So we use the same handler. + // The big difference is that projects can't have projects, and tasks can't have tasks + + NSMutableArray *children = [NSMutableArray array]; + [children addObjectsFromArray:[self tagsForTaskIdentifiedBy:uuid usingDatabaseAt:path ]]; + [children addObjectsFromArray:[self areasForTaskIdentifiedBy:uuid usingDatabaseAt:path ]]; + + if (object.primaryType == kProjectType) { + [children addObjectsFromArray:[self tasksForTaskIdentifiedBy:uuid usingDatabaseAt:path ]]; + } else { + [children addObjectsFromArray:[self projectsForTaskIdentifiedBy:uuid usingDatabaseAt:path ]]; + } + + [object setChildren:children]; + return YES; +} + +- (NSArray *)tagsForTaskIdentifiedBy:(NSString *)uuid usingDatabaseAt:(NSString *)path { + NSString *query = [NSString stringWithFormat: @"SELECT " + "tag.uuid as uuid, " + "tag.title as title " + "FROM TMTask AS task " + "LEFT JOIN TMTaskTag as junction " + "ON junction.tasks = task.uuid " + "LEFT JOIN TMTag as tag " + "ON junction.tags = tag.uuid " + "WHERE task.uuid = '%@' " + "AND tag.uuid IS NOT NULL " + "ORDER BY tag.title;", uuid]; + + NSArray *results = [SQLHelper executeSql:query onFile:path]; + NSMutableArray *objects = [NSMutableArray array]; + [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { + [objects addObject:[ObjectHelper tagFromSQLResult:dictionary usingPath:path ]]; + }]; + + return objects; +} + +- (NSArray *)projectsForTaskIdentifiedBy:(NSString *)uuid usingDatabaseAt:(NSString *)path { + NSString *query = [NSString stringWithFormat: @"SELECT " + "project.uuid as uuid, " + "project.creationDate as creationDate, " + "project.status as status, " + "project.title as title, " + "project.notes as notes, " + "project.area as area " + "FROM TMTask AS task " + "LEFT JOIN TMTask as project " + "ON task.project = project.uuid " + "AND project.type = 1 " + "WHERE task.uuid = '%@' " + "AND project.uuid IS NOT NULL " + "AND project.trashed = 0 " + "AND project.status = 0 " + "ORDER BY project.title;", uuid]; + + NSArray *results = [SQLHelper executeSql:query onFile:path]; + NSMutableArray *objects = [NSMutableArray array]; + [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { + [objects addObject:[ObjectHelper projectFromSQLResult:dictionary usingPath:path ]]; + }]; + + return objects; +} + +- (NSArray *)areasForTaskIdentifiedBy:(NSString *)uuid usingDatabaseAt:(NSString *)path { + NSString *query = [NSString stringWithFormat: @"SELECT " + "area.uuid as uuid, " + "area.title as title " + "FROM TMTask AS task " + "LEFT JOIN TMTask as project " + "ON task.project = project.uuid " + "LEFT JOIN TMArea as area " + "ON task.area = area.uuid OR project.area = area.uuid " + "WHERE task.uuid = '%@' " + "AND area.uuid IS NOT NULL " + "ORDER BY area.title;", uuid]; + + NSArray *results = [SQLHelper executeSql:query onFile:path]; + NSMutableArray *objects = [NSMutableArray array]; + if (results) { + [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { + [objects addObject:[ObjectHelper areaFromSQLResult:dictionary usingPath:path ]]; + }]; + } + + return objects; +} + +- (NSArray *)tasksForTaskIdentifiedBy:(NSString *)uuid usingDatabaseAt:(NSString *)path { + NSString *query = [NSString stringWithFormat: @"SELECT " + "uuid, " + "creationDate, " + "status, " + "title, " + "notes, " + "area " + "FROM TMTask " + "WHERE project = '%@' " + "AND type = 0 " + "AND trashed = 0 " + "AND status = 0 " + "ORDER BY title;", uuid]; + + NSArray *results = [SQLHelper executeSql:query onFile:path]; + NSMutableArray *objects = [NSMutableArray array]; + [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { + [objects addObject:[ObjectHelper taskFromSQLResult:dictionary usingPath:path ]]; + }]; + + return objects; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/QSThingsPlugin/Helpers/ObjectHelper.h b/QSThingsPlugin/Helpers/ObjectHelper.h new file mode 100644 index 0000000..fdb0a48 --- /dev/null +++ b/QSThingsPlugin/Helpers/ObjectHelper.h @@ -0,0 +1,18 @@ +#import "Constants.h" +#import <Foundation/Foundation.h> +#import <QSCore/QSCore.h> + +NS_ASSUME_NONNULL_BEGIN + +/** + Helper class to generate QSObjects + */ +@interface ObjectHelper : NSObject + ++ (QSObject *) areaFromSQLResult:(NSDictionary *)result usingPath:(NSString *)path; ++ (QSObject *) projectFromSQLResult:(NSDictionary *)result usingPath:(NSString *)path; ++ (QSObject *) taskFromSQLResult:(NSDictionary *)result usingPath:(NSString *)path; ++ (QSObject *) tagFromSQLResult:(NSDictionary *)result usingPath:(NSString *)path; +@end + +NS_ASSUME_NONNULL_END diff --git a/QSThingsPlugin/Helpers/ObjectHelper.m b/QSThingsPlugin/Helpers/ObjectHelper.m new file mode 100644 index 0000000..cf26127 --- /dev/null +++ b/QSThingsPlugin/Helpers/ObjectHelper.m @@ -0,0 +1,58 @@ +#import "ObjectHelper.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation ObjectHelper + +#pragma mark - SQL Results to QSObjects + ++ (QSObject *) areaFromSQLResult:(NSDictionary *)result usingPath:(NSString *)path { + NSMutableDictionary *configuration = [result mutableCopy]; + [configuration setObject:path forKey:@"path"]; + QSObject *area = [QSObject + makeObjectWithIdentifier:[result objectForKey:@"uuid"]]; + [area setObject:configuration forType:kAreaType]; + [area setName:[result objectForKey:@"title"]]; + [area setPrimaryType:kAreaType]; + return area; +} + ++ (QSObject *) projectFromSQLResult:(NSDictionary *)result usingPath:(NSString *)path { + NSMutableDictionary *configuration = [result mutableCopy]; + [configuration setObject:path forKey:@"path"]; + QSObject *project = [QSObject + makeObjectWithIdentifier:[result objectForKey:@"uuid"]]; + [project setObject:configuration forType:kProjectType]; + [project setName:[result objectForKey:@"title"]]; + [project setDetails:[result objectForKey:@"notes"]]; + [project setPrimaryType:kProjectType]; + + return project; +} ++ (QSObject *) taskFromSQLResult:(NSDictionary *)result usingPath:(NSString *)path { + NSMutableDictionary *configuration = [result mutableCopy]; + [configuration setObject:path forKey:@"path"]; + QSObject *task = [QSObject + makeObjectWithIdentifier:[result objectForKey:@"uuid"]]; + [task setObject:configuration forType:kTaskType]; + [task setName:[result objectForKey:@"title"]]; + [task setDetails:[result objectForKey:@"notes"]]; + [task setPrimaryType:kTaskType]; + + return task; +} ++ (QSObject *) tagFromSQLResult:(NSDictionary *)result usingPath:(NSString *)path { + NSMutableDictionary *configuration = [result mutableCopy]; + [configuration setObject:path forKey:@"path"]; + QSObject *tag = [QSObject + makeObjectWithIdentifier:[result objectForKey:@"uuid"]]; + [tag setObject:configuration forType:kTagType]; + [tag setName:[result objectForKey:@"title"]]; + [tag setPrimaryType:kTagType]; + + return tag; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/QSThingsPlugin/Helpers/SQLHelper.m b/QSThingsPlugin/Helpers/SQLHelper.m index c44ea22..c26dfc5 100644 --- a/QSThingsPlugin/Helpers/SQLHelper.m +++ b/QSThingsPlugin/Helpers/SQLHelper.m @@ -9,7 +9,7 @@ FMDatabase *database = [FMDatabase databaseWithPath:path]; if (![database openWithFlags:0x00000001]) { // 1 = SQLITE_OPEN_READONLY - NSLog(@"Could not open Firefox's places.sqlite DB."); + NSLog(@"Could not open Things database."); return @[]; } [database goodConnection]; diff --git a/QSThingsPlugin/Helpers/ThingsHelper.h b/QSThingsPlugin/Helpers/ThingsHelper.h index 29d3092..3f1ab6a 100644 --- a/QSThingsPlugin/Helpers/ThingsHelper.h +++ b/QSThingsPlugin/Helpers/ThingsHelper.h @@ -28,7 +28,6 @@ extern ThingsURLRoute const ThingsURLRouteAddProject; @returns YES if the URL was successfully opened, NO if something went wrong. */ + (BOOL)callRoute:(ThingsURLRoute)route withParameters:(NSDictionary *)parameters; - @end NS_ASSUME_NONNULL_END diff --git a/QSThingsPlugin/Helpers/ThingsHelper.m b/QSThingsPlugin/Helpers/ThingsHelper.m index cc7267b..be54d8c 100644 --- a/QSThingsPlugin/Helpers/ThingsHelper.m +++ b/QSThingsPlugin/Helpers/ThingsHelper.m @@ -11,15 +11,6 @@ ThingsURLRoute const ThingsURLRouteAddProject = @"add-project"; @implementation ThingsHelper -+ (instancetype)sharedInstance { - static ThingsHelper *sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedInstance = [[self alloc] init]; - }); - return sharedInstance; -} - + (BOOL)requiresAuthentication:(ThingsURLRoute)route { if ([route isEqualToString:ThingsURLRouteSearch] || [route isEqualToString:ThingsURLRouteShow]) { return NO; diff --git a/QSThingsPlugin/Parsers/QSThingsAppSectionsParser.m b/QSThingsPlugin/Parsers/QSThingsAppSectionsParser.m index 70a37ae..472a8fa 100644 --- a/QSThingsPlugin/Parsers/QSThingsAppSectionsParser.m +++ b/QSThingsPlugin/Parsers/QSThingsAppSectionsParser.m @@ -11,12 +11,14 @@ // confirm that this makes sense. - (NSArray *)objectsFromPath:(NSString *)path withSettings:(NSDictionary *)settings { NSMutableArray *objects = [NSMutableArray array]; + NSDictionary *dictionary = @{@"path": path}; // Inbox QSObject *inbox = [QSObject makeObjectWithIdentifier: ThingsAppSectionKeyInbox]; [inbox setName:@"Inbox"]; [inbox setPrimaryType:kAppSectionType]; + [inbox setObject:[dictionary copy] forType:kAppSectionType]; [objects addObject:inbox]; // Today @@ -24,6 +26,7 @@ makeObjectWithIdentifier: ThingsAppSectionKeyToday]; [today setName:@"Today"]; [today setPrimaryType:kAppSectionType]; + [today setObject:[dictionary copy] forType:kAppSectionType]; [objects addObject:today]; // Upcoming @@ -31,6 +34,7 @@ makeObjectWithIdentifier: ThingsAppSectionKeyUpcoming]; [upcoming setName:@"Upcoming"]; [upcoming setPrimaryType:kAppSectionType]; + [upcoming setObject:[dictionary copy] forType:kAppSectionType]; [objects addObject:upcoming]; // Anytime @@ -38,6 +42,7 @@ makeObjectWithIdentifier: ThingsAppSectionKeyAnytime]; [anytime setName:@"Anytime"]; [anytime setPrimaryType:kAppSectionType]; + [anytime setObject:[dictionary copy] forType:kAppSectionType]; [objects addObject:anytime]; // Anytime @@ -45,6 +50,7 @@ makeObjectWithIdentifier: ThingsAppSectionKeySomeday]; [someday setName:@"Someday"]; [someday setPrimaryType:kAppSectionType]; + [someday setObject:[dictionary copy] forType:kAppSectionType]; [objects addObject:someday]; return objects; diff --git a/QSThingsPlugin/Parsers/QSThingsAreasParser.h b/QSThingsPlugin/Parsers/QSThingsAreasParser.h index 74f829b..25601b7 100644 --- a/QSThingsPlugin/Parsers/QSThingsAreasParser.h +++ b/QSThingsPlugin/Parsers/QSThingsAreasParser.h @@ -1,3 +1,7 @@ +#import "Constants.h" +#import "SQLHelper.h" +#import "ObjectHelper.h" + @interface QSThingsAreasParser : QSParser { } diff --git a/QSThingsPlugin/Parsers/QSThingsAreasParser.m b/QSThingsPlugin/Parsers/QSThingsAreasParser.m index f147603..ca5e19f 100644 --- a/QSThingsPlugin/Parsers/QSThingsAreasParser.m +++ b/QSThingsPlugin/Parsers/QSThingsAreasParser.m @@ -1,6 +1,4 @@ -#import "Constants.h" #import "QSThingsAreasParser.h" -#import "SQLHelper.h" @implementation QSThingsAreasParser - (BOOL)validParserForPath:(NSString *)path { @@ -18,14 +16,7 @@ NSMutableArray *objects = [NSMutableArray array]; [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { - - QSObject *newObject = [QSObject - makeObjectWithIdentifier:[dictionary objectForKey:@"uuid"]]; - [newObject setObject:dictionary forType:kAreaType]; - [newObject setName:[dictionary objectForKey:@"title"]]; - [newObject setPrimaryType:kAreaType]; - - [objects addObject:newObject]; + [objects addObject:[ObjectHelper areaFromSQLResult:dictionary usingPath:path ]]; }]; return objects; diff --git a/QSThingsPlugin/Parsers/QSThingsProjectsParser.h b/QSThingsPlugin/Parsers/QSThingsProjectsParser.h index cee939d..0892fa1 100644 --- a/QSThingsPlugin/Parsers/QSThingsProjectsParser.h +++ b/QSThingsPlugin/Parsers/QSThingsProjectsParser.h @@ -1,3 +1,7 @@ +#import "Constants.h" +#import "SQLHelper.h" +#import "ObjectHelper.h" + @interface QSThingsProjectsParser : QSParser { } diff --git a/QSThingsPlugin/Parsers/QSThingsProjectsParser.m b/QSThingsPlugin/Parsers/QSThingsProjectsParser.m index 1a7956e..b59a420 100644 --- a/QSThingsPlugin/Parsers/QSThingsProjectsParser.m +++ b/QSThingsPlugin/Parsers/QSThingsProjectsParser.m @@ -1,6 +1,4 @@ -#import "Constants.h" #import "QSThingsProjectsParser.h" -#import "SQLHelper.h" @implementation QSThingsProjectsParser - (BOOL)validParserForPath:(NSString *)path { @@ -26,15 +24,7 @@ NSMutableArray *objects = [NSMutableArray array]; [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { - - QSObject *newObject = [QSObject - makeObjectWithIdentifier:[dictionary objectForKey:@"uuid"]]; - [newObject setObject:dictionary forType:kProjectType]; - [newObject setName:[dictionary objectForKey:@"title"]]; - [newObject setDetails:[dictionary objectForKey:@"notes"]]; - [newObject setPrimaryType:kProjectType]; - - [objects addObject:newObject]; + [objects addObject:[ObjectHelper projectFromSQLResult:dictionary usingPath:path ]]; }]; return objects; diff --git a/QSThingsPlugin/Parsers/QSThingsTagsParser.h b/QSThingsPlugin/Parsers/QSThingsTagsParser.h index d57aac1..ed78ec9 100644 --- a/QSThingsPlugin/Parsers/QSThingsTagsParser.h +++ b/QSThingsPlugin/Parsers/QSThingsTagsParser.h @@ -1,3 +1,7 @@ +#import "Constants.h" +#import "SQLHelper.h" +#import "ObjectHelper.h" + @interface QSThingsTagsParser : QSParser { } diff --git a/QSThingsPlugin/Parsers/QSThingsTagsParser.m b/QSThingsPlugin/Parsers/QSThingsTagsParser.m index 4a81de3..04202b0 100644 --- a/QSThingsPlugin/Parsers/QSThingsTagsParser.m +++ b/QSThingsPlugin/Parsers/QSThingsTagsParser.m @@ -1,6 +1,4 @@ -#import "Constants.h" #import "QSThingsTagsParser.h" -#import "SQLHelper.h" @implementation QSThingsTagsParser - (BOOL)validParserForPath:(NSString *)path { @@ -18,14 +16,7 @@ NSMutableArray *objects = [NSMutableArray array]; [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { - - QSObject *newObject = [QSObject - makeObjectWithIdentifier:[dictionary objectForKey:@"uuid"]]; - [newObject setObject:dictionary forType:kTagType]; - [newObject setName:[dictionary objectForKey:@"title"]]; - [newObject setPrimaryType:kTagType]; - - [objects addObject:newObject]; + [objects addObject:[ObjectHelper tagFromSQLResult:dictionary usingPath:path ]]; }]; return objects; diff --git a/QSThingsPlugin/Parsers/QSThingsTasksParser.h b/QSThingsPlugin/Parsers/QSThingsTasksParser.h index 7e3f1f2..50e8f05 100644 --- a/QSThingsPlugin/Parsers/QSThingsTasksParser.h +++ b/QSThingsPlugin/Parsers/QSThingsTasksParser.h @@ -1,3 +1,6 @@ +#import "Constants.h" +#import "SQLHelper.h" +#import "ObjectHelper.h" @interface QSThingsTasksParser : QSParser { } diff --git a/QSThingsPlugin/Parsers/QSThingsTasksParser.m b/QSThingsPlugin/Parsers/QSThingsTasksParser.m index b29d011..8f4415b 100644 --- a/QSThingsPlugin/Parsers/QSThingsTasksParser.m +++ b/QSThingsPlugin/Parsers/QSThingsTasksParser.m @@ -1,7 +1,4 @@ -#import "Constants.h" #import "QSThingsTasksParser.h" -#import "SQLHelper.h" -#import <os/log.h> @implementation QSThingsTasksParser - (BOOL)validParserForPath:(NSString *)path { @@ -27,15 +24,7 @@ NSMutableArray *objects = [NSMutableArray array]; [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) { - - QSObject *newObject = [QSObject - makeObjectWithIdentifier:[dictionary objectForKey:@"uuid"]]; - [newObject setObject:dictionary forType:kTaskType]; - [newObject setName:[dictionary objectForKey:@"title"]]; - [newObject setDetails:[dictionary objectForKey:@"notes"]]; - [newObject setPrimaryType:kTaskType]; - - [objects addObject:newObject]; + [objects addObject:[ObjectHelper taskFromSQLResult:dictionary usingPath:path ]]; }]; return objects; |