forked from DevToys-app/DevToysMac
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a65d40a
commit 8cb7b45
Showing
20 changed files
with
784 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
// | ||
// DateConverter.swift | ||
// DevToys | ||
// | ||
// Created by yuki on 2022/02/04. | ||
// | ||
|
||
import CoreUtil | ||
|
||
final class DateConverterViewController: PageViewController { | ||
private let cell = DateConverterView() | ||
|
||
@Observable var date = Date() | ||
|
||
private let isoFormatter = ISO8601DateFormatter() | ||
private let gmtFormatter = DateFormatter() => { | ||
$0.locale = Locale(identifier: "en_US_POSIX") | ||
$0.timeZone = TimeZone(abbreviation: "GMT") | ||
} | ||
|
||
override func loadView() { self.view = cell } | ||
|
||
override func viewDidLoad() { | ||
self.$date | ||
.sink{[unowned self] in | ||
self.cell.datePicker.date = $0 | ||
self.cell.unixTimeField.value = $0.timeIntervalSince1970.rounded() | ||
self.cell.isoDateField.string = isoFormatter.string(from: $0) | ||
self.cell.graphicDatePicker.dateValue = $0 | ||
} | ||
.store(in: &objectBag) | ||
|
||
self.cell.graphicDatePicker.actionPublisher | ||
.sink{[unowned self] in self.date = cell.graphicDatePicker.dateValue }.store(in: &objectBag) | ||
self.cell.nowButton.actionPublisher | ||
.sink{[unowned self] in self.date = Date() }.store(in: &objectBag) | ||
self.cell.datePicker.datePublisher | ||
.sink{[unowned self] in self.date = $0 }.store(in: &objectBag) | ||
self.cell.unixTimeField.valuePublisher | ||
.sink{[unowned self] in self.date = Date(timeIntervalSince1970: $0.reduce(self.date.timeIntervalSince1970)) }.store(in: &objectBag) | ||
self.cell.isoDateField.changeStringPublisher.compactMap{[unowned self] in isoFormatter.date(from: $0) } | ||
.sink{[unowned self] in self.date = $0 }.store(in: &objectBag) | ||
} | ||
} | ||
|
||
final private class DateConverterView: Page { | ||
|
||
let datePicker = DatePicker() | ||
let utcDatePicker = DatePicker() | ||
let nowButton = Button(title: "Now") | ||
let unixTimeField = NumberField() | ||
let isoDateField = TextField(showCopyButton: false) | ||
let graphicDatePicker = NSDatePicker() | ||
|
||
override func onAwake() { | ||
self.title = "Date Converter" | ||
|
||
self.addSection(Section(title: "Date", items: [ | ||
NSStackView() => { | ||
$0.distribution = .equalSpacing | ||
$0.addArrangedSubview(datePicker) | ||
$0.addArrangedSubview(nowButton) | ||
} | ||
])) | ||
self.datePicker.snp.remakeConstraints{ make in | ||
make.right.equalTo(nowButton.snp.left).inset(-8) | ||
} | ||
|
||
self.addSection(Section(title: "Unix Time", items: [unixTimeField])) | ||
self.unixTimeField.showStepper = false | ||
self.unixTimeField.snp.remakeConstraints{ make in | ||
make.height.equalTo(R.Size.controlHeight) | ||
} | ||
self.addSection(Section(title: "ISO 8601", items: [isoDateField])) | ||
|
||
self.addSection(Section(title: "Calender", items: [graphicDatePicker])) | ||
self.graphicDatePicker.datePickerStyle = .clockAndCalendar | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
// | ||
// XMLFormatter.swift | ||
// DevToys | ||
// | ||
// Created by yuki on 2022/02/04. | ||
// | ||
|
||
import CoreUtil | ||
|
||
final class XMLFormatterViewController: PageViewController { | ||
@RestorableState("xml.rawCode") private var rawCode: String = "" | ||
@RestorableState("xml.formattedCode") private var formattedCode: String = "" | ||
@RestorableState("xml.documentType") private var documentType: DocumentType = .xmlDocument | ||
@RestorableState("xml.pretty") private var pretty = true | ||
@RestorableState("xml.autofix") private var autofix = true | ||
|
||
private let cell = XMLFormatterView() | ||
|
||
override func loadView() { self.view = cell } | ||
|
||
override func viewDidLoad() { | ||
self.$rawCode | ||
.sink{[unowned self] in cell.inputSection.string = $0 }.store(in: &objectBag) | ||
self.$formattedCode | ||
.sink{[unowned self] in cell.outputSection.string = $0 }.store(in: &objectBag) | ||
self.$documentType | ||
.sink{[unowned self] in cell.documentTypeControl.selectedItem = $0 }.store(in: &objectBag) | ||
self.$pretty | ||
.sink{[unowned self] in cell.prettySwitch.isOn = $0 }.store(in: &objectBag) | ||
self.$autofix | ||
.sink{[unowned self] in cell.autoFixSwitch.isOn = $0 }.store(in: &objectBag) | ||
|
||
self.cell.prettySwitch.isOnPublisher | ||
.sink{[unowned self] in self.pretty = $0; updateFormattedCode() }.store(in: &objectBag) | ||
self.cell.autoFixSwitch.isOnPublisher | ||
.sink{[unowned self] in self.autofix = $0; updateFormattedCode() }.store(in: &objectBag) | ||
self.cell.documentTypeControl.itemPublisher | ||
.sink{[unowned self] in self.documentType = $0; updateFormattedCode() }.store(in: &objectBag) | ||
self.cell.inputSection.stringPublisher | ||
.sink{[unowned self] in self.rawCode = $0; updateFormattedCode() }.store(in: &objectBag) | ||
|
||
self.updateFormattedCode() | ||
} | ||
|
||
private func updateFormattedCode() { | ||
|
||
do { | ||
var options = XMLNode.Options() | ||
|
||
if pretty { | ||
options.insert(.nodePrettyPrint) | ||
} | ||
if autofix { | ||
switch documentType { | ||
case .htmlDocument: options.insert(.documentTidyHTML) | ||
case .xmlDocument: options.insert(.documentTidyXML) | ||
} | ||
} | ||
|
||
let document = try XMLDocument(xmlString: rawCode, options: options) | ||
|
||
self.formattedCode = document.xmlString(options: options) | ||
.replacingOccurrences(of: #"<?xml version="1.0" encoding="UTF-8"?>"#, with: "") | ||
.replacingOccurrences(of: #"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">"#, with: "") | ||
.replacingOccurrences(of: #"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">"#, with: "") | ||
.replacingOccurrences(of: #"<html xmlns="http://www.w3.org/1999/xhtml">"#, with: "<html>") | ||
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) | ||
|
||
} catch { | ||
self.formattedCode = "[Invalid Document]" | ||
} | ||
} | ||
} | ||
|
||
private enum DocumentType: String, TextItem { | ||
case htmlDocument = "HTML Document" | ||
case xmlDocument = "XML Document" | ||
|
||
var title: String { rawValue } | ||
} | ||
|
||
final private class XMLFormatterView: Page { | ||
let prettySwitch = NSSwitch() | ||
let autoFixSwitch = NSSwitch() | ||
let documentTypeControl = EnumPopupButton<DocumentType>() | ||
let inputSection = CodeViewSection(title: "Input", options: .defaultInput, language: .xml) | ||
let outputSection = CodeViewSection(title: "Output", options: .defaultOutput, language: .xml) | ||
|
||
private lazy var configurationSection = Section(title: "Configuration", items: [ | ||
Area(icon: R.Image.convert, title: "Document Type", control: documentTypeControl), | ||
NSStackView() => { | ||
$0.orientation = .horizontal | ||
$0.distribution = .fillEqually | ||
$0.addArrangedSubview(Area(icon: R.Image.format, title: "Auto Fix Document", control: autoFixSwitch)) | ||
$0.addArrangedSubview(Area(icon: R.Image.format, title: "Pretty Document", control: prettySwitch)) | ||
} | ||
]) | ||
|
||
private lazy var ioStack = self.addSection2(inputSection, outputSection) | ||
|
||
override func layout() { | ||
super.layout() | ||
|
||
self.ioStack.snp.remakeConstraints{ make in | ||
make.height.equalTo(max(240, self.frame.height - 160)) | ||
} | ||
} | ||
|
||
override func onAwake() { | ||
self.title = "XML Formatter" | ||
self.addSection(configurationSection) | ||
} | ||
} |
123 changes: 123 additions & 0 deletions
123
DevToys/DevToys/Body/Media/Image Converter/ImageConverter.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
// | ||
// ImageConverter.swift | ||
// DevToys | ||
// | ||
// Created by yuki on 2022/02/04. | ||
// | ||
|
||
import CoreUtil | ||
|
||
struct ImageConvertTask { | ||
let image: NSImage | ||
let title: String | ||
let size: CGSize | ||
let isDone: Promise<Void, Error> | ||
} | ||
|
||
enum ImageConverter { | ||
private static let destinationDirectory = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask)[0].appendingPathComponent("Converted Images") => { | ||
try? FileManager.default.createDirectory(at: $0, withIntermediateDirectories: true, attributes: nil) | ||
} | ||
|
||
static func convert(_ item: ImageItem, format: ImageFormatType, resize: Bool, size: CGSize, scale: ImageScaleMode, padding: Bool) -> ImageConvertTask { | ||
var resizeSize = item.image.size | ||
if resize { | ||
// switch scale { | ||
// case <#pattern#>: | ||
// <#code#> | ||
// default: | ||
// <#code#> | ||
// } | ||
} | ||
|
||
// item.image.resized(to: <#T##NSSize#>) | ||
let isDone = Promise<Data, Error>.asyncError{ resolve, reject in | ||
switch format { | ||
case .png: | ||
guard let data = item.image.png else { return reject("Data failed.") }; resolve(data) | ||
case .jpg: | ||
guard let data = item.image.jpeg else { return reject("Data failed.") }; resolve(data) | ||
case .tiff: | ||
guard let data = item.image.tiffRepresentation else { return reject("Data failed.") }; resolve(data) | ||
case .gif: | ||
guard let data = item.image.gif else { return reject("Data failed.") }; resolve(data) | ||
} | ||
} | ||
.tryPeek{ data in | ||
let url = destinationDirectory.appendingPathComponent("\(item.title).\(format.exp)") | ||
try data.write(to: url) | ||
} | ||
.receive(on: .main) | ||
.eraseToVoid() | ||
|
||
return ImageConvertTask(image: item.image, title: item.title, size: item.image.size, isDone: isDone) | ||
} | ||
} | ||
|
||
extension ImageFormatType { | ||
var exp: String { | ||
switch self { | ||
case .png: return "png" | ||
case .jpg: return "jpg" | ||
case .gif: return "gif" | ||
case .tiff: return "tiff" | ||
} | ||
} | ||
} | ||
|
||
|
||
extension NSImage { | ||
public var png: Data? { self.data(for: .png) } | ||
public var jpeg: Data? { self.data(for: .jpeg) } | ||
public var gif: Data? { self.data(for: .gif) } | ||
|
||
public convenience init(cgImage: CGImage) { self.init(cgImage: cgImage, size: cgImage.size) } | ||
|
||
public func data(for fileType: NSBitmapImageRep.FileType, properties: [NSBitmapImageRep.PropertyKey : Any] = [:]) -> Data? { | ||
guard | ||
let tiffRepresentation = self.tiffRepresentation, | ||
let bitmap = NSBitmapImageRep(data: tiffRepresentation), | ||
let rep = bitmap.representation(using: fileType, properties: properties) | ||
else { return nil } | ||
|
||
return rep | ||
} | ||
} | ||
|
||
extension CGImage { | ||
public var size: CGSize { CGSize(width: self.width, height: self.height) } | ||
} | ||
|
||
extension Promise { | ||
public func tryPeek(_ receiveOutput: @escaping (Output) throws -> ()) -> Promise<Output, Error> { | ||
Promise<Output, Error> { resolve, reject in | ||
self.sink({ output in | ||
do { try receiveOutput(output); resolve(output) } catch { reject(error) } | ||
}, reject) | ||
} | ||
} | ||
} | ||
|
||
extension NSImage { | ||
var pngData: Data { png! } | ||
|
||
func resized(to newSize: NSSize) -> NSImage { | ||
if let bitmapRep = NSBitmapImageRep( | ||
bitmapDataPlanes: nil, pixelsWide: Int(newSize.width), pixelsHigh: Int(newSize.height), | ||
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, | ||
colorSpaceName: .calibratedRGB, bytesPerRow: 0, bitsPerPixel: 0 | ||
) { | ||
bitmapRep.size = newSize | ||
NSGraphicsContext.saveGraphicsState() | ||
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: bitmapRep) | ||
draw(in: NSRect(x: 0, y: 0, width: newSize.width, height: newSize.height), from: .zero, operation: .copy, fraction: 1.0) | ||
NSGraphicsContext.restoreGraphicsState() | ||
|
||
let resizedImage = NSImage(size: newSize) | ||
resizedImage.addRepresentation(bitmapRep) | ||
return resizedImage | ||
} | ||
|
||
fatalError() | ||
} | ||
} |
Oops, something went wrong.