summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2025-09-06 23:03:04 +0200
committerRuben Beltran del Rio <git@r.bdr.sh>2025-09-06 23:03:04 +0200
commit6f7a0e57ce0650c9e91b15e75ec9a0bb85f6ddb6 (patch)
treeb4d623e54f552488408f13e5f6ad5bed54643a29
parent685a4d4b33991a315060145724d794d709ec6f3c (diff)
Make the provider functional
-rw-r--r--QSBookmarkProvider.h48
-rw-r--r--QSBookmarkProviderFactory.h28
-rw-r--r--QSBookmarkProviderFactory.m66
-rw-r--r--QSDeliciousAPIProvider.h25
-rw-r--r--QSDeliciousAPIProvider.m190
-rw-r--r--QSDeliciousPlugIn.xcodeproj/project.pbxproj53
-rw-r--r--QSDeliciousPlugInSource.nib/designable.nib930
-rw-r--r--QSDeliciousPlugInSource.nib/keyedobjects.nibbin9343 -> 0 bytes
-rw-r--r--QSDeliciousPlugInTests.m89
-rw-r--r--QSDeliciousPlugIn_Source.h23
-rw-r--r--QSDeliciousPlugIn_Source.m485
-rw-r--r--QSDeliciousPlugIn_Source.xib177
-rw-r--r--QSDeliciousPrefPane.h13
-rw-r--r--QSDeliciousPrefPane.m39
-rw-r--r--QSLinkdingProvider.h16
-rw-r--r--QSLinkdingProvider.m191
-rw-r--r--REFACTOR_GUIDE.md118
-rw-r--r--SocialSite.h23
-rw-r--r--SocialSite.m55
-rw-r--r--en.lproj/Localizable.strings19
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
deleted file mode 100644
index a794dc2..0000000
--- a/QSDeliciousPlugInSource.nib/keyedobjects.nib
+++ /dev/null
Binary files differ
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";