├── .gitignore ├── Icon ├── 1024BS.png └── 256BS.png ├── Test Files ├── 1.png ├── 2.png ├── 3.png ├── 4.png └── 5.png ├── imageviewer5 ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 256.png │ │ ├── 32.png │ │ ├── 512.png │ │ ├── 64.png │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── imageviewer5.entitlements ├── Credits.rtf ├── Info.plist ├── ContentView.swift ├── Base.lproj │ └── Main.storyboard └── AppDelegate.swift ├── imageviewer5.xcodeproj ├── xcuserdata │ └── djs.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.xcuserstate 3 | *.xcworkspace 4 | -------------------------------------------------------------------------------- /Icon/1024BS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/Icon/1024BS.png -------------------------------------------------------------------------------- /Icon/256BS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/Icon/256BS.png -------------------------------------------------------------------------------- /Test Files/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/Test Files/1.png -------------------------------------------------------------------------------- /Test Files/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/Test Files/2.png -------------------------------------------------------------------------------- /Test Files/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/Test Files/3.png -------------------------------------------------------------------------------- /Test Files/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/Test Files/4.png -------------------------------------------------------------------------------- /Test Files/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/Test Files/5.png -------------------------------------------------------------------------------- /imageviewer5/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /imageviewer5/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /imageviewer5/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/imageviewer5/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /imageviewer5/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/imageviewer5/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /imageviewer5/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/imageviewer5/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /imageviewer5/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/imageviewer5/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /imageviewer5/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/imageviewer5/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /imageviewer5/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/imageviewer5/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /imageviewer5/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdan/imageviewer5/HEAD/imageviewer5/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /imageviewer5/imageviewer5.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /imageviewer5.xcodeproj/xcuserdata/djs.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | imageviewer5.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /imageviewer5/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2513 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \paperw11900\paperh16840\margl1440\margr1440\vieww12600\viewh7800\viewkind0 6 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 7 | {\field{\*\fldinst{HYPERLINK "https://github.com/lambdan/imageviewer5"}}{\fldrslt 8 | \f0\fs24 \cf0 https://github.com/lambdan/imageviewer5}}} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 djs/lambdan.se 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /imageviewer5/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Screenshot 2021-06-08 at 20 39 11](https://user-images.githubusercontent.com/1690265/121239530-a80e6800-c899-11eb-9470-5884591a5408.png) 2 | 3 | When I switched to macOS, I missed [Irfanview](https://www.irfanview.com) from Windows. I wanted a Mac app to quickly go through images in a folder by hitting left and right arrow keys, and also be able to copy the images to paste in another folder or in a document. 4 | 5 | Thinking about it, it seemed like a pretty simple app to make and a great opportunity to dip my toes in Swift and Swift UI, so I did! 6 | 7 | # Features 8 | 9 | - Simple and fast (mostly thanks to Swift UI) 10 | - Navigate images in a folder by hitting left/right arrow 11 | - Copy image as... 12 | - ...an image (⌘C): Useful for pasting in a Finder window or into a Document 13 | - ...a file path (⌥⌘C): useful for pasting into a Terminal window 14 | - Can show some basic information (filename, resolution, filesize) about image 15 | - Trash (⌫) or Delete (⌥⌘⌫) files 16 | - Multiple ways to open images: 17 | - Drag n drop into window 18 | - Drag n drop onto icon 19 | - File > Open... menu 20 | - Right click image > Open With 21 | - Command line: `open -a /Applications/imageviewer5.app /path/to/image.jpg` 22 | 23 | # Download 24 | 25 | - Download from the [Releases](https://github.com/lambdan/imageviewer5/releases) page 26 | - Minimum macOS version is 11.0 27 | - Because I am not a registered developer your Mac will probably scream at you when you try to open it 28 | - Feel free to compile/build it yourself to avoid this 29 | 30 | # Known Issues 31 | 32 | See the [Issues](https://github.com/lambdan/imageviewer5/issues) page. Most annoying issue right now is probably that GIF images are displayed, but not animated ([#5](https://github.com/lambdan/imageviewer5/issues/5)). 33 | -------------------------------------------------------------------------------- /imageviewer5/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeIconFiles 11 | 12 | CFBundleTypeName 13 | Images 14 | CFBundleTypeRole 15 | Viewer 16 | LSHandlerRank 17 | Alternate 18 | LSItemContentTypes 19 | 20 | public.image 21 | 22 | 23 | 24 | CFBundleExecutable 25 | $(EXECUTABLE_NAME) 26 | CFBundleIconFile 27 | 28 | CFBundleIdentifier 29 | $(PRODUCT_BUNDLE_IDENTIFIER) 30 | CFBundleInfoDictionaryVersion 31 | 6.0 32 | CFBundleName 33 | $(PRODUCT_NAME) 34 | CFBundlePackageType 35 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 36 | CFBundleShortVersionString 37 | $(MARKETING_VERSION) 38 | CFBundleVersion 39 | 470 40 | LSApplicationCategoryType 41 | public.app-category.photography 42 | LSMinimumSystemVersion 43 | $(MACOSX_DEPLOYMENT_TARGET) 44 | NSHumanReadableCopyright 45 | Copyright © 2020-2021 djs. All rights reserved. 46 | NSMainStoryboardFile 47 | Main 48 | NSPrincipalClass 49 | NSApplication 50 | NSSupportsAutomaticTermination 51 | 52 | NSSupportsSuddenTermination 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /imageviewer5/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import AppKit 3 | import Combine 4 | import Foundation 5 | 6 | //var screen_width = NSScreen.main?.frame.width // Current display max width 7 | //var screen_height = NSScreen.main?.frame.height // and height 8 | 9 | 10 | struct ContentView: View, DropDelegate { 11 | 12 | // TODO should also implement validateDrop function to validate file extension 13 | func performDrop(info: DropInfo) -> Bool { 14 | guard let itemProvider = info.itemProviders(for: [(kUTTypeFileURL as String)]).first else { return false } 15 | itemProvider.loadItem(forTypeIdentifier: (kUTTypeFileURL as String), options: nil) { (data,error) in 16 | let url = URL(dataRepresentation: data as! Data, relativeTo: nil)! 17 | if HandledFileExtensions.contains(url.pathExtension) { 18 | DispatchQueue.main.async { 19 | set_new_url(in_url: url.absoluteString) 20 | send_NC(text: "Image drag n dropped") 21 | } 22 | } 23 | } 24 | return true 25 | } 26 | 27 | func dropEntered(info: DropInfo) { 28 | // TODO show that frame around like other apps 29 | //self.bgcolor = Color(.systemOrange) 30 | } 31 | 32 | func dropExited(info: DropInfo) { 33 | //self.bgcolor = Color(.systemBlue) 34 | } 35 | 36 | @State var url_string = get_URL_String() 37 | @State var imgW = CGFloat(get_IMG_Size(axis: "Width")) 38 | @State var imgH = CGFloat(get_IMG_Size(axis: "Height")) 39 | @State var showInfoBar = UD.bool(forKey: "Show Info Bar") 40 | @State var InfoBar_Text_Name = InfoBar_Name 41 | @State var InfoBar_Text_Format = InfoBar_Format 42 | @State var InfoBar_Text_FileSize = InfoBar_FileSize 43 | @State var InfoBar_Text_Misc = InfoBar_Misc 44 | //@State var bgcolor = Color(.systemBlue) 45 | 46 | 47 | let pub = NotificationCenter.default.publisher(for: NSNotification.Name(NCName)) 48 | 49 | var body: some View { 50 | VStack { 51 | if url_string == "" { 52 | Text("No image loaded").fixedSize().padding(50) 53 | } else { 54 | Image(nsImage: NSImage(contentsOf: URL(string:url_string)!)!).resizable().aspectRatio(contentMode: .fit) 55 | } 56 | 57 | if self.showInfoBar == true && url_string != "" { 58 | Spacer() 59 | HStack { 60 | Text(self.InfoBar_Text_Name).bold().help(Text(get_full_filepath())) 61 | Text(self.InfoBar_Text_Format) 62 | Text(self.InfoBar_Text_FileSize) 63 | Text(self.InfoBar_Text_Misc) 64 | }.padding() 65 | } 66 | 67 | 68 | } 69 | .onReceive(pub) {_ in 70 | // This gets run when notification is sent 71 | self.url_string = get_URL_String() 72 | self.imgW = CGFloat(get_IMG_Size(axis: "Width")) 73 | self.imgH = CGFloat(get_IMG_Size(axis: "Height")) 74 | self.showInfoBar = UD.bool(forKey: "Show Info Bar") 75 | self.InfoBar_Text_Name = InfoBar_Name 76 | self.InfoBar_Text_Format = InfoBar_Format 77 | self.InfoBar_Text_FileSize = InfoBar_FileSize 78 | self.InfoBar_Text_Misc = InfoBar_Misc 79 | } 80 | 81 | 82 | .frame(minWidth: 400, idealWidth: imgW, maxWidth: NSScreen.main?.frame.width, minHeight: 300, idealHeight: imgH, maxHeight: NSScreen.main?.frame.height) 83 | //.background(Rectangle().fill(bgcolor)) 84 | .onDrop(of: [(kUTTypeFileURL as String)], delegate: self) 85 | } 86 | } 87 | 88 | struct PrefsView: View { 89 | @State private var ShowIndexInTitle = UD.bool(forKey: "Show Index In Title") 90 | @State private var ShowNameInTitle = UD.bool(forKey: "Show Name In Title") 91 | @State private var ShowResolutionInTitle = UD.bool(forKey: "Show Resolution In Title") 92 | @State private var RememberLastSession = UD.bool(forKey: "Remember Last Session Image") 93 | @State private var StatusBarEnabled = UD.bool(forKey: "Show Info Bar") 94 | 95 | let pub = NotificationCenter.default.publisher(for: NSNotification.Name(NCName)) 96 | 97 | var body: some View { 98 | VStack { 99 | Form { 100 | 101 | Section() { 102 | Toggle("Remember Last Picture", isOn: $RememberLastSession).onChange(of: RememberLastSession) { newvalue in 103 | UD.setValue(newvalue, forKey: "Remember Last Session Image") 104 | SettingsUpdated() 105 | }.help("Automatically re-open the picture you last viewed the next time you open the app") 106 | 107 | Toggle("Info Bar", isOn: $StatusBarEnabled).onChange(of: StatusBarEnabled) { newvalue in 108 | ToggleInfoBar() 109 | }.help("Show information about the picture below it") 110 | 111 | 112 | } 113 | 114 | 115 | Divider() 116 | 117 | Section(header:Text("Window Title Preferences:")) { 118 | 119 | Toggle("Show Index", isOn: $ShowIndexInTitle).onChange(of: ShowIndexInTitle) { newvalue in 120 | UD.setValue(newvalue, forKey: "Show Index In Title") 121 | SettingsUpdated() 122 | }.help("[1/23]") 123 | 124 | Toggle("Show Name", isOn: $ShowNameInTitle).onChange(of: ShowNameInTitle) { newvalue in 125 | UD.setValue(newvalue, forKey: "Show Name In Title") 126 | SettingsUpdated() 127 | }.help("File Name.jpg") 128 | 129 | Toggle("Show Resolution", isOn: $ShowResolutionInTitle).onChange(of: ShowResolutionInTitle) { newvalue in 130 | UD.setValue(newvalue, forKey: "Show Resolution In Title") 131 | SettingsUpdated() 132 | }.help("1920x1080") 133 | 134 | } 135 | 136 | } 137 | 138 | } 139 | .onReceive(pub) {_ in 140 | 141 | } 142 | .padding().frame(minWidth: 300, minHeight: 300) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /imageviewer5/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | CA 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | CA 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /imageviewer5.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6A1A3888249EC2A000389BB0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A1A3887249EC2A000389BB0 /* AppDelegate.swift */; }; 11 | 6A1A388A249EC2A000389BB0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A1A3889249EC2A000389BB0 /* ContentView.swift */; }; 12 | 6A1A388C249EC2A200389BB0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A1A388B249EC2A200389BB0 /* Assets.xcassets */; }; 13 | 6A1A388F249EC2A200389BB0 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A1A388E249EC2A200389BB0 /* Preview Assets.xcassets */; }; 14 | 6A1A3892249EC2A200389BB0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6A1A3890249EC2A200389BB0 /* Main.storyboard */; }; 15 | 6AED24C324B764110000595D /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 6AED24C224B764110000595D /* Credits.rtf */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 6A1A3884249EC2A000389BB0 /* imageviewer5.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = imageviewer5.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 6A1A3887249EC2A000389BB0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 21 | 6A1A3889249EC2A000389BB0 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 22 | 6A1A388B249EC2A200389BB0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23 | 6A1A388E249EC2A200389BB0 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 24 | 6A1A3891249EC2A200389BB0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 25 | 6A1A3893249EC2A200389BB0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 26 | 6A1A3894249EC2A200389BB0 /* imageviewer5.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = imageviewer5.entitlements; sourceTree = ""; }; 27 | 6AED24C224B764110000595D /* Credits.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 28 | /* End PBXFileReference section */ 29 | 30 | /* Begin PBXFrameworksBuildPhase section */ 31 | 6A1A3881249EC2A000389BB0 /* Frameworks */ = { 32 | isa = PBXFrameworksBuildPhase; 33 | buildActionMask = 2147483647; 34 | files = ( 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | 6A1A387B249EC2A000389BB0 = { 42 | isa = PBXGroup; 43 | children = ( 44 | 6A1A3886249EC2A000389BB0 /* imageviewer5 */, 45 | 6A1A3885249EC2A000389BB0 /* Products */, 46 | ); 47 | sourceTree = ""; 48 | }; 49 | 6A1A3885249EC2A000389BB0 /* Products */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 6A1A3884249EC2A000389BB0 /* imageviewer5.app */, 53 | ); 54 | name = Products; 55 | sourceTree = ""; 56 | }; 57 | 6A1A3886249EC2A000389BB0 /* imageviewer5 */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 6AED24C224B764110000595D /* Credits.rtf */, 61 | 6A1A3887249EC2A000389BB0 /* AppDelegate.swift */, 62 | 6A1A3889249EC2A000389BB0 /* ContentView.swift */, 63 | 6A1A388B249EC2A200389BB0 /* Assets.xcassets */, 64 | 6A1A3890249EC2A200389BB0 /* Main.storyboard */, 65 | 6A1A3893249EC2A200389BB0 /* Info.plist */, 66 | 6A1A3894249EC2A200389BB0 /* imageviewer5.entitlements */, 67 | 6A1A388D249EC2A200389BB0 /* Preview Content */, 68 | ); 69 | path = imageviewer5; 70 | sourceTree = ""; 71 | }; 72 | 6A1A388D249EC2A200389BB0 /* Preview Content */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 6A1A388E249EC2A200389BB0 /* Preview Assets.xcassets */, 76 | ); 77 | path = "Preview Content"; 78 | sourceTree = ""; 79 | }; 80 | /* End PBXGroup section */ 81 | 82 | /* Begin PBXNativeTarget section */ 83 | 6A1A3883249EC2A000389BB0 /* imageviewer5 */ = { 84 | isa = PBXNativeTarget; 85 | buildConfigurationList = 6A1A3897249EC2A200389BB0 /* Build configuration list for PBXNativeTarget "imageviewer5" */; 86 | buildPhases = ( 87 | 6A1A3880249EC2A000389BB0 /* Sources */, 88 | 6A1A3881249EC2A000389BB0 /* Frameworks */, 89 | 6A1A3882249EC2A000389BB0 /* Resources */, 90 | 6AED24C424B764560000595D /* Auto-increment build number */, 91 | ); 92 | buildRules = ( 93 | ); 94 | dependencies = ( 95 | ); 96 | name = imageviewer5; 97 | productName = imageviewer5; 98 | productReference = 6A1A3884249EC2A000389BB0 /* imageviewer5.app */; 99 | productType = "com.apple.product-type.application"; 100 | }; 101 | /* End PBXNativeTarget section */ 102 | 103 | /* Begin PBXProject section */ 104 | 6A1A387C249EC2A000389BB0 /* Project object */ = { 105 | isa = PBXProject; 106 | attributes = { 107 | LastSwiftUpdateCheck = 1150; 108 | LastUpgradeCheck = 1150; 109 | ORGANIZATIONNAME = djs; 110 | TargetAttributes = { 111 | 6A1A3883249EC2A000389BB0 = { 112 | CreatedOnToolsVersion = 11.5; 113 | }; 114 | }; 115 | }; 116 | buildConfigurationList = 6A1A387F249EC2A000389BB0 /* Build configuration list for PBXProject "imageviewer5" */; 117 | compatibilityVersion = "Xcode 9.3"; 118 | developmentRegion = en; 119 | hasScannedForEncodings = 0; 120 | knownRegions = ( 121 | en, 122 | Base, 123 | ); 124 | mainGroup = 6A1A387B249EC2A000389BB0; 125 | productRefGroup = 6A1A3885249EC2A000389BB0 /* Products */; 126 | projectDirPath = ""; 127 | projectRoot = ""; 128 | targets = ( 129 | 6A1A3883249EC2A000389BB0 /* imageviewer5 */, 130 | ); 131 | }; 132 | /* End PBXProject section */ 133 | 134 | /* Begin PBXResourcesBuildPhase section */ 135 | 6A1A3882249EC2A000389BB0 /* Resources */ = { 136 | isa = PBXResourcesBuildPhase; 137 | buildActionMask = 2147483647; 138 | files = ( 139 | 6A1A3892249EC2A200389BB0 /* Main.storyboard in Resources */, 140 | 6A1A388F249EC2A200389BB0 /* Preview Assets.xcassets in Resources */, 141 | 6A1A388C249EC2A200389BB0 /* Assets.xcassets in Resources */, 142 | 6AED24C324B764110000595D /* Credits.rtf in Resources */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | /* End PBXResourcesBuildPhase section */ 147 | 148 | /* Begin PBXShellScriptBuildPhase section */ 149 | 6AED24C424B764560000595D /* Auto-increment build number */ = { 150 | isa = PBXShellScriptBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | ); 154 | inputFileListPaths = ( 155 | ); 156 | inputPaths = ( 157 | ); 158 | name = "Auto-increment build number"; 159 | outputFileListPaths = ( 160 | ); 161 | outputPaths = ( 162 | ); 163 | runOnlyForDeploymentPostprocessing = 0; 164 | shellPath = /bin/sh; 165 | shellScript = "buildNumber=$(/usr/libexec/PlistBuddy -c \"Print CFBundleVersion\" \"${PROJECT_DIR}/${INFOPLIST_FILE}\")\nbuildNumber=$(($buildNumber + 1))\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $buildNumber\" \"${PROJECT_DIR}/${INFOPLIST_FILE}\"\n"; 166 | }; 167 | /* End PBXShellScriptBuildPhase section */ 168 | 169 | /* Begin PBXSourcesBuildPhase section */ 170 | 6A1A3880249EC2A000389BB0 /* Sources */ = { 171 | isa = PBXSourcesBuildPhase; 172 | buildActionMask = 2147483647; 173 | files = ( 174 | 6A1A388A249EC2A000389BB0 /* ContentView.swift in Sources */, 175 | 6A1A3888249EC2A000389BB0 /* AppDelegate.swift in Sources */, 176 | ); 177 | runOnlyForDeploymentPostprocessing = 0; 178 | }; 179 | /* End PBXSourcesBuildPhase section */ 180 | 181 | /* Begin PBXVariantGroup section */ 182 | 6A1A3890249EC2A200389BB0 /* Main.storyboard */ = { 183 | isa = PBXVariantGroup; 184 | children = ( 185 | 6A1A3891249EC2A200389BB0 /* Base */, 186 | ); 187 | name = Main.storyboard; 188 | sourceTree = ""; 189 | }; 190 | /* End PBXVariantGroup section */ 191 | 192 | /* Begin XCBuildConfiguration section */ 193 | 6A1A3895249EC2A200389BB0 /* Debug */ = { 194 | isa = XCBuildConfiguration; 195 | buildSettings = { 196 | ALWAYS_SEARCH_USER_PATHS = NO; 197 | CLANG_ANALYZER_NONNULL = YES; 198 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 199 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 200 | CLANG_CXX_LIBRARY = "libc++"; 201 | CLANG_ENABLE_MODULES = YES; 202 | CLANG_ENABLE_OBJC_ARC = YES; 203 | CLANG_ENABLE_OBJC_WEAK = YES; 204 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 205 | CLANG_WARN_BOOL_CONVERSION = YES; 206 | CLANG_WARN_COMMA = YES; 207 | CLANG_WARN_CONSTANT_CONVERSION = YES; 208 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 209 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 210 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 211 | CLANG_WARN_EMPTY_BODY = YES; 212 | CLANG_WARN_ENUM_CONVERSION = YES; 213 | CLANG_WARN_INFINITE_RECURSION = YES; 214 | CLANG_WARN_INT_CONVERSION = YES; 215 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 217 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 218 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 219 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 220 | CLANG_WARN_STRICT_PROTOTYPES = YES; 221 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 222 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 223 | CLANG_WARN_UNREACHABLE_CODE = YES; 224 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 225 | COPY_PHASE_STRIP = NO; 226 | DEBUG_INFORMATION_FORMAT = dwarf; 227 | ENABLE_STRICT_OBJC_MSGSEND = YES; 228 | ENABLE_TESTABILITY = YES; 229 | GCC_C_LANGUAGE_STANDARD = gnu11; 230 | GCC_DYNAMIC_NO_PIC = NO; 231 | GCC_NO_COMMON_BLOCKS = YES; 232 | GCC_OPTIMIZATION_LEVEL = 0; 233 | GCC_PREPROCESSOR_DEFINITIONS = ( 234 | "DEBUG=1", 235 | "$(inherited)", 236 | ); 237 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 238 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 239 | GCC_WARN_UNDECLARED_SELECTOR = YES; 240 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 241 | GCC_WARN_UNUSED_FUNCTION = YES; 242 | GCC_WARN_UNUSED_VARIABLE = YES; 243 | MACOSX_DEPLOYMENT_TARGET = 10.15; 244 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 245 | MTL_FAST_MATH = YES; 246 | ONLY_ACTIVE_ARCH = YES; 247 | SDKROOT = macosx; 248 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 249 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 250 | }; 251 | name = Debug; 252 | }; 253 | 6A1A3896249EC2A200389BB0 /* Release */ = { 254 | isa = XCBuildConfiguration; 255 | buildSettings = { 256 | ALWAYS_SEARCH_USER_PATHS = NO; 257 | CLANG_ANALYZER_NONNULL = YES; 258 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 259 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 260 | CLANG_CXX_LIBRARY = "libc++"; 261 | CLANG_ENABLE_MODULES = YES; 262 | CLANG_ENABLE_OBJC_ARC = YES; 263 | CLANG_ENABLE_OBJC_WEAK = YES; 264 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 265 | CLANG_WARN_BOOL_CONVERSION = YES; 266 | CLANG_WARN_COMMA = YES; 267 | CLANG_WARN_CONSTANT_CONVERSION = YES; 268 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 269 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 270 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 271 | CLANG_WARN_EMPTY_BODY = YES; 272 | CLANG_WARN_ENUM_CONVERSION = YES; 273 | CLANG_WARN_INFINITE_RECURSION = YES; 274 | CLANG_WARN_INT_CONVERSION = YES; 275 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 276 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 277 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 278 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 279 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 280 | CLANG_WARN_STRICT_PROTOTYPES = YES; 281 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 282 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 283 | CLANG_WARN_UNREACHABLE_CODE = YES; 284 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 285 | COPY_PHASE_STRIP = NO; 286 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 287 | ENABLE_NS_ASSERTIONS = NO; 288 | ENABLE_STRICT_OBJC_MSGSEND = YES; 289 | GCC_C_LANGUAGE_STANDARD = gnu11; 290 | GCC_NO_COMMON_BLOCKS = YES; 291 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 292 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 293 | GCC_WARN_UNDECLARED_SELECTOR = YES; 294 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 295 | GCC_WARN_UNUSED_FUNCTION = YES; 296 | GCC_WARN_UNUSED_VARIABLE = YES; 297 | MACOSX_DEPLOYMENT_TARGET = 10.15; 298 | MTL_ENABLE_DEBUG_INFO = NO; 299 | MTL_FAST_MATH = YES; 300 | SDKROOT = macosx; 301 | SWIFT_COMPILATION_MODE = wholemodule; 302 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 303 | }; 304 | name = Release; 305 | }; 306 | 6A1A3898249EC2A200389BB0 /* Debug */ = { 307 | isa = XCBuildConfiguration; 308 | buildSettings = { 309 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 310 | CODE_SIGN_ENTITLEMENTS = imageviewer5/imageviewer5.entitlements; 311 | CODE_SIGN_STYLE = Automatic; 312 | COMBINE_HIDPI_IMAGES = YES; 313 | CURRENT_PROJECT_VERSION = 5; 314 | DEVELOPMENT_ASSET_PATHS = "\"imageviewer5/Preview Content\""; 315 | ENABLE_PREVIEWS = YES; 316 | INFOPLIST_FILE = imageviewer5/Info.plist; 317 | LD_RUNPATH_SEARCH_PATHS = ( 318 | "$(inherited)", 319 | "@executable_path/../Frameworks", 320 | ); 321 | MACOSX_DEPLOYMENT_TARGET = 11.0; 322 | MARKETING_VERSION = 1.6.1; 323 | PRODUCT_BUNDLE_IDENTIFIER = djs.imageviewer5; 324 | PRODUCT_NAME = "$(TARGET_NAME)"; 325 | SWIFT_VERSION = 5.0; 326 | }; 327 | name = Debug; 328 | }; 329 | 6A1A3899249EC2A200389BB0 /* Release */ = { 330 | isa = XCBuildConfiguration; 331 | buildSettings = { 332 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 333 | CODE_SIGN_ENTITLEMENTS = imageviewer5/imageviewer5.entitlements; 334 | CODE_SIGN_STYLE = Automatic; 335 | COMBINE_HIDPI_IMAGES = YES; 336 | CURRENT_PROJECT_VERSION = 5; 337 | DEVELOPMENT_ASSET_PATHS = "\"imageviewer5/Preview Content\""; 338 | ENABLE_PREVIEWS = YES; 339 | INFOPLIST_FILE = imageviewer5/Info.plist; 340 | LD_RUNPATH_SEARCH_PATHS = ( 341 | "$(inherited)", 342 | "@executable_path/../Frameworks", 343 | ); 344 | MACOSX_DEPLOYMENT_TARGET = 11.0; 345 | MARKETING_VERSION = 1.6.1; 346 | PRODUCT_BUNDLE_IDENTIFIER = djs.imageviewer5; 347 | PRODUCT_NAME = "$(TARGET_NAME)"; 348 | SWIFT_VERSION = 5.0; 349 | }; 350 | name = Release; 351 | }; 352 | /* End XCBuildConfiguration section */ 353 | 354 | /* Begin XCConfigurationList section */ 355 | 6A1A387F249EC2A000389BB0 /* Build configuration list for PBXProject "imageviewer5" */ = { 356 | isa = XCConfigurationList; 357 | buildConfigurations = ( 358 | 6A1A3895249EC2A200389BB0 /* Debug */, 359 | 6A1A3896249EC2A200389BB0 /* Release */, 360 | ); 361 | defaultConfigurationIsVisible = 0; 362 | defaultConfigurationName = Release; 363 | }; 364 | 6A1A3897249EC2A200389BB0 /* Build configuration list for PBXNativeTarget "imageviewer5" */ = { 365 | isa = XCConfigurationList; 366 | buildConfigurations = ( 367 | 6A1A3898249EC2A200389BB0 /* Debug */, 368 | 6A1A3899249EC2A200389BB0 /* Release */, 369 | ); 370 | defaultConfigurationIsVisible = 0; 371 | defaultConfigurationName = Release; 372 | }; 373 | /* End XCConfigurationList section */ 374 | }; 375 | rootObject = 6A1A387C249EC2A000389BB0 /* Project object */; 376 | } 377 | -------------------------------------------------------------------------------- /imageviewer5/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SwiftUI 3 | 4 | // Variables 5 | var DefaultWindowTitle = "imageviewer5" // Shown in the titlebar of the window when no image is loaded 6 | var window_spawned = false // Is the window created? 7 | 8 | var current_url = "" // file:// URL to the current image we are showing 9 | var current_image_width = 0 // width of current image in pixels 10 | var current_image_height = 0 // height -"- 11 | 12 | var current_folder = "" // file:// URL to the current folder we are showing images from 13 | var files_in_folder = [String]() // This array will hold images we can display 14 | 15 | var currentImageIndex = 0 // Which image in the array we are showing 16 | var totalFilesInFolder = 0 // How many images in the folder that we can show 17 | 18 | // Infobar vars 19 | var InfoBar_Name = "(Filename goes here)" 20 | var InfoBar_Format = "(Resolution etc goes here)" 21 | var InfoBar_FileSize = "(Filesize goes here)" 22 | var InfoBar_Misc = "(Misc stuff goes here)" 23 | 24 | 25 | let UD = UserDefaults.standard 26 | 27 | // 28 | // Main code... 29 | // 30 | 31 | let NCName = "ImageViewer_NC" 32 | let mainmenu = NSApplication.shared.mainMenu! 33 | let subMenu = mainmenu.item(withTitle: "File")?.submenu 34 | let InfoBar_MenuItem = mainmenu.item(withTitle: "Window")?.submenu?.item(withTitle: "Info Bar") 35 | let defaultWindowTitle = "imageviewer5" 36 | var window: NSWindow! 37 | 38 | let HandledFileExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "heif", "heic", "tif", "webp", "tiff"] // File extensions we can handle 39 | 40 | @NSApplicationMain 41 | class AppDelegate: NSObject, NSApplicationDelegate { 42 | 43 | 44 | // Right click open with 45 | private func application(_ sender: NSApplication, openFiles filenames: String) -> Bool { 46 | set_new_url(in_url: URL(fileURLWithPath: filenames).absoluteString) 47 | send_NC(text: "Opened via Open With") 48 | return true 49 | } 50 | 51 | // Drag and drop onto icon 52 | func application(_ sender: NSApplication, openFile filename: String) -> Bool { 53 | set_new_url(in_url: URL(fileURLWithPath: filename).absoluteString) 54 | send_NC(text: "Opened via drag and drop onto icon") 55 | return true 56 | } 57 | 58 | // Quit when all windows closed 59 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 60 | return true 61 | } 62 | 63 | @IBAction func loadFile(_sender: NSMenuItem) { 64 | let panel = NSOpenPanel() // maybe make this its own function so we can use it elsewhere too 65 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 66 | let result = panel.runModal() 67 | if result == .OK { 68 | set_new_url(in_url: panel.url!.absoluteString) 69 | send_NC(text: "Image via menubar > open") 70 | } 71 | } 72 | } 73 | 74 | @IBAction func nextFile(_sender: NSMenuItem) { 75 | //print("Pressed next") 76 | NextPic(inc: 1) 77 | } 78 | 79 | @IBAction func prevFile(_sender: NSMenuItem) { 80 | //print("Pressed prev") 81 | NextPic(inc: -1) 82 | } 83 | 84 | @IBAction func openPrefs(_sender: NSMenuItem) { 85 | OpenPrefsWindow() 86 | } 87 | 88 | @IBAction func copyFilePath(_sender: NSMenuItem) { 89 | // copy file path to image 90 | //print("copying path:", get_full_filepath()) 91 | let pb = NSPasteboard.general 92 | pb.clearContents() 93 | pb.setString(get_full_filepath(), forType: .string) 94 | } 95 | 96 | @IBAction func copyAsFile(_sender: NSMenuItem) { 97 | // copy as image, used for pasting in finder or in documents etc 98 | let pb = NSPasteboard.general 99 | pb.clearContents() 100 | pb.writeObjects(NSArray(object: URL(string: get_URL_String())!) as! [NSPasteboardWriting]) // TODO understand this line better, i have no idea why it works 101 | } 102 | 103 | @IBAction func deleteFile(_sender: NSMenuItem) { 104 | let fileManager = FileManager() 105 | do { 106 | try fileManager.removeItem(atPath: get_full_filepath()) 107 | 108 | if files_in_folder.count > 1 { // this wasnt the last image so just move to the next one 109 | current_folder = "" // need to do this to refresh the folder check.. TODO maybe just remove the deleted file from the array instead? 110 | NextPic(inc: 1) 111 | } else { // no files left in folder 112 | reset_everything() 113 | } 114 | } 115 | catch { 116 | //TODO probably need to do more here 117 | print("Error deleting file") 118 | } 119 | } 120 | 121 | @IBAction func trashFile(_sender: NSMenuItem) { 122 | let fileManager = FileManager() 123 | do { 124 | try fileManager.trashItem(at: URL(string: get_URL_String())!, resultingItemURL: nil) 125 | //TODO catch the resultingItemURL to add Undo functionality 126 | 127 | if files_in_folder.count > 1 { // this wasnt the last image so just move to the next one 128 | current_folder = "" // need to do this to refresh the folder check.. TODO maybe just remove the deleted file from the array instead? 129 | NextPic(inc: 1) 130 | } else { // no files left in folder 131 | reset_everything() 132 | } 133 | } 134 | catch { 135 | //TODO probably need to do more here 136 | print("Error trashing file") 137 | } 138 | } 139 | 140 | @IBAction func toggleInfoBar(_sender: NSMenuItem) { 141 | ToggleInfoBar() 142 | } 143 | 144 | 145 | func applicationDidFinishLaunching(_ aNotification: Notification) { 146 | loadUserDefaults() 147 | 148 | if UD.string(forKey: "Last Session Image") != "NULL" { 149 | // Make sure file exists 150 | let filePath = URL(string: UD.string(forKey: "Last Session Image")!)!.path 151 | let fileManager = FileManager.default 152 | 153 | if fileManager.fileExists(atPath: filePath) { // It exists 154 | set_new_url(in_url: UD.string(forKey: "Last Session Image")!) 155 | send_NC(text: "Loaded last session") 156 | } else { 157 | print("Image from last session was not found!") 158 | } 159 | } 160 | 161 | if current_url == "" { // No image loaded, spawn a blank window with the "No image loaded" text 162 | spawn_window() 163 | } 164 | 165 | if UD.bool(forKey: "Show Info Bar") == true { 166 | InfoBar_MenuItem?.state = .on 167 | } else { 168 | InfoBar_MenuItem?.state = .off 169 | } 170 | 171 | } 172 | 173 | func applicationWillTerminate(_ aNotification: Notification) { 174 | // Insert code here to tear down your application 175 | } 176 | 177 | 178 | } 179 | 180 | func spawn_window() { 181 | // Create the SwiftUI view that provides the window contents. 182 | let contentView = ContentView() 183 | 184 | // Create the window and set the content view. 185 | window = NSWindow( 186 | contentRect: NSRect(), 187 | styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], 188 | backing: .buffered, defer: false) 189 | 190 | //contentView.frame(minWidth: CGFloat(current_image_width), minHeight:CGFloat(current_image_height)) 191 | window.title = DefaultWindowTitle 192 | window.center() 193 | window.setFrameAutosaveName("Main Window") 194 | window.contentView = NSHostingView(rootView: contentView) 195 | window.makeKeyAndOrderFront(true) 196 | 197 | window_spawned = true 198 | 199 | } 200 | 201 | func set_new_url(in_url: String) { // This is the main thing. It gets called whenever we change picture. 202 | // TODO check if image is valid etc 203 | 204 | if window_spawned == false { 205 | spawn_window() 206 | } 207 | //window.makeKeyAndOrderFront(true) 208 | 209 | // Set all variables etc 210 | current_url = in_url 211 | check_image_folder(file_url_string: in_url) 212 | currentImageIndex = files_in_folder.firstIndex(of: current_url)! 213 | totalFilesInFolder = files_in_folder.count 214 | 215 | // Get image resolution, https://stackoverflow.com/a/13228091 216 | let img = NSImageRep(contentsOf: URL(string: current_url)!) 217 | current_image_width = Int( img!.pixelsWide ) 218 | current_image_height = Int( img!.pixelsHigh ) 219 | 220 | // Set up the window title 221 | var WindowTitle = "" 222 | 223 | if UD.bool(forKey: "Show Index In Title") == true { // First the index at the start... 224 | let TitlebarImageIndex = currentImageIndex + 1 // because 0/x looks weird 225 | WindowTitle = WindowTitle + "[" + String(TitlebarImageIndex) + "/" + String(totalFilesInFolder) + "] " 226 | } 227 | 228 | if UD.bool(forKey: "Show Name In Title") == true { 229 | WindowTitle = WindowTitle + get_filename() // ...filename in the middle... 230 | } 231 | 232 | if UD.bool(forKey: "Show Resolution In Title") == true { // And then the resolution at the end 233 | WindowTitle = WindowTitle + " (" + String(current_image_width) + "x" + String(current_image_height) + ")" 234 | } 235 | 236 | if WindowTitle == "" { 237 | WindowTitle = DefaultWindowTitle 238 | } 239 | 240 | window.title = WindowTitle 241 | 242 | // Generate text for the infobar 243 | InfoBar_Name = get_filename() 244 | InfoBar_Format = String(current_image_width) + "x" + String(current_image_height) // TODO show color depth etc too 245 | let TitlebarImageIndex = currentImageIndex + 1 // because 0/x looks weird 246 | InfoBar_Misc = String(TitlebarImageIndex) + "/" + String(totalFilesInFolder) 247 | InfoBar_FileSize = get_human_size(bytes: get_file_size(urlString: current_url)) 248 | 249 | 250 | // Enable menu items since we now should have a image loaded 251 | subMenu?.item(withTitle: "Next")?.isEnabled = true 252 | subMenu?.item(withTitle: "Previous")?.isEnabled = true 253 | subMenu?.item(withTitle: "Copy Image")?.isEnabled = true 254 | subMenu?.item(withTitle: "Copy Path to Image")?.isEnabled = true 255 | subMenu?.item(withTitle: "Delete")?.isEnabled = true // TODO enable this only if file exists 256 | subMenu?.item(withTitle: "Trash")?.isEnabled = true 257 | 258 | // Update last session image 259 | if UD.bool(forKey: "Remember Last Session Image") == true { 260 | UD.setValue(current_url, forKey: "Last Session Image") 261 | } else { 262 | UD.setValue("NULL", forKey: "Last Session Image") 263 | } 264 | 265 | } 266 | 267 | func get_URL_String() -> String { /* file://path/to/picture.jpg */ 268 | return current_url 269 | } 270 | 271 | func get_IMG_Size(axis: String) -> Int { 272 | if axis == "Width" { 273 | return current_image_width 274 | } 275 | if axis == "Height" { 276 | return current_image_height 277 | } 278 | return 0 279 | } 280 | 281 | func get_full_filepath() -> String { /* /path/to/picture.jpg */ 282 | let fp = String ( ((NSURL(string: get_URL_String())?.path)!)) 283 | return fp 284 | } 285 | 286 | func get_filename() -> String { /* picture.jpg */ 287 | let fn = String( (NSURL(string: get_URL_String())?.lastPathComponent)! ) 288 | return fn 289 | } 290 | 291 | func get_file_size(urlString: String) -> Int { 292 | let local_url = URL(string: urlString) 293 | do { 294 | let resources = try local_url!.resourceValues(forKeys:[.fileSizeKey]) 295 | let fileSize = resources.fileSize! 296 | return fileSize 297 | } catch { 298 | return 0 299 | } 300 | } 301 | 302 | func get_human_size(bytes: Int) -> String { 303 | // https://stackoverflow.com/a/42723243 304 | // get_human_size(bytes: get_file_size(urlString: current_url)) 305 | let bcf = ByteCountFormatter() 306 | //bcf.allowedUnits = [.useMB, .useKB] // optional: restricts the units to MB and KB only 307 | bcf.countStyle = .file 308 | bcf.isAdaptive = true 309 | let string = bcf.string(fromByteCount: Int64(bytes)) 310 | return string 311 | } 312 | 313 | func get_folder() -> String { /* /path/to/ */ 314 | let NSs = (get_URL_String() as NSString) 315 | let folderpath = NSs.deletingLastPathComponent 316 | let fp = String ( ((NSURL(string: folderpath)?.path)!)) 317 | return fp 318 | } 319 | 320 | func check_image_folder(file_url_string: String) { 321 | //print("Check image folder", file_url) 322 | let NSs = (file_url_string as NSString) 323 | 324 | //let filename = file_url_string 325 | let folderpath = NSs.deletingLastPathComponent 326 | //print(folderpath) 327 | 328 | 329 | if current_folder != folderpath { // Check if same folder so we dont re-do it 330 | current_folder = folderpath 331 | files_in_folder = [String]() 332 | // Check for files in folder 333 | do { 334 | let content = try FileManager.default.contentsOfDirectory(at: URL(string:folderpath)!, includingPropertiesForKeys: nil) 335 | //print("Files in folder:", content.count) // contains hidden files etc 336 | for file in content { 337 | if HandledFileExtensions.contains(file.pathExtension.lowercased()) { 338 | files_in_folder.append(file.absoluteString) 339 | } 340 | } 341 | 342 | files_in_folder.sort() // TODO maybe add sorting options? 343 | 344 | //print("Files to handle:", files_in_folder.count) 345 | } catch { 346 | print ("error numerating folder") 347 | } 348 | 349 | } 350 | 351 | } 352 | 353 | func NextPic(inc: Int) { 354 | //print("Current picture is", files_in_folder[curr!]) 355 | var next = currentImageIndex + inc 356 | 357 | if next >= totalFilesInFolder { 358 | // we've gone through all pictures in the folder... lets go to the first one! 359 | next = 0 // TODO show some fancy loop indication here 360 | } else if next < 0 { 361 | // we've gone through all pictures in the folder, but backwards... lets go to the last one! 362 | next = totalFilesInFolder - 1 363 | } 364 | //print("Next is", files_in_folder[next]) 365 | let next_url = files_in_folder[next] 366 | set_new_url(in_url: next_url) 367 | send_NC(text: "Next pic") 368 | } 369 | 370 | func send_NC(text: String) { 371 | //print("NC:", text) 372 | NotificationCenter.default.post(name: NSNotification.Name(rawValue: NCName), object: nil) 373 | } 374 | 375 | func reset_everything() { 376 | window.title = defaultWindowTitle 377 | 378 | current_url = "" // file:// URL to the current image we are showing 379 | current_image_width = 0 // width of current image in pixels 380 | current_image_height = 0 // height -"- 381 | 382 | current_folder = "" // file:// URL to the current folder we are showing images from 383 | files_in_folder = [String]() // This array will hold images we can display 384 | 385 | currentImageIndex = 0 // Which image in the array we are showing 386 | totalFilesInFolder = 0 // How many images in the folder that we can show 387 | 388 | // Disable menu items again 389 | subMenu?.item(withTitle: "Next")?.isEnabled = false 390 | subMenu?.item(withTitle: "Previous")?.isEnabled = false 391 | subMenu?.item(withTitle: "Copy Image")?.isEnabled = false 392 | subMenu?.item(withTitle: "Copy Path to Image")?.isEnabled = false 393 | subMenu?.item(withTitle: "Delete")?.isEnabled = false 394 | subMenu?.item(withTitle: "Trash")?.isEnabled = false 395 | 396 | // Tell window to refresh 397 | send_NC(text: "Reset") 398 | } 399 | 400 | func OpenPrefsWindow() { 401 | var windowRef: NSWindow 402 | windowRef = NSWindow( 403 | contentRect: NSRect(x: 100, y: 100, width: 100, height: 100), 404 | styleMask: [.titled, .closable], 405 | backing: .buffered, defer: false) 406 | windowRef.title = "Preferences" 407 | windowRef.center() 408 | windowRef.contentView = NSHostingView(rootView: PrefsView()) 409 | windowRef.makeKeyAndOrderFront(windowRef) 410 | NSApp.activate(ignoringOtherApps: true) 411 | windowRef.isReleasedWhenClosed = false 412 | } 413 | 414 | func isKeyPresentInUserDefaults(key: String) -> Bool { // https://smartcodezone.com/check-if-key-is-exists-in-userdefaults-in-swift/ 415 | return UD.object(forKey: key) != nil 416 | } 417 | 418 | func loadUserDefaults() { 419 | if isKeyPresentInUserDefaults(key: "Show Index In Title") == false { 420 | UD.set(false, forKey: "Show Index In Title") 421 | } 422 | 423 | if isKeyPresentInUserDefaults(key: "Show Name In Title") == false { 424 | UD.set(true, forKey: "Show Name In Title") 425 | } 426 | 427 | if isKeyPresentInUserDefaults(key: "Show Resolution In Title") == false { 428 | UD.set(false, forKey: "Show Resolution In Title") 429 | } 430 | 431 | if isKeyPresentInUserDefaults(key: "Show Info Bar") == false { 432 | UD.set(false, forKey: "Show Info Bar") 433 | } 434 | 435 | // Last session vars 436 | if isKeyPresentInUserDefaults(key: "Remember Last Session Image") == false { 437 | UD.set(true, forKey: "Remember Last Session Image") 438 | } 439 | if isKeyPresentInUserDefaults(key: "Last Session Image") == false { 440 | UD.set("NULL", forKey: "Last Session Image") 441 | } 442 | } 443 | 444 | func SettingsUpdated() { 445 | print("Settings updated") 446 | if current_url != "" { 447 | set_new_url(in_url: current_url) 448 | } 449 | } 450 | 451 | func ToggleInfoBar() { 452 | var local_InfoBarState = UD.bool(forKey: "Show Info Bar") 453 | 454 | if local_InfoBarState == false { 455 | // Enabled infobar 456 | local_InfoBarState = true 457 | InfoBar_MenuItem?.state = .on 458 | } else { 459 | local_InfoBarState = false 460 | InfoBar_MenuItem?.state = .off 461 | } 462 | 463 | UD.set(local_InfoBarState, forKey: "Show Info Bar") 464 | send_NC(text: "info bar toggled") 465 | } 466 | --------------------------------------------------------------------------------