From 4baf545245d8d3debf8f436b36b14cca23bf7a33 Mon Sep 17 00:00:00 2001 From: Kelvin Sherlock Date: Thu, 3 Jun 2021 23:01:15 -0400 Subject: [PATCH] bookmark manager --- Ample.xcodeproj/project.pbxproj | 8 ++ Ample/BookmarkManager.h | 34 +++++ Ample/BookmarkManager.m | 214 ++++++++++++++++++++++++++++++++ 3 files changed, 256 insertions(+) create mode 100644 Ample/BookmarkManager.h create mode 100644 Ample/BookmarkManager.m diff --git a/Ample.xcodeproj/project.pbxproj b/Ample.xcodeproj/project.pbxproj index 1d83da4..c9b292c 100644 --- a/Ample.xcodeproj/project.pbxproj +++ b/Ample.xcodeproj/project.pbxproj @@ -64,6 +64,8 @@ B6152B5B25F5B57E00605E6E /* Media.m in Sources */ = {isa = PBXBuildFile; fileRef = B6152B5925F5B57E00605E6E /* Media.m */; }; B615A99F26640940001FBF99 /* SlotView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B6E9A18125088B36005E7525 /* SlotView.xib */; }; B615A9A026640A70001FBF99 /* SlotViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B6E9A17F25088B1B005E7525 /* SlotViewController.m */; }; + B63005332666D6940014C381 /* BookmarkManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B63005322666D6940014C381 /* BookmarkManager.m */; }; + B63005342666D6940014C381 /* BookmarkManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B63005322666D6940014C381 /* BookmarkManager.m */; }; B6374AC4260EBBCF0045CA16 /* pty_shell.c in Sources */ = {isa = PBXBuildFile; fileRef = B6374AB6260EBB970045CA16 /* pty_shell.c */; }; B6374AC5260EBC5A0045CA16 /* pty_shell in CopyFiles */ = {isa = PBXBuildFile; fileRef = B6374ABD260EBBC90045CA16 /* pty_shell */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; B6374AD1260ECB400045CA16 /* macclas2.plist in Resources */ = {isa = PBXBuildFile; fileRef = B6374AC9260ECB3F0045CA16 /* macclas2.plist */; }; @@ -375,6 +377,8 @@ B6152B5525F4549F00605E6E /* Slot.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Slot.m; sourceTree = ""; }; B6152B5825F5B4F100605E6E /* Media.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Media.h; sourceTree = ""; }; B6152B5925F5B57E00605E6E /* Media.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Media.m; sourceTree = ""; }; + B63005312666D6940014C381 /* BookmarkManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BookmarkManager.h; sourceTree = ""; }; + B63005322666D6940014C381 /* BookmarkManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BookmarkManager.m; sourceTree = ""; }; B6374AB6260EBB970045CA16 /* pty_shell.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = pty_shell.c; sourceTree = ""; }; B6374ABD260EBBC90045CA16 /* pty_shell */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = pty_shell; sourceTree = BUILT_PRODUCTS_DIR; }; B6374AC9260ECB3F0045CA16 /* macclas2.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = macclas2.plist; sourceTree = ""; }; @@ -690,6 +694,8 @@ B608E17E2502FE0C00D53465 /* TransparentScroller.m */, B66D0FE62611386B000902F1 /* SoftwareList.m */, B66D0FE926113AA8000902F1 /* SoftwareList.h */, + B63005312666D6940014C381 /* BookmarkManager.h */, + B63005322666D6940014C381 /* BookmarkManager.m */, B6BA563A251685DA00B0C47D /* Window Controllers */, B6B9EA652506A5550080E70D /* EjectButton.h */, B6B9EA642506A5550080E70D /* EjectButton.m */, @@ -1154,6 +1160,7 @@ B60A6E1424EE0AE2004B7EEF /* FlippedView.m in Sources */, B6BA258024E99BE9005FB8FF /* AppDelegate.m in Sources */, B6004DF024FB05D600D38596 /* LogWindowController.m in Sources */, + B63005332666D6940014C381 /* BookmarkManager.m in Sources */, B66236A924FD9A34006CABD7 /* PreferencesWindowController.m in Sources */, B63C1F0F25B1447C0016A611 /* CheatSheetWindowController.m in Sources */, B64AF1F2250ECB2E00A09B9B /* DiskImagesWindowController.m in Sources */, @@ -1183,6 +1190,7 @@ B6E4B5B324FDE2670094A35C /* MediaViewController.m in Sources */, B64AF1F7250ED5E400A09B9B /* TableCellView.m in Sources */, B6E4B5B424FDE2670094A35C /* FlippedView.m in Sources */, + B63005342666D6940014C381 /* BookmarkManager.m in Sources */, B615A9A026640A70001FBF99 /* SlotViewController.m in Sources */, B6665C15265A0E3E00254939 /* AutocompleteControl.m in Sources */, B6E4B5B524FDE2670094A35C /* AppDelegate.m in Sources */, diff --git a/Ample/BookmarkManager.h b/Ample/BookmarkManager.h new file mode 100644 index 0000000..971bc33 --- /dev/null +++ b/Ample/BookmarkManager.h @@ -0,0 +1,34 @@ +// +// BookmarkManager.h +// Ample +// +// Created by Kelvin Sherlock on 6/1/2021. +// Copyright © 2021 Kelvin Sherlock. All rights reserved. +// + +#import + +@class NSMenu; + +NS_ASSUME_NONNULL_BEGIN + +@interface BookmarkManager : NSObject + +@property (weak) IBOutlet NSMenu *menu; + ++(instancetype)sharedManager; + +-(void)loadBookmarks; +-(void)updateMenu; + +-(BOOL)validateName: (NSString *)name; + +-(BOOL)saveBookmark: (NSDictionary *)bookmark name: (NSString *)name; +-(NSDictionary *)loadBookmarkFromURL: (NSURL *)url; + +-(BOOL)saveDefault: (NSDictionary *)bookmark; +-(NSDictionary *)loadDefault; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Ample/BookmarkManager.m b/Ample/BookmarkManager.m new file mode 100644 index 0000000..cae34ec --- /dev/null +++ b/Ample/BookmarkManager.m @@ -0,0 +1,214 @@ +// +// BookmarkManager.m +// Ample +// +// Created by Kelvin Sherlock on 6/1/2021. +// Copyright © 2021 Kelvin Sherlock. All rights reserved. +// + +#import "BookmarkManager.h" +#import "Ample.h" + +@interface BookmarkManager () { + NSArray *_urls; + NSURL *_bookmarkDirectory; +} + +@end + +@implementation BookmarkManager + +static BookmarkManager *singleton = nil; + +-(void)awakeFromNib { + if (!singleton) singleton = self; +} + ++(instancetype)sharedManager { + if (!singleton) singleton = [BookmarkManager new]; + return singleton; +} + +-(instancetype)init { + if (singleton) return singleton; + return [super init]; +} + +-(NSURL *)bookmarkDirectory { + + if (_bookmarkDirectory) return _bookmarkDirectory; + NSFileManager *fm = [NSFileManager defaultManager]; + + NSURL *url = SupportDirectory(); + url = [url URLByAppendingPathComponent: @"Bookmarks"]; + NSError *error = nil; + [fm createDirectoryAtURL: url withIntermediateDirectories: YES attributes: nil error: &error]; + if (error) NSLog(@"%@", error); + _bookmarkDirectory = url; + return url; +} + +/* disallow leading . + * disallow : or / characters. + */ +-(BOOL)validateName: (NSString *)name { + + enum { kMaxLength = 128 }; + unichar buffer[kMaxLength]; + NSUInteger length = [name length]; + if (length == 0 || length > kMaxLength) return NO; + [name getCharacters: buffer range: NSMakeRange(0, length)]; + if (buffer[0] == '.') return NO; + for (unsigned i = 0; i < length; ++i) { + unichar c = buffer[i]; + if (c == ':' || c == '/') return NO; + } + return YES; +} + + +-(NSDictionary *)loadDefault { + NSURL *url = [self bookmarkDirectory]; + url = [url URLByAppendingPathComponent: @".Default"]; + + NSDictionary *d; + + if (@available(macOS 10.13, *)) { + NSError *error = nil; + d = [NSDictionary dictionaryWithContentsOfURL: url error: &error]; + if (!d) NSLog(@"Error loading %@: %@", url, error); + } else { + d = [NSDictionary dictionaryWithContentsOfURL: url]; + if (!d) NSLog(@"Error loading %@", url); + } + return d; +} + +/* save as .Default */ +-(BOOL)saveDefault: (NSDictionary *)bookmark { + + NSURL *url = [self bookmarkDirectory]; + url = [url URLByAppendingPathComponent: @".Default"]; + + NSError *error = nil; + BOOL ok = NO; + if (@available(macOS 10.13, *)) { + ok = [bookmark writeToURL: url error: &error]; + if (!ok) NSLog(@"%@", error); + } else { + ok = [bookmark writeToURL: url atomically: YES]; + } + return ok; +} + +-(BOOL)saveBookmark: (NSDictionary *)bookmark name: (NSString *)name { + + NSError *error; + NSData *data = [NSPropertyListSerialization dataWithPropertyList: bookmark + format: NSPropertyListXMLFormat_v1_0 + options: 0 + error: &error]; + + + + NSURL *base = [self bookmarkDirectory]; + + NSURL *url = [base URLByAppendingPathComponent: name]; + + BOOL ok = [data writeToURL: url options: NSDataWritingWithoutOverwriting error: &error]; + + if (!ok) { + for (unsigned i = 1 ; i < 100; ++i) { + NSString *tmp = [name stringByAppendingFormat: @"(%d)", i]; + [base URLByAppendingPathComponent: tmp]; + + ok = [data writeToURL: url options: NSDataWritingWithoutOverwriting error: &error]; + if (ok) { + name = tmp; + break; + } + } + } + if (!ok) return NO; + + if (!_menu) return YES; // ? + + NSUInteger ix = [_urls indexOfObjectPassingTest: ^BOOL(NSURL *object, NSUInteger index, BOOL *stop){ + NSString *path = [object lastPathComponent]; + return [name caseInsensitiveCompare: path] == NSOrderedAscending; + }]; + + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle: name action: @selector(loadBookmark:) keyEquivalent: @""]; + [item setRepresentedObject: url]; + + if (ix == NSNotFound) { + _urls = [_urls arrayByAddingObject: url]; + [_menu addItem: item]; + } else { + + NSInteger n = [_menu numberOfItems]; + [_menu insertItem: item atIndex: n - [_urls count] + ix]; + NSMutableArray *tmp = [_urls mutableCopy]; + + [tmp insertObject: url atIndex: ix]; + } + + return YES; +} + +-(NSDictionary *)loadBookmarkFromURL: (NSURL *)url { + + NSDictionary *d; + + if (@available(macOS 10.13, *)) { + NSError *error = nil; + d = [NSDictionary dictionaryWithContentsOfURL: url error: &error]; + if (!d) NSLog(@"Error loading %@: %@", url, error); + } else { + d = [NSDictionary dictionaryWithContentsOfURL: url]; + if (!d) NSLog(@"Error loading %@", url); + } + return d; +} + + +-(void)loadBookmarks { + + NSURL *url = [self bookmarkDirectory]; + + NSFileManager *fm = [NSFileManager defaultManager]; + + NSError *error = nil; + + NSArray *files = [fm contentsOfDirectoryAtURL: url + includingPropertiesForKeys: nil + options: NSDirectoryEnumerationSkipsHiddenFiles + error: &error]; + + // bleh, has to create 2 new NSStrings for every comparison + files = [files sortedArrayUsingComparator: ^(NSURL *a, NSURL *b){ + NSString *aa = [a lastPathComponent]; + NSString *bb = [b lastPathComponent]; + return [aa caseInsensitiveCompare: bb]; + }]; + + + _urls = files; +} + +-(void)updateMenu { + + NSArray *menus = [_menu itemArray]; + for (NSMenuItem *item in [menus reverseObjectEnumerator]) { + if ([item tag] == 0xdeadbeef) [_menu removeItem: item]; + } + for (NSURL *url in _urls) { + NSString *title = [url lastPathComponent]; // [[url lastPathComponent] stringByDeletingPathExtension]; + + NSMenuItem *item = [_menu addItemWithTitle: title action: @selector(loadBookmark:) keyEquivalent: @""]; + [item setRepresentedObject: url]; + [item setTag: 0xdeadbeef]; + } +} + +@end