diff --git a/.gitignore b/.gitignore index 6fd060f87..7d9a0cd94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ dist/css/* dist/js/* +!dist/js/flowplayer/ +!dist/js/flowplayer/ node_modules .sass-cache .idea diff --git a/dist/index.html b/dist/index.html index 4bfac2bd2..d3fbbb0f6 100644 --- a/dist/index.html +++ b/dist/index.html @@ -21,6 +21,7 @@ + @@ -29,6 +30,7 @@ + diff --git a/dist/js/flowplayer/flowplayer-3.2.13.min.js b/dist/js/flowplayer/flowplayer-3.2.13.min.js new file mode 100644 index 000000000..eba948758 --- /dev/null +++ b/dist/js/flowplayer/flowplayer-3.2.13.min.js @@ -0,0 +1,22 @@ +/* + * flowplayer.js The Flowplayer API + * + * Copyright 2009-2011 Flowplayer Oy + * + * This file is part of Flowplayer. + * + * Flowplayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Flowplayer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Flowplayer. If not, see . + * + */ +!function(){function h(p){console.log("$f.fireEvent",[].slice.call(p))}function l(r){if(!r||typeof r!="object"){return r}var p=new r.constructor();for(var q in r){if(r.hasOwnProperty(q)){p[q]=l(r[q])}}return p}function n(u,r){if(!u){return}var p,q=0,s=u.length;if(s===undefined){for(p in u){if(r.call(u[p],p,u[p])===false){break}}}else{for(var t=u[0];q1){var u=arguments[1],r=(arguments.length==3)?arguments[2]:{};if(typeof u=="string"){u={src:u}}u=j({bgcolor:"#000000",version:[10,1],expressInstall:"http://releases.flowplayer.org/swf/expressinstall.swf",cachebusting:false},u);if(typeof p=="string"){if(p.indexOf(".")!=-1){var t=[];n(o(p),function(){t.push(new b(this,l(u),l(r)))});return new d(t)}else{var s=c(p);return new b(s!==null?s:l(p),l(u),l(r))}}else{if(p){return new b(p,l(u),l(r))}}}return null};j(window.$f,{fireEvent:function(){var q=[].slice.call(arguments);var r=$f(q[0]);return r?r._fireEvent(q.slice(1)):null},addPlugin:function(p,q){b.prototype[p]=q;return $f},each:n,extend:j});if(typeof jQuery=="function"){jQuery.fn.flowplayer=function(r,q){if(!arguments.length||typeof arguments[0]=="number"){var p=[];this.each(function(){var s=$f(this);if(s){p.push(s)}});return arguments.length?p[arguments[0]]:new d(p)}return this.each(function(){$f(this,l(r),q?l(q):{})})}}}();!function(){var h=document.all,j="http://get.adobe.com/flashplayer",c=typeof jQuery=="function",e=/(\d+)[^\d]+(\d+)[^\d]*(\d*)/,b={width:"100%",height:"100%",id:"_"+(""+Math.random()).slice(9),allowfullscreen:true,allowscriptaccess:"always",quality:"high",version:[3,0],onFail:null,expressInstall:null,w3c:false,cachebusting:false};if(window.attachEvent){window.attachEvent("onbeforeunload",function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){}})}function i(m,l){if(l){for(var f in l){if(l.hasOwnProperty(f)){m[f]=l[f]}}}return m}function a(f,n){var m=[];for(var l in f){if(f.hasOwnProperty(l)){m[l]=n(f[l])}}return m}window.flashembed=function(f,m,l){if(typeof f=="string"){f=document.getElementById(f.replace("#",""))}if(!f){return}if(typeof m=="string"){m={src:m}}return new d(f,i(i({},b),m),l)};var g=i(window.flashembed,{conf:b,getVersion:function(){var m,f,o;try{o=navigator.plugins["Shockwave Flash"];if(o[0].enabledPlugin!=null){f=o.description.slice(16)}}catch(p){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");f=m&&m.GetVariable("$version")}catch(n){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");f=m&&m.GetVariable("$version")}catch(l){}}}f=e.exec(f);return f?[1*f[1],1*f[(f[1]*1>9?2:3)]*1]:[0,0]},asString:function(l){if(l===null||l===undefined){return null}var f=typeof l;if(f=="object"&&l.push){f="array"}switch(f){case"string":l=l.replace(new RegExp('(["\\\\])',"g"),"\\$1");l=l.replace(/^\s?(\d+\.?\d*)%/,"$1pct");return'"'+l+'"';case"array":return"["+a(l,function(o){return g.asString(o)}).join(",")+"]";case"function":return'"function()"';case"object":var m=[];for(var n in l){if(l.hasOwnProperty(n)){m.push('"'+n+'":'+g.asString(l[n]))}}return"{"+m.join(",")+"}"}return String(l).replace(/\s/g," ").replace(/\'/g,'"')},getHTML:function(o,l){o=i({},o);var n=''}o.width=o.height=o.id=o.w3c=o.src=null;o.onFail=o.version=o.expressInstall=null;for(var m in o){if(o[m]){n+=''}}var p="";if(l){for(var f in l){if(l[f]){var q=l[f];p+=f+"="+(/function|object/.test(typeof q)?g.asString(q):q)+"&"}}p=p.slice(0,-1);n+='"}n+="";return n},isSupported:function(f){return k[0]>f[0]||k[0]==f[0]&&k[1]>=f[1]}});var k=g.getVersion();function d(f,n,m){if(g.isSupported(n.version)){f.innerHTML=g.getHTML(n,m)}else{if(n.expressInstall&&g.isSupported([6,65])){f.innerHTML=g.getHTML(i(n,{src:n.expressInstall}),{MMredirectURL:encodeURIComponent(location.href),MMplayerType:"PlugIn",MMdoctitle:document.title})}else{if(!f.innerHTML.replace(/\s/g,"")){f.innerHTML="

Flash version "+n.version+" or greater is required

"+(k[0]>0?"Your version is "+k:"You have no flash plugin installed")+"

"+(f.tagName=="A"?"

Click here to download latest version

":"

Download latest version from here

");if(f.tagName=="A"||f.tagName=="DIV"){f.onclick=function(){location.href=j}}}if(n.onFail){var l=n.onFail.call(this);if(typeof l=="string"){f.innerHTML=l}}}}if(h){window[n.id]=document.getElementById(n.id)}i(this,{getRoot:function(){return f},getOptions:function(){return n},getConf:function(){return m},getApi:function(){return f.firstChild}})}if(c){jQuery.tools=jQuery.tools||{version:"@VERSION"};jQuery.tools.flashembed={conf:b};jQuery.fn.flashembed=function(l,f){return this.each(function(){$(this).data("flashembed",flashembed(this,l,f))})}}}(); \ No newline at end of file diff --git a/dist/js/flowplayer/flowplayer-3.2.18.swf b/dist/js/flowplayer/flowplayer-3.2.18.swf new file mode 100644 index 000000000..aed1fcb12 Binary files /dev/null and b/dist/js/flowplayer/flowplayer-3.2.18.swf differ diff --git a/dist/js/flowplayer/flowplayer.controls-3.2.16.swf b/dist/js/flowplayer/flowplayer.controls-3.2.16.swf new file mode 100644 index 000000000..eacc8c029 Binary files /dev/null and b/dist/js/flowplayer/flowplayer.controls-3.2.16.swf differ diff --git a/js/app.js b/js/app.js index ac6d67509..99d89f991 100644 --- a/js/app.js +++ b/js/app.js @@ -1,15 +1,10 @@ -var appStyles = { - width: '800px', - marginLeft: 'auto', - marginRight: 'auto', -}; var App = React.createClass({ getInitialState: function() { // For now, routes are in format ?page or ?page=args var match, param, val; [match, param, val] = window.location.search.match(/\??([^=]*)(?:=(.*))?/); - if (['settings', 'help', 'start', 'watch', 'report'].indexOf(param) != -1) { + if (['settings', 'help', 'start', 'watch', 'report', 'files'].indexOf(param) != -1) { var viewingPage = param; } else { var viewingPage = 'home'; @@ -44,31 +39,21 @@ var App = React.createClass({ } }); }, - componentDidMount: function() { - lbry.getStartNotice(function(notice) { - if (notice) { - alert(notice); - } - }); - }, render: function() { if (this.state.viewingPage == 'home') { - var content = ; + return ; } else if (this.state.viewingPage == 'settings') { - var content = ; + return ; } else if (this.state.viewingPage == 'help') { - var content = ; + return ; } else if (this.state.viewingPage == 'watch') { - var content = ; + return ; } else if (this.state.viewingPage == 'report') { - var content = ; + return ; + } else if (this.state.viewingPage == 'files') { + return ; } else if (this.state.viewingPage == 'start') { - var content = ; + return ; } - return ( -
- {content} -
- ); } }); \ No newline at end of file diff --git a/js/component/common.js b/js/component/common.js index c1dd5610e..c4f9b7631 100644 --- a/js/component/common.js +++ b/js/component/common.js @@ -3,9 +3,10 @@ var Icon = React.createClass({ propTypes: { style: React.PropTypes.object, + fixed: React.PropTypes.boolean, }, render: function() { - var className = 'icon ' + this.props.icon; + var className = 'icon ' + ('fixed' in this.props ? 'icon-fixed-width ' : '') + this.props.icon; return } }); @@ -18,7 +19,8 @@ var Link = React.createClass({ className = (this.props.button ? 'button-block button-' + this.props.button : 'button-text') + (this.props.hidden ? ' hidden' : '') + (this.props.disabled ? ' disabled' : ''); return ( - + {this.props.icon ? icon : '' } {this.props.label} @@ -26,6 +28,79 @@ var Link = React.createClass({ } }); +// Generic menu styles +var menuStyle = { + border: '1px solid #aaa', + padding: '4px', + whiteSpace: 'nowrap', +}; + +var Menu = React.createClass({ + handleWindowClick: function(e) { + if (this.props.toggleButton && ReactDOM.findDOMNode(this.props.toggleButton).contains(e.target)) { + // Toggle button was clicked + this.setState({ + open: !this.state.open + }); + } else if (this.state.open && !this.refs.div.contains(e.target)) { + // Menu is open and user clicked outside of it + this.setState({ + open: false + }); + } + }, + propTypes: { + openButton: React.PropTypes.element, + }, + getInitialState: function() { + return { + open: false, + }; + }, + componentDidMount: function() { + window.addEventListener('click', this.handleWindowClick, false); + }, + componentWillUnmount: function() { + window.removeEventListener('click', this.handleWindowClick, false); + }, + render: function() { + return ( +
+ {this.props.children} +
+ ); + } +}); + +var menuItemStyle = { + display: 'block', +}; +var MenuItem = React.createClass({ + propTypes: { + href: React.PropTypes.string, + label: React.PropTypes.string, + icon: React.PropTypes.string, + onClick: React.PropTypes.function, + }, + getDefaultProps: function() { + return { + iconPosition: 'left', + } + }, + render: function() { + var icon = (this.props.icon ? : null); + + return ( + + {this.props.iconPosition == 'left' ? icon : null} + {this.props.label} + {this.props.iconPosition == 'left' ? null : icon} + + ); + } +}); + var creditAmountStyle = { color: '#216C2A', fontWeight: 'bold', @@ -48,4 +123,16 @@ var CreditAmount = React.createClass({ ); } +}); + +var subPageLogoStyle = { + maxWidth: '150px', + display: 'block', + marginTop: '36px', +}; + +var SubPageLogo = React.createClass({ + render: function() { + return ; + } }); \ No newline at end of file diff --git a/js/lbry.js b/js/lbry.js index 3c8d7a0a8..16b8f5ef8 100644 --- a/js/lbry.js +++ b/js/lbry.js @@ -103,6 +103,22 @@ lbry.getFileStatus = function(name, callback) { lbry.call('get_lbry_file', { 'name': name }, callback); } +lbry.getFilesInfo = function(callback) { + lbry.call('get_lbry_files', {}, callback); +} + +lbry.startFile = function(name, callback) { + lbry.call('start_lbry_file', { name: name }, callback); +} + +lbry.stopFile = function(name, callback) { + lbry.call('stop_lbry_file', { name: name }, callback); +} + +lbry.deleteFile = function(name, callback) { + lbry.call('delete_lbry_file', { name: name }, callback) +} + lbry.getVersionInfo = function(callback) { lbry.call('version', {}, callback); }; @@ -154,6 +170,26 @@ lbry.imagePath = function(file) return lbry.rootPath + '/img/' + file; } +lbry.getMediaType = function(filename) { + var dotIndex = filename.lastIndexOf('.'); + if (dotIndex == -1) { + return 'unknown'; + } + + var ext = filename.substr(dotIndex + 1); + if (/^mp4|mov|m4v|flv|f4v$/i.test(ext)) { + return 'video'; + } else if (/^mp3|m4a|aac|wav|flac|ogg$/i.test(ext)) { + return 'audio'; + } else if (/^html|htm|pdf|odf|doc|docx|md|markdown|txt$/i.test(ext)) { + return 'document'; + } else { + return 'unknown'; + } +} + lbry.stop = function(callback) { lbry.call('stop', {}, callback); }; + + diff --git a/js/page/help.js b/js/page/help.js index c4599692f..64f0845cb 100644 --- a/js/page/help.js +++ b/js/page/help.js @@ -3,7 +3,8 @@ var HelpPage = React.createClass({ render: function() { return ( -
+
+

Troubleshooting

Here are the most commonly encountered problems and what to try doing about them.

diff --git a/js/page/home.js b/js/page/home.js index 914c24248..a05acd075 100644 --- a/js/page/home.js +++ b/js/page/home.js @@ -228,23 +228,41 @@ var Header = React.createClass({ }); var topBarStyle = { - 'float': 'right' + 'float': 'right', + 'position': 'relative', + 'height': '26px', }, balanceStyle = { 'marginRight': '5px' -}, -closeIconStyle = { - 'color': '#ff5155' }; +var mainMenuStyle = { + position: 'absolute', + top: '26px', + right: '0px', +}; + +var MainMenu = React.createClass({ + render: function() { + var isLinux = /linux/i.test(navigator.userAgent); // @TODO: find a way to use getVersionInfo() here without messy state management + return ( +
+ + + + + {isLinux ? + : null} + +
+ ); + } +}); + var TopBar = React.createClass({ - onClose: function() { - window.location.href = "?start"; - }, getInitialState: function() { return { balance: 0, - showClose: /linux/i.test(navigator.userAgent) // @TODO: find a way to use getVersionInfo() here without messy state management }; }, componentDidMount: function() { @@ -254,27 +272,34 @@ var TopBar = React.createClass({ }); }.bind(this)); }, - + onClose: function() { + window.location.href = "?start"; + }, render: function() { return ( - - { ' ' } - - { ' ' } - + + + ); } }); var HomePage = React.createClass({ + componentDidMount: function() { + lbry.getStartNotice(function(notice) { + if (notice) { + alert(notice); + } + }); + }, render: function() { return ( -
+
diff --git a/js/page/my_files.js b/js/page/my_files.js new file mode 100644 index 000000000..8802c0cc2 --- /dev/null +++ b/js/page/my_files.js @@ -0,0 +1,138 @@ +var removeIconColumnStyle = { + fontSize: '1.3em', + height: '120px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}, +progressBarStyle = { + height: '15px', + width: '230px', + backgroundColor: '#444', + border: '2px solid #eee', + display: 'inline-block', +}, +myFilesRowImgStyle = { + maxHeight: '100px', + display: 'block', + marginLeft: 'auto', + marginRight: 'auto', + float: 'left' +}; + +var MyFilesRow = React.createClass({ + onRemoveClicked: function() { + var alertText = 'Are you sure you\'d like to remove "' + this.props.title + '?" This will ' + + (this.completed ? ' stop the download and ' : '') + + 'permanently remove the file from your system.'; + + if (confirm(alertText)) { + lbry.deleteFile(this.props.lbryUri); + } + }, + onPauseResumeClicked: function() { + if (this.props.stopped) { + lbry.startFile(this.props.lbryUri); + } else { + lbry.stopFile(this.props.lbryUri); + } + }, + render: function() { + if (this.props.completed) { + var pauseLink = null; + var curProgressBarStyle = {display: 'none'}; + } else { + var pauseLink = { this.onPauseResumeClicked() }} />; + + var curProgressBarStyle = Object.assign({}, progressBarStyle); + curProgressBarStyle.width = this.props.ratioLoaded * 230; + curProgressBarStyle.borderRightWidth = 230 - (this.props.ratioLoaded * 230) + 2; + } + + if (this.props.showWatchButton) { + // No support for lbry:// URLs in Windows or on Chrome yet + if (/windows|win32/i.test(navigator.userAgent) || (window.chrome && window.navigator.vendor == "Google Inc.")) { + var watchUri = "/?watch=" + this.props.lbryUri; + } else { + var watchUri = 'lbry://' + this.props.lbryUri; + } + + var watchLink = ; + } else { + var watchLink = null; + } + + return ( +
+
+ {'Photo +
+
+

{this.props.title}

+
+ { ' ' } + {this.props.completed ? 'Download complete' : (parseInt(this.props.ratioLoaded * 100) + '%')} +
{ pauseLink }
+
{ watchLink }
+
+
+ { this.onRemoveClicked() } } />
+
+
+ ); + } +}); + +var MyFilesPage = React.createClass({ + getInitialState: function() { + return { + filesInfo: null, + }; + }, + componentWillMount: function() { + this.updateFilesInfo(); + }, + updateFilesInfo: function() { + lbry.getFilesInfo((filesInfo) => { + this.setState({ + filesInfo: (filesInfo ? filesInfo : []), + }); + setTimeout(() => { this.updateFilesInfo() }, 1000); + }); + }, + render: function() { + if (this.state.filesInfo === null) { + return null; + } + + if (!this.state.filesInfo.length) { + var content = You haven't downloaded anything from LBRY yet. Go !; + } else { + var content = []; + for (let fileInfo of this.state.filesInfo) { + let {completed, written_bytes, total_bytes, lbry_uri, file_name, stopped, metadata} = fileInfo; + let {name, stream_name, thumbnail} = metadata; + + var title = (name || stream_name || ('lbry://' + lbry_uri)); + var ratioLoaded = written_bytes / total_bytes; + var showWatchButton = (lbry.getMediaType(file_name) == 'video'); + + content.push(); + } + } + return ( +
+ +

My files

+ {content} +
+ +
+
+ ); + } +}); \ No newline at end of file diff --git a/js/page/report.js b/js/page/report.js index ddfb999c4..bd46f2919 100644 --- a/js/page/report.js +++ b/js/page/report.js @@ -20,7 +20,8 @@ var ReportPage = React.createClass({ }, render: function() { return ( -
+
+

Report a bug

Please describe the problem you experienced and any information you think might be useful to us. Links to screenshots are great!

diff --git a/js/page/settings.js b/js/page/settings.js index 958bcd312..31b7c1d27 100644 --- a/js/page/settings.js +++ b/js/page/settings.js @@ -73,7 +73,8 @@ var SettingsPage = React.createClass({ } return ( -
+
+

Settings

Run on startup

diff --git a/js/page/start.js b/js/page/start.js index 1c5b1d0b6..2f91a91d1 100644 --- a/js/page/start.js +++ b/js/page/start.js @@ -4,7 +4,8 @@ var StartPage = React.createClass({ }, render: function() { return ( -
+
+

LBRY has closed

diff --git a/js/page/watch.js b/js/page/watch.js index a34e5eddb..c0af88688 100644 --- a/js/page/watch.js +++ b/js/page/watch.js @@ -1,6 +1,6 @@ var videoStyle = { width: '100%', - height: '100%', +// height: '100%', backgroundColor: '#000' }; @@ -19,19 +19,6 @@ var WatchPage = React.createClass({ lbry.getStream(this.props.name); this.updateLoadStatus(); }, - reloadIfNeeded: function() { - // Fallback option for loading problems: every 15 seconds, if the video hasn't reported being - // playable yet, ask it to reload. - if (!this.state.readyToPlay) { - this._video.load() - setTimeout(() => { this.reloadIfNeeded() }, 15000); - } - }, - onCanPlay: function() { - this.setState({ - readyToPlay: true - }); - }, updateLoadStatus: function() { lbry.getFileStatus(this.props.name, (status) => { if (!status || status.code != 'running' || status.written_bytes == 0) { @@ -44,31 +31,20 @@ var WatchPage = React.createClass({ setTimeout(() => { this.updateLoadStatus() }, 250); } else { this.setState({ - loadStatusMessage: "Buffering", - downloadStarted: true, - }); - setTimeout(() => { this.reloadIfNeeded() }, 15000); + readyToPlay: true + }) + flowplayer('player', 'js/flowplayer/flowplayer-3.2.18.swf'); } }); }, render: function() { - if (!this.state.downloadStarted) { - var video = null; - } else { - // If the download has started, render the