-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
OSX GUI Application
- Loading branch information
Showing
23 changed files
with
1,391 additions
and
11 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// | ||
// AppDelegate.swift | ||
// LoveLiver-osx | ||
// | ||
// Created by BAN Jun on 2016/02/08. | ||
// Copyright © 2016 mzp. All rights reserved. | ||
// | ||
|
||
import Cocoa | ||
import AVFoundation | ||
import AVKit | ||
import NorthLayout | ||
import Ikemen | ||
|
||
|
||
@NSApplicationMain | ||
class AppDelegate: NSObject, NSApplicationDelegate { | ||
func applicationOpenUntitledFile(sender: NSApplication) -> Bool { | ||
openDocument(sender) | ||
return true | ||
} | ||
|
||
@objc private func openDocument(sender: AnyObject?) { | ||
NSDocumentController.sharedDocumentController().openDocument(sender) | ||
} | ||
} | ||
|
68 changes: 68 additions & 0 deletions
68
LoveLiver-osx/Assets.xcassets/AppIcon.appiconset/Contents.json
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,68 @@ | ||
{ | ||
"images" : [ | ||
{ | ||
"size" : "16x16", | ||
"idiom" : "mac", | ||
"filename" : "icon16.png", | ||
"scale" : "1x" | ||
}, | ||
{ | ||
"size" : "16x16", | ||
"idiom" : "mac", | ||
"filename" : "[email protected]", | ||
"scale" : "2x" | ||
}, | ||
{ | ||
"size" : "32x32", | ||
"idiom" : "mac", | ||
"filename" : "icon32.png", | ||
"scale" : "1x" | ||
}, | ||
{ | ||
"size" : "32x32", | ||
"idiom" : "mac", | ||
"filename" : "[email protected]", | ||
"scale" : "2x" | ||
}, | ||
{ | ||
"size" : "128x128", | ||
"idiom" : "mac", | ||
"filename" : "icon128.png", | ||
"scale" : "1x" | ||
}, | ||
{ | ||
"size" : "128x128", | ||
"idiom" : "mac", | ||
"filename" : "[email protected]", | ||
"scale" : "2x" | ||
}, | ||
{ | ||
"size" : "256x256", | ||
"idiom" : "mac", | ||
"filename" : "icon256.png", | ||
"scale" : "1x" | ||
}, | ||
{ | ||
"size" : "256x256", | ||
"idiom" : "mac", | ||
"filename" : "[email protected]", | ||
"scale" : "2x" | ||
}, | ||
{ | ||
"size" : "512x512", | ||
"idiom" : "mac", | ||
"filename" : "icon512.png", | ||
"scale" : "1x" | ||
}, | ||
{ | ||
"size" : "512x512", | ||
"idiom" : "mac", | ||
"filename" : "[email protected]", | ||
"scale" : "2x" | ||
} | ||
], | ||
"info" : { | ||
"version" : 1, | ||
"author" : "xcode" | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,6 @@ | ||
{ | ||
"info" : { | ||
"version" : 1, | ||
"author" : "xcode" | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>CFBundleDevelopmentRegion</key> | ||
<string>en</string> | ||
<key>CFBundleDocumentTypes</key> | ||
<array> | ||
<dict> | ||
<key>CFBundleTypeExtensions</key> | ||
<array/> | ||
<key>CFBundleTypeMIMETypes</key> | ||
<array/> | ||
<key>CFBundleTypeName</key> | ||
<string>Movie</string> | ||
<key>CFBundleTypeRole</key> | ||
<string>Viewer</string> | ||
<key>LSItemContentTypes</key> | ||
<array> | ||
<string>public.movie</string> | ||
</array> | ||
<key>LSTypeIsPackage</key> | ||
<integer>0</integer> | ||
<key>NSDocumentClass</key> | ||
<string>LoveLiver.MovieDocument</string> | ||
</dict> | ||
</array> | ||
<key>CFBundleExecutable</key> | ||
<string>$(EXECUTABLE_NAME)</string> | ||
<key>CFBundleIconFile</key> | ||
<string></string> | ||
<key>CFBundleIdentifier</key> | ||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> | ||
<key>CFBundleInfoDictionaryVersion</key> | ||
<string>6.0</string> | ||
<key>CFBundleName</key> | ||
<string>$(PRODUCT_NAME)</string> | ||
<key>CFBundlePackageType</key> | ||
<string>APPL</string> | ||
<key>CFBundleShortVersionString</key> | ||
<string>1.0.0</string> | ||
<key>CFBundleSignature</key> | ||
<string>????</string> | ||
<key>CFBundleVersion</key> | ||
<string>1</string> | ||
<key>LSMinimumSystemVersion</key> | ||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string> | ||
<key>NSHumanReadableCopyright</key> | ||
<string>Copyright © 2016 mzp. All rights reserved.</string> | ||
<key>NSMainNibFile</key> | ||
<string>MainMenu</string> | ||
<key>NSPrincipalClass</key> | ||
<string>NSApplication</string> | ||
</dict> | ||
</plist> |
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,23 @@ | ||
// | ||
// MovieDocument.swift | ||
// LoveLiver | ||
// | ||
// Created by BAN Jun on 2016/02/09. | ||
// Copyright © 2016 mzp. All rights reserved. | ||
// | ||
|
||
import Cocoa | ||
|
||
|
||
class MovieDocument: NSDocument { | ||
override func readFromURL(url: NSURL, ofType typeName: String) throws { | ||
NSLog("%@", "opening \(url)") | ||
} | ||
|
||
override func makeWindowControllers() { | ||
let vc = MovieDocumentViewController(movieURL: fileURL!) | ||
let window = NSWindow(contentViewController: vc) | ||
let wc = NSWindowController(window: window) | ||
addWindowController(wc) | ||
} | ||
} |
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,195 @@ | ||
// | ||
// MovieDocumentViewController.swift | ||
// LoveLiver | ||
// | ||
// Created by BAN Jun on 2016/02/09. | ||
// Copyright © 2016 mzp. All rights reserved. | ||
// | ||
|
||
import Cocoa | ||
import AVFoundation | ||
import AVKit | ||
import NorthLayout | ||
import Ikemen | ||
|
||
|
||
private let outputDir = NSURL(fileURLWithPath: NSHomeDirectory()).URLByAppendingPathComponent("Pictures/LoveLiver") | ||
|
||
|
||
class MovieDocumentViewController: NSViewController { | ||
private let player: AVPlayer | ||
private let playerItem: AVPlayerItem | ||
private let imageGenerator: AVAssetImageGenerator | ||
private var exportSession: AVAssetExportSession? | ||
private var posterFrameTime: CMTime? | ||
|
||
private let playerView: AVPlayerView = AVPlayerView() ※ { v in | ||
v.controlsStyle = .Floating | ||
v.showsFrameSteppingButtons = true | ||
} | ||
private let posterFrameView = NSImageView() ※ { v in | ||
v.imageScaling = .ScaleProportionallyUpOrDown | ||
v.wantsLayer = true | ||
v.layer?.backgroundColor = NSColor.blackColor().CGColor | ||
v.setContentCompressionResistancePriority(NSLayoutPriorityFittingSizeCompression, forOrientation: .Horizontal) | ||
v.setContentCompressionResistancePriority(NSLayoutPriorityFittingSizeCompression, forOrientation: .Vertical) | ||
} | ||
private lazy var posterFrameButton: NSButton = NSButton() ※ { b in | ||
b.title = "Poster Frame ->>" | ||
b.setButtonType(.MomentaryLightButton) | ||
b.bezelStyle = .RoundedBezelStyle | ||
b.target = self | ||
b.action = "capturePosterFrame:" | ||
} | ||
|
||
private lazy var positionsLabel: NSTextField = NSTextField() ※ { tf in | ||
tf.bezeled = false | ||
tf.editable = false | ||
tf.drawsBackground = false | ||
tf.textColor = NSColor.grayColor() | ||
} | ||
|
||
private lazy var createLivePhotoButton: NSButton = NSButton() ※ { b in | ||
b.title = "Create Live Photo" | ||
b.setButtonType(.MomentaryLightButton) | ||
b.bezelStyle = .RoundedBezelStyle | ||
b.target = self | ||
b.action = "createLivePhoto:" | ||
} | ||
|
||
init!(movieURL: NSURL) { | ||
playerItem = AVPlayerItem(URL: movieURL) | ||
player = AVPlayer(playerItem: playerItem) | ||
playerView.player = player | ||
imageGenerator = AVAssetImageGenerator(asset: playerItem.asset) ※ { | ||
$0.requestedTimeToleranceBefore = kCMTimeZero | ||
$0.requestedTimeToleranceAfter = kCMTimeZero | ||
} | ||
super.init(nibName: nil, bundle: nil) | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
override func loadView() { | ||
view = NSView(frame: NSRect(x: 0, y: 0, width: 500, height: 500)) | ||
|
||
let autolayout = view.northLayoutFormat(["p": 20], [ | ||
"player": playerView, | ||
"posterButton": posterFrameButton, | ||
"posterView": posterFrameView, | ||
"createLivePhoto": createLivePhotoButton, | ||
"positionsLabel": positionsLabel, | ||
]) | ||
autolayout("H:|-p-[player]-p-[posterView(==player)]-p-|") | ||
autolayout("H:|-p-[posterButton(==player)]-p-[createLivePhoto(<=player)]-p-|") | ||
autolayout("H:[positionsLabel(==createLivePhoto)]-p-|") | ||
autolayout("V:|-p-[player]-p-[posterButton]") | ||
autolayout("V:|-p-[posterView]-p-[posterButton]") | ||
autolayout("V:[posterButton]-p-|") | ||
autolayout("V:[posterView][positionsLabel][createLivePhoto]-p-|") | ||
|
||
setupAspectRatioConstraints() | ||
updateViews() | ||
} | ||
|
||
private func setupAspectRatioConstraints() { | ||
// wait until movie is loaded | ||
guard playerView.videoBounds.width > 0 && playerView.videoBounds.height > 0 else { | ||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { | ||
self.setupAspectRatioConstraints() | ||
} | ||
return | ||
} | ||
|
||
self.playerView.addConstraint(NSLayoutConstraint( | ||
item: self.playerView, attribute: .Width, relatedBy: .Equal, | ||
toItem: self.playerView, attribute: .Height, multiplier: self.playerView.videoBounds.width / self.playerView.videoBounds.height, constant: 0)) | ||
} | ||
|
||
private func updateViews() { | ||
createLivePhotoButton.enabled = (posterFrameView.image != nil && exportSession == nil) | ||
positionsLabel.stringValue = positionsLabelText | ||
} | ||
|
||
private var positionsLabelText: String { | ||
guard let time = posterFrameTime else { return "" } | ||
let duration = CMTimeGetSeconds(time) | ||
let minutes = Int(floor(duration / 60)) | ||
let seconds = Int(floor(duration - Double(minutes) * 60)) | ||
let milliseconds = Int((duration - floor(duration)) * 100) | ||
let timeString = String(format: "%02d:%02d.%02d", minutes, seconds, milliseconds) | ||
return "Poster Frame: \(timeString)" | ||
} | ||
|
||
@objc private func capturePosterFrame(sender: AnyObject?) { | ||
guard let cgImage = try? imageGenerator.copyCGImageAtTime(player.currentTime(), actualTime: nil) else { return } | ||
let image = NSImage(CGImage: cgImage, size: CGSize(width: CGImageGetWidth(cgImage), height: CGImageGetHeight(cgImage))) | ||
posterFrameView.image = image | ||
posterFrameTime = player.currentTime() | ||
updateViews() | ||
} | ||
|
||
@objc private func createLivePhoto(sender: AnyObject?) { | ||
guard let image = posterFrameView.image else { return } | ||
|
||
guard let _ = try? NSFileManager.defaultManager().createDirectoryAtPath(outputDir.path!, withIntermediateDirectories: true, attributes: nil) else { return } | ||
|
||
let assetIdentifier = NSUUID().UUIDString | ||
let tmpImagePath = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent("\(assetIdentifier).tiff").path! | ||
let tmpMoviePath = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent("\(assetIdentifier).mov").path! | ||
let imagePath = outputDir.URLByAppendingPathComponent("\(assetIdentifier).JPG").path! | ||
let moviePath = outputDir.URLByAppendingPathComponent("\(assetIdentifier).MOV").path! | ||
let paths = [tmpImagePath, tmpMoviePath, imagePath, moviePath] | ||
|
||
for path in paths { | ||
guard !NSFileManager.defaultManager().fileExistsAtPath(path) else { return } | ||
} | ||
|
||
guard image.TIFFRepresentation?.writeToFile(tmpImagePath, atomically: true) == true else { return } | ||
// create AVAssetExportSession each time because it cannot be reused after export completion | ||
guard let session = AVAssetExportSession(asset: playerItem.asset, presetName: AVAssetExportPresetPassthrough) else { return } | ||
session.outputFileType = "com.apple.quicktime-movie" | ||
session.outputURL = NSURL(fileURLWithPath: tmpMoviePath) | ||
session.timeRange = CMTimeRange(start: player.currentTime(), duration: CMTime(value: 3*600, timescale: 600)) | ||
session.exportAsynchronouslyWithCompletionHandler { | ||
dispatch_async(dispatch_get_main_queue()) { | ||
switch session.status { | ||
case .Completed: | ||
JPEG(path: tmpImagePath).write(imagePath, assetIdentifier: assetIdentifier) | ||
NSLog("%@", "LivePhoto JPEG created: \(imagePath)") | ||
|
||
QuickTimeMov(path: tmpMoviePath).write(moviePath, assetIdentifier: assetIdentifier) | ||
NSLog("%@", "LivePhoto MOV created: \(moviePath)") | ||
|
||
self.showInFinderAndOpenInPhotos([imagePath, moviePath].map{NSURL(fileURLWithPath: $0)}) | ||
case .Cancelled, .Exporting, .Failed, .Unknown, .Waiting: | ||
NSLog("%@", "exportAsynchronouslyWithCompletionHandler = \(session.status)") | ||
} | ||
|
||
for path in [tmpImagePath, tmpMoviePath] { | ||
let _ = try? NSFileManager.defaultManager().removeItemAtPath(path) | ||
} | ||
self.exportSession = nil | ||
self.updateViews() | ||
} | ||
} | ||
exportSession = session | ||
updateViews() | ||
} | ||
|
||
private func showInFinderAndOpenInPhotos(fileURLs: [NSURL]) { | ||
NSWorkspace.sharedWorkspace().activateFileViewerSelectingURLs(fileURLs) | ||
|
||
// wait until Finder is active or timed out, | ||
// to avoid openURLs overtaking Finder activation | ||
dispatch_async(dispatch_get_global_queue(0, 0)) { | ||
let start = NSDate() | ||
while NSWorkspace.sharedWorkspace().frontmostApplication?.bundleIdentifier != "com.apple.finder" && NSDate().timeIntervalSinceDate(start) < 5 { | ||
NSThread.sleepForTimeInterval(0.1) | ||
} | ||
NSWorkspace.sharedWorkspace().openURLs(fileURLs, withAppBundleIdentifier: "com.apple.Photos", options: [], additionalEventParamDescriptor: nil, launchIdentifiers: nil) | ||
} | ||
} | ||
} |
Oops, something went wrong.