├── .gitattributes ├── .yo-rc.json ├── app ├── scripts.babel │ ├── env-example.js │ ├── background.js │ ├── chromereload.js │ ├── progress-bar.js │ ├── rewind-button.js │ ├── main.js │ ├── no-preview.js │ ├── video-sparkbar.js │ ├── options.js │ ├── profiles.youtube.js │ ├── storyboard.js │ ├── preview.js │ └── video-bookmark.js ├── images │ ├── add.png │ ├── icon.png │ ├── bg_2048.png │ ├── close.png │ ├── icon-16.png │ ├── search.png │ ├── icon-128.png │ ├── youtube-logo.png │ ├── bookmark-icon.png │ ├── muybridge_horse.png │ └── muybridge_race_horse.jpg ├── _locales │ └── en │ │ └── messages.json ├── rewind-button.html ├── manifest.json ├── options.html └── styles.scss │ ├── preview.scss │ └── options.scss ├── .bowerrc ├── .babelrc ├── assets ├── rainbow-cat-demo.gif ├── funny-cat-rating-bar.png ├── options-page-1280x800.png ├── preview-progress-bar.gif ├── large_promo_tile-920x680.png ├── obama-10-sec-rewind-button.png ├── options_page-1280x800-0.9.3.png ├── youtube-preview-screenshots-1-640x400.png └── rewind.svg ├── .gitignore ├── test ├── spec │ └── test.js └── index.html ├── bower.json ├── .editorconfig ├── package.json ├── README.md └── gulpfile.babel.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-mocha": {} 3 | } -------------------------------------------------------------------------------- /app/scripts.babel/env-example.js: -------------------------------------------------------------------------------- 1 | var API_KEY = 'api_key'; -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "comments": false 4 | } 5 | -------------------------------------------------------------------------------- /app/images/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/app/images/add.png -------------------------------------------------------------------------------- /app/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/app/images/icon.png -------------------------------------------------------------------------------- /app/images/bg_2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/app/images/bg_2048.png -------------------------------------------------------------------------------- /app/images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/app/images/close.png -------------------------------------------------------------------------------- /app/images/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/app/images/icon-16.png -------------------------------------------------------------------------------- /app/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/app/images/search.png -------------------------------------------------------------------------------- /app/images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/app/images/icon-128.png -------------------------------------------------------------------------------- /app/images/youtube-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/app/images/youtube-logo.png -------------------------------------------------------------------------------- /assets/rainbow-cat-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/assets/rainbow-cat-demo.gif -------------------------------------------------------------------------------- /app/images/bookmark-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/app/images/bookmark-icon.png -------------------------------------------------------------------------------- /app/images/muybridge_horse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/app/images/muybridge_horse.png -------------------------------------------------------------------------------- /assets/funny-cat-rating-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/assets/funny-cat-rating-bar.png -------------------------------------------------------------------------------- /assets/options-page-1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/assets/options-page-1280x800.png -------------------------------------------------------------------------------- /assets/preview-progress-bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/assets/preview-progress-bar.gif -------------------------------------------------------------------------------- /app/images/muybridge_race_horse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/app/images/muybridge_race_horse.jpg -------------------------------------------------------------------------------- /assets/large_promo_tile-920x680.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/assets/large_promo_tile-920x680.png -------------------------------------------------------------------------------- /assets/obama-10-sec-rewind-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/assets/obama-10-sec-rewind-button.png -------------------------------------------------------------------------------- /assets/options_page-1280x800-0.9.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/assets/options_page-1280x800-0.9.3.png -------------------------------------------------------------------------------- /assets/youtube-preview-screenshots-1-640x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tennisonchan/youtube-preview/HEAD/assets/youtube-preview-screenshots-1-640x400.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | node_modules 3 | temp 4 | .tmp 5 | dist 6 | .sass-cache 7 | app/bower_components 8 | test/bower_components 9 | package 10 | app/scripts 11 | app/styles 12 | 13 | ### YouTube Preview Extension ### 14 | env.js 15 | .sass-cache 16 | -------------------------------------------------------------------------------- /app/scripts.babel/background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onInstalled.addListener(function(details){ 2 | if(details.reason == 'install'){ 3 | var thisVersion = chrome.runtime.getManifest().version; 4 | chrome.tabs.create({ url: 'options.html' }); 5 | } 6 | }); -------------------------------------------------------------------------------- /test/spec/test.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | describe('Give it some context', function () { 5 | describe('maybe a bit more context here', function () { 6 | it('should run here few assertions', function () { 7 | 8 | }); 9 | }); 10 | }); 11 | })(); 12 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "youtube-preview", 3 | "private": true, 4 | "version": "0.0.0", 5 | "dependencies": { 6 | "material-design-lite": "^1.1.3", 7 | "jquery": "^2.2.2", 8 | "fuse.js": "fuse#^2.2.0" 9 | }, 10 | "devDependencies": { 11 | "chai": "^3.5.0", 12 | "mocha": "^2.4.5" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Youtube Preview", 4 | "description": "The name of the application" 5 | }, 6 | "appDescription": { 7 | "message": "Show likes and dislikes rating bar on video thumbnails, previewing YouTube video like gif, helping you decide if its worth watching.", 8 | "description": "The description of the application" 9 | } 10 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.json] 15 | indent_size = 2 16 | 17 | # We recommend you to keep these unchanged 18 | end_of_line = lf 19 | charset = utf-8 20 | trim_trailing_whitespace = true 21 | insert_final_newline = true 22 | 23 | [*.md] 24 | trim_trailing_whitespace = false 25 | -------------------------------------------------------------------------------- /app/scripts.babel/chromereload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Reload client for Chrome Apps & Extensions. 4 | // The reload client has a compatibility with livereload. 5 | // WARNING: only supports reload command. 6 | 7 | const LIVERELOAD_HOST = 'localhost:'; 8 | const LIVERELOAD_PORT = 35729; 9 | const connection = new WebSocket('ws://' + LIVERELOAD_HOST + LIVERELOAD_PORT + '/livereload'); 10 | 11 | connection.onerror = error => { 12 | console.log('reload connection got error:', error); 13 | }; 14 | 15 | connection.onmessage = e => { 16 | if (e.data) { 17 | const data = JSON.parse(e.data); 18 | if (data && data.command === 'reload') { 19 | chrome.runtime.reload(); 20 | } 21 | } 22 | }; -------------------------------------------------------------------------------- /app/scripts.babel/progress-bar.js: -------------------------------------------------------------------------------- 1 | var ProgressBar = function() { 2 | this.el = null; 3 | this.scrubber = null; 4 | this.progress = 0; 5 | } 6 | 7 | ProgressBar.prototype.getElement = function() { 8 | if (!this.el) { 9 | this.scrubber = $('
', { 10 | class: 'preview-scrubbed', 11 | }).css({ width: (this.progress * 100) + '%' }) 12 | 13 | 14 | this.el = $('
', { 15 | class: 'preview-scrubber' 16 | }) 17 | .wrapInner(this.scrubber); 18 | } 19 | 20 | return this.el; 21 | }; 22 | 23 | ProgressBar.prototype.update = function(progress) { 24 | this.progress = progress; 25 | 26 | this.scrubber.css({ 27 | width: (this.progress * 100) + '%' 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /app/rewind-button.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /app/scripts.babel/rewind-button.js: -------------------------------------------------------------------------------- 1 | var RewindButton = function(Profile) { 2 | var _this = { 3 | el: null, 4 | video: null 5 | }; 6 | 7 | function initialize(Profile) { 8 | _this.video = Profile.getMainVideo(document).get(0); 9 | } 10 | 11 | _this.create = function(target) { 12 | $.ajax(chrome.extension.getURL('rewind-button.html')) 13 | .then(function(el) { 14 | if ($(Profile.ytpRewindButton).length === 0) { 15 | _this.el = $(el).on({ 16 | click: function() { 17 | _this.video.currentTime = _this.video.currentTime - 10; 18 | _this.video.play(); 19 | } 20 | }).appendTo(target); 21 | } 22 | }); 23 | }; 24 | 25 | _this.remove = function() { 26 | _this.el.remove(); 27 | }; 28 | 29 | initialize(Profile); 30 | 31 | return _this; 32 | }; 33 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Spec Runner 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/scripts.babel/main.js: -------------------------------------------------------------------------------- 1 | /*jshint newcap: false*/ 2 | /*global Preview, Profiles */ 3 | 4 | (function(window, Preview, Profiles) { 5 | 6 | 'use strict'; 7 | 8 | var App, list = { 9 | 'www.youtube.com': 'youtube' 10 | }, 11 | config = { 12 | delayPreview: 20, 13 | previewInterval: 200, 14 | showRatingBar: true, 15 | showRewindButton: true 16 | }; 17 | 18 | chrome.storage.sync.get(config, function(config) { 19 | config.previewInterval = Number(config.previewInterval); 20 | config.delayPreview = Number(config.delayPreview); 21 | config.showRatingBar = config.showRatingBar; 22 | config.showRewindButton = config.showRewindButton; 23 | var profile = Profiles[list[window.location.host] || 'youtube'](); 24 | 25 | App = Preview(profile, config); 26 | chrome.storage.onChanged.addListener((changes) => { 27 | App.updateConfigs(changes); 28 | App.initialize(); 29 | }); 30 | }); 31 | 32 | 33 | })(window, Preview, Profiles); -------------------------------------------------------------------------------- /assets/rewind.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 11 | 12 | 10 13 | 14 | -------------------------------------------------------------------------------- /app/scripts.babel/no-preview.js: -------------------------------------------------------------------------------- 1 | var NoPreview = function() { 2 | this.el = null; 3 | this.frameHeight = 0; 4 | this.frameWidth = 0; 5 | this.height = 0; 6 | this.width = 0; 7 | this.isNoPreview = true; 8 | 9 | return this; 10 | }; 11 | 12 | NoPreview.prototype.set = function(key, value) { 13 | if (key !== undefined && value !== undefined) { 14 | this[key] = value; 15 | } 16 | return this; 17 | }; 18 | 19 | NoPreview.prototype.url = function(l, m) { 20 | return '#'; 21 | }; 22 | 23 | NoPreview.prototype.appendThumbTo = function(target) { 24 | if (!this.el && 25 | this.target.prevAll('.no-preview, .storyboard').length === 0) { 26 | this.el = $('
', { 27 | class: 'no-preview', 28 | text: 'No Preview' 29 | }) 30 | .css({ 31 | lineHeight: this.frameHeight + 'px', 32 | width: this.frameWidth, 33 | height: this.frameHeight, 34 | }).insertBefore(this.target); 35 | } 36 | 37 | return this.el; 38 | }; 39 | 40 | NoPreview.prototype.playingFrames = function(target) { 41 | return true; 42 | }; 43 | 44 | NoPreview.prototype.reset = function() { 45 | this.count = 0; 46 | }; -------------------------------------------------------------------------------- /app/scripts.babel/video-sparkbar.js: -------------------------------------------------------------------------------- 1 | var VideoSparkbar = function(id, statistics) { 2 | this.id = id; 3 | this.viewCount = Number(statistics.viewCount); 4 | this.likeCount = Number(statistics.likeCount) || 0; 5 | this.dislikeCount = Number(statistics.dislikeCount) || 0; 6 | this.ratingCount = this.likeCount + this.dislikeCount; 7 | this.favoriteCount = Number(statistics.favoriteCount); 8 | this.commentCount = Number(statistics.commentCount); 9 | }; 10 | 11 | VideoSparkbar.prototype.appendRatingTo = function($target) { 12 | if (!$target.length || !this.ratingCount) return; 13 | 14 | var sparkbar = this.createSparkbar(); 15 | this.upsertSparkbar($target, sparkbar); 16 | 17 | setTimeout(function() { 18 | sparkbar.removeClass('loading'); 19 | }, 500); 20 | }; 21 | 22 | VideoSparkbar.prototype.upsertSparkbar = function ($target, sparkbar) { 23 | if($target.siblings('.preview-sparkbars').length) { 24 | $target.siblings('.preview-sparkbars').replaceWith(sparkbar); 25 | } else { 26 | $target.after(sparkbar); 27 | } 28 | }; 29 | 30 | VideoSparkbar.prototype.createSparkbar = function() { 31 | var likesWidth = (this.likeCount * 100 / this.ratingCount); 32 | 33 | return $('
', { 34 | class: 'preview-sparkbars loading' 35 | }) 36 | .append($('
', { 37 | class: 'preview-sparkbar-likes', 38 | style: 'width: ' + likesWidth + '%;' 39 | })) 40 | .append($('
', { 41 | class: 'preview-sparkbar-dislikes', 42 | style: 'width: ' + (100 - likesWidth) + '%;' 43 | })); 44 | }; -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "version": "0.9.4", 4 | "manifest_version": 2, 5 | "description": "__MSG_appDescription__", 6 | "author": "Tennison Chan ", 7 | "icons": { 8 | "16": "images/icon.png", 9 | "48": "images/icon.png", 10 | "128": "images/icon.png" 11 | }, 12 | "default_locale": "en", 13 | "background": { 14 | "scripts": [ 15 | "scripts/background.js", 16 | "scripts/chromereload.js" 17 | ] 18 | }, 19 | "permissions": [ 20 | "storage" 21 | ], 22 | "options_page": "options.html", 23 | "content_scripts": [{ 24 | "css": [ 25 | "styles/preview.css" 26 | ], 27 | "js": [ 28 | "bower_components/material-design-lite/material.min.js", 29 | "bower_components/jquery/dist/jquery.min.js", 30 | "bower_components/fuse.js/src/fuse.min.js", 31 | "scripts/options.js", 32 | "scripts/env.js", 33 | "scripts/profiles.youtube.js", 34 | "scripts/rewind-button.js", 35 | "scripts/progress-bar.js", 36 | "scripts/video-bookmark.js", 37 | "scripts/preview.js", 38 | "scripts/no-preview.js", 39 | "scripts/storyboard.js", 40 | "scripts/video-sparkbar.js", 41 | "scripts/main.js" 42 | ], 43 | "matches": [ 44 | "*://www.youtube.com/*", 45 | "*://www.google.com/*" 46 | ], 47 | "all_frames": false, 48 | "run_at": "document_end" 49 | }], 50 | "web_accessible_resources": [ 51 | "images/bookmark-icon.png", 52 | "images/close.png", 53 | "images/add.png", 54 | "images/search.png", 55 | "images/bg_2048.png", 56 | "rewind-button.html" 57 | ] 58 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "youtube-preview", 3 | "private": true, 4 | "engines": { 5 | "node": ">=4.9.1" 6 | }, 7 | "scripts": { 8 | "pre-install": "npm install && bower install && npm run setup-env", 9 | "setup-env": "cp app/scripts.babel/env-example.js app/scripts.babel/env.js", 10 | "start": "gulp watch", 11 | "build": "gulp build --production", 12 | "package": "gulp build --production && gulp package" 13 | }, 14 | "devDependencies": { 15 | "babel-core": "^6.7.2", 16 | "babel-plugin-transform-remove-console": "^6.8.0", 17 | "babel-preset-es2015": "^6.6.0", 18 | "del": "^2.2.0", 19 | "gulp": "^3.9.1", 20 | "gulp-babel": "^6.1.2", 21 | "gulp-cache": "^0.4.3", 22 | "gulp-chrome-manifest": "0.0.13", 23 | "gulp-clean-css": "^2.0.3", 24 | "gulp-eslint": "^2.0.0", 25 | "gulp-htmlmin": "^1.3.0", 26 | "gulp-if": "^2.0.1", 27 | "gulp-imagemin": "^2.4.0", 28 | "gulp-livereload": "^3.8.1", 29 | "gulp-load-plugins": "^1.4.0", 30 | "gulp-plumber": "^1.1.0", 31 | "gulp-sass": "^2.3.1", 32 | "gulp-size": "^2.1.0", 33 | "gulp-sourcemaps": "^1.6.0", 34 | "gulp-uglify": "^2.0.0", 35 | "gulp-useref": "^3.0.8", 36 | "gulp-util": "^3.0.7", 37 | "gulp-zip": "^3.2.0", 38 | "main-bower-files": "^2.11.1", 39 | "run-sequence": "^1.1.5", 40 | "wiredep": "^4.0.0" 41 | }, 42 | "eslintConfig": { 43 | "env": { 44 | "node": true, 45 | "browser": true 46 | }, 47 | "globals": { 48 | "chrome": true 49 | }, 50 | "rules": { 51 | "eol-last": 0, 52 | "quotes": [ 53 | 2, 54 | "single" 55 | ] 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YouTube Previewer 2 | Youtube just got easier. Fast-forward through videos all before you have to watch the complete video. Jump to your favorite parts in a preview motion. Not to mention you can even rewind in 10-second in one click! 3 | 4 | [Download from Chrome Store](https://chrome.google.com/webstore/detail/youtube-preview/gbkgikkleehfibaknfmdphhhacjfkdap?utm_source=github) 5 | 6 | ## Features 7 | ### Preview Like a Gif 8 | Speed through Youtube Videos. Jump to your favorite part, even fast-forward to your favorite part, all before you have to watch the whole video. 9 | 10 | [![YouTube Preview Demo](https://raw.githubusercontent.com/tennisonchan/youtube-preview/78ed272/assets/rainbow-cat-demo.gif)](https://chrome.google.com/webstore/detail/youtube-preview/gbkgikkleehfibaknfmdphhhacjfkdap?utm_source=github&utm_campaign=demo) 11 | 12 | ### Rating (like / dislike) Bar 13 | Look at the rating bar beforehand so you don't have to waste your time. 14 | 15 | [![YouTube Preview Demo](https://raw.githubusercontent.com/tennisonchan/youtube-preview/78ed272/assets/funny-cat-rating-bar.png)](https://chrome.google.com/webstore/detail/youtube-preview/gbkgikkleehfibaknfmdphhhacjfkdap?utm_source=github&utm_campaign=demo) 16 | 17 | ### Rewind 10-sec 18 | Rewind 10-second in one click 19 | 20 | [![YouTube Preview Demo](https://raw.githubusercontent.com/tennisonchan/youtube-preview/78ed272/assets/obama-10-sec-rewind-button.png)](https://chrome.google.com/webstore/detail/youtube-preview/gbkgikkleehfibaknfmdphhhacjfkdap?utm_source=github&utm_campaign=demo) 21 | 22 | ### Setup 23 | ```sh 24 | npm run pre-install 25 | # Place your YouTube API key inside the env.js file 26 | # cp app/scripts.babel/env-example.js app/scripts.babel/env.js 27 | # npm install && bower install 28 | 29 | # Re-compile the sources code automatically and Livereload(chromereload.js) reloads the extension 30 | npm start 31 | 32 | # Make a production version extension 33 | npm run package 34 | ``` 35 | -------------------------------------------------------------------------------- /app/scripts.babel/options.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | var timeInterval; 4 | var frame = 0; 5 | var debounceTimout; 6 | 7 | $(function() { 8 | restore_options(); 9 | }); 10 | 11 | function restore_options() { 12 | chrome.storage.sync.get({ 13 | previewInterval: 200, 14 | showRatingBar: true, 15 | showRewindButton: true, 16 | }, function(data) { 17 | var previewInterval = Number(data.previewInterval); 18 | var showRatingBar = Boolean(data.showRatingBar); 19 | var showRewindButton = Boolean(data.showRewindButton); 20 | 21 | clearInterval(timeInterval); 22 | timeInterval = setInterval(animate, previewInterval); 23 | $('#preview-intervals-slide').val(previewInterval); 24 | $('#rating-bar-switch').prop('checked', showRatingBar); 25 | $('.rating-bar').toggleClass('on', showRatingBar); 26 | $('#rewind-button-switch').prop('checked', showRewindButton); 27 | }); 28 | } 29 | 30 | function save_option(key, value, message) { 31 | var data = {}; 32 | 33 | data[key] = value; 34 | 35 | chrome.storage.sync.set(data, function() { 36 | $('#snackbar').get(0).MaterialSnackbar.showSnackbar({ 37 | message: message || 'Saved!', 38 | timeout: 1000 39 | }); 40 | }); 41 | } 42 | 43 | function animate() { 44 | var topPosition = (frame % 16 / 4 | 0) * 236.5; 45 | var leftPosition = (frame % 16 % 4) * 322; 46 | 47 | $('.animated-background').css({ 48 | backgroundPosition: '-' + leftPosition + 'px -' + topPosition + 'px', 49 | }); 50 | 51 | frame++; 52 | } 53 | 54 | $('#preview-intervals-slide') 55 | .on('input', function(e) { 56 | clearInterval(timeInterval); 57 | timeInterval = setInterval(animate, this.value); 58 | 59 | clearTimeout(debounceTimout); 60 | debounceTimout = setTimeout(() => { 61 | save_option('previewInterval', this.value, 'Saved preview interval as ' + this.value + ' ms'); 62 | }, 500); 63 | }); 64 | 65 | $('#rating-bar-switch') 66 | .on('change', function() { 67 | let message = this.checked ? 'Show rating bar' : 'Hide rating bar'; 68 | $('.rating-bar').toggleClass('on', this.checked); 69 | save_option('showRatingBar', this.checked, message); 70 | }); 71 | 72 | $('#rewind-button-switch') 73 | .on('change', function() { 74 | let message = this.checked ? 'Show rewind button' : 'Hide rewind button'; 75 | save_option('showRewindButton', this.checked, message); 76 | }); 77 | 78 | })(jQuery); -------------------------------------------------------------------------------- /app/scripts.babel/profiles.youtube.js: -------------------------------------------------------------------------------- 1 | var Profiles = {}; 2 | 3 | Profiles.youtube = function() { 4 | var _target = null, 5 | _imgEl = null, 6 | imageIdRegEx = new RegExp('vi(_webp)?\\/([a-z0-9-_=]+)\\/([a-z]*default)\\.([a-z]+)*', 'i'), 7 | imageIdRegExV2 = new RegExp('vi(_webp)?\\/([a-z0-9-_=]+)\\/*', 'i'), 8 | videoIdRegEx = new RegExp('v=([a-z0-9-_=]+)', 'i'), 9 | channelImageIdRegEx = new RegExp('yts/img/pixel-([a-z0-9-_=]+)\\.([a-z]+)*', 'i'), 10 | storyboardRegExp = new RegExp('playerStoryboardSpecRenderer":{"spec":"(.*?)\"', 'i'); 11 | 12 | var _this = { 13 | bookmarkBtn: '.add-bookmark-btn', 14 | bookmarkInput: '#add-bookmark-input', 15 | bookmarkLineCloseBtn: '.bookmark-line-close-btn', 16 | bookmarkMark: '.bookmark-mark', 17 | bookmarkPanel: '.bookmark-panel', 18 | bookmarkPanelDismissBtn: '.bookmark-panel-dismiss-btn', 19 | bookmarkPanelHook: '#watch7-content', 20 | bookmarkPanelTrigger: '.action-panel-trigger-bookmarks', 21 | bookmarksScrollbox: '.bookmarks-scrollbox', 22 | bookmarksToggled: 'action-button-bookmarks-toggled', 23 | hideVideoControlClass: 'ytp-autohide', 24 | imgElement: 'img.yt-img-shadow, .ytp-videowall-still-image', 25 | thumbLinkSelector: 'a[href^=\'/watch\']:has(img), a[href*=\'/watch?v=\']:has(img), a[href*=\'/watch?v=\']:has(.ytp-videowall-still-image)', 26 | mainVideo: 'video', 27 | scrubber: '.preview-scrubber', 28 | moviePlayer: '#movie_player, .html5-video-player', 29 | progressBarList: '.ytp-progress-bar .ytp-progress-list', 30 | secondaryActions: '#watch8-secondary-actions, .watch-secondary-actions', 31 | videoThumb: 'yt-img-shadow.ytd-thumbnail, .video-thumb, .yt-uix-simple-thumb-wrap', 32 | ytpPlayProgress: '.ytp-play-progress', 33 | ytpScrubberButton: '.ytp-scrubber-button', 34 | ytpTimeCurrent: '.ytp-time-current', 35 | ytpLeftControls: '.ytp-left-controls', 36 | ytpRewindButton: '.ytp-rewind-button', 37 | getBookmarkPanelHook: function() { 38 | return $(this.bookmarkPanelHook); 39 | }, 40 | getMainVideo: function(el) { 41 | return $(this.mainVideo); 42 | }, 43 | getImgElement: function(el) { 44 | var imgEl = $(el).andSelf().find(this.imgElement); 45 | if (imgEl.length) { 46 | _target = $(el); 47 | _imgEl = imgEl; 48 | } 49 | return imgEl; 50 | }, 51 | getVideoURL: function(el) { 52 | el = el || _target; 53 | return $(el).attr('href'); 54 | }, 55 | getVideoId: function(videoThumbEl) { 56 | return $('[itemprop=videoId]').attr('content') || $('[data-video-id]').attr('data-video-id'); 57 | }, 58 | getVideoIdByElement: function(videoThumbEl) { 59 | var result, videoId = null, 60 | imgSrc = this.getImgElement(videoThumbEl).attr('data-thumb') || this.getImgElement(videoThumbEl).attr('src') || videoThumbEl.baseURI; 61 | if (videoThumbEl.dataset.vid) { 62 | videoId = videoThumbEl.dataset.vid; 63 | } else if (imageIdRegEx.test(imgSrc)) { 64 | result = imageIdRegEx.exec(imgSrc); 65 | videoId = result[2]; 66 | } else if (imageIdRegExV2.test(imgSrc)) { 67 | result = imageIdRegExV2.exec(imgSrc); 68 | videoId = result[2]; 69 | } else if (videoIdRegEx.test(imgSrc)) { 70 | result = videoIdRegEx.exec(imgSrc); 71 | videoId = result[1]; 72 | } else if (channelImageIdRegEx.test(imgSrc)) { 73 | result = channelImageIdRegEx.exec(imgSrc); 74 | videoId = result[1]; 75 | } 76 | return videoId; 77 | }, 78 | getVideoThumbs: function(el) { 79 | return $(el).find(this.videoThumb); 80 | }, 81 | getVideoThumb: function(el) { 82 | return $(el).parents(this.videoThumb); 83 | }, 84 | getStoryboardSpec: function(html) { 85 | return storyboardRegExp.test(html) ? storyboardRegExp.exec(html)[1] : null; 86 | }, 87 | }; 88 | 89 | return _this; 90 | }; -------------------------------------------------------------------------------- /app/scripts.babel/storyboard.js: -------------------------------------------------------------------------------- 1 | var Storyboard = function(storyboardSpec) { 2 | var result = storyboardSpec.split('|'); 3 | var baseUrl = result.shift(); 4 | var index = result.length - 1; 5 | var resolution = result[index].split('#'); 6 | 7 | this.init(resolution, baseUrl, index); 8 | 9 | console.log(this); 10 | return this; 11 | }; 12 | 13 | Storyboard.prototype.init = function(resolution, baseUrl, index) { 14 | this.baseUrl = baseUrl; 15 | this.col = Number(resolution[4]); 16 | this.count = 0; 17 | this.el = null; 18 | this.resolution = resolution; 19 | this.frameWidth = Number(resolution[0]); 20 | this.frameHeight = Number(resolution[1]); 21 | this.height = Number(resolution[1]); 22 | this.index = index; 23 | this.ms = Number(resolution[5]); 24 | this.row = Number(resolution[3]); 25 | this.sigh = resolution[7]; 26 | this.totalFrames = Number(resolution[2]); 27 | this.unit = resolution[6]; 28 | this.width = Number(resolution[0]); 29 | this.progressBar = null; 30 | this.isPlaying = false; 31 | 32 | this.maxPage = Math.ceil(this.totalFrames / (this.row * this.col)); 33 | }; 34 | 35 | Storyboard.prototype.set = function(key, value) { 36 | if (key !== undefined && value !== undefined) { 37 | this[key] = value; 38 | } 39 | return this; 40 | }; 41 | 42 | Storyboard.prototype.getScale = function() { 43 | const proportionWidth = this.frameWidth / this.width; 44 | const proportionHeight = this.frameHeight / this.height; 45 | return Math.min(proportionWidth, proportionHeight); 46 | }; 47 | 48 | Storyboard.prototype.appendThumbTo = function(target) { 49 | if (!this.el) { 50 | const scale = this.getScale(); 51 | const prevElment = target.prevAll('.no-preview, .storyboard'); 52 | this.el = prevElment.length ? prevElment : $('
', { 53 | class: 'storyboard' 54 | }).css({ 55 | width: this.width * scale, 56 | height: this.height * scale, 57 | margin: 'auto', 58 | }); 59 | 60 | this.el 61 | .append(this.getProgressBar().getElement()) 62 | .insertBefore(target); 63 | } 64 | 65 | return this.el; 66 | }; 67 | 68 | Storyboard.prototype.playingFrames = function() { 69 | if (!this.el) return false; 70 | 71 | var pos = this.getPosition(); 72 | this.el.css({ 73 | backgroundImage: 'url(' + this.url() + ')', 74 | backgroundPosition: pos.left + 'px ' + pos.top + 'px', 75 | backgroundSize: pos.width + 'px ' + pos.height + 'px' 76 | }); 77 | 78 | this.increaseCount(); 79 | this.progressBar.update(this.count / this.totalFrames); 80 | 81 | return true; 82 | }; 83 | 84 | Storyboard.prototype.page = function() { 85 | var page = Math.floor(this.count / (this.col * this.row)); 86 | return page % this.maxPage; 87 | }; 88 | 89 | Storyboard.prototype.url = function() { 90 | const $L = this.index; 91 | const $M = this.page(); 92 | const $N = this.unit.replace('$M', $M); 93 | return this.baseUrl.replace(/\\/g, '').replace('$L', $L).replace('$N', $N) + '&sigh=' + this.sigh; 94 | }; 95 | 96 | Storyboard.prototype.getPosition = function() { 97 | var row = this.row; 98 | const scale = this.getScale(); 99 | 100 | if(this.maxPage === this.page() + 1) { 101 | var rest = this.totalFrames % (this.col * this.row); 102 | row = Math.ceil(rest / this.col); 103 | } 104 | return { 105 | scale, 106 | left: -1 * this.width * scale * (this.count % this.col), 107 | top: -1 * this.height * scale * (Math.floor((this.count / row)) % row), 108 | width: this.width * this.col * scale, 109 | height: this.height * row * scale, 110 | }; 111 | }; 112 | 113 | Storyboard.prototype.increaseCount = function() { 114 | this.count = (this.count + 1) % this.totalFrames; 115 | return this.count; 116 | }; 117 | 118 | Storyboard.prototype.reset = function() { 119 | this.count = 0; 120 | }; 121 | 122 | Storyboard.prototype.getProgressBar = function() { 123 | if(!this.progressBar) { 124 | this.progressBar = new ProgressBar(); 125 | } 126 | 127 | return this.progressBar; 128 | }; 129 | 130 | Storyboard.prototype.setFrame = function(progress) { 131 | this.count = Math.floor(this.totalFrames * progress); 132 | }; -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | // generated on 2016-03-25 using generator-chrome-extension 0.5.6 2 | import gulp from 'gulp'; 3 | import util from 'gulp-util'; 4 | import gulpLoadPlugins from 'gulp-load-plugins'; 5 | import del from 'del'; 6 | import runSequence from 'run-sequence'; 7 | import {stream as wiredep} from 'wiredep'; 8 | 9 | const $ = gulpLoadPlugins(); 10 | 11 | gulp.task('extras', () => { 12 | return gulp.src([ 13 | 'app/*.*', 14 | 'app/_locales/**', 15 | '!app/scripts.babel', 16 | '!app/*.json', 17 | '!app/*.html', 18 | '!app/styles.scss' 19 | ], { 20 | base: 'app', 21 | dot: true 22 | }).pipe(gulp.dest('dist')); 23 | }); 24 | 25 | function lint(files, options) { 26 | return () => { 27 | return gulp.src(files) 28 | .pipe($.eslint(options)) 29 | .pipe($.eslint.format()); 30 | }; 31 | } 32 | 33 | gulp.task('lint', lint('app/scripts.babel/**/*.js', { 34 | env: { 35 | es6: true 36 | } 37 | })); 38 | 39 | gulp.task('images', () => { 40 | return gulp.src('app/images/**/*') 41 | .pipe($.if($.if.isFile, $.cache($.imagemin({ 42 | progressive: true, 43 | interlaced: true, 44 | // don't remove IDs from SVGs, they are often used 45 | // as hooks for embedding and styling 46 | svgoPlugins: [{cleanupIDs: false}] 47 | })) 48 | .on('error', function (err) { 49 | console.log(err); 50 | this.end(); 51 | }))) 52 | .pipe(gulp.dest('dist/images')); 53 | }); 54 | 55 | gulp.task('styles', () => { 56 | return gulp.src('app/styles.scss/*.scss') 57 | .pipe($.plumber()) 58 | .pipe($.sass.sync({ 59 | outputStyle: 'expanded', 60 | precision: 10, 61 | includePaths: ['.'] 62 | }).on('error', $.sass.logError)) 63 | .pipe(gulp.dest('app/styles')); 64 | }); 65 | 66 | gulp.task('build-styles', () => { 67 | return gulp.src('app/styles.scss/*.scss') 68 | .pipe($.plumber()) 69 | .pipe($.sass.sync({ 70 | outputStyle: 'compressed', 71 | precision: 10, 72 | includePaths: ['.'] 73 | }).on('error', $.sass.logError)) 74 | .pipe(gulp.dest('dist/styles')); 75 | }); 76 | 77 | gulp.task('html', ['styles'], () => { 78 | return gulp.src('app/*.html') 79 | .pipe($.useref({searchPath: ['.tmp', 'app', '.']})) 80 | .pipe($.sourcemaps.init()) 81 | .pipe($.if('*.js', $.uglify({compress: {drop_console: true}}))) 82 | .pipe($.if('*.css', $.cleanCss({compatibility: '*'}))) 83 | .pipe($.sourcemaps.write()) 84 | .pipe($.if('*.html', $.htmlmin({removeComments: true, collapseWhitespace: true}))) 85 | .pipe(gulp.dest('dist')); 86 | }); 87 | 88 | gulp.task('chromeManifest', () => { 89 | return gulp.src('app/manifest.json') 90 | .pipe($.chromeManifest({ 91 | buildnumber: false, 92 | background: { 93 | target: 'scripts/background.js', 94 | exclude: [ 95 | 'scripts/chromereload.js' 96 | ] 97 | } 98 | })) 99 | .pipe($.if('*.css', $.cleanCss({compatibility: '*'}))) 100 | .pipe($.if('*.js', $.sourcemaps.init())) 101 | .pipe($.if('*.js', $.uglify())) 102 | .pipe($.if('*.js', $.sourcemaps.write('.'))) 103 | .pipe(gulp.dest('dist')); 104 | }); 105 | 106 | gulp.task('babel', () => { 107 | var babelOption = { 108 | presets: ['es2015'], 109 | comments: false 110 | }; 111 | if (util.env.production) { 112 | babelOption.plugins = ["transform-remove-console"] 113 | } 114 | 115 | return gulp.src('app/scripts.babel/**/*.js') 116 | .pipe($.babel(babelOption)) 117 | .pipe(gulp.dest('app/scripts')); 118 | }); 119 | 120 | gulp.task('clean', del.bind(null, ['.tmp', 'dist'])); 121 | 122 | gulp.task('watch', ['lint', 'babel', 'html', 'styles'], () => { 123 | $.livereload.listen(); 124 | 125 | gulp.watch([ 126 | 'app/*.html', 127 | 'app/scripts/**/*.js', 128 | 'app/images/**/*', 129 | 'app/styles/**/*', 130 | 'app/_locales/**/*.json' 131 | ]).on('change', $.livereload.reload); 132 | 133 | gulp.watch('app/scripts.babel/**/*.js', ['lint', 'babel']); 134 | gulp.watch('app/styles.scss/**/*.scss', ['styles']); 135 | gulp.watch('bower.json', ['wiredep']); 136 | }); 137 | 138 | gulp.task('size', () => { 139 | return gulp.src('dist/**/*').pipe($.size({title: 'build', gzip: true})); 140 | }); 141 | 142 | gulp.task('wiredep', () => { 143 | gulp.src('app/*.html') 144 | .pipe(wiredep({ 145 | ignorePath: /^(\.\.\/)*\.\./ 146 | })) 147 | .pipe(gulp.dest('app')); 148 | }); 149 | 150 | gulp.task('package', function () { 151 | var manifest = require('./dist/manifest.json'); 152 | return gulp.src('dist/**') 153 | .pipe($.zip('youtube-preview-' + manifest.version + '.zip')) 154 | .pipe(gulp.dest('package')); 155 | }); 156 | 157 | gulp.task('build', ['clean'], (cb) => { 158 | runSequence( 159 | 'lint', 'babel', 'chromeManifest', 160 | ['html', 'build-styles', 'images', 'extras'], 161 | cb); 162 | }); 163 | 164 | gulp.task('default', ['clean'], cb => { 165 | runSequence('build', cb); 166 | }); 167 | -------------------------------------------------------------------------------- /app/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | YouTube Extension Options 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 |
16 |
17 |
18 |
19 |
20 |

Preview Speed

21 | Adjust the slider below to change the preview intervals. 22 |
23 |
24 |

25 | 26 |

27 |
28 |
29 | 30 | 51 | 52 | 77 | 78 | 110 | 111 |
112 |
113 |
114 |
115 | 116 |
117 |
118 |
119 |
120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /app/scripts.babel/preview.js: -------------------------------------------------------------------------------- 1 | /*global Storyboard, VideoSparkbar, VideoBookmark, API_KEY */ 2 | 3 | var cache = {}, 4 | timeout = null; 5 | 6 | function debounce(fn, delay) { 7 | return function() { 8 | var context = this, 9 | args = arguments; 10 | clearTimeout(timeout); 11 | timeout = setTimeout(function() { 12 | timeout = null; 13 | fn.apply(context, args); 14 | }, delay); 15 | }; 16 | } 17 | 18 | function requestUrl(baseURL, paramsObject) { 19 | if (paramsObject) { 20 | baseURL += $.param(paramsObject); 21 | } 22 | return baseURL; 23 | } 24 | 25 | var chunk = (arr, n) => arr.length ? [arr.slice(0, n), ...chunk(arr.slice(n), n)] : []; 26 | 27 | var Preview = function(Profile, config) { 28 | var _this = { 29 | isPlay: false, 30 | storyboard: null, 31 | imgEl: null, 32 | rewindButton: null, 33 | videoList: {}, 34 | updateConfigs: function(changes) { 35 | for (var key in changes) { 36 | config[key] = changes[key].newValue; 37 | } 38 | return config; 39 | }, 40 | initialize: function() { 41 | // document.removeEventListener('DOMNodeInserted', _this.onDOMNodeInserted); 42 | // document.addEventListener('DOMNodeInserted', _this.onDOMNodeInserted, true); 43 | // _this.delegateOnVideoThumb(); 44 | // _this.injectRewindButton(); 45 | 46 | $(document) 47 | .off('mouseenter mouseleave mousemove click') 48 | .on(scrubberEventHandler, Profile.scrubber) 49 | .on(thumbLinkEventHandler, Profile.thumbLinkSelector); 50 | 51 | _this.videoBookmark = new VideoBookmark(Profile); 52 | }, 53 | injectRewindButton: function() { 54 | if (config.showRewindButton) { 55 | _this.rewindButton = new RewindButton(Profile); 56 | _this.rewindButton.create(Profile.ytpLeftControls); 57 | } else { 58 | _this.rewindButton && _this.rewindButton.remove(); 59 | } 60 | }, 61 | onDOMNodeInserted: function(evt) { 62 | var el = evt.target, 63 | nodeName = el.nodeName.toLowerCase(); 64 | 65 | if (nodeName === 'video') { 66 | _this.videoBookmark.delegateOnVideoBookmark(el); 67 | 68 | _this.injectRewindButton(); 69 | } 70 | 71 | if (['#comment', '#text', 'script', 'style', 'input', 'iframe', 'embed', 'button', 'video', 'link'].indexOf(nodeName) === -1) { 72 | _this.delegateOnVideoThumb(el); 73 | } 74 | return false; 75 | }, 76 | appendRatingTo: function(item) { 77 | var videoSparkbar = new VideoSparkbar(item.id, item.statistics); 78 | videoSparkbar.appendRatingTo($(item.el)); 79 | }, 80 | delegateOnVideoThumb: function(el) { 81 | if (!config.showRatingBar) { return false; } 82 | 83 | var videoElementIdMap = Array.from(Profile.getVideoThumbs(el || document)) 84 | .filter((videoThumbEl) => videoThumbEl.offsetWidth === 0 || videoThumbEl.offsetWidth > 50) 85 | .reduce((acc, videoThumbEl) => { 86 | var id = Profile.getVideoIdByElement(videoThumbEl); 87 | if (id && _this.videoList[id]) { 88 | _this.appendRatingTo(_this.videoList[id]); 89 | return acc; 90 | } 91 | return id ? Object.assign(acc, { [id]: videoThumbEl }) : acc; 92 | }, {}); 93 | 94 | var videoIds = Object.keys(videoElementIdMap); 95 | if (!videoIds.length) return; 96 | 97 | _this.retrieveVideoData(videoIds) 98 | .then((items) => { 99 | items 100 | .filter(item => !!item.statistics) 101 | .forEach(item => { 102 | _this.videoList[item.id] = { 103 | id: item.id, 104 | statistics: item.statistics, 105 | el: videoElementIdMap[item.id] 106 | }; 107 | _this.appendRatingTo(_this.videoList[item.id]); 108 | }) 109 | }); 110 | }, 111 | retrieveVideoData: function(videoIds) { 112 | return Promise.all(chunk(videoIds, 50) 113 | .map((chunkedVideoIds) => 114 | $.ajax({ 115 | url: requestUrl('//www.googleapis.com/youtube/v3/videos?', { 116 | part: 'statistics', 117 | id: chunkedVideoIds.join(','), 118 | key: API_KEY 119 | }), 120 | dataType: 'json', 121 | }) 122 | )).then(promises => promises.reduce((acc, resp) => [...acc, ...resp.items], [])); 123 | }, 124 | getStoryboard: function(storyboardSpec) { 125 | return storyboardSpec ? new Storyboard(storyboardSpec) : new NoPreview(); 126 | }, 127 | loadPreviewImg: function(storyboard, imgEl) { 128 | var parent = Profile.getVideoThumb(imgEl); 129 | storyboard.set('target', imgEl); 130 | storyboard.set('frameWidth', parent.width() || imgEl.width()); 131 | storyboard.set('frameHeight', parent.height() || imgEl.height()); 132 | if (storyboard.isNoPreview) { 133 | storyboard.appendThumbTo(); 134 | } else { 135 | var img = new Image(); 136 | img.src = storyboard.url(); 137 | img.onload = function() { 138 | storyboard.appendThumbTo(parent); 139 | _this.framesPlaying(); 140 | }; 141 | } 142 | }, 143 | framesPlaying: function() { 144 | clearTimeout(timeout); 145 | if (_this.isPlay && _this.storyboard.playingFrames()) { 146 | timeout = setTimeout(function() { 147 | _this.framesPlaying(); 148 | }, config.previewInterval); 149 | } else { 150 | thumbLinkEventHandler.mouseleave(); 151 | } 152 | } 153 | }; 154 | 155 | var thumbLinkEventHandler = { 156 | mouseenter: debounce(function() { 157 | var videoUrl = Profile.getVideoURL(this); 158 | var imgEl = Profile.getImgElement(this); 159 | _this.isPlay = true; 160 | _this.storyboard && _this.storyboard.reset(); 161 | 162 | if (cache[videoUrl]) { 163 | _this.storyboard = cache[videoUrl]; 164 | _this.loadPreviewImg(_this.storyboard, imgEl); 165 | } else { 166 | $.ajax({ 167 | url: videoUrl, 168 | dataType: 'text', 169 | success: function(html) { 170 | var storyboardSpec = Profile.getStoryboardSpec(html); 171 | var storyboard = _this.getStoryboard(storyboardSpec); 172 | if (storyboard && !cache[this.url]) { 173 | _this.storyboard = storyboard; 174 | cache[this.url] = storyboard; 175 | _this.loadPreviewImg(_this.storyboard, imgEl); 176 | } 177 | }, 178 | fail: function() { 179 | var noPreview = new NoPreview(); 180 | _this.storyboard = noPreview; 181 | cache[this.url] = noPreview; 182 | _this.loadPreviewImg(_this.storyboard, imgEl); 183 | } 184 | }); 185 | } 186 | }, config.delayPreview), 187 | mouseleave: function() { 188 | _this.storyboard && _this.storyboard.reset(); 189 | _this.isPlay = false; 190 | clearTimeout(timeout); 191 | }, 192 | }; 193 | 194 | var scrubberEventHandler = { 195 | mousemove: function(evt) { 196 | var progress = evt.offsetX / evt.currentTarget.clientWidth; 197 | _this.isPlay = false; 198 | _this.storyboard.setFrame(progress); 199 | _this.storyboard.playingFrames(); 200 | }, 201 | mouseleave: function() { 202 | _this.isPlay = true; 203 | _this.framesPlaying(); 204 | }, 205 | click: function(evt) { 206 | evt.preventDefault(); 207 | var progress = evt.offsetX / evt.currentTarget.clientWidth; 208 | var listener = $(evt.currentTarget).parents(Profile.thumbLinkSelector); 209 | var videoTimeString = listener.find('.video-time').text() || listener.next('.video-time').text(); 210 | if (videoTimeString) { 211 | var videoTimeArray = videoTimeString.split(':'); 212 | var videoTimeInSec = 0; 213 | for(var i = 0; i < videoTimeArray.length; i++) { 214 | videoTimeInSec += videoTimeArray[i] * Math.pow(60, videoTimeArray.length - i - 1); 215 | } 216 | window.location.search = window.location.search + '&t=' + Math.floor(videoTimeInSec * progress) + 's'; 217 | } 218 | } 219 | }; 220 | 221 | _this.initialize(); 222 | 223 | return _this; 224 | }; -------------------------------------------------------------------------------- /app/scripts.babel/video-bookmark.js: -------------------------------------------------------------------------------- 1 | /*global Fuse */ 2 | 3 | function displayTime(time) { 4 | var h = ~~(time / 3600); 5 | var m = (~~((time % 3600) / 60)).toString(); 6 | var s = ('0' + time % 60).substr(-2); 7 | return [h, m, s].filter(function(val) { 8 | return val; 9 | }).join(':'); 10 | } 11 | 12 | var Mark = function(note, atTime, timestamp) { 13 | this.atTime = atTime; 14 | this.bookmarkHighlightClass = 'bookmark-highlight'; 15 | this.item = this.createBookmarkLine(note, atTime); 16 | this.mark = this.createMarkOnProcessbar(); 17 | this.note = note; 18 | this.timestamp = timestamp; 19 | }; 20 | 21 | Mark.prototype.appendToBar = function(target, duration) { 22 | this.mark 23 | .css({ 24 | left: this.atTime * 100 / duration + '%' 25 | }) 26 | .appendTo(target); 27 | 28 | return this; 29 | }; 30 | 31 | Mark.prototype.highlight = function() { 32 | this.mark.addClass(this.bookmarkHighlightClass); 33 | }; 34 | 35 | Mark.prototype.lightout = function() { 36 | this.mark.removeClass(this.bookmarkHighlightClass); 37 | }; 38 | 39 | Mark.prototype.appendToList = function(target) { 40 | this.item.appendTo(target); 41 | 42 | return this; 43 | }; 44 | 45 | Mark.prototype.remove = function(target) { 46 | this.item.remove(); 47 | 48 | return this; 49 | }; 50 | 51 | Mark.prototype.createBookmarkLine = function(note, atTime) { 52 | return $( 53 | '
' + 54 | '
' + displayTime(atTime) + '
' + 55 | '
' + note + '
' + 56 | '
' + 57 | '
' 58 | ); 59 | }; 60 | 61 | Mark.prototype.createMarkOnProcessbar = function() { 62 | return $('
', { 63 | class: 'bookmark-mark' 64 | }); 65 | }; 66 | 67 | var BookmarkStorage = function(videoId) { 68 | var _storage = {}; 69 | 70 | function initialize() { 71 | _storage.videoId = videoId; 72 | _storage.localStorageKey = 'yt-preview::' + videoId; 73 | _storage.bookmarks = _storage.loadBookmarks(videoId); 74 | } 75 | 76 | function updateStorage(data) { 77 | _storage.bookmarks = data; 78 | 79 | localStorage[_storage.localStorageKey] = JSON.stringify({ 80 | data: data 81 | }); 82 | } 83 | 84 | _storage.loadBookmarks = function() { 85 | var stringData = localStorage[_storage.localStorageKey]; 86 | 87 | if (stringData) { 88 | return JSON.parse(stringData).data || []; 89 | } else { 90 | return []; 91 | } 92 | }; 93 | 94 | _storage.addBookmark = function(object) { 95 | var notes = _storage.bookmarks; 96 | 97 | notes.push(object); 98 | 99 | notes.sort(function(a, b) { 100 | return a.atTime - b.atTime; 101 | }); 102 | 103 | updateStorage(notes); 104 | }; 105 | 106 | _storage.removeBookmark = function(timestamp) { 107 | var notes = $.map(_storage.bookmarks, function(val, i) { 108 | if (val.timestamp !== timestamp) { 109 | return val; 110 | } 111 | }); 112 | 113 | notes.sort(function(a, b) { 114 | return a.atTime - b.atTime; 115 | }); 116 | 117 | updateStorage(notes); 118 | }; 119 | 120 | initialize(); 121 | 122 | return _storage; 123 | }; 124 | 125 | var VideoBookmark = function(Profile) { 126 | var _this = {}, 127 | _storage = null; 128 | 129 | _this.bookmarkId = 0; 130 | 131 | _this.initialize = function() { 132 | _this.delegateOnVideoBookmark(); 133 | 134 | $(document) 135 | .on({ 136 | click: _this.submitBookmark 137 | }, Profile.bookmarkBtn) 138 | .on({ 139 | focus: _this.addBookmarkInputFocus, 140 | blur: _this.addBookmarkInputBlur, 141 | keyup: _this.addBookmarkInputKeyup 142 | }, Profile.bookmarkInput) 143 | .on({ 144 | click: _this.onTriggerBookmarks 145 | }, Profile.bookmarkPanelTrigger) 146 | .on({ 147 | click: _this.onTriggerBookmarks 148 | }, Profile.bookmarkPanelDismissBtn); 149 | }; 150 | 151 | _this.delegateOnVideoBookmark = function(el) { 152 | Profile.getMainVideo(el || document) 153 | .off('loadedmetadata') 154 | .on('loadedmetadata', _this.onLoadedMetaData); 155 | }; 156 | 157 | _this.onLoadedMetaData = function(evt) { 158 | console.log('afterLoadedMetaData'); 159 | 160 | _this.video = evt.target; 161 | _this.videoId = Profile.getVideoId(); 162 | 163 | _storage = new BookmarkStorage(_this.videoId); 164 | 165 | $(_this.video) 166 | .off('timeupdate') 167 | .on('timeupdate', function() { 168 | var currentTime = Math.floor(_this.video.currentTime); 169 | var ratio = currentTime / Math.floor(_this.video.duration); 170 | $(Profile.ytpTimeCurrent).text(displayTime(currentTime)); 171 | $(Profile.ytpScrubberButton).css('left', ratio * 100 + '%'); 172 | $(Profile.ytpPlayProgress).css('transform', 'scaleX(' + ratio + ')'); 173 | }); 174 | 175 | var bookmarkPanelHook = Profile.getBookmarkPanelHook(); 176 | var secondaryActionsList = $(Profile.secondaryActions); 177 | 178 | if (bookmarkPanelHook.find(Profile.bookmarkPanel).length === 0) { 179 | _this.bookmarkPanel = _this.createBookmarkPanel(); 180 | _this.bookmarksScrollbox = _this.bookmarkPanel.find(Profile.bookmarksScrollbox); 181 | _this.bookmarkPanel.prependTo(bookmarkPanelHook); 182 | } 183 | if (secondaryActionsList.find(Profile.bookmarkPanelTrigger).length === 0) { 184 | _this.bookmarkToggler = _this.createBookmarkToggler(); 185 | _this.bookmarkToggler.appendTo(secondaryActionsList); 186 | } 187 | 188 | _this.removeBookmarks(); 189 | _this.loadBookmarks(_storage.bookmarks); 190 | }; 191 | 192 | _this.loadBookmarks = function(bookmarks) { 193 | bookmarks.forEach(function(bookmark) { 194 | _this.addMark(bookmark.note, bookmark.atTime, bookmark.timestamp); 195 | }); 196 | }; 197 | 198 | _this.removeBookmarks = function() { 199 | $(Profile.bookmarkMark).remove(); 200 | }; 201 | 202 | _this.addBookmarkInputFocus = function(evt) { 203 | console.log('focus'); 204 | _this.isVideoPaused = _this.video.paused; 205 | _this.video.pause(); 206 | }; 207 | 208 | _this.addBookmarkInputBlur = function() { 209 | console.log('blur'); 210 | if (!_this.isVideoPaused) { 211 | _this.video.play(); 212 | } 213 | }; 214 | 215 | _this.submitBookmark = function() { 216 | var addBookmarInput = $(Profile.bookmarkInput); 217 | var value = addBookmarInput.val(); 218 | 219 | if (value) { 220 | _this.addBookmark(value); 221 | addBookmarInput.val('').blur(); 222 | } 223 | }; 224 | 225 | _this.addBookmarkInputKeyup = function(evt) { 226 | var value = evt.target.value; 227 | console.log('keyup'); 228 | 229 | if (evt.keyCode === 13) { 230 | console.log('enter'); 231 | _this.submitBookmark(); 232 | } else { 233 | if (value.length > 2) { 234 | _this.render(new Fuse(_storage.bookmarks, { 235 | keys: ['note'] 236 | }).search(value)); 237 | } else { 238 | _this.render(_storage.bookmarks); 239 | } 240 | } 241 | }; 242 | 243 | _this.createBookmarkToggler = function() { 244 | var bookmarkToggler = $( 245 | '' 248 | ); 249 | 250 | return bookmarkToggler; 251 | }; 252 | 253 | _this.onTriggerBookmarks = function(evt) { 254 | if (_this.bookmarkToggler.hasClass(Profile.bookmarksToggled)) { 255 | _this.bookmarkPanel.hide(); 256 | _this.bookmarkToggler.removeClass(Profile.bookmarksToggled); 257 | } else { 258 | _this.bookmarkPanel.show(); 259 | _this.bookmarkToggler.addClass(Profile.bookmarksToggled); 260 | } 261 | }; 262 | 263 | _this.createBookmarkPanel = function() { 264 | var bookmarksNotFound = 'The bookmark could not be loaded.'; 265 | var bookmarkPanelTitle = 'Bookmark list'; 266 | 267 | return $( 268 | '' 304 | ); 305 | }; 306 | 307 | _this.addBookmark = function(note) { 308 | _storage.addBookmark({ 309 | note: note, 310 | timestamp: +new Date(), 311 | atTime: Math.floor(_this.video.currentTime) 312 | }); 313 | 314 | _this.render(_storage.bookmarks); 315 | }; 316 | 317 | _this.render = function(bookmarks) { 318 | $(Profile.bookmarksScrollbox).html(''); 319 | _this.loadBookmarks(bookmarks); 320 | }; 321 | 322 | _this.addMark = function(note, atTime, timestamp) { 323 | var mark = new Mark(note, atTime, timestamp); 324 | var moviePlayer = $(Profile.moviePlayer); 325 | var progressBar = $(Profile.progressBarList); 326 | 327 | mark.item 328 | .on({ 329 | click: function(evt) { 330 | if (_this.video && _this.video.currentTime) { 331 | _this.video.currentTime = evt.currentTarget.dataset.time; 332 | _this.video.play(); 333 | } 334 | }, 335 | mouseover: function() { 336 | mark.highlight(); 337 | moviePlayer.removeClass(Profile.hideVideoControlClass); 338 | }, 339 | mouseout: function() { 340 | mark.lightout(); 341 | } 342 | }) 343 | .on('click', Profile.bookmarkLineCloseBtn, function(e) { 344 | e.stopPropagation(); 345 | mark.remove(); 346 | _storage.removeBookmark(mark.timestamp); 347 | }); 348 | 349 | mark 350 | .appendToBar(progressBar, _this.video.duration) 351 | .appendToList(_this.bookmarksScrollbox); 352 | }; 353 | 354 | _this.initialize(); 355 | 356 | return _this; 357 | }; -------------------------------------------------------------------------------- /app/styles.scss/preview.scss: -------------------------------------------------------------------------------- 1 | $video-time-bottom: 6px; 2 | $preview-sparkbars-height: 6px; 3 | $storyboard-background-color: #000; 4 | $no-preview-background-color: rgba(0, 0, 0, 0.5); 5 | $no-preview-color: rgba(255, 255, 255, 0.5); 6 | $no-preview-font-size: 20px; 7 | $preview-sparkbars-likes-background-color: #167ac6; 8 | $preview-sparkbars-dislikes-background-color: #ccc; 9 | $enlarged-video-list-content-wrapper-margin-left: 180px; 10 | $enlarged-video-list-thumb-height: 90px; 11 | $enlarged-video-list-thumb-width: 160px; 12 | $enlarged-video-list-image-height: 118px; 13 | $enlarged-video-list-image-width: 160px; 14 | $playlist-preview-sparkbars-margin-in-radio-playlist: 0 0 0 15px; 15 | $playlist-preview-sparkbars-margin-in-video-playlist: 0 0 0 28px; 16 | $playlist-preview-sparkbars-margin: 0 0 0 15px; 17 | $playlist-preview-sparkbars-width: 72px; 18 | $video-image-72-top-adjustment: -6px; 19 | $video-image-120-top-adjustment: -11px; 20 | $video-image-top-reset: 0; 21 | $enlarged-playlist-preview-sparkbars-width: 160px; 22 | $enlarged-playlist-video-thumb-height: 90px; 23 | $enlarged-playlist-video-thumb-width: 160px; 24 | $enlarged-playlist-video-image-height: 118px; 25 | $enlarged-playlist-video-image-width: 160px; 26 | $enlarged-video-image-top-adjustment: -14px; 27 | $enlarged-video-image-top-reset: 0; 28 | 29 | 30 | .ytd-thumbnail { 31 | &:hover { 32 | .watched-badge { 33 | display: none; 34 | } 35 | 36 | .watched .video-thumb { 37 | opacity: 1; 38 | } 39 | 40 | .no-preview{ 41 | display: block; 42 | } 43 | 44 | .storyboard { 45 | display: block; 46 | & + .ytp-videowall-still-image, 47 | & + img { 48 | display: none; 49 | } 50 | & + .ytd-thumbnail > img { 51 | display: none; 52 | } 53 | } 54 | 55 | .storyboard ~ #mouseover-overlay { 56 | display: none !important; 57 | } 58 | } 59 | } 60 | .storyboard { 61 | display: none; 62 | background-color: $storyboard-background-color; 63 | } 64 | .no-preview { 65 | display: none; 66 | background-color: $no-preview-background-color; 67 | color: $no-preview-color; 68 | font-size: $no-preview-font-size; 69 | position: absolute; 70 | text-align: center; 71 | z-index: 1; 72 | } 73 | /* 74 | Modification of original layout 75 | video-time: it's covering the rating sparkbars 76 | yt-thumb-clip: it's weird positioning affecting the preview position 77 | thumb-wrapper: set overflow to visible to show the sparkbars 78 | */ 79 | .video-list-item.related-list-item .video-actions, 80 | .video-list-item.related-list-item .video-time, 81 | .yt-lockup-thumbnail .video-actions, 82 | .yt-lockup-thumbnail .video-time { 83 | bottom: $video-time-bottom; 84 | } 85 | .video-thumb .yt-thumb-clip { 86 | bottom: 0; 87 | left: 0; 88 | right: 0; 89 | top: 0; 90 | } 91 | .video-list-item.related-list-item .thumb-wrapper { 92 | padding-bottom: 4px; 93 | } 94 | .yt-thumb-72 .yt-thumb-clip { 95 | .storyboard + img { 96 | top: $video-image-top-reset; 97 | } 98 | img { 99 | position: relative; 100 | top: $video-image-72-top-adjustment; 101 | } 102 | } 103 | .yt-thumb-120 .yt-thumb-clip { 104 | .storyboard + .ytd-thumbnail { 105 | top: $video-image-top-reset; 106 | } 107 | img { 108 | position: relative; 109 | top: $video-image-120-top-adjustment; 110 | } 111 | } 112 | /* 113 | Large size thumb 370px, .yt-thumb-370 114 | */ 115 | .yt-thumb-370 .yt-thumb-clip { 116 | img { 117 | margin-top: -2.2rem; 118 | } 119 | } 120 | /* 121 | Preview Sparkbars 122 | */ 123 | .watch-queue-item .preview-sparkbars, 124 | .playlist-video .preview-sparkbars { 125 | float: left; 126 | background-color: $preview-sparkbars-dislikes-background-color; 127 | margin: $playlist-preview-sparkbars-margin-in-video-playlist; 128 | width: $playlist-preview-sparkbars-width; 129 | } 130 | .radio-playlist .preview-sparkbars { 131 | margin: $playlist-preview-sparkbars-margin-in-radio-playlist !important; 132 | } 133 | .preview-sparkbars { 134 | position: relative; 135 | clear: both; 136 | height: $preview-sparkbars-height; 137 | overflow: hidden; 138 | transition: height 0.5s; 139 | > .preview-sparkbar-likes { 140 | background: $preview-sparkbars-likes-background-color; 141 | } 142 | > .preview-sparkbar-dislikes { 143 | background: $preview-sparkbars-dislikes-background-color; 144 | } 145 | > .preview-sparkbar-dislikes, 146 | > .preview-sparkbar-likes { 147 | float: left; 148 | height: $preview-sparkbars-height; 149 | transition: width 0.5s; 150 | } 151 | &.loading { 152 | height: 0; 153 | > .preview-sparkbar-dislikes, 154 | > .preview-sparkbar-likes { 155 | width: 0 !important; 156 | } 157 | } 158 | } 159 | .bookmark-mark { 160 | background-color: rgb(255, 179, 62); 161 | bottom: 0; 162 | height: 100%; 163 | left: 0; 164 | position: absolute; 165 | transform-origin: 0 0; 166 | width: 3px; 167 | z-index: 42; 168 | &.bookmark-highlight { 169 | width: 0; 170 | height: 0; 171 | border-left: 6px solid transparent; 172 | border-right: 6px solid transparent; 173 | border-top: 6px solid rgb(255, 179, 62); 174 | background-color: transparent; 175 | margin-bottom: 6px; 176 | margin-left: -3px; 177 | } 178 | } 179 | .bookmarks-menu { 180 | width: 100%; 181 | #bookmark-search { 182 | margin-top: 3px; 183 | max-width: 100%; 184 | overflow: hidden; 185 | padding: 0; 186 | position: relative; 187 | .add-bookmark-btn { 188 | background: #f8f8f8; 189 | border-bottom-left-radius: 0; 190 | border-color: #d3d3d3; 191 | border-left: 0; 192 | border-top-left-radius: 0; 193 | color: #333; 194 | float: right; 195 | height: 29px; 196 | padding: 0; 197 | span { 198 | font-size: 20px; 199 | i { 200 | display: inline-block; 201 | background-size: auto; 202 | border: none; 203 | box-shadow: none; 204 | height: 15px; 205 | opacity: 0.6; 206 | margin: 0 25px; 207 | padding: 0; 208 | text-indent: -10000px; 209 | width: 15px; 210 | &.search-btn-icon { 211 | background: url('chrome-extension://__MSG_@@extension_id__/images/search.png'); 212 | } 213 | &.add-btn-icon { 214 | background: url('chrome-extension://__MSG_@@extension_id__/images/add.png'); 215 | } 216 | } 217 | } 218 | } 219 | #bookmark-search-terms { 220 | background-color: #fff; 221 | border: 1px solid #ccc; 222 | box-shadow: inset 0 1px 2px #eee; 223 | box-sizing: border-box; 224 | font-size: 14px; 225 | height: 29px; 226 | line-height: 30px; 227 | margin: 0 0 2px; 228 | overflow: hidden; 229 | position: relative; 230 | transition: border-color 0.2s ease; 231 | input { 232 | background: transparent; 233 | border: 0; 234 | box-sizing: border-box; 235 | display: inline-block; 236 | font-size: 14px; 237 | height: 100%; 238 | left: 0; 239 | margin: 0; 240 | outline: none; 241 | padding: 2px 6px; 242 | position: absolute; 243 | width: 100%; 244 | } 245 | } 246 | } 247 | } 248 | .bookmark-panel-dismiss-btn { 249 | background: none; 250 | border: none; 251 | box-shadow: none; 252 | color: #333; 253 | filter: alpha(opacity=50); 254 | opacity: 0.5; 255 | position: absolute; 256 | right: 3px; 257 | top: 3px; 258 | &.yt-uix-button { 259 | border-radius: 2px; 260 | border: none; 261 | box-shadow: none; 262 | cursor: pointer; 263 | display: inline-block; 264 | font-size: 11px; 265 | font-weight: 500; 266 | height: 28px; 267 | line-height: normal; 268 | outline: 0; 269 | padding: 0 10px; 270 | text-decoration: none; 271 | vertical-align: middle; 272 | white-space: nowrap; 273 | word-wrap: normal; 274 | } 275 | &:before { 276 | background-size: auto; 277 | background: url('chrome-extension://__MSG_@@extension_id__/images/close.png'); 278 | content: ''; 279 | display: inline-block; 280 | height: 10px; 281 | vertical-align: middle; 282 | width: 10px; 283 | } 284 | } 285 | .bookmark-panel { 286 | background: #fff; 287 | border: 0; 288 | box-shadow: 0 1px 2px rgba(0,0,0,.1); 289 | box-sizing: border-box; 290 | margin: 0 0 10px; 291 | padding: 15px; 292 | position: relative; 293 | } 294 | .bookmarks-scrollbox { 295 | cursor: pointer; 296 | margin: 5px 3px; 297 | max-height: 15.6em; 298 | overflow-x: hidden; 299 | overflow-y: scroll; 300 | .bookmark-line { 301 | border-radius: 3px; 302 | border: 1px solid #fff; 303 | line-height: 1.3em; 304 | margin-right: 2px; 305 | width: 100%; 306 | &:hover { 307 | background-color: #d0e0fa; 308 | .bookmark-line-close-btn:before { 309 | display: inline-block; 310 | } 311 | } 312 | .bookmark-line-time { 313 | display: inline-block; 314 | width: 40px; 315 | color: #666; 316 | vertical-align: middle; 317 | } 318 | .bookmark-line-text { 319 | display: inline-block; 320 | overflow: hidden; 321 | width: 520px; 322 | vertical-align: middle; 323 | } 324 | .bookmark-line-close-btn { 325 | background: none; 326 | border: none; 327 | box-shadow: none; 328 | color: #333; 329 | cursor: pointer; 330 | display: inline-block; 331 | filter: alpha(opacity=50); 332 | float: right; 333 | line-height: normal; 334 | opacity: 0.5; 335 | outline: 0; 336 | padding-left: 11px; 337 | padding-right: 11px; 338 | text-decoration: none; 339 | vertical-align: middle; 340 | &:before { 341 | background-image: url('chrome-extension://__MSG_@@extension_id__/images/close.png'); 342 | background-repeat: no-repeat; 343 | background-size: 8px 8px; 344 | content: ''; 345 | display: none; 346 | height: 8px; 347 | vertical-align: middle; 348 | width: 8px; 349 | } 350 | } 351 | } 352 | } 353 | .action-panel-trigger-bookmarks { 354 | background: none; 355 | border-radius: 0; 356 | border: none; 357 | box-shadow: none; 358 | cursor: pointer; 359 | display: inline-block; 360 | font-family: Roboto,arial,sans-serif; 361 | font-size: 11px; 362 | font-weight: 500; 363 | height: 28px; 364 | line-height: normal; 365 | margin: 0; 366 | opacity: 0.5; 367 | outline: 0; 368 | padding: 0 10px; 369 | text-decoration: none; 370 | vertical-align: middle; 371 | white-space: nowrap; 372 | word-wrap: normal; 373 | &.action-button-bookmarks-toggled { 374 | opacity: 0.8; 375 | } 376 | &:before { 377 | background: url('chrome-extension://__MSG_@@extension_id__/images/bookmark-icon.png'); 378 | content: ''; 379 | display: inline-block; 380 | height: 20px; 381 | margin-right: 6px; 382 | vertical-align: middle; 383 | width: 20px; 384 | } 385 | } 386 | 387 | .ytp-videowall-still div.preview-scrubber { 388 | bottom: 20px; 389 | top: auto; 390 | } 391 | 392 | div.preview-scrubber { 393 | position: absolute; 394 | z-index: 3; 395 | top: 8px; 396 | left: 15px; 397 | right: 15px; 398 | height: 4px; 399 | border-radius: 4px; 400 | background: #111; 401 | background: linear-gradient(#000000,#222222); 402 | box-shadow: rgba(255,255,255,0.2) 0 0 0 1px; 403 | transition: opacity 0.2s linear; 404 | padding: 1px; 405 | display: block; 406 | } 407 | 408 | div.preview-scrubber .preview-scrubbed { 409 | background: #ffb7b7; 410 | background: linear-gradient(to bottom, #ffb7b7 0%,#f00000 50%,#f00000 100%); 411 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffb7b7', endColorstr='#f00000',GradientType=0 ); 412 | height: 4px; 413 | border-radius: 4px; 414 | min-width: 6px; 415 | } -------------------------------------------------------------------------------- /app/styles.scss/options.scss: -------------------------------------------------------------------------------- 1 | @import '../bower_components/material-design-lite/material.min'; 2 | 3 | $preview-sparkbars-height: 6px; 4 | $preview-sparkbars-likes-background-color: #167ac6; 5 | $preview-sparkbars-dislikes-background-color: #ccc; 6 | 7 | body.option-page::before { 8 | background-attachment: fixed; 9 | background-size: cover; 10 | bottom: 0; 11 | content: ''; 12 | left: 0; 13 | position: fixed; 14 | right: 0; 15 | top: 0; 16 | will-change: transform; 17 | z-index: -1; 18 | } 19 | @media (max-width: 512px) and (min-resolution: 1.5dppx), (max-width: 1024px) and (max-resolution: 1.5dppx) { 20 | body.option-page::before { 21 | background-image: url('chrome-extension://__MSG_@@extension_id__/images/bg_2048.png'); 22 | background-position: center; 23 | } 24 | } 25 | @media (min-width: 513px) and (max-width: 1024px) and (min-resolution: 1.5dppx), (min-width: 1025px) and (max-width: 2048px) and (max-resolution: 1.5dppx) { 26 | body.option-page::before { 27 | background-image: url('chrome-extension://__MSG_@@extension_id__/images/bg_2048.png'); 28 | background-position: center; 29 | } 30 | } 31 | @media (min-width: 1025px) and (min-resolution: 1.5dppx), (min-width: 2049px) and (max-resolution: 1.5dppx) { 32 | body.option-page::before { 33 | background-image: url('chrome-extension://__MSG_@@extension_id__/images/bg_2048.png'); 34 | background-position: center; 35 | } 36 | } 37 | body.option-page .preview-options { 38 | font-family: 'Roboto', 'Helvetica', sans-serif; 39 | } 40 | .preview-options__posts { 41 | align-items: center; 42 | height: 100%; 43 | justify-content: center; 44 | } 45 | .mdl-card { 46 | min-width: 334px; 47 | } 48 | .mdl-card .mdl-card__title { 49 | color: #fff; 50 | padding: 0; 51 | height: 250px; 52 | } 53 | .preview-intervals .animated-background { 54 | background-color: #ffffff; 55 | background-repeat: no-repeat; 56 | background-size: 1288px 946px; 57 | background-image: url('chrome-extension://__MSG_@@extension_id__/images/muybridge_horse.png'); 58 | height: 236.5px; 59 | margin-left: auto; 60 | margin-right: auto; 61 | width: 322px; 62 | } 63 | .preview-sparkbars { 64 | clear: both; 65 | height: 0; 66 | left: 0; 67 | overflow: hidden; 68 | position: absolute; 69 | top: 250px; 70 | transition: height 0.5s; 71 | width: 100%; 72 | > .preview-sparkbar-likes { 73 | background: $preview-sparkbars-likes-background-color; 74 | } 75 | > .preview-sparkbar-dislikes { 76 | background: $preview-sparkbars-dislikes-background-color; 77 | } 78 | > .preview-sparkbar-dislikes, 79 | > .preview-sparkbar-likes { 80 | float: left; 81 | height: $preview-sparkbars-height; 82 | transition: width 0.5s; 83 | } 84 | } 85 | .rating-bar { 86 | &.on { 87 | .preview-sparkbars { 88 | height: $preview-sparkbars-height; 89 | } 90 | } 91 | 92 | .background { 93 | background-color: #ffffff; 94 | background-repeat: no-repeat; 95 | background-size: 322px 236.5px; 96 | background-image: url('chrome-extension://__MSG_@@extension_id__/images/youtube-logo.png'); 97 | height: 236.5px; 98 | margin-left: auto; 99 | margin-right: auto; 100 | width: 322px; 101 | } 102 | } 103 | 104 | .mdl-card__title-text { 105 | color: #ffffff; 106 | left: 18px; 107 | position: absolute; 108 | text-shadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0,0,0,.1), 0 0 5px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,.2), 0 5px 10px rgba(0,0,0,.25), 0 10px 10px rgba(0,0,0,.2), 0 20px 20px rgba(0,0,0,.15); 109 | top: 210px; 110 | } 111 | .mdl-snackbar--active { 112 | transform: translate(-50%, 0); 113 | } 114 | .preview-options .preview-options__posts { 115 | display: flex; 116 | flex-shrink: 0; 117 | margin: 0 auto; 118 | max-width: 900px; 119 | padding: 0; 120 | width: 100%; 121 | } 122 | .preview-options.mdl-layout .mdl-layout__content { 123 | height: 100%; 124 | -webkit-overflow-scrolling: touch; 125 | } 126 | .preview-options .mdl-card { 127 | align-items: stretch; 128 | display: flex; 129 | flex-direction: column; 130 | min-height: 360px; 131 | } 132 | .preview-options .mdl-card__media { 133 | align-items: flex-end; 134 | background-size: cover; 135 | box-sizing: border-box; 136 | cursor: pointer; 137 | display: flex; 138 | flex-direction: row; 139 | flex-grow: 1; 140 | padding: 24px; 141 | } 142 | .preview-options .mdl-card__media a, 143 | .preview-options .mdl-card__title a { 144 | color: inherit; 145 | } 146 | .preview-options .mdl-card__supporting-text { 147 | align-items: center; 148 | display: flex; 149 | min-height: 64px; 150 | padding: 16px; 151 | width: 100%; 152 | } 153 | .preview-options .mdl-card__supporting-text strong { 154 | font-weight: 400; 155 | } 156 | .preview-options .mdl-card__media ~ .mdl-card__supporting-text { 157 | min-height: 64px; 158 | } 159 | .preview-options .mdl-card__supporting-text:not(:last-child) { 160 | box-sizing: border-box; 161 | min-height: 76px; 162 | } 163 | .preview-options:not(.preview-options--blogpost) .mdl-card__supporting-text ~ .mdl-card__supporting-text { 164 | border-top: 1px solid rgba(0,0,0,0.1); 165 | } 166 | .preview-options .mdl-card__actions:first-child { 167 | margin-left: 0; 168 | } 169 | .preview-options .meta { 170 | align-items: center; 171 | box-sizing: border-box; 172 | display: flex; 173 | flex-direction: row; 174 | height: auto; 175 | justify-content: flex-start; 176 | padding: 16px; 177 | } 178 | .preview-options .meta > .meta__favorites { 179 | flex-direction: row; 180 | font-size: 13px; 181 | font-weight: 500; 182 | margin: 0 8px; 183 | } 184 | .preview-options .meta > .meta__favorites .material-icons { 185 | cursor: pointer; 186 | font-size: 2em; 187 | margin-left: 12px; 188 | } 189 | .preview-options .mdl-card .meta.meta--fill { 190 | justify-content: space-between; 191 | } 192 | .preview-options .meta > *:first-child { 193 | margin-right: 16px; 194 | } 195 | .preview-options .meta > * { 196 | display: flex; 197 | flex-direction: column; 198 | } 199 | .preview-options.is-small-screen .preview-options__posts > .mdl-card.coffee-pic { 200 | order: 0; 201 | } 202 | .preview-options.is-small-screen .preview-options__posts > .mdl-card.something-else { 203 | order: -1; 204 | } 205 | .preview-options .something-else .mdl-card__media { 206 | align-items: center; 207 | display: flex; 208 | flex-direction: column; 209 | justify-content: center; 210 | } 211 | .preview-options .something-else > button { 212 | position: absolute; 213 | right: 28px; 214 | top: 0; 215 | transform: translate(0px, -28px); 216 | } 217 | .preview-options .something-else .mdl-card__media { 218 | border-top-left-radius: 2px; 219 | border-top-right-radius: 2px; 220 | font-size: 13px; 221 | font-weight: 500; 222 | } 223 | .preview-options .something-else .mdl-card__media img { 224 | height: 64px; 225 | margin-bottom: 10px; 226 | width: 64px; 227 | } 228 | .preview-options .something-else .mdl-card__supporting-text { 229 | background-color: #F5F5F5; 230 | border-bottom-left-radius: 2px; 231 | border-bottom-right-radius: 2px; 232 | } 233 | .preview-options .preview-options__posts > .demo-nav { 234 | align-items: center; 235 | color: white; 236 | display: flex; 237 | flex-direction: row; 238 | font-weight: 500; 239 | justify-content: space-between; 240 | margin: 12px 15px; 241 | } 242 | .preview-options .preview-options__posts > .demo-nav > .demo-nav__button { 243 | color: white; 244 | text-decoration: none; 245 | } 246 | .preview-options .preview-options__posts > .demo-nav .mdl-button { 247 | color: rgba(0,0,0,0.54); 248 | background-color: white; 249 | } 250 | .preview-options .preview-options__posts > .demo-nav > .demo-nav__button:first-child .mdl-button { 251 | margin-right: 16px; 252 | } 253 | .preview-options .preview-options__posts > .demo-nav > .demo-nav__button:last-child .mdl-button { 254 | margin-left: 16px; 255 | } 256 | .preview-options .mdl-card > a { 257 | color: inherit; 258 | font-weight: inherit; 259 | text-decoration: none; 260 | } 261 | .preview-options .mdl-card h3 { 262 | margin: 0; 263 | } 264 | .preview-options .mdl-card h3 a { 265 | text-decoration: none; 266 | } 267 | .preview-options .mdl-card h3.quote:after, 268 | .preview-options .mdl-card h3.quote:before { 269 | display: block; 270 | font-size: 3em; 271 | margin-top: 0.5em; 272 | } 273 | .preview-options .mdl-card h3.quote:before { 274 | content: '“'; 275 | } 276 | .preview-options .mdl-card h3.quote:after { 277 | content: '”'; 278 | } 279 | .preview-options--blogpost .custom-header { 280 | background-color: transparent; 281 | } 282 | .preview-options--blogpost .comments { 283 | background-color: #EEE; 284 | } 285 | .preview-options--blogpost .meta > * { 286 | align-items: center; 287 | } 288 | .preview-options--blogpost .meta + .mdl-card__supporting-text { 289 | border: 0; 290 | display: flex; 291 | flex-direction: column; 292 | } 293 | .preview-options--blogpost .meta + .mdl-card__supporting-text p { 294 | font-size: 16px; 295 | line-height: 28px; 296 | margin: 16px auto; 297 | max-width: 512px; 298 | } 299 | .preview-options--blogpost .comments { 300 | align-items: stretch; 301 | box-sizing: border-box; 302 | display: flex; 303 | flex-direction: column; 304 | justify-content: flex-start; 305 | padding: 32px; 306 | } 307 | .preview-options--blogpost .comments > form { 308 | display: flex; 309 | flex-direction: row; 310 | margin-bottom: 16px; 311 | } 312 | .preview-options--blogpost .comments > form .mdl-textfield { 313 | color: rgb(97, 97, 97); 314 | flex-grow: 1; 315 | margin-right: 16px; 316 | } 317 | /* Workaround for Firefox. 318 | * User agent stylesheet kept overwriting the font in FF only. 319 | */ 320 | .preview-options--blogpost .comments > form .mdl-textfield .mdl-textfield__input { 321 | font-family: 'Roboto', 'Helvetica', sans-serif; 322 | } 323 | .preview-options--blogpost .comments > form .mdl-textfield input, 324 | .preview-options--blogpost .comments > form .mdl-textfield textarea { 325 | resize: none; 326 | } 327 | .preview-options--blogpost .comments > form button { 328 | background-color: rgba(0, 0, 0, 0.24); 329 | color: white; 330 | margin-top: 20px; 331 | } 332 | .preview-options--blogpost .comments .comment { 333 | align-items: stretch; 334 | display: flex; 335 | flex-direction: column; 336 | } 337 | .preview-options--blogpost .comments .comment > .comment__header { 338 | align-items: center; 339 | display: flex; 340 | flex-direction: row; 341 | margin-bottom: 16px; 342 | } 343 | .preview-options--blogpost .comments .comment > .comment__header > .comment__avatar { 344 | border-radius: 24px; 345 | height: 48px; 346 | margin-right: 16px; 347 | width: 48px; 348 | } 349 | .preview-options--blogpost .comments .comment > .comment__header > .comment__author { 350 | display: flex; 351 | flex-direction: column; 352 | flex-grow: 1; 353 | } 354 | .preview-options--blogpost .comments .comment > .comment__text { 355 | line-height: 1.5em; 356 | } 357 | .preview-options--blogpost .comments .comment > .comment__actions { 358 | align-items: center; 359 | display: flex; 360 | flex-direction: row; 361 | font-size: 0.8em; 362 | justify-content: flex-start; 363 | margin-top: 16px; 364 | } 365 | .preview-options--blogpost .comments .comment > .comment__actions button { 366 | color: rgba(0, 0, 0, 0.24); 367 | margin-right: 16px; 368 | } 369 | .preview-options--blogpost .comments .comment > .comment__answers { 370 | padding-left: 48px; 371 | padding-top: 32px; 372 | } 373 | .preview-options--blogpost .demo-back { 374 | color: white; 375 | left: 16px; 376 | position: absolute; 377 | top: 16px; 378 | z-index: 9999; 379 | } 380 | .preview-options .section-spacer { 381 | flex-grow: 1; 382 | } 383 | .preview-options .something-else { 384 | overflow: visible; 385 | z-index: 10; 386 | } 387 | .preview-options .amazing .mdl-card__title { 388 | background-color: #263238; 389 | } 390 | /* Fixes for IE 10 */ 391 | .mdl-grid { 392 | display: flex !important; 393 | flex-wrap: nowrap !important; 394 | } 395 | .preview-options .mdl-mini-footer { 396 | align-items: center; 397 | background-color: white; 398 | box-sizing: border-box; 399 | height: 120px; 400 | margin-top: 80px; 401 | padding: 40px; 402 | } 403 | 404 | .rewind-button { 405 | .background { 406 | background-color: #d21216; 407 | background-repeat: no-repeat; 408 | background-size: 322px 236.5px; 409 | height: 236.5px; 410 | margin-left: auto; 411 | margin-right: auto; 412 | width: 322px; 413 | } 414 | } --------------------------------------------------------------------------------