├── launch.sh ├── banner.bmp ├── close.png ├── dialog.bmp ├── icon.ico ├── icon.png ├── icons.ai ├── small.bmp ├── tiny.bmp ├── maximize.png ├── minimize.png ├── restore.png ├── imgErrThumb.png ├── vidErrThumb.png ├── Controller ├── package.json ├── video.png ├── directory.png ├── directory_open.png ├── video_overlay.png ├── video_left_overlay.png ├── video_right_overlay.png ├── FilenameSorter.js ├── index.html ├── Collection.js ├── Directory.js ├── index.css ├── ThumbWarrior.js ├── scum.js └── index.js ├── Visualizer ├── package.json ├── drag.png ├── mute.png ├── muted.png ├── VideoManager.js ├── NoplayBlock.js ├── index.html └── index.css ├── launch.vbs ├── .gitignore ├── gulpfile.js ├── winbuild.bat ├── package.json ├── paypage.html ├── LICENSE ├── LICENSE.rtf ├── index.html ├── CODE_OF_CONDUCT.md ├── support.css ├── README.md ├── winstaller.wxs └── index.js /launch.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | nw/nw . 3 | -------------------------------------------------------------------------------- /banner.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/banner.bmp -------------------------------------------------------------------------------- /close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/close.png -------------------------------------------------------------------------------- /dialog.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/dialog.bmp -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/icon.ico -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/icon.png -------------------------------------------------------------------------------- /icons.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/icons.ai -------------------------------------------------------------------------------- /small.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/small.bmp -------------------------------------------------------------------------------- /tiny.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/tiny.bmp -------------------------------------------------------------------------------- /maximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/maximize.png -------------------------------------------------------------------------------- /minimize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/minimize.png -------------------------------------------------------------------------------- /restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/restore.png -------------------------------------------------------------------------------- /imgErrThumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/imgErrThumb.png -------------------------------------------------------------------------------- /vidErrThumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/vidErrThumb.png -------------------------------------------------------------------------------- /Controller/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Controller", 3 | "main":"index.js" 4 | } 5 | -------------------------------------------------------------------------------- /Visualizer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Visualizer", 3 | "main":"index.js" 4 | } 5 | -------------------------------------------------------------------------------- /launch.vbs: -------------------------------------------------------------------------------- 1 | 'launch.vbs 2 | CreateObject("Wscript.Shell").Run "nw.x86\nw.exe .", 0, True 3 | -------------------------------------------------------------------------------- /Controller/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/Controller/video.png -------------------------------------------------------------------------------- /Visualizer/drag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/Visualizer/drag.png -------------------------------------------------------------------------------- /Visualizer/mute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/Visualizer/mute.png -------------------------------------------------------------------------------- /Visualizer/muted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/Visualizer/muted.png -------------------------------------------------------------------------------- /Controller/directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/Controller/directory.png -------------------------------------------------------------------------------- /Controller/directory_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/Controller/directory_open.png -------------------------------------------------------------------------------- /Controller/video_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/Controller/video_overlay.png -------------------------------------------------------------------------------- /Controller/video_left_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/Controller/video_left_overlay.png -------------------------------------------------------------------------------- /Controller/video_right_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenanigans/PornViewer/HEAD/Controller/video_right_overlay.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | !node_modules/*.js 3 | node_modules.wxs 4 | nw 5 | nw.old 6 | nw.x86 7 | nw.x64 8 | resources 9 | build 10 | node_modules.wxs 11 | node_modules.wxs 12 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 2 | var gulp = require ('gulp'); 3 | var zip = require ('gulp-zip'); 4 | var watch = require ('gulp-watch'); 5 | 6 | gulp.task('default', function(){ 7 | gulp.watch ('src/**', [ 'default' ]); 8 | return gulp.src('src/**') 9 | .pipe (zip('package.zip', { compress:false })) 10 | .pipe (gulp.dest('./')) 11 | ; 12 | }); 13 | 14 | gulp.task('once', function(){ 15 | return gulp.src('src/**') 16 | .pipe (zip('package.zip', { compress:false })) 17 | .pipe (gulp.dest('./')) 18 | ; 19 | }); 20 | -------------------------------------------------------------------------------- /winbuild.bat: -------------------------------------------------------------------------------- 1 | "%wix%bin\heat.exe" dir "node_modules" -gg -ke -cg NodeModules -dr INSTALLDIR -template Product -out "node_modules.wxs" -sw5150 2 | REM "%wix%bin\candle.exe" *.wxs -ext WixUtilExtension -arch x64 -dPlatform=x64 -out build\x64\ 3 | "%wix%bin\candle.exe" *.wxs -ext WixUtilExtension -arch x86 -dPlatform=x86 -out build\x86\ 4 | REM "%wix%bin\light.exe" -ext WixUIExtension -ext WixUtilExtension build\x64\*.wixobj -out build\PornViewer_x64.msi -sw1076 -b node_modules 5 | "%wix%bin\light.exe" -ext WixUIExtension -ext WixUtilExtension build\x86\*.wixobj -out build\PornViewer_x86.msi -sw1076 -b node_modules 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PornViewer", 3 | "main": "index.html", 4 | "version": "0.2.1", 5 | "dependencies": { 6 | "async": "^1.4.2", 7 | "cachew": "^0.1.1", 8 | "graceful-fs": "^4.1.2", 9 | "image-type": "^2.0.2", 10 | "infosex": "^0.1.3", 11 | "lwip": "^0.0.7", 12 | "mkdirp": "^0.5.1", 13 | "needle": "^0.11.0", 14 | "scum": "0.0.1", 15 | "surveil": "0.0.3", 16 | "wcjs-renderer": "git://github.com/shenanigans/wcjs-renderer#f83d1f1e4b56dcbe89a4454232f6ae08f4773a2a" 17 | }, 18 | "window": { 19 | "title": "Support PornViewer", 20 | "icon": "icon.png", 21 | "show": false, 22 | "toolbar": false, 23 | "width": 600, 24 | "height": 635 25 | }, 26 | "webkit": { 27 | "plugin": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Controller/FilenameSorter.js: -------------------------------------------------------------------------------- 1 | 2 | var RE_FNAME = /^(\d*)(.*?)(\d*)$/; 3 | module.exports = function (able, baker) { 4 | able = RE_FNAME.exec (able.toLowerCase()); 5 | baker = RE_FNAME.exec (baker.toLowerCase()); 6 | 7 | // leading number 8 | if (able[1]) { 9 | if (!baker[1]) 10 | return -1; 11 | aNum = Number (able[1]); 12 | bNum = Number (baker[1]); 13 | if (aNum < bNum) 14 | return -1; 15 | if (aNum > bNum) 16 | return 1; 17 | } else if (baker[1]) 18 | return 1; 19 | 20 | // text portion 21 | if (able[2] < baker[2]) 22 | return -1; 23 | if (able[2] > baker[2]) 24 | return 1; 25 | 26 | // trailing number 27 | if (able[3]) { 28 | if (!baker[3]) 29 | return -1; 30 | aNum = Number (able[3]); 31 | bNum = Number (baker[3]); 32 | if (aNum < bNum) 33 | return -1; 34 | if (aNum > bNum) 35 | return 1; 36 | } else if (baker[3]) 37 | return 1; 38 | 39 | // identical filename(?!) 40 | return 0; 41 | }; 42 | -------------------------------------------------------------------------------- /paypage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kevin "Schmidty" Smith 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 | -------------------------------------------------------------------------------- /LICENSE.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033{\fonttbl{\f0\fnil\fcharset0 Calibri;}} 2 | {\*\generator Riched20 10.0.10240}\viewkind4\uc1 3 | \pard\sl240\slmult1\f0\fs22\lang9 The MIT License (MIT)\par 4 | \par 5 | Copyright (c) 2015 Kevin "Schmidty" Smith\par 6 | \par 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\par 8 | \par 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\par 10 | \par 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\par 12 | } 13 | -------------------------------------------------------------------------------- /Controller/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PornController 5 | 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 23 | Slideshow: 24 |
29 | 30 | seconds 31 |
32 |
33 |
34 |
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /Visualizer/VideoManager.js: -------------------------------------------------------------------------------- 1 | 2 | var chimera = require ('wcjs-renderer'); 3 | 4 | // var RE_LEFT = /left:(-?\d+)px/; 5 | module.exports = function (parentElem, filepath) { 6 | var document = parentElem.ownerDocument; 7 | parentElem.innerHTML = '
'; 8 | var container = document.getElementById ('VideoContainer'); 9 | var canvas = document.getElementById ('VideoCanvas'); 10 | 11 | var vlc = chimera.init (canvas); 12 | vlc.play ('file:///' + filepath); 13 | 14 | vlc.events.once ('FrameReady', function (frame) { 15 | var canvasWidth = frame.width; 16 | var canvasHeight = frame.height; 17 | canvas.setAttribute ('width', canvasWidth); 18 | canvas.setAttribute ('height', canvasHeight); 19 | var containerHeight = container.clientHeight; 20 | var containerWidth = container.clientWidth; 21 | var wideRatio = containerWidth / canvasWidth; 22 | var tallRatio = containerHeight / canvasHeight; 23 | var useRatio = wideRatio < tallRatio ? wideRatio : tallRatio; 24 | var useHeight = Math.floor (canvasHeight * useRatio); 25 | canvas.setAttribute ( 26 | 'style', 27 | 'width:' + Math.floor (canvasWidth * useRatio) + 'px;' 28 | + 'height:' + useHeight + 'px;' 29 | + 'margin-top:' + Math.max (0, Math.floor((containerHeight - useHeight) / 2)) + 'px;' 30 | ); 31 | }); 32 | 33 | return vlc; 34 | }; 35 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Please Support PornViewer!

9 |

10 | A lot of effort has gone into making PornViewer. If you have enjoyed this application it 11 | would be fair if you paid a little bit of money for it. Plus if you do, this annoying 12 | message will never appear again! 13 |

14 |
15 | 16 | 17 |
Pay for PornViewer
18 |
I already paid!
19 |
20 |

Enter the email address you used to pay for PornViewer.

21 | 22 |
Check Email
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /Visualizer/NoplayBlock.js: -------------------------------------------------------------------------------- 1 | 2 | var MINUTE = 1000 * 60; 3 | var HOUR = MINUTE * 60; 4 | function toTimeStr (mils) { 5 | var hours = Math.floor (mils / HOUR); 6 | var minutes = Math.floor ((mils % HOUR) / MINUTE); 7 | var seconds = Math.floor ((mils % MINUTE) / 1000); 8 | var str = ''; 9 | if (hours) 10 | str += hours + ':'; 11 | str += hours ? minutes < 10 ? '0' + minutes : minutes : minutes; 12 | str += ':' + (seconds < 10 ? '0' + seconds : seconds); 13 | return str; 14 | } 15 | 16 | function NoplayBlock (document, start, end) { 17 | this.elem = document.createElement ('div'); 18 | this.elem.className = 'noplayBlock'; 19 | 20 | var startHandle, endHandle; 21 | if (start) { 22 | this.start = start; 23 | startHandle = document.createElement ('div'); 24 | startHandle.className = 'seekHandle startHandle'; 25 | this.startSpan = document.createElement ('span'); 26 | this.startSpan.textContent = toTimeStr (start); 27 | startHandle.appendChild (this.startSpan); 28 | startHandle.appendChild (document.createElement ('div')); 29 | } else { 30 | this.isFirst = true; 31 | this.elem.setAttribute ('id', 'StartBlock'); 32 | } 33 | 34 | if (end) { 35 | this.end = end; 36 | endHandle = document.createElement ('div'); 37 | endHandle.className = 'seekHandle endHandle'; 38 | this.endSpan = document.createElement ('span'); 39 | this.endSpan.textContent = toTimeStr (end); 40 | endHandle.appendChild (this.endSpan); 41 | endHandle.appendChild (document.createElement ('div')); 42 | } else { 43 | this.isLast = true; 44 | this.elem.setAttribute ('id', 'EndBlock'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Controller/Collection.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require ('fs'); 3 | var path = require ('path'); 4 | var gaze = require ('gaze'); 5 | 6 | function Collection (parent, controller, dirpath, name) { 7 | this.parent = parent; 8 | this.controller = controller; 9 | this.dirpath = dirpath; 10 | this.name = name; 11 | 12 | this.children = {}; 13 | 14 | // create DOM stuff 15 | this.elem = this.document.createElement ('div'); 16 | this.elem.setAttribute ('class', 'collection'); 17 | this.elem.setAttribute ('data-name', name); 18 | var collectionImg = this.document.createElement ('img'); 19 | collectionImg.setAttribute ('src', 'controller/collection.png'); 20 | this.elem.appendChild (collectionImg); 21 | this.elem.appendChild (this.document.createTextNode (name)); 22 | this.childrenElem = this.document.createElement ('div'); 23 | this.childrenElem.setAttribute ('class', 'children'); 24 | this.elem.appendChild (this.childrenElem); 25 | 26 | // insert into DOM 27 | var children = parent.childrenElem.children; 28 | if (!children.length || children[children.length-1].getAttribute ('data-name') < name) 29 | parent.childrenElem.appendChild (this.elem); 30 | else { 31 | // pretty typically Directories are created in order, so bottom-up linear scan is fine 32 | var done = false; 33 | for (var i=children.length-1; i>=0; i--) 34 | if (children[i].getAttribute ('data-name') < name) { 35 | parent.childrenElem.insertBefore (children[i+1], this.elem); 36 | done = true; 37 | break; 38 | } 39 | if (!done) 40 | parent.childrenElem.insertBefore (children[0], this.elem); 41 | } 42 | }; 43 | 44 | Collection.prototype.addChild = function (name) { 45 | if (Object.hasOwnProperty.call (this.children, name)) 46 | return this.children[name]; 47 | return this.children[name] = new Collection ( 48 | this, 49 | this.controller, 50 | path.join (this.dirpath, name), 51 | name 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Schmidty's Code of Conduct 2 | ========================== 3 | This Code of Conduct describes the behavior standards expected of contributors and other 4 | participants in the `PornViewer` open source software project. 5 | 6 | 1. Community Conduct 7 | -------------------- 8 | You have the right to express any opinion. You have the right to respond to any opinion in any way. 9 | You have the right to publish any statement which meets all of the following qualifications: 10 | * The statement is directly relevant to the `PornViewer` project. 11 | * The statement is not calculated to disrupt normal conversation or trigger off-topic controversy. 12 | * The statement is not in violation of the laws of the United States of the America. 13 | * The statement contains no assertion, true or false, of real world information related to another 14 | person, living or dead, which is not readily available from free public sources. 15 | 16 | You have every right to disagree with this Code of Conduct. You may advocate for its change and you 17 | may advocate behavior which violates it as long as you do not participate in a way which violates 18 | the concurrent text of the Code of Conduct. 19 | 20 | 2. Community Standards 21 | ---------------------- 22 | Material contributions to the `PornViewer` project will be judged only by the fitness for purpose of the 23 | contributed material. Officially sanctioned maintainers of the project are obligated to refrain from 24 | participating in off-topic conversations. This project will not reject any functionally adequate 25 | contribution on the basis of statements made by the contributing party not directly relating to the 26 | `PornViewer` project. Contributions from active detractors of the project itself may be rejected out of 27 | pure paranoid fear that the contribution may contain disguised intentional flaws. 28 | 29 | 3. Statement of Agenda 30 | ---------------------- 31 | The maintainers reject the philosophy that anyone should be protected from hearing the opinions of 32 | anyone else. It is our avowed position that preventing the discussion of divisive opinions serves no 33 | ultimate purpose except to shield such opinions from criticism. We believe in open discussion, not 34 | forcedly pleasant discussion. 35 | 36 | At the same time, we assert that the `PornViewer` project is not an appropriate forum for the discussion 37 | of anything whatsoever except for relevant engineering issues. A modest effort will be made to 38 | moderate or eliminate all disruptive conversation which does not advance project goals. The project 39 | maintainers offer no guarantee that the project will be free from offensive discourse. 40 | -------------------------------------------------------------------------------- /Visualizer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PornViewer 5 | 6 | 7 | 8 |
9 | 10 |
11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 | 19 | 24 | 25 | 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 |
44 | 45 |
46 | Reset View 47 |
48 | 49 |
50 | Audio Stream 51 |
52 |
53 |
54 | 0 disable 55 |
56 |
57 |
58 |
59 | Subtitles 60 |
61 |
62 |
63 | 0 disable 64 |
65 |
66 |
67 |
68 | Always Start Here 69 |
70 |
71 | Always Stop Here 72 |
73 |
74 | Skip This Part 75 |
76 |
77 | Reset Video Playback 78 |
79 |
80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /support.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | text-align: center; 4 | font-family: verdana; 5 | margin: 0; 6 | padding: 3em 0 3em 0; 7 | overflow: hidden; 8 | } 9 | 10 | p { 11 | max-width: 25em; 12 | margin: 1em auto; 13 | } 14 | 15 | .dolla { 16 | display: inline-block; 17 | margin-right: 0.1em; 18 | padding-bottom: 0.1em; 19 | vertical-align: middle; 20 | font-size: 230%; 21 | color: #E600DD; 22 | font-family: cursive; 23 | } 24 | 25 | #PayArea { 26 | max-width: 17em; 27 | padding: 1em 0.5em 2em; 28 | margin: 2em auto; 29 | border: 1px solid #B6B6B6; 30 | box-shadow: 0 0 1em rgba(0, 0, 0, 0.21); 31 | position: relative; 32 | } 33 | 34 | #PayAmount { 35 | width: 5em; 36 | border: none; 37 | background-color: #E7E6E1; 38 | font-size: 110%; 39 | padding: 0.2em 0 0.1em 0.2em; 40 | } 41 | 42 | #PayMe, #AlreadyPaidButton { 43 | background-color: #E600DD; 44 | color: #fff; 45 | font-size: 115%; 46 | font-weight: 900; 47 | min-width: 1em; 48 | margin: 0.4em auto 1em; 49 | padding: 0.7em 1em; 50 | display: inline-block; 51 | cursor: pointer; 52 | } 53 | 54 | #EndAnnoyanceButton { 55 | color: #00f; 56 | cursor: pointer; 57 | } 58 | 59 | #PayFrame { 60 | position: fixed; 61 | top: 0; 62 | left: 0; 63 | width: 100%; 64 | height: 100%; 65 | display: none; 66 | overflow: hidden; 67 | } 68 | 69 | #PayFrame.active { 70 | display: block; 71 | } 72 | 73 | .overlayView.active { 74 | overflow: none; 75 | } 76 | 77 | #EndAnnoyanceCollapso { 78 | display: none; 79 | position: relative; 80 | } 81 | 82 | #EndAnnoyanceCollapso.active { 83 | display: block; 84 | } 85 | 86 | .throbber { 87 | height: 1.2em; 88 | width: 1.2em; 89 | border-radius: 50%; 90 | background-color: rgb(230, 0, 221); 91 | position: relative; 92 | } 93 | 94 | @-webkit-keyframes spin { 95 | 0% { 96 | -webkit-transform: rotate(0deg); 97 | } 98 | 100% { 99 | -webkit-transform: rotate(360deg); 100 | } 101 | } 102 | 103 | .throbber .spinner { 104 | position: absolute; 105 | top: 5%; 106 | left: 5%; 107 | width: 90%; 108 | height: 90%; 109 | -webkit-animation: spin 1s infinite; 110 | -webkit-animation-timing-function: ease; 111 | } 112 | 113 | .throbber .spinner .pip { 114 | position: absolute; 115 | top: 0; 116 | right: 42%; 117 | width: 16%; 118 | height: 16%; 119 | border-radius: 50%; 120 | background-color: rgba(255, 255, 255, 0.66); 121 | } 122 | 123 | .throbber .core + .spinner { 124 | -webkit-animation-delay: 0.2s; 125 | } 126 | 127 | .throbber .spinner:last-child { 128 | -webkit-animation-delay: 0.4s; 129 | } 130 | 131 | @-webkit-keyframes throb { 132 | 0% { 133 | top: 40%; 134 | left: 40%; 135 | width: 20%; 136 | height: 20%; 137 | background-color: rgba(255, 255, 255, 0.0); 138 | } 139 | 40% { 140 | top: 10%; 141 | left: 10%; 142 | width: 80%; 143 | height: 80%; 144 | background-color: rgba(255, 255, 255, 0.4); 145 | } 146 | 60% { 147 | top: 10%; 148 | left: 10%; 149 | width: 80%; 150 | height: 80%; 151 | background-color: rgba(255, 255, 255, 0.4); 152 | } 153 | 100% { 154 | top: 40%; 155 | left: 40%; 156 | width: 20%; 157 | height: 20%; 158 | background-color: rgba(255, 255, 255, 0.0); 159 | } 160 | } 161 | 162 | .throbber .core { 163 | position: absolute; 164 | left: 50%; 165 | -webkit-animation: throb 1s infinite; 166 | -webkit-animation-timing-function: ease; 167 | border-radius: 50%; 168 | } 169 | 170 | #NetworkThrobber { 171 | position: absolute; 172 | top: 50%; 173 | left: 50%; 174 | display: none; 175 | } 176 | 177 | #NetworkThrobber.active { 178 | display: block; 179 | } 180 | 181 | #NetworkThrobber .throbber { 182 | width: 100px; 183 | height: 100px; 184 | left: -50px; 185 | top: -50px; 186 | opacity: 1; 187 | box-shadow: 0 0 100px 10px #000; 188 | } 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PornViewer 2 | An image and video viewer designed to provide a pleasurable viewing experience for pornographic 3 | material. This doesn't necessarily make it poor at viewing regular photos and videos. While 4 | theoretically cross-platform, it is currently only available on Windows. 5 | 6 | ![screenshot](http://i.imgur.com/MVzG6xH.jpg) 7 | 8 | ## Why 9 | Win10 deprecated good ol' photo and fax viewer which for me beat everything else I tried. I built it 10 | in [nw.js](http://nwjs.io/) because I have an inappropriately close relationship with Node. I didn't 11 | build it in Electron **A** because I've done nw.js once before and **B** because the Electron 12 | maintainers seem to like ES6 and I disagree, packaging is a lil twisty, and some other kinda trivial 13 | irks. 14 | 15 | 16 | 17 | * clean design that doesn't waste your pixels or time 18 | * versatile keyboard controls 19 | * uses libvlc to play a generous assortment of video formats 20 | * picks video thumbnails from a more useful point in the video, which is surprisingly helpful 21 | * skip the part in a video with somebody's dumb sweaty face hogging the screen 22 | * animated gifs play with a pretty high quality upscale, it's nice 23 | * pretty fast thumbnail caching and sorting with a tight thumbnail view 24 | 25 | ## How 26 | * Use dem arrow keys. 27 | * Use alt/option or control with dem arrow keys to skip your videos. Add shift for less skippage. 28 | * Right click in the viewing area, or tap alt. 29 | * move images or directories around by dragging and dropping them. 30 | 31 | ## Caveats 32 | You're gonna use a healthy chunk of your OS drive for thumbnails. Think 1-5 gigabytes. If you ever 33 | run the uninstaller and it takes a solid three minutes, that's the thumbnails. 34 | 35 | Windows users, think carefully before activating the file association options during install. The 36 | associated image type's non-thumbnail icon will become a lil dickbutt and double-clicking image files 37 | anywhere on the system will launch a window called PornViewer. You can at least be assured that it 38 | will **not** briefly show the last-viewed image as I consider this feature a priority. 39 | 40 | ## Installation 41 | Use one of these installer links below. If you like it, please [help me not be so broke](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=PN6C2AZTS2FP8&lc=US¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted). 42 | 43 | ### Windows Installer 44 | * [0.2.1](https://github.com/shenanigans/PornViewer/releases/download/0.2.1/PornViewer_x86.msi) 45 | 46 | ### Linux Binaries 47 | I've been having a problem with the `lwip` module, it's supposed to statically bind its own libpng 48 | but it still somehow gets confused by the older version dynamically bound by node-webkit. Probably I 49 | need to build my own node-webkit with libpng bound statically. If anybody has some thoughts on this 50 | I'd love to hear them. Now that there's video support involved I'm kinda intimidated by this whole 51 | problem so it might be a while before I get a linux build up. 52 | 53 | ### OSX 54 | My dumbass apple laptop bricked out when its battery died. If you wanna try it yourself, see the 55 | [build instructions](#building-pornviewer) near the bottom. 56 | 57 | 58 | ## Infrequently Asked Questions 59 | #### Is there a safe-for-work version of this? 60 | No. You're welcome to fork this repo and make one. If you want my opinion I think you should call it 61 | `lolphotos`. 62 | 63 | #### What's next for PornViewer? 64 | * fake directories called collections 65 | * browse from multiple directories or collections at once 66 | * "cascade view" will tile images (including gifs) to fill the entire monitor with scrolling porn 67 | 68 | 69 | ## Building PornViewer 70 | You're going to need [nodejs](https://nodejs.org) and the npm thingy it comes with. Linux users are 71 | advised to **always** install Node.js from source. If you're on Windows, you will need MinGW. I 72 | recommend just using the lovely [command-line git installer](https://git-scm.com/downloads). You 73 | will need your platform's support files for `gyp` builds. That's build-essential or yummy equivalent 74 | on linux, xcode on osx and visual studio 2013 (it **must** be 2013) on windows. 75 | 76 | Clone this repository and download the most recent stable version of 77 | [node-webkit](https://github.com/nwjs/nw.js#downloads). Unzip it, put it in the repository 78 | directory and rename it `nw`. If you're building a windows msi, copy the contents of the `nw` 79 | directory into a new directory called `resources\x64\` or `resources\x86\`. 80 | 81 | Then do this stuff: 82 | ```shell 83 | cd PornViewer 84 | npm install 85 | npm install -g nw-gyp 86 | ``` 87 | 88 | #### Setting Up [`lwip`](https://github.com/EyalAr/lwip) 89 | ```shell 90 | cd node_modules/lwip 91 | nw-gyp clean 92 | nw-gyp configure --target=0.12.3 93 | # on windows add --msvs_version=2013 94 | # for x86 add --arch=ia32 95 | nw-gyp build 96 | # for x86 add --arch=ia32 97 | cd ../../ 98 | ``` 99 | 100 | #### Setting Up [`webchimera.js`](https://github.com/RSATom/WebChimera.js) 101 | If you're very very lucky, this will "just work". If not, you'll need to deal with customizing your 102 | distribution of the `webchimera.js` package. You'll find it at 103 | `node_modules\wcjs-renderer\node_modules\webchimera.js`. 104 | On Win10 x64 I habitually use [`wcjs-prebuilt`](https://github.com/Ivshti/wcjs-prebuilt) and just 105 | rename its directory. Note that on windows x64 you currently need to use [both](https://github.com/Ivshti/wcjs-prebuilt/issues/10#issuecomment-149008366) 106 | of [these](https://github.com/Ivshti/wcjs-prebuilt/issues/10#issuecomment-149201701) manual patches. 107 | 108 | #### Launching 109 | Use either `launch.sh` or `launch.vbs` to start the application from the source directory. 110 | 111 | #### Building an MSI 112 | You'll need [WiX](http://wixtoolset.org/). Use a DOS shell to run `winbuild.bat`. This will build 113 | one or two `.msi` files in `build\` depending on which architecture(s) have been prepared 114 | completely. Commands for an arch with no resource files provided will fail quickly and fairly 115 | quietly. 116 | 117 | 118 | ## LICENSE 119 | The MIT License (MIT) 120 | 121 | Copyright (c) 2015 Kevin "Schmidty" Smith 122 | 123 | Permission is hereby granted, free of charge, to any person obtaining a copy 124 | of this software and associated documentation files (the "Software"), to deal 125 | in the Software without restriction, including without limitation the rights 126 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 127 | copies of the Software, and to permit persons to whom the Software is 128 | furnished to do so, subject to the following conditions: 129 | 130 | The above copyright notice and this permission notice shall be included in all 131 | copies or substantial portions of the Software. 132 | 133 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 134 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 135 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 136 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 137 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 138 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 139 | SOFTWARE. 140 | -------------------------------------------------------------------------------- /Controller/Directory.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require ('graceful-fs'); 3 | var path = require ('path'); 4 | var async = require ('async'); 5 | // var gaze = require ('gaze'); 6 | var sorter = require ('./FilenameSorter'); 7 | 8 | var POPUP_TIMEOUT = 850; 9 | 10 | function Directory (parent, controller, dirpath, name, extraName) { 11 | this.parent = parent; 12 | this.root = parent.root; 13 | this.controller = controller; 14 | this.dirpath = dirpath; 15 | this.name = name; 16 | 17 | this.children = {}; 18 | 19 | // create DOM stuff 20 | var self = this; 21 | this.elem = controller.document.createElement ('div'); 22 | this.elem.setAttribute ('class', 'directory'); 23 | this.elem.setAttribute ('data-name', name); 24 | this.elem.on ('selectstart', function (event) { 25 | event.preventDefault(); 26 | event.stopPropagation(); 27 | return false; 28 | }); 29 | this.elem.setAttribute ('draggable', 'true'); 30 | this.elem.on ('drag', function (event) { 31 | event.stopPropagation(); 32 | self.elem.addClass ('dragging'); 33 | }); 34 | var dragData = { type:'directory', path:self.dirpath, name:name }; 35 | this.elem.on ('dragstart', function (event) { 36 | event.stopPropagation(); 37 | event.dataTransfer.setData ( 38 | 'application/json', 39 | JSON.stringify (dragData) 40 | ); 41 | }); 42 | this.elem.on ('dragend', function(){ 43 | self.elem.dropClass ('dragging'); 44 | }); 45 | var popupTimer; 46 | this.elem.on ('dragenter', function (event) { 47 | var dropData = event.dataTransfer.getData ('application/json'); 48 | if (!dropData) 49 | return; // some other drop occured, let it bubble 50 | event.stopPropagation(); 51 | self.elem.addClass ('dragover'); 52 | popupTimer = setTimeout (function(){ 53 | self.open(); 54 | }, POPUP_TIMEOUT); 55 | }); 56 | this.elem.on ('dragleave', function (event) { 57 | event.stopPropagation(); 58 | self.elem.dropClass ('dragover'); 59 | clearTimeout (popupTimer); 60 | }); 61 | this.elem.on ('drop', function (event) { 62 | var dropData = event.dataTransfer.getData ('application/json'); 63 | if (!dropData) 64 | return; // some other drop occured, let it bubble 65 | dropData = JSON.parse (dropData); 66 | 67 | event.stopPropagation(); 68 | self.elem.dropClass ('dragover'); 69 | clearTimeout (popupTimer); 70 | 71 | var isDir = dropData.type == 'directory'; 72 | var oldNode; 73 | if (isDir) 74 | oldNode = self.root.getDir (dropData.path); 75 | fs.rename (dropData.path, path.join (self.dirpath, dropData.name), function (err) { 76 | if (!isDir) 77 | return; 78 | self.addChild (dropData.name); 79 | if (!oldNode) return; 80 | oldNode.elem.dropClass ('moving'); 81 | fs.stat (dropData.path, function (err, stats) { 82 | if (err || !stats) { 83 | oldNode.elem.dispose(); 84 | delete oldNode.parent.children[oldNode.name]; 85 | } 86 | }); 87 | }); 88 | if (!oldNode) return; 89 | oldNode.elem.addClass ('moving'); 90 | }); 91 | this.directoryImg = controller.document.createElement ('img'); 92 | this.directoryImg.setAttribute ('src', 'directory.png'); 93 | this.directoryImg.on ('click', function(){ self.toggleOpen(); }); 94 | this.elem.appendChild (this.directoryImg); 95 | var titleElem = controller.document.createElement ('div'); 96 | titleElem.setAttribute ('class', 'title'); 97 | titleElem.appendChild (controller.document.createTextNode (extraName || name)); 98 | titleElem.on ('click', function(){ 99 | self.open(); 100 | controller.select (dirpath, self.elem); 101 | }); 102 | this.elem.appendChild (titleElem); 103 | this.childrenElem = controller.document.createElement ('div'); 104 | this.childrenElem.setAttribute ('class', 'children'); 105 | this.elem.appendChild (this.childrenElem); 106 | 107 | // insert into DOM 108 | var children = parent.childrenElem.children; 109 | if (!children.length || children[children.length-1].getAttribute ('data-name') < name) 110 | parent.childrenElem.appendChild (this.elem); 111 | else { 112 | // pretty typically Directories are created in order, so bottom-up linear scan is fine 113 | var done = false; 114 | for (var i=children.length-1; i>=0; i--) 115 | if (children[i].getAttribute ('data-name') < name) { 116 | parent.childrenElem.insertBefore (this.elem, children[i+1]); 117 | done = true; 118 | break; 119 | } 120 | if (!done) 121 | parent.childrenElem.insertBefore (this.elem, children[0]); 122 | } 123 | } 124 | module.exports = Directory; 125 | 126 | Directory.prototype.addChild = function (name) { 127 | if (Object.hasOwnProperty.call (this.children, name)) { 128 | this.controller.revealDirectory(); 129 | return this.children[name]; 130 | } 131 | var child = this.children[name] = new Directory ( 132 | this, 133 | this.controller, 134 | path.join (this.dirpath, name), 135 | name 136 | ); 137 | this.controller.revealDirectory(); 138 | return child; 139 | }; 140 | 141 | Directory.prototype.open = function(){ 142 | // if (this.isOpen) 143 | // return; 144 | if (!this.isOpen) { 145 | this.elem.addClass ('open'); 146 | this.directoryImg.setAttribute ('src', 'directory_open.png'); 147 | } 148 | this.isOpen = true; 149 | 150 | var self = this; 151 | fs.readdir (this.dirpath, function (err, children) { 152 | if (err) { 153 | // directory is missing or unreadable 154 | delete self.parent.children[self.name]; 155 | self.elem.dispose(); 156 | return; 157 | } 158 | 159 | children.sort (sorter); 160 | 161 | // trim missing 162 | var unknown = []; 163 | var newNames = {}; 164 | for (var i=0,j=children.length; i .children { 171 | display: block; 172 | } 173 | 174 | .directory.open > .children:empty { 175 | 176 | } 177 | 178 | .directory.open > .children:empty:before { 179 | content: "empty"; 180 | color: #A9A8A8; 181 | font-style: italic; 182 | } 183 | 184 | .directory.selected > .title { 185 | color: #fff; 186 | background-color: #E600DD; 187 | } 188 | 189 | .directory.dragging { 190 | opacity: 0.5; 191 | } 192 | 193 | .directory.dragover > .children { 194 | border-color: #E600E3; 195 | } 196 | 197 | .directory.dragover > .title { 198 | text-decoration: underline; 199 | color: #E600E3; 200 | } 201 | 202 | .directory.moving { 203 | opacity: 0.5; 204 | } 205 | 206 | #Host { 207 | height: 100%; 208 | overflow-y: hidden; 209 | position: relative; 210 | } 211 | 212 | #Controls { 213 | padding: 0 0 0 30px; 214 | float: left; 215 | } 216 | 217 | #Controls select, #Controls input { 218 | -webkit-app-region: no-drag; 219 | vertical-align: top; 220 | } 221 | 222 | .thumbs { 223 | line-height: 0; 224 | overflow-y: scroll; 225 | position: absolute; 226 | top: 0; 227 | right: 0; 228 | bottom: 0; 229 | left: 0; 230 | padding: 0.5em 0; 231 | } 232 | 233 | .thumbs .thumb { 234 | display: inline-block; 235 | width: 150px; 236 | height: 150px; 237 | text-align: center; 238 | vertical-align: top; 239 | padding: 1px; 240 | position: relative; 241 | overflow: hidden; 242 | cursor: pointer; 243 | } 244 | 245 | .thumbs .bogus { 246 | display: none; 247 | } 248 | 249 | .thumb.loading { 250 | background-image: url('../icon.png'); 251 | background-size: 60%; 252 | background-position: center; 253 | background-repeat: no-repeat; 254 | } 255 | .thumb.video.loading { 256 | background-image: url('video.png'); 257 | } 258 | 259 | .thumb img { 260 | } 261 | 262 | .thumb.selected { 263 | /* outline: 3px solid #E600E3; */ 264 | background-color: #E600E3; 265 | } 266 | 267 | .thumb.selected img { 268 | opacity: 0.8; 269 | } 270 | 271 | .thumb .filename { 272 | display: none; 273 | position: absolute; 274 | right: 0; 275 | bottom: 0px; 276 | height: 8px; 277 | left: 0; 278 | font-size: 12px; 279 | padding: 12px 0 0px 1px; 280 | background-color: #fff; 281 | text-align: left; 282 | width: 5000px; 283 | } 284 | 285 | .filename .text { 286 | display: inline-block; 287 | min-width: 150px; 288 | text-align: center; 289 | } 290 | 291 | .filename .whiteout { 292 | position: absolute; 293 | top: 0; 294 | left: 112px; 295 | bottom: 0; 296 | width: 40px; 297 | background: -webkit-linear-gradient(left, transparent, white); 298 | } 299 | 300 | .thumb:hover .filename, .thumb.selected .filename, .thumb.video .filename { 301 | display: block; 302 | } 303 | 304 | .thumb.dragging { 305 | opacity: 0.5; 306 | background-color: transparent; 307 | } 308 | 309 | #MultidragOuter { 310 | position: absolute; 311 | z-index: -1; 312 | } 313 | 314 | #MultidragOuter div { 315 | position: relative; 316 | top: -6px; 317 | left: -6px; 318 | } 319 | 320 | #MultidragOuter, #MultidragOuter div { 321 | background-color: #FFE7FE; 322 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.73); 323 | } 324 | 325 | #Multidrag { 326 | min-width: 52px; 327 | padding: 0 4px; 328 | height: 60px; 329 | font-size: 48px; 330 | text-align: center; 331 | } 332 | 333 | #MultidragWhiteout { 334 | width: 200%; 335 | background-color: #fff; 336 | } 337 | 338 | #SlideshowControls { 339 | -webkit-app-region: no-drag; 340 | } 341 | 342 | .slideshowPlay { 343 | display: inline-block; 344 | width: 23px; 345 | height: 25px; 346 | margin: -6px 0; 347 | cursor: pointer; 348 | text-align: center; 349 | transition: background-color 0.3s ease-out; 350 | } 351 | 352 | .slideshowPlay:hover, #SlideshowPause:hover { 353 | background-color: #FF62F9; 354 | } 355 | 356 | #SlideshowControls.playing .slideshowPlay { 357 | display: none; 358 | } 359 | 360 | #SlideshowPause { 361 | display: none; 362 | width: 46px; 363 | height: 25px; 364 | margin: -6px 0; 365 | cursor: pointer; 366 | position: relative; 367 | } 368 | 369 | #SlideshowPause div { 370 | background-color: #fff; 371 | width: 5px; 372 | height: 19px; 373 | position: absolute; 374 | top: 3px; 375 | left: initial; 376 | right: 15px; 377 | } 378 | 379 | #SlideshowPause div:first-child { 380 | left: 15px; 381 | right: initial; 382 | } 383 | 384 | #SlideshowControls.playing #SlideshowPause { 385 | display: inline-block; 386 | } 387 | 388 | #SlideshowLeft div { 389 | margin-top: 3px; 390 | margin-left: 3px; 391 | width: 0; 392 | height: 0; 393 | border-style: solid; 394 | border-width: 9.5px 16px 9.5px 0; 395 | border-color: transparent #fff transparent transparent; 396 | } 397 | 398 | #SlideshowRight div { 399 | margin-top: 3px; 400 | margin-left: 4px; 401 | width: 0; 402 | height: 0; 403 | border-style: solid; 404 | border-width: 9.5px 0 9.5px 16px; 405 | border-color: transparent transparent transparent #fff; 406 | } 407 | 408 | #SlideshowSeconds { 409 | border: none; 410 | outline: none; 411 | width: 3em; 412 | height: 17px; 413 | padding-left: 0.3em; 414 | } 415 | -------------------------------------------------------------------------------- /Visualizer/index.css: -------------------------------------------------------------------------------- 1 | 2 | html { 3 | image-rendering: optimizeQuality; 4 | font-family: tahoma, sans-serif; 5 | } 6 | 7 | #Display { 8 | position: fixed; 9 | width: 100%; 10 | height: 100%; 11 | top: 0; 12 | right: 0; 13 | bottom: 0; 14 | left: 0; 15 | background-color: rgba(0, 0, 0, 0.7); 16 | } 17 | 18 | #Dancer { 19 | position: fixed; 20 | } 21 | 22 | #Dancer img { 23 | width: 100%; 24 | height: 100%; 25 | } 26 | 27 | .image #ImageControls { 28 | display: block; 29 | height: 23px; 30 | } 31 | 32 | #VLC { 33 | position: fixed; 34 | top: 0; 35 | right: 0; 36 | bottom: 0; 37 | left: 0; 38 | display: none; 39 | } 40 | 41 | #VLC.active { 42 | display: block; 43 | } 44 | 45 | #VideoContainer { 46 | position: absolute; 47 | top: 0; 48 | right: 0; 49 | bottom: 0; 50 | left: 0; 51 | text-align: center; 52 | } 53 | 54 | #VideoContainer canvas { } 55 | 56 | .controlPane { 57 | position: relative; 58 | margin-top: 4px; 59 | } 60 | 61 | .controlPane > * { 62 | -webkit-app-region: no-drag; 63 | } 64 | 65 | #VideoControls { 66 | display: none; 67 | margin-bottom: -3px; 68 | } 69 | 70 | .video #VideoControls { 71 | display: block; 72 | } 73 | 74 | #PlayPause { 75 | width: 40px; 76 | height: 26px; 77 | cursor: pointer; 78 | position: relative; 79 | background-color: #880198; 80 | } 81 | 82 | #PlayButton { 83 | position: absolute; 84 | top: 5px; 85 | left: 15px; 86 | border-top: 8px solid transparent; 87 | border-bottom: 8px solid transparent; 88 | border-left: 13px solid #FFF; 89 | } 90 | 91 | #PlayPause .pause { 92 | display: none; 93 | position: absolute; 94 | top: 5px; 95 | bottom: 4px; 96 | width: 3px; 97 | background-color: #FFF; 98 | } 99 | 100 | #PauseButtonZero { 101 | left: 14px; 102 | } 103 | 104 | #PauseButtonOne { 105 | right: 14px; 106 | } 107 | 108 | #PlayPause.playing #PlayButton { 109 | display: none; 110 | 111 | } 112 | 113 | #PlayPause.playing .pause { 114 | display: block; 115 | } 116 | 117 | #SeekBar { 118 | position: absolute; 119 | top: 8px; 120 | right: 217px; 121 | bottom: 6px; 122 | left: 50px; 123 | background-color: #FFB2FC; 124 | cursor: pointer; 125 | } 126 | 127 | #SeekCaret { 128 | position: absolute; 129 | width: 0; 130 | left: 0; 131 | } 132 | 133 | #SeekCaret .caret { 134 | position: absolute; 135 | cursor: -webkit-grab; 136 | border: 6px solid #7E077A; 137 | width: 4px; 138 | height: 12px; 139 | top: -6px; 140 | left: -8px; 141 | } 142 | 143 | #SeekCaret .text { 144 | position: absolute; 145 | top: -3px; 146 | left: -3px; 147 | font-size: 14px; 148 | color: #6B0166; 149 | padding-left: 13px; 150 | z-index: 9001; 151 | } 152 | 153 | #SeekCaret .text.left { 154 | left: initial; 155 | 156 | right: 0; 157 | padding-left: 0; 158 | padding-right: 10px; 159 | } 160 | 161 | #MuteIndicator { 162 | position: absolute; 163 | top: 7px; 164 | right: 171px; 165 | height: 17px; 166 | } 167 | 168 | #VolumeBar { 169 | position: absolute; 170 | top: 4px; 171 | right: 14px; 172 | width: 0; 173 | height: 0; 174 | border-bottom: 19px solid #FFB2FC; 175 | border-left: 145px solid transparent; 176 | cursor: pointer; 177 | } 178 | 179 | #VolumeCaret { 180 | position: absolute; 181 | width: 0; 182 | left: 0px; 183 | } 184 | 185 | #VolumeCaret div { 186 | position: absolute; 187 | left: -4px; 188 | width: 8px; 189 | height: 24px; 190 | background-color: #64006F; 191 | top: -2px; 192 | cursor: -webkit-grab; 193 | } 194 | 195 | .nocurse { 196 | cursor: none; 197 | } 198 | 199 | .draggingImage { 200 | cursor: move; 201 | } 202 | 203 | #Controls { 204 | -webkit-app-region: drag; 205 | -webkit-user-select: none; 206 | position: fixed; 207 | top: 0; 208 | right: 0; 209 | left: 0; 210 | background-color: #E600DD; 211 | transition: opacity 0.5s ease-out; 212 | opacity: 0; 213 | color: #fff; 214 | font-family: verdana; 215 | text-align: center; 216 | padding: 3px 0; 217 | /* height: 19px; */ 218 | } 219 | 220 | #EventScreen { 221 | -webkit-app-region: no-drag; 222 | position: absolute; 223 | top: 0; 224 | right: 0; 225 | bottom: 0; 226 | left: 0; 227 | } 228 | 229 | #Controls select, #Controls input { 230 | -webkit-app-region: no-drag; 231 | vertical-align: top; 232 | } 233 | 234 | #Controls label { 235 | line-height: 5px; 236 | } 237 | 238 | #Controls input { 239 | margin: 0; 240 | vertical-align: middle; 241 | } 242 | 243 | #Controls:hover, #Controls.visible { 244 | opacity: 1; 245 | } 246 | 247 | #Controls:hover #EventScreen, #Controls.visible #EventScreen { 248 | display: none; 249 | } 250 | 251 | #Drag { 252 | float: left; 253 | height: 22px; 254 | margin: -2px 0 -2px 1px; 255 | } 256 | 257 | .controlButton { 258 | float: right; 259 | height: 25px; 260 | width: 31px; 261 | margin: -3px 0; 262 | transition: background-color 0.3s ease-in-out; 263 | cursor: pointer; 264 | -webkit-app-region: no-drag; 265 | background-position: center; 266 | background-size: contain; 267 | background-repeat: no-repeat; 268 | } 269 | 270 | .controlButton:hover { 271 | background-color: rgba(0, 0, 0, 0.35); 272 | } 273 | 274 | #Minimize { 275 | background-image: url('../minimize.png'); 276 | } 277 | 278 | #Maximize { 279 | background-image: url('../maximize.png'); 280 | } 281 | 282 | #Maximize.restore { 283 | background-image: url('../restore.png'); 284 | } 285 | 286 | #Close { 287 | background-image: url('../close.png'); 288 | } 289 | 290 | #Close:hover { 291 | background-color: rgba(255, 0, 0, 0.62); 292 | } 293 | 294 | #ContextMenu { 295 | visibility: hidden; 296 | position: fixed; 297 | top: 50px; 298 | left: 460px; 299 | font-size: 16px; 300 | background-color: #fff; 301 | font-weight: 700; 302 | font-family: verdana; 303 | color: #222; 304 | box-shadow: 0 0 28px rgba(255, 255, 255, 0.21); 305 | } 306 | 307 | #ContextMenu.active { 308 | visibility: visible; 309 | } 310 | 311 | #CX_Options_Video { 312 | color: #BEBEBE; 313 | } 314 | 315 | #CX_Options_Video.active { 316 | color: #000; 317 | } 318 | 319 | #ContextMenuKeyboardTarget { 320 | position: absolute; 321 | left: -9001px; 322 | } 323 | 324 | .CX_OptionList { 325 | display: none; 326 | width: 160px; 327 | height: 100%; 328 | } 329 | 330 | .CX_OptionList.active { 331 | display: inline-block; 332 | } 333 | 334 | .CX_Option { 335 | padding: 4px 5px; 336 | border-bottom: 1px solid #D5D5D5; 337 | cursor: pointer; 338 | transition: background-color 0.3s ease-out; 339 | background-color: #fff; 340 | } 341 | 342 | .CX_Option.active { 343 | background-color: #860281 !important; 344 | color: #fff !important; 345 | } 346 | 347 | .CX_Option.active .CX_Shortcut { 348 | color: #FF63F9 !important; 349 | } 350 | 351 | .CX_Section { 352 | padding: 4px 5px; 353 | border-bottom: 1px solid #D5D5D5; 354 | cursor: pointer; 355 | transition: background-color 0.3s ease-out; 356 | position: relative; 357 | } 358 | 359 | .CX_Section.empty { 360 | color: #C7C7C7; 361 | } 362 | 363 | .CX_Children { 364 | display: none; 365 | margin-top: 0.2em; 366 | } 367 | 368 | .open .CX_Children { 369 | display: block; 370 | } 371 | 372 | .CX_Detail_Area { 373 | display: none; 374 | width: 350px; 375 | height: 100%; 376 | } 377 | 378 | .CX_Detail_Area.active { 379 | display: inline-block; 380 | } 381 | 382 | #CX_Details_Time { 383 | min-height: 100px; 384 | } 385 | 386 | .active > .CX_Option:hover, .active > .CX_Section:hover, .CX_Section.open { 387 | background-color: #FDC4FC; 388 | } 389 | 390 | .CX_Shortcut { 391 | color: #D000C8; 392 | font-weight: 900; 393 | } 394 | 395 | #CX_Options_Video.active .CX_Shortcut { 396 | color: #D000C8; 397 | } 398 | 399 | #CX_Options_Video .CX_Shortcut { 400 | color: #FF7EFA; 401 | } 402 | 403 | .CX_Section.empty .CX_Shortcut { 404 | color: #FF7EFA !important; 405 | } 406 | 407 | .mono { 408 | font-family: monospace; 409 | } 410 | 411 | .openable { 412 | position: absolute; 413 | top: 0.15em; 414 | right: 0.3em; 415 | width: 0; 416 | height: 0; 417 | border-style: solid; 418 | border-width: 0.7em 0 0.7em 15px; 419 | border-color: transparent transparent transparent #C5C5C5; 420 | } 421 | 422 | .empty .openable { 423 | display: none; 424 | } 425 | 426 | .CX_Section.open > .openable { 427 | display: none; 428 | } 429 | 430 | #StartBlock { 431 | left: 0; 432 | } 433 | 434 | #EndBlock { 435 | right: 0; 436 | } 437 | 438 | #TestBlock { 439 | left: 15em; 440 | width: 1em; 441 | } 442 | 443 | .noplayBlock { 444 | background-color: #6A7488; 445 | height: 100%; 446 | position: absolute; 447 | } 448 | 449 | .seekHandle { 450 | position: absolute; 451 | display: none; 452 | cursor: -webkit-grab; 453 | min-height: 1.2em; 454 | width: 0; 455 | } 456 | 457 | .noplayBlock:hover .seekHandle, .noplayBlock.showHandles .seekHandle { 458 | display: inline-block; 459 | } 460 | 461 | .seekHandle div { 462 | position: absolute; 463 | width: 0; 464 | height: 0; 465 | border-style: solid; 466 | } 467 | 468 | .seekHandle span { 469 | background-color: #343434; 470 | padding: 0.1em 0.2em; 471 | color: #fff; 472 | box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.58); 473 | } 474 | 475 | .startHandle { 476 | bottom: -26px; 477 | left: 0; 478 | } 479 | 480 | .startHandle div { 481 | border-width: 21px 0 0 0.4em; 482 | border-color: transparent transparent transparent #343434; 483 | top: -22px; 484 | left: 0; 485 | } 486 | 487 | .startHandle span, #EndBlock .startHandle span { 488 | border-radius: 0 0.2em 0.2em 0.2em; 489 | } 490 | 491 | .endHandle { 492 | bottom: -53px; 493 | right: 0; 494 | text-align: right; 495 | } 496 | 497 | .endHandle div { 498 | border-width: 47px 0.4em 0 0; 499 | border-color: transparent #343434 transparent transparent; 500 | top: -49px; 501 | right: 0; 502 | } 503 | 504 | .endHandle span, #StartBlock .endHandle span { 505 | position: absolute; 506 | top: -2px; 507 | right: 0; 508 | border-radius: 0.2em 0 0.2em 0.2em; 509 | } 510 | 511 | #StartBlock .endHandle { 512 | right: 0; 513 | bottom: -26px; 514 | } 515 | 516 | #StartBlock .endHandle div { 517 | border-width: 17px 0.4em 0 0; 518 | top: -19px; 519 | } 520 | 521 | #EndBlock .startHandle { 522 | bottom: -26px; 523 | right: 100%; 524 | } 525 | 526 | #EndBlock .startHandle div { 527 | border-width: 17px 0 0 0.4em; 528 | border-color: transparent transparent transparent #343434; 529 | top: -19px; 530 | left: 0; 531 | } 532 | -------------------------------------------------------------------------------- /winstaller.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /Controller/ThumbWarrior.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require ('path'); 3 | var fs = require ('fs'); 4 | var lwip = require ('lwip'); 5 | var async = require ('async'); 6 | var mkdirp = require ('mkdirp'); 7 | var getType = require ('image-type'); 8 | var chimera = require ('wcjs-renderer'); 9 | 10 | var gui = global.window.nwDispatcher.requireNwGui(); 11 | 12 | 13 | /** @module PornViewer:ThumbWarrior 14 | 15 | */ 16 | 17 | var ZOOM_INTO_MIN = 3 / 4; 18 | var ZOOM_INTO_MAX = 4 / 3; 19 | var MAX_CLIP = 0.20; 20 | var THUMB_SIZE = 150; 21 | var VID_THUMB_WIDTH = 126; 22 | var VID_THUMB_HEIGHT = 130; 23 | var CWD = process.cwd(); 24 | var THUMBS_DIR = path.join (gui.App.dataPath, 'thumbs'); 25 | var IMAGE_EXT = [ '.jpg', '.jpeg', '.png', '.gif' ]; 26 | var VID_THUMB = 'file://' + path.join (__dirname, 'video_thumb.png'); 27 | var VID_THUMB_TIMEOUT = 10000; 28 | mkdirp.sync (THUMBS_DIR); 29 | 30 | function FingerTrap (width) { 31 | this.width = width || 1; 32 | this.taken = 0; 33 | this.queue = []; 34 | this.isPaused = false; 35 | }; 36 | FingerTrap.prototype.take = function (callback) { 37 | if (!this.isPaused && this.taken < this.width) { 38 | this.taken++; 39 | process.nextTick (callback); 40 | return; 41 | } 42 | this.queue.push (callback); 43 | }; 44 | FingerTrap.prototype.free = function(){ 45 | if (!this.queue.length || this.isPaused) 46 | this.taken--; 47 | else 48 | process.nextTick (this.queue.shift()); 49 | }; 50 | FingerTrap.prototype.pause = function(){ 51 | this.isPaused = true; 52 | }; 53 | FingerTrap.prototype.play = function(){ 54 | this.isPaused = false; 55 | while (this.queue.length && this.taken < this.width) { 56 | this.taken++; 57 | process.nextTick (this.queue.shift()); 58 | } 59 | }; 60 | 61 | // these are global, not per-warrior 62 | var IMG_THUMB_LOCK = new FingerTrap (8); 63 | var VID_THUMB_LOCK = new FingerTrap (1); 64 | 65 | function ThumbWarrior (document) { 66 | this.document = document; 67 | 68 | var waitingForOverlays = 3; 69 | var waitingCallback; 70 | var alreadyDone = false; 71 | 72 | var videoThumbOverlay = this.videoThumbOverlay = document.createElement ('img'); 73 | videoThumbOverlay.setAttribute ('style', 'position:fixed;left:-1000px;width:150px;height:150px;'); 74 | videoThumbOverlay.setAttribute ('src', 'video_overlay.png'); 75 | document.body.appendChild (videoThumbOverlay); 76 | 77 | var videoLeftGutter = this.videoLeftGutter = document.createElement ('img'); 78 | videoLeftGutter.setAttribute ('src', 'video_left_overlay.png'); 79 | 80 | var videoRightGutter = this.videoRightGutter = document.createElement ('img'); 81 | videoRightGutter.setAttribute ('src', 'video_right_overlay.png'); 82 | 83 | this.workingCanvas = document.createElement ('canvas'); 84 | this.workingCanvas.setAttribute ('style', 'position:fixed;left:-1000px;width:150px;height:150px;'); 85 | document.body.appendChild (this.workingCanvas); 86 | this.targetCanvas = document.createElement ('canvas'); 87 | this.targetCanvas.setAttribute ('style', 'position:fixed;left:-1000px;width:150px;height:150px;'); 88 | this.targetCanvas.setAttribute ('width', THUMB_SIZE); 89 | this.targetCanvas.setAttribute ('height', THUMB_SIZE); 90 | document.body.appendChild (this.targetCanvas); 91 | this.finalCanvas = document.createElement ('canvas'); 92 | this.finalCanvas.setAttribute ('style', 'position:fixed;left:-1000px;width:150px;height:150px;'); 93 | document.body.appendChild (this.finalCanvas); 94 | } 95 | 96 | ThumbWarrior.prototype.processThumb = function (filepath, thumbpath, callback) { 97 | IMG_THUMB_LOCK.take (function(){ 98 | var finalImage; 99 | var stats = {}; 100 | async.parallel ([ 101 | function (callback) { 102 | fs.readFile (filepath, function (err, buf) { 103 | if (err) 104 | return callback (err); 105 | imageType = getType (buf); 106 | if (!imageType) 107 | return callback (new Error ('not a known image format')); 108 | stats.type = imageType.ext; 109 | lwip.open (buf, imageType.ext, function (err, image) { 110 | if (err) 111 | return callback (err); 112 | finalImage = image; 113 | 114 | var width = image.width(); 115 | var height = image.height(); 116 | stats.width = width; 117 | stats.height = height; 118 | stats.pixels = width * height; 119 | 120 | if (width <= THUMB_SIZE && height <= THUMB_SIZE) 121 | return callback(); 122 | 123 | if (width == height) 124 | return image.resize (150, 150, function (err, image) { 125 | if (err) 126 | return callback (err); 127 | finalImage = image; 128 | callback(); 129 | }); 130 | 131 | // var top, right, bottom, left, scale; 132 | var finalWidth, finalHeight; 133 | if (width > height) { 134 | var maxClip = width * MAX_CLIP; 135 | newWidth = Math.max (width - maxClip, height); 136 | newHeight = height; 137 | scale = THUMB_SIZE / newWidth; 138 | } else { 139 | var maxClip = width * MAX_CLIP; 140 | newHeight = Math.max (height - maxClip, width); 141 | newWidth = width; 142 | scale = THUMB_SIZE / newHeight; 143 | } 144 | 145 | // finalize the transform 146 | var batch = image.batch() 147 | .crop (newWidth, newHeight) 148 | .scale (scale) 149 | ; 150 | batch.exec (function (err, image) { 151 | if (err) 152 | return callback (err); 153 | finalImage = image; 154 | callback(); 155 | }); 156 | }); 157 | }); 158 | }, 159 | function (callback) { 160 | fs.stat (filepath, function (err, filestats) { 161 | if (err) 162 | return callback (err); 163 | stats.size = filestats.size; 164 | stats.created = filestats.ctime.getTime(); 165 | stats.modified = filestats.mtime.getTime(); 166 | callback(); 167 | }); 168 | } 169 | ], function (err) { 170 | IMG_THUMB_LOCK.free(); 171 | if (err) 172 | return callback (err, undefined, stats); 173 | 174 | // write the thumbnail data to disc 175 | finalImage.writeFile (thumbpath, 'png', function (err) { 176 | if (err) 177 | return callback (err, undefined, stats); 178 | var finalHeight = finalImage.height(); 179 | var pad = finalHeight < THUMB_SIZE ? Math.floor ((THUMB_SIZE - finalHeight) / 2) : 0; 180 | callback (undefined, pad, stats); 181 | }); 182 | }); 183 | }); 184 | }; 185 | 186 | ThumbWarrior.prototype.processVideoThumb = function (filepath, thumbpath, callback) { 187 | var self = this; 188 | VID_THUMB_LOCK.take (function(){ 189 | var finalImage, thumbHeight; 190 | var stats = { type:'video' }; 191 | async.parallel ([ 192 | function (callback) { 193 | self.targetCanvas.getContext ('2d').clearRect (0, 0, self.targetCanvas.width, self.targetCanvas.height); 194 | self.finalCanvas.getContext ('2d').clearRect (0, 0, self.finalCanvas.width, self.finalCanvas.height); 195 | 196 | var alreadyDone = false; 197 | var vlc = chimera.init (self.workingCanvas, [], { preserveDrawingBuffer:true }); 198 | vlc.audio.mute = true; 199 | vlc.play ('file:///' + filepath); 200 | // wait for the second frame after seeking 201 | var didSeek = false; 202 | var targetTime; 203 | var armed = false; 204 | var fname = path.parse (filepath).base; 205 | vlc.onerror = function (error) { 206 | console.log ('vlc error', error); 207 | }; 208 | function cancelCall(){ 209 | if (alreadyDone) 210 | return; 211 | alreadyDone = true; 212 | vlc.stop(); 213 | callback (new Error ('failed to render any frames within timeout')); 214 | } 215 | var cancellationTimeout = setTimeout (cancelCall, VID_THUMB_TIMEOUT); 216 | async.parallel ([ 217 | function (callback) { 218 | vlc.events.once ('LengthChanged', function (length) { 219 | stats.length = length; 220 | callback(); 221 | }); 222 | }, 223 | function (callback) { 224 | vlc.events.on ('FrameReady', function (frame) { 225 | if (alreadyDone) { 226 | vlc.stop(); 227 | return; 228 | } 229 | if (!didSeek) { 230 | didSeek = true; 231 | targetTime = Math.floor (vlc.time = vlc.length * 0.2); 232 | clearTimeout (cancellationTimeout); 233 | cancellationTimeout = setTimeout (cancelCall, VID_THUMB_TIMEOUT); 234 | return; 235 | } 236 | if (vlc.time < targetTime) 237 | return; 238 | if (!armed) { 239 | armed = vlc.time; 240 | return; 241 | } 242 | // must advance PAST the arming frame 243 | if (vlc.time <= armed) 244 | return; 245 | clearTimeout (cancellationTimeout); 246 | vlc.stop(); 247 | alreadyDone = true; 248 | 249 | stats.width = frame.width; 250 | stats.height = frame.height; 251 | stats.pixels = stats.width * stats.height; 252 | 253 | var wideRatio = VID_THUMB_WIDTH / frame.width; 254 | var tallRatio = VID_THUMB_HEIGHT / frame.height; 255 | var context = self.targetCanvas.getContext ('2d'); 256 | var newWidth, newHeight; 257 | if (wideRatio < tallRatio) { 258 | newWidth = Math.floor (frame.width * wideRatio); 259 | newHeight = Math.floor (frame.height * wideRatio); 260 | } else { 261 | newWidth = Math.floor (frame.width * tallRatio); 262 | newHeight = Math.floor (frame.height * tallRatio); 263 | } 264 | try { 265 | context.drawImage ( 266 | self.workingCanvas, 267 | Math.floor ((THUMB_SIZE - newWidth) / 2), 268 | 0, 269 | newWidth, 270 | newHeight 271 | ); 272 | if (newWidth == VID_THUMB_WIDTH) { 273 | context.drawImage (self.videoThumbOverlay, 0, 0); 274 | self.finalCanvas.setAttribute ('width', THUMB_SIZE); 275 | newWidth = THUMB_SIZE; 276 | } else { 277 | var gutter = (THUMB_SIZE - VID_THUMB_WIDTH) / 2; 278 | context.drawImage ( 279 | self.videoLeftGutter, 280 | Math.floor ((THUMB_SIZE - newWidth) / 2) - gutter, 281 | 0 282 | ); 283 | context.drawImage ( 284 | self.videoRightGutter, 285 | Math.floor ((THUMB_SIZE + newWidth) / 2) - self.videoRightGutter.width + gutter, 286 | 0 287 | ); 288 | newWidth += THUMB_SIZE - VID_THUMB_WIDTH 289 | self.finalCanvas.setAttribute ('width', newWidth); 290 | } 291 | self.finalCanvas.setAttribute ('height', newHeight); 292 | self.finalCanvas.getContext ('2d').drawImage ( 293 | self.targetCanvas, 294 | Math.floor ((THUMB_SIZE - newWidth) / 2), 295 | 0, 296 | newWidth, 297 | newHeight, 298 | 0, 299 | 0, 300 | newWidth, 301 | newHeight 302 | ); 303 | finalImage = new Buffer ( 304 | self.finalCanvas.toDataURL().replace(/^data:image\/\w+;base64,/, ""), 305 | 'base64' 306 | ); 307 | } catch (err) { 308 | return callback (err); 309 | } 310 | thumbHeight = newHeight; 311 | vlc.stop(); 312 | callback(); 313 | }); 314 | } 315 | ], callback); 316 | }, 317 | function (callback) { 318 | fs.stat (filepath, function (err, filestats) { 319 | if (err) 320 | return callback (err); 321 | stats.size = filestats.size; 322 | stats.created = filestats.ctime.getTime(); 323 | stats.modified = filestats.mtime.getTime(); 324 | callback(); 325 | }); 326 | } 327 | ], function (err) { 328 | VID_THUMB_LOCK.free(); 329 | if (err) 330 | return callback (err, undefined, stats); 331 | 332 | // write the thumbnail data to disc and update the thumbnail database 333 | fs.writeFile (thumbpath, finalImage, function (err) { 334 | if (err) 335 | return callback (err); 336 | if (thumbHeight < VID_THUMB_HEIGHT) 337 | pad = Math.floor ((VID_THUMB_HEIGHT - thumbHeight) / 2); 338 | callback (undefined, pad, stats); 339 | }); 340 | }); 341 | }); 342 | }; 343 | 344 | module.exports = ThumbWarrior; 345 | -------------------------------------------------------------------------------- /Controller/scum.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (window) { 3 | 4 | /** @property/Function Object.addPermanent 5 | Attaches a non-enumerable static property to an arbitrary Object. Usually used to attach 6 | nefarious extra methods to native prototypes. Be aware that every time you do this, a small part 7 | of your integrity is permenantly removed. 8 | @argument/Object target 9 | Recipient of the non-enumerable properties. 10 | @argument/String 11 | */ 12 | function addPermanent (target, name, value) { 13 | try { 14 | Object.defineProperty (target, name, { 15 | 'enumerable': false, 16 | 'configurable': false, 17 | 'writable': true, 18 | 'value': value 19 | }); 20 | } catch (err) { 21 | console.trace(); 22 | } 23 | return target; 24 | }; 25 | addPermanent (Object, 'addPermanent', addPermanent); 26 | 27 | /* @member/Function window.Element#addClass 28 | Add a css classname to the class attribute. 29 | @name Element#addClass 30 | @function 31 | @param {String} classname A string representing the class. Assumed to be a 32 | single, valid class name. 33 | @returns {Element} self. 34 | */ 35 | Object.addPermanent (window.Element.prototype, "addClass", function (classname) { 36 | var current = this.className.length ? this.className.split (' ') : []; 37 | if (current.indexOf (classname) >= 0) return this; 38 | current.push (classname); 39 | this.className = current.join (" "); 40 | return this; 41 | }); 42 | 43 | 44 | /** 45 | Drop a css classname from the class attribute. 46 | @name Element#dropClass 47 | @function 48 | @param {String} classname A string representing the class. Assumed to be a 49 | single, valid class name. 50 | @returns {Element} self. 51 | */ 52 | Object.addPermanent (window.Element.prototype, "dropClass", function (classname) { 53 | var current = this.className.length ? this.className.split (' ') : []; 54 | var i = current.indexOf (classname); 55 | if (i < 0) return this; 56 | current.splice (i, 1); 57 | this.className = current.join (" "); 58 | return this; 59 | }); 60 | 61 | 62 | /** 63 | Test whether a given class has been set. 64 | @name Element#hasClass 65 | @function 66 | @param {String} classname The classname to search for. 67 | @returns {Boolean} Whether the classname exists on this Element. 68 | */ 69 | Object.addPermanent (window.Element.prototype, "hasClass", function (classname) { 70 | var current = this.className.length ? this.className.split (' ') : []; 71 | return current.indexOf(classname) >= 0 ? true : false; 72 | }); 73 | 74 | 75 | /** 76 | Set the exact list of css classes on an Element with an Array. 77 | @name Element#setClass 78 | @function 79 | @param {Array[String]} classname An array of css classes to be set on this Element. 80 | @returns {Element} self. 81 | */ 82 | Object.addPermanent (window.Element.prototype, "setClass", function (classname) { 83 | if (!(classname instanceof Array)) 84 | classname = Array.prototype.slice.call (arguments); 85 | this.className = classname.join (" "); 86 | return this; 87 | }); 88 | 89 | /** @property/Object Object.DROP_LISTENER 90 | This constant is used with event listeners. Throwing it during an event handler will efficiently 91 | dequeue the handler. 92 | */ 93 | var DROP_LISTENER = {}; 94 | Object.addPermanent (Object, 'DROP_LISTENER', DROP_LISTENER); 95 | 96 | 97 | function addEventListener (event, call) { 98 | var listeners; 99 | if (!this._listeners) { 100 | this._listeners = {}; 101 | listeners = this._listeners[event] = [ call ]; 102 | } else { 103 | if (Object.hasOwnProperty.call (this._listeners, event)) { 104 | this._listeners[event].push (call); 105 | // the event listener function was already created 106 | return this; 107 | } 108 | listeners = this._listeners[event] = [ call ]; 109 | } 110 | 111 | // the listener chain for this event was empty until now 112 | // add a property to catch DOM events. 113 | // Instead of keeping up with all possible DOM events, just make props for all events. 114 | var elem = this; 115 | this["on"+event] = function(){ 116 | var ok = true; 117 | for (var i=0, j=listeners.length; i OPTIMIZE_APPEND_DOC_FRAG) { 285 | var frag = window.document.createDocumentFragment(); 286 | for (var i=0,j=contents.length-1; i=0; i--) 293 | this.parentNode.insertBefore (contents[i], anchor); 294 | return this; 295 | }); 296 | 297 | 298 | /** @member/Function Node#dispose 299 | Remove this Node or Element from the DOM. Just a little more elegant than calling 300 | `elem.parentNode.removeChild (elem);`. 301 | @returns/Node 302 | Self. 303 | */ 304 | Object.addPermanent (window.Node.prototype, "dispose", function(){ 305 | if (this.parentNode) this.parentNode.removeChild (this); 306 | return this; 307 | }); 308 | 309 | 310 | /** @member/Function Element#disposeChildren 311 | @synonym Element#dropChildren 312 | Convenience method to call `dispose` on all child nodes. 313 | @returns/Element 314 | Self. 315 | */ 316 | Object.addPermanent (window.Element.prototype, "disposeChildren", function(){ 317 | while (this.firstChild) 318 | this.removeChild (this.firstChild); 319 | return this; 320 | }); 321 | Object.addPermanent ( 322 | window.Element.prototype, 323 | "dropChildren", 324 | window.Element.prototype.disposeChildren 325 | ); 326 | 327 | 328 | /** @member/Function Node#appendText 329 | Append a text to this Node with the provided content. When called on an Element, the last Node 330 | is used, if it is a textual Node, or a new Text Node will be created and appended. 331 | @argument/String text 332 | Text content to append. 333 | @returns Node 334 | The Text Node containing the appended content. 335 | */ 336 | Object.addPermanent (window.Node.prototype, "appendText", function (text) { 337 | this.textContent = this.textContent + text; 338 | return this; 339 | }); 340 | Object.addPermanent (window.Element.prototype, "appendText", function (text) { 341 | if (Object.typeStr(this.lastChild) == 'textnode') { 342 | this.lastChild.textContent = this.lastChild.textContent + text; 343 | return this; 344 | } 345 | var newNode = window.document.createTextNode (text); 346 | this.appendChild (newNode); 347 | return this; 348 | }); 349 | 350 | 351 | /** @member/Function Element#append 352 | Synonym for native `appendChild`, except it accepts any number of Node arguments, or an Array 353 | of Nodes. 354 | @argument/Array contents 355 | @optional 356 | An Array of Nodes to append to this Element. Mutually exclusive with the `newChild` argument(s). 357 | @argument/Node newChild 358 | @optional 359 | Any number of Nodes to append to this Element. Mutually exclusive with the `contents` argument. 360 | @returns/Element 361 | Self. 362 | */ 363 | Object.addPermanent (window.Element.prototype, "append", function (contents) { 364 | if (!contents) // called without args, append nothing 365 | return this; 366 | 367 | var contentsType = Object.typeStr (contents); 368 | if (contentsType != 'array' && contentsType != 'nodelist') 369 | contents = Array.prototype.slice.call (arguments); 370 | 371 | if (contents.length > OPTIMIZE_APPEND_DOC_FRAG) { 372 | var frag = window.document.createDocumentFragment(); 373 | for (var i=0,j=contents.length; i OPTIMIZE_APPEND_DOC_FRAG) { 407 | var frag = window.document.createDocumentFragment(); 408 | for (var i=0,j=contents.length; i OPTIMIZE_APPEND_DOC_FRAG) { 453 | var frag = window.document.createDocumentFragment(); 454 | for (var i=0,j=contents.length; i OPTIMIZE_APPEND_DOC_FRAG) { 492 | var frag = window.document.createDocumentFragment(); 493 | for (var i in contents) 494 | frag.appendChild (contents[i]); 495 | if (sib) 496 | parent.insertBefore (frag, sib); 497 | else 498 | parent.appendChild (frag); 499 | return this; 500 | } 501 | 502 | if (sib) 503 | for (var i=0,j=contents.length; i=0; i--) 70 | if (reg[i] !== KONAMI[i]) 71 | return; 72 | winnder.showDevTools(); 73 | if (!mainWindowOpened) { 74 | mainWindowOpened = true; 75 | gui.Window.get().showDevTools(); 76 | } 77 | }); 78 | } 79 | 80 | // set up window position 81 | var winState = window.localStorage.winState; 82 | var screens = gui.Screen.Init().screens; 83 | if (winState) { 84 | winState = JSON.parse (winState); 85 | // make sure this state is still displayable 86 | 87 | // delete winState; 88 | } 89 | if (!winState) { 90 | if (screens.length > 1) { 91 | // use two monitors on the bottom right position 92 | screens.sort (function (able, baker) { 93 | var aX = able.work_area.x; 94 | var bX = baker.work_area.x; 95 | if (aX < bX) 96 | return -1; 97 | if (aX > bX) 98 | return 1; 99 | var aY = able.work_area.y; 100 | var bY = baker.work_area.y; 101 | if (aY < bY) 102 | return -1; 103 | if (aY > bY) 104 | return 1; 105 | return 0; 106 | }); 107 | var controllerState = screens[screens.length-2].work_area; 108 | var visualizerState = screens[screens.length-1].work_area; 109 | winState = { 110 | controller: { 111 | maximize: true, 112 | x: controllerState.x, 113 | y: controllerState.y, 114 | width: Math.floor (0.7 * controllerState.width), 115 | height: Math.floor (0.7 * controllerState.height) 116 | }, 117 | visualizer: { 118 | maximize: true, 119 | x: visualizerState.x, 120 | y: visualizerState.y, 121 | width: Math.floor (0.7 * visualizerState.width), 122 | height: Math.floor (0.7 * visualizerState.height) 123 | } 124 | }; 125 | } else if (screens.length != 1) { 126 | winState = { 127 | controller:{ x:0, y:0, width:CONTROLLER_MIN_WIDTH, height: 600 }, 128 | visualizer:{ x:CONTROLLER_MIN_WIDTH, y:0, width:800 - CONTROLLER_MIN_WIDTH, height: 600 } 129 | }; 130 | } else { 131 | var onlyScreen = screens[0].work_area; 132 | var maxControllerWidth = Math.floor (onlyScreen.width / 2); 133 | var controllerWidth; 134 | if (maxControllerWidth <= CONTROLLER_MIN_WIDTH) 135 | controllerWidth = CONTROLLER_MIN_WIDTH; 136 | else { 137 | var rowCount = Math.floor (( maxControllerWidth - CONTROLLER_BASE_WIDTH ) / 150); 138 | controllerWidth = CONTROLLER_BASE_WIDTH + ( 150 * rowCount ); 139 | } 140 | winState = { 141 | controller: { 142 | x: onlyScreen.x, 143 | y: onlyScreen.y, 144 | width: controllerWidth, 145 | height: onlyScreen.height 146 | }, 147 | visualizer: { 148 | x: onlyScreen.x + controllerWidth, 149 | y: onlyScreen.y, 150 | width: onlyScreen.width - controllerWidth, 151 | height: onlyScreen.height 152 | } 153 | }; 154 | 155 | } 156 | } 157 | 158 | var Window = gui.Window.get(); 159 | var beggarArmed = false; 160 | var alreadyRan = false; 161 | Window.on ('loaded', function(){ 162 | if (alreadyRan) 163 | return; 164 | alreadyRan = true; 165 | 166 | scum (Window.window); 167 | var dorkument = Window.window.document; 168 | 169 | // uncomment to show the primary console at startup 170 | // Window.showDevTools(); 171 | 172 | // show beggar window immediately 173 | // Window.show(); 174 | 175 | // comment out all this stuff when working on the beggar 176 | var nextNag = window.localStorage.nag; 177 | if (nextNag == 'NEVER') 178 | return; 179 | if (!nextNag) { 180 | window.localStorage.nag = 10; 181 | return; 182 | } 183 | nextNag = Number (nextNag); 184 | if (nextNag > 0) { 185 | window.localStorage.nag = nextNag - 1; 186 | return; 187 | } 188 | window.localStorage.nag = 10; 189 | beggarArmed = true; 190 | 191 | // setup the beggar for display later 192 | var payAmount = dorkument.getElementById ('PayAmount'); 193 | payAmount.on ('keypress', function (event) { 194 | var current = payAmount.value; 195 | var next = current + String.fromCharCode (event.charCode); 196 | var nextNum = Number (next); 197 | if (isNaN (nextNum)) 198 | return false; 199 | return true; 200 | }); 201 | 202 | var payFrame = dorkument.getElementById ('PayFrame'); 203 | var throbber = dorkument.getElementById ('NetworkThrobber'); 204 | payFrame.contentWindow.setup ( 205 | function (token) { 206 | throbber.addClass ('active'); 207 | needle.post ( 208 | 'http://kaztl.com/payment', 209 | { 210 | email: token.email, 211 | token: token.id, 212 | cents: Math.floor (payAmount.value * 100) 213 | }, 214 | { json:true, parse:'json' }, 215 | function (err, response) { 216 | throbber.dropClass ('active'); 217 | if (err) { 218 | window.alert ( 219 | '\ 220 | A network error occured. Please double check your internet connection \ 221 | and try again!' 222 | ); 223 | return; 224 | } 225 | if (response.statusCode != 200) { 226 | window.alert ( 227 | 'An error occured: ' 228 | + response.body.error 229 | + '\nPlease try again!' 230 | ); 231 | return; 232 | } 233 | 234 | // woo! thanks for the moneys! 235 | window.localStorage.nag = 'NEVER'; 236 | window.document.body.innerHTML = '

Thank You!

\n\ 237 |

Open source software is this developer\'s day job. Therefor, each donation is deeply appreciated \ 238 | no matter how big or small. And remember, it\'s all for a good cause: more porn!

\n\ 239 |

And don\'t worry, these guilt-trip naggy messages begging for money will never appear again!

'; 240 | setTimeout (function(){ 241 | Window.close(); 242 | setTimeout (function(){ 243 | gui.App.quit(); 244 | }, 50); 245 | }, 15000); 246 | } 247 | ); 248 | }, 249 | function(){ 250 | payFrame.className = 'active'; 251 | payAmount.setAttribute ('disabled', true); 252 | }, 253 | function(){ 254 | payFrame.className = ''; 255 | payAmount.setAttribute ('disabled', false); 256 | } 257 | ); 258 | dorkument.getElementById ('PayMe').on ('click', function(){ 259 | try { 260 | var innerDocument = payFrame.contentDocument.body.children[0].contentDocument; 261 | var newStyle = innerDocument.createElement ('style'); 262 | newStyle.innerHTML = ".overlayView.active { overflow:hidden !important; }"; 263 | innerDocument.head.appendChild (newStyle); 264 | } catch (err) { 265 | console.log ('could not tweak pay frame document', err); 266 | } 267 | payFrame.contentWindow.handle (Math.floor (payAmount.value * 100)); 268 | }); 269 | 270 | var alreadyPaidButton = dorkument.getElementById ('EndAnnoyanceButton'); 271 | var alreadyPaidCollapso = dorkument.getElementById ('EndAnnoyanceCollapso'); 272 | alreadyPaidButton.on ('click', function(){ 273 | alreadyPaidCollapso.addClass ('active'); 274 | alreadyPaidButton.dispose(); 275 | }); 276 | var lookupPaymentEmail = dorkument.getElementById ('AlreadyPaidEmail'); 277 | var lookupPaymentButton = dorkument.getElementById ('AlreadyPaidButton'); 278 | lookupPaymentButton.on ('click', function(){ 279 | lookupPaymentEmail.setAttribute ('disabled', true); 280 | throbber.addClass ('active'); 281 | needle.get ( 282 | 'http://kaztl.com/payment?email=' + encodeURIComponent (lookupPaymentEmail.value), 283 | { parse:'json' }, 284 | function (err, response) { 285 | lookupPaymentEmail.setAttribute ('disabled', false); 286 | throbber.dropClass ('active'); 287 | if (err) { 288 | window.alert ( 289 | '\ 290 | A network error occured. Please double check your internet connection \ 291 | and try again!' 292 | ); 293 | return; 294 | } 295 | if (response.statusCode != 200) { 296 | if (response.statusCode == 404) 297 | window.alert ( 298 | 'Your payment could not be found. Are you sure this is the email ' 299 | + 'address you used to pay before, and have you typed it correctly?' 300 | ); 301 | else 302 | window.alert ( 303 | 'An error occured: ' + response.body.error + '\n\nPlease try again!' 304 | ); 305 | return; 306 | } 307 | 308 | window.localStorage.nag = 'NEVER'; 309 | window.document.body.innerHTML = '

Nagging Deactivated

\n\ 310 |

Your nag messages have been permanently deactivated. But by the way: Open source software is \ 311 | this developer\'s day job. The more you give, the better your porn viewing experience can become!\ 312 |

'; 313 | setTimeout (function(){ 314 | Window.close(); 315 | setTimeout (function(){ 316 | gui.App.quit(); 317 | }, 50); 318 | }, 15000); 319 | } 320 | ); 321 | }); 322 | }); 323 | 324 | // basic cross-window event listeners 325 | controllerWindow.on ('close', shutdown); 326 | visualizerWindow.on ('close', shutdown); 327 | 328 | var dead = false; 329 | function shutdown(){ 330 | if (dead) 331 | return; 332 | dead = true; 333 | 334 | window.localStorage.winState = JSON.stringify ({ 335 | controller: { 336 | x: controllerWindow.x, 337 | y: controllerWindow.y, 338 | width: controllerWindow.width, 339 | height: controllerWindow.height, 340 | maximize: controllerWindow.window.document 341 | .getElementById ('Maximize').hasClass ('restore') 342 | }, 343 | visualizer: { 344 | x: visualizerWindow.x, 345 | y: visualizerWindow.y, 346 | width: visualizerWindow.width, 347 | height: visualizerWindow.height, 348 | maximize: visualizerWindow.window.document 349 | .getElementById ('Maximize').hasClass ('restore') 350 | }, 351 | }); 352 | 353 | if (controller) 354 | window.localStorage.prefs_dev_con = JSON.stringify (controller.prefs); 355 | if (visualizer) 356 | window.localStorage.prefs_dev_viz = JSON.stringify (visualizer.prefs); 357 | visualizer.savePron (function (err) { 358 | controllerWindow.close (true); 359 | visualizerWindow.close (true); 360 | if (beggarArmed) { 361 | beggarArmed = false; 362 | Window.show(); 363 | return; 364 | } 365 | setTimeout (function(){ 366 | gui.App.quit(); 367 | }, 50); 368 | }); 369 | } 370 | 371 | // load opened file or last path 372 | var openPath, openDir, openFilename, ext; 373 | if (gui.App.argv.length) { 374 | openPath = gui.App.argv[0]; 375 | // exists? directory? 376 | try { 377 | var stats = fs.statSync (openPath); 378 | if (stats.isDirectory()) 379 | openDir = openPath; 380 | else { 381 | var pathinfo = path.parse (openPath); 382 | openDir = pathinfo.dir; 383 | openFilename = pathinfo.base; 384 | if (Object.hasOwnProperty.call (SHOW_EXT, pathinfo.ext)) 385 | ext = pathinfo.ext.slice (1); 386 | else { 387 | delete openPath; 388 | // show an error message 389 | // TODO 390 | } 391 | } 392 | } catch (err) { /* fall through */ } 393 | } 394 | if (!openDir && !(openDir = window.localStorage.lastPath)) 395 | openDir = window.localStorage.lastPath = process.env[ 396 | process.platform = 'win32' ? 'USERPROFILE' : 'HOME' 397 | ]; 398 | 399 | function handleKey (event) { 400 | if (!event.altKey && !event.ctrlKey) { 401 | if (event.keyCode >= 37 && event.keyCode <= 40) { 402 | controller.go (event.keyCode); 403 | return false; 404 | } else if (event.keyCode == 32) { 405 | visualizer.playpause(); 406 | return false; 407 | } 408 | return true; 409 | } 410 | 411 | if (!visualizer.vlc) 412 | return true; 413 | var timeShift; 414 | switch (event.keyCode) { 415 | case 37: 416 | timeShift = -15 * 1000; 417 | break; 418 | case 38: 419 | timeShift = 90 * 1000; 420 | break; 421 | case 39: 422 | timeShift = 15 * 1000; 423 | break; 424 | case 40: 425 | timeShift = -90 * 1000; 426 | break; 427 | default: 428 | return true; 429 | } 430 | if (event.shiftKey) 431 | timeShift /= 3; 432 | visualizer.jump (timeShift); 433 | return false; 434 | } 435 | 436 | // load both windows 437 | async.parallel ([ 438 | function (callback) { 439 | controllerWindow.on ('loaded', function(){ 440 | scum (controllerWindow.window); 441 | konamifyWinnder (controllerWindow); 442 | controllerWindow.x = winState.controller.x; 443 | controllerWindow.y = winState.controller.y; 444 | controllerWindow.resizeTo ( 445 | winState.controller.width, 446 | winState.controller.height 447 | ); 448 | if (winState.controller.maximize) 449 | controllerWindow.maximize(); 450 | 451 | // setup controls 452 | // min - max - close 453 | controllerWindow.window.document.getElementById ('Minimize').on ('click', function(){ 454 | controllerWindow.minimize(); 455 | }); 456 | var maxElem = controllerWindow.window.document.getElementById ('Maximize'); 457 | var isMaximized = false; 458 | maxElem.on ('click', function(){ 459 | if (isMaximized) 460 | controllerWindow.unmaximize(); 461 | else 462 | controllerWindow.maximize(); 463 | }); 464 | controllerWindow.on ('maximize', function(){ 465 | isMaximized = true; 466 | maxElem.addClass ('restore'); 467 | }); 468 | controllerWindow.on ('unmaximize', function(){ 469 | isMaximized = false; 470 | maxElem.dropClass ('restore'); 471 | }); 472 | controllerWindow.window.document.getElementById ('Close').on ('click', shutdown); 473 | 474 | // controller ready 475 | callback(); 476 | }); 477 | }, 478 | function (callback) { 479 | visualizerWindow.on ('loaded', function(){ 480 | scum (visualizerWindow.window); 481 | konamifyWinnder (visualizerWindow); 482 | visualizerWindow.x = winState.visualizer.x; 483 | visualizerWindow.y = winState.visualizer.y; 484 | visualizerWindow.resizeTo ( 485 | winState.visualizer.width, 486 | winState.visualizer.height 487 | ); 488 | if (winState.visualizer.maximize) 489 | visualizerWindow.maximize(); 490 | var vizPrefs = window.localStorage.prefs_dev_viz; 491 | if (vizPrefs) 492 | vizPrefs = JSON.parse (vizPrefs) 493 | else 494 | vizPrefs = {}; 495 | visualizer = new Visualizer ( 496 | visualizerWindow, 497 | vizPrefs 498 | ); 499 | 500 | // setup controls 501 | // min - max - close 502 | visualizer.document.getElementById ('Minimize').on ('click', function(){ 503 | visualizerWindow.minimize(); 504 | }); 505 | var maxElem = visualizer.document.getElementById ('Maximize'); 506 | var isMaximized = false; 507 | maxElem.on ('click', function(){ 508 | if (isMaximized) 509 | visualizerWindow.unmaximize(); 510 | else 511 | visualizerWindow.maximize(); 512 | }); 513 | visualizerWindow.on ('maximize', function(){ 514 | isMaximized = true; 515 | maxElem.addClass ('restore'); 516 | }); 517 | visualizerWindow.on ('unmaximize', function(){ 518 | isMaximized = false; 519 | maxElem.dropClass ('restore'); 520 | }); 521 | visualizer.document.getElementById ('Close').on ('click', shutdown); 522 | 523 | // bump controls into view whenever the mouse moves 524 | var controlsTimer; 525 | var Theatre = visualizer.document.getElementById ('Theatre'); 526 | function bumpControls(){ 527 | clearTimeout (controlsTimer); 528 | visualizer.controlsElem.addClass ('visible'); 529 | Theatre.dropClass ('nocurse'); 530 | controlsTimer = setTimeout (function(){ 531 | visualizer.controlsElem.dropClass ('visible'); 532 | Theatre.addClass ('nocurse'); 533 | visualizer.modeSelect.blur(); 534 | }, CONTROLS_TIMEOUT); 535 | } 536 | visualizer.document.body.on ('mousemove', bumpControls); 537 | visualizer.controlsElem.on ('mousemove', bumpControls); 538 | 539 | // keyboard navigation events 540 | visualizer.document.body.on ('keydown', handleKey); 541 | 542 | // visualizer ready 543 | callback(); 544 | }); 545 | } 546 | ], function (err) { 547 | if (err) { 548 | // just fail 549 | gui.App.quit(); 550 | return; 551 | } 552 | 553 | // ready to start the controller now 554 | var conPrefs = window.localStorage.prefs_dev_con; 555 | if (conPrefs) 556 | conPrefs = JSON.parse (conPrefs) 557 | else 558 | conPrefs = {}; 559 | controller = new Controller ( 560 | controllerWindow, 561 | visualizer, 562 | conPrefs 563 | ); 564 | controller.document.body.on ('keydown', handleKey); 565 | controller.on ('display', function (prawn) { 566 | visualizer.display (prawn); 567 | }); 568 | controller.on ('preload', function (prawn) { 569 | visualizer.preload (prawn); 570 | }); 571 | 572 | // if launched as an Open command, open the requested file immediately 573 | if (openPath) { 574 | controller.selectedImagePath = openPath; 575 | var prawn = new Pron (controller.warrior, openDir, openFilename); 576 | visualizer.display (prawn); 577 | visualizerWindow.focus(); 578 | } 579 | 580 | // reveal current path 581 | controller.currentPath = openDir; 582 | controller.openCurrent (function (err) { 583 | if (err) 584 | return; 585 | if (openPath) 586 | controller.showImage (undefined, openPath); 587 | }); 588 | 589 | // wait for future file open operations 590 | function openFile (cmdline) { 591 | // exists? directory? 592 | var filename, oldPath = controller.currentPath; 593 | try { 594 | var openPath; 595 | if (process.platform == 'win32') 596 | openPath = /"([^"]+)"$/.exec (cmdline)[1]; 597 | else 598 | openPath = cmdline.split (/ /g)[1]; 599 | 600 | var stats = fs.statSync (openPath); 601 | if (stats.isDirectory()) 602 | controller.currentPath = openPath; 603 | else { 604 | var pathinfo = path.parse (openPath); 605 | controller.currentPath = pathinfo.dir; 606 | var ext = pathinfo.ext; 607 | if (Object.hasOwnProperty.call (SHOW_EXT, ext)) { 608 | controller.manualScrolling = false; 609 | controller.selectedImagePath = openPath; 610 | var prawn = new Pron (controller.warrior, pathinfo.dir, pathinfo.base); 611 | visualizer.display (prawn); 612 | visualizerWindow.focus(); 613 | } 614 | } 615 | } catch (err) { return false; } 616 | 617 | if (controller.currentPath === oldPath) { 618 | controller.showImage (undefined, openPath); 619 | return; 620 | } 621 | controller.openCurrent(function (err) { 622 | if (err) 623 | return; 624 | controller.showImage (undefined, openPath); 625 | }); 626 | 627 | return false; 628 | } 629 | gui.App.on ('open', openFile); 630 | 631 | controllerWindow.window.on ('dragover', function (event) { 632 | event.preventDefault(); 633 | return false; 634 | }); 635 | controllerWindow.window.on ('drop', function (event) { 636 | event.preventDefault(); 637 | var files = event.dataTransfer.files; 638 | if (!files.length) 639 | return false; 640 | openFile ('PornViewer "'+files[files.length-1].path+'"'); 641 | return false; 642 | }); 643 | 644 | visualizerWindow.window.on ('dragover', function (event) { 645 | event.preventDefault(); 646 | return false; 647 | }); 648 | visualizerWindow.window.on ('drop', function (event) { 649 | event.preventDefault(); 650 | var files = event.dataTransfer.files; 651 | if (!files.length) 652 | return false; 653 | openFile ('PornViewer "'+files[files.length-1].path+'"'); 654 | return false; 655 | }); 656 | }); 657 | -------------------------------------------------------------------------------- /Controller/index.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require ('fs'); 3 | var path = require ('path'); 4 | var EventEmitter = require ('events').EventEmitter; 5 | var util = require ('util'); 6 | var async = require ('async'); 7 | var surveil = require ('surveil'); 8 | var ThumbWarrior = require ('./ThumbWarrior'); 9 | // var Collection = require ('./Collection'); 10 | var Directory = require ('./Directory'); 11 | var FilenameSorter = require ('./FilenameSorter'); 12 | var Pron = require ('Pron'); 13 | 14 | var DRIVE_REGEX = /([\w ]+\w) +(\w:)/; 15 | var KNOWN_EXT = [ '.jpg', '.jpeg', '.png', '.gif', '.wmv', '.avi', '.mkv', '.rm', '.mp4', '.m4v' ]; 16 | var IMAGE_EXT = [ '.jpg', '.jpeg', '.png', '.gif' ]; 17 | var IMAGE_EXT_MAP = { '.jpg':true, '.jpeg':true, '.png':true, '.gif':true }; 18 | 19 | var UP = 38; 20 | var RIGHT = 39; 21 | var DOWN = 40; 22 | var LEFT = 37; 23 | function Controller (winnder, visualizer, prefs) { 24 | EventEmitter.call (this); 25 | this.window = winnder; 26 | this.document = winnder.window.document; 27 | this.visualizer = visualizer; 28 | this.warrior = new ThumbWarrior (this.document); 29 | this.prefs = prefs || {}; 30 | 31 | this.hostElem = this.document.getElementById ('Host'); 32 | var self = this; 33 | 34 | this.initialResizeClip = 100; // limit the initially rapid resize watchdog poll 35 | var currentWidth = this.document.body.clientWidth; 36 | var currentHeight = this.document.body.clientHeight; 37 | function resize (event) { 38 | if (currentWidth != self.document.body.clientWidth 39 | || currentHeight != self.document.body.clientHeight 40 | ) { 41 | currentWidth = self.document.body.clientWidth; 42 | currentHeight = self.document.body.clientHeight; 43 | self.revealDirectory(); 44 | self.revealThumb(); 45 | 46 | // handle interval timing 47 | if ( initialInterval && ( !event || !--self.initialResizeClip ) ) { 48 | clearInterval (initialInterval); 49 | initialInterval = undefined; 50 | delete initialInterval; 51 | setInterval (resize, 1000); 52 | } 53 | } 54 | } 55 | // when the window first resizes at startup, the resize event isn't sent. We have to poll. 56 | var initialInterval = setInterval (resize, 100); 57 | winnder.on ('resize', resize); 58 | 59 | // controls 60 | this.thumbsTop = this.document.getElementById ('Controls').getBoundingClientRect().bottom; 61 | this.sortSelect = this.document.getElementById ('Sort'); 62 | this.sortBy = this.prefs.sort || 'name'; 63 | this.sortSelect.value = this.sortBy; 64 | this.sortSelect.on ('change', function(){ 65 | self.prefs.sort = self.sortBy = self.sortSelect.value; 66 | if (self.sortBy == 'random') { 67 | self.thumbsElem.randomizeChildren(); 68 | return; 69 | } 70 | // sort existing 71 | var potemkin = []; 72 | Array.prototype.push.apply (potemkin, self.thumbsElem.children); 73 | var attr = 'data-'+self.sortBy; 74 | if (self.sortBy == 'name') 75 | potemkin.sort (function (able, baker) { 76 | aVal = able.getAttribute (attr); 77 | bVal = baker.getAttribute (attr); 78 | if (aVal === null) 79 | if (bVal === null) 80 | return 0; 81 | else 82 | return 1; 83 | else if (bVal === null) 84 | return -1; 85 | return FilenameSorter (aVal, bVal); 86 | }); 87 | else 88 | potemkin.sort (function (able, baker) { 89 | aVal = able.getAttribute (attr); 90 | bVal = baker.getAttribute (attr); 91 | if (aVal === null) 92 | if (bVal === null) 93 | return 0; 94 | else 95 | return 1; 96 | else if (bVal === null) 97 | return -1; 98 | aVal = Number (aVal); 99 | bVal = Number (bVal); 100 | if (aVal > bVal) 101 | return -1; 102 | if (aVal == bVal) 103 | return 0; 104 | return 1; 105 | }); 106 | for (var i=0,j=potemkin.length; i self.treeElem.clientHeight + self.treeTop) 251 | offset = Math.min ( 252 | position.bottom - self.treeElem.clientHeight - self.treeTop, 253 | position.top - self.treeTop 254 | ); 255 | if (!offset) 256 | return; 257 | self.treeElem.scrollTop += offset; 258 | }, 100); 259 | }; 260 | 261 | Controller.prototype.openCurrent = function (listed) { 262 | var pathArr = this.currentPath 263 | .split (process.platform == 'win32' ? /[\/\\]/g : /\//g) 264 | .filter (Boolean) 265 | ; 266 | if (process.platform != 'win32') 267 | pathArr[0] = '/'+pathArr[0]; 268 | var level = new Directory (this.root, this, pathArr[0], pathArr[0]); 269 | this.root.children[pathArr[0]] = level; 270 | level.open(); 271 | for (var i=1,j=pathArr.length; i= value) { 538 | this.thumbsElem.appendChild (container); 539 | return; 540 | } 541 | } else if (other !== null && FilenameSorter (other, value) <= 0) { 542 | this.thumbsElem.appendChild (container); 543 | return; 544 | } 545 | 546 | var middle = thumbs.length / 2; 547 | var step = middle; 548 | var next = Math.floor (middle); 549 | var i; 550 | var done = false; 551 | do { 552 | step /= 2; 553 | i = next; 554 | other = thumbs[i].getAttribute (attr); 555 | if (other === null) { 556 | next = Math.floor (middle -= step); 557 | continue; 558 | } 559 | if (isNumAttr) { 560 | other = Number (other); 561 | if (other <= value) { 562 | var prior = Number (thumbs[i-1].getAttribute (attr)); 563 | if (prior >= value) { 564 | this.thumbsElem.insertBefore (container, thumbs[i]); 565 | done = true; 566 | break; 567 | } 568 | next = Math.floor (middle -= step); 569 | continue; 570 | } 571 | next = Math.floor (middle += step); 572 | } else { 573 | if (FilenameSorter (value, other) <= 0) { 574 | var prior = thumbs[i-1].getAttribute (attr); 575 | if (FilenameSorter (prior, value) <= 0) { 576 | this.thumbsElem.insertBefore (container, thumbs[i]); 577 | done = true; 578 | break; 579 | } 580 | next = Math.floor (middle -= step); 581 | continue; 582 | } 583 | next = Math.floor (middle += step); 584 | } 585 | } while (i != next); 586 | if (!done) 587 | this.thumbsElem.insertBefore (container, thumbs[i+1]); 588 | }; 589 | 590 | /** @member/Function revealThumb 591 | If autoscrolling is enabled, scrolls the thumb container Element to reveal the [currently 592 | selected thumbnail](#selectedImage). 593 | */ 594 | Controller.prototype.revealThumb = function(){ 595 | if (this.manualScrolling) // don't autoscroll while the user is trying to scroll 596 | return; 597 | if (!this.thumbsElem || !this.selectedImage) 598 | return; 599 | 600 | var position = this.selectedImage.getBoundingClientRect(); 601 | var offset = 0; 602 | if (position.top < this.thumbsTop) 603 | offset = position.top - this.thumbsTop; 604 | else if (position.bottom > this.window.window.innerHeight) 605 | offset = position.bottom - this.window.window.innerHeight; 606 | 607 | if (offset > 0) 608 | offset = Math.floor (offset); 609 | else 610 | offset = Math.ceil (offset); 611 | if (!offset) 612 | return; 613 | 614 | this.autoScrolling = true; 615 | this.thumbsElem.scrollTop += offset; 616 | }; 617 | 618 | Controller.prototype.showImage = function (thumbElem, prawn) { 619 | if (typeof prawn == 'string') 620 | if (Object.hasOwnProperty.call (this.pronMap, prawn)) 621 | prawn = this.pronMap[prawn]; 622 | else 623 | return; 624 | if (this.selectedImage) 625 | this.selectedImage.dropClass ('selected'); 626 | // var thumbIndex; 627 | if (!thumbElem) { 628 | // search for thumbElem 629 | var done = false; 630 | for (var i=0,j=this.thumbsElem.children.length; i= 3) { 658 | // if (thumbIndex > 0) 659 | // self.emit ('preload', self.thumbsElem.children[thumbIndex-1].getAttribute ('data-path')); 660 | // // self.visualizer.preload (self.thumbsElem.children[thumbIndex-1].getAttribute ('data-path')); 661 | // if (thumbIndex + 1 < self.thumbsElem.children.length) 662 | // self.emit ('preload', self.thumbsElem.children[thumbIndex+1].getAttribute ('data-path')); 663 | // // self.visualizer.preload (self.thumbsElem.children[thumbIndex+1].getAttribute ('data-path')); 664 | // var rowWidth = Math.floor (self.thumbsElem.clientWidth / self.selectedImage.clientWidth); 665 | // if (thumbCount >= rowWidth) { 666 | // self.emit ('preload', self.lookUp().getAttribute ('data-path')); 667 | // // self.visualizer.preload (self.lookUp().getAttribute ('data-path')); 668 | // if (thumbCount > 2 * rowWidth) 669 | // self.emit ('preload', self.lookDown().getAttribute ('data-path')); 670 | // // self.visualizer.preload (self.lookDown().getAttribute ('data-path')); 671 | // } 672 | // } 673 | // }, 50); 674 | }; 675 | 676 | Controller.prototype.lookUp = function(){ 677 | var rowWidth = Math.floor (this.thumbsElem.clientWidth / this.selectedImage.clientWidth); 678 | var currentIndex = Array.prototype.indexOf.call (this.thumbsElem.children, this.selectedImage); 679 | if (rowWidth > this.thumbsElem.children.length) 680 | return this.thumbsElem.children[currentIndex]; 681 | if (currentIndex >= rowWidth) 682 | currentIndex -= rowWidth; 683 | else { 684 | // round the world 685 | var lastRowLength = this.thumbsElem.children.length % rowWidth; 686 | if (!lastRowLength) 687 | currentIndex = this.thumbsElem.children.length - currentIndex - 1; 688 | else if (currentIndex >= lastRowLength) 689 | currentIndex = this.thumbsElem.children.length - 1; 690 | else 691 | currentIndex = this.thumbsElem.children.length - lastRowLength + currentIndex; 692 | } 693 | return this.thumbsElem.children[currentIndex]; 694 | }; 695 | 696 | Controller.prototype.lookDown = function(){ 697 | var rowWidth = Math.floor (this.thumbsElem.clientWidth / this.selectedImage.clientWidth); 698 | var currentIndex = Array.prototype.indexOf.call (this.thumbsElem.children, this.selectedImage); 699 | if (rowWidth > this.thumbsElem.children.length) 700 | return this.thumbsElem.children[currentIndex]; 701 | currentIndex += rowWidth; 702 | if (currentIndex >= this.thumbsElem.children.length) 703 | currentIndex = currentIndex % rowWidth; 704 | return this.thumbsElem.children[currentIndex]; 705 | }; 706 | 707 | Controller.prototype.go = function (direction) { 708 | // keyboard overrides manual scrolling 709 | this.manualScrolling = false; 710 | 711 | if (!this.thumbsElem.children.length) 712 | return; 713 | 714 | if (!this.selectedImage) { 715 | if (direction > 38) { 716 | this.showImage ( 717 | this.thumbsElem.firstChild, 718 | this.pronMap[this.thumbsElem.firstChild.getAttribute ('data-name')] 719 | ); 720 | this.thumbsElem.scrollTop = 0; 721 | } else { 722 | this.showImage ( 723 | this.thumbsElem.lastChild, 724 | this.pronMap[this.thumbsElem.lastChild.getAttribute ('data-name')] 725 | ); 726 | this.thumbsElem.scrollTop = this.thumbsElem.scrollHeight; 727 | } 728 | return; 729 | } 730 | 731 | var rowWidth = Math.floor (this.thumbsElem.clientWidth / this.selectedImage.clientWidth); 732 | var next = this.selectedImage; 733 | switch (direction) { 734 | case 37: 735 | // go left 736 | if (!this.selectedImage.previousSibling) 737 | next = this.thumbsElem.lastChild; 738 | else 739 | next = this.selectedImage.previousSibling; 740 | break; 741 | case 38: 742 | // go up 743 | next = this.lookUp(); 744 | break; 745 | case 39: 746 | // go right 747 | if (!this.selectedImage.nextSibling) 748 | return this.showImage ( 749 | this.thumbsElem.firstChild, 750 | this.pronMap[this.thumbsElem.firstChild.getAttribute ('data-name')] 751 | ); 752 | next = this.selectedImage.nextSibling; 753 | break; 754 | case 40: 755 | // go down 756 | next = this.lookDown(); 757 | break; 758 | } 759 | 760 | this.showImage (next, this.pronMap[next.getAttribute ('data-name')]); 761 | }; 762 | --------------------------------------------------------------------------------