diff --git a/CopyHistory/CopyHistory.xcodeproj/project.pbxproj b/CopyHistory/CopyHistory.xcodeproj/project.pbxproj index 54ebede..3acc7f8 100644 --- a/CopyHistory/CopyHistory.xcodeproj/project.pbxproj +++ b/CopyHistory/CopyHistory.xcodeproj/project.pbxproj @@ -271,7 +271,7 @@ CODE_SIGN_ENTITLEMENTS = CopyHistory/CopyHistory.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 17; DEVELOPMENT_TEAM = H6UU7923NK; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -285,7 +285,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.0.6; + MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = "jp.po-miyasaka.CopyHistory.CopyHistory"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -301,7 +301,7 @@ CODE_SIGN_ENTITLEMENTS = CopyHistory/CopyHistory.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 17; DEVELOPMENT_TEAM = H6UU7923NK; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -315,7 +315,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.0.6; + MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = "jp.po-miyasaka.CopyHistory.CopyHistory"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/CopyHistory/CopyHistory/AppDelegate.swift b/CopyHistory/CopyHistory/AppDelegate.swift index 0f545b7..598d35f 100644 --- a/CopyHistory/CopyHistory/AppDelegate.swift +++ b/CopyHistory/CopyHistory/AppDelegate.swift @@ -27,8 +27,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { statusBar = .init( ContentView(), width: 500, - height: 700, - image: NSImage(imageLiteralResourceName: "logo.svg") + height: NSScreen.main?.frame.height ?? 800.0, + image: NSImage(imageLiteralResourceName: "logo") ) } @@ -42,7 +42,7 @@ private final class StatusBarController: NSObject, NSPopoverDeleg var popover: NSPopover? var statusBarItem: NSStatusItem? - init(_: Content, width: Int, height: Int, image: NSImage) { + init(_: Content, width: CGFloat, height: CGFloat, image: NSImage) { super.init() let popover = NSPopover() popover.contentSize = NSSize(width: width, height: height) diff --git a/CopyHistory/CopyHistory/ContentView.swift b/CopyHistory/CopyHistory/ContentView.swift index 47a6832..ea7b68e 100644 --- a/CopyHistory/CopyHistory/ContentView.swift +++ b/CopyHistory/CopyHistory/ContentView.swift @@ -7,6 +7,7 @@ import StoreKit import SwiftUI +import WebKit struct ContentView: View { @StateObject var pasteboardService: PasteboardService = .build() @@ -14,6 +15,9 @@ struct ContentView: View { @FocusState var isFocus @State var isAlertPresented: Bool = false @State var focusedItemIndex: Int? + @AppStorage("isExpanded") var isExpanded: Bool = true + @AppStorage("isShowingRTF") var isShowingRTF: Bool = false + @AppStorage("isShowingHTML") var isShowingHTML: Bool = false let versionString: String = { let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String @@ -103,18 +107,25 @@ struct ContentView: View { Text("Up:") Text("Down:") Text("Select:") + Text("Delete:") Text("Star:") + Text(isExpanded ? "Minify cells:" : "Expand cells:") + Text(isShowingRTF ? "Stop Showing as RTF:" : "Show as RTF (slow):") + Text(isShowingHTML ? "Stop Showing as HTML:" : "Show as HTML (slow):") } VStack(alignment: .leading, spacing: 5) { Text("⌘ + ↑ or k") Text("⌘ + ↓ or j") Text("⌘ + ↩") + Text("⌘ + ⇧ + d") Text("⌘ + o") + Text("⌘ + e") + Text("⌘ + r") + Text("⌘ + a") } }.font(.caption) .foregroundColor(Color.gray) .padding(.bottom, 1) - } Spacer() @@ -143,19 +154,28 @@ struct ContentView: View { @ViewBuilder func MainView() -> some View { ScrollView { + Spacer() // there is a mysterious plain view at the top of the scrollview and it overlays this content. so this is put here ScrollViewReader { proxy in - ForEach(Array(zip(pasteboardService.copiedItems.indices, pasteboardService.copiedItems)), id: \.1.dataHash) { index, item in + VStack(spacing: 0) { // This doesn't make ScrollView + ForEach make additional padding + ForEach(Array(zip(pasteboardService.copiedItems.indices, pasteboardService.copiedItems)), id: \.1.dataHash) { index, item in - Row(item: item, - didSelected: { item in - focusedItemIndex = nil - pasteboardService.didSelected(item) - NSApplication.shared.deactivate() - }, - favoriteButtonDidTap: { item in pasteboardService.favoriteButtonDidTap(item) }, - deleteButtonDidTap: { item in pasteboardService.deleteButtonDidTap(item) }, - isFocused: index == focusedItemIndex).id(item.dataHash) + Row(item: item, + favorite: item.favorite, + didSelected: { item in + focusedItemIndex = nil + pasteboardService.didSelected(item) + NSApplication.shared.deactivate() + }, + favoriteButtonDidTap: { item in pasteboardService.favoriteButtonDidTap(item) }, + deleteButtonDidTap: { item in pasteboardService.deleteButtonDidTap(item) }, + isFocused: index == focusedItemIndex, + isExpanded: $isExpanded, + isShowingRTF: $isShowingRTF, + isShowingHTML: $isShowingHTML) + .id(item.dataHash) + } } + HStack { KeyboardCommandButtons(action: { scroll(proxy: proxy, direction: .down) }, keys: [.init(main: .downArrow, sub: .command), .init(main: "j", sub: .command)]) @@ -173,18 +193,38 @@ struct ContentView: View { }, keys: [.init(main: .return, sub: .command)]) + KeyboardCommandButtons(action: { + if let i = focusedItemIndex, pasteboardService.copiedItems.endIndex > i { + pasteboardService.deleteButtonDidTap(pasteboardService.copiedItems[i]) + } + + }, keys: [.init(main: "d", sub: .command.union(.shift))]).transaction { transaction in + transaction.animation = nil + } + KeyboardCommandButtons(action: { if let i = focusedItemIndex, pasteboardService.copiedItems.endIndex > i { pasteboardService.favoriteButtonDidTap(pasteboardService.copiedItems[i]) } }, keys: [.init(main: "o", sub: .command)]) + + KeyboardCommandButtons(action: { + isExpanded.toggle() + }, keys: [.init(main: "e", sub: .command)]) + + KeyboardCommandButtons(action: { + isShowingRTF.toggle() + }, keys: [.init(main: "r", sub: .command)]) + + KeyboardCommandButtons(action: { + isShowingHTML.toggle() + }, keys: [.init(main: "a", sub: .command)]) } + .opacity(0) .frame(width: .leastNonzeroMagnitude, height: .leastNonzeroMagnitude) - } - .padding(.horizontal) - .listStyle(.inset(alternatesRowBackgrounds: false)) + }.padding(.horizontal) } } @@ -193,35 +233,37 @@ struct ContentView: View { Group { HStack { Menu { - Button(action: { - isShowingKeyboardShortcuts.toggle() - }, label: { - Text(isShowingKeyboardShortcuts ? "Hide keyboard shortcuts" : "Show keyboard shortcuts") - }) - Divider() - Button(action: { - if let url = URL(string: "https://miyashi.app/articles/copy_history_mark_2_shortcut_launch") { - NSWorkspace.shared.open(url) - } - }, label: { - Text("About launching with a keyboard shortcut (open the Website)") - }) - Divider() - Button(action: { - if let url = URL(string: "https://miyashi.app/articles/copy_history_mark_2") { - NSWorkspace.shared.open(url) - } - }, label: { - Text("About CopyHistory (open the Website)") - }) - Divider() - Button(action: { - SKStoreReviewController.requestReview() - }, label: { - Text("Rate CopyHistory✨") - }) - Divider() + MenuItems(contents: [ + .init(text: isShowingKeyboardShortcuts ? "Hide keyboard shortcuts" : "Show keyboard shortcuts", action: { + isShowingKeyboardShortcuts.toggle() + }), + .init(text: isExpanded ? "Minify cells" : "Expand cells", action: { + isExpanded.toggle() + }), + .init(text: isShowingRTF ? "Stop Showing as RTF" : "Show as RTF (slow)", action: { + isShowingRTF.toggle() + }), + .init(text: isShowingHTML ? "Stop Showing as HTML" : "Show as HTML (slow)", action: { + isShowingKeyboardShortcuts.toggle() + }), + .init(text: "About launching with a keyboard shortcut (open the Website)", action: { + if let url = URL(string: "https://miyashi.app/articles/copy_history_mark_2_shortcut_launch") { + NSWorkspace.shared.open(url) + } + }), + .init(text: "About CopyHistory (open the Website)", action: { + if let url = URL(string: "https://miyashi.app/articles/copy_history_mark_2") { + NSWorkspace.shared.open(url) + } + }), + .init(text: "Rate CopyHistory✨", action: { + SKStoreReviewController.requestReview() + }), + + ]) + Text(versionString) + } label: { Image(systemName: "latch.2.case") .font(.title) @@ -252,6 +294,26 @@ struct ContentView: View { } } +struct MenuItems: View { + struct Content: Identifiable { + var id: String { text } + let text: String + let action: () -> Void + } + + let contents: [Content] + var body: some View { + ForEach(contents) { content in + Button(action: { + content.action() + }, label: { + Text(content.text) + }) + Divider() + } + } +} + struct KeyboardCommandButtons: View { struct Key: Identifiable { let id: UUID = .init() @@ -274,31 +336,64 @@ struct KeyboardCommandButtons: View { } } -struct Row: View { +struct Row: View, Equatable { let item: CopiedItem + let favorite: Bool let didSelected: (CopiedItem) -> Void let favoriteButtonDidTap: (CopiedItem) -> Void let deleteButtonDidTap: (CopiedItem) -> Void - let isFocused: Bool + var isFocused: Bool + @Binding var isExpanded: Bool // to render realtime, using @Binding + @Binding var isShowingRTF: Bool + @Binding var isShowingHTML: Bool var body: some View { VStack { HStack { + if isFocused { + Color.mainAccent.frame(width: 5, alignment: .leading) + } Button(action: { withAnimation { didSelected(item) } }, label: { - VStack { + ZStack { + Color.mainViewBackground + + // spreading Button's Taparea was very difficult , but ZStack + Color make it + // TODO: servey for alternative to Color + // + // + // https://stackoverflow.com/questions/57333573/swiftui-button-tap-only-on-text-portion + // https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-a-tappable-button + HStack { - Text(item.name ?? "No Name") - .font(.callout) - .foregroundColor(isFocused ? .mainAccent : .primary) + Group { + if let content = item.content, let image = NSImage(data: content) { + Image(nsImage: image).resizable().scaledToFit().frame(maxHeight: 300) + } else if isShowingRTF, item.contentTypeString?.contains("rtf") == true, let attributedString = item.attributeString { + Text(AttributedString(attributedString)) + + } else if isShowingHTML, item.contentTypeString?.contains("html") == true, let attributedString = item.htmlString { + Text(AttributedString(attributedString)) + } else if let url = item.fileURL { + // TODO: why images disappear after first + // if let image = NSImage(contentsOf: url) { + // Image(nsImage: image).resizable().scaledToFit() + // } else { + Text("\(url.absoluteString)") + .font(.callout) + // } + } else { + Text(item.name ?? "No Name").font(.callout) + } + }.padding(.vertical, 8).lineLimit(isExpanded ? 20 : 1) + Spacer() } } - .frame(minHeight: 44) - .contentShape(RoundedRectangle(cornerRadius: 20)) + }) VStack(alignment: .trailing) { @@ -308,9 +403,9 @@ struct Row: View { Button(action: { favoriteButtonDidTap(item) }, label: { - Image(systemName: item.favorite ? "star.fill" : "star") - .foregroundColor(item.favorite ? Color.mainAccent : Color.primary) - .frame(minHeight: 44) + Image(systemName: favorite ? "star.fill" : "star") + .foregroundColor(favorite ? Color.mainAccent : Color.primary) + .frame(width: 44, height: 44) .contentShape(RoundedRectangle(cornerRadius: 20)) }) .buttonStyle(PlainButtonStyle()) @@ -321,10 +416,15 @@ struct Row: View { }) .buttonStyle(PlainButtonStyle()) } - .frame(height: 30) - Divider().padding(EdgeInsets()) } .buttonStyle(PlainButtonStyle()) + Divider() + } + + static func == (lhs: Row, rhs: Row) -> Bool { + // This comparation make Row stop unneeded rendering. + return lhs.isFocused == rhs.isFocused && + lhs.favorite == rhs.favorite } } @@ -338,3 +438,35 @@ extension Color { static var mainViewBackground = Color("mainViewBackground") static var mainAccent = Color("AccentColor") } + +// wanna show big preview +// struct WebViewer: NSViewRepresentable { +// let contentString: String +// let content: Data +// +// init? (content: Data?) { +// guard let content = content, let contentString = String(data: content, encoding: .utf8) else { return nil } +// self.content = content +// self.contentString = contentString +// } +// +// func makeNSView(context _: Context) -> WKWebView { +// let view = WKWebView(frame: .zero) +// +// let html = " \(contentString)" +// let filePath = NSHomeDirectory() + "/Library/hoge.html" +// FileManager.default.createFile(atPath: filePath, contents: html.data(using: .utf8)) +// +// let localurl = URL(fileURLWithPath: filePath ) +// let allowAccess = URL(fileURLWithPath: NSHomeDirectory()) +// +// view.loadFileURL(localurl, allowingReadAccessTo: allowAccess) +// return view +// } +// +// func updateNSView(_ view: WKWebView, context _: Context) { +// +// } +// +// typealias NSViewType = WKWebView +// } diff --git a/CopyHistory/CopyHistory/CopyHistory.entitlements b/CopyHistory/CopyHistory/CopyHistory.entitlements index f2ef3ae..852fa1a 100644 --- a/CopyHistory/CopyHistory/CopyHistory.entitlements +++ b/CopyHistory/CopyHistory/CopyHistory.entitlements @@ -2,9 +2,7 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + diff --git a/CopyHistory/CopyHistory/PasteboardService.swift b/CopyHistory/CopyHistory/PasteboardService.swift index 130af21..92b4788 100644 --- a/CopyHistory/CopyHistory/PasteboardService.swift +++ b/CopyHistory/CopyHistory/PasteboardService.swift @@ -185,4 +185,26 @@ extension CopiedItem { if binarySize == 0 { return "-" } return Self.formatter.string(fromByteCount: binarySize) } + + var attributeString: NSAttributedString? { + guard let content = content else { return nil } + let attributeString = (try? NSAttributedString(data: content, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil)) + return attributeString + } + + var htmlString: NSAttributedString? { + guard let content = content else { return nil } + let attributeString = (try? NSAttributedString(data: content, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)) + + return attributeString + } + + var fileURL: URL? { + guard contentTypeString?.contains("file-url") == true, + let content = content, + let path = String(data: content, encoding: .utf8), + let url = URL(string: path) else { return nil } +// url.startAccessingSecurityScopedResource() + return url + } }