mirror of
https://github.com/ksherlock/ample.git
synced 2024-11-15 22:07:28 +00:00
652 lines
17 KiB
Objective-C
652 lines
17 KiB
Objective-C
//
|
|
// BookmarkManager.m
|
|
// Ample
|
|
//
|
|
// Created by Kelvin Sherlock on 6/1/2021.
|
|
// Copyright © 2021 Kelvin Sherlock. All rights reserved.
|
|
//
|
|
|
|
#import "BookmarkManager.h"
|
|
#import "Ample.h"
|
|
|
|
#import "Bookmark.h"
|
|
#import "DiskImage.h"
|
|
#import "Menu.h"
|
|
|
|
|
|
@interface BookmarkManager () {
|
|
|
|
NSPersistentStoreCoordinator *_psc;
|
|
NSManagedObjectContext *_moc;
|
|
NSManagedObjectModel *_mom;
|
|
NSPersistentStore *_store;
|
|
|
|
NSFetchRequest *_defaultRequest;
|
|
|
|
|
|
NSURL *_bookmarkDirectory;
|
|
NSArrayController *_items;
|
|
NSUInteger _newMenuGeneration;
|
|
NSUInteger _currentMenuGeneration;
|
|
|
|
Bookmark *_currentBookmark;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface BookmarkManager (MenuDelegate) <NSMenuDelegate>
|
|
@end
|
|
|
|
@implementation BookmarkManager
|
|
|
|
static BookmarkManager *singleton = nil;
|
|
|
|
-(void)awakeFromNib {
|
|
if (!singleton) {
|
|
singleton = self;
|
|
if (!_items) [self initMenus];
|
|
}
|
|
}
|
|
|
|
+(instancetype)sharedManager {
|
|
if (!singleton) singleton = [BookmarkManager new];
|
|
return singleton;
|
|
}
|
|
|
|
-(instancetype)init {
|
|
if (singleton) return singleton;
|
|
|
|
|
|
if ((self = [super init])) {
|
|
[self initCoreData];
|
|
|
|
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
|
|
|
[nc addObserver: self selector: @selector(willTerminate:) name: NSApplicationWillTerminateNotification object: nil];
|
|
[nc addObserver: self selector: @selector(diskImageAdded:) name: kNotificationDiskImageAdded object: nil];
|
|
|
|
|
|
_newMenuGeneration = 1;
|
|
_currentMenuGeneration = 0;
|
|
}
|
|
|
|
|
|
//singleton = self;
|
|
return self;
|
|
}
|
|
|
|
-(NSManagedObjectContext *)managedObjectContext {
|
|
return _moc;
|
|
}
|
|
|
|
-(void)initCoreData {
|
|
|
|
NSError *error;
|
|
BOOL new = NO;
|
|
|
|
NSBundle *bundle = [NSBundle mainBundle];
|
|
NSURL *url = [bundle URLForResource: @"Ample" withExtension: @"momd"];
|
|
_mom = [[NSManagedObjectModel alloc] initWithContentsOfURL: url];
|
|
|
|
_psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: _mom];
|
|
|
|
_moc = [[NSManagedObjectContext alloc] initWithConcurrencyType: NSMainQueueConcurrencyType];
|
|
[_moc setPersistentStoreCoordinator: _psc];
|
|
|
|
//[_moc setMergePolicy: [NSMergePolicy rollbackMergePolicy]];
|
|
|
|
url = SupportDirectory();
|
|
#if 0
|
|
url = [url URLByAppendingPathComponent: @"Ample.db"];
|
|
|
|
if (![url checkResourceIsReachableAndReturnError: &error])
|
|
new = YES;
|
|
|
|
|
|
_store = [_psc addPersistentStoreWithType: NSSQLiteStoreType
|
|
configuration: nil
|
|
URL: url
|
|
options: nil
|
|
error: &error];
|
|
#else
|
|
|
|
url = [url URLByAppendingPathComponent: @"Ample.xml"];
|
|
|
|
if (![url checkResourceIsReachableAndReturnError: &error])
|
|
new = YES;
|
|
|
|
|
|
_store = [_psc addPersistentStoreWithType: NSXMLStoreType
|
|
configuration: nil
|
|
URL: url
|
|
options: nil
|
|
error: &error];
|
|
|
|
|
|
#endif
|
|
_defaultRequest = [Bookmark fetchRequest];
|
|
[_defaultRequest setPredicate: [NSPredicate predicateWithFormat: @"automatic == TRUE"]];
|
|
|
|
if (new) {
|
|
[self convertLegacyBookmarks];
|
|
[self convertLegacyDiskImages];
|
|
}
|
|
|
|
}
|
|
|
|
-(void)willTerminate: (NSNotification *)notification {
|
|
|
|
NSError *error;
|
|
|
|
if (![_moc save: &error]) {
|
|
NSLog(@"%@", error);
|
|
}
|
|
}
|
|
|
|
|
|
-(void)convertLegacyBookmarks {
|
|
|
|
//NSEntityDescription *e;
|
|
|
|
//e = [NSEntityDescription entityForName: @"Bookmark" inManagedObjectContext: moc];
|
|
|
|
NSURL *url = [self bookmarkDirectory];
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
NSError *error = nil;
|
|
|
|
NSArray *files = [fm contentsOfDirectoryAtURL: url
|
|
includingPropertiesForKeys: nil
|
|
options: NSDirectoryEnumerationSkipsHiddenFiles
|
|
error: &error];
|
|
|
|
NSDate *now = [NSDate date];
|
|
for (NSURL *url in files) {
|
|
|
|
|
|
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfURL: url];
|
|
//NSData *data = [NSPropertyListSerialization dataWithPropertyList: dict format: NSPropertyListBinaryFormat_v1_0 options: 0 error: &error];
|
|
|
|
Bookmark *b = (Bookmark *)[NSEntityDescription insertNewObjectForEntityForName: @"Bookmark" inManagedObjectContext: _moc];
|
|
|
|
[b setName: [url lastPathComponent]];
|
|
[b setDictionary: dict];
|
|
//[b setData: data];
|
|
[b setMachine: [dict objectForKey: @"machine"]];
|
|
[b setCreated: now];
|
|
}
|
|
|
|
// default...
|
|
|
|
url = [url URLByAppendingPathComponent: @".Default"];
|
|
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfURL: url];
|
|
if (dict) {
|
|
//NSData *data = [NSPropertyListSerialization dataWithPropertyList: dict format: NSPropertyListBinaryFormat_v1_0 options: 0 error: &error];
|
|
|
|
NSString *name = [self uniqueBookmarkName: @"Default"];
|
|
Bookmark *b = (Bookmark *)[NSEntityDescription insertNewObjectForEntityForName: @"Bookmark" inManagedObjectContext: _moc];
|
|
|
|
[b setName: name];
|
|
[b setAutomatic: YES];
|
|
[b setDictionary: dict];
|
|
//[b setData: data];
|
|
[b setMachine: [dict objectForKey: @"machine"]];
|
|
[b setCreated: now];
|
|
|
|
}
|
|
|
|
if (![_moc save: &error]) {
|
|
NSLog(@"%@", error);
|
|
}
|
|
}
|
|
|
|
-(void)convertLegacyDiskImages {
|
|
|
|
NSError *error;
|
|
NSURL *sd = SupportDirectory();
|
|
NSURL *url = [sd URLByAppendingPathComponent: @"RecentDiskImages.plist"];
|
|
|
|
NSArray *array = [NSArray arrayWithContentsOfURL: url];
|
|
if (!array) return;
|
|
|
|
for (NSDictionary *d in array) {
|
|
|
|
NSManagedObject *o = [NSEntityDescription insertNewObjectForEntityForName: @"DiskImage" inManagedObjectContext: _moc];
|
|
[o setValue: [d objectForKey: @"date"] forKey: @"added"];
|
|
[o setValue: [d objectForKey: @"date"] forKey: @"accessed"];
|
|
[o setValue: [d objectForKey: @"path"] forKey: @"path"];
|
|
[o setValue: [d objectForKey: @"size"] forKey: @"size"];
|
|
}
|
|
|
|
if (![_moc save: &error]) {
|
|
NSLog(@"%@", error);
|
|
}
|
|
}
|
|
|
|
-(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;
|
|
}
|
|
|
|
|
|
-(Bookmark *)defaultBookmark {
|
|
|
|
NSFetchRequest *req;
|
|
NSError *error;
|
|
NSArray *array;
|
|
|
|
req = [Bookmark fetchRequest];
|
|
[req setPredicate: [NSPredicate predicateWithFormat: @"automatic == TRUE"]];
|
|
array = [_moc executeFetchRequest: req error: &error];
|
|
|
|
return [array firstObject];
|
|
}
|
|
|
|
-(NSDictionary *)loadDefault {
|
|
Bookmark *b = [self defaultBookmark];
|
|
return [b dictionary];
|
|
}
|
|
|
|
/* save as .Default */
|
|
-(NSError *)saveDefault: (NSDictionary *)bookmark {
|
|
|
|
return nil;
|
|
|
|
#if 0
|
|
/* check if it already exists */
|
|
|
|
NSFetchRequest *req;
|
|
NSError *error;
|
|
NSArray *array;
|
|
Bookmark *b;
|
|
NSDate *now = [NSDate date];
|
|
BOOL ok;
|
|
|
|
req = [[NSFetchRequest alloc] initWithEntityName: @"Default"];
|
|
array = [_moc executeFetchRequest: req error: &error];
|
|
|
|
|
|
b = [array firstObject];
|
|
if (b) {
|
|
[b setModified: now];
|
|
} else {
|
|
b = (Bookmark *)[NSEntityDescription insertNewObjectForEntityForName: @"Default" inManagedObjectContext: _moc];
|
|
|
|
[b setName: @"Default"];
|
|
[b setCreated: now];
|
|
}
|
|
[b setDictionary: bookmark];
|
|
[b setMachine: [bookmark objectForKey: @"machine"]];
|
|
|
|
ok = [_moc save: &error];
|
|
if (!ok) NSLog(@"%@", error);
|
|
return error;
|
|
|
|
#if 0
|
|
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;
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
|
|
-(NSError *)setAutomatic: (Bookmark *)bookmark {
|
|
|
|
NSError *error = nil;
|
|
NSFetchRequest *req = [Bookmark fetchRequest];
|
|
[req setPredicate: [NSPredicate predicateWithFormat: @"automatic == TRUE"]];
|
|
|
|
NSArray *array = [_moc executeFetchRequest: req error: &error];
|
|
for (Bookmark *b in array) {
|
|
if (b != bookmark) [b setAutomatic: NO];
|
|
}
|
|
[bookmark setAutomatic: YES];
|
|
if (error) return error;
|
|
[_moc save: &error];
|
|
return error;
|
|
}
|
|
|
|
-(NSError *)saveBookmark: (NSDictionary *)bookmark name: (NSString *)name automatic: (BOOL)automatic {
|
|
|
|
NSDate *now = [NSDate date];
|
|
NSError *error;
|
|
BOOL ok;
|
|
|
|
Bookmark *b = (Bookmark *)[NSEntityDescription insertNewObjectForEntityForName: @"Bookmark" inManagedObjectContext: _moc];
|
|
|
|
[b setName: name];
|
|
[b setDictionary: bookmark];
|
|
[b setMachine: [bookmark objectForKey: @"machine"]];
|
|
[b setCreated: now];
|
|
[b setAutomatic: automatic];
|
|
|
|
ok = [b validateForInsert: &error];
|
|
if (!ok) {
|
|
// will be useful, eg "name is too long"
|
|
// keys: NSValidationErrorObject, NSLocalizedDescription, NSValidationErrorKey, NSValidationErrorValue
|
|
//NSLog(@"%@", error);
|
|
[_moc deleteObject: b];
|
|
return error;
|
|
}
|
|
|
|
|
|
ok = [_moc save: &error];
|
|
if (!ok) {
|
|
//NSLog(@"%@", error);
|
|
[_moc deleteObject: b];
|
|
error = [NSError errorWithDomain: @"Ample" code: 0 userInfo: @{ NSLocalizedDescriptionKey: @"Duplicate name" }];
|
|
return error;
|
|
}
|
|
|
|
if (automatic) {
|
|
[self setAutomatic: b];
|
|
}
|
|
|
|
|
|
return nil;
|
|
}
|
|
|
|
-(void)loadBookmarks {
|
|
|
|
NSSortDescriptor *s = [NSSortDescriptor sortDescriptorWithKey: @"name" ascending: YES selector: @selector(caseInsensitiveCompare:)];
|
|
|
|
|
|
_items = [NSArrayController new];
|
|
[_items setManagedObjectContext: _moc];
|
|
[_items setAvoidsEmptySelection: NO];
|
|
[_items setAutomaticallyPreparesContent: YES];
|
|
[_items setAutomaticallyRearrangesObjects: YES];
|
|
[_items setEntityName: @"Bookmark"];
|
|
[_items setSortDescriptors: @[ s ]];
|
|
|
|
[_items fetch: nil];
|
|
}
|
|
|
|
|
|
/* extract the number from a trailing " (%d)" */
|
|
static int extract_number(NSString *s, NSInteger offset) {
|
|
|
|
unichar buffer[32];
|
|
NSInteger len = [s length] - offset;
|
|
unichar c;
|
|
int i;
|
|
int n = 0;
|
|
|
|
if (len < 4) return -1; /* " (1)"*/
|
|
if (len > 8) return -1; /* " (99999)" */
|
|
|
|
NSRange r = NSMakeRange(offset, len);
|
|
[s getCharacters: buffer range: r];
|
|
|
|
buffer[len] = 0;
|
|
i = 0;
|
|
if (buffer[i++] != ' ') return -1;
|
|
if (buffer[i++] != '(') return -1;
|
|
|
|
c = buffer[i++];
|
|
if (c < '1' || c > '9') return -1;
|
|
n = c - '0';
|
|
|
|
for (;;) {
|
|
c = buffer[i];
|
|
if (c < '0' || c > '9') break;
|
|
n = n * 10 + (c - '0');
|
|
++i;
|
|
}
|
|
|
|
if (buffer[i++] != ')') return -1;
|
|
if (buffer[i++] != 0) return -1;
|
|
|
|
return n;
|
|
}
|
|
|
|
|
|
-(NSString *)uniqueBookmarkName: (NSString *)name {
|
|
|
|
NSInteger length = [name length];
|
|
|
|
NSError *error = nil;
|
|
NSPredicate *p = [NSPredicate predicateWithFormat: @"name BEGINSWITH %@", name];
|
|
NSFetchRequest *req = [NSFetchRequest fetchRequestWithEntityName: @"Bookmark"];
|
|
[req setPredicate: p];
|
|
|
|
NSArray *array = [_moc executeFetchRequest: req error: &error];
|
|
if (![array count]) return name;
|
|
|
|
uint64_t bits = 1; /* mark 0 as unavailable */
|
|
NSInteger max = 0;
|
|
BOOL exact = NO;
|
|
for (Bookmark *b in array) {
|
|
NSString *s = [b name];
|
|
if ([name isEqualToString: s]) {
|
|
exact = YES;
|
|
continue;
|
|
}
|
|
int n = extract_number(s, length);
|
|
if (n < 1) continue;
|
|
if (n > max) max = n;
|
|
if (n < 64)
|
|
bits |= (1 << n);
|
|
}
|
|
if (!exact) return name;
|
|
|
|
if (bits == (uint64_t)-1) {
|
|
return [name stringByAppendingFormat: @" (%u)", (int)(max + 1)];
|
|
}
|
|
|
|
#if 1
|
|
int ix = 0;
|
|
while (bits & 0x01) {
|
|
++ix;
|
|
bits >>= 1;
|
|
}
|
|
#else
|
|
// this doesn't work correctly.
|
|
int ix = __builtin_ffsll(~bits);
|
|
#endif
|
|
return [name stringByAppendingFormat: @" (%u)", ix];
|
|
|
|
|
|
}
|
|
|
|
|
|
-(BOOL)addDiskImage: (NSObject *)pathOrURL {
|
|
|
|
NSError *error;
|
|
|
|
NSString *path = nil;
|
|
NSURL *url = nil;
|
|
if ([pathOrURL isKindOfClass: [NSString class]]) {
|
|
path = (NSString *)pathOrURL;
|
|
} else if ([pathOrURL isKindOfClass: [NSURL class]]){
|
|
url = (NSURL *)pathOrURL;
|
|
|
|
path = [NSString stringWithCString: [url fileSystemRepresentation] encoding: NSUTF8StringEncoding];
|
|
}
|
|
if (!path) return NO;
|
|
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
|
|
|
|
NSDictionary *attr = [fm attributesOfItemAtPath: path error: &error];
|
|
if (error) {
|
|
NSLog(@"%@ : %@", path, error);
|
|
return NO;
|
|
}
|
|
|
|
NSNumber *size = [attr objectForKey: NSFileSize];
|
|
|
|
NSDate *now = [NSDate date];
|
|
|
|
NSPredicate *p = [NSPredicate predicateWithFormat: @"path = %@", path];
|
|
NSFetchRequest *req = [NSFetchRequest fetchRequestWithEntityName: @"DiskImage"];
|
|
[req setPredicate: p];
|
|
|
|
|
|
NSArray *array = [_moc executeFetchRequest: req error: &error];
|
|
BOOL found = 0;
|
|
for (NSManagedObject *o in array) {
|
|
found = YES;
|
|
[o setValue: now forKey: @"accessed"];
|
|
}
|
|
if (found) return NO;
|
|
|
|
DiskImage *o = [NSEntityDescription insertNewObjectForEntityForName: @"DiskImage" inManagedObjectContext: _moc];
|
|
|
|
|
|
[o setPath: path];
|
|
[o setAdded: now];
|
|
[o setAccessed: now];
|
|
[o setSize: [size longLongValue]];
|
|
[o updatePath];
|
|
|
|
if (![_moc save: &error]) {
|
|
NSLog(@"%@", error);
|
|
[_moc deleteObject: o];
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
-(void)diskImageAdded: (NSNotification *)notification {
|
|
|
|
NSURL *url = [notification object];
|
|
if (url) [self addDiskImage: url];
|
|
}
|
|
|
|
static NSString *kMenuContext = @"";
|
|
|
|
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
|
|
if (context == (__bridge void * _Nullable)(kMenuContext)) {
|
|
|
|
//NSLog(@"observeValueForKeyPath %@", keyPath);
|
|
|
|
_newMenuGeneration++;
|
|
|
|
return;
|
|
}
|
|
|
|
[super observeValueForKeyPath: keyPath ofObject: object change: change context: context];
|
|
}
|
|
|
|
-(void)initMenus {
|
|
|
|
if (!_items) {
|
|
[self loadBookmarks];
|
|
[_items addObserver: self forKeyPath: @"arrangedObjects.name" options: 0 context: (__bridge void * _Nullable)(kMenuContext)];
|
|
[_items addObserver: self forKeyPath: @"arrangedObjects.automatic" options: 0 context: (__bridge void * _Nullable)(kMenuContext)];
|
|
}
|
|
|
|
}
|
|
|
|
-(IBAction)bookmarkMenu:(id)sender
|
|
{
|
|
}
|
|
|
|
-(Bookmark *)currentBookmark {
|
|
return _currentBookmark;
|
|
}
|
|
-(void)setCurrentBookmark:(Bookmark *)currentBookmark {
|
|
if (currentBookmark == _currentBookmark) return;
|
|
_currentBookmark = currentBookmark;
|
|
_newMenuGeneration++;
|
|
}
|
|
|
|
-(void)menuNeedsUpdate:(NSMenu *)menu {
|
|
|
|
if (_currentMenuGeneration == _newMenuGeneration) return;
|
|
_currentMenuGeneration = _newMenuGeneration;
|
|
|
|
if (_currentBookmark && _updateMenuItem) {
|
|
NSString *title = [NSString stringWithFormat: @"Update “%@”", [_currentBookmark name]];
|
|
[_updateMenuItem setHidden: NO];
|
|
[_updateMenuItem setTitle: title];
|
|
[_updateMenuItem setRepresentedObject: _currentBookmark];
|
|
} else {
|
|
[_updateMenuItem setHidden: YES];
|
|
[_updateMenuItem setRepresentedObject: nil];
|
|
}
|
|
|
|
|
|
|
|
NSArray *menus = [menu itemArray];
|
|
for (NSMenuItem *item in [menus reverseObjectEnumerator]) {
|
|
if ([item tag] == 0xdeadbeef) [_menu removeItem: item];
|
|
}
|
|
|
|
NSArray *array = [_items arrangedObjects];
|
|
for (Bookmark *b in array) {
|
|
|
|
NSString *title = [b name];
|
|
NSMenuItem *item = [menu addItemWithTitle: title action: @selector(bookmarkMenu:) keyEquivalent: @""];
|
|
[item setRepresentedObject: b];
|
|
[item setTag: 0xdeadbeef];
|
|
if ([b automatic]) {
|
|
|
|
[item setOnStateImage: [NSImage imageNamed: NSImageNameStatusAvailable]];
|
|
[item setState: NSOnState];
|
|
}
|
|
//if ([b automatic]) [item setAttributedTitle: ItalicMenuString([b name])];
|
|
//[item setState: [b automatic] ? NSMixedState : NSOffState];
|
|
}
|
|
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
/* MacOS 12.1+ doesn't like class clusters in nibs -
|
|
|
|
[General] This coder is expecting the replaced object 0x600000938f60 to be returned from
|
|
NSClassSwapper.initWithCoder instead of <BookmarkManager: 0x600000905da0>
|
|
|
|
*/
|
|
@interface BookmarkManagerProxy : NSProxy {
|
|
BookmarkManager *_target;
|
|
}
|
|
@end
|
|
|
|
@implementation BookmarkManagerProxy
|
|
|
|
-(id)init {
|
|
_target = [BookmarkManager sharedManager];
|
|
return self;
|
|
}
|
|
|
|
-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
|
|
return [_target methodSignatureForSelector: sel];
|
|
}
|
|
|
|
+(BOOL)respondsToSelector:(SEL)aSelector {
|
|
return [BookmarkManager respondsToSelector: aSelector];
|
|
}
|
|
-(void)forwardInvocation:(NSInvocation *)invocation {
|
|
if ([_target respondsToSelector: [invocation selector]]) {
|
|
[invocation invokeWithTarget: _target];
|
|
} else {
|
|
[super forwardInvocation: invocation];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@end
|