├── Imaginex ├── Media │ ├── AppIcon.png │ ├── deleted.png │ ├── spinner.gif │ ├── AppIcon16.png │ ├── AppIcon32.png │ ├── AppIcon128.png │ ├── AppIcon256.png │ ├── AppIcon512.png │ ├── icon_reload32.png │ ├── icon_reload48.png │ ├── icon_zoomin32.png │ ├── icon_zoomin48.png │ ├── icon_zoomout32.png │ ├── icon_zoomout48.png │ ├── icon_openfolder32.png │ ├── icon_openfolder48.png │ ├── icon_zoomtofit32.png │ ├── icon_zoomtofit48.png │ ├── icon_setwallpaper32.png │ └── icon_setwallpaper48.png ├── Assets.xcassets │ ├── Contents.json │ ├── Spinner.imageset │ │ ├── swift.jpg │ │ └── Contents.json │ ├── deleted.imageset │ │ ├── deleted.png │ │ └── Contents.json │ └── AppIcon.appiconset │ │ ├── AppIcon128.png │ │ ├── AppIcon16.png │ │ ├── AppIcon256.png │ │ ├── AppIcon32.png │ │ ├── AppIcon512.png │ │ └── Contents.json ├── settings.txt ├── DataUtils.swift ├── AppDelegate.swift ├── ColorUtils.swift ├── Gallery.swift ├── Spinner.swift ├── ViewThumbnail.swift ├── DateUtils.swift ├── Info.plist ├── UIUtils.swift ├── TODO.txt ├── StringUtils.swift ├── ViewThumbnail.xib ├── GalleryController.swift ├── SerialFetcher.swift ├── Settings.swift ├── FileUtils.swift ├── ViewController.swift └── Base.lproj │ └── Main.storyboard ├── Imaginex.xcodeproj ├── xcuserdata │ └── mini.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ ├── xcschememanagement.plist │ │ └── Imaginex.xcscheme ├── project.xcworkspace │ ├── xcuserdata │ │ └── mini.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── contents.xcworkspacedata └── project.pbxproj ├── README.md └── TODO.txt /Imaginex/Media/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/AppIcon.png -------------------------------------------------------------------------------- /Imaginex/Media/deleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/deleted.png -------------------------------------------------------------------------------- /Imaginex/Media/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/spinner.gif -------------------------------------------------------------------------------- /Imaginex/Media/AppIcon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/AppIcon16.png -------------------------------------------------------------------------------- /Imaginex/Media/AppIcon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/AppIcon32.png -------------------------------------------------------------------------------- /Imaginex/Media/AppIcon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/AppIcon128.png -------------------------------------------------------------------------------- /Imaginex/Media/AppIcon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/AppIcon256.png -------------------------------------------------------------------------------- /Imaginex/Media/AppIcon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/AppIcon512.png -------------------------------------------------------------------------------- /Imaginex/Media/icon_reload32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/icon_reload32.png -------------------------------------------------------------------------------- /Imaginex/Media/icon_reload48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/icon_reload48.png -------------------------------------------------------------------------------- /Imaginex/Media/icon_zoomin32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/icon_zoomin32.png -------------------------------------------------------------------------------- /Imaginex/Media/icon_zoomin48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/icon_zoomin48.png -------------------------------------------------------------------------------- /Imaginex/Media/icon_zoomout32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/icon_zoomout32.png -------------------------------------------------------------------------------- /Imaginex/Media/icon_zoomout48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/icon_zoomout48.png -------------------------------------------------------------------------------- /Imaginex/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Imaginex/Media/icon_openfolder32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/icon_openfolder32.png -------------------------------------------------------------------------------- /Imaginex/Media/icon_openfolder48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/icon_openfolder48.png -------------------------------------------------------------------------------- /Imaginex/Media/icon_zoomtofit32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/icon_zoomtofit32.png -------------------------------------------------------------------------------- /Imaginex/Media/icon_zoomtofit48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/icon_zoomtofit48.png -------------------------------------------------------------------------------- /Imaginex/Media/icon_setwallpaper32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/icon_setwallpaper32.png -------------------------------------------------------------------------------- /Imaginex/Media/icon_setwallpaper48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Media/icon_setwallpaper48.png -------------------------------------------------------------------------------- /Imaginex/Assets.xcassets/Spinner.imageset/swift.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Assets.xcassets/Spinner.imageset/swift.jpg -------------------------------------------------------------------------------- /Imaginex/Assets.xcassets/deleted.imageset/deleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Assets.xcassets/deleted.imageset/deleted.png -------------------------------------------------------------------------------- /Imaginex/Assets.xcassets/AppIcon.appiconset/AppIcon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Assets.xcassets/AppIcon.appiconset/AppIcon128.png -------------------------------------------------------------------------------- /Imaginex/Assets.xcassets/AppIcon.appiconset/AppIcon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Assets.xcassets/AppIcon.appiconset/AppIcon16.png -------------------------------------------------------------------------------- /Imaginex/Assets.xcassets/AppIcon.appiconset/AppIcon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Assets.xcassets/AppIcon.appiconset/AppIcon256.png -------------------------------------------------------------------------------- /Imaginex/Assets.xcassets/AppIcon.appiconset/AppIcon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Assets.xcassets/AppIcon.appiconset/AppIcon32.png -------------------------------------------------------------------------------- /Imaginex/Assets.xcassets/AppIcon.appiconset/AppIcon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex/Assets.xcassets/AppIcon.appiconset/AppIcon512.png -------------------------------------------------------------------------------- /Imaginex.xcodeproj/xcuserdata/mini.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /Imaginex.xcodeproj/project.xcworkspace/xcuserdata/mini.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuyawa/Imaginex/HEAD/Imaginex.xcodeproj/project.xcworkspace/xcuserdata/mini.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Imaginex/Assets.xcassets/Spinner.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "swift.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Imaginex/Assets.xcassets/deleted.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "deleted.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Imaginex.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 10 | 11 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Imaginex/settings.txt: -------------------------------------------------------------------------------- 1 | { 2 | "version":"1.0", 3 | "galleries":[ 4 | { 5 | "name" : "Latest", 6 | "url" : "http://imgur.com/gallery", 7 | "image" : "nopic.jpg" 8 | }, 9 | { 10 | "name" : "Funny", 11 | "url" : "http://imgur.com/r/funny", 12 | "image" : "nopic.jpg" 13 | }, 14 | { 15 | "name" : "EarthPorn", 16 | "url" : "http://imgur.com/r/earthporn", 17 | "image" : "nopic.jpg" 18 | }, 19 | { 20 | "name" : "Space", 21 | "url" : "http://imgur.com/r/space", 22 | "image" : "nopic.jpg" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Imaginex 2 | 3 | **Imaginex** is an image browser for the desktop developed in Swift. Select your favorite galleries from imgur.com and download all the images from the web to your desktop for better consumption. You can then zoom in or out, explore the gallery folder or set an image as a wallpaper for your desktop. 4 | 5 | Here is a screenshot of the application: 6 | 7 | ![Imaginex Screenshot](http://i.imgur.com/HiC07Hx.jpg) 8 | 9 | It comes predefined with four galleries like Latest, Funny, EarthPorn and Space, but you can add or remove as many galleries as you want. 10 | 11 | All comments and suggestions for improvement are welcome. -------------------------------------------------------------------------------- /Imaginex.xcodeproj/xcuserdata/mini.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Imaginex.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 3DCA84631DAE8B93001AB31E 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Imaginex/DataUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataUtils.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 10/24/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Dictionary { 12 | func toJson() -> String { 13 | let invalidJson = "{\"error\":\"Invalid JSON\"}" 14 | do { 15 | let json = try JSONSerialization.data(withJSONObject: self, options: .prettyPrinted) 16 | return String(data: json, encoding: String.Encoding.utf8) ?? invalidJson 17 | } catch let error as NSError { 18 | print(error) 19 | return invalidJson 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Imaginex/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 10/12/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | func applicationDidFinishLaunching(_ aNotification: Notification) { 15 | print("Hello!") 16 | } 17 | 18 | func applicationWillTerminate(_ aNotification: Notification) { 19 | // Insert code here to tear down your application 20 | } 21 | 22 | // Add this handler to all apps, close on red button click 23 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 24 | print("Goodbye!") 25 | return true 26 | } 27 | 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Imaginex/ColorUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorUtils.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 12/5/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | 12 | // let red = NSColor(hex:0xff0000) 13 | extension NSColor { 14 | convenience init(hex: Int) { 15 | var opacity : CGFloat = 1.0 16 | 17 | if hex > 0xffffff { 18 | opacity = CGFloat((hex >> 24) & 0xff) / 255 19 | } 20 | 21 | let parts = ( 22 | R: CGFloat((hex >> 16) & 0xff) / 255, 23 | G: CGFloat((hex >> 08) & 0xff) / 255, 24 | B: CGFloat((hex >> 00) & 0xff) / 255, 25 | A: opacity 26 | ) 27 | 28 | self.init(red: parts.R, green: parts.G, blue: parts.B, alpha: parts.A) 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Imaginex/Gallery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Gallery.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 10/18/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Gallery { 12 | var name : String = "" 13 | var url : String = "" 14 | var image : String = "" 15 | var items : [GalleryItem] = [GalleryItem]() 16 | } 17 | 18 | class GalleryItem { 19 | var title :String = "" 20 | var link :String = "" 21 | var desc :String = "" 22 | var imageUrl :String = "" 23 | var imageType :String = "" 24 | var imageHeight :String = "" 25 | var imageWidth :String = "" 26 | var imageSize :String = "" 27 | var imageName :String = "" 28 | var imagePath :String = "" 29 | var thumbUrl :String = "" 30 | var thumbName :String = "" 31 | var thumbPath :String = "" 32 | } 33 | -------------------------------------------------------------------------------- /Imaginex/Spinner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Spinner.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 10/20/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | 12 | class Spinner { 13 | var X :Int = 100 14 | var Y :Int = 100 15 | var width :Int = 32 16 | var height :Int = 32 17 | var view :NSView 18 | var control :NSProgressIndicator 19 | 20 | init(with view: NSView) { 21 | self.view = view 22 | self.control = NSProgressIndicator() 23 | control.isIndeterminate = true 24 | control.style = NSProgressIndicatorStyle.spinningStyle 25 | } 26 | 27 | func show(){ 28 | control.frame = NSRect(x: self.X, y: self.Y, width: self.width, height: self.height) 29 | view.addSubview(control) 30 | } 31 | 32 | func hide() { 33 | view.willRemoveSubview(control) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Imaginex/ViewThumbnail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewThumbnail.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 10/15/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ViewThumbnail: NSCollectionViewItem { 12 | 13 | var imageFile : NSImage? { 14 | didSet { 15 | //guard viewLoaded else { return } 16 | imageView?.image = imageFile 17 | //print("Image assigned") 18 | } 19 | } 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | view.wantsLayer = true 24 | //view.layer?.backgroundColor = CGColor.black 25 | view.layer?.borderWidth = 0.0 26 | view.layer?.borderColor = CGColor.black 27 | //print("thumb init") 28 | } 29 | 30 | func setHighlight(_ selected: Bool) { 31 | view.layer?.borderWidth = selected ? 5.0 : 0.0 32 | //view.layer?.borderColor = CGColor.black 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Imaginex/DateUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateUtils.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 10/12/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class DateUtils { 12 | static func fromString(_ text: String) -> Date { 13 | // No format? use default 14 | return fromString(text, format: "yyyy-MM-dd HH:mm:ss") 15 | } 16 | 17 | static func fromString(_ text: String, format: String) -> Date { 18 | var date = Date(timeIntervalSince1970: 0) 19 | if !text.isEmpty { 20 | let formatter = DateFormatter() 21 | formatter.dateFormat = format 22 | date = formatter.date(from: text)! 23 | } 24 | return date 25 | } 26 | } 27 | 28 | extension Date { 29 | func toString() -> String { 30 | let formatter = DateFormatter() 31 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 32 | let text = formatter.string(from: self) 33 | return text 34 | } 35 | 36 | func toString(format: String) -> String { 37 | let formatter = DateFormatter() 38 | formatter.dateFormat = format 39 | let text = formatter.string(from: self) 40 | return text 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Imaginex/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "AppIcon16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "idiom" : "mac", 11 | "size" : "16x16", 12 | "scale" : "2x" 13 | }, 14 | { 15 | "size" : "32x32", 16 | "idiom" : "mac", 17 | "filename" : "AppIcon32.png", 18 | "scale" : "1x" 19 | }, 20 | { 21 | "idiom" : "mac", 22 | "size" : "32x32", 23 | "scale" : "2x" 24 | }, 25 | { 26 | "size" : "128x128", 27 | "idiom" : "mac", 28 | "filename" : "AppIcon128.png", 29 | "scale" : "1x" 30 | }, 31 | { 32 | "idiom" : "mac", 33 | "size" : "128x128", 34 | "scale" : "2x" 35 | }, 36 | { 37 | "size" : "256x256", 38 | "idiom" : "mac", 39 | "filename" : "AppIcon256.png", 40 | "scale" : "1x" 41 | }, 42 | { 43 | "idiom" : "mac", 44 | "size" : "256x256", 45 | "scale" : "2x" 46 | }, 47 | { 48 | "size" : "512x512", 49 | "idiom" : "mac", 50 | "filename" : "AppIcon512.png", 51 | "scale" : "1x" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "size" : "512x512", 56 | "scale" : "2x" 57 | } 58 | ], 59 | "info" : { 60 | "version" : 1, 61 | "author" : "xcode" 62 | } 63 | } -------------------------------------------------------------------------------- /Imaginex/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSApplicationCategoryType 24 | public.app-category.photography 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSExceptionDomains 32 | 33 | imgur.com 34 | 35 | 36 | 37 | NSHumanReadableCopyright 38 | Copyright © 2016 Armonia. All rights reserved. 39 | NSMainStoryboardFile 40 | Main 41 | NSPrincipalClass 42 | NSApplication 43 | 44 | 45 | -------------------------------------------------------------------------------- /Imaginex/UIUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIUtils.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 10/18/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | 12 | 13 | /* Use: 14 | AlertOK("Everything is OK").show() 15 | AlertOK(title:"Warning", info:"Something went wrong").show() 16 | */ 17 | class AlertOK { 18 | var title :String = "Warning" 19 | var info :String = "Something went wrong" 20 | 21 | init(_ info: String){ 22 | self.title = "Alert" 23 | self.info = info 24 | } 25 | 26 | init(title: String, info: String){ 27 | self.title = title 28 | self.info = info 29 | } 30 | 31 | func show() { 32 | let alert = NSAlert() 33 | alert.messageText = title 34 | alert.informativeText = info 35 | alert.addButton(withTitle: "OK") 36 | alert.runModal() 37 | } 38 | } 39 | 40 | class DialogYesNo { 41 | var title :String = "Choice" 42 | var info :String = "Would you like to proceed?" 43 | 44 | init(_ info: String){ 45 | self.title = "Choice" 46 | self.info = info 47 | } 48 | 49 | init(title: String, info: String){ 50 | self.title = title 51 | self.info = info 52 | } 53 | 54 | func choice() -> Bool{ 55 | var ok = false 56 | let alert = NSAlert() 57 | alert.messageText = title 58 | alert.informativeText = info 59 | alert.addButton(withTitle: "NO") 60 | alert.addButton(withTitle: "YES") 61 | ok = (alert.runModal() == NSAlertSecondButtonReturn ) 62 | return ok 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | FIXME: 2 | 3 | - Uncommited transacions in thread? 4 | CoreAnimation: warning, deleted thread with uncommitted CATransaction; set CA_DEBUG_TRANSACTIONS=1 in environment to log backtraces. 5 | 6 | TODO: 7 | 8 | - Change app icon to blue lens 9 | - Gallery form in dark, show as modal slide 10 | - On add gallery form, disable buttons if info not valid 11 | if fields are empty 12 | if gallery already exists 13 | if gallery url can not be fetched 14 | ? Use notifications to update UI 15 | ? Remember last gallery for next run 16 | ? reorganize code from viewcontroller to classes, 17 | ? zoom in on double click. For next version 18 | 19 | ------------------------------------------------------------------------------------- 20 | 21 | x add autoDownload item to Images menu. On: download on select. Off: do not download. Useful to delete ugly images 22 | x delete image on DEL key from gallery and thumbs 23 | x onDelete: rename thumb to _qwerty.jpg, delete main image, use deleted.jpg as main image for visual cue 24 | x onList: do not download again, check for _image.jpg as deleted 25 | x read 100 thumbs from folder 26 | x Get images from thumbs folder ordered by created time 27 | x On gallery refresh, get html from url, parse regex for thumbs, download all thumbs, show first thumb 28 | 29 | ------------------------------------------------------------------------------------- 30 | 31 | Version 2: 32 | 33 | - App will fetch image list from server, asking for images since last visit 34 | - Users will be able to recommend feeds 35 | - Server will return list of images and client will download thumbs and images on demand 36 | - server.com/api/gallery/earthporn/2016-11-20/08:30:55 37 | List: imgname, imgtitle, imgurl, thumb, thumburl 38 | 39 | Version for tvOS: 40 | - Huge pics! 41 | -------------------------------------------------------------------------------- /Imaginex/TODO.txt: -------------------------------------------------------------------------------- 1 | FIXME: 2 | 3 | - Uncommited transacions in thread? 4 | CoreAnimation: warning, deleted thread with uncommitted CATransaction; set CA_DEBUG_TRANSACTIONS=1 in environment to log backtraces. 5 | 6 | TODO: 7 | 8 | - Change app icon to blue lens 9 | - Gallery form in dark, show as modal slide 10 | - On add gallery form, disable buttons if info not valid 11 | if fields are empty 12 | if gallery already exists 13 | if gallery url can not be fetched 14 | ? Use notifications to update UI 15 | ? Remember last gallery for next run 16 | ? reorganize code from viewcontroller to classes, 17 | ? zoom in on double click. For next version 18 | 19 | ------------------------------------------------------------------------------------- 20 | 21 | x add autoDownload item to Images menu. On: download on select. Off: do not download. Useful to delete ugly images 22 | x delete image on DEL key from gallery and thumbs 23 | x onDelete: rename thumb to _qwerty.jpg, delete main image, use deleted.jpg as main image for visual cue 24 | x onList: do not download again, check for _image.jpg as deleted 25 | x read 100 thumbs from folder 26 | x Get images from thumbs folder ordered by created time 27 | x On gallery refresh, get html from url, parse regex for thumbs, download all thumbs, show first thumb 28 | 29 | ------------------------------------------------------------------------------------- 30 | 31 | Version 2: 32 | 33 | - App will fetch image list from server, asking for images since last visit 34 | - Users will be able to recommend feeds 35 | - Server will return list of images and client will download thumbs and images on demand 36 | - server.com/api/gallery/earthporn/2016-11-20/08:30:55 37 | List: imgname, imgtitle, imgurl, thumb, thumburl 38 | 39 | Version for tvOS: 40 | - Huge pics! 41 | -------------------------------------------------------------------------------- /Imaginex/StringUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringUtils.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 11/23/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | 13 | func matchFirst(_ pattern: String) -> String { 14 | let first = self.range(of: pattern, options: .regularExpression)! 15 | let match = self.substring(with: first) 16 | 17 | return match 18 | } 19 | 20 | func matchAll(_ pattern: String) -> [String] { 21 | let all = NSRange(location: 0, length: self.characters.count) 22 | var matches = [String]() 23 | 24 | do { 25 | let regex = try NSRegularExpression(pattern: pattern, options: []) 26 | let results = regex.matches(in: self, options: [], range: all) 27 | 28 | for item in results { 29 | let first = item.rangeAt(1) 30 | let range = self.rangeIndex(first) 31 | let match = self.substring(with: range) 32 | 33 | matches.append(match) 34 | } 35 | } catch { 36 | print(error) 37 | } 38 | 39 | return matches 40 | } 41 | 42 | // Painful conversion from a Range to a Range 43 | func rangeIndex(_ range: NSRange) -> Range { 44 | let index1 = self.utf16.index(self.utf16.startIndex, offsetBy: range.location, limitedBy: self.utf16.endIndex) 45 | let index2 = self.utf16.index(index1!, offsetBy: range.length, limitedBy: self.utf16.endIndex) 46 | let bound1 = String.Index(index1!, within: self)! 47 | let bound2 = String.Index(index2!, within: self)! 48 | let result = Range(uncheckedBounds: (bound1, bound2)) 49 | 50 | return result 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Imaginex/ViewThumbnail.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Imaginex/GalleryController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GalleryController.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 10/23/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | class GalleryController: NSViewController { 13 | 14 | enum responseType { 15 | case saved, cancelled 16 | } 17 | 18 | var gallery :Gallery = Gallery() 19 | var settings :Settings = Settings() 20 | var response :responseType = .cancelled 21 | 22 | @IBOutlet weak var textName: NSTextField! 23 | @IBOutlet weak var textURL : NSTextField! 24 | 25 | @IBAction func buttonAddGallery(_ sender: NSButton) { 26 | addGallery() 27 | } 28 | 29 | @IBAction func buttonCancel(_ sender: NSButton) { 30 | cancelEvent() 31 | } 32 | 33 | 34 | //-- View delegates 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | settings.load() 39 | } 40 | 41 | override func viewDidAppear() { 42 | if let window = self.view.window { 43 | window.delegate = self 44 | window.styleMask.remove(.resizable) // Non resizeable 45 | window.styleMask.remove(.fullScreen) // Non maximizeable 46 | window.styleMask.remove(.miniaturizable) // Non minimizeable 47 | } 48 | } 49 | 50 | override func viewWillDisappear() { 51 | // Return values to caller? 52 | } 53 | 54 | 55 | //-- User events 56 | 57 | func addGallery() { 58 | 59 | // get data from fields 60 | let name = textName.stringValue 61 | let url = textURL.stringValue 62 | 63 | // validate fields 64 | if name.isEmpty { 65 | AlertOK("Gallery name can not be empty").show() 66 | return 67 | } 68 | 69 | if url.isEmpty { 70 | AlertOK("Gallery URL can not be empty").show() 71 | return 72 | } 73 | 74 | // add gallery to settings 75 | gallery.name = name 76 | gallery.url = url 77 | gallery.image = "nopic.jpg" 78 | let ok = settings.addGallery(gallery) // and save 79 | if ok { 80 | self.response = .saved 81 | windowRelease() 82 | } else { 83 | print("Gallery can not be saved. Try again") 84 | cancelEvent() 85 | } 86 | } 87 | 88 | func cancelEvent() { 89 | self.response = .cancelled 90 | windowRelease() 91 | } 92 | 93 | func windowRelease() { 94 | self.view.window?.close() 95 | } 96 | //-- End user events 97 | } 98 | 99 | 100 | extension GalleryController : NSWindowDelegate { 101 | func windowShouldClose(_ sender: Any) -> Bool { 102 | // This method is called form the red button in the window bar 103 | // Return false to avoid closing it 104 | return true 105 | } 106 | 107 | func windowWillClose(_ notification: Notification) { 108 | // This method is called from anywhere after the window is closed 109 | let app = NSApplication.shared() 110 | app.stopModal() 111 | } 112 | } 113 | 114 | //-- END 115 | -------------------------------------------------------------------------------- /Imaginex.xcodeproj/xcuserdata/mini.xcuserdatad/xcschemes/Imaginex.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Imaginex/SerialFetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SerialFetcher.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 10/14/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol didFinishDownload { 12 | func showDownloadedImage(_ path: String) 13 | func showDownloadedThumb(_ path: String) 14 | func refreshCurrentGallery() 15 | } 16 | 17 | class ThumbFetcher { 18 | var name: String 19 | var thumbs: [String] 20 | var currentIndex: Int = 0 21 | var isReversed: Bool = false 22 | var delegate: didFinishDownload? 23 | 24 | init(gallery name: String, thumbs: [String]){ 25 | self.name = name 26 | self.thumbs = thumbs 27 | } 28 | 29 | init(gallery name: String, thumbs: [String], inReverse: Bool){ 30 | self.name = name 31 | self.thumbs = thumbs 32 | if inReverse { 33 | isReversed = true 34 | currentIndex = thumbs.count-1 // Start from last 35 | } 36 | } 37 | 38 | func next() -> String? { 39 | if isReversed { 40 | if currentIndex >= 0 && currentIndex < thumbs.count { 41 | let item = thumbs[currentIndex] 42 | currentIndex -= 1 43 | return item 44 | } 45 | return nil 46 | } else { 47 | if currentIndex < thumbs.count { 48 | let item = thumbs[currentIndex] 49 | currentIndex += 1 50 | return item 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | func loop() { 57 | guard let item = self.next() else { 58 | print("--Finished loop for thumbs") 59 | // Call UI delegate here 60 | self.delegate?.refreshCurrentGallery() 61 | return 62 | } 63 | 64 | let media = FileUtils.getThumbsFolder(gallery: name) 65 | 66 | let thumbUrl = item 67 | let thumbName = FileUtils.getImageNameFromUrl(thumbUrl) 68 | let thumbFull = media.appendingPathComponent(thumbName) 69 | let thumbPath = thumbFull.path 70 | 71 | if FileUtils.fileExists(thumbPath) { 72 | print("- Thumbnail \(thumbName) exists, not downloaded") 73 | self.loop() // Next thumb 74 | return 75 | } 76 | 77 | // Check for deleted 78 | let thumbNameX = "_"+thumbName 79 | let thumbFullX = media.appendingPathComponent(thumbNameX) 80 | let thumbPathX = thumbFullX.path 81 | 82 | if FileUtils.fileExists(thumbPathX) { 83 | print("- Image \(thumbName) has been deleted, not downloaded") 84 | self.loop() 85 | return 86 | } 87 | 88 | print("Downloading thumb: \(thumbUrl)") 89 | //print("To \(thumbPath)\n") 90 | 91 | do { 92 | // This is a serial async task, once finished will call next 93 | try FileUtils.download(fromUrl: thumbUrl, toFile: thumbPath) { location, response, error in 94 | guard error == nil else { 95 | print("Error downloading thumbnail \(thumbUrl)") 96 | self.loop() // Next thumb 97 | return 98 | } 99 | print("Download finished for thumbnail \(thumbUrl)") 100 | // TODO: Add thumbnail to collection as first item 101 | // Use some kind of event dispatcher? 102 | // Or pass collectionView to this constructor? 103 | self.delegate?.showDownloadedThumb(thumbPath) 104 | self.loop() // Next thumb 105 | } 106 | } catch { 107 | print("Unkown error") 108 | self.loop() // Next thumb 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /Imaginex/Settings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Settings.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 10/12/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Settings { 12 | var version :String = "1.0" 13 | var galleries :[Gallery] = [Gallery]() 14 | 15 | func firstTime() { 16 | let _ = FileUtils.verifyAppFolder() // If not exists, create it 17 | print("Settings file not found") 18 | // if not settings.txt in app folder, create it 19 | if let path = Bundle.main.path(forResource: "settings", ofType: "txt") { 20 | do { 21 | let text = try String(contentsOfFile: path) 22 | // Save in app folder 23 | let docs = FileUtils.getAppFolder() 24 | let file = docs.appendingPathComponent("settings.txt") 25 | FileUtils.save(name: file.path, content: text) 26 | print("Settings file created") 27 | } catch { 28 | print("Error accessing settings file") 29 | } 30 | } 31 | } 32 | 33 | func load(){ 34 | let file = FileUtils.getSettingsFileName() 35 | if !FileUtils.fileExists(file) { 36 | firstTime() // create a basic settings file 37 | } 38 | 39 | // Read file settings.txt as json 40 | var json = FileUtils.loadAsJson(file) 41 | 42 | // assign values from file 43 | if let v = json?["version"] as? String { self.version = v } 44 | if let list = json?["galleries"] as? NSArray { 45 | for item in list { 46 | if let data = item as? [String:AnyObject] { 47 | let gallery = Gallery() 48 | guard 49 | let name = data["name"] as? String, 50 | let url = data["url"] as? String, 51 | let image = data["image"] as? String 52 | else { 53 | print("No data \(type(of:data))") 54 | break 55 | } 56 | gallery.name = name 57 | gallery.url = url 58 | gallery.image = image 59 | self.galleries.append(gallery) 60 | } else { 61 | print("No item \(type(of:item))") 62 | } 63 | } 64 | } else { 65 | print("No galleries") 66 | } 67 | 68 | print("Version \(self.version)") 69 | //for item in self.galleries { 70 | // print(item.name, item.updated) 71 | //} 72 | print("-") 73 | } 74 | 75 | func save() { 76 | let path = FileUtils.getSettingsFileName() 77 | let json = self.toJson() 78 | print("Saving settings in \(path)") 79 | print(json) 80 | FileUtils.save(name: path, content: json) 81 | } 82 | 83 | func toDictionary() -> [String:Any] { 84 | var data = [String:Any]() 85 | data["version"] = self.version 86 | var items = [[String:Any]]() // Array of dicks 87 | 88 | for item in self.galleries { 89 | var gallery = [String:Any]() 90 | gallery["name"] = item.name 91 | gallery["url"] = item.url 92 | gallery["image"] = item.image 93 | items.append(gallery) 94 | } 95 | data["galleries"] = items 96 | 97 | return data 98 | } 99 | 100 | func toJson() -> String { 101 | let data = self.toDictionary() 102 | return data.toJson() // uses Dictionary extension from DataUtils 103 | } 104 | 105 | func findGalleryIndex(name: String) -> Int { 106 | var index = 0 107 | for item in galleries { 108 | if item.name.lowercased() == name.lowercased() { 109 | print("Gallery found at #\(index)") 110 | return index 111 | } 112 | index += 1 113 | } 114 | return -1 115 | } 116 | 117 | func addGallery(_ gallery :Gallery) -> Bool { 118 | // name and url are required 119 | let index = findGalleryIndex(name: gallery.name) 120 | 121 | if index >= 0 { 122 | print("Gallery already exists") 123 | AlertOK("Gallery already exists").show() 124 | return false 125 | } 126 | 127 | self.galleries.append(gallery) 128 | self.save() 129 | 130 | // create media/thumbs folder 131 | let _ = FileUtils.verifyImagesFolder(gallery: gallery.name) 132 | let _ = FileUtils.verifyThumbsFolder(gallery: gallery.name) 133 | return true 134 | } 135 | 136 | func removeGallery(name :String) { 137 | let index = findGalleryIndex(name: name) 138 | if index >= 0 { 139 | self.removeGallery(at: index) 140 | } 141 | } 142 | 143 | func removeGallery(at index :Int) { 144 | self.galleries.remove(at: index) 145 | self.save() 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Imaginex/FileUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileUtils.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 10/12/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Path { 12 | static var Documents : URL { 13 | let filer = FileManager.default 14 | let docs = filer.urls(for: .documentDirectory, in: .userDomainMask).first! 15 | return docs 16 | } 17 | } 18 | 19 | class FileUtils { 20 | 21 | static func getAppName() -> String { 22 | return Bundle.main.infoDictionary![kCFBundleNameKey as String] as! String 23 | } 24 | 25 | static func getAppFolder(asString: Bool) -> String { 26 | return getAppFolder().path 27 | } 28 | 29 | static func getAppFolder() -> URL { 30 | let filer = FileManager.default 31 | let docs = filer.urls(for: .documentDirectory, in: .userDomainMask).first! 32 | let url = docs.appendingPathComponent(getAppName(), isDirectory: true) 33 | return url 34 | } 35 | 36 | static func getMediaFolder() -> URL { 37 | let filer = FileManager.default 38 | let docs = filer.urls(for: .documentDirectory, in: .userDomainMask).first! 39 | let full = docs.appendingPathComponent(getAppName(), isDirectory: true) 40 | let url = full.appendingPathComponent("media", isDirectory: true) 41 | return url 42 | } 43 | 44 | static func getImagesFolder(gallery: String) -> URL { 45 | let filer = FileManager.default 46 | let docs = filer.urls(for: .documentDirectory, in: .userDomainMask).first! 47 | let full = docs.appendingPathComponent(getAppName(), isDirectory: true) 48 | let media = full.appendingPathComponent("media", isDirectory: true) 49 | let url = media.appendingPathComponent(gallery.lowercased(), isDirectory: true) 50 | return url 51 | } 52 | 53 | static func getImagePath(gallery name: String, forFile file: String) -> String { 54 | let folder = getImagesFolder(gallery: name.lowercased()) 55 | let path = folder.appendingPathComponent(file).path 56 | return path 57 | } 58 | 59 | static func getPathRelativeToDocs(_ path :String) -> String { 60 | // TODO: get Documents folder from OS 61 | if path.contains("/Documents") { 62 | let pos = path.range(of: "/Documents", options: [.backwards])?.lowerBound 63 | let rel = path.substring(from: pos!) 64 | return rel 65 | } 66 | return path 67 | } 68 | 69 | static func getImageNameFromUrl(_ url: String) -> String { 70 | return (url as NSString).lastPathComponent 71 | } 72 | 73 | static func getThumbsFolder(gallery: String) -> URL { 74 | let filer = FileManager.default 75 | let docs = filer.urls(for: .documentDirectory, in: .userDomainMask).first! 76 | let full = docs.appendingPathComponent(getAppName(), isDirectory: true) 77 | let media = full.appendingPathComponent("media", isDirectory: true) 78 | let pics = media.appendingPathComponent(gallery.lowercased(), isDirectory: true) 79 | let url = pics.appendingPathComponent(".thumbs", isDirectory: true) 80 | return url 81 | } 82 | 83 | static func getThumbnailPath(gallery name: String, forFile file: String) -> String { 84 | let folder = getThumbsFolder(gallery: name.lowercased()) 85 | let path = folder.appendingPathComponent(file).path 86 | return path 87 | } 88 | 89 | static func getSettingsFileName() -> String { 90 | let folder = getAppFolder() 91 | let file = folder.appendingPathComponent("settings.txt") 92 | return file.path 93 | } 94 | 95 | static func verifyAppFolder() -> Bool { 96 | let path = getAppFolder().path 97 | //print("App folder: \(path)") 98 | return verifyFolder(path) 99 | } 100 | 101 | static func verifyMediaFolder() -> Bool { 102 | let path = getMediaFolder().path 103 | //print("Media folder: \(path)") 104 | return verifyFolder(path) 105 | } 106 | 107 | static func verifyImagesFolder(gallery name: String) -> Bool { 108 | let path = getImagesFolder(gallery: name.lowercased()).path 109 | //print("Images folder for \(name): \(path)") 110 | return verifyFolder(path) 111 | } 112 | 113 | static func verifyThumbsFolder(gallery name: String) -> Bool { 114 | let path = getThumbsFolder(gallery: name.lowercased()).path 115 | //print("Thumbs folder for \(name): \(path)") 116 | return verifyFolder(path) 117 | } 118 | 119 | static func verifyFolder(_ path: String) -> Bool { 120 | do { 121 | var isDir :ObjCBool = false 122 | let filer = FileManager.default 123 | 124 | if filer.fileExists(atPath: path, isDirectory: &isDir) { 125 | if isDir.boolValue { 126 | //print("Folder exists in \(path)") 127 | return true 128 | } else { 129 | print("Exists as file. Creating as folder") 130 | } 131 | } else { 132 | print("Folder does not exist. Creating new folder in \(path)") 133 | } 134 | 135 | // Create new folder 136 | try filer.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil) 137 | 138 | } catch let error as NSError { 139 | print("Error verifying folder: \(path)") 140 | print(error) 141 | return false 142 | } 143 | 144 | return true 145 | } 146 | 147 | 148 | static func fileExists(_ name: String) -> Bool { 149 | if FileManager.default.fileExists(atPath: name) { 150 | return true 151 | } 152 | return false 153 | } 154 | /* 155 | static func fileExistsInDocs(_ name: String) -> Bool { 156 | let docs = getAppFolder() 157 | let full = docs.appendingPathComponent(name) 158 | //print("Checking in app folder \(full.path)") 159 | if FileManager.default.fileExists(atPath: full.path) { 160 | return true 161 | } 162 | return false 163 | } 164 | 165 | static func fileExistsInMedia(_ name: String) -> Bool { 166 | let docs = getMediaFolder() 167 | let full = docs.appendingPathComponent(name) 168 | //print("Checking in media folder \(full.path)") 169 | if FileManager.default.fileExists(atPath: full.path) { 170 | return true 171 | } 172 | return false 173 | } 174 | */ 175 | /* 176 | static func listFilesInMedia(_ name: String, max: Int) -> [String] { 177 | var list = [String]() 178 | let folder = getThumbsFolder(gallery: name).path 179 | do { 180 | list = try FileManager.default.contentsOfDirectory(atPath: folder) 181 | } catch { 182 | print("Error listing thumbnails") 183 | } 184 | return list 185 | } 186 | */ 187 | 188 | static func listFilesInMedia(gallery name: String, max: Int) -> [String]? { 189 | _ = verifyThumbsFolder(gallery: name) 190 | let folder = getThumbsFolder(gallery: name) 191 | let props = [URLResourceKey.localizedNameKey, URLResourceKey.creationDateKey] 192 | 193 | if let fileArray = try? FileManager.default.contentsOfDirectory(at: folder, includingPropertiesForKeys: props, options: .skipsHiddenFiles) { 194 | let results = fileArray.map { url -> (String, Date) in 195 | do { 196 | var created = try url.resourceValues(forKeys: [URLResourceKey.creationDateKey]) 197 | return (url.lastPathComponent, created.creationDate!) 198 | } catch { 199 | print(error) 200 | } 201 | return ("Error", Date()) 202 | } 203 | 204 | let ordered = results.sorted(by: { $0.1 > $1.1 }) // sort descending creation dates 205 | let names = ordered.map { $0.0 } // extract file names 206 | 207 | return names 208 | } else { 209 | return nil 210 | } 211 | } 212 | 213 | static func getFileInfo(_ name: String) -> [FileAttributeKey: Any]? { 214 | do { 215 | let info = try FileManager.default.attributesOfItem(atPath: name) 216 | return info 217 | } catch { 218 | print(error) 219 | } 220 | 221 | return nil 222 | } 223 | 224 | static func deleteFile(_ path: String) { 225 | do { 226 | if fileExists(path) { 227 | try FileManager.default.removeItem(atPath: path) 228 | } 229 | } catch { 230 | print(error) 231 | } 232 | } 233 | 234 | static func renameFile(_ path: String, to newName: String) { 235 | do { 236 | if fileExists(path) { 237 | try FileManager.default.moveItem(atPath: path, toPath: newName) 238 | } 239 | } catch { 240 | print(error) 241 | } 242 | } 243 | 244 | // Load file from Documents/App folder 245 | static func load(_ name: String) -> String { 246 | var content :String = "" 247 | 248 | do { 249 | if fileExists(name) { 250 | try content = String(contentsOfFile: name, encoding: String.Encoding.utf8) 251 | } else { 252 | print("File not found: \(name)") 253 | } 254 | } catch let error as NSError { 255 | print("Error reading file in \(name)") 256 | print(error) 257 | } 258 | 259 | return content 260 | } 261 | 262 | 263 | static func loadAsJson(_ name: String) -> [String:AnyObject]? { 264 | let content :String = self.load(name) 265 | //print("|"+content+"|") 266 | if let data = content.data(using: String.Encoding.utf8) { 267 | do { 268 | let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:AnyObject] 269 | return json 270 | } catch let error as NSError { 271 | print("Error parsing json from \(name)") 272 | print(error) 273 | } 274 | } 275 | return nil 276 | } 277 | 278 | // Save file to Documents/App folder 279 | static func save(name: String, content: String) { 280 | do { 281 | try content.write(toFile: name, atomically: false, encoding: String.Encoding.utf8) 282 | print("FILE UTILS: FIle saved") 283 | } catch let error as NSError { 284 | print("Error writing file to \(name)") 285 | print(error) 286 | } 287 | } 288 | 289 | static func saveAsJson(name: String, data: [String:AnyObject]) { 290 | let invalidJson = "\"error\":\"Invalid JSON\"" 291 | do { 292 | let json = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted) 293 | let text = String(data: json, encoding: String.Encoding.utf8) ?? invalidJson 294 | self.save(name: name, content: text) 295 | } catch let error as NSError { 296 | print("Error saving json for \(name)") 297 | print(error) 298 | } 299 | } 300 | 301 | // Download to memory 302 | /* 303 | static func download(fromUrl: String, callback: @escaping (_ data:Data?, _ response:URLResponse?, _ error:Error?) -> Void) throws { 304 | let uri = URL(string:fromUrl) 305 | let task = URLSession.shared.dataTask(with: uri!){ data, response, error in 306 | guard data != nil && error == nil else { 307 | print("(UTIL)Error downloading \(fromUrl)") 308 | print("(UTIL)Message: \(error)") 309 | return 310 | } 311 | 312 | do { 313 | print("(UTIL)Downloaded.") 314 | callback(data, response, error) 315 | } 316 | } 317 | task.resume() 318 | } 319 | */ 320 | 321 | // Download to file 322 | static func download(fromUrl: String, toFile: String, callback: @escaping (_ location :URL?, _ response :URLResponse?, _ error :Error?) -> Void) throws { 323 | let uri = URL(string:fromUrl) 324 | let task = URLSession.shared.downloadTask(with: uri!){ location, response, error in 325 | guard location != nil && error == nil else { 326 | print("(UTIL)Error downloading \(fromUrl)") 327 | print("(UTIL)Message: \(error)") 328 | print("-- End error") 329 | //callback(location, response, error) 330 | return 331 | //return 332 | } 333 | 334 | let fileManager = FileManager.default 335 | //let source = location?.absoluteString 336 | let source = location?.path 337 | //let target = URL(string: toFile) 338 | let target = toFile 339 | do { 340 | try fileManager.moveItem(atPath: source!, toPath: target) 341 | //(at: location!, to: url!) 342 | //print("(UTIL)Downloaded.") 343 | callback(location, response, error) 344 | } catch let error as NSError { 345 | print("Error moving file:") 346 | print(" Source: \(source)") 347 | print(" Target: \(target)") 348 | print(error) 349 | print("-- End error") 350 | } 351 | } 352 | task.resume() 353 | } 354 | 355 | } 356 | 357 | -------------------------------------------------------------------------------- /Imaginex.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3D04700F1DF5A96E00DAEF37 /* ColorUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D04700E1DF5A96E00DAEF37 /* ColorUtils.swift */; }; 11 | 3D69C1AE1DB848CB0082C216 /* spinner.gif in Resources */ = {isa = PBXBuildFile; fileRef = 3D69C1AD1DB848CB0082C216 /* spinner.gif */; }; 12 | 3D810BFB1DB6EEFE00647814 /* UIUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D810BFA1DB6EEFE00647814 /* UIUtils.swift */; }; 13 | 3D810BFD1DB7125400647814 /* Gallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D810BFC1DB7125400647814 /* Gallery.swift */; }; 14 | 3D96DE771DB17F6800D7AF9E /* SerialFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D96DE761DB17F6800D7AF9E /* SerialFetcher.swift */; }; 15 | 3D9ED0101DAF21CC0035F82C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9ED00F1DAF21CC0035F82C /* Settings.swift */; }; 16 | 3DA9B87D1DAEF64C008E7140 /* DateUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DA9B87C1DAEF64C008E7140 /* DateUtils.swift */; }; 17 | 3DC24C761DE8FF1100E7C8F5 /* deleted.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DC24C751DE8FF1100E7C8F5 /* deleted.png */; }; 18 | 3DC304701DBC328F00052331 /* icon_openfolder32.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DC3046A1DBC328F00052331 /* icon_openfolder32.png */; }; 19 | 3DC304711DBC328F00052331 /* icon_reload32.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DC3046B1DBC328F00052331 /* icon_reload32.png */; }; 20 | 3DC304721DBC328F00052331 /* icon_setwallpaper32.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DC3046C1DBC328F00052331 /* icon_setwallpaper32.png */; }; 21 | 3DC304731DBC328F00052331 /* icon_zoomin32.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DC3046D1DBC328F00052331 /* icon_zoomin32.png */; }; 22 | 3DC304741DBC328F00052331 /* icon_zoomout32.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DC3046E1DBC328F00052331 /* icon_zoomout32.png */; }; 23 | 3DC304751DBC328F00052331 /* icon_zoomtofit32.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DC3046F1DBC328F00052331 /* icon_zoomtofit32.png */; }; 24 | 3DCA84681DAE8B93001AB31E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA84671DAE8B93001AB31E /* AppDelegate.swift */; }; 25 | 3DCA846A1DAE8B93001AB31E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA84691DAE8B93001AB31E /* ViewController.swift */; }; 26 | 3DCA846C1DAE8B93001AB31E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3DCA846B1DAE8B93001AB31E /* Assets.xcassets */; }; 27 | 3DCA846F1DAE8B94001AB31E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3DCA846D1DAE8B94001AB31E /* Main.storyboard */; }; 28 | 3DCA847B1DAEE412001AB31E /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA847A1DAEE412001AB31E /* FileUtils.swift */; }; 29 | 3DCDCB0A1DB6A12F005106E9 /* settings.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3DCDCB091DB6A12F005106E9 /* settings.txt */; }; 30 | 3DCE62771DBE5C8400F1AF98 /* DataUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCE62761DBE5C8400F1AF98 /* DataUtils.swift */; }; 31 | 3DCFB5AA1DB30926009227F4 /* ViewThumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCFB5A81DB30925009227F4 /* ViewThumbnail.swift */; }; 32 | 3DCFB5AB1DB30926009227F4 /* ViewThumbnail.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3DCFB5A91DB30925009227F4 /* ViewThumbnail.xib */; }; 33 | 3DD4C47B1DB9B5040004CCE6 /* Spinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD4C47A1DB9B5040004CCE6 /* Spinner.swift */; }; 34 | 3DD641841DBC25CB0027AF7A /* AppIcon32.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DD641811DBC25CB0027AF7A /* AppIcon32.png */; }; 35 | 3DD641851DBC25CB0027AF7A /* AppIcon128.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DD641821DBC25CB0027AF7A /* AppIcon128.png */; }; 36 | 3DD641861DBC25CB0027AF7A /* AppIcon512.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DD641831DBC25CB0027AF7A /* AppIcon512.png */; }; 37 | 3DD6418A1DBC28A60027AF7A /* AppIcon256.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DD641881DBC28A60027AF7A /* AppIcon256.png */; }; 38 | 3DD6418C1DBC28E40027AF7A /* AppIcon16.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DD6418B1DBC28E40027AF7A /* AppIcon16.png */; }; 39 | 3DDFB6031DBD0701001BD20C /* GalleryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DDFB6021DBD0701001BD20C /* GalleryController.swift */; }; 40 | 3DFED5F61DE68335002AA89E /* StringUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFED5F51DE68335002AA89E /* StringUtils.swift */; }; 41 | /* End PBXBuildFile section */ 42 | 43 | /* Begin PBXFileReference section */ 44 | 3D04700E1DF5A96E00DAEF37 /* ColorUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ColorUtils.swift; path = Imaginex/ColorUtils.swift; sourceTree = ""; }; 45 | 3D69C1AD1DB848CB0082C216 /* spinner.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; name = spinner.gif; path = Media/spinner.gif; sourceTree = ""; }; 46 | 3D810BFA1DB6EEFE00647814 /* UIUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIUtils.swift; path = Imaginex/UIUtils.swift; sourceTree = ""; }; 47 | 3D810BFC1DB7125400647814 /* Gallery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Gallery.swift; sourceTree = ""; }; 48 | 3D96DE761DB17F6800D7AF9E /* SerialFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SerialFetcher.swift; path = Imaginex/SerialFetcher.swift; sourceTree = ""; }; 49 | 3D9ED00F1DAF21CC0035F82C /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; 50 | 3DA9B87C1DAEF64C008E7140 /* DateUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DateUtils.swift; path = Imaginex/DateUtils.swift; sourceTree = ""; }; 51 | 3DC24C751DE8FF1100E7C8F5 /* deleted.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = deleted.png; path = Media/deleted.png; sourceTree = ""; }; 52 | 3DC3046A1DBC328F00052331 /* icon_openfolder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = icon_openfolder32.png; path = Media/icon_openfolder32.png; sourceTree = ""; }; 53 | 3DC3046B1DBC328F00052331 /* icon_reload32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = icon_reload32.png; path = Media/icon_reload32.png; sourceTree = ""; }; 54 | 3DC3046C1DBC328F00052331 /* icon_setwallpaper32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = icon_setwallpaper32.png; path = Media/icon_setwallpaper32.png; sourceTree = ""; }; 55 | 3DC3046D1DBC328F00052331 /* icon_zoomin32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = icon_zoomin32.png; path = Media/icon_zoomin32.png; sourceTree = ""; }; 56 | 3DC3046E1DBC328F00052331 /* icon_zoomout32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = icon_zoomout32.png; path = Media/icon_zoomout32.png; sourceTree = ""; }; 57 | 3DC3046F1DBC328F00052331 /* icon_zoomtofit32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = icon_zoomtofit32.png; path = Media/icon_zoomtofit32.png; sourceTree = ""; }; 58 | 3DCA84641DAE8B93001AB31E /* Imaginex.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Imaginex.app; sourceTree = BUILT_PRODUCTS_DIR; }; 59 | 3DCA84671DAE8B93001AB31E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 60 | 3DCA84691DAE8B93001AB31E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; wrapsLines = 0; }; 61 | 3DCA846B1DAE8B93001AB31E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 62 | 3DCA846E1DAE8B94001AB31E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 63 | 3DCA84701DAE8B94001AB31E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 64 | 3DCA84761DAED817001AB31E /* TODO.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TODO.txt; sourceTree = ""; wrapsLines = 0; }; 65 | 3DCA847A1DAEE412001AB31E /* FileUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FileUtils.swift; path = Imaginex/FileUtils.swift; sourceTree = ""; wrapsLines = 0; }; 66 | 3DCDCB091DB6A12F005106E9 /* settings.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = settings.txt; sourceTree = ""; }; 67 | 3DCE62761DBE5C8400F1AF98 /* DataUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DataUtils.swift; path = Imaginex/DataUtils.swift; sourceTree = ""; }; 68 | 3DCFB5A81DB30925009227F4 /* ViewThumbnail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewThumbnail.swift; sourceTree = ""; }; 69 | 3DCFB5A91DB30925009227F4 /* ViewThumbnail.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ViewThumbnail.xib; sourceTree = ""; }; 70 | 3DD4C47A1DB9B5040004CCE6 /* Spinner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Spinner.swift; sourceTree = ""; }; 71 | 3DD641811DBC25CB0027AF7A /* AppIcon32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = AppIcon32.png; path = Media/AppIcon32.png; sourceTree = ""; }; 72 | 3DD641821DBC25CB0027AF7A /* AppIcon128.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = AppIcon128.png; path = Media/AppIcon128.png; sourceTree = ""; }; 73 | 3DD641831DBC25CB0027AF7A /* AppIcon512.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = AppIcon512.png; path = Media/AppIcon512.png; sourceTree = ""; }; 74 | 3DD641881DBC28A60027AF7A /* AppIcon256.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = AppIcon256.png; path = Media/AppIcon256.png; sourceTree = ""; }; 75 | 3DD6418B1DBC28E40027AF7A /* AppIcon16.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = AppIcon16.png; path = Media/AppIcon16.png; sourceTree = ""; }; 76 | 3DDFB6021DBD0701001BD20C /* GalleryController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryController.swift; sourceTree = ""; }; 77 | 3DFED5F51DE68335002AA89E /* StringUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StringUtils.swift; path = Imaginex/StringUtils.swift; sourceTree = ""; wrapsLines = 0; }; 78 | /* End PBXFileReference section */ 79 | 80 | /* Begin PBXFrameworksBuildPhase section */ 81 | 3DCA84611DAE8B93001AB31E /* Frameworks */ = { 82 | isa = PBXFrameworksBuildPhase; 83 | buildActionMask = 2147483647; 84 | files = ( 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | /* End PBXFrameworksBuildPhase section */ 89 | 90 | /* Begin PBXGroup section */ 91 | 3D04C2881DAFD73A00D89325 /* Utils */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 3D04700E1DF5A96E00DAEF37 /* ColorUtils.swift */, 95 | 3DCE62761DBE5C8400F1AF98 /* DataUtils.swift */, 96 | 3DA9B87C1DAEF64C008E7140 /* DateUtils.swift */, 97 | 3DCA847A1DAEE412001AB31E /* FileUtils.swift */, 98 | 3D96DE761DB17F6800D7AF9E /* SerialFetcher.swift */, 99 | 3DFED5F51DE68335002AA89E /* StringUtils.swift */, 100 | 3D810BFA1DB6EEFE00647814 /* UIUtils.swift */, 101 | ); 102 | name = Utils; 103 | path = ..; 104 | sourceTree = ""; 105 | }; 106 | 3D69C1AC1DB848820082C216 /* Media */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 3DC24C751DE8FF1100E7C8F5 /* deleted.png */, 110 | 3DC3046A1DBC328F00052331 /* icon_openfolder32.png */, 111 | 3DC3046B1DBC328F00052331 /* icon_reload32.png */, 112 | 3DC3046C1DBC328F00052331 /* icon_setwallpaper32.png */, 113 | 3DC3046D1DBC328F00052331 /* icon_zoomin32.png */, 114 | 3DC3046E1DBC328F00052331 /* icon_zoomout32.png */, 115 | 3DC3046F1DBC328F00052331 /* icon_zoomtofit32.png */, 116 | 3DD6418B1DBC28E40027AF7A /* AppIcon16.png */, 117 | 3DD641881DBC28A60027AF7A /* AppIcon256.png */, 118 | 3DD641811DBC25CB0027AF7A /* AppIcon32.png */, 119 | 3DD641821DBC25CB0027AF7A /* AppIcon128.png */, 120 | 3DD641831DBC25CB0027AF7A /* AppIcon512.png */, 121 | 3D69C1AD1DB848CB0082C216 /* spinner.gif */, 122 | ); 123 | name = Media; 124 | sourceTree = ""; 125 | }; 126 | 3DCA845B1DAE8B93001AB31E = { 127 | isa = PBXGroup; 128 | children = ( 129 | 3DCA84661DAE8B93001AB31E /* Imaginex */, 130 | 3DCA84651DAE8B93001AB31E /* Products */, 131 | ); 132 | sourceTree = ""; 133 | }; 134 | 3DCA84651DAE8B93001AB31E /* Products */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 3DCA84641DAE8B93001AB31E /* Imaginex.app */, 138 | ); 139 | name = Products; 140 | sourceTree = ""; 141 | }; 142 | 3DCA84661DAE8B93001AB31E /* Imaginex */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 3DCA84761DAED817001AB31E /* TODO.txt */, 146 | 3DCDCB091DB6A12F005106E9 /* settings.txt */, 147 | 3DCA84701DAE8B94001AB31E /* Info.plist */, 148 | 3DCA84671DAE8B93001AB31E /* AppDelegate.swift */, 149 | 3DCA84691DAE8B93001AB31E /* ViewController.swift */, 150 | 3DDFB6021DBD0701001BD20C /* GalleryController.swift */, 151 | 3DCFB5A81DB30925009227F4 /* ViewThumbnail.swift */, 152 | 3DCFB5A91DB30925009227F4 /* ViewThumbnail.xib */, 153 | 3DCA846D1DAE8B94001AB31E /* Main.storyboard */, 154 | 3DCE62781DBE91F900F1AF98 /* Classes */, 155 | 3DCA846B1DAE8B93001AB31E /* Assets.xcassets */, 156 | 3D69C1AC1DB848820082C216 /* Media */, 157 | 3D04C2881DAFD73A00D89325 /* Utils */, 158 | ); 159 | path = Imaginex; 160 | sourceTree = ""; 161 | }; 162 | 3DCE62781DBE91F900F1AF98 /* Classes */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 3D810BFC1DB7125400647814 /* Gallery.swift */, 166 | 3D9ED00F1DAF21CC0035F82C /* Settings.swift */, 167 | 3DD4C47A1DB9B5040004CCE6 /* Spinner.swift */, 168 | ); 169 | name = Classes; 170 | sourceTree = ""; 171 | }; 172 | /* End PBXGroup section */ 173 | 174 | /* Begin PBXNativeTarget section */ 175 | 3DCA84631DAE8B93001AB31E /* Imaginex */ = { 176 | isa = PBXNativeTarget; 177 | buildConfigurationList = 3DCA84731DAE8B94001AB31E /* Build configuration list for PBXNativeTarget "Imaginex" */; 178 | buildPhases = ( 179 | 3DCA84601DAE8B93001AB31E /* Sources */, 180 | 3DCA84611DAE8B93001AB31E /* Frameworks */, 181 | 3DCA84621DAE8B93001AB31E /* Resources */, 182 | ); 183 | buildRules = ( 184 | ); 185 | dependencies = ( 186 | ); 187 | name = Imaginex; 188 | productName = ImgurDaily; 189 | productReference = 3DCA84641DAE8B93001AB31E /* Imaginex.app */; 190 | productType = "com.apple.product-type.application"; 191 | }; 192 | /* End PBXNativeTarget section */ 193 | 194 | /* Begin PBXProject section */ 195 | 3DCA845C1DAE8B93001AB31E /* Project object */ = { 196 | isa = PBXProject; 197 | attributes = { 198 | LastSwiftUpdateCheck = 0800; 199 | LastUpgradeCheck = 0800; 200 | ORGANIZATIONNAME = Armonia; 201 | TargetAttributes = { 202 | 3DCA84631DAE8B93001AB31E = { 203 | CreatedOnToolsVersion = 8.0; 204 | DevelopmentTeam = W7PMV9XFGM; 205 | ProvisioningStyle = Automatic; 206 | }; 207 | }; 208 | }; 209 | buildConfigurationList = 3DCA845F1DAE8B93001AB31E /* Build configuration list for PBXProject "Imaginex" */; 210 | compatibilityVersion = "Xcode 3.2"; 211 | developmentRegion = English; 212 | hasScannedForEncodings = 0; 213 | knownRegions = ( 214 | en, 215 | Base, 216 | ); 217 | mainGroup = 3DCA845B1DAE8B93001AB31E; 218 | productRefGroup = 3DCA84651DAE8B93001AB31E /* Products */; 219 | projectDirPath = ""; 220 | projectRoot = ""; 221 | targets = ( 222 | 3DCA84631DAE8B93001AB31E /* Imaginex */, 223 | ); 224 | }; 225 | /* End PBXProject section */ 226 | 227 | /* Begin PBXResourcesBuildPhase section */ 228 | 3DCA84621DAE8B93001AB31E /* Resources */ = { 229 | isa = PBXResourcesBuildPhase; 230 | buildActionMask = 2147483647; 231 | files = ( 232 | 3DC304701DBC328F00052331 /* icon_openfolder32.png in Resources */, 233 | 3DC304741DBC328F00052331 /* icon_zoomout32.png in Resources */, 234 | 3DCA846C1DAE8B93001AB31E /* Assets.xcassets in Resources */, 235 | 3DCA846F1DAE8B94001AB31E /* Main.storyboard in Resources */, 236 | 3DD6418A1DBC28A60027AF7A /* AppIcon256.png in Resources */, 237 | 3DD6418C1DBC28E40027AF7A /* AppIcon16.png in Resources */, 238 | 3DD641851DBC25CB0027AF7A /* AppIcon128.png in Resources */, 239 | 3DD641861DBC25CB0027AF7A /* AppIcon512.png in Resources */, 240 | 3DC304731DBC328F00052331 /* icon_zoomin32.png in Resources */, 241 | 3D69C1AE1DB848CB0082C216 /* spinner.gif in Resources */, 242 | 3DC304711DBC328F00052331 /* icon_reload32.png in Resources */, 243 | 3DC304721DBC328F00052331 /* icon_setwallpaper32.png in Resources */, 244 | 3DC24C761DE8FF1100E7C8F5 /* deleted.png in Resources */, 245 | 3DCFB5AB1DB30926009227F4 /* ViewThumbnail.xib in Resources */, 246 | 3DCDCB0A1DB6A12F005106E9 /* settings.txt in Resources */, 247 | 3DD641841DBC25CB0027AF7A /* AppIcon32.png in Resources */, 248 | 3DC304751DBC328F00052331 /* icon_zoomtofit32.png in Resources */, 249 | ); 250 | runOnlyForDeploymentPostprocessing = 0; 251 | }; 252 | /* End PBXResourcesBuildPhase section */ 253 | 254 | /* Begin PBXSourcesBuildPhase section */ 255 | 3DCA84601DAE8B93001AB31E /* Sources */ = { 256 | isa = PBXSourcesBuildPhase; 257 | buildActionMask = 2147483647; 258 | files = ( 259 | 3DDFB6031DBD0701001BD20C /* GalleryController.swift in Sources */, 260 | 3D04700F1DF5A96E00DAEF37 /* ColorUtils.swift in Sources */, 261 | 3DCA847B1DAEE412001AB31E /* FileUtils.swift in Sources */, 262 | 3DCFB5AA1DB30926009227F4 /* ViewThumbnail.swift in Sources */, 263 | 3D96DE771DB17F6800D7AF9E /* SerialFetcher.swift in Sources */, 264 | 3DFED5F61DE68335002AA89E /* StringUtils.swift in Sources */, 265 | 3DD4C47B1DB9B5040004CCE6 /* Spinner.swift in Sources */, 266 | 3DCE62771DBE5C8400F1AF98 /* DataUtils.swift in Sources */, 267 | 3DA9B87D1DAEF64C008E7140 /* DateUtils.swift in Sources */, 268 | 3D810BFD1DB7125400647814 /* Gallery.swift in Sources */, 269 | 3D810BFB1DB6EEFE00647814 /* UIUtils.swift in Sources */, 270 | 3DCA846A1DAE8B93001AB31E /* ViewController.swift in Sources */, 271 | 3DCA84681DAE8B93001AB31E /* AppDelegate.swift in Sources */, 272 | 3D9ED0101DAF21CC0035F82C /* Settings.swift in Sources */, 273 | ); 274 | runOnlyForDeploymentPostprocessing = 0; 275 | }; 276 | /* End PBXSourcesBuildPhase section */ 277 | 278 | /* Begin PBXVariantGroup section */ 279 | 3DCA846D1DAE8B94001AB31E /* Main.storyboard */ = { 280 | isa = PBXVariantGroup; 281 | children = ( 282 | 3DCA846E1DAE8B94001AB31E /* Base */, 283 | ); 284 | name = Main.storyboard; 285 | sourceTree = ""; 286 | }; 287 | /* End PBXVariantGroup section */ 288 | 289 | /* Begin XCBuildConfiguration section */ 290 | 3DCA84711DAE8B94001AB31E /* Debug */ = { 291 | isa = XCBuildConfiguration; 292 | buildSettings = { 293 | ALWAYS_SEARCH_USER_PATHS = NO; 294 | CLANG_ANALYZER_NONNULL = YES; 295 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 296 | CLANG_CXX_LIBRARY = "libc++"; 297 | CLANG_ENABLE_MODULES = YES; 298 | CLANG_ENABLE_OBJC_ARC = YES; 299 | CLANG_WARN_BOOL_CONVERSION = YES; 300 | CLANG_WARN_CONSTANT_CONVERSION = YES; 301 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 302 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 303 | CLANG_WARN_EMPTY_BODY = YES; 304 | CLANG_WARN_ENUM_CONVERSION = YES; 305 | CLANG_WARN_INFINITE_RECURSION = YES; 306 | CLANG_WARN_INT_CONVERSION = YES; 307 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 308 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 309 | CLANG_WARN_UNREACHABLE_CODE = YES; 310 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 311 | CODE_SIGN_IDENTITY = "-"; 312 | COPY_PHASE_STRIP = NO; 313 | DEBUG_INFORMATION_FORMAT = dwarf; 314 | ENABLE_STRICT_OBJC_MSGSEND = YES; 315 | ENABLE_TESTABILITY = YES; 316 | GCC_C_LANGUAGE_STANDARD = gnu99; 317 | GCC_DYNAMIC_NO_PIC = NO; 318 | GCC_NO_COMMON_BLOCKS = YES; 319 | GCC_OPTIMIZATION_LEVEL = 0; 320 | GCC_PREPROCESSOR_DEFINITIONS = ( 321 | "DEBUG=1", 322 | "$(inherited)", 323 | ); 324 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 325 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 326 | GCC_WARN_UNDECLARED_SELECTOR = YES; 327 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 328 | GCC_WARN_UNUSED_FUNCTION = YES; 329 | GCC_WARN_UNUSED_VARIABLE = YES; 330 | MACOSX_DEPLOYMENT_TARGET = 10.12; 331 | MTL_ENABLE_DEBUG_INFO = YES; 332 | ONLY_ACTIVE_ARCH = YES; 333 | SDKROOT = macosx; 334 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 335 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 336 | }; 337 | name = Debug; 338 | }; 339 | 3DCA84721DAE8B94001AB31E /* Release */ = { 340 | isa = XCBuildConfiguration; 341 | buildSettings = { 342 | ALWAYS_SEARCH_USER_PATHS = NO; 343 | CLANG_ANALYZER_NONNULL = YES; 344 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 345 | CLANG_CXX_LIBRARY = "libc++"; 346 | CLANG_ENABLE_MODULES = YES; 347 | CLANG_ENABLE_OBJC_ARC = YES; 348 | CLANG_WARN_BOOL_CONVERSION = YES; 349 | CLANG_WARN_CONSTANT_CONVERSION = YES; 350 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 351 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 352 | CLANG_WARN_EMPTY_BODY = YES; 353 | CLANG_WARN_ENUM_CONVERSION = YES; 354 | CLANG_WARN_INFINITE_RECURSION = YES; 355 | CLANG_WARN_INT_CONVERSION = YES; 356 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 357 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 358 | CLANG_WARN_UNREACHABLE_CODE = YES; 359 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 360 | CODE_SIGN_IDENTITY = "-"; 361 | COPY_PHASE_STRIP = NO; 362 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 363 | ENABLE_NS_ASSERTIONS = NO; 364 | ENABLE_STRICT_OBJC_MSGSEND = YES; 365 | GCC_C_LANGUAGE_STANDARD = gnu99; 366 | GCC_NO_COMMON_BLOCKS = YES; 367 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 368 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 369 | GCC_WARN_UNDECLARED_SELECTOR = YES; 370 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 371 | GCC_WARN_UNUSED_FUNCTION = YES; 372 | GCC_WARN_UNUSED_VARIABLE = YES; 373 | MACOSX_DEPLOYMENT_TARGET = 10.12; 374 | MTL_ENABLE_DEBUG_INFO = NO; 375 | SDKROOT = macosx; 376 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 377 | }; 378 | name = Release; 379 | }; 380 | 3DCA84741DAE8B94001AB31E /* Debug */ = { 381 | isa = XCBuildConfiguration; 382 | buildSettings = { 383 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 384 | CODE_SIGN_IDENTITY = ""; 385 | COMBINE_HIDPI_IMAGES = YES; 386 | DEVELOPMENT_TEAM = W7PMV9XFGM; 387 | INFOPLIST_FILE = "$(SRCROOT)/Imaginex/Info.plist"; 388 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 389 | MACOSX_DEPLOYMENT_TARGET = 10.11; 390 | PRODUCT_BUNDLE_IDENTIFIER = Armonia.Imaginex; 391 | PRODUCT_NAME = "$(TARGET_NAME)"; 392 | SWIFT_VERSION = 3.0; 393 | }; 394 | name = Debug; 395 | }; 396 | 3DCA84751DAE8B94001AB31E /* Release */ = { 397 | isa = XCBuildConfiguration; 398 | buildSettings = { 399 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 400 | CODE_SIGN_IDENTITY = ""; 401 | COMBINE_HIDPI_IMAGES = YES; 402 | DEVELOPMENT_TEAM = W7PMV9XFGM; 403 | INFOPLIST_FILE = "$(SRCROOT)/Imaginex/Info.plist"; 404 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 405 | MACOSX_DEPLOYMENT_TARGET = 10.11; 406 | PRODUCT_BUNDLE_IDENTIFIER = Armonia.Imaginex; 407 | PRODUCT_NAME = "$(TARGET_NAME)"; 408 | SWIFT_VERSION = 3.0; 409 | }; 410 | name = Release; 411 | }; 412 | /* End XCBuildConfiguration section */ 413 | 414 | /* Begin XCConfigurationList section */ 415 | 3DCA845F1DAE8B93001AB31E /* Build configuration list for PBXProject "Imaginex" */ = { 416 | isa = XCConfigurationList; 417 | buildConfigurations = ( 418 | 3DCA84711DAE8B94001AB31E /* Debug */, 419 | 3DCA84721DAE8B94001AB31E /* Release */, 420 | ); 421 | defaultConfigurationIsVisible = 0; 422 | defaultConfigurationName = Release; 423 | }; 424 | 3DCA84731DAE8B94001AB31E /* Build configuration list for PBXNativeTarget "Imaginex" */ = { 425 | isa = XCConfigurationList; 426 | buildConfigurations = ( 427 | 3DCA84741DAE8B94001AB31E /* Debug */, 428 | 3DCA84751DAE8B94001AB31E /* Release */, 429 | ); 430 | defaultConfigurationIsVisible = 0; 431 | defaultConfigurationName = Release; 432 | }; 433 | /* End XCConfigurationList section */ 434 | }; 435 | rootObject = 3DCA845C1DAE8B93001AB31E /* Project object */; 436 | } 437 | -------------------------------------------------------------------------------- /Imaginex/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Imaginex 4 | // 5 | // Created by Mac Mini on 10/12/16. 6 | // Copyright © 2016 Armonia. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ViewController: NSViewController, didFinishDownload { 12 | 13 | var windowGallery : NSWindow = NSWindow() 14 | var settings = Settings() 15 | var galleries = [Gallery]() 16 | var gallery = Gallery() 17 | var selected = GalleryItem() 18 | var zoomValue = 1.0 19 | var autoDownload = true 20 | 21 | @IBOutlet weak var mainWindow : NSWindow! 22 | @IBOutlet weak var viewDesktop : NSView! 23 | @IBOutlet weak var mainArea : NSScrollView! 24 | @IBOutlet weak var mainImage : NSImageView! 25 | @IBOutlet weak var collectionView: NSCollectionView! 26 | @IBOutlet weak var statusBar : NSView! 27 | @IBOutlet weak var statusText : NSTextField! 28 | @IBOutlet weak var spinner : NSProgressIndicator! 29 | 30 | 31 | //-- Menu actions 32 | 33 | @IBAction func onAutoDownload(_ sender: NSMenuItem) { 34 | imageAutoDownload() 35 | } 36 | 37 | @IBAction func addGallery(_ sender: NSMenuItem) { 38 | addGallery() 39 | } 40 | 41 | @IBAction func deleteGallery(_ sender: NSMenuItem) { 42 | removeGallery() 43 | } 44 | 45 | @IBAction func refreshGallery(_ sender: NSMenuItem) { 46 | refreshGallery() 47 | } 48 | 49 | @IBAction func onSelectGallery(_ sender: NSMenuItem) { 50 | selectGallery(byName: sender.title.lowercased()) 51 | } 52 | 53 | @IBAction func onFirstItem(_ sender: NSMenuItem) { 54 | goFirstThumbnail() 55 | } 56 | 57 | @IBAction func onLastItem(_ sender: NSMenuItem) { 58 | goLastThumbnail() 59 | } 60 | 61 | override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { 62 | //TODO: enable/disable items 63 | //if menuItem.title == "Refresh" { return false } 64 | return true 65 | } 66 | 67 | 68 | //-- Toolbar actions 69 | 70 | @IBAction func onRefresh(_ sender: NSButton) { 71 | refreshGallery() 72 | } 73 | 74 | @IBAction func onZoomOut(_ sender: NSButton) { 75 | zoomOut() 76 | } 77 | 78 | @IBAction func onZoomToFit(_ sender: NSButton) { 79 | zoomToFit() 80 | } 81 | 82 | @IBAction func onZoomIn(_ sender: NSButton) { 83 | zoomIn() 84 | } 85 | 86 | @IBAction func onOpenFinder(_ sender: NSButton) { 87 | openFinder(file: selected.imagePath) 88 | } 89 | 90 | @IBAction func onSetWallpaper(_ sender: NSButton) { 91 | setAsWallpaper(file: selected.imagePath) 92 | } 93 | 94 | @IBAction func onDeleteImage(_ sender: NSButton) { 95 | deleteImage(file: selected.imagePath) 96 | } 97 | 98 | 99 | //-- View methods 100 | 101 | override func viewDidLoad() { 102 | super.viewDidLoad() 103 | initialize() 104 | } 105 | 106 | override func viewDidAppear() { 107 | // Dark 108 | if let window = self.view.window { 109 | window.appearance = NSAppearance(named: NSAppearanceNameVibrantDark) 110 | } 111 | } 112 | 113 | override var representedObject: Any? { 114 | didSet { 115 | // Update the view, if already loaded. 116 | } 117 | } 118 | 119 | 120 | func initialize() { 121 | viewDesktop.appearance = NSAppearance(named: NSAppearanceNameVibrantDark) 122 | showStatus("Ready") 123 | 124 | // Async after all UI has been shown 125 | DispatchQueue.main.async { 126 | self.settings.load() 127 | self.loadGalleries() 128 | self.selectFirstGallery() 129 | } 130 | } 131 | 132 | 133 | //-- Status bar 134 | 135 | enum StatusType { 136 | case info, data, warn, error 137 | } 138 | 139 | func showStatus(_ text: String) { 140 | showStatus(text: text, type: .info) // Default 141 | } 142 | 143 | func showStatus(text: String, type:StatusType) { 144 | //print(text) 145 | statusText.stringValue = text 146 | setStatusColor(for: type) 147 | } 148 | 149 | func setStatusColor(for type: StatusType){ 150 | let CrayonLead = NSColor(hex:0x252525) 151 | switch type { 152 | case .info : // white on black 153 | statusText.textColor = NSColor.darkGray 154 | statusBar.backgroundColor = CrayonLead 155 | break 156 | case .data : // green on black 157 | statusText.textColor = NSColor.green 158 | statusBar.backgroundColor = CrayonLead 159 | break 160 | case .warn : // yellow on black 161 | statusText.textColor = NSColor.yellow 162 | statusBar.backgroundColor = CrayonLead 163 | break 164 | case .error: // white on red 165 | //statusText.textColor = NSColor.white 166 | //statusBar.backgroundColor = NSColor.red 167 | statusText.textColor = NSColor.red 168 | statusBar.backgroundColor = CrayonLead 169 | break 170 | } 171 | } 172 | 173 | 174 | // Add galleries to menu 175 | 176 | func loadGalleries(){ 177 | galleries = settings.galleries // Alias 178 | let names: [String] = galleries.map{ $0.name } 179 | buildGalleriesMenu(names) 180 | } 181 | 182 | func buildGalleriesMenu(_ list:[String]) { 183 | let mainMenu = NSApplication.shared().mainMenu 184 | let menuGalleries = mainMenu?.item(withTitle: "Galleries") //.item(at: 2) 185 | 186 | var pos = 0 187 | for name in list { 188 | let menuItem = NSMenuItem() 189 | menuItem.title = name 190 | menuItem.isEnabled = true 191 | menuItem.action = #selector(ViewController.onSelectGallery(_:)) 192 | menuGalleries?.submenu?.insertItem(menuItem, at: pos) 193 | pos += 1 194 | } 195 | } 196 | 197 | func addToMenu(name :String) { 198 | let mainMenu = NSApplication.shared().mainMenu 199 | let menuGalleries = mainMenu?.item(withTitle: "Galleries") 200 | 201 | // find point of insertion 202 | var pos = 0 203 | for item in (menuGalleries?.submenu?.items)! { 204 | if item.isSeparatorItem { 205 | break 206 | } 207 | print(item.title) 208 | pos += 1 209 | } 210 | 211 | let menuItem = NSMenuItem() 212 | menuItem.title = name 213 | menuItem.isEnabled = true 214 | menuItem.action = #selector(ViewController.onSelectGallery(_:)) 215 | menuGalleries?.submenu?.insertItem(menuItem, at: pos) 216 | } 217 | 218 | func removeFromMenu(name :String) { 219 | let mainMenu = NSApplication.shared().mainMenu 220 | let menuGalleries = mainMenu?.item(withTitle: "Galleries") 221 | 222 | for item in (menuGalleries?.submenu?.items)! { 223 | if item.isSeparatorItem { 224 | print("-- separator") 225 | break 226 | } 227 | print(item.title) 228 | if item.title == name { 229 | print("Removing menu item: \(item.title)") 230 | menuGalleries?.submenu?.removeItem(item) 231 | } 232 | } 233 | } 234 | 235 | func imageAutoDownload() { 236 | autoDownload = !autoDownload 237 | let mainMenu = NSApplication.shared().mainMenu 238 | let menuItem = mainMenu?.item(withTitle: "Images") //.item(at: 2) 239 | let option = menuItem?.submenu?.item(withTitle: "AutoDownload") 240 | option?.state = (autoDownload ? 1 : 0) 241 | option?.isEnabled = autoDownload 242 | } 243 | 244 | func addGallery() { 245 | let storyboard = NSStoryboard(name: "Main", bundle: nil) 246 | let controller = storyboard.instantiateController(withIdentifier: "viewGalleryController") as! GalleryController 247 | windowGallery = NSWindow(contentViewController: controller) 248 | let app = NSApplication.shared() 249 | // Modal direct 250 | app.runModal(for: windowGallery) 251 | 252 | // On return 253 | print(".\(controller.response)") 254 | if controller.response == .saved { 255 | let name = controller.gallery.name // response form modal 256 | print("Added gallery: \(name)") 257 | settings.load() // reload with new gallery 258 | galleries = settings.galleries // reassign to global var 259 | gallery = findGallery(byName: name) 260 | guard !gallery.name.isEmpty else { 261 | print("Gallery recently added not found") 262 | return 263 | } 264 | addToMenu(name: name) 265 | showThumbnails() 266 | } else { 267 | print("User cancelled") 268 | // Do nothing 269 | //let _ = settings.toDictionary() 270 | } 271 | print("Modal session ended") 272 | } 273 | 274 | func removeGallery() { 275 | let dialog = DialogYesNo(title: "Remove gallery", info: "Are you sure you want to remove it from the list?\nYour images in 'Media' folder won't be lost") 276 | if dialog.choice() { 277 | removeFromMenu(name: gallery.name) 278 | settings.removeGallery(name: gallery.name) 279 | selectFirstGallery() 280 | showStatus(text: "Gallery removed from list", type: .warn) 281 | } 282 | } 283 | 284 | func fetchImages(gallery name: String) { 285 | if settings.galleries.count < 1 { 286 | self.showStatus(text: "No galleries available", type: .error) 287 | return 288 | } 289 | 290 | gallery = findGallery(byName: name) 291 | guard !gallery.name.isEmpty else { 292 | self.showStatus(text: "Gallery [\(name)] not found", type: .error) 293 | return 294 | } 295 | 296 | // Fetch gallery thumbs from html 297 | showStatus("Downloading images from [\(name)]...") 298 | 299 | let url = URL(string: gallery.url) 300 | 301 | // Get html from gallery 302 | let task = URLSession.shared.dataTask(with: url!) { data, response, error in 303 | guard error == nil else { 304 | if (error as! URLError?) != nil { 305 | DispatchQueue.main.async(execute: { 306 | self.showStatus(text: "Internet error. Check Connection", type: .error) 307 | }) 308 | } else { 309 | DispatchQueue.main.async(execute: { 310 | self.showStatus(text: "Error fetching images. Try again later", type: .error) 311 | }) 312 | } 313 | print(error) 314 | print("-- End Error") 315 | return 316 | } 317 | 318 | if let data = data, let html = String(data: data, encoding: String.Encoding.utf8) { 319 | //print(html) 320 | // FileUtils.save(name: path, content: html) 321 | // Download thumbs 322 | let regex = "\"\"" 323 | let matches = html.matchAll(regex) 324 | let thumbs = matches.map{ "http:"+$0 } 325 | //print(thumbs) 326 | 327 | if thumbs.count > 0 { 328 | self.downloadThumbs(gallery: name, thumbs: thumbs) 329 | } 330 | } else { 331 | print("No data") 332 | return 333 | } 334 | } 335 | task.resume() 336 | } 337 | 338 | // Get them from Imgur if not exist locally 339 | 340 | func downloadThumbs(gallery name: String, thumbs: [String]) { 341 | print("Downloads:") 342 | if thumbs.count < 1 { 343 | showStatus(text: "No images to download", type: .error) 344 | return 345 | } 346 | 347 | // docs/Imaginex/media 348 | if !FileUtils.verifyMediaFolder() { 349 | showStatus(text: "Media folder not found", type: .error) 350 | return 351 | } 352 | 353 | // docs/Imaginex/media/{gallery} 354 | if !FileUtils.verifyImagesFolder(gallery: gallery.name) { 355 | showStatus(text: "Images folder for gallery [\(name)] not found", type: .error) 356 | return 357 | } 358 | 359 | // docs/Imaginex/media/{gallery}/.thumbs 360 | if !FileUtils.verifyThumbsFolder(gallery: gallery.name) { 361 | showStatus(text: "Thumbs folder for gallery [\(name)] not found", type: .error) 362 | return 363 | } 364 | 365 | // Chained serial async download one image at a time 366 | // Start with thumbs since they are smaller and will show all first 367 | 368 | // Async process, add callback on finish 369 | let thumbFetcher = ThumbFetcher(gallery: name, thumbs: thumbs, inReverse: true) 370 | thumbFetcher.delegate = self // protocol to show thumb after downloaded 371 | thumbFetcher.loop() 372 | } 373 | 374 | // Get them from Imgur if not exist locally 375 | 376 | func downloadImage(_ url :String, gallery name: String) { 377 | if url.isEmpty { 378 | showStatus(text: "No URL to download", type: .error) 379 | return 380 | } 381 | 382 | if name.isEmpty { 383 | showStatus(text: "No gallery specified", type: .error) 384 | return 385 | } 386 | 387 | if !FileUtils.verifyMediaFolder() { 388 | showStatus(text: "Media folder not found", type: .error) 389 | return 390 | } 391 | 392 | if !FileUtils.verifyImagesFolder(gallery: name) { 393 | showStatus(text: "Media folder for gallery [\(name)] not found", type: .error) 394 | return 395 | } 396 | 397 | let image = FileUtils.getImageNameFromUrl(url) 398 | let folder = FileUtils.getImagesFolder(gallery: name) 399 | let path = folder.appendingPathComponent(image).path 400 | 401 | // Async process, add callback on finish 402 | do { 403 | try FileUtils.download(fromUrl: url, toFile: path){ url, response, error in 404 | self.showImage(path: path) 405 | } 406 | } catch let error as NSError { 407 | showStatus(text: "Error downloading image from url: \(url)", type: .error) 408 | print(error) 409 | } 410 | } 411 | 412 | func selectFirstGallery() { 413 | selectGallery(0) 414 | } 415 | 416 | func selectGallery(_ index :Int) { 417 | if index >= galleries.count { return } 418 | gallery = galleries[index] 419 | self.view.window?.title = gallery.name 420 | showThumbnails() 421 | } 422 | 423 | func selectGallery(byName name: String) { 424 | var index = 0 425 | for item in galleries { 426 | if item.name.lowercased() == name.lowercased() { 427 | selectGallery(index) 428 | return 429 | } 430 | index += 1 431 | } 432 | showStatus(text: "Gallery [\(name)] not available", type: .error) 433 | } 434 | 435 | func findGallery(byName name: String) -> Gallery { 436 | for item in galleries { 437 | if item.name.lowercased() == name.lowercased() { 438 | return item 439 | } 440 | } 441 | showStatus(text: "Gallery [\(name)] not found", type: .error) 442 | return Gallery() 443 | } 444 | 445 | func refreshGallery() { 446 | let name = gallery.name.lowercased() 447 | fetchImages(gallery: name) 448 | // downloadThumbs 449 | // showThumbs 450 | // showFirstImage 451 | } 452 | 453 | // Get last 100 thumbs from media/gallery/thumbs folder 454 | func showThumbnails() { 455 | let name = gallery.name 456 | print("Show thumbnails") 457 | 458 | gallery.items = [GalleryItem]() 459 | 460 | // Get thumbs from folder[name] 461 | guard let thumbs = FileUtils.listFilesInMedia(gallery: name, max: 100) else { 462 | showStatus("Empty gallery. Refresh to get images from server") 463 | return 464 | } 465 | 466 | for item in thumbs { 467 | // item is the file name without path, just the name.jpg 468 | if item.hasPrefix(".") { continue } // system file 469 | if item.hasPrefix("_") { continue } // deleted 470 | let file = NSString(string: item) 471 | let ext = file.pathExtension 472 | let thumb = file.deletingPathExtension 473 | let image = String(thumb.characters.dropLast()) // remove the 'b' for thumbs 474 | let imageName = image+"."+ext 475 | //print(item,imageName) 476 | let some = GalleryItem() 477 | //some.title = item.title 478 | //some.link = item.link 479 | //some.desc = item.desc 480 | some.imageUrl = "http://i.imgur.com/"+imageName 481 | //some.imageType = item.imageType 482 | //some.imageHeight = item.imageHeight 483 | //some.imageWidth = item.imageWidth 484 | //some.imageSize = item.imageSize 485 | some.imageName = imageName 486 | some.imagePath = FileUtils.getImagePath(gallery: gallery.name, forFile: imageName) 487 | some.thumbUrl = "http://i.imgur.com/"+item 488 | some.thumbName = item 489 | some.thumbPath = FileUtils.getThumbnailPath(gallery: gallery.name, forFile: item) 490 | gallery.items.append(some) 491 | } 492 | 493 | if gallery.items.count < 1 { 494 | showStatus("Empty gallery. No thumbnails to show") 495 | return 496 | } 497 | 498 | showStatus("Refreshing gallery, wait a moment...") 499 | 500 | collectionView.delegate = self 501 | collectionView.dataSource = self 502 | collectionView.reloadData() 503 | 504 | goFirstThumbnail() 505 | } 506 | 507 | 508 | func getLocalThumbs(_ name: String, max: Int) -> [String] { 509 | var numItems = max 510 | if max < 1 { numItems = 100 } 511 | let list = FileUtils.listFilesInMedia(gallery: name, max: numItems)! 512 | return list 513 | } 514 | 515 | // Invoke delegate select/deselect 516 | func deselectThumbnail() { 517 | let current = collectionView.selectionIndexPaths.first! 518 | collectionView.delegate?.collectionView!(collectionView, didDeselectItemsAt: [current]) 519 | } 520 | 521 | // Invoke delegate select/deselect 522 | func selectThumbnail(_ index :Int) { 523 | let newpos = IndexPath(item: index, section: 0) 524 | collectionView.delegate?.collectionView!(collectionView, didSelectItemsAt: [newpos]) 525 | } 526 | 527 | func goFirstThumbnail(){ 528 | collectionView.deselectAll(nil) 529 | let index = IndexPath(item: 0, section: 0) 530 | collectionView.selectItems(at: [index], scrollPosition: NSCollectionViewScrollPosition.left) 531 | selectThumbnail(0) 532 | } 533 | 534 | func goLastThumbnail(){ 535 | collectionView.deselectAll(nil) 536 | let last = collectionView.numberOfItems(inSection: 0) - 1 537 | let index = IndexPath(item: last, section: 0) 538 | collectionView.selectItems(at: [index], scrollPosition: NSCollectionViewScrollPosition.right) 539 | selectThumbnail(last) 540 | } 541 | 542 | 543 | func showFirstImage() { 544 | showImage(index: 0) 545 | } 546 | 547 | // Protocol didFinishDownload 548 | func showDownloadedImage(_ path: String) { 549 | showImage(path: path) 550 | } 551 | 552 | // Protocol didFinishDownload 553 | func showDownloadedThumb(_ path: String) { 554 | // add thumbnail to head of collection 555 | let item = GalleryItem() 556 | item.thumbName = FileUtils.getImageNameFromUrl(path) 557 | item.thumbPath = path 558 | gallery.items.insert(item, at: 0) 559 | print("First thumb") 560 | collectionView.reloadData() 561 | //goFirstThumbnail() // No. It will download the image and thats not wanted 562 | } 563 | 564 | // Protocol didFinishDownload 565 | func refreshCurrentGallery() { 566 | print("Refreshing gallery...") 567 | DispatchQueue.main.async(execute: { 568 | self.showThumbnails() 569 | }) 570 | } 571 | 572 | func showImage(index :Int) { 573 | if index >= gallery.items.count { return } 574 | selected = gallery.items[index] 575 | let folder = FileUtils.getImagesFolder(gallery: gallery.name) 576 | let imageName = gallery.items[index].imageName 577 | let imagePath = folder.appendingPathComponent(imageName) 578 | let imageFile = imagePath.path 579 | let fileExtension = imagePath.pathExtension 580 | 581 | if imageName.hasPrefix("_") { /* deleted */ 582 | showDeletedImage() 583 | showStatus(text: "Image: \(imagePath) has been deleted", type: .info) 584 | return 585 | } 586 | 587 | DispatchQueue.main.async(execute: { 588 | self.spinner.stopAnimation(self.view) 589 | self.spinner.isHidden = true 590 | }) 591 | 592 | if FileUtils.fileExists(imageFile) { 593 | let image = NSImage(byReferencingFile: imageFile)! 594 | 595 | if fileExtension == "gif" { 596 | print("GIF file") 597 | mainImage.canDrawSubviewsIntoLayer = true 598 | mainImage.animates = true 599 | } 600 | mainImage.image = image 601 | 602 | // Image Info 603 | let fileInfo = FileUtils.getFileInfo(imageFile)! 604 | let sizeval = fileInfo[FileAttributeKey.size] as! Double 605 | let sizekb :Double = sizeval / 1024.00 606 | let sizeInKB = String(format: "%.0f KB", sizekb) 607 | let width = String(format: "%.0f", image.size.width) 608 | let height = String(format: "%.0f", image.size.height) 609 | let imageInfo = " [\(width)x\(height)] \(sizeInKB)" 610 | let imagePath = FileUtils.getPathRelativeToDocs(imageFile) + imageInfo 611 | showStatus(text: "Image: \(imagePath)", type: .info) 612 | 613 | } else { 614 | // UI cue to downloading, spinning caret 615 | /* 616 | let path = Bundle.main.path(forResource: "spinner", ofType: "gif") 617 | let gif = URL(fileURLWithPath: path!) 618 | let image = NSImage(byReferencing: gif) 619 | mainImage.canDrawSubviewsIntoLayer = true 620 | mainImage.animates = true 621 | mainImage.image = image 622 | */ 623 | // download image 624 | let url = gallery.items[index].imageUrl 625 | DispatchQueue.main.async(execute: { 626 | self.showStatus("Downloading image: \(url)") 627 | self.spinner.isHidden = false 628 | self.spinner.startAnimation(self.view) 629 | }) 630 | 631 | downloadImage(url, gallery: gallery.name) 632 | } 633 | } 634 | 635 | func showImage(path: String) { 636 | showStatus(text: "Image: \(FileUtils.getPathRelativeToDocs(path))", type: .info) 637 | DispatchQueue.main.async(execute: { 638 | self.spinner.stopAnimation(self.view) 639 | self.spinner.isHidden = true 640 | }) 641 | 642 | if FileUtils.fileExists(path) { 643 | let fileExtension = (path as NSString).pathExtension 644 | if fileExtension == "gif" { 645 | print("GIF file") 646 | mainImage.canDrawSubviewsIntoLayer = true 647 | mainImage.animates = true 648 | } 649 | let image = NSImage(byReferencingFile: path) 650 | mainImage.image = image 651 | } else { 652 | showStatus(text: "Image not found: \(FileUtils.getPathRelativeToDocs(path))", type: .error) 653 | } 654 | } 655 | 656 | func zoomOut() { 657 | guard zoomValue>1 else { return } 658 | zoomValue -= 0.5 659 | let center = NSPoint(x: mainArea.bounds.width/2, y: mainArea.bounds.height/2) 660 | mainArea.setMagnification(CGFloat(zoomValue), centeredAt: center) 661 | } 662 | 663 | func zoomIn() { 664 | guard zoomValue<4 else { return } 665 | zoomValue += 0.5 666 | let center = NSPoint(x: mainArea.bounds.width/2, y: mainArea.bounds.height/2) 667 | mainArea.setMagnification(CGFloat(zoomValue), centeredAt: center) 668 | } 669 | 670 | func zoomToFit() { 671 | zoomValue = 1.0 672 | let center = NSPoint(x: mainArea.bounds.width/2, y: mainArea.bounds.height/2) 673 | mainArea.setMagnification(CGFloat(zoomValue), centeredAt: center) 674 | // TODO: zoomToFit not working 675 | // let rect = NSRect(x: 0, y: 0, width: 0, height: 0) 676 | // mainArea.magnify(toFit: rect) 677 | } 678 | 679 | func openFinder(file :String) { 680 | let url = URL(fileURLWithPath: file) 681 | let selected = [url] 682 | showStatus("Opening in Finder: \(FileUtils.getPathRelativeToDocs(file))") 683 | NSWorkspace.shared().activateFileViewerSelecting(selected) 684 | } 685 | 686 | func setAsWallpaper(file :String) { 687 | let space = NSWorkspace.shared() 688 | let screen = NSScreen.main() 689 | let url = URL(fileURLWithPath: file) 690 | showStatus("Setting as wallpaper: \(FileUtils.getPathRelativeToDocs(file))") 691 | do { 692 | try space.setDesktopImageURL(url, for: screen!) 693 | } catch { 694 | print(error) 695 | showStatus(text: "Error setting image as wallpaper", type: .warn) 696 | } 697 | } 698 | 699 | func deleteImage(file :String) { 700 | guard let selected = collectionView.selectionIndexes.first else { return } 701 | let thumbFile = gallery.items[selected].thumbPath 702 | let newName = "_" + gallery.items[selected].thumbName 703 | let newFile = FileUtils.getThumbnailPath(gallery: gallery.name, forFile: newName) 704 | gallery.items[selected].thumbName = "_"+gallery.items[selected].thumbName 705 | gallery.items[selected].imageName = "_"+gallery.items[selected].imageName 706 | FileUtils.deleteFile(file) 707 | FileUtils.renameFile(thumbFile, to: newFile) 708 | showDeletedImage() 709 | showStatus("Image has been deleted") 710 | } 711 | 712 | func showDeletedImage() { 713 | mainImage.image = NSImage(named: "deleted") 714 | } 715 | 716 | } 717 | 718 | 719 | //---- EXTENSIONS 720 | 721 | // NSVIEW 722 | extension NSView { 723 | var backgroundColor :NSColor? { 724 | get { 725 | if let colorRef = self.layer?.backgroundColor { 726 | return NSColor(cgColor: colorRef) 727 | } else { 728 | return nil 729 | } 730 | } 731 | set { 732 | self.wantsLayer = true 733 | self.layer?.backgroundColor = newValue?.cgColor 734 | } 735 | } 736 | } 737 | 738 | 739 | // THUMBNAILS COLLECTION 740 | 741 | extension ViewController : NSCollectionViewDataSource { 742 | func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { 743 | return gallery.items.count 744 | } 745 | 746 | func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { 747 | let item = collectionView.makeItem(withIdentifier: "ViewThumbnail", for: indexPath) 748 | 749 | guard let thumb = item as? ViewThumbnail else { return item } 750 | 751 | // IndexPath is a tuple (section, item) use the item number as index, section is zero for our list 752 | let folder = FileUtils.getThumbsFolder(gallery: gallery.name) 753 | let imageFile = folder.appendingPathComponent(gallery.items[indexPath.item].thumbName).path 754 | let image = NSImage(byReferencingFile: imageFile) 755 | 756 | thumb.imageFile = image 757 | 758 | var hilite = false 759 | let selectedIndex = collectionView.selectionIndexPaths.first 760 | if selectedIndex == indexPath { hilite = true } 761 | thumb.setHighlight(hilite) 762 | 763 | return item 764 | } 765 | } 766 | 767 | extension ViewController : NSCollectionViewDelegate { 768 | func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { 769 | 770 | guard let indexPath = indexPaths.first else { 771 | return 772 | } 773 | 774 | if autoDownload { 775 | showImage(index: indexPath.item) 776 | } 777 | 778 | guard let item = collectionView.item(at: indexPath) else { 779 | return 780 | } 781 | 782 | let thumb = (item as! ViewThumbnail) 783 | thumb.setHighlight(true) 784 | 785 | } 786 | 787 | func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set) { 788 | guard let indexPath = indexPaths.first else { return } 789 | guard let item = collectionView.item(at: indexPath) else { return } 790 | let thumb = (item as! ViewThumbnail) 791 | thumb.setHighlight(false) 792 | } 793 | } 794 | -------------------------------------------------------------------------------- /Imaginex/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | Default 585 | 586 | 587 | 588 | 589 | 590 | 591 | Left to Right 592 | 593 | 594 | 595 | 596 | 597 | 598 | Right to Left 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | Default 610 | 611 | 612 | 613 | 614 | 615 | 616 | Left to Right 617 | 618 | 619 | 620 | 621 | 622 | 623 | Right to Left 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 822 | 838 | 849 | 860 | 871 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 915 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 975 | 989 | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | Enter the name of the gallery as in Space, usually one word to identify it in the list of galleries, the first letter will be capitalized 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 1054 | 1055 | 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | 1063 | 1064 | 1065 | --------------------------------------------------------------------------------