diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-09-06 23:03:04 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-09-06 23:03:04 +0200 |
| commit | 6f7a0e57ce0650c9e91b15e75ec9a0bb85f6ddb6 (patch) | |
| tree | b4d623e54f552488408f13e5f6ad5bed54643a29 | |
| parent | 685a4d4b33991a315060145724d794d709ec6f3c (diff) | |
Make the provider functional
| -rw-r--r-- | QSBookmarkProvider.h | 48 | ||||
| -rw-r--r-- | QSBookmarkProviderFactory.h | 28 | ||||
| -rw-r--r-- | QSBookmarkProviderFactory.m | 66 | ||||
| -rw-r--r-- | QSDeliciousAPIProvider.h | 25 | ||||
| -rw-r--r-- | QSDeliciousAPIProvider.m | 190 | ||||
| -rw-r--r-- | QSDeliciousPlugIn.xcodeproj/project.pbxproj | 53 | ||||
| -rw-r--r-- | QSDeliciousPlugInSource.nib/designable.nib | 930 | ||||
| -rw-r--r-- | QSDeliciousPlugInSource.nib/keyedobjects.nib | bin | 9343 -> 0 bytes | |||
| -rw-r--r-- | QSDeliciousPlugInTests.m | 89 | ||||
| -rw-r--r-- | QSDeliciousPlugIn_Source.h | 23 | ||||
| -rw-r--r-- | QSDeliciousPlugIn_Source.m | 485 | ||||
| -rw-r--r-- | QSDeliciousPlugIn_Source.xib | 177 | ||||
| -rw-r--r-- | QSDeliciousPrefPane.h | 13 | ||||
| -rw-r--r-- | QSDeliciousPrefPane.m | 39 | ||||
| -rw-r--r-- | QSLinkdingProvider.h | 16 | ||||
| -rw-r--r-- | QSLinkdingProvider.m | 191 | ||||
| -rw-r--r-- | REFACTOR_GUIDE.md | 118 | ||||
| -rw-r--r-- | SocialSite.h | 23 | ||||
| -rw-r--r-- | SocialSite.m | 55 | ||||
| -rw-r--r-- | en.lproj/Localizable.strings | 19 |
20 files changed, 1304 insertions, 1284 deletions
diff --git a/QSBookmarkProvider.h b/QSBookmarkProvider.h new file mode 100644 index 0000000..735ae82 --- /dev/null +++ b/QSBookmarkProvider.h @@ -0,0 +1,48 @@ +// +// QSBookmarkProvider.h +// QSDeliciousPlugIn +// +// Protocol for social bookmark providers +// + +#import <Foundation/Foundation.h> +#import "SocialSite.h" + +@class QSObject; + +@protocol QSBookmarkProvider <NSObject> + +@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 new file mode 100644 index 0000000..e2e694b --- /dev/null +++ b/QSBookmarkProviderFactory.h @@ -0,0 +1,28 @@ +// +// QSBookmarkProviderFactory.h +// QSDeliciousPlugIn +// +// Factory for managing bookmark providers +// + +#import <Foundation/Foundation.h> +#import "QSBookmarkProvider.h" +#import "SocialSite.h" + +@interface QSBookmarkProviderFactory : NSObject + +@property (nonatomic, strong, readonly) NSArray<id<QSBookmarkProvider>> *providers; + ++ (instancetype)sharedFactory; + +/** + * Get the appropriate provider for the given site configuration + */ +- (id<QSBookmarkProvider>)providerForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; + +/** + * Get all available providers + */ +- (NSArray<id<QSBookmarkProvider>> *)allProviders; + +@end
\ No newline at end of file diff --git a/QSBookmarkProviderFactory.m b/QSBookmarkProviderFactory.m new file mode 100644 index 0000000..6fcfdfb --- /dev/null +++ b/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<id<QSBookmarkProvider>> *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<QSBookmarkProvider>)providerForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + for (id<QSBookmarkProvider> provider in self.providers) { + if ([provider canHandleSite:site username:username password:password host:host]) { + return provider; + } + } + return nil; +} + +- (NSArray<id<QSBookmarkProvider>> *)allProviders { + return self.providers; +} + +@end diff --git a/QSDeliciousAPIProvider.h b/QSDeliciousAPIProvider.h new file mode 100644 index 0000000..0064264 --- /dev/null +++ b/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 <Foundation/Foundation.h> +#import "QSBookmarkProvider.h" + +@interface QSDeliciousAPIProvider : NSObject <QSBookmarkProvider, NSXMLParserDelegate> + +@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 new file mode 100644 index 0000000..d97d1d2 --- /dev/null +++ b/QSDeliciousAPIProvider.m @@ -0,0 +1,190 @@ +// +// QSDeliciousAPIProvider.m +// QSDeliciousPlugIn +// + +#import "QSDeliciousAPIProvider.h" +#import "SocialSite.h" +#import <QSCore/QSCore.h> + +@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 ce2a3d5..ebe2b52 100644 --- a/QSDeliciousPlugIn.xcodeproj/project.pbxproj +++ b/QSDeliciousPlugIn.xcodeproj/project.pbxproj @@ -8,30 +8,43 @@ /* 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 */; }; + 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 = "<group>"; }; 7F9441A00803A9D9007EDC31 /* QSObjectSource.name.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = QSObjectSource.name.strings; sourceTree = "<group>"; }; 7FB0DC190B91FF8600A5B6FF /* bookmark_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bookmark_icon.png; sourceTree = "<group>"; }; 7FB0DC2D0B91FFC500A5B6FF /* QSCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QSCore.framework; path = /Applications/Quicksilver.app/Contents/Frameworks/QSCore.framework; sourceTree = "<absolute>"; }; 7FB0DC2E0B91FFC600A5B6FF /* QSFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QSFoundation.framework; path = /Applications/Quicksilver.app/Contents/Frameworks/QSFoundation.framework; sourceTree = "<absolute>"; }; 8D1AC9730486D14A00FE50C9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 8D1AC9740486D14A00FE50C9 /* Social Bookmarks Plugin.qsplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Social Bookmarks Plugin.qsplugin"; sourceTree = BUILT_PRODUCTS_DIR; }; + B5CF7D5C2E6B7370008A0EE6 /* SocialSite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SocialSite.h; sourceTree = "<group>"; }; + B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SocialSite.m; sourceTree = "<group>"; }; + B5CF7D5E2E6B737E008A0EE6 /* QSBookmarkProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSBookmarkProvider.h; sourceTree = "<group>"; }; + B5CF7D5F2E6B7386008A0EE6 /* QSDeliciousAPIProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSDeliciousAPIProvider.h; sourceTree = "<group>"; }; + B5CF7D602E6B73A0008A0EE6 /* QSDeliciousAPIProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousAPIProvider.m; sourceTree = "<group>"; }; + B5CF7D612E6B73A5008A0EE6 /* QSLinkdingProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSLinkdingProvider.h; sourceTree = "<group>"; }; + B5CF7D622E6B73C9008A0EE6 /* QSLinkdingProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSLinkdingProvider.m; sourceTree = "<group>"; }; + B5CF7D632E6B73D1008A0EE6 /* QSBookmarkProviderFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSBookmarkProviderFactory.h; sourceTree = "<group>"; }; + B5CF7D642E6B73DA008A0EE6 /* QSBookmarkProviderFactory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSBookmarkProviderFactory.m; sourceTree = "<group>"; }; + B5CF7D672E6B7457008A0EE6 /* QSDeliciousPlugInTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousPlugInTests.m; sourceTree = "<group>"; }; + B5CF7D682E6B746E008A0EE6 /* REFACTOR_GUIDE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = REFACTOR_GUIDE.md; sourceTree = "<group>"; }; + B5CF7D692E6B74FE008A0EE6 /* QSDeliciousPlugIn_Source.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QSDeliciousPlugIn_Source.xib; sourceTree = "<group>"; }; D475F9AF18B2992D0012243C /* Common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = "<group>"; }; D475F9B018B2992D0012243C /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; }; D475F9B118B2992D0012243C /* Developer.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Developer.xcconfig; sourceTree = "<group>"; }; @@ -42,9 +55,7 @@ DD92D38A0106425D02CA0E72 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = "<absolute>"; }; E182BCCF06FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = QSDeliciousPlugIn_Source.h; sourceTree = "<group>"; }; E182BCD006FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousPlugIn_Source.m; sourceTree = "<group>"; }; - E182BE2206FC9AB5007BF2C2 /* QSDeliciousPrefPane.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = QSDeliciousPrefPane.h; sourceTree = "<group>"; }; - E182BE2306FC9AB5007BF2C2 /* QSDeliciousPrefPane.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousPrefPane.m; sourceTree = "<group>"; }; - E182BE3B06FC9B13007BF2C2 /* en */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; + E182BE3B06FC9B13007BF2C2 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; E182BE5A06FC9C46007BF2C2 /* PreferencePanes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PreferencePanes.framework; path = /System/Library/Frameworks/PreferencePanes.framework; sourceTree = "<absolute>"; }; /* End PBXFileReference section */ @@ -118,12 +129,21 @@ 32DBCF9F0370C38200C91783 /* Other Sources */ = { isa = PBXGroup; children = ( + B5CF7D5C2E6B7370008A0EE6 /* SocialSite.h */, + B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */, + B5CF7D5E2E6B737E008A0EE6 /* QSBookmarkProvider.h */, + B5CF7D5F2E6B7386008A0EE6 /* QSDeliciousAPIProvider.h */, + B5CF7D602E6B73A0008A0EE6 /* QSDeliciousAPIProvider.m */, + B5CF7D612E6B73A5008A0EE6 /* QSLinkdingProvider.h */, + B5CF7D622E6B73C9008A0EE6 /* QSLinkdingProvider.m */, + 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 */, - 7F8AD05007F2503600011548 /* QSDeliciousPlugInSource.nib */, - E182BE2206FC9AB5007BF2C2 /* QSDeliciousPrefPane.h */, - E182BE2306FC9AB5007BF2C2 /* QSDeliciousPrefPane.m */, ); name = "Other Sources"; sourceTree = "<group>"; @@ -171,6 +191,11 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 0500; + TargetAttributes = { + 8D1AC9600486D14A00FE50C9 = { + ProvisioningStyle = Manual; + }; + }; }; buildConfigurationList = 7F07AFAE085E432E00E2AFC4 /* Build configuration list for PBXProject "QSDeliciousPlugIn" */; compatibilityVersion = "Xcode 3.2"; @@ -199,9 +224,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 +255,11 @@ 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 */, ); 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 @@ -<?xml version="1.0" encoding="UTF-8"?> -<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10"> - <data> - <int key="IBDocument.SystemTarget">1060</int> - <string key="IBDocument.SystemVersion">10F569</string> - <string key="IBDocument.InterfaceBuilderVersion">762</string> - <string key="IBDocument.AppKitVersion">1038.29</string> - <string key="IBDocument.HIToolboxVersion">461.00</string> - <object class="NSMutableDictionary" key="IBDocument.PluginVersions"> - <bool key="EncodedWithXMLCoder">YES</bool> - <object class="NSArray" key="dict.sortedKeys" id="0"> - <bool key="EncodedWithXMLCoder">YES</bool> - </object> - <object class="NSMutableArray" key="dict.values"> - <bool key="EncodedWithXMLCoder">YES</bool> - </object> - </object> - <object class="NSMutableArray" key="IBDocument.EditedObjectIDs"> - <bool key="EncodedWithXMLCoder">YES</bool> - <integer value="6"/> - </object> - <reference key="IBDocument.PluginDependencies" ref="0"/> - <object class="NSMutableDictionary" key="IBDocument.Metadata"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference key="dict.sortedKeys" ref="0"/> - <object class="NSMutableArray" key="dict.values"> - <bool key="EncodedWithXMLCoder">YES</bool> - </object> - </object> - <object class="NSMutableArray" key="IBDocument.RootObjects" id="791608857"> - <bool key="EncodedWithXMLCoder">YES</bool> - <object class="NSCustomObject" id="755236013"> - <string key="NSClassName">QSDeliciousPlugIn_Source</string> - </object> - <object class="NSCustomObject" id="949435413"> - <string key="NSClassName">FirstResponder</string> - </object> - <object class="NSCustomObject" id="1037187928"> - <string key="NSClassName">NSApplication</string> - </object> - <object class="NSWindowTemplate" id="523381760"> - <int key="NSWindowStyleMask">15</int> - <int key="NSWindowBacking">2</int> - <string key="NSWindowRect">{{62, 510}, {270, 151}}</string> - <int key="NSWTFlags">1886912512</int> - <object class="NSMutableString" key="NSWindowTitle"> - <characters key="NS.bytes">Window</characters> - </object> - <string key="NSWindowClass">NSWindow</string> - <object class="NSMutableString" key="NSViewClass"> - <characters key="NS.bytes">View</characters> - </object> - <string key="NSWindowContentMaxSize">{1.79769e+308, 1.79769e+308}</string> - <string key="NSWindowContentMinSize">{213, 107}</string> - <object class="NSView" key="NSWindowView" id="246154705"> - <reference key="NSNextResponder"/> - <int key="NSvFlags">256</int> - <object class="NSMutableArray" key="NSSubviews"> - <bool key="EncodedWithXMLCoder">YES</bool> - <object class="NSTextField" id="220887054"> - <reference key="NSNextResponder" ref="246154705"/> - <int key="NSvFlags">266</int> - <string key="NSFrame">{{89, 85}, {150, 19}}</string> - <reference key="NSSuperview" ref="246154705"/> - <bool key="NSEnabled">YES</bool> - <object class="NSTextFieldCell" key="NSCell" id="388661522"> - <int key="NSCellFlags">-1804468671</int> - <int key="NSCellFlags2">4326400</int> - <string key="NSContents"/> - <object class="NSFont" key="NSSupport" id="26"> - <string key="NSName">LucidaGrande</string> - <double key="NSSize">11</double> - <int key="NSfFlags">3100</int> - </object> - <reference key="NSControlView" ref="220887054"/> - <bool key="NSDrawsBackground">YES</bool> - <object class="NSColor" key="NSBackgroundColor" id="251902835"> - <int key="NSColorSpace">6</int> - <string key="NSCatalogName">System</string> - <string key="NSColorName">textBackgroundColor</string> - <object class="NSColor" key="NSColor"> - <int key="NSColorSpace">3</int> - <bytes key="NSWhite">MQA</bytes> - </object> - </object> - <object class="NSColor" key="NSTextColor" id="941678540"> - <int key="NSColorSpace">6</int> - <string key="NSCatalogName">System</string> - <string key="NSColorName">textColor</string> - <object class="NSColor" key="NSColor" id="697840755"> - <int key="NSColorSpace">3</int> - <bytes key="NSWhite">MAA</bytes> - </object> - </object> - </object> - </object> - <object class="NSTextField" id="487755233"> - <reference key="NSNextResponder" ref="246154705"/> - <int key="NSvFlags">264</int> - <string key="NSFrame">{{6, 88}, {78, 14}}</string> - <reference key="NSSuperview" ref="246154705"/> - <bool key="NSEnabled">YES</bool> - <object class="NSTextFieldCell" key="NSCell" id="837401498"> - <int key="NSCellFlags">67239424</int> - <int key="NSCellFlags2">71434240</int> - <string key="NSContents">Name:</string> - <reference key="NSSupport" ref="26"/> - <reference key="NSControlView" ref="487755233"/> - <object class="NSColor" key="NSBackgroundColor" id="685423289"> - <int key="NSColorSpace">6</int> - <string key="NSCatalogName">System</string> - <string key="NSColorName">controlColor</string> - <object class="NSColor" key="NSColor"> - <int key="NSColorSpace">3</int> - <bytes key="NSWhite">MC42NjY2NjY2NjY3AA</bytes> - </object> - </object> - <object class="NSColor" key="NSTextColor" id="690248854"> - <int key="NSColorSpace">6</int> - <string key="NSCatalogName">System</string> - <string key="NSColorName">controlTextColor</string> - <reference key="NSColor" ref="697840755"/> - </object> - </object> - </object> - <object class="NSTextField" id="697429448"> - <reference key="NSNextResponder" ref="246154705"/> - <int key="NSvFlags">264</int> - <string key="NSFrame">{{6, 61}, {78, 14}}</string> - <reference key="NSSuperview" ref="246154705"/> - <bool key="NSEnabled">YES</bool> - <object class="NSTextFieldCell" key="NSCell" id="314007513"> - <int key="NSCellFlags">67239424</int> - <int key="NSCellFlags2">71434240</int> - <string key="NSContents">Password:</string> - <reference key="NSSupport" ref="26"/> - <reference key="NSControlView" ref="697429448"/> - <reference key="NSBackgroundColor" ref="685423289"/> - <reference key="NSTextColor" ref="690248854"/> - </object> - </object> - <object class="NSTextField" id="932260227"> - <reference key="NSNextResponder" ref="246154705"/> - <int key="NSvFlags">266</int> - <string key="NSFrame">{{89, 58}, {150, 19}}</string> - <reference key="NSSuperview" ref="246154705"/> - <bool key="NSEnabled">YES</bool> - <object class="NSTextFieldCell" key="NSCell" id="574259110"> - <int key="NSCellFlags">-1804468671</int> - <int key="NSCellFlags2">4326400</int> - <string key="NSContents"/> - <reference key="NSSupport" ref="26"/> - <reference key="NSControlView" ref="932260227"/> - <bool key="NSDrawsBackground">YES</bool> - <reference key="NSBackgroundColor" ref="251902835"/> - <reference key="NSTextColor" ref="941678540"/> - </object> - </object> - <object class="NSButton" id="1030464026"> - <reference key="NSNextResponder" ref="246154705"/> - <int key="NSvFlags">264</int> - <string key="NSFrame">{{86, 34}, {87, 18}}</string> - <reference key="NSSuperview" ref="246154705"/> - <bool key="NSEnabled">YES</bool> - <object class="NSButtonCell" key="NSCell" id="11812574"> - <int key="NSCellFlags">67239424</int> - <int key="NSCellFlags2">131072</int> - <string key="NSContents">Include tags</string> - <reference key="NSSupport" ref="26"/> - <reference key="NSControlView" ref="1030464026"/> - <int key="NSButtonFlags">1211912703</int> - <int key="NSButtonFlags2">2</int> - <object class="NSButtonImageSource" key="NSAlternateImage"> - <string key="NSImageName">NSSwitch</string> - </object> - <string key="NSAlternateContents"/> - <string key="NSKeyEquivalent"/> - <int key="NSPeriodicDelay">200</int> - <int key="NSPeriodicInterval">25</int> - </object> - </object> - <object class="NSPopUpButton" id="583724646"> - <reference key="NSNextResponder" ref="246154705"/> - <int key="NSvFlags">264</int> - <string key="NSFrame">{{86, 113}, {126, 26}}</string> - <reference key="NSSuperview" ref="246154705"/> - <bool key="NSEnabled">YES</bool> - <object class="NSPopUpButtonCell" key="NSCell" id="986910851"> - <int key="NSCellFlags">-2076049856</int> - <int key="NSCellFlags2">2048</int> - <object class="NSFont" key="NSSupport"> - <string key="NSName">LucidaGrande</string> - <double key="NSSize">13</double> - <int key="NSfFlags">1044</int> - </object> - <reference key="NSControlView" ref="583724646"/> - <int key="NSButtonFlags">109199615</int> - <int key="NSButtonFlags2">1</int> - <object class="NSFont" key="NSAlternateImage"> - <string key="NSName">LucidaGrande</string> - <double key="NSSize">13</double> - <int key="NSfFlags">16</int> - </object> - <string key="NSAlternateContents"/> - <object class="NSMutableString" key="NSKeyEquivalent"> - <characters key="NS.bytes"/> - </object> - <int key="NSPeriodicDelay">400</int> - <int key="NSPeriodicInterval">75</int> - <object class="NSMenuItem" key="NSMenuItem" id="842183426"> - <reference key="NSMenu" ref="396821977"/> - <string key="NSTitle">del.icio.us</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <int key="NSState">1</int> - <object class="NSCustomResource" key="NSOnImage" id="596243717"> - <string key="NSClassName">NSImage</string> - <string key="NSResourceName">NSMenuCheckmark</string> - </object> - <object class="NSCustomResource" key="NSMixedImage" id="117193000"> - <string key="NSClassName">NSImage</string> - <string key="NSResourceName">NSMenuMixedState</string> - </object> - <string key="NSAction">_popUpItemAction:</string> - <reference key="NSTarget" ref="986910851"/> - </object> - <bool key="NSMenuItemRespectAlignment">YES</bool> - <object class="NSMenu" key="NSMenu" id="396821977"> - <object class="NSMutableString" key="NSTitle"> - <characters key="NS.bytes">OtherViews</characters> - </object> - <object class="NSMutableArray" key="NSMenuItems"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference ref="842183426"/> - <object class="NSMenuItem" id="174640103"> - <reference key="NSMenu" ref="396821977"/> - <string key="NSTitle">ma.gnolia</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="596243717"/> - <reference key="NSMixedImage" ref="117193000"/> - <string key="NSAction">_popUpItemAction:</string> - <int key="NSTag">1</int> - <reference key="NSTarget" ref="986910851"/> - </object> - <object class="NSMenuItem" id="257964281"> - <reference key="NSMenu" ref="396821977"/> - <string key="NSTitle">pinboard.in</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="596243717"/> - <reference key="NSMixedImage" ref="117193000"/> - <string key="NSAction">_popUpItemAction:</string> - <int key="NSTag">2</int> - <reference key="NSTarget" ref="986910851"/> - </object> - </object> - </object> - <int key="NSPreferredEdge">3</int> - <bool key="NSUsesItemFromMenu">YES</bool> - <bool key="NSAltersState">YES</bool> - <int key="NSArrowPosition">1</int> - </object> - </object> - <object class="NSTextField" id="397416172"> - <reference key="NSNextResponder" ref="246154705"/> - <int key="NSvFlags">264</int> - <string key="NSFrame">{{6, 119}, {78, 14}}</string> - <reference key="NSSuperview" ref="246154705"/> - <bool key="NSEnabled">YES</bool> - <object class="NSTextFieldCell" key="NSCell" id="816301873"> - <int key="NSCellFlags">67239424</int> - <int key="NSCellFlags2">71434240</int> - <string key="NSContents">Site:</string> - <reference key="NSSupport" ref="26"/> - <reference key="NSControlView" ref="397416172"/> - <reference key="NSBackgroundColor" ref="685423289"/> - <reference key="NSTextColor" ref="690248854"/> - </object> - </object> - </object> - <string key="NSFrameSize">{270, 151}</string> - <reference key="NSSuperview"/> - </object> - <string key="NSScreenRect">{{0, 0}, {1024, 746}}</string> - <string key="NSMinSize">{213, 129}</string> - <string key="NSMaxSize">{1.79769e+308, 1.79769e+308}</string> - </object> - <object class="NSObjectController" id="1002997712"> - <object class="NSMutableArray" key="NSDeclaredKeys"> - <bool key="EncodedWithXMLCoder">YES</bool> - <string>username</string> - <string>includeTags</string> - <string>site</string> - </object> - <bool key="NSEditable">YES</bool> - <object class="_NSManagedProxy" key="_NSManagedProxy"/> - </object> - </object> - <object class="IBObjectContainer" key="IBDocument.Objects"> - <object class="NSMutableArray" key="connectionRecords"> - <bool key="EncodedWithXMLCoder">YES</bool> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">nextKeyView</string> - <reference key="source" ref="220887054"/> - <reference key="destination" ref="932260227"/> - </object> - <int key="connectionID">56</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">nextKeyView</string> - <reference key="source" ref="932260227"/> - <reference key="destination" ref="220887054"/> - </object> - <int key="connectionID">57</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">userField</string> - <reference key="source" ref="755236013"/> - <reference key="destination" ref="220887054"/> - </object> - <int key="connectionID">65</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">passField</string> - <reference key="source" ref="755236013"/> - <reference key="destination" ref="932260227"/> - </object> - <int key="connectionID">66</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">settingsView</string> - <reference key="source" ref="755236013"/> - <reference key="destination" ref="246154705"/> - </object> - <int key="connectionID">69</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">value: selection.username</string> - <reference key="source" ref="220887054"/> - <reference key="destination" ref="1002997712"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="220887054"/> - <reference key="NSDestination" ref="1002997712"/> - <string key="NSLabel">value: selection.username</string> - <string key="NSBinding">value</string> - <string key="NSKeyPath">selection.username</string> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">82</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">value: selection.includeTags</string> - <reference key="source" ref="1030464026"/> - <reference key="destination" ref="1002997712"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="1030464026"/> - <reference key="NSDestination" ref="1002997712"/> - <string key="NSLabel">value: selection.includeTags</string> - <string key="NSBinding">value</string> - <string key="NSKeyPath">selection.includeTags</string> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">84</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">value: currentPassword</string> - <reference key="source" ref="932260227"/> - <reference key="destination" ref="755236013"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="932260227"/> - <reference key="NSDestination" ref="755236013"/> - <string key="NSLabel">value: currentPassword</string> - <string key="NSBinding">value</string> - <string key="NSKeyPath">currentPassword</string> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">85</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">contentObject: selection.info</string> - <reference key="source" ref="1002997712"/> - <reference key="destination" ref="755236013"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="1002997712"/> - <reference key="NSDestination" ref="755236013"/> - <string key="NSLabel">contentObject: selection.info</string> - <string key="NSBinding">contentObject</string> - <string key="NSKeyPath">selection.info</string> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">91</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">selectedTag: selection.site</string> - <reference key="source" ref="583724646"/> - <reference key="destination" ref="1002997712"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="583724646"/> - <reference key="NSDestination" ref="1002997712"/> - <string key="NSLabel">selectedTag: selection.site</string> - <string key="NSBinding">selectedTag</string> - <string key="NSKeyPath">selection.site</string> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">98</int> - </object> - </object> - <object class="IBMutableOrderedSet" key="objectRecords"> - <object class="NSArray" key="orderedObjects"> - <bool key="EncodedWithXMLCoder">YES</bool> - <object class="IBObjectRecord"> - <int key="objectID">0</int> - <reference key="object" ref="0"/> - <reference key="children" ref="791608857"/> - <nil key="parent"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">-2</int> - <reference key="object" ref="755236013"/> - <reference key="parent" ref="0"/> - <string key="objectName">File's Owner</string> - </object> - <object class="IBObjectRecord"> - <int key="objectID">-1</int> - <reference key="object" ref="949435413"/> - <reference key="parent" ref="0"/> - <string key="objectName">First Responder</string> - </object> - <object class="IBObjectRecord"> - <int key="objectID">5</int> - <reference key="object" ref="523381760"/> - <object class="NSMutableArray" key="children"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference ref="246154705"/> - </object> - <reference key="parent" ref="0"/> - <string key="objectName">Window</string> - </object> - <object class="IBObjectRecord"> - <int key="objectID">6</int> - <reference key="object" ref="246154705"/> - <object class="NSMutableArray" key="children"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference ref="220887054"/> - <reference ref="487755233"/> - <reference ref="697429448"/> - <reference ref="932260227"/> - <reference ref="1030464026"/> - <reference ref="583724646"/> - <reference ref="397416172"/> - </object> - <reference key="parent" ref="523381760"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">50</int> - <reference key="object" ref="220887054"/> - <object class="NSMutableArray" key="children"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference ref="388661522"/> - </object> - <reference key="parent" ref="246154705"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">51</int> - <reference key="object" ref="487755233"/> - <object class="NSMutableArray" key="children"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference ref="837401498"/> - </object> - <reference key="parent" ref="246154705"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">52</int> - <reference key="object" ref="697429448"/> - <object class="NSMutableArray" key="children"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference ref="314007513"/> - </object> - <reference key="parent" ref="246154705"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">53</int> - <reference key="object" ref="932260227"/> - <object class="NSMutableArray" key="children"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference ref="574259110"/> - </object> - <reference key="parent" ref="246154705"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">70</int> - <reference key="object" ref="1030464026"/> - <object class="NSMutableArray" key="children"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference ref="11812574"/> - </object> - <reference key="parent" ref="246154705"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">92</int> - <reference key="object" ref="583724646"/> - <object class="NSMutableArray" key="children"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference ref="986910851"/> - </object> - <reference key="parent" ref="246154705"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">99</int> - <reference key="object" ref="397416172"/> - <object class="NSMutableArray" key="children"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference ref="816301873"/> - </object> - <reference key="parent" ref="246154705"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">75</int> - <reference key="object" ref="1002997712"/> - <reference key="parent" ref="0"/> - <string key="objectName">Settings</string> - </object> - <object class="IBObjectRecord"> - <int key="objectID">101</int> - <reference key="object" ref="388661522"/> - <reference key="parent" ref="220887054"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">102</int> - <reference key="object" ref="837401498"/> - <reference key="parent" ref="487755233"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">103</int> - <reference key="object" ref="314007513"/> - <reference key="parent" ref="697429448"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">104</int> - <reference key="object" ref="574259110"/> - <reference key="parent" ref="932260227"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">105</int> - <reference key="object" ref="11812574"/> - <reference key="parent" ref="1030464026"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">106</int> - <reference key="object" ref="986910851"/> - <object class="NSMutableArray" key="children"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference ref="396821977"/> - </object> - <reference key="parent" ref="583724646"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">107</int> - <reference key="object" ref="816301873"/> - <reference key="parent" ref="397416172"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">93</int> - <reference key="object" ref="396821977"/> - <object class="NSMutableArray" key="children"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference ref="174640103"/> - <reference ref="842183426"/> - <reference ref="257964281"/> - </object> - <reference key="parent" ref="986910851"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">95</int> - <reference key="object" ref="174640103"/> - <reference key="parent" ref="396821977"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">94</int> - <reference key="object" ref="842183426"/> - <reference key="parent" ref="396821977"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">-3</int> - <reference key="object" ref="1037187928"/> - <reference key="parent" ref="0"/> - <string key="objectName">Application</string> - </object> - <object class="IBObjectRecord"> - <int key="objectID">108</int> - <reference key="object" ref="257964281"/> - <reference key="parent" ref="396821977"/> - </object> - </object> - </object> - <object class="NSMutableDictionary" key="flattenedProperties"> - <bool key="EncodedWithXMLCoder">YES</bool> - <object class="NSArray" key="dict.sortedKeys"> - <bool key="EncodedWithXMLCoder">YES</bool> - <string>108.ImportedFromIB2</string> - <string>5.IBEditorWindowLastContentRect</string> - <string>5.IBWindowTemplateEditedContentRect</string> - <string>5.ImportedFromIB2</string> - <string>5.windowTemplate.hasMinSize</string> - <string>5.windowTemplate.minSize</string> - <string>50.ImportedFromIB2</string> - <string>51.ImportedFromIB2</string> - <string>52.ImportedFromIB2</string> - <string>53.CustomClassName</string> - <string>53.ImportedFromIB2</string> - <string>6.ImportedFromIB2</string> - <string>70.ImportedFromIB2</string> - <string>75.ImportedFromIB2</string> - <string>92.ImportedFromIB2</string> - <string>93.IBEditorWindowLastContentRect</string> - <string>93.ImportedFromIB2</string> - <string>94.ImportedFromIB2</string> - <string>95.ImportedFromIB2</string> - <string>99.ImportedFromIB2</string> - </object> - <object class="NSMutableArray" key="dict.values"> - <bool key="EncodedWithXMLCoder">YES</bool> - <boolean value="YES"/> - <string>{{0, 994}, {270, 151}}</string> - <string>{{0, 994}, {270, 151}}</string> - <boolean value="YES"/> - <boolean value="YES"/> - <string>{213, 107}</string> - <boolean value="YES"/> - <boolean value="YES"/> - <boolean value="YES"/> - <string>NSSecureTextField</string> - <boolean value="YES"/> - <boolean value="YES"/> - <boolean value="YES"/> - <boolean value="YES"/> - <boolean value="YES"/> - <string>{{75, 1070}, {145, 63}}</string> - <boolean value="YES"/> - <boolean value="YES"/> - <boolean value="YES"/> - <boolean value="YES"/> - </object> - </object> - <object class="NSMutableDictionary" key="unlocalizedProperties"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference key="dict.sortedKeys" ref="0"/> - <object class="NSMutableArray" key="dict.values"> - <bool key="EncodedWithXMLCoder">YES</bool> - </object> - </object> - <nil key="activeLocalization"/> - <object class="NSMutableDictionary" key="localizations"> - <bool key="EncodedWithXMLCoder">YES</bool> - <reference key="dict.sortedKeys" ref="0"/> - <object class="NSMutableArray" key="dict.values"> - <bool key="EncodedWithXMLCoder">YES</bool> - </object> - </object> - <nil key="sourceID"/> - <int key="maxID">108</int> - </object> - <object class="IBClassDescriber" key="IBDocument.Classes"> - <object class="NSMutableArray" key="referencedPartialClassDescriptions"> - <bool key="EncodedWithXMLCoder">YES</bool> - <object class="IBPartialClassDescription"> - <string key="className">FirstResponder</string> - <string key="superclassName">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBUserSource</string> - <string key="minorKey"/> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBUserSource</string> - <string key="minorKey"/> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">QSDeliciousPlugIn_Source</string> - <string key="superclassName">QSObjectSource</string> - <object class="NSMutableDictionary" key="outlets"> - <bool key="EncodedWithXMLCoder">YES</bool> - <object class="NSArray" key="dict.sortedKeys"> - <bool key="EncodedWithXMLCoder">YES</bool> - <string>passField</string> - <string>userField</string> - </object> - <object class="NSMutableArray" key="dict.values"> - <bool key="EncodedWithXMLCoder">YES</bool> - <string>NSTextField</string> - <string>NSTextField</string> - </object> - </object> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBProjectSource</string> - <string key="minorKey">QSDeliciousPlugIn_Source.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">QSDeliciousPlugIn_Source</string> - <string key="superclassName">QSObjectSource</string> - <object class="NSMutableDictionary" key="actions"> - <string key="NS.key.0">savePassword:</string> - <string key="NS.object.0">id</string> - </object> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBUserSource</string> - <string key="minorKey"/> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">QSObjectSource</string> - <string key="superclassName">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBUserSource</string> - <string key="minorKey"/> - </object> - </object> - </object> - <object class="NSMutableArray" key="referencedPartialClassDescriptionsV3.2+"> - <bool key="EncodedWithXMLCoder">YES</bool> - <object class="IBPartialClassDescription"> - <string key="className">NSApplication</string> - <object class="NSMutableDictionary" key="actions"> - <string key="NS.key.0">relaunch:</string> - <string key="NS.object.0">id</string> - </object> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSFoundation.framework/Headers/NSApplication_BLTRExtensions.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSCore.framework/Headers/QSIconLoader.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSCore.framework/Headers/QSObject.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier" id="662394336"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSCore.framework/Headers/QSObjectSource.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSCore.framework/Headers/QSObject_FileHandling.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSCore.framework/Headers/QSPlugIn.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSCore.framework/Headers/QSProxyObject.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSCore.framework/Headers/QSRegistry.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSCore.framework/Headers/QSTask.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSCore.framework/Headers/QSTriggerCenter.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSCore.framework/Headers/QSURLDownloadWrapper.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSCore.framework/Headers/UKFileWatcher.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSCore.framework/Headers/UKKQueue.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSCore.framework/Headers/UKMainThreadProxy.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSFoundation.framework/Headers/NDHotKeyEvent.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSFoundation.framework/Headers/NSArray_BLTRExtensions.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSFoundation.framework/Headers/NSObject+BLTRExtensions.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSObject</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSFoundation.framework/Headers/NSObject+ReaperExtensions.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSView</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSFoundation.framework/Headers/NSView_BLTRExtensions.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">NSWindow</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBFrameworkSource</string> - <string key="minorKey">QSFoundation.framework/Headers/NSWindow_BLTRExtensions.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">QSObjectSource</string> - <string key="superclassName">NSObject</string> - <object class="NSMutableDictionary" key="outlets"> - <string key="NS.key.0">settingsView</string> - <string key="NS.object.0">NSView</string> - </object> - <reference key="sourceIdentifier" ref="662394336"/> - </object> - </object> - </object> - <int key="IBDocument.localizationMode">0</int> - <string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string> - <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies"> - <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string> - <integer value="1060" key="NS.object.0"/> - </object> - <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencyDefaults"> - <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string> - <integer value="1060" key="NS.object.0"/> - </object> - <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies"> - <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3</string> - <integer value="3000" key="NS.object.0"/> - </object> - <bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool> - <nil key="IBDocument.LastKnownRelativeProjectPath"/> - <int key="IBDocument.defaultPropertyAccessControl">3</int> - <object class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes"> - <bool key="EncodedWithXMLCoder">YES</bool> - <object class="NSArray" key="dict.sortedKeys"> - <bool key="EncodedWithXMLCoder">YES</bool> - <string>NSMenuCheckmark</string> - <string>NSMenuMixedState</string> - </object> - <object class="NSMutableArray" key="dict.values"> - <bool key="EncodedWithXMLCoder">YES</bool> - <string>{9, 8}</string> - <string>{7, 2}</string> - </object> - </object> - </data> -</archive> diff --git a/QSDeliciousPlugInSource.nib/keyedobjects.nib b/QSDeliciousPlugInSource.nib/keyedobjects.nib Binary files differdeleted file mode 100644 index a794dc2..0000000 --- a/QSDeliciousPlugInSource.nib/keyedobjects.nib +++ /dev/null diff --git a/QSDeliciousPlugInTests.m b/QSDeliciousPlugInTests.m new file mode 100644 index 0000000..3aff4ff --- /dev/null +++ b/QSDeliciousPlugInTests.m @@ -0,0 +1,89 @@ +// +// QSDeliciousPlugInTests.m +// QSDeliciousPlugIn +// + +#import <Testing/Testing.h> +#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<QSBookmarkProvider> 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<QSBookmarkProvider> 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<QSBookmarkProvider> 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<QSBookmarkProvider> 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 89729de..9d22d91 100644 --- a/QSDeliciousPlugIn_Source.h +++ b/QSDeliciousPlugIn_Source.h @@ -6,17 +6,20 @@ // Copyright __MyCompanyName__ 2004. All rights reserved. // +#import <Foundation/Foundation.h> +#import <QSCore/QSCore.h> +#import "SocialSite.h" +#import "QSBookmarkProvider.h" +#import "QSBookmarkProviderFactory.h" -#import "QSDeliciousPlugIn_Source.h" - -@interface QSDeliciousPlugIn_Source : QSObjectSource <NSXMLParserDelegate> { - 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; } - @end +@interface QSCatalogEntry (OldStyleSourceSupport) +@property NSMutableDictionary *info; +- (id)objectForKey:(NSString *)key; +@end diff --git a/QSDeliciousPlugIn_Source.m b/QSDeliciousPlugIn_Source.m index 8dcc9b4..43a32cb 100644 --- a/QSDeliciousPlugIn_Source.m +++ b/QSDeliciousPlugIn_Source.m @@ -8,126 +8,143 @@ #import "QSDeliciousPlugIn_Source.h" #import <QSCore/QSCore.h> - #import <Security/Security.h> @implementation QSDeliciousPlugIn_Source + (void)initialize { - [self setKeys:[NSArray arrayWithObject:@"selection"] triggerChangeNotificationsForDependentKey:@"currentPassword"]; + [self setKeys:[NSArray arrayWithObject:@"selection"] triggerChangeNotificationsForDependentKey:@"currentPassword"]; } - (BOOL)indexIsValidFromDate:(NSDate *)indexDate forEntry:(NSDictionary *)theEntry { - return -[indexDate timeIntervalSinceNow] < 24 * 60 * 60; + return -[indexDate timeIntervalSinceNow] < 24 * 60 * 60; } -- (BOOL)isVisibleSource{ return YES; } +- (BOOL)isVisibleSource{ + return YES; +} - (NSImage *) iconForEntry:(NSDictionary *)dict { return [[NSBundle bundleForClass:[self class]]imageNamed:@"bookmark_icon"]; } -- (NSString *) mainNibName { - return @"QSDeliciousPrefPane"; +- (NSView *)settingsView +{ + if (![super settingsView]) { + [[NSBundle bundleForClass:[self class]] loadNibNamed:NSStringFromClass([self class]) owner:self topLevelObjects:NULL]; + } + return [super settingsView]; +} + +#pragma mark - Settings Helpers + +- (SocialSite)siteIndex { + NSDictionary *settings = self.selectedEntry.sourceSettings; + return [settings objectForKey:@"site"] != nil ? [[settings objectForKey:@"site"] integerValue] : SocialSiteDelicious; +} + +- (NSString *)currentUsername { + return [self.selectedEntry.sourceSettings objectForKey:@"username"]; +} + +- (NSString *)currentHost { + return [self.selectedEntry.sourceSettings objectForKey:@"host"]; } -- (void)populateFields { - NSLog(@"populating: %@/%@", [self.selectedEntry.sourceSettings objectForKey:@"username"], [self.selectedEntry.sourceSettings objectForKey:@"site"]); +- (NSString *)currentPassword { + return [self.selectedEntry.sourceSettings objectForKey:@"password"]; } -- (NSView *) settingsView { - if (![super settingsView]) { - [[NSBundle bundleForClass:[self class]] loadNibNamed:@"QSDeliciousPlugInSource" owner:self topLevelObjects:NULL]; - } - return [super settingsView]; +- (BOOL)includeTags { + return [[self.selectedEntry.sourceSettings objectForKey:@"includeTags"] boolValue]; } -// Keychain Access -- The QS Built-in ones seems to be broken +#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; + 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; + 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; } - (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 *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 *)keychainPasswordForURL:(NSURL *)url { - return [self passwordForHost:[url host] user:[url user] andScheme:[url scheme]]; + return [self passwordForHost:[url host] user:[url user] andScheme:[url scheme]]; } - (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, + 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], @@ -138,218 +155,130 @@ (UInt32)[pass length], [pass UTF8String], NULL); } - - return err; -} - -// Site Index/API/URL - -- (NSInteger)siteIndex { - NSDictionary *settings = self.selectedEntry.sourceSettings; - return [settings objectForKey:@"site"] != nil ? [[settings objectForKey:@"site"] integerValue] : 0; -} - -- (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; -} - -- (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; + + return err; } -- (NSString *)tagURLForIndex:(NSInteger)siteIndex { - return [NSString stringWithFormat:@"tag.%@", [self reversedSiteURLForIndex:[self siteIndex]]]; -} - -- (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; -} - -// Current Site/API URL - -- (NSString *)currentSiteURL { - return [self siteURLForIndex:[self siteIndex]]; -} - -- (NSString *)currentAPIURL { - return [self apiURLForIndex:[self siteIndex]]; -} - -// Useranme - -- (NSString *)currentUsername { - return [self.selectedEntry.sourceSettings objectForKey:@"username"]; -} - -// Password Related - -- (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; +- (NSString *)oldCurrentPassword { + NSString *account = [self currentUsername]; + if (!account) return nil; + + SocialSite site = [self siteIndex]; + NSString *host = nil; + + // 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]; + } + + NSURL *keychainURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@@%@/", account, host]]; + NSString *password = [self keychainPasswordForURL:keychainURL]; + + return password; } - (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]; -} - -// Bookarmk/Caching - -- (NSData *)cachedBookmarkData { - NSString *cachePath=[QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", [self currentSiteURL]], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", [self currentUsername]]]; - return [NSData dataWithContentsOfFile:cachePath]; -} - -- (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; + NSString *account = [self currentUsername]; + if (!account) return; + if ([newPassword length] <= 0) return; + + SocialSite site = [self siteIndex]; + NSString *host = nil; + + // 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]; + } + + NSURL *keychainURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%@@%@/", account, newPassword, host]]; + + [self addURLPasswordToKeychain:keychainURL]; } -- (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 - Objects For Entry - (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; + NSLog(@"WE HAVE BEEN REQUESTED"); + SocialSite site = [self siteIndex]; + NSString *username = [self currentUsername]; + NSString *password = [self currentPassword]; + NSString *host = [self currentHost]; + BOOL includeTags = [self includeTags]; + // Get the appropriate provider using the factory + QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; + id<QSBookmarkProvider> 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]; } -- (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; -} - -// Object Handler Methods - -/* -- (void)setQuickIconForObject:(QSObject *)object { - [object setIcon:nil]; // An icon that is either already in memory or easy to load +- (NSArray *)objectsForTag:(NSString *)tag username:(NSString *)username { + SocialSite site = [self siteIndex]; + NSString *password = [self currentPassword]; + NSString *host = [self currentHost]; + + // Get the appropriate provider using the factory + QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; + id<QSBookmarkProvider> 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 @[]; } -- (BOOL)loadIconForObject:(QSObject *)object { - return NO; - id data=[object objectForType:QSDeliciousPlugInType]; - [object setIcon:nil]; - return YES; -} -*/ +#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; -} - -// 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]; -} - -- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { - NSLog(@"XML Parser ended %@ %@ %@", elementName, namespaceURI, qName); + SocialSite site = [self siteIndex]; + + NSString *tagType = nil; + if (site == SocialSiteLinkding) { + tagType = @"tag.linkding"; + } else { + NSString *reversedURL = [SocialSiteHelper reversedSiteURLForSite:site]; + 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; + + NSArray *children = [self objectsForTag:tag username:username]; + [object setChildren:children]; + return YES; } @end diff --git a/QSDeliciousPlugIn_Source.xib b/QSDeliciousPlugIn_Source.xib new file mode 100644 index 0000000..65d8dda --- /dev/null +++ b/QSDeliciousPlugIn_Source.xib @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24123.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> + <dependencies> + <deployment identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24123.1"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="QSDeliciousPlugIn_Source"> + <connections> + <outlet property="hostField" destination="host-text-field" id="host-outlet"/> + <outlet property="passField" destination="password-text-field" id="password-outlet"/> + <outlet property="settingsView" destination="settings-view" id="settings-view-outlet"/> + <outlet property="userField" destination="username-text-field" id="username-outlet"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application" customClass="NSObject"/> + <customView id="settings-view"> + <rect key="frame" x="0.0" y="0.0" width="249" height="252"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <subviews> + <textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="site-label"> + <rect key="frame" x="35" y="218" width="53" height="16"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <constraints> + <constraint firstAttribute="height" constant="16" id="Eoo-3h-Uin"/> + </constraints> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Service:" id="site-label-cell"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + <textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="username-label"> + <rect key="frame" x="18" y="179" width="70" height="16"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <constraints> + <constraint firstAttribute="height" constant="16" id="y8T-Rp-nV6"/> + </constraints> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Username:" id="username-label-cell"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + <textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="password-label"> + <rect key="frame" x="22" y="140" width="66" height="16"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <constraints> + <constraint firstAttribute="height" constant="16" id="BpN-ax-hN6"/> + </constraints> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Password:" id="password-label-cell"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + <textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="host-label"> + <rect key="frame" x="51" y="101" width="37" height="16"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <constraints> + <constraint firstAttribute="height" constant="16" id="SmD-SH-xQe"/> + </constraints> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Host:" id="host-label-cell"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + <textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="host-help-label"> + <rect key="frame" x="94" y="75" width="137" height="14"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="left" title="Required for Linkding" id="host-help-label-cell"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + <button id="include-tags-checkbox"> + <rect key="frame" x="96" y="52" width="99" height="16"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <buttonCell key="cell" type="check" title="Include Tags" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="include-tags-checkbox-cell"> + <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + <constraints> + <constraint firstAttribute="height" constant="16" id="egr-kg-1on"/> + </constraints> + <connections> + <binding destination="-2" name="value" keyPath="selectedEntry.sourceSettings.includeTags" id="lGp-jw-qTE"/> + </connections> + </button> + <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="site-popup"> + <rect key="frame" x="96" y="214" width="133" height="24"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> + <popUpButtonCell key="cell" type="push" title="del.icio.us" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="site-delicious" id="site-popup-cell"> + <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="message"/> + <menu key="menu" id="site-menu"> + <items> + <menuItem title="del.icio.us" state="on" id="site-delicious"/> + <menuItem title="ma.gnolia.com" tag="1" id="site-magnolia"/> + <menuItem title="Pinboard" tag="2" id="site-pinboard"/> + <menuItem title="Linkding" tag="3" id="site-linkding"/> + </items> + </menu> + </popUpButtonCell> + <connections> + <binding destination="-2" name="selectedTag" keyPath="selectedEntry.sourceSettings.site" id="QdB-Zt-A1e"/> + </connections> + </popUpButton> + <textField focusRingType="none" verticalHuggingPriority="750" id="username-text-field"> + <rect key="frame" x="96" y="175" width="133" height="24"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> + <constraints> + <constraint firstAttribute="height" constant="24" id="fas-E9-Pf6"/> + </constraints> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="username-text-field-cell"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <connections> + <binding destination="-2" name="value" keyPath="selectedEntry.sourceSettings.username" id="Xmw-cK-SeB"/> + </connections> + </textField> + <secureTextField focusRingType="none" verticalHuggingPriority="750" id="password-text-field"> + <rect key="frame" x="96" y="136" width="133" height="24"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> + <constraints> + <constraint firstAttribute="height" constant="24" id="dQF-Rc-KB8"/> + </constraints> + <secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" usesSingleLineMode="YES" id="password-text-field-cell"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> + <allowedInputSourceLocales> + <string>NSAllRomanInputSourcesLocaleIdentifier</string> + </allowedInputSourceLocales> + </secureTextFieldCell> + <connections> + <binding destination="-2" name="value" keyPath="selectedEntry.sourceSettings.password" id="iVD-TC-GTV"/> + </connections> + </secureTextField> + <textField focusRingType="none" verticalHuggingPriority="750" id="host-text-field"> + <rect key="frame" x="96" y="97" width="133" height="24"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> + <constraints> + <constraint firstAttribute="height" constant="24" id="bfW-V3-8A5"/> + </constraints> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="host-text-field-cell"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <connections> + <binding destination="-2" name="value" keyPath="selectedEntry.sourceSettings.host" id="7qk-HC-e8v"/> + </connections> + </textField> + </subviews> + <point key="canvasLocation" x="155.5" y="180"/> + </customView> + <objectController id="JpK-62-U8F" userLabel="Settings"> + <declaredKeys> + <string>info.username</string> + <string>info.site</string> + <string>info.includeTags</string> + <string>info.host</string> + </declaredKeys> + <connections> + <binding destination="-2" name="contentObject" keyPath="self.selectedEntry" id="r1i-y5-4Mi"/> + <outlet property="content" destination="-2" id="UlK-eW-ayD"/> + </connections> + </objectController> + </objects> +</document> 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 <Foundation/Foundation.h> -#import <PreferencePanes/PreferencePanes.h> - - -@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 <QSCore/QSResourceManager.h> - -@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/QSLinkdingProvider.h b/QSLinkdingProvider.h new file mode 100644 index 0000000..28807e8 --- /dev/null +++ b/QSLinkdingProvider.h @@ -0,0 +1,16 @@ +// +// QSLinkdingProvider.h +// QSDeliciousPlugIn +// +// Provider for Linkding API (JSON-based with API key) +// + +#import <Foundation/Foundation.h> +#import "QSBookmarkProvider.h" + +@interface QSLinkdingProvider : NSObject <QSBookmarkProvider> + +- (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 new file mode 100644 index 0000000..f97f5c7 --- /dev/null +++ b/QSLinkdingProvider.m @@ -0,0 +1,191 @@ +// +// QSLinkdingProvider.m +// QSDeliciousPlugIn +// + +#import "QSLinkdingProvider.h" +#import "SocialSite.h" +#import <QSCore/QSCore.h> + +@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 new file mode 100644 index 0000000..2176a98 --- /dev/null +++ b/REFACTOR_GUIDE.md @@ -0,0 +1,118 @@ +# 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<QSBookmarkProvider> 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 <QSBookmarkProvider> +@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 new file mode 100644 index 0000000..f2c848c --- /dev/null +++ b/SocialSite.h @@ -0,0 +1,23 @@ +// +// SocialSite.h +// QSDeliciousPlugIn +// +// Social bookmark site enumeration +// + +#import <Foundation/Foundation.h> + +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 new file mode 100644 index 0000000..d002235 --- /dev/null +++ b/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 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"; |