├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------