diff --git a/Factories/QSBookmarkProviderFactory.h b/Factories/QSBookmarkProviderFactory.h new file mode 100644 index 0000000..bc0ae79 --- /dev/null +++ b/Factories/QSBookmarkProviderFactory.h @@ -0,0 +1,32 @@ +// +// QSBookmarkProviderFactory.h +// QSDeliciousPlugIn +// +// Factory for managing bookmark providers +// + +#import "QSBookmarkProvider.h" +#import "SocialSite.h" +#import + +@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..56b4146 --- /dev/null +++ b/Factories/QSBookmarkProviderFactory.m @@ -0,0 +1,70 @@ +// +// 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 Delicious / Pinboard API providers for each supported site + QSDeliciousAPIProvider *deliciousAPIProvider = + [[QSDeliciousAPIProvider alloc] init]; + + // Create Linkding provider + QSLinkdingProvider *linkdingProvider = [[QSLinkdingProvider alloc] init]; + + [mutableProviders addObject:deliciousAPIProvider]; + [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/Info.plist b/Info.plist index b916b8c..e84ffd7 100644 --- a/Info.plist +++ b/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 2.1.0 + 3.0.0 CFBundleVersion - 131 + 150 NSPrincipalClass QSDeliciousPlugIn QSPlugIn @@ -42,7 +42,7 @@ QSObjectHandlers - us.icio.del.tag + qs.tag.socialbookmark QSDeliciousPlugIn_Source QSObjectSources diff --git a/Protocols/QSBookmarkProvider.h b/Protocols/QSBookmarkProvider.h new file mode 100644 index 0000000..48e1417 --- /dev/null +++ b/Protocols/QSBookmarkProvider.h @@ -0,0 +1,45 @@ +// +// QSBookmarkProvider.h +// QSDeliciousPlugIn +// +// Protocol for social bookmark providers +// + +#import "SocialSite.h" +#import + +@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; + +@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; + +@end diff --git a/Providers/QSDeliciousAPIProvider.h b/Providers/QSDeliciousAPIProvider.h new file mode 100644 index 0000000..1628c38 --- /dev/null +++ b/Providers/QSDeliciousAPIProvider.h @@ -0,0 +1,30 @@ +// +// QSDeliciousAPIProvider.h +// QSDeliciousPlugIn +// +// Base provider for Pinboard v1 API (XML-based with Basic Auth) +// Used by Delicious, Magnolia, and Pinboard +// + +#import "QSBookmarkProvider.h" +#import + +@interface QSDeliciousAPIProvider + : NSObject + +@property(nonatomic, strong) NSMutableArray *posts; + +- (NSString *)apiURLForSite:(SocialSite)site andHost:(NSString *)host; +- (NSURL *)requestURLForSite:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + host:(NSString *)host; +- (NSData *)cachedBookmarkDataForSite:(SocialSite)site + username:(NSString *)username + host:(NSString *)host; +- (void)cacheBookmarkData:(NSData *)data + forSite:(SocialSite)site + username:(NSString *)username + host:(NSString *)host; + +@end diff --git a/Providers/QSDeliciousAPIProvider.m b/Providers/QSDeliciousAPIProvider.m new file mode 100644 index 0000000..a697618 --- /dev/null +++ b/Providers/QSDeliciousAPIProvider.m @@ -0,0 +1,260 @@ +// +// QSDeliciousAPIProvider.m +// QSDeliciousPlugIn +// + +#import "QSDeliciousAPIProvider.h" +#import "Constants.h" +#import "SocialSite.h" +#import + +@implementation QSDeliciousAPIProvider + +- (BOOL)canHandleSite:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + host:(NSString *)host { + return (site == SocialSiteDelicious || site == SocialSiteMagnolia || + site == SocialSitePinboard || + site == SocialSiteSelfHostedDeliciousCompatible) && + username.length > 0 && password.length > 0 && + (site != SocialSiteSelfHostedDeliciousCompatible || host.length > 0); +} + +- (NSString *)apiURLForSite:(SocialSite)site andHost:(NSString *)host { + switch (site) { + case SocialSiteDelicious: + return @"api.del.icio.us/v1"; + case SocialSiteMagnolia: + return @"ma.gnolia.com/api/mirrord/v1"; + case SocialSitePinboard: + return @"https://api.pinboard.in/v1"; + case SocialSiteSelfHostedDeliciousCompatible: + return [NSString stringWithFormat:@"%@/v1", host]; + default: + return nil; + } +} +- (BOOL)usesAuthToken:(SocialSite)site { + switch (site) { + case SocialSitePinboard: + case SocialSiteSelfHostedDeliciousCompatible: + return YES; + default: + return NO; + } +} + +- (NSURL *)requestURLForSite:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + host:(NSString *)host { + NSString *apiURL = [self apiURLForSite:site andHost:host]; + if (!apiURL) + return nil; + + NSString *urlString; + if ([self usesAuthToken:site]) { + // Pinboard and pinboard compatible sites require an + // auth token rather than a password. + urlString = [NSString + stringWithFormat:@"%@/posts/all?auth_token=%@", apiURL, password]; + } else { + urlString = [NSString stringWithFormat:@"https://%@:%@@%@/posts/all?", + username, password, apiURL]; + } + return [NSURL URLWithString:urlString]; +} + +#pragma mark - Cache + +- (NSString *)cachePathForSite:(SocialSite)site + username:(NSString *)username + host:(NSString *)host + create:(BOOL)create { + + NSString *siteURL = [SocialSiteHelper cacheKeyForSite:site]; + NSString *safeHost = + [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet + alphanumericCharacterSet] + invertedSet]] + componentsJoinedByString:@"-"]; + return [QSApplicationSupportSubPath( + [NSString stringWithFormat:@"Caches/%@/", siteURL], create) + stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.xml", + safeHost, + username]]; +} + +- (NSData *)cachedBookmarkDataForSite:(SocialSite)site + username:(NSString *)username + host:(NSString *)host { + NSString *cachePath = [self cachePathForSite:site + username:username + host:host + create:NO]; + return [NSData dataWithContentsOfFile:cachePath]; +} + +- (void)cacheBookmarkData:(NSData *)data + forSite:(SocialSite)site + username:(NSString *)username + host:(NSString *)host { + NSString *cachePath = [self cachePathForSite:site + username:username + host:host + create:YES]; + [data writeToFile:cachePath atomically:NO]; +} + +- (NSArray *)fetchBookmarksForSite:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + identifier:(NSString *)identifier + host:(NSString *)host + includeTags:(BOOL)includeTags { + + NSData *data = [self cachedBookmarkDataForSite:site + username:username + host:host]; + + 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:@"GET"]; + [theRequest setValue:@"text/xml" forHTTPHeaderField:@"Content-type"]; + [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" + forHTTPHeaderField:@"User-Agent"]; + + NSError *error = nil; + data = [NSURLConnection sendSynchronousRequest:theRequest + returningResponse:nil + error:&error]; + + if (error) { + NSLog(@"Error fetching bookmarks: %@", error.localizedDescription); + return @[]; + } + + [self cacheBookmarkData:data forSite:site username:username host:host]; + } + + 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]; + + for (NSDictionary *post in self.posts) { + QSObject *newObject = [self objectForPost:post]; + if (newObject) { + [objects addObject:newObject]; + + if (includeTags) { + NSString *tagString = [post objectForKey:@"tag"]; + if (tagString.length > 0) { + [tagSet + addObjectsFromArray:[tagString componentsSeparatedByString:@" "]]; + } + } + } + } + + if (includeTags) { + for (NSString *tag in tagSet) { + if (tag.length > 0) { + QSObject *tagObject = [QSObject + makeObjectWithIdentifier: + [NSString + stringWithFormat:@"[%@ tag]:%@", + [SocialSiteHelper displayNameForSite:site], + tag]]; + + [tagObject setObject:tag forType:kTagType]; + [tagObject setObject:@(site) forMeta:kTagSiteField]; + [tagObject setObject:username forMeta:kTagUsernameField]; + [tagObject setObject:host forMeta:kTagHostField]; + [tagObject setObject:identifier forMeta:kTagIdentifierField]; + [tagObject setName:tag]; + [tagObject setPrimaryType:kTagType]; + [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 + host:host]; + 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..0537a0c --- /dev/null +++ b/Providers/QSLinkdingProvider.h @@ -0,0 +1,19 @@ +// +// QSLinkdingProvider.h +// QSDeliciousPlugIn +// +// Provider for Linkding API (JSON-based with API key) +// + +#import "QSBookmarkProvider.h" +#import + +@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..32b4c16 --- /dev/null +++ b/Providers/QSLinkdingProvider.m @@ -0,0 +1,228 @@ +// +// QSLinkdingProvider.m +// QSDeliciousPlugIn +// + +#import "QSLinkdingProvider.h" +#import "Constants.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; +} + +- (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 @[]; + } + + NSData *data = [self cachedBookmarkDataForHost:host username:username]; + + if (![data length]) { + NSString *baseURL = host; + if (![baseURL hasPrefix:@"http://"] && ![baseURL hasPrefix:@"https://"]) { + baseURL = [NSString stringWithFormat:@"https://%@", baseURL]; + } + if ([baseURL hasSuffix:@"/"]) { + baseURL = [baseURL substringToIndex:[baseURL length] - 1]; + } + + // I'm being lazy with the limit for now. This should instead loop while + // there is a next, but then the caching will also need to be changed. If + // you have a particularly large linkding library, I apologize. + NSString *urlString = + [NSString stringWithFormat:@"%@/api/bookmarks/?limit=10000", 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 @[]; + } + + [self cacheBookmarkData:data forHost:host username:username]; + } + + 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]; + + for (NSDictionary *bookmark in results) { + QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; + if (newObject) { + [objects addObject:newObject]; + + if (includeTags) { + NSArray *tags = [bookmark objectForKey:@"tag_names"]; + if (tags && [tags isKindOfClass:[NSArray class]]) { + [tagSet addObjectsFromArray:tags]; + } + } + } + } + + if (includeTags) { + for (NSString *tag in tagSet) { + if (tag.length > 0) { + QSObject *tagObject = [QSObject + makeObjectWithIdentifier:[NSString + stringWithFormat:@"[Linkding tag]:%@", + tag]]; + [tagObject setObject:tag forType:kTagType]; + [tagObject setObject:@(site) forMeta:kTagSiteField]; + [tagObject setObject:username forMeta:kTagUsernameField]; + [tagObject setObject:host forMeta:kTagHostField]; + // We need the identifier to be able to fetch the keychain password + [tagObject setObject:identifier forMeta:kTagIdentifierField]; + [tagObject setName:tag]; + [tagObject setPrimaryType:kTagType]; + [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/QSDeliciousPlugIn.xcodeproj/project.pbxproj b/QSDeliciousPlugIn.xcodeproj/project.pbxproj index ce2a3d5..803ae6e 100644 --- a/QSDeliciousPlugIn.xcodeproj/project.pbxproj +++ b/QSDeliciousPlugIn.xcodeproj/project.pbxproj @@ -8,30 +8,44 @@ /* Begin PBXBuildFile section */ 28855EDF1207EA17003DC758 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28855EDE1207EA17003DC758 /* Security.framework */; }; - 7F8AD05107F2503600011548 /* QSDeliciousPlugInSource.nib in Resources */ = {isa = PBXBuildFile; fileRef = 7F8AD05007F2503600011548 /* QSDeliciousPlugInSource.nib */; }; 7F9441A10803A9D9007EDC31 /* QSObjectSource.name.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7F9441A00803A9D9007EDC31 /* QSObjectSource.name.strings */; }; 7FB0DC1A0B91FF8600A5B6FF /* bookmark_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 7FB0DC190B91FF8600A5B6FF /* bookmark_icon.png */; }; 7FB0DC2F0B91FFC600A5B6FF /* QSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FB0DC2D0B91FFC500A5B6FF /* QSCore.framework */; }; 7FB0DC300B91FFC600A5B6FF /* QSFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FB0DC2E0B91FFC600A5B6FF /* QSFoundation.framework */; }; 8D1AC9700486D14A00FE50C9 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD92D38A0106425D02CA0E72 /* Cocoa.framework */; }; + B59368092E6F04E200DBD0F1 /* Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = B59368082E6F04DF00DBD0F1 /* Constants.m */; }; + B5CF7D6A2E6B7631008A0EE6 /* QSDeliciousPlugIn_Source.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5CF7D692E6B74FE008A0EE6 /* QSDeliciousPlugIn_Source.xib */; }; + B5CF7D6E2E6B7A53008A0EE6 /* QSDeliciousAPIProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = B5CF7D602E6B73A0008A0EE6 /* QSDeliciousAPIProvider.m */; }; + B5CF7D6F2E6B7A53008A0EE6 /* QSBookmarkProviderFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = B5CF7D642E6B73DA008A0EE6 /* QSBookmarkProviderFactory.m */; }; + B5CF7D702E6B7A53008A0EE6 /* QSLinkdingProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = B5CF7D622E6B73C9008A0EE6 /* QSLinkdingProvider.m */; }; + B5CF7D712E6B7A53008A0EE6 /* SocialSite.m in Sources */ = {isa = PBXBuildFile; fileRef = B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */; }; D486257618B29CCE00D8CAE4 /* del.icio.us.png in Resources */ = {isa = PBXBuildFile; fileRef = D486257518B29CCE00D8CAE4 /* del.icio.us.png */; }; E182BCD306FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h in Resources */ = {isa = PBXBuildFile; fileRef = E182BCCF06FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h */; }; E182BCD406FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m in Sources */ = {isa = PBXBuildFile; fileRef = E182BCD006FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m */; }; - E182BE2506FC9AB5007BF2C2 /* QSDeliciousPrefPane.h in Resources */ = {isa = PBXBuildFile; fileRef = E182BE2206FC9AB5007BF2C2 /* QSDeliciousPrefPane.h */; }; - E182BE2606FC9AB5007BF2C2 /* QSDeliciousPrefPane.m in Sources */ = {isa = PBXBuildFile; fileRef = E182BE2306FC9AB5007BF2C2 /* QSDeliciousPrefPane.m */; }; E182BE3C06FC9B13007BF2C2 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E182BE3A06FC9B13007BF2C2 /* Localizable.strings */; }; E182BE5B06FC9C46007BF2C2 /* PreferencePanes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E182BE5A06FC9C46007BF2C2 /* PreferencePanes.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 28855EDE1207EA17003DC758 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; - 7F8AD05007F2503600011548 /* QSDeliciousPlugInSource.nib */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; path = QSDeliciousPlugInSource.nib; sourceTree = ""; }; 7F9441A00803A9D9007EDC31 /* QSObjectSource.name.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = QSObjectSource.name.strings; sourceTree = ""; }; 7FB0DC190B91FF8600A5B6FF /* bookmark_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bookmark_icon.png; sourceTree = ""; }; 7FB0DC2D0B91FFC500A5B6FF /* QSCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QSCore.framework; path = /Applications/Quicksilver.app/Contents/Frameworks/QSCore.framework; sourceTree = ""; }; 7FB0DC2E0B91FFC600A5B6FF /* QSFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QSFoundation.framework; path = /Applications/Quicksilver.app/Contents/Frameworks/QSFoundation.framework; sourceTree = ""; }; 8D1AC9730486D14A00FE50C9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 8D1AC9740486D14A00FE50C9 /* Social Bookmarks Plugin.qsplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Social Bookmarks Plugin.qsplugin"; sourceTree = BUILT_PRODUCTS_DIR; }; + B59368072E6F021E00DBD0F1 /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; + B59368082E6F04DF00DBD0F1 /* Constants.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Constants.m; sourceTree = ""; }; + B5CF7D5C2E6B7370008A0EE6 /* SocialSite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SocialSite.h; sourceTree = ""; }; + B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SocialSite.m; sourceTree = ""; }; + B5CF7D5E2E6B737E008A0EE6 /* QSBookmarkProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSBookmarkProvider.h; sourceTree = ""; }; + B5CF7D5F2E6B7386008A0EE6 /* QSDeliciousAPIProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSDeliciousAPIProvider.h; sourceTree = ""; }; + B5CF7D602E6B73A0008A0EE6 /* QSDeliciousAPIProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousAPIProvider.m; sourceTree = ""; }; + B5CF7D612E6B73A5008A0EE6 /* QSLinkdingProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSLinkdingProvider.h; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; D475F9B118B2992D0012243C /* Developer.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Developer.xcconfig; sourceTree = ""; }; @@ -42,9 +56,7 @@ DD92D38A0106425D02CA0E72 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; E182BCCF06FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = QSDeliciousPlugIn_Source.h; sourceTree = ""; }; E182BCD006FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousPlugIn_Source.m; sourceTree = ""; }; - E182BE2206FC9AB5007BF2C2 /* QSDeliciousPrefPane.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = QSDeliciousPrefPane.h; sourceTree = ""; }; - E182BE2306FC9AB5007BF2C2 /* QSDeliciousPrefPane.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousPrefPane.m; sourceTree = ""; }; - E182BE3B06FC9B13007BF2C2 /* en */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + E182BE3B06FC9B13007BF2C2 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E182BE5A06FC9C46007BF2C2 /* PreferencePanes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PreferencePanes.framework; path = /System/Library/Frameworks/PreferencePanes.framework; sourceTree = ""; }; /* End PBXFileReference section */ @@ -118,16 +130,57 @@ 32DBCF9F0370C38200C91783 /* Other Sources */ = { isa = PBXGroup; children = ( + B59368062E6EC67D00DBD0F1 /* Factories */, + B59368052E6EC66500DBD0F1 /* Protocols */, + B59368042E6EC65000DBD0F1 /* Types */, + B59368032E6EC63700DBD0F1 /* Providers */, + B5CF7D692E6B74FE008A0EE6 /* QSDeliciousPlugIn_Source.xib */, E182BE3A06FC9B13007BF2C2 /* Localizable.strings */, E182BCCF06FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h */, E182BCD006FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m */, - 7F8AD05007F2503600011548 /* QSDeliciousPlugInSource.nib */, - E182BE2206FC9AB5007BF2C2 /* QSDeliciousPrefPane.h */, - E182BE2306FC9AB5007BF2C2 /* QSDeliciousPrefPane.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 = ( + B59368072E6F021E00DBD0F1 /* Constants.h */, + B59368082E6F04DF00DBD0F1 /* Constants.m */, + 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 */, + ); + path = Factories; + sourceTree = ""; + }; D475F9AE18B2992D0012243C /* Configuration */ = { isa = PBXGroup; children = ( @@ -171,6 +224,11 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 0500; + TargetAttributes = { + 8D1AC9600486D14A00FE50C9 = { + ProvisioningStyle = Manual; + }; + }; }; buildConfigurationList = 7F07AFAE085E432E00E2AFC4 /* Build configuration list for PBXProject "QSDeliciousPlugIn" */; compatibilityVersion = "Xcode 3.2"; @@ -199,9 +257,8 @@ files = ( E182BCD306FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h in Resources */, D486257618B29CCE00D8CAE4 /* del.icio.us.png in Resources */, - E182BE2506FC9AB5007BF2C2 /* QSDeliciousPrefPane.h in Resources */, E182BE3C06FC9B13007BF2C2 /* Localizable.strings in Resources */, - 7F8AD05107F2503600011548 /* QSDeliciousPlugInSource.nib in Resources */, + B5CF7D6A2E6B7631008A0EE6 /* QSDeliciousPlugIn_Source.xib in Resources */, 7F9441A10803A9D9007EDC31 /* QSObjectSource.name.strings in Resources */, 7FB0DC1A0B91FF8600A5B6FF /* bookmark_icon.png in Resources */, ); @@ -231,8 +288,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B5CF7D6E2E6B7A53008A0EE6 /* QSDeliciousAPIProvider.m in Sources */, + B5CF7D6F2E6B7A53008A0EE6 /* QSBookmarkProviderFactory.m in Sources */, + B5CF7D702E6B7A53008A0EE6 /* QSLinkdingProvider.m in Sources */, + B5CF7D712E6B7A53008A0EE6 /* SocialSite.m in Sources */, E182BCD406FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m in Sources */, - E182BE2606FC9AB5007BF2C2 /* QSDeliciousPrefPane.m in Sources */, + B59368092E6F04E200DBD0F1 /* Constants.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/QSDeliciousPlugInSource.nib/designable.nib b/QSDeliciousPlugInSource.nib/designable.nib deleted file mode 100644 index 030965a..0000000 --- a/QSDeliciousPlugInSource.nib/designable.nib +++ /dev/null @@ -1,930 +0,0 @@ - - - - 1060 - 10F569 - 762 - 1038.29 - 461.00 - - YES - - YES - - - YES - - - - YES - - - - - YES - - - YES - - - - YES - - QSDeliciousPlugIn_Source - - - FirstResponder - - - NSApplication - - - 15 - 2 - {{62, 510}, {270, 151}} - 1886912512 - - Window - - NSWindow - - View - - {1.79769e+308, 1.79769e+308} - {213, 107} - - - 256 - - YES - - - 266 - {{89, 85}, {150, 19}} - - YES - - -1804468671 - 4326400 - - - LucidaGrande - 11 - 3100 - - - YES - - 6 - System - textBackgroundColor - - 3 - MQA - - - - 6 - System - textColor - - 3 - MAA - - - - - - - 264 - {{6, 88}, {78, 14}} - - YES - - 67239424 - 71434240 - Name: - - - - 6 - System - controlColor - - 3 - MC42NjY2NjY2NjY3AA - - - - 6 - System - controlTextColor - - - - - - - 264 - {{6, 61}, {78, 14}} - - YES - - 67239424 - 71434240 - Password: - - - - - - - - - 266 - {{89, 58}, {150, 19}} - - YES - - -1804468671 - 4326400 - - - - YES - - - - - - - 264 - {{86, 34}, {87, 18}} - - YES - - 67239424 - 131072 - Include tags - - - 1211912703 - 2 - - NSSwitch - - - - 200 - 25 - - - - - 264 - {{86, 113}, {126, 26}} - - YES - - -2076049856 - 2048 - - LucidaGrande - 13 - 1044 - - - 109199615 - 1 - - LucidaGrande - 13 - 16 - - - - - - 400 - 75 - - - del.icio.us - - 1048576 - 2147483647 - 1 - - NSImage - NSMenuCheckmark - - - NSImage - NSMenuMixedState - - _popUpItemAction: - - - YES - - - OtherViews - - - YES - - - - ma.gnolia - - 1048576 - 2147483647 - - - _popUpItemAction: - 1 - - - - - pinboard.in - - 1048576 - 2147483647 - - - _popUpItemAction: - 2 - - - - - 3 - YES - YES - 1 - - - - - 264 - {{6, 119}, {78, 14}} - - YES - - 67239424 - 71434240 - Site: - - - - - - - - {270, 151} - - - {{0, 0}, {1024, 746}} - {213, 129} - {1.79769e+308, 1.79769e+308} - - - - YES - username - includeTags - site - - YES - - - - - - YES - - - nextKeyView - - - - 56 - - - - nextKeyView - - - - 57 - - - - userField - - - - 65 - - - - passField - - - - 66 - - - - settingsView - - - - 69 - - - - value: selection.username - - - - - - value: selection.username - value - selection.username - 2 - - - 82 - - - - value: selection.includeTags - - - - - - value: selection.includeTags - value - selection.includeTags - 2 - - - 84 - - - - value: currentPassword - - - - - - value: currentPassword - value - currentPassword - 2 - - - 85 - - - - contentObject: selection.info - - - - - - contentObject: selection.info - contentObject - selection.info - 2 - - - 91 - - - - selectedTag: selection.site - - - - - - selectedTag: selection.site - selectedTag - selection.site - 2 - - - 98 - - - - - YES - - 0 - - - - - - -2 - - - File's Owner - - - -1 - - - First Responder - - - 5 - - - YES - - - - Window - - - 6 - - - YES - - - - - - - - - - - - 50 - - - YES - - - - - - 51 - - - YES - - - - - - 52 - - - YES - - - - - - 53 - - - YES - - - - - - 70 - - - YES - - - - - - 92 - - - YES - - - - - - 99 - - - YES - - - - - - 75 - - - Settings - - - 101 - - - - - 102 - - - - - 103 - - - - - 104 - - - - - 105 - - - - - 106 - - - YES - - - - - - 107 - - - - - 93 - - - YES - - - - - - - - 95 - - - - - 94 - - - - - -3 - - - Application - - - 108 - - - - - - - YES - - YES - 108.ImportedFromIB2 - 5.IBEditorWindowLastContentRect - 5.IBWindowTemplateEditedContentRect - 5.ImportedFromIB2 - 5.windowTemplate.hasMinSize - 5.windowTemplate.minSize - 50.ImportedFromIB2 - 51.ImportedFromIB2 - 52.ImportedFromIB2 - 53.CustomClassName - 53.ImportedFromIB2 - 6.ImportedFromIB2 - 70.ImportedFromIB2 - 75.ImportedFromIB2 - 92.ImportedFromIB2 - 93.IBEditorWindowLastContentRect - 93.ImportedFromIB2 - 94.ImportedFromIB2 - 95.ImportedFromIB2 - 99.ImportedFromIB2 - - - YES - - {{0, 994}, {270, 151}} - {{0, 994}, {270, 151}} - - - {213, 107} - - - - NSSecureTextField - - - - - - {{75, 1070}, {145, 63}} - - - - - - - - YES - - - YES - - - - - YES - - - YES - - - - 108 - - - - YES - - FirstResponder - NSObject - - IBUserSource - - - - - NSObject - - IBUserSource - - - - - QSDeliciousPlugIn_Source - QSObjectSource - - YES - - YES - passField - userField - - - YES - NSTextField - NSTextField - - - - IBProjectSource - QSDeliciousPlugIn_Source.h - - - - QSDeliciousPlugIn_Source - QSObjectSource - - savePassword: - id - - - IBUserSource - - - - - QSObjectSource - NSObject - - IBUserSource - - - - - - YES - - NSApplication - - relaunch: - id - - - IBFrameworkSource - QSFoundation.framework/Headers/NSApplication_BLTRExtensions.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSIconLoader.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSObject.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSObjectSource.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSObject_FileHandling.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSPlugIn.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSProxyObject.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSRegistry.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSTask.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSTriggerCenter.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSURLDownloadWrapper.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/UKFileWatcher.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/UKKQueue.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/UKMainThreadProxy.h - - - - NSObject - - IBFrameworkSource - QSFoundation.framework/Headers/NDHotKeyEvent.h - - - - NSObject - - IBFrameworkSource - QSFoundation.framework/Headers/NSArray_BLTRExtensions.h - - - - NSObject - - IBFrameworkSource - QSFoundation.framework/Headers/NSObject+BLTRExtensions.h - - - - NSObject - - IBFrameworkSource - QSFoundation.framework/Headers/NSObject+ReaperExtensions.h - - - - NSView - - IBFrameworkSource - QSFoundation.framework/Headers/NSView_BLTRExtensions.h - - - - NSWindow - - IBFrameworkSource - QSFoundation.framework/Headers/NSWindow_BLTRExtensions.h - - - - QSObjectSource - NSObject - - settingsView - NSView - - - - - - 0 - IBCocoaFramework - - com.apple.InterfaceBuilder.CocoaPlugin.macosx - - - - com.apple.InterfaceBuilder.CocoaPlugin.macosx - - - - com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 - - - YES - - 3 - - YES - - YES - NSMenuCheckmark - NSMenuMixedState - - - YES - {9, 8} - {7, 2} - - - - diff --git a/QSDeliciousPlugInSource.nib/keyedobjects.nib b/QSDeliciousPlugInSource.nib/keyedobjects.nib deleted file mode 100644 index a794dc2..0000000 Binary files a/QSDeliciousPlugInSource.nib/keyedobjects.nib and /dev/null differ diff --git a/QSDeliciousPlugIn_Source.h b/QSDeliciousPlugIn_Source.h index 89729de..26d613c 100644 --- a/QSDeliciousPlugIn_Source.h +++ b/QSDeliciousPlugIn_Source.h @@ -6,17 +6,23 @@ // Copyright __MyCompanyName__ 2004. All rights reserved. // +#import "QSBookmarkProvider.h" +#import "QSBookmarkProviderFactory.h" +#import "SocialSite.h" +#import +#import -#import "QSDeliciousPlugIn_Source.h" - -@interface QSDeliciousPlugIn_Source : QSObjectSource { - NSMutableArray *posts; - NSMutableArray *tags; - NSMutableArray *dates; - - IBOutlet NSTextField *userField; - IBOutlet NSTextField *passField; +@interface QSDeliciousPlugIn_Source : QSObjectSource { + IBOutlet NSTextField *userField; + IBOutlet NSTextField *passField; + IBOutlet NSTextField *hostField; } +@property(nonatomic, strong) NSString *internalPassword; +- (IBAction)settingsChanged:(id)sender; @end +@interface QSCatalogEntry (OldStyleSourceSupport) +@property NSMutableDictionary *info; +- (id)objectForKey:(NSString *)key; +@end diff --git a/QSDeliciousPlugIn_Source.m b/QSDeliciousPlugIn_Source.m index 8dcc9b4..e864514 100644 --- a/QSDeliciousPlugIn_Source.m +++ b/QSDeliciousPlugIn_Source.m @@ -7,349 +7,337 @@ // #import "QSDeliciousPlugIn_Source.h" +#import "Constants.h" #import - #import @implementation QSDeliciousPlugIn_Source -+ (void)initialize { - [self setKeys:[NSArray arrayWithObject:@"selection"] triggerChangeNotificationsForDependentKey:@"currentPassword"]; -} +#pragma mark - Lifecycle -- (BOOL)indexIsValidFromDate:(NSDate *)indexDate forEntry:(NSDictionary *)theEntry { - return -[indexDate timeIntervalSinceNow] < 24 * 60 * 60; +// This method will get called whenever we change which +// active entry is selected. +- (void)setSelectedEntry:(id)selectedEntry { + [super setSelectedEntry:selectedEntry]; + [self loadPasswordFromKeychain]; } -- (BOOL)isVisibleSource{ return YES; } +#pragma mark - Quicksilver Source Methods -- (NSImage *) iconForEntry:(NSDictionary *)dict { - return [[NSBundle bundleForClass:[self class]]imageNamed:@"bookmark_icon"]; +- (BOOL)indexIsValidFromDate:(NSDate *)indexDate + forEntry:(NSDictionary *)theEntry { + return -[indexDate timeIntervalSinceNow] < 24 * 60 * 60; } -- (NSString *) mainNibName { - return @"QSDeliciousPrefPane"; +- (BOOL)isVisibleSource { + return YES; } -- (void)populateFields { - NSLog(@"populating: %@/%@", [self.selectedEntry.sourceSettings objectForKey:@"username"], [self.selectedEntry.sourceSettings objectForKey:@"site"]); +- (NSImage *)iconForEntry:(NSDictionary *)dict { + return [[NSBundle bundleForClass:[self class]] imageNamed:@"bookmark_icon"]; } -- (NSView *) settingsView { - if (![super settingsView]) { - [[NSBundle bundleForClass:[self class]] loadNibNamed:@"QSDeliciousPlugInSource" owner:self topLevelObjects:NULL]; - } - return [super settingsView]; +- (NSView *)settingsView { + if (![super settingsView]) { + [[NSBundle bundleForClass:[self class]] + loadNibNamed:NSStringFromClass([self class]) + owner:self + topLevelObjects:NULL]; + } + return [super settingsView]; } -// Keychain Access -- The QS Built-in ones seems to be broken - -- (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; -} +#pragma mark - Settings Helpers -- (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; +- (SocialSite)siteIndex { + NSDictionary *settings = self.selectedEntry.sourceSettings; + return [settings objectForKey:@"site"] != nil + ? [[settings objectForKey:@"site"] integerValue] + : SocialSiteDelicious; } -- (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 *)currentUsername { + return [self.selectedEntry.sourceSettings objectForKey:@"username"]; } -- (NSString *)keychainPasswordForURL:(NSURL *)url { - return [self passwordForHost:[url host] user:[url user] andScheme:[url scheme]]; +- (NSString *)currentHost { + return [self.selectedEntry.sourceSettings objectForKey:@"host"]; } -- (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], - NULL); - } - - return err; +- (NSString *)currentPassword { + return self.internalPassword; } -// Site Index/API/URL - -- (NSInteger)siteIndex { - NSDictionary *settings = self.selectedEntry.sourceSettings; - return [settings objectForKey:@"site"] != nil ? [[settings objectForKey:@"site"] integerValue] : 0; +- (BOOL)includeTags { + return [[self.selectedEntry.sourceSettings objectForKey:@"includeTags"] + boolValue]; } -- (NSString *)siteURLForIndex:(NSInteger)siteIndex { - if (siteIndex == 0) return @"del.icio.us"; - else if (siteIndex == 1) return @"ma.gnolia.com"; - else if (siteIndex == 2) return @"pinboard.in"; - else return nil; +// 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]; + [self willChangeValueForKey:@"isHostVisible"]; + [self didChangeValueForKey:@"isHostVisible"]; } -- (NSString *)reversedSiteURLForIndex:(NSInteger)siteIndex { - if (siteIndex == 0) return @"us.icio.del"; - else if (siteIndex == 1) return @"com.gnolia.ma"; - else if (siteIndex == 2) return @"in.pinboard"; - else return nil; -} +#pragma mark - Keychain Helper Methods -- (NSString *)tagURLForIndex:(NSInteger)siteIndex { - return [NSString stringWithFormat:@"tag.%@", [self reversedSiteURLForIndex:[self siteIndex]]]; +- (NSString *)keychainKeyForIdentifier:(NSString *)identifier { + return [NSString stringWithFormat:@"QSSocialBookmarks-%@", identifier]; } -- (NSString *)apiURLForIndex:(NSInteger)siteIndex { - if (siteIndex == 0) return @"api.del.icio.us/v1"; - else if (siteIndex == 1) return @"ma.gnolia.com/api/mirrord/v1"; - else if (siteIndex == 2) return @"api.pinboard.in/v1"; - else return nil; -} +- (NSString *)passwordFromKeychainForKey:(NSString *)key { + const char *service = "QSSocialBookmarks"; + const char *account = [key UTF8String]; -// Current Site/API URL + UInt32 passwordLength = 0; + void *passwordData = NULL; -- (NSString *)currentSiteURL { - return [self siteURLForIndex:[self siteIndex]]; -} + 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; + } -- (NSString *)currentAPIURL { - return [self apiURLForIndex:[self siteIndex]]; + return nil; } -// Useranme +- (OSStatus)savePasswordToKeychainForKey:(NSString *)key + password:(NSString *)password { + const char *service = "QSSocialBookmarks"; + const char *account = [key UTF8String]; + const char *passwordCString = [password UTF8String]; -- (NSString *)currentUsername { - return [self.selectedEntry.sourceSettings objectForKey:@"username"]; -} + // First try to find existing item + SecKeychainItemRef item = NULL; + OSStatus findStatus = SecKeychainFindGenericPassword( + NULL, (UInt32)strlen(service), service, (UInt32)strlen(account), account, + NULL, NULL, &item); -// Password Related + 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); + } -- (NSString *)currentPassword { - NSString *account = [self currentUsername]; - if (!account) return nil; - - NSURL *keychainURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@@%@/", account, [self currentSiteURL]]]; - NSString *password = [self keychainPasswordForURL:keychainURL]; - - return password; + return status; } -- (void)setCurrentPassword:(NSString *)newPassword { - NSString *account = [self currentUsername]; - if (!account) return; - if ([newPassword length] <= 0) return; - - NSURL *keychainURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%@@%@/", account, newPassword, [self currentSiteURL]]]; - - [self addURLPasswordToKeychain:keychainURL]; +- (OSStatus)deletePasswordFromKeychainForKey:(NSString *)key { + const char *service = "QSSocialBookmarks"; + const char *account = [key UTF8String]; + + 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; + } + + return findStatus; } -// Bookarmk/Caching +#pragma mark - Password Keychain Methods + +- (void)loadPasswordFromKeychain { + if (!self.selectedEntry || !self.selectedEntry.identifier) { + self.internalPassword = nil; + return; + } -- (NSData *)cachedBookmarkData { - NSString *cachePath=[QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", [self currentSiteURL]], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", [self currentUsername]]]; - return [NSData dataWithContentsOfFile:cachePath]; + NSString *keychainKey = + [self keychainKeyForIdentifier:self.selectedEntry.identifier]; + [self setPassword:[self passwordFromKeychainForKey:keychainKey]]; } -- (NSData *)bookmarkData { - if (![self currentUsername] || ![self currentPassword]) return nil; - - NSString *urlString = [NSString stringWithFormat:@"https://%@:%@@%@/posts/all?", [self currentUsername], [self currentPassword], [self currentAPIURL]]; - NSURL *requestURL = [NSURL URLWithString:urlString]; - - 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; - NSData *data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error]; - - NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", [self currentSiteURL]], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", [self currentUsername]]]; - [data writeToFile:cachePath atomically:NO]; - - return data; +- (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); + } } -- (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]; - //NSDate *date=[NSCalendarDate dateWithString:[post objectForKey:@"time"] - // calendarFormat:@"%Y-%m-%dT%H:%M:%SZ"]; - //[date setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; - //[newObject setObject:date forMeta:kQSObjectCreationDate]; - return newObject; +#pragma mark - Password Property Accessors + +- (NSString *)password { + return self.internalPassword; } -- (NSArray *)objectsForEntry:(NSDictionary *)theEntry { - NSData *data = nil;//[self cachedBookmarkData]; - if (![data length]) data = [self bookmarkData]; - - NSXMLParser *postParser = [[NSXMLParser alloc]initWithData:data]; - [postParser setDelegate:self]; - - posts = [NSMutableArray arrayWithCapacity:1]; - - [postParser parse]; - - NSMutableArray *objects=[NSMutableArray arrayWithCapacity:1]; - QSObject *newObject; - NSEnumerator *e=[posts objectEnumerator]; - NSDictionary *post; - NSMutableSet *tagSet=[NSMutableSet set]; - while(post=[e nextObject]){ - newObject=[self objectForPost:post]; - [tagSet addObjectsFromArray:[[post objectForKey:@"tag"]componentsSeparatedByString:@" "]]; - [objects addObject:newObject]; - } - NSString *tag; - e=[tagSet objectEnumerator]; - - if ([[theEntry objectForKey:@"includeTags"]boolValue]){ - while(tag=[e nextObject]){ - newObject=[QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[del.icio.us tag]:%@",tag]]; - [newObject setObject:tag forType:[self tagURLForIndex:[self siteIndex]]]; - [newObject setObject:[self currentUsername] forMeta:@"us.icio.del.username"]; - [newObject setName:tag]; - [newObject setPrimaryType:[self tagURLForIndex:[self siteIndex]]]; - [objects addObject:newObject]; - } - } - [postParser release]; - - return objects; - +- (void)setPassword:(NSString *)password { + self.internalPassword = password; + + // Save to keychain asynchronously + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + ^{ + [self savePasswordToKeychain]; + }); } -- (NSArray *)objectsForTag:(NSString *)tag username:(NSString *)username{ - NSData *data=[self cachedBookmarkData]; - - NSXMLParser *postParser=[[NSXMLParser alloc]initWithData:data]; - [postParser setDelegate:self]; - posts=[NSMutableArray arrayWithCapacity:1]; - [postParser parse]; - - NSMutableArray *objects=[NSMutableArray arrayWithCapacity:1]; - QSObject *newObject; - NSEnumerator *e=[posts objectEnumerator]; - NSDictionary *post; - // NSMutableSet *tagSet=[NSMutableSet set]; - while(post=[e nextObject]){ - if ([[post objectForKey:@"tag"]rangeOfString:tag].location==NSNotFound)continue; - newObject=[self objectForPost:post]; - [objects addObject:newObject]; - } - return objects; +#pragma mark - Host Visibility Control +- (BOOL)isHostVisible { + return [SocialSiteHelper hasVariableHost:[self siteIndex]]; +} +- (void)setIsHostVisible:(BOOL)isVisible { } -// Object Handler Methods +#pragma mark - Objects For Entry -/* -- (void)setQuickIconForObject:(QSObject *)object { - [object setIcon:nil]; // An icon that is either already in memory or easy to load +- (NSArray *)objectsForEntry:(QSCatalogEntry *)theEntry { + + 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"]; + + 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 + identifier:identifier + host:host + includeTags:includeTags]; } -- (BOOL)loadIconForObject:(QSObject *)object { - return NO; - id data=[object objectForType:QSDeliciousPlugInType]; - [object setIcon:nil]; - return YES; +- (NSArray *)objectsForTag:(NSString *)tag + site:(SocialSite)site + username:(NSString *)username + identifier:(NSString *)identifier + host:(NSString *)host { + + NSString *keychainKey = [self keychainKeyForIdentifier:identifier]; + NSString *password = [self passwordFromKeychainForKey:keychainKey]; + + 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 @[]; + } + + // Check if provider supports tag-based fetching + if ([provider respondsToSelector:@selector + (fetchBookmarksForTag:site:username:password:host:)]) { + return [provider fetchBookmarksForTag:tag + site:site + username:username + password:password + host:host]; + } + + return @[]; } -*/ + +#pragma mark - Object Handler Methods - (void)setQuickIconForObject:(QSObject *)object { - [object setIcon:[[NSBundle bundleForClass:[self class]]imageNamed:@"bookmark_icon"]]; + [object setIcon:[[NSBundle bundleForClass:[self class]] + imageNamed:@"bookmark_icon"]]; } -- (BOOL)loadChildrenForObject:(QSObject *)object { - [object setChildren:[self objectsForTag:[object objectForType:[self tagURLForIndex:[self siteIndex]]] - username:[object objectForMeta:@"us.icio.del.username"]]]; - return YES; +// All our objects will have children. URLs will have tags, and tags will have +// URLs. +- (BOOL)objectHasChildren:(QSObject *)object { + return YES; } -// NSXMLParserDelegate Functions - -- (void)parser:(NSXMLParser*)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { - NSLog(@"XML Parser started %@ %@ %@ %@",elementName,namespaceURI,qName,attributeDict); - if ([elementName isEqualToString:@"post"] && attributeDict) - [posts addObject:attributeDict]; -} +// 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 { -- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { - NSLog(@"XML Parser ended %@ %@ %@", elementName, namespaceURI, qName); + 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 *tag = [object objectForType:kTagType]; + if (!tag) { + NSLog(@"We could not find a valid tag type."); + return NO; + } + + NSArray *children = [self objectsForTag:tag + site:site + username:username + identifier:identifier + host:host]; + [object setChildren:children]; + return YES; } @end diff --git a/QSDeliciousPlugIn_Source.xib b/QSDeliciousPlugIn_Source.xib new file mode 100644 index 0000000..5f56ddc --- /dev/null +++ b/QSDeliciousPlugIn_Source.xib @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + info.username + info.site + info.includeTags + info.host + + + + + + + + diff --git a/QSDeliciousPrefPane.h b/QSDeliciousPrefPane.h deleted file mode 100644 index 494a9ff..0000000 --- a/QSDeliciousPrefPane.h +++ /dev/null @@ -1,13 +0,0 @@ - - -#import -#import - - -@interface QSDeliciousPrefPane : NSPreferencePane { - IBOutlet NSTextField *userField; - IBOutlet NSTextField *passField; -} - -- (IBAction)savePassword:(id)sender; -@end diff --git a/QSDeliciousPrefPane.m b/QSDeliciousPrefPane.m deleted file mode 100644 index f0dde4e..0000000 --- a/QSDeliciousPrefPane.m +++ /dev/null @@ -1,39 +0,0 @@ - - -#import "QSDeliciousPrefPane.h" -#import - -@implementation QSDeliciousPrefPane -- (id)init { - self = [super initWithBundle:[NSBundle bundleForClass:[QSDeliciousPrefPane class]]]; - if (self) { - } - return self; -} - -- (NSImage *) icon{ - return [[NSBundle bundleForClass:[self class]] imageNamed:@"del.icio.us"]; -} - -- (NSString *) mainNibName{ - return @"QSDeliciousPrefPane"; -} - -- (void)awakeFromNib{ - NSString *account=[[NSUserDefaults standardUserDefaults] objectForKey:@"QSDeliciousUserName"]; - NSURL *url=[NSURL URLWithString:[NSString stringWithFormat:@"http://%@@del.icio.us/",account]]; - NSString *password=[url keychainPassword]; - if (account)[userField setStringValue:account]; - if (password)[passField setStringValue:password]; -} - -- (IBAction)savePassword:(id)sender{ - NSString *account=[userField stringValue]; - NSString *pass=[passField stringValue]; - - NSURL *url=[NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%@@del.icio.us/",account,pass]]; - if ([pass length]) - [url addPasswordToKeychain]; -} - -@end diff --git a/README.markdown b/README.markdown index c22d609..edfd9d2 100644 --- a/README.markdown +++ b/README.markdown @@ -58,4 +58,14 @@ By downloading and/or using this software you agree to the following terms of us limitations under the License. -Which basically means: whatever you do, I can't be held accountable if something breaks. \ No newline at end of file +Which basically means: whatever you do, I can't be held accountable if something breaks. + +Supported Services +----------- + +This plugin supports Delicious compatible services (eg. Linkhut), and Linkding. It's important to note that delicious compatible systems aren't all compatible in the same way, so you might need to make some adjustments: + + +- For every self-hosted service, make sure to include the protocol (eg. https://). +- For delicious compatible items, include everything before `/v1`. So if it's `https://api.ln.ht/v1` you just include `https://api.ln.ht`. But if it's `https://myservice.com/api/v1` then you should include `https://myservice.com/api` with no trailing slash. +- For delicious compatible items, check what they expect for the `auth_token` field and use that as your password. For example, in pinboard it's `username:token`, while in linkhut it's just `token`. diff --git a/Types/Constants.h b/Types/Constants.h new file mode 100644 index 0000000..75dd240 --- /dev/null +++ b/Types/Constants.h @@ -0,0 +1,9 @@ + +#pragma mark - Tag QSObject Constants + +extern NSString *const kTagType; + +extern NSString *const kTagSiteField; +extern NSString *const kTagUsernameField; +extern NSString *const kTagHostField; +extern NSString *const kTagIdentifierField; diff --git a/Types/Constants.m b/Types/Constants.m new file mode 100644 index 0000000..9300b54 --- /dev/null +++ b/Types/Constants.m @@ -0,0 +1,8 @@ +#pragma mark - Tag QSObject Constants + +NSString *const kTagType = @"qs.tag.socialbookmark"; + +NSString *const kTagSiteField = @"source.site"; +NSString *const kTagUsernameField = @"source.username"; +NSString *const kTagHostField = @"source.host"; +NSString *const kTagIdentifierField = @"source.identifier"; diff --git a/Types/SocialSite.h b/Types/SocialSite.h new file mode 100644 index 0000000..2510594 --- /dev/null +++ b/Types/SocialSite.h @@ -0,0 +1,24 @@ +// +// SocialSite.h +// QSDeliciousPlugIn +// +// Social bookmark site enumeration +// + +#import + +typedef NS_ENUM(NSInteger, SocialSite) { + SocialSiteDelicious = 0, + SocialSiteMagnolia = 1, + SocialSitePinboard = 2, + SocialSiteLinkding = 3, + SocialSiteSelfHostedDeliciousCompatible = 4 +}; + +@interface SocialSiteHelper : NSObject + ++ (NSString *)displayNameForSite:(SocialSite)site; ++ (NSString *)cacheKeyForSite:(SocialSite)site; ++ (BOOL)hasVariableHost:(SocialSite)site; + +@end diff --git a/Types/SocialSite.m b/Types/SocialSite.m new file mode 100644 index 0000000..258386d --- /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"; + case SocialSiteSelfHostedDeliciousCompatible: + return @"Self-Hosted (Delicious Compatible)"; + default: + return @"Unknown"; + } +} + +// This is used for caching key ++ (NSString *)cacheKeyForSite:(SocialSite)site { + switch (site) { + case SocialSiteDelicious: + return @"del.icio.us"; + case SocialSiteMagnolia: + return @"ma.gnolia.com"; + case SocialSitePinboard: + return @"pinboard.in"; + case SocialSiteSelfHostedDeliciousCompatible: + return @"self-hosted-delicious"; + case SocialSiteLinkding: + return @"linkding"; + default: + return nil; + } +} + ++ (BOOL)hasVariableHost:(SocialSite)site { + switch (site) { + case SocialSiteSelfHostedDeliciousCompatible: + case SocialSiteLinkding: + return YES; + default: + return NO; + } +} + +@end diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index ad177ea..c117aca 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -1 +1,18 @@ -"QSDeliciousPrefPane" = "del.icio.us"; +"QSDeliciousPrefPane" = "Social Bookmarks"; + +/* Site Names */ +"SiteDelicious" = "del.icio.us"; +"SiteMagnolia" = "ma.gnolia.com"; +"SitePinboard" = "Pinboard"; +"SiteLinkding" = "Linkding"; + +/* UI Labels */ +"Username" = "Username"; +"Password" = "Password"; +"Host" = "Host"; +"IncludeTags" = "Include Tags"; +"Site" = "Bookmark Service"; + +/* Help Text */ +"LinkdingHostHelp" = "For Linkding, enter your server URL (e.g., https://bookmarks.example.com)"; +"LinkdingPasswordHelp" = "For Linkding, use your API Token as the password";