diff --git a/clients/macuploader/.gitignore b/clients/macuploader/.gitignore new file mode 100644 index 00000000..8796195e --- /dev/null +++ b/clients/macuploader/.gitignore @@ -0,0 +1,137 @@ + +# Created by https://www.gitignore.io/api/osx,xcode,objective-c,vim + +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Xcode ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + + +### Objective-C ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xcuserstate + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +### Objective-C Patch ### +*.xcscmblueprint + + +### Vim ### +# swap +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags diff --git a/clients/macuploader/Graphics/appicon.sketch b/clients/macuploader/Graphics/appicon.sketch new file mode 100644 index 00000000..b20695d1 Binary files /dev/null and b/clients/macuploader/Graphics/appicon.sketch differ diff --git a/clients/macuploader/Graphics/export.png b/clients/macuploader/Graphics/export.png new file mode 100644 index 00000000..b5083be0 Binary files /dev/null and b/clients/macuploader/Graphics/export.png differ diff --git a/clients/macuploader/Graphics/menubar.sketch b/clients/macuploader/Graphics/menubar.sketch new file mode 100644 index 00000000..55ed3814 Binary files /dev/null and b/clients/macuploader/Graphics/menubar.sketch differ diff --git a/clients/macuploader/Microbit Uploader.xcodeproj/project.pbxproj b/clients/macuploader/Microbit Uploader.xcodeproj/project.pbxproj new file mode 100644 index 00000000..6b0ae1fa --- /dev/null +++ b/clients/macuploader/Microbit Uploader.xcodeproj/project.pbxproj @@ -0,0 +1,307 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + E93040071D895D1F00D931CA /* DirectoryWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = E93040061D895D1F00D931CA /* DirectoryWatcher.m */; }; + E930400A1D89620900D931CA /* Uploader.m in Sources */ = {isa = PBXBuildFile; fileRef = E93040091D89620900D931CA /* Uploader.m */; }; + E9F4FEE21D8709980071D783 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E9F4FEE11D8709980071D783 /* AppDelegate.m */; }; + E9F4FEE51D8709980071D783 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E9F4FEE41D8709980071D783 /* main.m */; }; + E9F4FEE71D8709980071D783 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E9F4FEE61D8709980071D783 /* Assets.xcassets */; }; + E9F4FEEA1D8709980071D783 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9F4FEE81D8709980071D783 /* MainMenu.xib */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + E93040051D895D1F00D931CA /* DirectoryWatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DirectoryWatcher.h; sourceTree = ""; }; + E93040061D895D1F00D931CA /* DirectoryWatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DirectoryWatcher.m; sourceTree = ""; }; + E93040081D89620900D931CA /* Uploader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Uploader.h; sourceTree = ""; }; + E93040091D89620900D931CA /* Uploader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Uploader.m; sourceTree = ""; }; + E9F4FEDD1D8709980071D783 /* Microbit Uploader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Microbit Uploader.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + E9F4FEE01D8709980071D783 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + E9F4FEE11D8709980071D783 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + E9F4FEE41D8709980071D783 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + E9F4FEE61D8709980071D783 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + E9F4FEE91D8709980071D783 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + E9F4FEEB1D8709980071D783 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + E9F4FEDA1D8709980071D783 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + E9F4FED41D8709980071D783 = { + isa = PBXGroup; + children = ( + E9F4FEDF1D8709980071D783 /* Microbit Uploader */, + E9F4FEDE1D8709980071D783 /* Products */, + ); + sourceTree = ""; + }; + E9F4FEDE1D8709980071D783 /* Products */ = { + isa = PBXGroup; + children = ( + E9F4FEDD1D8709980071D783 /* Microbit Uploader.app */, + ); + name = Products; + sourceTree = ""; + }; + E9F4FEDF1D8709980071D783 /* Microbit Uploader */ = { + isa = PBXGroup; + children = ( + E9F4FEE01D8709980071D783 /* AppDelegate.h */, + E9F4FEE11D8709980071D783 /* AppDelegate.m */, + E9F4FEE61D8709980071D783 /* Assets.xcassets */, + E9F4FEE81D8709980071D783 /* MainMenu.xib */, + E9F4FEEB1D8709980071D783 /* Info.plist */, + E9F4FEE31D8709980071D783 /* Supporting Files */, + E93040051D895D1F00D931CA /* DirectoryWatcher.h */, + E93040061D895D1F00D931CA /* DirectoryWatcher.m */, + E93040081D89620900D931CA /* Uploader.h */, + E93040091D89620900D931CA /* Uploader.m */, + ); + path = "Microbit Uploader"; + sourceTree = ""; + }; + E9F4FEE31D8709980071D783 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + E9F4FEE41D8709980071D783 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + E9F4FEDC1D8709980071D783 /* Microbit Uploader */ = { + isa = PBXNativeTarget; + buildConfigurationList = E9F4FEEE1D8709980071D783 /* Build configuration list for PBXNativeTarget "Microbit Uploader" */; + buildPhases = ( + E9F4FED91D8709980071D783 /* Sources */, + E9F4FEDA1D8709980071D783 /* Frameworks */, + E9F4FEDB1D8709980071D783 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Microbit Uploader"; + productName = "Microbit Uploader"; + productReference = E9F4FEDD1D8709980071D783 /* Microbit Uploader.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E9F4FED51D8709980071D783 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = thomasdenney; + TargetAttributes = { + E9F4FEDC1D8709980071D783 = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = E9F4FED81D8709980071D783 /* Build configuration list for PBXProject "Microbit Uploader" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = E9F4FED41D8709980071D783; + productRefGroup = E9F4FEDE1D8709980071D783 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E9F4FEDC1D8709980071D783 /* Microbit Uploader */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + E9F4FEDB1D8709980071D783 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E9F4FEE71D8709980071D783 /* Assets.xcassets in Resources */, + E9F4FEEA1D8709980071D783 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + E9F4FED91D8709980071D783 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E9F4FEE51D8709980071D783 /* main.m in Sources */, + E930400A1D89620900D931CA /* Uploader.m in Sources */, + E9F4FEE21D8709980071D783 /* AppDelegate.m in Sources */, + E93040071D895D1F00D931CA /* DirectoryWatcher.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + E9F4FEE81D8709980071D783 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + E9F4FEE91D8709980071D783 /* Base */, + ); + name = MainMenu.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + E9F4FEEC1D8709980071D783 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + E9F4FEED1D8709980071D783 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + }; + name = Release; + }; + E9F4FEEF1D8709980071D783 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = "Microbit Uploader/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.thomasdenney.Microbit-Uploader"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + E9F4FEF01D8709980071D783 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = "Microbit Uploader/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.thomasdenney.Microbit-Uploader"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + E9F4FED81D8709980071D783 /* Build configuration list for PBXProject "Microbit Uploader" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E9F4FEEC1D8709980071D783 /* Debug */, + E9F4FEED1D8709980071D783 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E9F4FEEE1D8709980071D783 /* Build configuration list for PBXNativeTarget "Microbit Uploader" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E9F4FEEF1D8709980071D783 /* Debug */, + E9F4FEF01D8709980071D783 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = E9F4FED51D8709980071D783 /* Project object */; +} diff --git a/clients/macuploader/Microbit Uploader.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/clients/macuploader/Microbit Uploader.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..8de3976b --- /dev/null +++ b/clients/macuploader/Microbit Uploader.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/clients/macuploader/Microbit Uploader/AppDelegate.h b/clients/macuploader/Microbit Uploader/AppDelegate.h new file mode 100644 index 00000000..33011082 --- /dev/null +++ b/clients/macuploader/Microbit Uploader/AppDelegate.h @@ -0,0 +1,6 @@ +#import + +@interface AppDelegate : NSObject + +@end + diff --git a/clients/macuploader/Microbit Uploader/AppDelegate.m b/clients/macuploader/Microbit Uploader/AppDelegate.m new file mode 100644 index 00000000..937991e6 --- /dev/null +++ b/clients/macuploader/Microbit Uploader/AppDelegate.m @@ -0,0 +1,124 @@ +#import "AppDelegate.h" +#import "DirectoryWatcher.h" +#import "Uploader.h" + +@interface AppDelegate () + +@property (weak) IBOutlet NSWindow *window; +@property DirectoryWatcher * watcher; +@property Uploader * uploader; +@property NSStatusItem * menubarItem; + +@end + +@implementation AppDelegate + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + // Insert code here to initialize your application + self.watcher = [[DirectoryWatcher alloc] initWithPath:[self downloadsDirectory]]; + self.watcher.delegate = self; + [self.watcher startWatching]; + + self.uploader = [[Uploader alloc] init]; + self.uploader.delegate = self; + + [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; + + [self createMenuBarIcon]; + [self configureVolumeMountNotifications]; + [self showActiveMicroBits]; +} + +- (void)applicationWillTerminate:(NSNotification *)aNotification { + // Insert code here to tear down your application + [self.watcher stopWatching]; +} + +- (void)dealloc { + [[NSWorkspace sharedWorkspace].notificationCenter removeObserver:self]; +} + +#pragma mark - Directory + +- (void)watcher:(DirectoryWatcher *)watcher observedNewFileAtPath:(NSString *)path { + NSString * fullPath = [watcher.path stringByAppendingPathComponent:path]; + if ([self.uploader shouldUploadFileAtPath:fullPath]) { + [self.uploader uploadFile:fullPath]; + } +} + +- (NSString*)downloadsDirectory { + NSArray * paths = NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSUserDomainMask, YES); + return paths.firstObject; +} + +#pragma mark - Uploader delegate + +- (void)uploader:(Uploader *)uploader transferredFile:(NSString *)file toMicroBit:(NSString *)microbit { + [self showNotification:@"micro:bit upload" withDescription:[NSString stringWithFormat:@"%@ uploaded to %@", file.lastPathComponent, microbit]]; +} + +- (void)uploader:(Uploader *)uploader failedToTransferFile:(NSString *)file toMicroBit:(NSString *)microbit { + [self showNotification:@"micro:bit upload failed" withDescription:[NSString stringWithFormat:@"Couldn't transfer %@ to %@", file.lastPathComponent, microbit]]; +} + +- (void)showNotification:(NSString*)title withDescription:(NSString*)description { + NSUserNotification * notification = [NSUserNotification new]; + notification.title = title; + notification.informativeText = description; + notification.soundName = NSUserNotificationDefaultSoundName; + [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; +} + +#pragma mark - NSUserNotificationCenterDelegate + +- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification { + return YES; +} + +#pragma mark - Volume mount/unmount notification + +- (void)configureVolumeMountNotifications { + [[NSWorkspace sharedWorkspace].notificationCenter addObserver:self selector:@selector(volumeMountNotification:) name:NSWorkspaceDidRenameVolumeNotification object:nil]; + [[NSWorkspace sharedWorkspace].notificationCenter addObserver:self selector:@selector(volumeMountNotification:) name:NSWorkspaceDidMountNotification object:nil]; + [[NSWorkspace sharedWorkspace].notificationCenter addObserver:self selector:@selector(volumeMountNotification:) name:NSWorkspaceDidUnmountNotification object:nil]; +} + +- (void)volumeMountNotification:(NSNotification*)sender { + //Delay upadting the menu to give the chance for the disk to fully mount or unmount + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self showActiveMicroBits]; + }); +} + +#pragma mark - Menu bar app + +- (void)createMenuBarIcon { + self.menubarItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; + self.menubarItem.button.image = [NSImage imageNamed:@"menubar"]; +} + +- (void)showActiveMicroBits { + NSMenu * menu = [NSMenu new]; + NSString * countString; + NSUInteger count = self.uploader.microBitPaths.count; + if (count == 0) { + countString = @"No connect micro:bits"; + } + else if (count == 1) { + countString = @"1 connected micro:bit"; + } + else { + countString = [NSString stringWithFormat:@"%lu connected micro:bits", count]; + } + NSMenuItem * microBitCount = [[NSMenuItem alloc] initWithTitle:countString action:nil keyEquivalent:@""]; + microBitCount.enabled = NO; + [menu addItem:microBitCount]; + + NSMenuItem * quitItem = [[NSMenuItem alloc] initWithTitle:@"Quit" action:@selector(terminate:) keyEquivalent:@"q"]; + [menu addItem:quitItem]; + + self.menubarItem.menu = menu; +} + +@end diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/Contents.json b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..7cd4f8e1 --- /dev/null +++ b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "icon_16x16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "icon_16x16@2x.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "icon_32x32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "icon_32x32@2x.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "icon_128x128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "icon_128x128@2x.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "icon_256x256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "icon_256x256@2x.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "icon_512x512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "icon_512x512@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 00000000..7f2e9fb6 Binary files /dev/null and b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 00000000..f89a95d3 Binary files /dev/null and b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 00000000..b64a612b Binary files /dev/null and b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 00000000..5fa386ce Binary files /dev/null and b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 00000000..f89a95d3 Binary files /dev/null and b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 00000000..dcad3bc9 Binary files /dev/null and b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 00000000..5fa386ce Binary files /dev/null and b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 00000000..3e0a5d82 Binary files /dev/null and b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 00000000..dcad3bc9 Binary files /dev/null and b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 00000000..67ceb3db Binary files /dev/null and b/clients/macuploader/Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/Contents.json b/clients/macuploader/Microbit Uploader/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/clients/macuploader/Microbit Uploader/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/menubar.imageset/Contents.json b/clients/macuploader/Microbit Uploader/Assets.xcassets/menubar.imageset/Contents.json new file mode 100644 index 00000000..8b5725fa --- /dev/null +++ b/clients/macuploader/Microbit Uploader/Assets.xcassets/menubar.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "mac", + "filename" : "menubar.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/clients/macuploader/Microbit Uploader/Assets.xcassets/menubar.imageset/menubar.pdf b/clients/macuploader/Microbit Uploader/Assets.xcassets/menubar.imageset/menubar.pdf new file mode 100644 index 00000000..0cc7edc1 Binary files /dev/null and b/clients/macuploader/Microbit Uploader/Assets.xcassets/menubar.imageset/menubar.pdf differ diff --git a/clients/macuploader/Microbit Uploader/Base.lproj/MainMenu.xib b/clients/macuploader/Microbit Uploader/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..8b34a85a --- /dev/null +++ b/clients/macuploader/Microbit Uploader/Base.lproj/MainMenu.xib @@ -0,0 +1,681 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clients/macuploader/Microbit Uploader/DirectoryWatcher.h b/clients/macuploader/Microbit Uploader/DirectoryWatcher.h new file mode 100644 index 00000000..ed346353 --- /dev/null +++ b/clients/macuploader/Microbit Uploader/DirectoryWatcher.h @@ -0,0 +1,24 @@ +#import + +@class DirectoryWatcher; + +@protocol DirectoryWatcherDelegate + +- (void)watcher:(DirectoryWatcher*)watcher observedNewFileAtPath:(NSString*)path; + +@end + +@interface DirectoryWatcher : NSObject + +- (instancetype)initWithPath:(NSString*)path; + +@property (readonly) NSString * path; + +@property id delegate; + +- (void)startWatching; + +//Automatically called when deallocated +- (void)stopWatching; + +@end diff --git a/clients/macuploader/Microbit Uploader/DirectoryWatcher.m b/clients/macuploader/Microbit Uploader/DirectoryWatcher.m new file mode 100644 index 00000000..66290d67 --- /dev/null +++ b/clients/macuploader/Microbit Uploader/DirectoryWatcher.m @@ -0,0 +1,73 @@ +#import "DirectoryWatcher.h" +#import + +void callback(ConstFSEventStreamRef streamRef, void * info, size_t numEvents, void * eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]); + +@interface DirectoryWatcher () + +@property NSString * path; +@property NSMutableSet* knownFiles; +@property FSEventStreamRef stream; + +- (void)rescanPathWithEvents:(BOOL)sendEvents; + +@end + +@implementation DirectoryWatcher + +- (instancetype)initWithPath:(NSString *)path { + if (!path) { + return nil; + } + + self = [super init]; + if (self) { + self.path = path; + } + return self; +} + +- (void)dealloc { + [self stopWatching]; +} + +- (void)startWatching { + self.knownFiles = [NSMutableSet new]; + [self rescanPathWithEvents:NO]; + + CFStringRef path = (__bridge CFStringRef)(self.path); + CFArrayRef pathsToWatch = CFArrayCreate(NULL, (const void**)&path, 1, NULL); + CFAbsoluteTime latency = 1; + FSEventStreamContext context = { 0, (__bridge void * _Nullable)(self), NULL, NULL, NULL }; + self.stream = FSEventStreamCreate(NULL, &callback, &context, pathsToWatch, kFSEventStreamEventIdSinceNow, latency, kFSEventStreamCreateFlagNone); + FSEventStreamScheduleWithRunLoop(self.stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + FSEventStreamStart(self.stream); +} + +- (void)stopWatching { + if (self.stream) { + FSEventStreamStop(self.stream); + FSEventStreamInvalidate(self.stream); + FSEventStreamRelease(self.stream); + self.stream = nil; + } +} + +- (void)rescanPathWithEvents:(BOOL)sendEvents { + NSArray* downloadFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.path error:nil]; + for (NSString * file in downloadFiles) { + if (![self.knownFiles containsObject:file]) { + if (sendEvents) { + [self.delegate watcher:self observedNewFileAtPath:file]; + } + [self.knownFiles addObject:file]; + } + } +} + +@end + +void callback(ConstFSEventStreamRef streamRef, void * info, size_t numEvents, void * eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]) { + DirectoryWatcher * watcher = (__bridge DirectoryWatcher*)info; + [watcher rescanPathWithEvents:YES]; +} diff --git a/clients/macuploader/Microbit Uploader/Info.plist b/clients/macuploader/Microbit Uploader/Info.plist new file mode 100644 index 00000000..9502495d --- /dev/null +++ b/clients/macuploader/Microbit Uploader/Info.plist @@ -0,0 +1,36 @@ + + + + + LSUIElement + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2016 Thomas Denney. All rights reserved. + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/clients/macuploader/Microbit Uploader/Uploader.h b/clients/macuploader/Microbit Uploader/Uploader.h new file mode 100644 index 00000000..8c3557ac --- /dev/null +++ b/clients/macuploader/Microbit Uploader/Uploader.h @@ -0,0 +1,21 @@ +#import + +@class Uploader; + +@protocol UploaderDelegate + +- (void)uploader:(Uploader*)uploader transferredFile:(NSString*)file toMicroBit:(NSString*)microbit; +- (void)uploader:(Uploader*)uploader failedToTransferFile:(NSString*)file toMicroBit:(NSString*)microbit; + +@end + +@interface Uploader : NSObject + +@property id delegate; + +- (BOOL)shouldUploadFileAtPath:(NSString*)path; +- (NSArray*)microBitPaths; +- (void)uploadFile:(NSString*)file; +- (void)uploadFile:(NSString*)file toMicroBit:(NSString*)path; + +@end diff --git a/clients/macuploader/Microbit Uploader/Uploader.m b/clients/macuploader/Microbit Uploader/Uploader.m new file mode 100644 index 00000000..e9c30c1b --- /dev/null +++ b/clients/macuploader/Microbit Uploader/Uploader.m @@ -0,0 +1,74 @@ +#import "Uploader.h" + +@interface Uploader () + +@property NSOperationQueue * backgroundCopyQueue; + +@end + +@implementation Uploader + +- (instancetype)init { + self = [super init]; + if (self) { + self.backgroundCopyQueue = [NSOperationQueue new]; + } + return self; +} + +- (BOOL)shouldUploadFileAtPath:(NSString *)path { + //Whilst Safari is downloading the file it appends .download to the name + NSRegularExpression * ignoreDownload = [NSRegularExpression regularExpressionWithPattern:@".download$" options:NSRegularExpressionCaseInsensitive error:nil]; + if ([ignoreDownload numberOfMatchesInString:path.lastPathComponent options:0 range:NSMakeRange(0, path.lastPathComponent.length)] > 0) { + return NO; + } + + //Chrome and Firefox create .hex files + NSRegularExpression * hexFiles = [NSRegularExpression regularExpressionWithPattern:@".hex$" options:NSRegularExpressionCaseInsensitive error:nil]; + if ([hexFiles numberOfMatchesInString:path.lastPathComponent options:0 range:NSMakeRange(0, path.lastPathComponent.length)] > 0) { + return YES; + } + + //Safari tends to just name files 'Unknown X' + NSRegularExpression * unknownFiles = [NSRegularExpression regularExpressionWithPattern:@"^Unknown(( |-)[0-9]+)?" options:NSRegularExpressionCaseInsensitive error:nil]; + if ([unknownFiles numberOfMatchesInString:path.lastPathComponent options:0 range:NSMakeRange(0, path.lastPathComponent.length)]) { + return YES; + } + + return NO; +} + +- (NSArray*)microBitPaths { + NSArray* allVolumes = [[NSFileManager defaultManager] mountedVolumeURLsIncludingResourceValuesForKeys:nil options:NSVolumeEnumerationSkipHiddenVolumes]; + NSMutableArray* microbitPaths = [NSMutableArray new]; + NSRegularExpression * microbitRegex = [NSRegularExpression regularExpressionWithPattern:@"^MICROBIT" options:NSRegularExpressionCaseInsensitive error:nil]; + for (NSURL * volume in allVolumes) { + NSString * lastPathComponent = volume.lastPathComponent; + if ([microbitRegex numberOfMatchesInString:lastPathComponent options:0 range:NSMakeRange(0, lastPathComponent.length)] > 0) { + [microbitPaths addObject:volume.path]; + } + } + + return microbitPaths; +} + +- (void)uploadFile:(NSString *)file { + for (NSString * microbit in [self microBitPaths]) { + [self uploadFile:file toMicroBit:microbit]; + } +} + +- (void)uploadFile:(NSString *)file toMicroBit:(NSString *)path { + [self.backgroundCopyQueue addOperationWithBlock:^{ + NSError * copyError; + NSString * destination = [path stringByAppendingPathComponent:file.lastPathComponent]; + if (![[NSFileManager defaultManager] copyItemAtPath:file toPath:destination error:©Error]) { + [self.delegate uploader:self failedToTransferFile:file toMicroBit:path.lastPathComponent]; + } + else { + [self.delegate uploader:self transferredFile:file toMicroBit:path.lastPathComponent]; + } + }]; +} + +@end diff --git a/clients/macuploader/Microbit Uploader/main.m b/clients/macuploader/Microbit Uploader/main.m new file mode 100644 index 00000000..8a6799b4 --- /dev/null +++ b/clients/macuploader/Microbit Uploader/main.m @@ -0,0 +1,5 @@ +#import + +int main(int argc, const char * argv[]) { + return NSApplicationMain(argc, argv); +} diff --git a/clients/macuploader/README.md b/clients/macuploader/README.md new file mode 100644 index 00000000..732d87fd --- /dev/null +++ b/clients/macuploader/README.md @@ -0,0 +1,40 @@ +# micro:bit uploader for OS X + +![](Microbit Uploader/Assets.xcassets/AppIcon.appiconset/icon_256x256.png) + +This project is a clone of the [Windows +uploader](https://codethemicrobit.com/uploader), but for OS X. Once launched, +the app runs in your menu bar and will automatically deploy any HEX files to +your `micro:bit`. Like the Windows version, it is compatible with any browser +that can run [codethemicrobit.com](http://codethemicrobit.com). + +## Install the built version + +1. Download the latest `.zip` release from the `Release` directory +2. Unzip it +3. Drag `Microbit Uploader` to your Applications folder and launch it + +## Building + +To build the project you'll need a copy of OS X 10.11 or higher and Xcode 8 or +higher (you may be able to build on earlier OSes or versions of Xcode, but this +remains untested). Once you have a development environment set up, just build +and run `Microbit Uploader.xcodeproj`. + +## Distributing + +1. Open the Xcode project +2. Product > Archive +3. Export: + + ![Export](Graphics/export.png) + +4. You will then have the option of either signing or not-signing the + application: + + a) If you have an Apple developer account, select 'Export a Developer + ID-signed Application' + + b) If you don't have a developer ID, select 'Export as a macOS App' + +5. Zip the produced app and upload to CDN or equivalent diff --git a/clients/macuploader/Release/Microbit Uploader v1.0.zip b/clients/macuploader/Release/Microbit Uploader v1.0.zip new file mode 100644 index 00000000..d226cb83 Binary files /dev/null and b/clients/macuploader/Release/Microbit Uploader v1.0.zip differ diff --git a/pxtarget.json b/pxtarget.json index f7d9bd73..3fee1f0d 100644 --- a/pxtarget.json +++ b/pxtarget.json @@ -266,61 +266,61 @@ "name": "connection", "os": "*", "browser": "*", - "path": "/static/mb/device/usb-generic.jpg" + "path": "/doccdn/static/mb/device/usb-generic.jpg" }, { "name": "connection", "os": "mac", "browser": "*", - "path": "/static/mb/device/usb-mac.jpg" + "path": "/doccdn/static/mb/device/usb-mac.jpg" }, { "name": "save", "os": "windows", "browser": "firefox", - "path": "/static/mb/device/usb-windows-firefox-1.png" + "path": "/doccdn/static/mb/device/usb-windows-firefox-1.png" }, { "name": "save", "os": "mac", "browser": "firefox", - "path": "/static/mb/device/usb-osx-firefox-1.png" + "path": "/doccdn/static/mb/device/usb-osx-firefox-1.png" }, { "name": "save", "os": "mac", "browser": "chrome", - "path": "/static/mb/device/usb-osx-chrome.png" + "path": "/doccdn/static/mb/device/usb-osx-chrome.png" }, { "name": "save", "os": "windows", "browser": "edge", - "path": "/static/mb/device/usb-windows-edge-1.png" + "path": "/doccdn/static/mb/device/usb-windows-edge-1.png" }, { "name": "save", "os": "windows", "browser": "ie", - "path": "/static/mb/device/usb-windows-ie11-1.png" + "path": "/doccdn/static/mb/device/usb-windows-ie11-1.png" }, { "name": "save", "os": "windows", "browser": "chrome", - "path": "/static/mb/device/usb-windows-chrome.png" + "path": "/doccdn/static/mb/device/usb-windows-chrome.png" }, { "name": "copy", "os": "mac", "browser": "*", - "path": "/static/mb/device/usb-osx-dnd.png" + "path": "/doccdn/static/mb/device/usb-osx-dnd.png" }, { "name": "copy", "os": "windows", "browser": "*", - "path": "/static/mb/device/usb-windows-sendto.jpg" + "path": "/doccdn/static/mb/device/usb-windows-sendto.jpg" } ] },