├── 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 | 
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 |
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 |
912 |
913 |
914 |
915 |
916 |
917 |
918 |
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 |
--------------------------------------------------------------------------------