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 |
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 | 
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!
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 |