From a5e25cc05e5e33e114331cc826211c89adbcfa6a Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Mon, 8 Sep 2025 10:38:39 +0200 Subject: Reorganize code, WIP save for tag handling --- Factories/QSBookmarkProviderFactory.h | 28 +++ Factories/QSBookmarkProviderFactory.m | 66 ++++++ Protocols/QSBookmarkProvider.h | 48 +++++ Providers/QSDeliciousAPIProvider.h | 25 +++ Providers/QSDeliciousAPIProvider.m | 193 +++++++++++++++++ Providers/QSLinkdingProvider.h | 16 ++ Providers/QSLinkdingProvider.m | 192 +++++++++++++++++ QSBookmarkProvider.h | 48 ----- QSBookmarkProviderFactory.h | 28 --- QSBookmarkProviderFactory.m | 66 ------ QSDeliciousAPIProvider.h | 25 --- QSDeliciousAPIProvider.m | 190 ----------------- QSDeliciousPlugIn.xcodeproj/project.pbxproj | 52 +++-- QSDeliciousPlugInTests.m | 89 -------- QSDeliciousPlugIn_Source.h | 1 + QSDeliciousPlugIn_Source.m | 317 +++++++++++++++------------- QSDeliciousPlugIn_Source.xib | 2 +- QSLinkdingProvider.h | 16 -- QSLinkdingProvider.m | 191 ----------------- REFACTOR_GUIDE.md | 118 ----------- SocialSite.h | 23 -- SocialSite.m | 55 ----- Types/SocialSite.h | 23 ++ Types/SocialSite.m | 55 +++++ 24 files changed, 855 insertions(+), 1012 deletions(-) create mode 100644 Factories/QSBookmarkProviderFactory.h create mode 100644 Factories/QSBookmarkProviderFactory.m create mode 100644 Protocols/QSBookmarkProvider.h create mode 100644 Providers/QSDeliciousAPIProvider.h create mode 100644 Providers/QSDeliciousAPIProvider.m create mode 100644 Providers/QSLinkdingProvider.h create mode 100644 Providers/QSLinkdingProvider.m delete mode 100644 QSBookmarkProvider.h delete mode 100644 QSBookmarkProviderFactory.h delete mode 100644 QSBookmarkProviderFactory.m delete mode 100644 QSDeliciousAPIProvider.h delete mode 100644 QSDeliciousAPIProvider.m delete mode 100644 QSDeliciousPlugInTests.m delete mode 100644 QSLinkdingProvider.h delete mode 100644 QSLinkdingProvider.m delete mode 100644 REFACTOR_GUIDE.md delete mode 100644 SocialSite.h delete mode 100644 SocialSite.m create mode 100644 Types/SocialSite.h create mode 100644 Types/SocialSite.m diff --git a/Factories/QSBookmarkProviderFactory.h b/Factories/QSBookmarkProviderFactory.h new file mode 100644 index 0000000..e2e694b --- /dev/null +++ b/Factories/QSBookmarkProviderFactory.h @@ -0,0 +1,28 @@ +// +// QSBookmarkProviderFactory.h +// QSDeliciousPlugIn +// +// Factory for managing bookmark providers +// + +#import +#import "QSBookmarkProvider.h" +#import "SocialSite.h" + +@interface QSBookmarkProviderFactory : NSObject + +@property (nonatomic, strong, readonly) NSArray> *providers; + ++ (instancetype)sharedFactory; + +/** + * Get the appropriate provider for the given site configuration + */ +- (id)providerForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; + +/** + * Get all available providers + */ +- (NSArray> *)allProviders; + +@end \ No newline at end of file diff --git a/Factories/QSBookmarkProviderFactory.m b/Factories/QSBookmarkProviderFactory.m new file mode 100644 index 0000000..6fcfdfb --- /dev/null +++ b/Factories/QSBookmarkProviderFactory.m @@ -0,0 +1,66 @@ +// +// QSBookmarkProviderFactory.m +// QSDeliciousPlugIn +// + +#import "QSBookmarkProviderFactory.h" +#import "QSDeliciousAPIProvider.h" +#import "QSLinkdingProvider.h" +#import "SocialSite.h" + +@interface QSBookmarkProviderFactory () +@property (nonatomic, strong, readwrite) NSArray> *providers; +@end + +@implementation QSBookmarkProviderFactory + ++ (instancetype)sharedFactory { + static QSBookmarkProviderFactory *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + [self setupProviders]; + } + return self; +} + +- (void)setupProviders { + NSMutableArray *mutableProviders = [NSMutableArray array]; + + // Create Pinboard API providers for each supported site + QSDeliciousAPIProvider *deliciousProvider = [[QSDeliciousAPIProvider alloc] initWithSite:SocialSiteDelicious]; + QSDeliciousAPIProvider *magnoliaProvider = [[QSDeliciousAPIProvider alloc] initWithSite:SocialSiteMagnolia]; + QSDeliciousAPIProvider *pinboardProvider = [[QSDeliciousAPIProvider alloc] initWithSite:SocialSitePinboard]; + + // Create Linkding provider + QSLinkdingProvider *linkdingProvider = [[QSLinkdingProvider alloc] init]; + + [mutableProviders addObject:deliciousProvider]; + [mutableProviders addObject:magnoliaProvider]; + [mutableProviders addObject:pinboardProvider]; + [mutableProviders addObject:linkdingProvider]; + + self.providers = [mutableProviders copy]; +} + +- (id)providerForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + for (id provider in self.providers) { + if ([provider canHandleSite:site username:username password:password host:host]) { + return provider; + } + } + return nil; +} + +- (NSArray> *)allProviders { + return self.providers; +} + +@end diff --git a/Protocols/QSBookmarkProvider.h b/Protocols/QSBookmarkProvider.h new file mode 100644 index 0000000..136ea2d --- /dev/null +++ b/Protocols/QSBookmarkProvider.h @@ -0,0 +1,48 @@ +// +// QSBookmarkProvider.h +// QSDeliciousPlugIn +// +// Protocol for social bookmark providers +// + +#import +#import "SocialSite.h" + +@class QSObject; + +@protocol QSBookmarkProvider + +@required +/** + * Check if this provider can handle the given site configuration + */ +- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; + +/** + * Fetch bookmarks for the given configuration + * Returns an NSArray of QSObject instances + */ +- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password identifier:(NSString *)identifier host:(NSString *)host includeTags:(BOOL)includeTags; + +/** + * Get the supported site for this provider + */ +- (SocialSite)supportedSite; + +/** + * Get display name for this provider + */ +- (NSString *)providerName; + +@optional +/** + * Get bookmarks for a specific tag (used for child loading) + */ +- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; + +/** + * Get the tag URL type identifier for this provider + */ +- (NSString *)tagURLType; + +@end diff --git a/Providers/QSDeliciousAPIProvider.h b/Providers/QSDeliciousAPIProvider.h new file mode 100644 index 0000000..0064264 --- /dev/null +++ b/Providers/QSDeliciousAPIProvider.h @@ -0,0 +1,25 @@ +// +// QSDeliciousAPIProvider.h +// QSDeliciousPlugIn +// +// Base provider for Pinboard v1 API (XML-based with Basic Auth) +// Used by Delicious, Magnolia, and Pinboard +// + +#import +#import "QSBookmarkProvider.h" + +@interface QSDeliciousAPIProvider : NSObject + +@property (nonatomic, strong) NSMutableArray *posts; +@property (nonatomic, assign) SocialSite site; + +- (instancetype)initWithSite:(SocialSite)site; + +// Subclasses can override these methods +- (NSString *)apiURLForSite:(SocialSite)site; +- (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; +- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username; +- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username; + +@end diff --git a/Providers/QSDeliciousAPIProvider.m b/Providers/QSDeliciousAPIProvider.m new file mode 100644 index 0000000..ab09f88 --- /dev/null +++ b/Providers/QSDeliciousAPIProvider.m @@ -0,0 +1,193 @@ +// +// QSDeliciousAPIProvider.m +// QSDeliciousPlugIn +// + +#import "QSDeliciousAPIProvider.h" +#import "SocialSite.h" +#import + +@implementation QSDeliciousAPIProvider + +- (instancetype)initWithSite:(SocialSite)site { + self = [super init]; + if (self) { + _site = site; + _posts = [NSMutableArray array]; + } + return self; +} + +- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + return (site == self.site) && + username.length > 0 && + password.length > 0 && + (site != SocialSiteLinkding); // This provider doesn't handle Linkding +} + +- (SocialSite)supportedSite { + return self.site; +} + +- (NSString *)providerName { + return [SocialSiteHelper displayNameForSite:self.site]; +} + +- (NSString *)apiURLForSite:(SocialSite)site { + switch (site) { + case SocialSiteDelicious: + return @"api.del.icio.us/v1"; + case SocialSiteMagnolia: + return @"ma.gnolia.com/api/mirrord/v1"; + case SocialSitePinboard: + return @"api.pinboard.in/v1"; + default: + return nil; + } +} + +- (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + NSString *apiURL = [self apiURLForSite:site]; + if (!apiURL) return nil; + + NSString *urlString = [NSString stringWithFormat:@"https://%@:%@@%@/posts/all?", username, password, apiURL]; + return [NSURL URLWithString:urlString]; +} + +- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username { + NSString *siteURL = [SocialSiteHelper siteURLForSite:site]; + NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]]; + return [NSData dataWithContentsOfFile:cachePath]; +} + +- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username { + NSString *siteURL = [SocialSiteHelper siteURLForSite:site]; + NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]]; + [data writeToFile:cachePath atomically:NO]; +} + +- (NSString *)tagURLType { + return [NSString stringWithFormat:@"tag.%@", [SocialSiteHelper reversedSiteURLForSite:self.site]]; +} + +- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password identifier:(NSString *)identifier host:(NSString *)host includeTags:(BOOL)includeTags { + + // Try cached data first + NSData *data = [self cachedBookmarkDataForSite:site username:username]; + + // If no cached data, fetch from API + if (![data length]) { + NSURL *requestURL = [self requestURLForSite:site username:username password:password host:host]; + if (!requestURL) return @[]; + + NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:60.0]; + [theRequest setHTTPMethod:@"POST"]; + [theRequest setValue:@"text/xml" forHTTPHeaderField:@"Content-type"]; + [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"]; + + NSError *error; + data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error]; + + if (error) { + NSLog(@"Error fetching bookmarks: %@", error.localizedDescription); + return @[]; + } + + // Cache the data + [self cacheBookmarkData:data forSite:site username:username]; + } + + // Parse XML data + NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; + [postParser setDelegate:self]; + + self.posts = [NSMutableArray arrayWithCapacity:1]; + [postParser parse]; + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + NSMutableSet *tagSet = [NSMutableSet set]; + + // Create bookmark objects + for (NSDictionary *post in self.posts) { + QSObject *newObject = [self objectForPost:post]; + if (newObject) { + [objects addObject:newObject]; + + // Collect tags if requested + if (includeTags) { + NSString *tagString = [post objectForKey:@"tag"]; + if (tagString.length > 0) { + [tagSet addObjectsFromArray:[tagString componentsSeparatedByString:@" "]]; + } + } + } + } + + // Create tag objects if requested + if (includeTags) { + for (NSString *tag in tagSet) { + if (tag.length > 0) { + QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[%@ tag]:%@", [self providerName], tag]]; + [tagObject setObject:tag forType:[self tagURLType]]; + [tagObject setObject:@(site) forMeta:@"source.site"]; + [tagObject setObject:username forMeta:@"source.username"]; + [tagObject setObject:host forMeta:@"source.host"]; + [tagObject setObject:identifier forMeta:@"source.identifier"]; + [tagObject setName:tag]; + [tagObject setPrimaryType:[self tagURLType]]; + [objects addObject:tagObject]; + } + } + } + + return objects; +} + +- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + NSData *data = [self cachedBookmarkDataForSite:site username:username]; + if (!data) return @[]; + + NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; + [postParser setDelegate:self]; + self.posts = [NSMutableArray arrayWithCapacity:1]; + [postParser parse]; + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + + for (NSDictionary *post in self.posts) { + NSString *postTags = [post objectForKey:@"tag"]; + if ([postTags rangeOfString:tag].location != NSNotFound) { + QSObject *newObject = [self objectForPost:post]; + if (newObject) { + [objects addObject:newObject]; + } + } + } + + return objects; +} + +- (QSObject *)objectForPost:(NSDictionary *)post { + QSObject *newObject = [QSObject makeObjectWithIdentifier:[post objectForKey:@"hash"]]; + [newObject setObject:[post objectForKey:@"href"] forType:QSURLType]; + [newObject setName:[post objectForKey:@"description"]]; + [newObject setDetails:[post objectForKey:@"extended"]]; + [newObject setPrimaryType:QSURLType]; + return newObject; +} + +#pragma mark - NSXMLParserDelegate + +- (void)parser:(NSXMLParser*)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { + if ([elementName isEqualToString:@"post"] && attributeDict) { + [self.posts addObject:attributeDict]; + } +} + +- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { + // Implementation if needed +} + +@end diff --git a/Providers/QSLinkdingProvider.h b/Providers/QSLinkdingProvider.h new file mode 100644 index 0000000..28807e8 --- /dev/null +++ b/Providers/QSLinkdingProvider.h @@ -0,0 +1,16 @@ +// +// QSLinkdingProvider.h +// QSDeliciousPlugIn +// +// Provider for Linkding API (JSON-based with API key) +// + +#import +#import "QSBookmarkProvider.h" + +@interface QSLinkdingProvider : NSObject + +- (NSData *)cachedBookmarkDataForHost:(NSString *)host username:(NSString *)username; +- (void)cacheBookmarkData:(NSData *)data forHost:(NSString *)host username:(NSString *)username; + +@end \ No newline at end of file diff --git a/Providers/QSLinkdingProvider.m b/Providers/QSLinkdingProvider.m new file mode 100644 index 0000000..094f318 --- /dev/null +++ b/Providers/QSLinkdingProvider.m @@ -0,0 +1,192 @@ +// +// QSLinkdingProvider.m +// QSDeliciousPlugIn +// + +#import "QSLinkdingProvider.h" +#import "SocialSite.h" +#import + +@implementation QSLinkdingProvider + +- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + return (site == SocialSiteLinkding) && + username.length > 0 && + password.length > 0 && // password is API token for Linkding + host.length > 0; +} + +- (SocialSite)supportedSite { + return SocialSiteLinkding; +} + +- (NSString *)providerName { + return @"Linkding"; +} + +- (NSString *)tagURLType { + return @"tag.linkding"; +} + +- (NSData *)cachedBookmarkDataForHost:(NSString *)host username:(NSString *)username { + // Create a safe filename from host + NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; + NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/linkding/"], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", safeHost, username]]; + return [NSData dataWithContentsOfFile:cachePath]; +} + +- (void)cacheBookmarkData:(NSData *)data forHost:(NSString *)host username:(NSString *)username { + // Create a safe filename from host + NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; + NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/linkding/"], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", safeHost, username]]; + [data writeToFile:cachePath atomically:NO]; +} + +- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password identifier:(NSString *)identifier host:(NSString *)host includeTags:(BOOL)includeTags { + + if (![self canHandleSite:site username:username password:password host:host]) { + return @[]; + } + + // Try cached data first + NSData *data = [self cachedBookmarkDataForHost:host username:username]; + + // If no cached data, fetch from API + if (![data length]) { + // Construct Linkding API URL + NSString *baseURL = host; + if (![baseURL hasPrefix:@"http://"] && ![baseURL hasPrefix:@"https://"]) { + baseURL = [NSString stringWithFormat:@"https://%@", baseURL]; + } + if ([baseURL hasSuffix:@"/"]) { + baseURL = [baseURL substringToIndex:[baseURL length] - 1]; + } + + NSString *urlString = [NSString stringWithFormat:@"%@/api/bookmarks/", baseURL]; + NSURL *requestURL = [NSURL URLWithString:urlString]; + + if (!requestURL) { + NSLog(@"Invalid Linkding host URL: %@", host); + return @[]; + } + + NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:60.0]; + [theRequest setHTTPMethod:@"GET"]; + [theRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + [theRequest setValue:[NSString stringWithFormat:@"Token %@", password] forHTTPHeaderField:@"Authorization"]; + [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"]; + + NSError *error = nil; + data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error]; + + if (error) { + NSLog(@"Error fetching Linkding bookmarks: %@", error.localizedDescription); + return @[]; + } + + // Cache the data + [self cacheBookmarkData:data forHost:host username:username]; + } + + // Parse JSON data + NSError *jsonError = nil; + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + + if (jsonError) { + NSLog(@"Error parsing Linkding JSON: %@", jsonError.localizedDescription); + return @[]; + } + + NSArray *results = [jsonResponse objectForKey:@"results"]; + if (!results || ![results isKindOfClass:[NSArray class]]) { + NSLog(@"Invalid Linkding response format"); + return @[]; + } + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + NSMutableSet *tagSet = [NSMutableSet set]; + + // Create bookmark objects + for (NSDictionary *bookmark in results) { + QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; + if (newObject) { + [objects addObject:newObject]; + + // Collect tags if requested + if (includeTags) { + NSArray *tags = [bookmark objectForKey:@"tag_names"]; + if (tags && [tags isKindOfClass:[NSArray class]]) { + [tagSet addObjectsFromArray:tags]; + } + } + } + } + + // Create tag objects if requested + if (includeTags) { + for (NSString *tag in tagSet) { + if (tag.length > 0) { + QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[Linkding tag]:%@", tag]]; + [tagObject setObject:tag forType:[self tagURLType]]; + [tagObject setObject:@(site) forMeta:@"source.site"]; + [tagObject setObject:username forMeta:@"source.username"]; + [tagObject setObject:host forMeta:@"source.host"]; + // We need the identifier to be able to fetch the keychain password + [tagObject setObject:identifier forMeta:@"source.identifier"]; + [tagObject setName:tag]; + [tagObject setPrimaryType:[self tagURLType]]; + [objects addObject:tagObject]; + } + } + } + + return objects; +} + +- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + NSData *data = [self cachedBookmarkDataForHost:host username:username]; + if (!data) return @[]; + + NSError *jsonError; + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + + if (jsonError) return @[]; + + NSArray *results = [jsonResponse objectForKey:@"results"]; + if (!results || ![results isKindOfClass:[NSArray class]]) return @[]; + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + + for (NSDictionary *bookmark in results) { + NSArray *tags = [bookmark objectForKey:@"tag_names"]; + if (tags && [tags isKindOfClass:[NSArray class]] && [tags containsObject:tag]) { + QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; + if (newObject) { + [objects addObject:newObject]; + } + } + } + + return objects; +} + +- (QSObject *)objectForLinkdingBookmark:(NSDictionary *)bookmark { + NSNumber *bookmarkId = [bookmark objectForKey:@"id"]; + NSString *url = [bookmark objectForKey:@"url"]; + NSString *title = [bookmark objectForKey:@"title"]; + NSString *description = [bookmark objectForKey:@"description"]; + + if (!bookmarkId || !url) return nil; + + QSObject *newObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"linkding-%@", bookmarkId]]; + [newObject setObject:url forType:QSURLType]; + [newObject setName:title.length > 0 ? title : url]; + [newObject setDetails:description.length > 0 ? description : @""]; + [newObject setPrimaryType:QSURLType]; + + return newObject; +} + +@end diff --git a/QSBookmarkProvider.h b/QSBookmarkProvider.h deleted file mode 100644 index 735ae82..0000000 --- a/QSBookmarkProvider.h +++ /dev/null @@ -1,48 +0,0 @@ -// -// QSBookmarkProvider.h -// QSDeliciousPlugIn -// -// Protocol for social bookmark providers -// - -#import -#import "SocialSite.h" - -@class QSObject; - -@protocol QSBookmarkProvider - -@required -/** - * Check if this provider can handle the given site configuration - */ -- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; - -/** - * Fetch bookmarks for the given configuration - * Returns an NSArray of QSObject instances - */ -- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host includeTags:(BOOL)includeTags; - -/** - * Get the supported site for this provider - */ -- (SocialSite)supportedSite; - -/** - * Get display name for this provider - */ -- (NSString *)providerName; - -@optional -/** - * Get bookmarks for a specific tag (used for child loading) - */ -- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; - -/** - * Get the tag URL type identifier for this provider - */ -- (NSString *)tagURLType; - -@end \ No newline at end of file diff --git a/QSBookmarkProviderFactory.h b/QSBookmarkProviderFactory.h deleted file mode 100644 index e2e694b..0000000 --- a/QSBookmarkProviderFactory.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// QSBookmarkProviderFactory.h -// QSDeliciousPlugIn -// -// Factory for managing bookmark providers -// - -#import -#import "QSBookmarkProvider.h" -#import "SocialSite.h" - -@interface QSBookmarkProviderFactory : NSObject - -@property (nonatomic, strong, readonly) NSArray> *providers; - -+ (instancetype)sharedFactory; - -/** - * Get the appropriate provider for the given site configuration - */ -- (id)providerForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; - -/** - * Get all available providers - */ -- (NSArray> *)allProviders; - -@end \ No newline at end of file diff --git a/QSBookmarkProviderFactory.m b/QSBookmarkProviderFactory.m deleted file mode 100644 index 6fcfdfb..0000000 --- a/QSBookmarkProviderFactory.m +++ /dev/null @@ -1,66 +0,0 @@ -// -// QSBookmarkProviderFactory.m -// QSDeliciousPlugIn -// - -#import "QSBookmarkProviderFactory.h" -#import "QSDeliciousAPIProvider.h" -#import "QSLinkdingProvider.h" -#import "SocialSite.h" - -@interface QSBookmarkProviderFactory () -@property (nonatomic, strong, readwrite) NSArray> *providers; -@end - -@implementation QSBookmarkProviderFactory - -+ (instancetype)sharedFactory { - static QSBookmarkProviderFactory *sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedInstance = [[self alloc] init]; - }); - return sharedInstance; -} - -- (instancetype)init { - self = [super init]; - if (self) { - [self setupProviders]; - } - return self; -} - -- (void)setupProviders { - NSMutableArray *mutableProviders = [NSMutableArray array]; - - // Create Pinboard API providers for each supported site - QSDeliciousAPIProvider *deliciousProvider = [[QSDeliciousAPIProvider alloc] initWithSite:SocialSiteDelicious]; - QSDeliciousAPIProvider *magnoliaProvider = [[QSDeliciousAPIProvider alloc] initWithSite:SocialSiteMagnolia]; - QSDeliciousAPIProvider *pinboardProvider = [[QSDeliciousAPIProvider alloc] initWithSite:SocialSitePinboard]; - - // Create Linkding provider - QSLinkdingProvider *linkdingProvider = [[QSLinkdingProvider alloc] init]; - - [mutableProviders addObject:deliciousProvider]; - [mutableProviders addObject:magnoliaProvider]; - [mutableProviders addObject:pinboardProvider]; - [mutableProviders addObject:linkdingProvider]; - - self.providers = [mutableProviders copy]; -} - -- (id)providerForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - for (id provider in self.providers) { - if ([provider canHandleSite:site username:username password:password host:host]) { - return provider; - } - } - return nil; -} - -- (NSArray> *)allProviders { - return self.providers; -} - -@end diff --git a/QSDeliciousAPIProvider.h b/QSDeliciousAPIProvider.h deleted file mode 100644 index 0064264..0000000 --- a/QSDeliciousAPIProvider.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// QSDeliciousAPIProvider.h -// QSDeliciousPlugIn -// -// Base provider for Pinboard v1 API (XML-based with Basic Auth) -// Used by Delicious, Magnolia, and Pinboard -// - -#import -#import "QSBookmarkProvider.h" - -@interface QSDeliciousAPIProvider : NSObject - -@property (nonatomic, strong) NSMutableArray *posts; -@property (nonatomic, assign) SocialSite site; - -- (instancetype)initWithSite:(SocialSite)site; - -// Subclasses can override these methods -- (NSString *)apiURLForSite:(SocialSite)site; -- (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; -- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username; -- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username; - -@end diff --git a/QSDeliciousAPIProvider.m b/QSDeliciousAPIProvider.m deleted file mode 100644 index d97d1d2..0000000 --- a/QSDeliciousAPIProvider.m +++ /dev/null @@ -1,190 +0,0 @@ -// -// QSDeliciousAPIProvider.m -// QSDeliciousPlugIn -// - -#import "QSDeliciousAPIProvider.h" -#import "SocialSite.h" -#import - -@implementation QSDeliciousAPIProvider - -- (instancetype)initWithSite:(SocialSite)site { - self = [super init]; - if (self) { - _site = site; - _posts = [NSMutableArray array]; - } - return self; -} - -- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - return (site == self.site) && - username.length > 0 && - password.length > 0 && - (site != SocialSiteLinkding); // This provider doesn't handle Linkding -} - -- (SocialSite)supportedSite { - return self.site; -} - -- (NSString *)providerName { - return [SocialSiteHelper displayNameForSite:self.site]; -} - -- (NSString *)apiURLForSite:(SocialSite)site { - switch (site) { - case SocialSiteDelicious: - return @"api.del.icio.us/v1"; - case SocialSiteMagnolia: - return @"ma.gnolia.com/api/mirrord/v1"; - case SocialSitePinboard: - return @"api.pinboard.in/v1"; - default: - return nil; - } -} - -- (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - NSString *apiURL = [self apiURLForSite:site]; - if (!apiURL) return nil; - - NSString *urlString = [NSString stringWithFormat:@"https://%@:%@@%@/posts/all?", username, password, apiURL]; - return [NSURL URLWithString:urlString]; -} - -- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username { - NSString *siteURL = [SocialSiteHelper siteURLForSite:site]; - NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]]; - return [NSData dataWithContentsOfFile:cachePath]; -} - -- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username { - NSString *siteURL = [SocialSiteHelper siteURLForSite:site]; - NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]]; - [data writeToFile:cachePath atomically:NO]; -} - -- (NSString *)tagURLType { - return [NSString stringWithFormat:@"tag.%@", [SocialSiteHelper reversedSiteURLForSite:self.site]]; -} - -- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host includeTags:(BOOL)includeTags { - - // Try cached data first - NSData *data = [self cachedBookmarkDataForSite:site username:username]; - - // If no cached data, fetch from API - if (![data length]) { - NSURL *requestURL = [self requestURLForSite:site username:username password:password host:host]; - if (!requestURL) return @[]; - - NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL - cachePolicy:NSURLRequestUseProtocolCachePolicy - timeoutInterval:60.0]; - [theRequest setHTTPMethod:@"POST"]; - [theRequest setValue:@"text/xml" forHTTPHeaderField:@"Content-type"]; - [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"]; - - NSError *error; - data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error]; - - if (error) { - NSLog(@"Error fetching bookmarks: %@", error.localizedDescription); - return @[]; - } - - // Cache the data - [self cacheBookmarkData:data forSite:site username:username]; - } - - // Parse XML data - NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; - [postParser setDelegate:self]; - - self.posts = [NSMutableArray arrayWithCapacity:1]; - [postParser parse]; - - NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; - NSMutableSet *tagSet = [NSMutableSet set]; - - // Create bookmark objects - for (NSDictionary *post in self.posts) { - QSObject *newObject = [self objectForPost:post]; - if (newObject) { - [objects addObject:newObject]; - - // Collect tags if requested - if (includeTags) { - NSString *tagString = [post objectForKey:@"tag"]; - if (tagString.length > 0) { - [tagSet addObjectsFromArray:[tagString componentsSeparatedByString:@" "]]; - } - } - } - } - - // Create tag objects if requested - if (includeTags) { - for (NSString *tag in tagSet) { - if (tag.length > 0) { - QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[%@ tag]:%@", [self providerName], tag]]; - [tagObject setObject:tag forType:[self tagURLType]]; - [tagObject setObject:username forMeta:[NSString stringWithFormat:@"%@.username", [SocialSiteHelper reversedSiteURLForSite:site]]]; - [tagObject setName:tag]; - [tagObject setPrimaryType:[self tagURLType]]; - [objects addObject:tagObject]; - } - } - } - - return objects; -} - -- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - NSData *data = [self cachedBookmarkDataForSite:site username:username]; - if (!data) return @[]; - - NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; - [postParser setDelegate:self]; - self.posts = [NSMutableArray arrayWithCapacity:1]; - [postParser parse]; - - NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; - - for (NSDictionary *post in self.posts) { - NSString *postTags = [post objectForKey:@"tag"]; - if ([postTags rangeOfString:tag].location != NSNotFound) { - QSObject *newObject = [self objectForPost:post]; - if (newObject) { - [objects addObject:newObject]; - } - } - } - - return objects; -} - -- (QSObject *)objectForPost:(NSDictionary *)post { - QSObject *newObject = [QSObject makeObjectWithIdentifier:[post objectForKey:@"hash"]]; - [newObject setObject:[post objectForKey:@"href"] forType:QSURLType]; - [newObject setName:[post objectForKey:@"description"]]; - [newObject setDetails:[post objectForKey:@"extended"]]; - [newObject setPrimaryType:QSURLType]; - return newObject; -} - -#pragma mark - NSXMLParserDelegate - -- (void)parser:(NSXMLParser*)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { - if ([elementName isEqualToString:@"post"] && attributeDict) { - [self.posts addObject:attributeDict]; - } -} - -- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { - // Implementation if needed -} - -@end diff --git a/QSDeliciousPlugIn.xcodeproj/project.pbxproj b/QSDeliciousPlugIn.xcodeproj/project.pbxproj index ebe2b52..d68169d 100644 --- a/QSDeliciousPlugIn.xcodeproj/project.pbxproj +++ b/QSDeliciousPlugIn.xcodeproj/project.pbxproj @@ -42,8 +42,6 @@ B5CF7D622E6B73C9008A0EE6 /* QSLinkdingProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSLinkdingProvider.m; sourceTree = ""; }; B5CF7D632E6B73D1008A0EE6 /* QSBookmarkProviderFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSBookmarkProviderFactory.h; sourceTree = ""; }; B5CF7D642E6B73DA008A0EE6 /* QSBookmarkProviderFactory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSBookmarkProviderFactory.m; sourceTree = ""; }; - B5CF7D672E6B7457008A0EE6 /* QSDeliciousPlugInTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousPlugInTests.m; sourceTree = ""; }; - B5CF7D682E6B746E008A0EE6 /* REFACTOR_GUIDE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = REFACTOR_GUIDE.md; sourceTree = ""; }; B5CF7D692E6B74FE008A0EE6 /* QSDeliciousPlugIn_Source.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QSDeliciousPlugIn_Source.xib; sourceTree = ""; }; D475F9AF18B2992D0012243C /* Common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = ""; }; D475F9B018B2992D0012243C /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; @@ -129,23 +127,53 @@ 32DBCF9F0370C38200C91783 /* Other Sources */ = { isa = PBXGroup; children = ( - B5CF7D5C2E6B7370008A0EE6 /* SocialSite.h */, - B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */, - B5CF7D5E2E6B737E008A0EE6 /* QSBookmarkProvider.h */, + B59368062E6EC67D00DBD0F1 /* Factories */, + B59368052E6EC66500DBD0F1 /* Protocols */, + B59368042E6EC65000DBD0F1 /* Types */, + B59368032E6EC63700DBD0F1 /* Providers */, + B5CF7D692E6B74FE008A0EE6 /* QSDeliciousPlugIn_Source.xib */, + E182BE3A06FC9B13007BF2C2 /* Localizable.strings */, + E182BCCF06FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h */, + E182BCD006FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m */, + ); + name = "Other Sources"; + sourceTree = ""; + }; + B59368032E6EC63700DBD0F1 /* Providers */ = { + isa = PBXGroup; + children = ( B5CF7D5F2E6B7386008A0EE6 /* QSDeliciousAPIProvider.h */, B5CF7D602E6B73A0008A0EE6 /* QSDeliciousAPIProvider.m */, B5CF7D612E6B73A5008A0EE6 /* QSLinkdingProvider.h */, B5CF7D622E6B73C9008A0EE6 /* QSLinkdingProvider.m */, + ); + path = Providers; + sourceTree = ""; + }; + B59368042E6EC65000DBD0F1 /* Types */ = { + isa = PBXGroup; + children = ( + B5CF7D5C2E6B7370008A0EE6 /* SocialSite.h */, + B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */, + ); + path = Types; + sourceTree = ""; + }; + B59368052E6EC66500DBD0F1 /* Protocols */ = { + isa = PBXGroup; + children = ( + B5CF7D5E2E6B737E008A0EE6 /* QSBookmarkProvider.h */, + ); + path = Protocols; + sourceTree = ""; + }; + B59368062E6EC67D00DBD0F1 /* Factories */ = { + isa = PBXGroup; + children = ( B5CF7D632E6B73D1008A0EE6 /* QSBookmarkProviderFactory.h */, B5CF7D642E6B73DA008A0EE6 /* QSBookmarkProviderFactory.m */, - B5CF7D672E6B7457008A0EE6 /* QSDeliciousPlugInTests.m */, - B5CF7D682E6B746E008A0EE6 /* REFACTOR_GUIDE.md */, - B5CF7D692E6B74FE008A0EE6 /* QSDeliciousPlugIn_Source.xib */, - E182BE3A06FC9B13007BF2C2 /* Localizable.strings */, - E182BCCF06FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h */, - E182BCD006FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m */, ); - name = "Other Sources"; + path = Factories; sourceTree = ""; }; D475F9AE18B2992D0012243C /* Configuration */ = { diff --git a/QSDeliciousPlugInTests.m b/QSDeliciousPlugInTests.m deleted file mode 100644 index 3aff4ff..0000000 --- a/QSDeliciousPlugInTests.m +++ /dev/null @@ -1,89 +0,0 @@ -// -// QSDeliciousPlugInTests.m -// QSDeliciousPlugIn -// - -#import -#import "QSBookmarkProviderFactory.h" -#import "QSDeliciousAPIProvider.h" -#import "QSLinkdingProvider.h" -#import "SocialSite.h" - -@suite("QSDeliciousPlugIn Strategy Pattern Tests") -struct QSDeliciousPlugInTests { - - @Test("Factory returns correct provider for Delicious") - func testDeliciousProvider() async throws { - QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; - - id provider = [factory providerForSite:SocialSiteDelicious - username:@"testuser" - password:@"testpass" - host:nil]; - - #expect(provider != nil, "Should return a provider for Delicious"); - #expect([provider isKindOfClass:[QSDeliciousAPIProvider class]], "Should return a QSDeliciousAPIProvider for Delicious"); - #expect([provider supportedSite] == SocialSiteDelicious, "Provider should support Delicious site"); - } - - @Test("Factory returns correct provider for Pinboard") - func testPinboardProvider() async throws { - QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; - - id provider = [factory providerForSite:SocialSitePinboard - username:@"testuser" - password:@"testpass" - host:nil]; - - #expect(provider != nil, "Should return a provider for Pinboard"); - #expect([provider isKindOfClass:[QSDeliciousAPIProvider class]], "Should return a QSDeliciousAPIProvider for Pinboard"); - #expect([provider supportedSite] == SocialSitePinboard, "Provider should support Pinboard site"); - } - - @Test("Factory returns correct provider for Linkding") - func testLinkdingProvider() async throws { - QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; - - id provider = [factory providerForSite:SocialSiteLinkding - username:@"testuser" - password:@"testtoken" - host:@"https://bookmarks.example.com"]; - - #expect(provider != nil, "Should return a provider for Linkding"); - #expect([provider isKindOfClass:[QSLinkdingProvider class]], "Should return a QSLinkdingProvider for Linkding"); - #expect([provider supportedSite] == SocialSiteLinkding, "Provider should support Linkding site"); - } - - @Test("Factory returns nil for invalid configuration") - func testInvalidConfiguration() async throws { - QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; - - // Test with empty username - id provider = [factory providerForSite:SocialSiteDelicious - username:@"" - password:@"testpass" - host:nil]; - - #expect(provider == nil, "Should return nil for empty username"); - - // Test Linkding without host - provider = [factory providerForSite:SocialSiteLinkding - username:@"testuser" - password:@"testtoken" - host:@""]; - - #expect(provider == nil, "Should return nil for Linkding without host"); - } - - @Test("SocialSite helper methods work correctly") - func testSocialSiteHelpers() async throws { - #expect([[SocialSiteHelper displayNameForSite:SocialSiteDelicious] isEqualToString:@"del.icio.us"]); - #expect([[SocialSiteHelper displayNameForSite:SocialSiteLinkding] isEqualToString:@"Linkding"]); - - #expect([[SocialSiteHelper siteURLForSite:SocialSitePinboard] isEqualToString:@"pinboard.in"]); - #expect([[SocialSiteHelper siteURLForSite:SocialSiteLinkding] isEqualToString:@""]); - - #expect([[SocialSiteHelper reversedSiteURLForSite:SocialSiteDelicious] isEqualToString:@"us.icio.del"]); - #expect([[SocialSiteHelper reversedSiteURLForSite:SocialSiteLinkding] isEqualToString:@"linkding"]); - } -} diff --git a/QSDeliciousPlugIn_Source.h b/QSDeliciousPlugIn_Source.h index 1eff1c7..56456c5 100644 --- a/QSDeliciousPlugIn_Source.h +++ b/QSDeliciousPlugIn_Source.h @@ -17,6 +17,7 @@ IBOutlet NSTextField *passField; IBOutlet NSTextField *hostField; } +@property (nonatomic, strong) NSString *internalPassword; - (IBAction)settingsChanged:(id)sender; @end diff --git a/QSDeliciousPlugIn_Source.m b/QSDeliciousPlugIn_Source.m index fb09a97..55eedf0 100644 --- a/QSDeliciousPlugIn_Source.m +++ b/QSDeliciousPlugIn_Source.m @@ -9,9 +9,21 @@ #import "QSDeliciousPlugIn_Source.h" #import #import +#import @implementation QSDeliciousPlugIn_Source +#pragma mark - Lifecycle + +// This method will get called whenever we change which +// active entry is selected. +- (void)setSelectedEntry:(id)selectedEntry { + [super setSelectedEntry:selectedEntry]; + [self loadPasswordFromKeychain]; +} + +#pragma mark - Quicksilver Source Methods + - (BOOL)indexIsValidFromDate:(NSDate *)indexDate forEntry:(NSDictionary *)theEntry { return -[indexDate timeIntervalSinceNow] < 24 * 60 * 60; } @@ -48,7 +60,7 @@ } - (NSString *)currentPassword { - return [self.selectedEntry.sourceSettings objectForKey:@"password"]; + return self.internalPassword; } - (BOOL)includeTags { @@ -56,174 +68,159 @@ } +// This method is called on action from all the NIB methods +// to force the catalog to save the current values. - (IBAction)settingsChanged:(id)sender { [[NSNotificationCenter defaultCenter] postNotificationName:QSCatalogEntryChangedNotification object:self.selectedEntry]; } -#pragma mark - Keychain Access - -- (SecProtocolType)protocolTypeForString:(NSString *)protocol { - if ([protocol isEqualToString:@"ftp"]) return kSecProtocolTypeFTP; - else if ([protocol isEqualToString:@"http"]) return kSecProtocolTypeHTTP; - else if ([protocol isEqualToString:@"sftp"]) return kSecProtocolTypeFTPS; - else if ([protocol isEqualToString:@"eppc"]) return kSecProtocolTypeEPPC; - else if ([protocol isEqualToString:@"afp"]) return kSecProtocolTypeAFP; - else if ([protocol isEqualToString:@"smb"]) return kSecProtocolTypeSMB; - else if ([protocol isEqualToString:@"ssh"]) return kSecProtocolTypeSSH; - else if ([protocol isEqualToString:@"telnet"]) return kSecProtocolTypeTelnet; - return 0; -} -- (NSString *)passwordForHost:(NSString *)host user:(NSString *)user andType:(SecProtocolType)type { - const char *buffer; - UInt32 length = 0; - OSErr err; - - err = SecKeychainFindInternetPassword(NULL, - (UInt32)[host length], [host UTF8String], - 0, - NULL, - (UInt32)[user length], [user UTF8String], - 0, NULL, - 0, - type, - 0, - &length, (void**)&buffer, - NULL); - - if (err == noErr) { - NSString *password = [NSString stringWithUTF8String:buffer]; - SecKeychainItemFreeContent(NULL,(void *)buffer); - return password; - } - return nil; -} +#pragma mark - Keychain Helper Methods -- (NSString *)passwordForHost:(NSString *)host user:(NSString *)user andScheme:(NSString *)scheme { - NSString *password = nil; - - SecProtocolType type = [self protocolTypeForString:scheme]; - - password = [self passwordForHost:host user:user andType:type]; - - if (!password && type == kSecProtocolTypeFTP) - password = [self passwordForHost:host user:user andType:kSecProtocolTypeFTPAccount]; // Workaround for Transmit's old type usage - if ( !password ) - password = [self passwordForHost:host user:user andType:0]; - if ( !password ) - NSLog(@"Couldn't find password. URL:%@ %@ %@", host, user,scheme ); - return password; +- (NSString *)keychainKeyForIdentifier:(NSString *)identifier { + return [NSString stringWithFormat:@"QSSocialBookmarks-%@", identifier]; } -- (NSString *)keychainPasswordForURL:(NSURL *)url { - return [self passwordForHost:[url host] user:[url user] andScheme:[url scheme]]; +- (NSString *)passwordFromKeychainForKey:(NSString *)key { + const char *service = "QSSocialBookmarks"; + const char *account = [key UTF8String]; + + UInt32 passwordLength = 0; + void *passwordData = NULL; + + OSStatus status = SecKeychainFindGenericPassword(NULL, + (UInt32)strlen(service), service, + (UInt32)strlen(account), account, + &passwordLength, &passwordData, + NULL); + + if (status == errSecSuccess && passwordData != NULL) { + NSString *password = [[NSString alloc] initWithBytes:passwordData + length:passwordLength + encoding:NSUTF8StringEncoding]; + SecKeychainItemFreeContent(NULL, passwordData); + return password; + } + + return nil; } -- (OSErr)addURLPasswordToKeychain:(NSURL *)url { - OSErr err; - - NSString *host = [url host]; - NSString *user = [url user]; - NSString *pass = [url password]; - - SecProtocolType type = [self protocolTypeForString:[url scheme]]; - - SecKeychainItemRef existing = NULL; - - err = SecKeychainFindInternetPassword(NULL, - (UInt32)[host length], [host UTF8String], - 0, NULL, - (UInt32)[user length], [user UTF8String], - 0, NULL, - 0, - type, - 0, - NULL,NULL, - &existing); - - if ( !err ) { - err = SecKeychainItemModifyContent( existing, NULL, (UInt32)[pass length], [pass UTF8String] ); - CFRelease( existing ); - } else { - err = SecKeychainAddInternetPassword(NULL, - (UInt32)[host length], [host UTF8String], - 0, NULL, - (UInt32)[user length], [user UTF8String], - 0, NULL, - 0, - type, - 0, - (UInt32)[pass length], [pass UTF8String], +- (OSStatus)savePasswordToKeychainForKey:(NSString *)key password:(NSString *)password { + const char *service = "QSSocialBookmarks"; + const char *account = [key UTF8String]; + const char *passwordCString = [password UTF8String]; + + // First try to find existing item + SecKeychainItemRef item = NULL; + OSStatus findStatus = SecKeychainFindGenericPassword(NULL, + (UInt32)strlen(service), service, + (UInt32)strlen(account), account, + NULL, NULL, + &item); + + OSStatus status; + if (findStatus == errSecSuccess) { + // Update existing item + status = SecKeychainItemModifyAttributesAndData(item, + NULL, + (UInt32)strlen(passwordCString), + passwordCString); + CFRelease(item); + } else { + // Create new item + status = SecKeychainAddGenericPassword(NULL, + (UInt32)strlen(service), service, + (UInt32)strlen(account), account, + (UInt32)strlen(passwordCString), passwordCString, NULL); } - - return err; + + return status; } -- (NSString *)oldCurrentPassword { - NSString *account = [self currentUsername]; - if (!account) return nil; - - SocialSite site = [self siteIndex]; - NSString *host = nil; +- (OSStatus)deletePasswordFromKeychainForKey:(NSString *)key { + const char *service = "QSSocialBookmarks"; + const char *account = [key UTF8String]; - // For Linkding, use the custom host; for others, use the standard site URL - if (site == SocialSiteLinkding) { - host = [self currentHost]; - if (!host) return nil; - } else { - host = [SocialSiteHelper siteURLForSite:site]; + SecKeychainItemRef item = NULL; + OSStatus findStatus = SecKeychainFindGenericPassword(NULL, + (UInt32)strlen(service), service, + (UInt32)strlen(account), account, + NULL, NULL, + &item); + + if (findStatus == errSecSuccess) { + OSStatus deleteStatus = SecKeychainItemDelete(item); + CFRelease(item); + return deleteStatus; } - - NSURL *keychainURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@@%@/", account, host]]; - NSString *password = [self keychainPasswordForURL:keychainURL]; - - return password; + + return findStatus; } -- (void)setCurrentPassword:(NSString *)newPassword { - NSString *account = [self currentUsername]; - if (!account) return; - if ([newPassword length] <= 0) return; - - SocialSite site = [self siteIndex]; - NSString *host = nil; +#pragma mark - Password Keychain Methods + +- (void)loadPasswordFromKeychain { + if (!self.selectedEntry || !self.selectedEntry.identifier) { + self.internalPassword = nil; + return; + } - // For Linkding, use the custom host; for others, use the standard site URL - if (site == SocialSiteLinkding) { - host = [self currentHost]; - if (!host) return; - } else { - host = [SocialSiteHelper siteURLForSite:site]; + NSString *keychainKey = [self keychainKeyForIdentifier:self.selectedEntry.identifier]; + [self setPassword: [self passwordFromKeychainForKey:keychainKey]]; +} + +- (void)savePasswordToKeychain { + if (!self.selectedEntry || !self.selectedEntry.identifier || !self.internalPassword) { + return; + } + + NSString *keychainKey = [self keychainKeyForIdentifier:self.selectedEntry.identifier]; + OSStatus status = [self savePasswordToKeychainForKey:keychainKey password:self.internalPassword]; + + if (status != errSecSuccess) { + NSLog(@"Failed to save password to keychain for key: %@, status: %d", keychainKey, (int)status); } - - NSURL *keychainURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%@@%@/", account, newPassword, host]]; - - [self addURLPasswordToKeychain:keychainURL]; +} + +#pragma mark - Password Property Accessors + +- (NSString *)password { + return self.internalPassword; +} + +- (void)setPassword:(NSString *)password { + self.internalPassword = password; + + // Save to keychain asynchronously + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self savePasswordToKeychain]; + }); } #pragma mark - Objects For Entry -- (NSArray *)objectsForEntry:(NSDictionary *)theEntry { - NSLog(@"WE HAVE BEEN REQUESTED"); +- (NSArray *)objectsForEntry:(QSCatalogEntry *)theEntry { - SocialSite site = [self siteIndex]; - NSString *username = [self currentUsername]; - NSString *password = [self currentPassword]; - NSString *host = [self currentHost]; - BOOL includeTags = [self includeTags]; + NSDictionary *settings = theEntry.sourceSettings; + + SocialSite site = [settings objectForKey:@"site"] != nil ? [[settings objectForKey:@"site"] integerValue] : SocialSiteDelicious; + NSString *username = [settings objectForKey:@"username"]; + NSString *identifier = theEntry.identifier; + NSString *keychainKey = [self keychainKeyForIdentifier:identifier]; + NSString *password = [self passwordFromKeychainForKey:keychainKey]; + NSString *host = [settings objectForKey:@"host"]; + BOOL includeTags = [settings objectForKey:@"includeTags"]; - // Get the appropriate provider using the factory QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; id provider = [factory providerForSite:site username:username password:password host:host]; - NSLog(@"Checking for %ld, user %@, pass %@, host %@", (long)site, username, password, host); if (!provider) { NSLog(@"No provider available for site %ld with username %@", (long)site, username); return @[]; } - return [provider fetchBookmarksForSite:site username:username password:password host:host includeTags:includeTags]; + return [provider fetchBookmarksForSite:site username:username password:password identifier:identifier host:host includeTags:includeTags]; } - (NSArray *)objectsForTag:(NSString *)tag username:(NSString *)username { @@ -254,10 +251,37 @@ [object setIcon:[[NSBundle bundleForClass:[self class]]imageNamed:@"bookmark_icon"]]; } +// This will receive a tag object. Tag objects will have the +// source configuration in the meta: source.username, +// source.site, source.host and source.identifier. - (BOOL)loadChildrenForObject:(QSObject *)object { - SocialSite site = [self siteIndex]; - - NSString *tagType = nil; + + NSNumber *siteNumber = [object objectForMeta:@"source.site"]; + if (!siteNumber) { + NSLog(@"The tag didn't have a valid site."); + return NO; + } + + SocialSite site = [siteNumber integerValue]; + + NSString *username = [object objectForMeta:@"source.username"]; + if (!username) { + NSLog(@"The tag didn't have a valid username."); + return NO; + } + NSString *identifier = [object objectForMeta:@"source.identifier"]; + if (!identifier) { + NSLog(@"The tag didn't have a valid identifier."); + return NO; + } + + NSString *host = [object objectForMeta:@"source.host"]; + if (site == SocialSiteLinkding && !host) { + NSLog(@"The tag didn't have a host, and its site requires it."); + return NO; + } + + NSString *tagType = nil; if (site == SocialSiteLinkding) { tagType = @"tag.linkding"; } else { @@ -265,18 +289,11 @@ tagType = [NSString stringWithFormat:@"tag.%@", reversedURL]; } - NSString *tag = [object objectForType:tagType]; - if (!tag) return NO; - - NSString *username = nil; - if (site == SocialSiteLinkding) { - username = [object objectForMeta:@"linkding.username"]; - } else { - NSString *reversedURL = [SocialSiteHelper reversedSiteURLForSite:site]; - username = [object objectForMeta:[NSString stringWithFormat:@"%@.username", reversedURL]]; - } - - if (!username) return NO; + NSString *tag = [object objectForType:tagType]; + if (!tag) { + NSLog(@"We could not find a valid tag type."); + return NO; + } NSArray *children = [self objectsForTag:tag username:username]; [object setChildren:children]; diff --git a/QSDeliciousPlugIn_Source.xib b/QSDeliciousPlugIn_Source.xib index 83b620f..c688346 100644 --- a/QSDeliciousPlugIn_Source.xib +++ b/QSDeliciousPlugIn_Source.xib @@ -144,7 +144,7 @@ - + diff --git a/QSLinkdingProvider.h b/QSLinkdingProvider.h deleted file mode 100644 index 28807e8..0000000 --- a/QSLinkdingProvider.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// QSLinkdingProvider.h -// QSDeliciousPlugIn -// -// Provider for Linkding API (JSON-based with API key) -// - -#import -#import "QSBookmarkProvider.h" - -@interface QSLinkdingProvider : NSObject - -- (NSData *)cachedBookmarkDataForHost:(NSString *)host username:(NSString *)username; -- (void)cacheBookmarkData:(NSData *)data forHost:(NSString *)host username:(NSString *)username; - -@end \ No newline at end of file diff --git a/QSLinkdingProvider.m b/QSLinkdingProvider.m deleted file mode 100644 index f97f5c7..0000000 --- a/QSLinkdingProvider.m +++ /dev/null @@ -1,191 +0,0 @@ -// -// QSLinkdingProvider.m -// QSDeliciousPlugIn -// - -#import "QSLinkdingProvider.h" -#import "SocialSite.h" -#import - -@implementation QSLinkdingProvider - -- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - return (site == SocialSiteLinkding) && - username.length > 0 && - password.length > 0 && // password is API token for Linkding - host.length > 0; -} - -- (SocialSite)supportedSite { - return SocialSiteLinkding; -} - -- (NSString *)providerName { - return @"Linkding"; -} - -- (NSString *)tagURLType { - return @"tag.linkding"; -} - -- (NSData *)cachedBookmarkDataForHost:(NSString *)host username:(NSString *)username { - // Create a safe filename from host - NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; - NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/linkding/"], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", safeHost, username]]; - return [NSData dataWithContentsOfFile:cachePath]; -} - -- (void)cacheBookmarkData:(NSData *)data forHost:(NSString *)host username:(NSString *)username { - // Create a safe filename from host - NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; - NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/linkding/"], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", safeHost, username]]; - [data writeToFile:cachePath atomically:NO]; -} - -- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host includeTags:(BOOL)includeTags { - - if (![self canHandleSite:site username:username password:password host:host]) { - return @[]; - } - - // Try cached data first - NSData *data = [self cachedBookmarkDataForHost:host username:username]; - - // If no cached data, fetch from API - if (![data length]) { - // Construct Linkding API URL - NSString *baseURL = host; - if (![baseURL hasPrefix:@"http://"] && ![baseURL hasPrefix:@"https://"]) { - baseURL = [NSString stringWithFormat:@"https://%@", baseURL]; - } - if ([baseURL hasSuffix:@"/"]) { - baseURL = [baseURL substringToIndex:[baseURL length] - 1]; - } - - NSString *urlString = [NSString stringWithFormat:@"%@/api/bookmarks/", baseURL]; - NSURL *requestURL = [NSURL URLWithString:urlString]; - - if (!requestURL) { - NSLog(@"Invalid Linkding host URL: %@", host); - return @[]; - } - - NSLog(@"WE ARE ABOUT TO REQUEST TO URL: %@", requestURL); - - NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL - cachePolicy:NSURLRequestUseProtocolCachePolicy - timeoutInterval:60.0]; - [theRequest setHTTPMethod:@"GET"]; - [theRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; - [theRequest setValue:[NSString stringWithFormat:@"Token %@", password] forHTTPHeaderField:@"Authorization"]; - [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"]; - - NSError *error = nil; - data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error]; - - if (error) { - NSLog(@"Error fetching Linkding bookmarks: %@", error.localizedDescription); - return @[]; - } - - // Cache the data - [self cacheBookmarkData:data forHost:host username:username]; - } - - // Parse JSON data - NSError *jsonError = nil; - NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; - - if (jsonError) { - NSLog(@"Error parsing Linkding JSON: %@", jsonError.localizedDescription); - return @[]; - } - - NSArray *results = [jsonResponse objectForKey:@"results"]; - if (!results || ![results isKindOfClass:[NSArray class]]) { - NSLog(@"Invalid Linkding response format"); - return @[]; - } - - NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; - NSMutableSet *tagSet = [NSMutableSet set]; - - // Create bookmark objects - for (NSDictionary *bookmark in results) { - QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; - if (newObject) { - [objects addObject:newObject]; - - // Collect tags if requested - if (includeTags) { - NSArray *tags = [bookmark objectForKey:@"tag_names"]; - if (tags && [tags isKindOfClass:[NSArray class]]) { - [tagSet addObjectsFromArray:tags]; - } - } - } - } - - // Create tag objects if requested - if (includeTags) { - for (NSString *tag in tagSet) { - if (tag.length > 0) { - QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[Linkding tag]:%@", tag]]; - [tagObject setObject:tag forType:[self tagURLType]]; - [tagObject setObject:username forMeta:@"linkding.username"]; - [tagObject setObject:host forMeta:@"linkding.host"]; - [tagObject setName:tag]; - [tagObject setPrimaryType:[self tagURLType]]; - [objects addObject:tagObject]; - } - } - } - - return objects; -} - -- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - NSData *data = [self cachedBookmarkDataForHost:host username:username]; - if (!data) return @[]; - - NSError *jsonError; - NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; - - if (jsonError) return @[]; - - NSArray *results = [jsonResponse objectForKey:@"results"]; - if (!results || ![results isKindOfClass:[NSArray class]]) return @[]; - - NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; - - for (NSDictionary *bookmark in results) { - NSArray *tags = [bookmark objectForKey:@"tag_names"]; - if (tags && [tags isKindOfClass:[NSArray class]] && [tags containsObject:tag]) { - QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; - if (newObject) { - [objects addObject:newObject]; - } - } - } - - return objects; -} - -- (QSObject *)objectForLinkdingBookmark:(NSDictionary *)bookmark { - NSNumber *bookmarkId = [bookmark objectForKey:@"id"]; - NSString *url = [bookmark objectForKey:@"url"]; - NSString *title = [bookmark objectForKey:@"title"]; - NSString *description = [bookmark objectForKey:@"description"]; - - if (!bookmarkId || !url) return nil; - - QSObject *newObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"linkding-%@", bookmarkId]]; - [newObject setObject:url forType:QSURLType]; - [newObject setName:title.length > 0 ? title : url]; - [newObject setDetails:description.length > 0 ? description : @""]; - [newObject setPrimaryType:QSURLType]; - - return newObject; -} - -@end diff --git a/REFACTOR_GUIDE.md b/REFACTOR_GUIDE.md deleted file mode 100644 index 2176a98..0000000 --- a/REFACTOR_GUIDE.md +++ /dev/null @@ -1,118 +0,0 @@ -# QSDeliciousPlugIn Refactor Guide - -This document outlines the refactoring changes made to support multiple social bookmark providers using the Strategy Pattern. - -## Overview - -The plugin has been refactored from a monolithic implementation to a modular, extensible architecture that makes it easy to add new bookmark providers. - -## Architecture - -### Core Components - -1. **SocialSite Enum** (`SocialSite.h/.m`) - - Defines supported bookmark services - - Helper methods for display names and URLs - -2. **QSBookmarkProvider Protocol** (`QSBookmarkProvider.h`) - - Defines the interface all providers must implement - - Key methods: `canHandleSite:username:password:host:`, `fetchBookmarksForSite:username:password:host:includeTags:` - -3. **QSBookmarkProviderFactory** (`QSBookmarkProviderFactory.h/.m`) - - Singleton factory that manages all providers - - Returns the appropriate provider for a given configuration - -4. **Provider Implementations** - - `QSDeliciousAPIProvider`: Handles XML-based Delicious / Pinboard v1 API (Delicious, Magnolia, Pinboard) - - `QSLinkdingProvider`: Handles JSON-based Linkding API - -### Strategy Pattern Implementation - -The main source file now uses the strategy pattern: - -```objective-c -// Get the appropriate provider using the factory -QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; -id provider = [factory providerForSite:site username:username password:password host:host]; - -if (!provider) { - NSLog(@"No provider available for site %ld with username %@", (long)site, username); - return @[]; -} - -return [provider fetchBookmarksForSite:site username:username password:password host:host includeTags:includeTags]; -``` - -## Supported Services - -| Service | ID | API Type | Authentication | Host Required | -|---------|----|---------| -------------- | ------------- | -| del.icio.us | 0 | XML/Basic Auth | Username/Password | No | -| ma.gnolia.com | 1 | XML/Basic Auth | Username/Password | No | -| Pinboard | 2 | XML/Basic Auth | Username/Password | No | -| Linkding | 3 | JSON/Token Auth | Username/API Token | Yes | - -## Adding New Providers - -1. Add a new case to the `SocialSite` enum -2. Update `SocialSiteHelper` methods -3. Create a new provider class implementing `QSBookmarkProvider` -4. Add the provider to `QSBookmarkProviderFactory.setupProviders` - -Example new provider structure: - -```objective-c -@interface QSMyNewProvider : NSObject -@end - -@implementation QSMyNewProvider - -- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - return (site == SocialSiteMyNew) && username.length > 0 && password.length > 0; -} - -- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host includeTags:(BOOL)includeTags { - // Implementation here -} - -// ... other required methods -@end -``` - -## Interface Bindings - -The NIB file should be updated to include: - -- **Settings Dictionary** with keys: - - `username` (NSString) - - `password` (NSString) - bound to File's Owner directly - - `site` (NSInteger) - SocialSite enum value - - `host` (NSString) - required for Linkding, optional for others - - `includeTags` (BOOL) - -## Migration from Old Code - -The original `QSDeliciousPlugIn_Source.m` has been refactored into `QSDeliciousPlugIn_Source_New.m`. Key changes: - -1. Removed hardcoded site logic -2. Removed XML parsing from main class (moved to providers) -3. Added strategy pattern implementation -4. Added support for custom hosts (Linkding) -5. Simplified the main object fetching logic - -## Testing - -Tests are included in `QSDeliciousPlugInTests.m` using Swift Testing framework: -- Factory provider selection tests -- Configuration validation tests -- Helper method tests - -## Linkding Configuration - -For Linkding users: -1. Set Site to "Linkding" (value 3) -2. Enter your Linkding server URL in the Host field (e.g., `https://bookmarks.example.com`) -3. Use your API Token as the Password -4. Enter your username (though it's mainly for caching purposes in Linkding) - -The Linkding provider will automatically handle URL construction and JSON parsing. diff --git a/SocialSite.h b/SocialSite.h deleted file mode 100644 index f2c848c..0000000 --- a/SocialSite.h +++ /dev/null @@ -1,23 +0,0 @@ -// -// SocialSite.h -// QSDeliciousPlugIn -// -// Social bookmark site enumeration -// - -#import - -typedef NS_ENUM(NSInteger, SocialSite) { - SocialSiteDelicious = 0, - SocialSiteMagnolia = 1, - SocialSitePinboard = 2, - SocialSiteLinkding = 3 -}; - -@interface SocialSiteHelper : NSObject - -+ (NSString *)displayNameForSite:(SocialSite)site; -+ (NSString *)siteURLForSite:(SocialSite)site; -+ (NSString *)reversedSiteURLForSite:(SocialSite)site; - -@end \ No newline at end of file diff --git a/SocialSite.m b/SocialSite.m deleted file mode 100644 index d002235..0000000 --- a/SocialSite.m +++ /dev/null @@ -1,55 +0,0 @@ -// -// SocialSite.m -// QSDeliciousPlugIn -// - -#import "SocialSite.h" - -@implementation SocialSiteHelper - -+ (NSString *)displayNameForSite:(SocialSite)site { - switch (site) { - case SocialSiteDelicious: - return @"del.icio.us"; - case SocialSiteMagnolia: - return @"ma.gnolia.com"; - case SocialSitePinboard: - return @"Pinboard"; - case SocialSiteLinkding: - return @"Linkding"; - default: - return @"Unknown"; - } -} - -+ (NSString *)siteURLForSite:(SocialSite)site { - switch (site) { - case SocialSiteDelicious: - return @"del.icio.us"; - case SocialSiteMagnolia: - return @"ma.gnolia.com"; - case SocialSitePinboard: - return @"pinboard.in"; - case SocialSiteLinkding: - return @""; // Will be provided by user as host - default: - return nil; - } -} - -+ (NSString *)reversedSiteURLForSite:(SocialSite)site { - switch (site) { - case SocialSiteDelicious: - return @"us.icio.del"; - case SocialSiteMagnolia: - return @"com.gnolia.ma"; - case SocialSitePinboard: - return @"in.pinboard"; - case SocialSiteLinkding: - return @"linkding"; // Generic identifier - default: - return nil; - } -} - -@end \ No newline at end of file diff --git a/Types/SocialSite.h b/Types/SocialSite.h new file mode 100644 index 0000000..f2c848c --- /dev/null +++ b/Types/SocialSite.h @@ -0,0 +1,23 @@ +// +// SocialSite.h +// QSDeliciousPlugIn +// +// Social bookmark site enumeration +// + +#import + +typedef NS_ENUM(NSInteger, SocialSite) { + SocialSiteDelicious = 0, + SocialSiteMagnolia = 1, + SocialSitePinboard = 2, + SocialSiteLinkding = 3 +}; + +@interface SocialSiteHelper : NSObject + ++ (NSString *)displayNameForSite:(SocialSite)site; ++ (NSString *)siteURLForSite:(SocialSite)site; ++ (NSString *)reversedSiteURLForSite:(SocialSite)site; + +@end \ No newline at end of file diff --git a/Types/SocialSite.m b/Types/SocialSite.m new file mode 100644 index 0000000..d002235 --- /dev/null +++ b/Types/SocialSite.m @@ -0,0 +1,55 @@ +// +// SocialSite.m +// QSDeliciousPlugIn +// + +#import "SocialSite.h" + +@implementation SocialSiteHelper + ++ (NSString *)displayNameForSite:(SocialSite)site { + switch (site) { + case SocialSiteDelicious: + return @"del.icio.us"; + case SocialSiteMagnolia: + return @"ma.gnolia.com"; + case SocialSitePinboard: + return @"Pinboard"; + case SocialSiteLinkding: + return @"Linkding"; + default: + return @"Unknown"; + } +} + ++ (NSString *)siteURLForSite:(SocialSite)site { + switch (site) { + case SocialSiteDelicious: + return @"del.icio.us"; + case SocialSiteMagnolia: + return @"ma.gnolia.com"; + case SocialSitePinboard: + return @"pinboard.in"; + case SocialSiteLinkding: + return @""; // Will be provided by user as host + default: + return nil; + } +} + ++ (NSString *)reversedSiteURLForSite:(SocialSite)site { + switch (site) { + case SocialSiteDelicious: + return @"us.icio.del"; + case SocialSiteMagnolia: + return @"com.gnolia.ma"; + case SocialSitePinboard: + return @"in.pinboard"; + case SocialSiteLinkding: + return @"linkding"; // Generic identifier + default: + return nil; + } +} + +@end \ No newline at end of file -- cgit