├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bower.json ├── gulpfile.js ├── package.json └── src ├── app-drawer.html ├── app-item.html ├── auto-suggestions.html ├── bookmark-item.html ├── coffee ├── Column.coffee ├── FeedColumn.coffee └── Tabbie.coffee ├── column-chooser.html ├── columns ├── apps │ └── Apps.coffee ├── behance │ ├── Behance.coffee │ └── behance-item.html ├── bookmarks │ ├── Bookmarks.coffee │ └── bookmark-tabs.html ├── closedtabs │ └── ClosedTabs.coffee ├── codepen │ ├── Codepen.coffee │ └── codepen-item.html ├── customcolumn │ ├── CustomColumn.coffee │ └── feedly-item.html ├── designernews │ ├── DesignerNews.coffee │ └── dn-item.html ├── dribbble │ ├── Dribbble.coffee │ └── dribbble-item.html ├── github │ ├── GitHub.coffee │ ├── github-dialog.html │ └── github-item.html ├── gmail │ ├── Gmail.coffee │ ├── gmail-auth.html │ ├── gmail-dialog.html │ └── gmail-item.html ├── hackernews │ ├── HackerNews.coffee │ └── hn-item.html ├── lobsters │ ├── Lobsters.coffee │ ├── lobsters-dialog.html │ └── lobsters-item.html ├── producthunt │ ├── ProductHunt.coffee │ ├── ph-dialog.html │ ├── ph-item.html │ └── ph-thumb.html ├── pushbullet │ ├── PushBullet.coffee │ ├── pushbullet-auth.html │ ├── pushbullet-dialog.html │ └── pushbullet-item.html ├── reddit │ ├── Reddit.coffee │ ├── reddit-dialog.html │ ├── reddit-error.html │ └── reddit-item.html ├── speeddial │ ├── SpeedDial.coffee │ ├── speed-dial-dialog.html │ └── speed-dial-item.html └── topsites │ └── TopSites.coffee ├── config.rb ├── fab-anim.html ├── font ├── Roboto-Regular-webfont.woff └── RobotoSlab-Regular-webfont.ttf ├── fullscreen-dialog.html ├── img ├── arrow_up.svg ├── chrome.png ├── column-apps.png ├── column-behance.png ├── column-bookmarks.png ├── column-closedtabs.png ├── column-codepen.png ├── column-designernews.png ├── column-dribble.png ├── column-github.png ├── column-gmail.png ├── column-hackernews.png ├── column-lobsters.png ├── column-producthunt.png ├── column-pushbullet.png ├── column-reddit.png ├── column-speeddial.png ├── column-topsites.png ├── column-unknown.png ├── comment.svg ├── comment_hover.svg ├── default-speeddialitem-icon.png ├── icon_128.png ├── icon_48.png ├── icon_64.png ├── tour-add.png ├── tour-edit.png └── tour-more.png ├── item-card.html ├── item-column.html ├── manifest.json ├── recently-item.html ├── sass ├── feeditem.scss └── screen.scss ├── tab.html ├── tabbie-dialog.html ├── time-ago.html └── tour-step.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | bower_components/ 4 | dist/** 5 | node_modules/ 6 | src/.sass-cache -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | before_script: 5 | - gem update --system 6 | - gem install compass 7 | - npm install -g gulp bower 8 | - bower install 9 | script: gulp 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Your first column 2 | Making columns for Tabbie is extremely easy, and fun! 3 | This guide will show you how to make a basic column that gets some data from a external source (like pratically any column) 4 | We won't care if your column is not in that format. A column doesn't necessarily need to be in the above said format, but for the sake of simplicity we'll use FeedColumn in this guide. You are free to extend from Column as well, however, that won't be explained here. 5 | Feeds can be from a JSON or a RSS source. 6 | I assume you have at least a little bit of understanding of polymer and coffeescript, and of course, frontend development in general. 7 | This guide is still WIP and a more in-depth, 'real' documentation page is planned. 8 | 9 | ## Step 1: Setting up your development environment. 10 | 11 | - Make sure you've installed 12 | - [node](http://nodejs.org) 13 | - [Ruby](https://www.ruby-lang.org) (or use [rvm](https://rvm.io/)) 14 | - [Git](http://git-scm.org) 15 | - Fork the project by clicking on fork at the right top. 16 | - Clone your fork to your local machine. 17 | ```bash 18 | git clone https://github.com/yourusername/tabbie 19 | cd tabbie 20 | ``` 21 | 22 | 23 | - Run the following (in your working directory) 24 | ```bash 25 | # ignore this command if you already have gulp & bower installed. Remove 'sudo' from the beginning if on windows 26 | sudo npm install -g gulp bower 27 | # ignore if you already installed compass 28 | gem install compass 29 | npm install 30 | bower install 31 | gulp watch # every time you run this commands will overwrite the 'dist' folder with the new changed updates if there're, you can re-run this command several times to test your changes. 32 | ``` 33 | - **Load the extension into chrome**: 34 | A new folder called 'dist' has appeared in your working directory. Everything in src/ will be compiled to dist/. Do not touch dist as it's all compiled files. 35 | Load the dist folder into chrome by going to settings > extensions. Enable developer mode if you haven't already and choose 'Load unpacked extension...' and choose the dist folder. 36 | 37 | ## Step 2: File structure 38 | Start up by creating said files. It is easiest to copy a existing column and go from there. 39 | The average column looks like this: 40 | ``` 41 | |-- reddit 42 | | 43 | |--- Reddit.coffee 44 | |--- reddit-item.html 45 | ---- reddit-dialog.html 46 | ``` 47 | - The coffee file, is the main class of the column, all column logic and settings happen here. 48 | - The \*-item.html file which is used by FeedColumn (more on this below) 49 | - The \*-dialog.html has the contents of the configuration dialog. Optional. 50 | 51 | Start by renaming all files to your column (Reddit.coffee > Myservice.coffee, reddit-dialog.html > myservice-dialog.html), etc. 52 | Also rename the class file and the register call (`tabbie.register "Myservice"`) at the bottom of the class 53 | 54 | ## Step 3: Column image 55 | A column image exists of a 150x150 png in the src/img directory. 56 | Column image can be defined with the 'thumb' property within a column's class. (more on that below) 57 | Files should be in column-\*.png. 58 | Column images have a background that comes from [the google design color palette](https://www.google.com/design/spec/style/color.html#color-color-palette). 59 | It is very important that column images have a background on them, as tabbie automatically looks for the most dominant color, and uses it for the rest of a column's 'toolbar'. 60 | The logo itself on the column image must be white (#FFFFF), and a width of 75% of the total image, centered within the image itself. 61 | Preferably vector (check out [font-awesome](http://fontawesome.io), or google's [material icons](https://google.github.io/material-design-icons/)) 62 | 63 | ## Step 4: [Get coding](http://media.giphy.com/media/6OrCT1jVbonHG/giphy.gif) 64 | At this point I mostly assume you've got enough to get going, the existing columns are perfect examples of what is possible and how things need to be defined, I'll try to get into some more detail on everything down here below. 65 | 66 | Here's an example of a column that uses all properties available (some are optional, see below) 67 | ```coffee 68 | class Columns.Lobsters extends Columns.FeedColumn 69 | name: "Lobste.rs" 70 | width: 1 71 | thumb: "column-lobsters.png" 72 | 73 | element: "lobsters-item" 74 | dialog: "lobsters-dialog" 75 | dataPath: "data.items" 76 | childPath: "data" 77 | 78 | refresh: (holderEl, columnEl) => 79 | if not @config.listing then @config.listing = 0 80 | 81 | switch @config.listing 82 | when 0 then listing = "hottest" 83 | when 1 then listing = "newest" 84 | 85 | @url = "https://lobste.rs/"+listing+".json" 86 | super holderEl, columnEl 87 | 88 | tabbie.register "Lobsters" 89 | ``` 90 | In this example we, as you can see, manipulate the 'url' property by overriding refresh. We change the url based on how what the configuration is (configured by the user trough the dialog). 91 | Note that overriding refresh is not necessary, but done here for demonstration purposes. If your url never changes, you won't have to override refresh. 92 | 93 | 94 | Properties you can override: 95 | _Note_: Fields not marked as required are optional. 96 | 97 | **name** _required_ : Your column's display name. 98 | **width**: The width of your column (1 = 1 * 25% of the screen) 99 | **thumb** _required_: The filename of your column's image in src/img 100 | **element** _required_: The element name of the item you defined in _columnname_-item.html 101 | **dialog**: The element name of your configuration dialog in _columnname_-dialog.html 102 | **dataPath**: The 'path' of your data, if the server doesn't return a array as response, you'll need to use this. For example if your server response is formatted like `{ data: { items: [] } }`, then your data path will be 'data.items' 103 | **childPath**: Same as dataPath, if the items in your array are not the 'root' of your item, use childPath. 104 | **dataType**: defaults to 'json', if your source is a RSS feed, use 'xml' here. 105 | **config**: Contains user's configuration, configured trough the dialog. 106 | **cache**: Contains cache, which has an array of previously loaded items. 107 | 108 | Functions you can override: 109 | 110 | **refresh** (columnElement, holderElement): Called when refreshing is needed. columnElement is a reference to the column element defined in 'element'. holderElement is the holder in which items will need to be added. 111 | **draw** (data, holderElement): This is where FeedColumn appends children to the holder. data is an array with the server's response. holderElement is the element where items need to be appended to. 112 | 113 | 114 | ### Configuring your **columnName.coffee** for RSS - XML BASED - feeds 115 | tabbie is configured by default to hadle JSON sources automatically, but you can tweak it to handle your xml based column with this little instructions: 116 | - Make a permission-request over the browser, because some browsers disable downloading data over domains 117 | - Modify fields to handle your XML source (Built-in, dont worry) 118 | - Go to your columnName-item.coffee and start coding 119 | 120 | first of all you have to make sure you are politely requesting the user to give you his permission over downloading this XML over his broswer by using this code: 121 | ```coffee 122 | attemptAdd: (successCallback) -> 123 | chrome.permissions.request 124 | origins: ['http://feeds.feedburner.com/'] ## put here your rss domain 125 | , (granted) => 126 | if granted and typeof successCallback is 'function' then successCallback() 127 | 128 | ``` 129 | Make sure to put your own RSS domain, in this example the XML url is `http://feeds.feedburner.com/scientificamerican?fmt=xml` so the RSS domain is `http://feeds.feedburner.com/`. 130 | **note**: post this requesting code below your fields section and over your `tabbie.register "columnName"` line, like so 131 | 132 | ```coffee 133 | class Columns.columnName extends Columns.FeedColumn 134 | name: "Column Name" 135 | width: 1 136 | thumb: "img/column-columnName.png" 137 | link: "https://www.columnWebsite.com/" 138 | 139 | element: "columnName-item" 140 | url: "http://feeds.feedburner.com/scientificamerican?fmt=xml" ## your xml url instead of .JSON one 141 | responseType: "xml" ## it's dangerously important as it's our little magic tweak 142 | xmlTag: "item" ## put here your parent tag which contains " The full post style ", more detailed below 143 | 144 | attemptAdd: (successCallback) -> 145 | chrome.permissions.request 146 | origins: ['http://feeds.feedburner.com/'] 147 | , (granted) => 148 | if granted and typeof successCallback is 'function' then successCallback() 149 | 150 | tabbie.register "ScientificAmerican" 151 | ``` 152 | So, if your XML file was like this: 153 | ```xml 154 | 155 | The Most Momentous Year in the History of Paleoanthropology 156 | http://rss.sciam.com/~r/ScientificAmerican-News/~3/8Flz11GwMxs/ 157 | Sat, 13 Jun 2015 00:01:00 GMT 158 | Evolutionary Biology 159 | In an excerpt from his new book Ian Tattersall lays out the story of how a scientific giant in the field of evolution put forth a spectacularly incorrect theory about the diversity of hominids:F7zBnMyn0Lo 160 | 161 | 162 | ``` 163 | Your **xmlTag** will be like that `xmlTag: "post"` 164 | 165 | Now your finished with the columnName.coffe, go to your columnName-item.html and start coding, nothing changes when you want to the `` tag in your `<h1></h1>` just write down - as usual - : 166 | ```html 167 | <h1>{{item.title}}</h1> 168 | ``` 169 | and so on. 170 | 171 | 172 | ## Step 5: Your elements 173 | - The **-dialog** element has 2 variables, 'config' and 'column'. 174 | You can add controls that use the the config's values as vlue (for example `<input type='text' value='{{config.username}}>`) 175 | `column` is a reference to your column class (for example `Column name: <input type='text' value='{{column.name}}>` 176 | - The **-item** element has a single variable, called 'item', item represents one of the values in the server's response. 177 | 178 | ## Step 6: Pull request 179 | When you're satisfied with your work, you can file a pull request to the main tabbie repo, this is needed for your column to appear in the official extension. 180 | You can send us a pull request straight from github itself, [just hit the 'compare & review' button on your repo's page.](https://help.github.com/articles/using-pull-requests/) 181 | We will then review your work, and if it's all good, it'll get added, yay! 182 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); 2 | you may not use this file except in compliance with the License. 3 | You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | 13 | Author: Jari Zwarts (@JariZwarts) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## RIP Tabbie 2 | 3 | Hi there, 4 | Tabbie is now officially deprecated. 5 | It does not function anymore because of it's reliance of [rather experimental features in chrome](https://www.polymer-project.org/blog/2015-12-01-deprecating-deep), that have now been completely removed. 6 | 7 | We're working on a rewrite over on [quarryapp/app](https://github.com/quarryapp/app). 8 | Follow me [on twitter](https://twitter.com/JariZwarts) to stay up to date on it's progress. 9 | 10 | ---- 11 | 12 | # Tabbie <span style='float:right'>[![Join the chat at https://gitter.im/jariz/tabbie](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jariz/tabbie?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/jariz/tabbie.svg?branch=master&style=flat-square)](https://travis-ci.org/jariz/tabbie) [![](https://img.shields.io/badge/Chrome-Extension-yellow.svg?style=flat-square)](https://chrome.google.com/webstore/detail/tabbie/kckhddfnffeofnfjcpdffpeiljicclbd) 13 | 14 | ![](https://cloud.githubusercontent.com/assets/1415847/7947591/5ebce982-097e-11e5-99b8-2ceb979dfda7.png) 15 | ![](https://cloud.githubusercontent.com/assets/1415847/7947610/806ab334-097e-11e5-91d4-9852390391f3.png) 16 | ![](https://cloud.githubusercontent.com/assets/1415847/7947613/86c0d312-097e-11e5-9c8d-3ce7cfa5361b.png) 17 | 18 | ### What is it? 19 | 20 | Tabbie keeps you informed, inspired, and up to date through it's beautiful and customizable columns. 21 | Tabbie replaces your 'new tab' page with your favorite websites. 22 | Choose exactly what content you want to see. 23 | 24 | ### Content 25 | 26 | Tabbie has a bunch of build in columns, but **any RSS-complaint site indexed by [Feedly](https://feedly.com) can be added to Tabbie.** 27 | 28 | Tabbie by default comes with the following services / sites; 29 | - Dribbble 30 | - Behance 31 | - HackerNews 32 | - Designer News 33 | - GitHub 34 | - Reddit (frontpage/multireddit/frontpage of your account) 35 | - Lobste.rs 36 | - ProductHunt 37 | - Gmail 38 | - PushBullet 39 | - Codepen 40 | - Chrome Apps, Bookmarks, and Top sites. 41 | 42 | ### Features 43 | - Material design based on Google's design principles. 44 | - Reposition columns 45 | - Customizable grid 46 | - Add/remove columns 47 | - Customize column-specific settings 48 | - Decentralized, gets it data straight from 3rd party API's 49 | - Resize columns 50 | - Rename columns 51 | 52 | 53 | ## Contributing to Tabbie 54 | Want to add your favorite website? Want to add a awesome column that has nothing to do with websites? You can! 55 | It is extremely simple to contribute to Tabbie! You won't even need to install the extension. 56 | 57 | - [Creating your first Tabbie column.](https://github.com/jariz/tabbie/blob/master/CONTRIBUTING.md) 58 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "newtab", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "JariZ <jarizw@gmail.com>" 6 | ], 7 | "license": "MIT", 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "test", 13 | "tests" 14 | ], 15 | "dependencies": { 16 | "polymer": "Polymer/polymer#~0.5.3", 17 | "core-elements": "Polymer/core-elements#~0.5.4", 18 | "paper-elements": "Polymer/paper-elements#~0.5.4", 19 | "store.js": "~1.3.17", 20 | "color-thief": "*", 21 | "packery": "~1.3.2", 22 | "draggabilly": "~1.1.1", 23 | "momentjs": "~2.9.0", 24 | "google-signin": "~0.2.1", 25 | "pleasejs": "~0.4.2", 26 | "pushbullet-js": "https://github.com/alexschneider/pushbullet-js.git", 27 | "URIjs": "~1.15.0", 28 | "underscore": "~1.8.3" 29 | }, 30 | "resolutions": { 31 | "webcomponentsjs": "^0.6.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | vulcanize = require('gulp-vulcanize'), 3 | compass = require('gulp-compass'), 4 | plumber = require('gulp-plumber'), 5 | path = require('path'), 6 | concat = require('gulp-concat'), 7 | coffee = require('gulp-coffee'), 8 | sourcemaps = require('gulp-sourcemaps'), 9 | runSequence = require('run-sequence'), 10 | browserSync = require('browser-sync'), 11 | del = require('del'), 12 | zip = require('gulp-zip'); 13 | 14 | gulp.task('default', ['html', 'compass', 'coffee', 'libs', 'copy']) 15 | gulp.task('package', function() { 16 | return runSequence('html', 'compass', 'coffee', 'libs', 'copy', 'zip'); 17 | }) 18 | 19 | gulp.task('html', function() { 20 | return runSequence('columns', 'vulcanize', function() { 21 | del('src/columns/compiled') 22 | }); 23 | }) 24 | 25 | gulp.task('columns', function() { 26 | return gulp.src('src/columns/**/*.html') 27 | .pipe(plumber()) 28 | .pipe(concat('columns.html')) 29 | .pipe(gulp.dest('src/columns/compiled')) 30 | }) 31 | 32 | gulp.task('libs', function() { 33 | return gulp.src([ 34 | 'bower_components/web-animations-js/web-animations-next-lite.min.js', 35 | 'bower_components/polymer/polymer.js', 36 | 'bower_components/core-focusable/core-focusable.js', 37 | 'bower_components/core-focusable/polymer-mixin.js', 38 | 39 | 'bower_components/packery/dist/packery.pkgd.js', 40 | 'bower_components/store.js/store.js', 41 | 'bower_components/color-thief/src/color-thief.js', 42 | 'bower_components/draggabilly/dist/draggabilly.pkgd.js', 43 | 'bower_components/fetch/fetch.js', 44 | 'bower_components/momentjs/moment.js', 45 | 'bower_components/pleasejs/src/Please.js', 46 | 'bower_components/pushbullet-js/pushbullet.js', 47 | 'bower_components/URIjs/src/URI.min.js', 48 | 'bower_components/underscore/underscore-min.js' 49 | ]) 50 | .pipe(concat('libs.js')) 51 | .pipe(gulp.dest('dist/js')) 52 | }) 53 | 54 | gulp.task('copy', function() { 55 | //for files that don't need to be compiled. but just copied 56 | gulp.src('src/font/*') 57 | .pipe(gulp.dest("dist/font")); 58 | gulp.src('src/img/*') 59 | .pipe(gulp.dest("dist/img")); 60 | return gulp.src('src/manifest.json') 61 | .pipe(gulp.dest('dist')) 62 | }) 63 | 64 | gulp.task('zip', function() { 65 | return gulp.src("dist/**") 66 | .pipe(zip('tabbie.zip')) 67 | .pipe(gulp.dest('./')) 68 | }); 69 | 70 | gulp.task('vulcanize', function () { 71 | return gulp.src('src/**.html') 72 | .pipe(plumber()) 73 | .pipe(vulcanize({ 74 | dest: 'dist', 75 | strip: false, 76 | csp: true, // chrome does not approve of inline scripts 77 | excludes: { 78 | imports: [ 79 | //do not use roboto import because it requires external server (imported trough screen.scss) 80 | 'roboto.html', 81 | 82 | //do not use the following imports as they try to import scripts from it's bower location, which we don't package. 83 | //(these get packaged in libs.js) 84 | 'core-focusable.html', 85 | 'polymer.html', 86 | 'web-animations.html' 87 | ] 88 | } 89 | })) 90 | .pipe(gulp.dest('dist')) 91 | }); 92 | 93 | gulp.task('coffee', function() { 94 | return gulp.src('src/**/*.coffee') 95 | .pipe(plumber()) 96 | .pipe(sourcemaps.init()) 97 | .pipe(coffee({ 98 | bare: true 99 | })) 100 | .pipe(concat('main.js')) 101 | .pipe(sourcemaps.write()) 102 | .pipe(gulp.dest('dist/js')) 103 | }); 104 | 105 | gulp.task('compass', function() { 106 | return gulp.src('src/sass/*.scss') 107 | .pipe(plumber()) 108 | .pipe(compass({ 109 | project: path.join(__dirname, 'src'), 110 | css: path.join(__dirname, 'dist/css'), 111 | sourcemap: true 112 | })) 113 | .pipe(gulp.dest('dist/css')); 114 | }); 115 | 116 | gulp.task('serve', ['default'], function () { 117 | browserSync({ 118 | server: { 119 | baseDir: '.' 120 | }, 121 | startPath: 'dist/tab.html', 122 | reloadDelay: 1500 123 | }); 124 | }); 125 | 126 | gulp.task('reload', function () { 127 | return browserSync.reload(); 128 | }); 129 | 130 | gulp.task('watch', ['default'], /*['serve'], */function () { 131 | gulp.watch('src/**/*.scss', ['compass', 'reload']); 132 | 133 | gulp.watch('src/**/*.html', ['html', 'reload']); 134 | 135 | gulp.watch('src/**/*.coffee', ['coffee', 'reload']); 136 | 137 | gulp.watch('src/manifest.json', ['copy']); 138 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackertab", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "tab.html", 6 | "dependencies": { 7 | "browser-sync": "^2.0.1", 8 | "del": "^1.1.1", 9 | "gulp": "^3.8.10", 10 | "gulp-coffee": "^2.2.0", 11 | "gulp-compass": "^2.0.3", 12 | "gulp-concat": "^2.4.3", 13 | "gulp-plumber": "^0.6.6", 14 | "gulp-sourcemaps": "^1.3.0", 15 | "gulp-vulcanize": "^5.0.0", 16 | "gulp-zip": "^2.0.2", 17 | "run-sequence": "^1.0.2" 18 | }, 19 | "devDependencies": {}, 20 | "scripts": { 21 | "test": "echo \"Error: no test specified\" && exit 1" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/jariz/hackertab.git" 26 | }, 27 | "author": "jariz", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/jariz/hackertab/issues" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app-drawer.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../bower_components/paper-shadow/paper-shadow.html"> 3 | <link rel="import" href="../bower_components/core-overlay/core-overlay.html"> 4 | <polymer-element name="app-drawer"> 5 | <template> 6 | <style> 7 | :host { 8 | position: fixed; 9 | z-index: 105; 10 | left:0; 11 | top:0; 12 | width: 100%; 13 | height: 100%; 14 | visibility: hidden; 15 | } 16 | 17 | paper-shadow { 18 | position: absolute !important; 19 | right: 0px; 20 | top:50px; 21 | 22 | width:400px; 23 | height: 400px; 24 | background: #fff; 25 | border-radius: 0 0 3px 3px; 26 | opacity:0; 27 | } 28 | 29 | .content { 30 | width:400px; 31 | height:400px; 32 | overflow:hidden; 33 | overflow-y:auto; 34 | opacity:0; 35 | /*transition:opacity 300ms ease-out;*/ 36 | /*transition-delay: 300ms;*/ 37 | } 38 | 39 | paper-shadow.playing { 40 | opacity:1; 41 | /*-webkit-animation: tabbieDrawer 300ms ease-out;*/ 42 | /*-webkit-animation-direction: normal;*/ 43 | } 44 | 45 | paper-shadow.playing .content { 46 | opacity:1; 47 | } 48 | 49 | paper-shadow.playing.reverse .content { 50 | /*transition-delay: 0ms;*/ 51 | /*opacity:0;*/ 52 | } 53 | 54 | paper-shadow.playing.reverse { 55 | /*-webkit-animation-direction: reverse;*/ 56 | /*-webkit-animation-delay: 500ms;*/ 57 | } 58 | 59 | @-webkit-keyframes tabbieDrawer { 60 | 0% { 61 | opacity:0; 62 | top:0; 63 | right:25px; 64 | width:50px; 65 | height:50px; 66 | border-radius: 50%; 67 | } 68 | 69 | 50% { 70 | opacity:1; 71 | } 72 | 73 | 99% { 74 | border-radius: 3px; 75 | } 76 | 77 | 100% { 78 | opacity:1; 79 | top:50px; 80 | right:0; 81 | width:400px; 82 | height:400px; 83 | border-radius: 0 0 3px 3px; 84 | } 85 | } 86 | </style> 87 | <div class="overlay" fit></div> 88 | <paper-shadow> 89 | <div class="content"> 90 | <content></content> 91 | </div> 92 | </paper-shadow> 93 | </template> 94 | <script> 95 | Polymer({ 96 | attached: function() { 97 | var self = this; 98 | //overlay click handler that hides the drawer 99 | this.querySelector("html /deep/ .overlay").addEventListener("click", function() { 100 | self.hide(); 101 | }); 102 | //handler that resets the drawer after hide animation has finished 103 | this.querySelector("html /deep/ paper-shadow").addEventListener("webkitAnimationEnd", function() { 104 | if(!this.classList.contains("reverse")) return; 105 | this.classList.remove("playing"); 106 | this.classList.remove("reverse") 107 | self.style.visibility = "hidden" 108 | }); 109 | }, 110 | show: function() { 111 | this.opened = true; 112 | this.style.visibility = "visible" 113 | var paper = this.querySelector("html /deep/ paper-shadow"); 114 | paper.classList.add("playing"); 115 | }, 116 | opened: false, 117 | openedChanged: function() { 118 | this.fire("opened-changed"); 119 | }, 120 | hide: function() { 121 | this.opened = false; 122 | this.style.visibility = "hidden" 123 | var paper = this.querySelector("html /deep/ paper-shadow"); 124 | paper.classList.add("reverse"); 125 | } 126 | }) 127 | </script> 128 | </polymer-element> 129 | -------------------------------------------------------------------------------- /src/app-item.html: -------------------------------------------------------------------------------- 1 | <polymer-element name="app-item" attributes="name icon" noscript> 2 | <template> 3 | <style> 4 | :host { 5 | width:50%; 6 | float:left; 7 | position: relative; 8 | padding:10px; 9 | cursor:pointer; 10 | flex: 1 0 150px; 11 | } 12 | h5 { 13 | width:100%; 14 | text-align: center; 15 | } 16 | img { 17 | width:64px; 18 | height:64px; 19 | display:block; 20 | margin:0 auto; 21 | } 22 | </style> 23 | 24 | <paper-ripple fit></paper-ripple> 25 | <img src="{{icon}}"> 26 | <h5>{{name}}</h5> 27 | </template> 28 | </polymer-element> -------------------------------------------------------------------------------- /src/auto-suggestions.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../bower_components/paper-ripple/paper-ripple.html"> 3 | 4 | <polymer-element name="auto-suggestions" attributes="suggestions error"> 5 | <template> 6 | <style> 7 | .item[highlighted] { 8 | background-color: #E3F2FD; 9 | } 10 | 11 | .item { 12 | padding:30px 20px; 13 | width:100%; 14 | box-sizing: border-box; 15 | position: relative; 16 | cursor:pointer; 17 | transition:background 250ms; 18 | } 19 | 20 | .item img { 21 | float:left; 22 | margin-right:10px; 23 | margin-top: 3px; 24 | width:32px; 25 | height:32px; 26 | } 27 | 28 | .item h4 { 29 | margin:0; 30 | overflow:hidden; 31 | text-overflow: ellipsis; 32 | position: relative; 33 | white-space: nowrap; 34 | } 35 | 36 | .item .website { 37 | font-size: 10px; 38 | background-color: #a2bf72; 39 | padding: 0.2em 0.7em; 40 | float: right; 41 | color:#fff; 42 | } 43 | 44 | .item h5 { 45 | color:#424242; 46 | margin:0; 47 | font-size: 0.7em; 48 | overflow:hidden; 49 | text-overflow: ellipsis; 50 | position: relative; 51 | white-space: nowrap; 52 | } 53 | 54 | .error { 55 | font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; 56 | text-align: center; 57 | padding-top: 80px; 58 | padding-bottom: 80px; 59 | color:#646464; 60 | } 61 | 62 | .error p { 63 | font-size: .9em; 64 | margin: 0.2em 0 0; 65 | } 66 | 67 | .error core-icon { 68 | width:64px; 69 | height:64px; 70 | } 71 | </style> 72 | <div vertical layout wrap class="list"> 73 | <template repeat="{{suggestion in suggestions}}"> 74 | <div highlighted?="{{suggestion == activeSuggestion}}" class="item" on-click="{{choose}}" on-mouseenter="{{highlightCurrent}}"> 75 | <img src="{{suggestion.iconUrl ? suggestion.iconUrl : 'img/column-unknown.png'}}"> 76 | <span style="background-color: {{suggestion.website | formatWebsite | color}}" class="website">{{suggestion.website | formatWebsite}}</span> 77 | <h4>{{suggestion.title}}</h4> 78 | <h5>{{suggestion.description}}</h5> 79 | <paper-ripple fit></paper-ripple> 80 | </div> 81 | </template> 82 | 83 | <div class="error" hidden?="{{!error}}"> 84 | <core-icon icon="cloud-off"></core-icon> 85 | <p>Unable to get your sites. Is your internet down?</p> 86 | </div> 87 | 88 | <!-- todo feedly shoutout --> 89 | </div> 90 | 91 | </template> 92 | <script> 93 | Polymer({ 94 | highlightDown: function() { 95 | if(this.index < (this.suggestions.length - 1)) { 96 | this.index++; 97 | this.indexChanged() 98 | } 99 | }, 100 | highlightUp: function() { 101 | if(this.index > 0) { 102 | this.index--; 103 | this.indexChanged() 104 | } 105 | }, 106 | highlightChosen: function() { 107 | this.fire("suggestion-chosen", this.activeSuggestion) 108 | }, 109 | highlightCurrent: function(e) { 110 | this.activeSuggestion = e.target.templateInstance.model.suggestion 111 | 112 | //not so nice way to find index based on a element #vanillajsthings 113 | this.index = Array.prototype.indexOf.call(this.shadowRoot.querySelector(".list").children, e.target) - 1; 114 | }, 115 | indexChanged: function() { 116 | this.activeSuggestion = this.suggestions[this.index]; 117 | }, 118 | suggestionsChanged: function() { 119 | if(this.suggestions.length) { 120 | this.index = 0; 121 | this.indexChanged() 122 | } 123 | }, 124 | index: 0, 125 | activeSuggestion: undefined, 126 | colors: {}, 127 | color: function(id) { 128 | if(id in this.colors) return this.colors[id]; 129 | else return this.colors[id] = Please.make_color()[0]; 130 | }, 131 | formatWebsite: function(url) { 132 | var host = new URI(url).hostname(); 133 | if(host.substring(0, 4) == "www.") host = host.substring(4) 134 | return host 135 | }, 136 | choose: function(e) { 137 | this.fire("suggestion-chosen", e.target.templateInstance.model.suggestion); 138 | }, 139 | 140 | suggestions: [], 141 | error: false 142 | }); 143 | </script> 144 | </polymer-element> -------------------------------------------------------------------------------- /src/bookmark-item.html: -------------------------------------------------------------------------------- 1 | <link href="../bower_components/polymer/polymer.html" rel="import"> 2 | <link href="../bower_components/paper-item/paper-item.html" rel="import"> 3 | <link href="time-ago.html" rel="import"> 4 | <polymer-element name="bookmark-item" attributes="title url date level opened item" noscript> 5 | <template> 6 | <style> 7 | paper-item { 8 | padding-left: calc(15px * {{level}}); 9 | font-size:12px; 10 | } 11 | img, core-icon { 12 | margin-right:10px; 13 | float:left; 14 | color:#616161; 15 | } 16 | img { 17 | margin-right:18px; 18 | } 19 | 20 | p { 21 | margin: 0; 22 | } 23 | 24 | a { 25 | color:#000; 26 | } 27 | 28 | time-ago { 29 | float:right; 30 | color:#616161; 31 | } 32 | </style> 33 | <template if="{{!folder}}"> 34 | <a href="{{url}}"> 35 | <paper-item> 36 | <p flex?="{{showdate}}"><img src="chrome://favicon/{{url}}"> {{title}}</p> 37 | <template if="{{showdate}}"> 38 | <time-ago epoch="false" datetime="{{date}}"></time-ago> 39 | </template> 40 | </paper-item> 41 | </a> 42 | </template> 43 | <template if="{{folder}}"> 44 | <paper-item> 45 | <core-icon icon="{{!opened ? 'folder' : 'folder-open'}}"></core-icon> {{title}} 46 | </paper-item> 47 | </template> 48 | 49 | </template> 50 | </polymer-element> -------------------------------------------------------------------------------- /src/coffee/Column.coffee: -------------------------------------------------------------------------------- 1 | window.Columns = window.Columns || {} 2 | window.Columns.Column = class Column 3 | constructor: (properties, dontCalculateColor) -> 4 | @className = @constructor.name 5 | 6 | if properties then @[key] = properties[key] for key of properties when typeof properties[key] isnt 'function' 7 | 8 | if not dontCalculateColor and not @color 9 | thmb = document.createElement "img" 10 | thmb.addEventListener "load", => 11 | ct = new ColorThief thmb 12 | @color = ct.getColor thmb 13 | thmb.src = @thumb 14 | 15 | if not @config then @config = {} 16 | if not @cache then @cache = [] 17 | @refreshing = false 18 | @reloading = true 19 | @_refreshing = false 20 | 21 | error: (holderElement) -> 22 | holderElement.setAttribute("hidden", "") 23 | colEl = holderElement.parentElement; 24 | error = colEl.querySelector(".error") 25 | error.removeAttribute("hidden") 26 | error.offsetTop #re-render hack 27 | error.style.opacity = 1 28 | 29 | settings: (cb) -> 30 | if @dialog 31 | dialog = document.createElement @dialog 32 | dialog.config = @config 33 | dialog.column = @ 34 | document.body.appendChild dialog 35 | #warning, nasty code ahead 36 | _d = null 37 | dialog.toggle = -> 38 | if not _d then _d = this.shadowRoot.querySelector "tabbie-dialog" 39 | _d.toggle() 40 | dialog.toggle() 41 | dialog.shadowRoot.querySelector("tabbie-dialog").shadowRoot.querySelector("paper-button.ok").addEventListener "click", -> 42 | @config = dialog.config 43 | tabbie.sync @ 44 | dialog.toggle() 45 | if typeof cb is 'function' then cb dialog 46 | else if typeof cb is 'function' then cb dialog 47 | 48 | refresh: (columnElement, holderElement) -> 49 | 50 | attemptAdd: (successCallback) -> 51 | if typeof successCallback is 'function' then successCallback() 52 | 53 | handleHandler = undefined 54 | editMode: (enable) => 55 | trans = tabbie.meta.byId "core-transition-center" 56 | handle = @columnElement.querySelector "html /deep/ .handle" 57 | 58 | if enable 59 | getPercentage = (target, width) => 60 | if width 61 | base = document.querySelector(".grid-sizer").clientWidth 62 | absolute = Math.round((target.style.width.substring(0, target.style.width.length - 2) ) / base) 63 | final = absolute * 25 64 | if final is 0 then final = 25 65 | else 66 | base = document.querySelector(".grid-sizer").clientHeight 67 | absolute = Math.round((target.style.height.substring(0, target.style.height.length - 2) ) / base) 68 | final = absolute * 50 69 | if final is 0 then final = 50 70 | 71 | if final > 100 then final = 100 72 | console.info "[getPercentage] width?", width, "base", base, "absolute", absolute, "final", final, "%" 73 | return final 74 | 75 | preview = document.createElement "div" 76 | preview.classList.add "resize-preview" 77 | preview.style.visibility = "hidden" 78 | document.querySelector(".column-holder").appendChild preview 79 | 80 | #resize logic 81 | target = @columnElement 82 | handle.addEventListener "mousedown", @handleHandler = (event) => 83 | event.preventDefault() 84 | target.style.transition = "none" 85 | startX = event.clientX - target.clientWidth 86 | startY = event.clientY - target.clientHeight 87 | mouseUpBound = false 88 | console.log "startY", startY, "startX", startX 89 | document.addEventListener "mousemove", msmv = (event) => 90 | event.preventDefault() 91 | newX = event.clientX - startX 92 | newY = event.clientY - startY 93 | 94 | if preview.style.visibility isnt "visible" 95 | preview.style.visibility = "visible" 96 | preview.style.top = target.style.top 97 | preview.style.left = target.style.left 98 | 99 | preview.style.width = getPercentage(target, true)+"%" 100 | preview.style.height = getPercentage(target, false)+"%" 101 | 102 | target.style.zIndex = 107 103 | target.style.width = newX + 'px' 104 | target.style.height = newY + 'px' 105 | 106 | if not mouseUpBound 107 | mouseUpBound = true 108 | document.addEventListener "mouseup", msp = (event) => 109 | event.preventDefault() 110 | #clean up events 111 | document.removeEventListener "mousemove", msmv 112 | document.removeEventListener "mouseup", msp 113 | 114 | target.style.transition = "width 250ms, height 250ms" 115 | widthPerc = getPercentage target, true 116 | heightPerc = getPercentage target, false 117 | target.style.width = widthPerc + "%" 118 | target.style.height = heightPerc + "%" 119 | @width = widthPerc / 25 120 | @height = heightPerc / 50 121 | preview.style.visibility = "hidden" 122 | tabbie.sync @ 123 | target.addEventListener "webkitTransitionEnd", trnstn = -> 124 | target.removeEventListener "webkitTransitionEnd", trnstn 125 | target.style.zIndex = 1 126 | tabbie.packery.layout() 127 | 128 | handle.style.visibility = "visible" 129 | @draggie.enable() 130 | @columnElement.classList.add "draggable" 131 | for editable in @editables 132 | editable.removeAttribute "hidden" 133 | editable.offsetTop #hack that forces re-render 134 | 135 | trans.go editable, 136 | opened: true 137 | else 138 | if @handleHandler then handle.removeEventListener "mousedown", @handleHandler 139 | handle.style.visibility = "hidden" 140 | @draggie.disable() 141 | @columnElement.classList.remove "draggable" 142 | for editable in @editables 143 | trans.go editable, 144 | opened: false 145 | 146 | trans.listenOnce editable, trans.completeEventName, (e) -> 147 | e.setAttribute "hidden", "" 148 | , [editable] 149 | 150 | render: (columnElement, holderElement) -> 151 | if @flex then holderElement.classList.add "flex" 152 | else holderElement.classList.remove "flex" 153 | 154 | @columnElement = columnElement 155 | 156 | trans = tabbie.meta.byId "core-transition-center" 157 | for editable in @columnElement.querySelectorAll "html /deep/ .editable" 158 | if not @dialog and editable.classList.contains "settings" then continue 159 | trans.setup editable 160 | @editables.push editable 161 | 162 | spinner = columnElement.querySelector "html /deep/ paper-spinner" 163 | progress = columnElement.querySelector "html /deep/ paper-progress" 164 | 165 | try 166 | Object.defineProperty @, "loading", 167 | get: -> spinner.active 168 | set: (val) -> spinner.active = val 169 | 170 | timeout = false 171 | Object.defineProperty @, "refreshing", 172 | get: -> @_refreshing 173 | set: (val) -> 174 | @_refreshing = false 175 | if val 176 | progress.style.opacity = 1 177 | else 178 | if timeout then clearTimeout timeout 179 | timeout = setTimeout -> 180 | progress.style.opacity = 0 181 | , 400 182 | catch e 183 | console.warn(e) 184 | 185 | #Internally used for restoring/saving columns (don't touch) 186 | className: "" 187 | 188 | #Automatically generated based on thumb image (don't touch) 189 | color: "" 190 | 191 | #more internal shiz 192 | columnElement: null 193 | editables: [] 194 | draggie: null 195 | 196 | #Internally used to determine which properties to keep when saving 197 | syncedProperties:[ 198 | "cache", 199 | "config", 200 | "className", 201 | "id", 202 | "color", 203 | "width", 204 | "height", 205 | "name", 206 | "url", 207 | "baseUrl", 208 | "link", 209 | "thumb", 210 | "custom" 211 | ] 212 | 213 | #Column name 214 | name: "Empty column" 215 | 216 | #Column grid width (width * 25%) 217 | width: 1 218 | 219 | #Column grid height (height * 50%) 220 | height: 1 221 | 222 | #Configuration dialog ID 223 | dialog: null 224 | 225 | #Thumbnail image path 226 | thumb: "img/column-unknown.png" 227 | 228 | #Configurations trough dialogs etc get saved in here 229 | config: {} 230 | 231 | #Cache 232 | cache: [] 233 | 234 | loading: true 235 | refreshing: false 236 | 237 | #If set to true, this will cause the holder to be a flexbox 238 | flex: false 239 | 240 | #whether to hande column as a custom column or not 241 | custom: false 242 | 243 | toJSON: -> 244 | result = {} 245 | result[key] = @[key] for key of @ when @syncedProperties.indexOf(key) isnt -1 246 | result 247 | -------------------------------------------------------------------------------- /src/coffee/FeedColumn.coffee: -------------------------------------------------------------------------------- 1 | # FeedColumn makes it easy to make feed-based columns. 2 | # Summed up: it makes a request from a API endpoint that returns json, 3 | # It loops trough it's result, adds a element for each item in the loop, sets 'item' on the element, and its it to the holder. 4 | # Also automatically takes care of caching. 5 | 6 | class Columns.FeedColumn extends Columns.Column 7 | 8 | # The element name that will be inserted 9 | element: false 10 | 11 | # API endpoint 12 | url: false 13 | 14 | # Response type (json, xml) 15 | responseType: 'json' 16 | 17 | # Path inside returned JSON object that has the array we'll loop trough. 18 | # Example: data.children when returned obj from server is { data: { children: [] } } 19 | # Leave null for no path (i.e. when server directly returns array) 20 | dataPath: null 21 | 22 | # Same as dataPath, but for items in the array itself 23 | childPath: null 24 | 25 | baseUrl: false 26 | infiniteScroll: false 27 | page: 1 28 | 29 | draw: (data, holderElement) -> 30 | @loading = false 31 | 32 | if @flex then holderElement.classList.add "flex" 33 | else holderElement.classList.remove "flex" 34 | 35 | if not @element 36 | console.warn "Please define the 'element' property on your column class!" 37 | return 38 | 39 | if @dataPath then data = eval "data." + @dataPath 40 | 41 | if @responseType is 'xml' 42 | parser = new DOMParser 43 | xmlDoc = parser.parseFromString data, 'text/xml' 44 | items = xmlDoc.getElementsByTagName @xmlTag 45 | data = [] 46 | 47 | for item in items 48 | converted = {} 49 | nodes = item.childNodes 50 | for el in nodes 51 | converted[el.nodeName] = el.textContent 52 | data.push converted 53 | 54 | for child in data 55 | card = document.createElement @element 56 | if @childPath then child = eval "child." + @childPath 57 | card.item = child 58 | holderElement.appendChild card 59 | 60 | #needed for proper flex 61 | for num in [0..10] when @flex 62 | hack = document.createElement @element 63 | hack.className = "hack" 64 | holderElement.appendChild hack 65 | 66 | refresh: (columnElement, holderElement, adding) -> 67 | @refreshing = true 68 | 69 | if @infiniteScroll and adding 70 | @baseUrl = @url if not @baseUrl 71 | @url = @baseUrl.replace "{PAGENUM}", @page 72 | 73 | else if @page == "" then @url = @baseUrl.replace "{PAGENUM}", "" 74 | else if @baseUrl then @url = @baseUrl 75 | 76 | if @url.includes "{PAGENUM}" then @url = @url.replace "{PAGENUM}", "" 77 | 78 | if not @url 79 | console.warn "Please define the 'url' property on your column class!" 80 | return 81 | 82 | fetch(@url) 83 | .then (response) => 84 | if response.status is 200 85 | dataType = 'json' 86 | dataType = 'text' if @responseType is 'xml' 87 | Promise.resolve response[dataType]() 88 | else Promise.reject new Error response.statusText 89 | .then (data) => 90 | @refreshing = false 91 | @cache = data 92 | tabbie.sync @ 93 | holderElement.innerHTML = "" if not adding 94 | if @flex then hack.remove() for hack in holderElement.querySelectorAll ".hack" 95 | @draw @cache, holderElement 96 | .catch (error) => 97 | console.error error 98 | @refreshing = false 99 | @loading = false 100 | 101 | #no cached data to display? show error 102 | if not @cache or @cache.length is 0 then @error holderElement 103 | 104 | render: (columnElement, holderElement) -> 105 | super columnElement, holderElement 106 | 107 | if @infiniteScroll then holderElement.addEventListener "scroll", => 108 | if not @refreshing and holderElement.scrollTop + holderElement.clientHeight >= holderElement.scrollHeight - 100 109 | if typeof @page is 'number' then @page++ 110 | @refresh columnElement, holderElement, true 111 | 112 | if Object.keys(@cache).length 113 | @draw @cache, holderElement 114 | @refresh columnElement, holderElement 115 | -------------------------------------------------------------------------------- /src/column-chooser.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../bower_components/paper-button/paper-button.html"> 3 | <link rel="import" href="../bower_components/paper-shadow/paper-shadow.html"> 4 | <link rel="import" href="../bower_components/paper-ripple/paper-ripple.html"> 5 | <link rel="import" href="../bower_components/paper-dialog/paper-action-dialog.html"> 6 | <polymer-element name="column-chooser" attributes="columns packery"> 7 | <template> 8 | <style> 9 | * { 10 | box-sizing: border-box; 11 | } 12 | 13 | .column { 14 | text-align: center; 15 | width: 25%; 16 | min-width:170px; 17 | display:inline-block; 18 | position: relative; 19 | transition: 250ms background; 20 | /*background: #fff;*/ 21 | cursor: pointer; 22 | } 23 | 24 | .column .content { 25 | padding: 20px; 26 | } 27 | 28 | .column .content h3 { 29 | overflow: hidden; 30 | text-overflow: ellipsis; 31 | position: relative; 32 | white-space: nowrap; 33 | } 34 | 35 | .grid { 36 | height:100% !important; 37 | } 38 | 39 | .hack { 40 | height: 0; 41 | margin:0; 42 | } 43 | 44 | paper-icon-button.remove { 45 | position: absolute; 46 | right: 0; 47 | top: 0; 48 | z-index:1; 49 | } 50 | </style> 51 | <div class="grid"> 52 | <template repeat="{{column in columns}}"> 53 | <div class="column"> 54 | <div class="content"> 55 | <template if="{{column.custom}}"> 56 | <paper-icon-button on-click="{{attemptDelete}}" class="remove" icon="remove-circle"></paper-icon-button> 57 | </template> 58 | <paper-ripple fit></paper-ripple> 59 | <template if="{{column.thumb}}"> 60 | <img width="150" height="150" src="{{column.thumb}}"> 61 | </template> 62 | <h3>{{column.name}}</h3> 63 | </div> 64 | </div> 65 | </template> 66 | </div> 67 | 68 | <paper-action-dialog transition="core-transition-center" heading="Are you sure?" backdrop id="confirm"> 69 | <p>This will remove the column from your overview, you will have to add it again if you want it back.</p> 70 | 71 | <paper-button affirmative>No</paper-button> 72 | <paper-button affirmative yes autofocus on-click="{{deleteColumn}}">Yes</paper-button> 73 | </paper-action-dialog> 74 | 75 | </template> 76 | <script> 77 | Polymer({ 78 | attemptDelete: function(e) { 79 | this.columnToBeDeleted = e.target.templateInstance.model.column; 80 | this.shadowRoot.querySelector("#confirm").toggle() 81 | }, 82 | deleteColumn: function() { 83 | this.fire("delete-column", this.columnToBeDeleted) 84 | }, 85 | columnsChanged: function(changes) { 86 | console.log('columnsChanged', changes) 87 | var self = this; 88 | if(this.packery) { 89 | this.async(function() { 90 | self.packery.reloadItems(); 91 | self.packery._resetLayout(); 92 | self.packery.layoutItems(self.packery.items, true); 93 | }) 94 | } 95 | }, 96 | shown: function() { 97 | var self = this; 98 | if(!this.packery) this.packery = new Packery(this.shadowRoot.querySelector(".grid"), { 99 | itemSelector: ".column" 100 | }) 101 | this.packery.layout() 102 | this.packery.on("removeComplete", function() { 103 | self.packery.layout() 104 | }) 105 | }, 106 | packery: undefined, 107 | columns: [] 108 | }); 109 | </script> 110 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/apps/Apps.coffee: -------------------------------------------------------------------------------- 1 | class Columns.Apps extends Columns.Column 2 | name: "Apps" 3 | thumb: "img/column-apps.png" 4 | flex: true 5 | link: "chrome://apps" 6 | 7 | attemptAdd: (successCallback) => 8 | chrome.permissions.request 9 | permissions: ["management"], 10 | origins: ["chrome://favicon/*"] 11 | , (granted) => 12 | if granted 13 | if typeof successCallback is 'function' then successCallback() 14 | 15 | refresh: (columnElement, holderElement) -> 16 | holderElement.innerHTML = "" 17 | chrome.management.getAll (extensions) => 18 | for extension in extensions when extension.type.indexOf("app") isnt -1 and not extension.disabled 19 | console.log extension 20 | app = document.createElement "app-item" 21 | try 22 | app.name = extension.name 23 | app.icon = extension.icons[extension.icons.length-1].url 24 | app.id = extension.id 25 | catch e 26 | console.warn e 27 | 28 | app.addEventListener "click", -> chrome.management.launchApp this.id 29 | holderElement.appendChild app 30 | 31 | #needed for proper flex 32 | for num in [0..10] when @flex 33 | hack = document.createElement "app-item" 34 | hack.className = "hack" 35 | holderElement.appendChild hack 36 | 37 | render: (columnElement, holderElement) -> 38 | super columnElement, holderElement 39 | @refreshing = false 40 | @loading = false 41 | 42 | @refresh columnElement, holderElement 43 | 44 | tabbie.register "Apps" -------------------------------------------------------------------------------- /src/columns/behance/Behance.coffee: -------------------------------------------------------------------------------- 1 | class Columns.Behance extends Columns.FeedColumn 2 | name: "Behance" 3 | width: 2 4 | thumb: "img/column-behance.png" 5 | link: "https://www.behance.net/" 6 | 7 | element: "behance-item" 8 | url: "https://api.behance.net/v2/projects?page={PAGENUM}&api_key=IRZkzuavyQ8XBNihD290wtgt4AlwYo6X" 9 | 10 | dataPath: "projects" 11 | flex: true 12 | infiniteScroll: true 13 | 14 | tabbie.register "Behance" -------------------------------------------------------------------------------- /src/columns/behance/behance-item.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../../../bower_components/paper-ripple/paper-ripple.html"> 3 | <link rel="import" href="../../../bower_components/core-icon/core-icon.html"> 4 | <link rel="import" href="../../../bower_components/core-icons/communication-icons.html"> 5 | <link rel="import" href="../../../bower_components/core-icons/image-icons.html"> 6 | <link rel="import" href="../../time-ago.html"> 7 | 8 | <polymer-element name="behance-item" attributes="item"> 9 | <template> 10 | <style> 11 | :host { 12 | flex: 1 0 200px; 13 | font-size: 10px; 14 | color: black; 15 | display: inline-block; 16 | margin: 10px 0 0 10px; 17 | position: relative; 18 | overflow: hidden; 19 | box-sizing: border-box; 20 | } 21 | 22 | h1 { 23 | font-size: 14px; 24 | font-family: 'Roboto Slab', 'Roboto', sans-serif; 25 | } 26 | 27 | .img { 28 | width: 100%; 29 | transition: transform 250ms; 30 | } 31 | 32 | a { 33 | color: rgba(0, 0, 0, 0.87); 34 | text-decoration: none; 35 | } 36 | 37 | .overlay { 38 | position: absolute; 39 | opacity: 0; 40 | height: 100%; 41 | width: 100%; 42 | padding: 10px; 43 | background-color: rgba(255, 255, 255, .5); 44 | transition: opacity 250ms; 45 | z-index: 1; 46 | } 47 | 48 | paper-ripple { 49 | position: absolute; 50 | width: 100%; 51 | height: 100%; 52 | z-index: 2; 53 | } 54 | 55 | a:hover > .overlay { 56 | opacity: 1; 57 | } 58 | 59 | a:hover > .img { 60 | transform: scale(1.2) 61 | } 62 | 63 | .minigrid div { 64 | width: 30%; 65 | float: left; 66 | text-align: center; 67 | color: #484848; 68 | } 69 | 70 | .minigrid div span { 71 | 72 | } 73 | 74 | .minigrid div core-icon { 75 | width: 15px; 76 | height: 15px; 77 | } 78 | 79 | .author { 80 | position: absolute; 81 | bottom: 25px; 82 | } 83 | 84 | .author img { 85 | width: 25px; 86 | height: 25px; 87 | border-radius: 50%; 88 | float: left; 89 | margin-right: 10px; 90 | } 91 | 92 | .author span { 93 | display: inline-block; 94 | padding-top: 7px; 95 | } 96 | 97 | </style> 98 | 99 | <a target="_blank" href="{{item.url}}"> 100 | <paper-ripple></paper-ripple> 101 | <div class="overlay"> 102 | <h1>{{item.name}}</h1> 103 | 104 | <div class="minigrid"> 105 | <div> 106 | <core-icon icon="favorite"></core-icon> 107 | <span>{{item.stats.appreciations}}</span> 108 | </div> 109 | <div> 110 | <core-icon icon="communication:comment"></core-icon> 111 | <span>{{item.stats.comments}}</span> 112 | </div> 113 | <div> 114 | <core-icon icon="image:remove-red-eye"></core-icon> 115 | <span>{{item.stats.views}}</span> 116 | </div> 117 | </div> 118 | <div class="author"> 119 | <img src="{{item.owners[0].images['50']}}"> <span>{{item.owners[0].first_name}} {{item.owners[0].last_name}}</span> 120 | </div> 121 | </div> 122 | <img src="{{item.covers['404']}}" class="img"> 123 | </a> 124 | 125 | </template> 126 | <script> 127 | Polymer({ 128 | }); 129 | </script> 130 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/bookmarks/Bookmarks.coffee: -------------------------------------------------------------------------------- 1 | class Columns.Bookmarks extends Columns.Column 2 | name: "Bookmarks" 3 | thumb: "img/column-bookmarks.png" 4 | link: "chrome://bookmarks" 5 | 6 | attemptAdd: (successCallback) => 7 | chrome.permissions.request 8 | permissions: ["bookmarks"], 9 | origins: ["chrome://favicon/*"] 10 | , (granted) => 11 | if granted 12 | if typeof successCallback is 'function' then successCallback() 13 | 14 | refresh: (columnElement, holderElement) -> 15 | @tabs.innerHTML = "" 16 | recent = document.createElement "div" 17 | recent.classList.add "recent" 18 | @tabs.appendChild recent 19 | all = document.createElement "div" 20 | all.classList.add "all" 21 | @tabs.appendChild all 22 | 23 | chrome.bookmarks.getRecent 20, (tree) => 24 | tabbie.renderBookmarkTree recent, tree, 0 25 | 26 | chrome.bookmarks.getTree (tree) => 27 | tree = tree[0].children 28 | tabbie.renderBookmarkTree all, tree, 0 29 | 30 | render: (columnElement, holderElement) -> 31 | super columnElement, holderElement 32 | @refreshing = false 33 | @loading = false 34 | 35 | holderElement.innerHTML = "" 36 | @tabs = document.createElement "bookmark-tabs" 37 | holderElement.appendChild @tabs 38 | 39 | @refresh columnElement, holderElement 40 | 41 | tabbie.register "Bookmarks" -------------------------------------------------------------------------------- /src/columns/bookmarks/bookmark-tabs.html: -------------------------------------------------------------------------------- 1 | <polymer-element name="bookmark-tabs" noscript> 2 | <template> 3 | <style> 4 | :host { 5 | height: 100%; 6 | } 7 | 8 | ::content div { 9 | height: 100%; 10 | width: 100%; 11 | overflow-y: auto; 12 | } 13 | 14 | paper-tabs { 15 | background:#607d8b; 16 | color:#fff; 17 | } 18 | 19 | paper-tabs::shadow #selectionBar { 20 | background-color: #FFF; 21 | } 22 | 23 | paper-tabs paper-tab::shadow #ink { 24 | color: #FFF; 25 | } 26 | 27 | core-animated-pages { 28 | height: calc(100% - 48px); 29 | } 30 | </style> 31 | <paper-tabs selected="{{selected}}"> 32 | <paper-tab>RECENT</paper-tab> 33 | <paper-tab>ALL</paper-tab> 34 | </paper-tabs> 35 | <core-animated-pages selected="{{selected}}" transitions="slide-from-right"> 36 | <content></content> 37 | </core-animated-pages> 38 | </template> 39 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/closedtabs/ClosedTabs.coffee: -------------------------------------------------------------------------------- 1 | class Columns.ClosedTabs extends Columns.Column 2 | name: "Closed tabs" 3 | thumb: "img/column-closedtabs.png" 4 | link: "chrome://history" 5 | 6 | attemptAdd: (successCallback) => 7 | chrome.permissions.request 8 | permissions: ["sessions", "tabs"], 9 | origins: ["chrome://favicon/*"] 10 | , (granted) => 11 | if granted 12 | if typeof successCallback is 'function' then successCallback() 13 | 14 | refresh: (columnElement, holderElement) -> 15 | holderElement.innerHTML = "" 16 | chrome.sessions.getRecentlyClosed (sites) => 17 | for site in sites 18 | paper = document.createElement "recently-item" 19 | if site.hasOwnProperty("tab") 20 | paper.window = 0 21 | paper.url = site.tab.url 22 | paper.title = site.tab.title 23 | paper.sessId = site.tab.sessionId 24 | else 25 | paper.window = 1 26 | paper.tab_count = site.window.tabs.length 27 | paper.sessId = site.window.sessionId 28 | 29 | paper.addEventListener "click", -> 30 | chrome.sessions.restore this.sessId 31 | 32 | holderElement.appendChild paper 33 | 34 | render: (columnElement, holderElement) -> 35 | super columnElement, holderElement 36 | @refreshing = false 37 | @loading = false 38 | @refresh columnElement, holderElement 39 | 40 | tabbie.register "ClosedTabs" -------------------------------------------------------------------------------- /src/columns/codepen/Codepen.coffee: -------------------------------------------------------------------------------- 1 | class Columns.Codepen extends Columns.FeedColumn 2 | name: "Codepen" 3 | width: 2 4 | thumb: "img/column-codepen.png" 5 | link: "https://codepen.io" 6 | 7 | element: "codepen-item" 8 | url: "http://codepen.io/picks/feed/" 9 | responseType: "xml" 10 | xmlTag: "item" 11 | flex: true 12 | 13 | attemptAdd: (successCallback) -> 14 | chrome.permissions.request 15 | origins: ['http://codepen.io/'] 16 | , (granted) => 17 | if granted and typeof successCallback is 'function' then successCallback() 18 | 19 | tabbie.register "Codepen" 20 | -------------------------------------------------------------------------------- /src/columns/codepen/codepen-item.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../../../bower_components/paper-ripple/paper-ripple.html"> 3 | <link rel="import" href="../../../bower_components/core-icon/core-icon.html"> 4 | <link rel="import" href="../../../bower_components/core-icons/communication-icons.html"> 5 | <link rel="import" href="../../../bower_components/core-icons/image-icons.html"> 6 | <link rel="import" href="../../time-ago.html"> 7 | 8 | <polymer-element name="codepen-item" attributes="item"> 9 | <template> 10 | <style> 11 | :host { 12 | flex: 1 0 200px; 13 | font-size:10px; 14 | color:black; 15 | display:inline-block; 16 | margin: 10px 0 0 10px; 17 | position: relative; 18 | overflow:hidden; 19 | box-sizing:border-box; 20 | } 21 | 22 | h1 { 23 | font-size:14px; 24 | font-family: 'Roboto Slab', 'Roboto', sans-serif; 25 | } 26 | 27 | .img { 28 | width:100%; 29 | transition: transform 250ms; 30 | } 31 | 32 | a { 33 | color: rgba(0, 0, 0, 0.87); 34 | text-decoration: none; 35 | } 36 | 37 | .overlay { 38 | position: absolute; 39 | opacity:0; 40 | height: 100%; 41 | width:100%; 42 | padding:10px; 43 | background-color: rgba(255, 255, 255, .5); 44 | transition:opacity 250ms; 45 | z-index: 1; 46 | text-shadow:0 0 1px #fff; 47 | } 48 | 49 | paper-ripple { 50 | position: absolute; 51 | width: 100%; 52 | height:100%; 53 | z-index: 2; 54 | } 55 | 56 | a:hover > .overlay { 57 | opacity:1; 58 | } 59 | 60 | a:hover > .img { 61 | transform: scale(1.2) 62 | } 63 | 64 | .author { 65 | display:inline-block; 66 | } 67 | 68 | </style> 69 | 70 | <template if="{{item.title}}"> 71 | <a target="_blank" href="{{item.link}}"> 72 | <paper-ripple></paper-ripple> 73 | <div class="overlay"> 74 | <h1>{{item.title}}</h1> 75 | <div class="author">{{item['dc:creator']}}</div> 76 | </div> 77 | <img src="{{item.description | getSrc}}" class="img"> 78 | </a> 79 | </template> 80 | 81 | </template> 82 | <script> 83 | Polymer({ 84 | getSrc: function(description) { 85 | var elem = document.createElement("div"); 86 | elem.innerHTML = description; 87 | var images = elem.getElementsByTagName("img"); 88 | return images[0].src; 89 | } 90 | }); 91 | </script> 92 | </polymer-element> 93 | -------------------------------------------------------------------------------- /src/columns/customcolumn/CustomColumn.coffee: -------------------------------------------------------------------------------- 1 | #CustomColumn is part of the tabbie core. 2 | #blabla 3 | 4 | class Columns.CustomColumn extends Columns.FeedColumn 5 | element: "feedly-item" 6 | responseType: "json" 7 | page: "", 8 | infiniteScroll: true, 9 | 10 | draw: (data, holderElement) => 11 | if typeof data.length isnt 'number' 12 | @page = data.continuation 13 | data = data.items 14 | @cache = data 15 | super data, holderElement 16 | 17 | attemptAdd: (successCallback) => 18 | chrome.permissions.request 19 | origins: ["https://feedly.com/"] 20 | , (granted) => 21 | if granted 22 | if typeof successCallback is "function" then successCallback(); -------------------------------------------------------------------------------- /src/columns/customcolumn/feedly-item.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../../time-ago.html"> 3 | 4 | <polymer-element name="feedly-item" attributes="item"> 5 | <template> 6 | <style> 7 | :host { 8 | /*padding:10px;*/ 9 | padding-right:10px; 10 | position: relative; 11 | } 12 | 13 | .media { 14 | position:relative; 15 | } 16 | 17 | h1 { 18 | font-size:16px; 19 | font-family: 'Roboto Slab', 'Roboto', sans-serif; 20 | font-weight:normal; 21 | } 22 | 23 | .media h1 { 24 | color:#fff; 25 | position:absolute; 26 | bottom:0; 27 | background:rgba(0,0,0,.5); 28 | width:100%; 29 | left:0; 30 | padding:5px 10px; 31 | margin:0; 32 | box-sizing: border-box; 33 | } 34 | 35 | .media { 36 | min-height:200px; 37 | width:100%; 38 | position: relative; 39 | background:#eee; 40 | } 41 | 42 | .media .fake-img { 43 | display:none; 44 | } 45 | 46 | .media .img { 47 | opacity:0; 48 | transition:opacity 500ms; 49 | background: no-repeat center center; 50 | background-size: cover; 51 | } 52 | 53 | paper-ripple { 54 | z-index:1; 55 | } 56 | 57 | .media h1 a { 58 | color:#fff; 59 | } 60 | 61 | a { 62 | color:#2c2c2c; 63 | text-decoration: none; 64 | } 65 | 66 | .info { 67 | color: #616161; 68 | font-size:10px; 69 | padding: 10px; 70 | font-weight: lighter; 71 | border-bottom:1px solid #d2d2d2; 72 | } 73 | 74 | footer { 75 | margin-top:10px; 76 | } 77 | 78 | .info p { 79 | margin:0; 80 | } 81 | 82 | footer p { 83 | width:50%; 84 | white-space: nowrap; 85 | text-overflow: ellipsis; 86 | overflow:hidden; 87 | } 88 | 89 | footer p:last-child { 90 | text-align:right; 91 | } 92 | </style> 93 | 94 | <a fit target="_blank" href="{{item.alternate[0].href}}"> 95 | <paper-ripple fit></paper-ripple> 96 | </a> 97 | 98 | <template if="{{item.visual.url | isValidUrl}}"> 99 | 100 | <div class="media"> 101 | <img on-load="{{imageLoaded}}" src="{{item.visual.url}}" class="fake-img"> 102 | <div fit class="img"></div> 103 | <h1><a target="_blank" href="{{item.alternate.href}}">{{item.title}}</a></h1> 104 | </div> 105 | </template> 106 | 107 | <div class="info"> 108 | 109 | <template if="{{item.visual.url | isInvalidUrl}}"> 110 | <h1>{{item.title}}</h1> 111 | </template> 112 | 113 | <p>{{item.article | filterHtml | summarize}}</p> 114 | 115 | <footer layout horizontal> 116 | <p> 117 | <template if="{{item.author}}"> 118 | by {{item.author}} 119 | </template> 120 | </p> 121 | <p> 122 | <time-ago datetime="{{item.published}}" epoch="false"></time-ago> 123 | </p> 124 | </footer> 125 | 126 | </div> 127 | 128 | </template> 129 | <script> 130 | Polymer({ 131 | isValidUrl: function(url) { 132 | return typeof url === "string" && url.substring(0, 4) === "http" 133 | }, 134 | 135 | isInvalidUrl: function(url) { 136 | //not very pretty, but polymer doesn't have an else statement 137 | return !this.isValidUrl(url); 138 | }, 139 | 140 | summarize: function(content) { 141 | if(typeof content !== 'string') return content; 142 | 143 | var words = content.split(" ") 144 | if(words.length > 50) { 145 | //more than 50 words, let's make it a bit shorter 146 | words = words.filter(function(word, i) { 147 | return i < 50; 148 | }) 149 | content = words.join(' ') + '...'; 150 | } 151 | 152 | return content; 153 | }, 154 | 155 | filterHtml: function(string) { 156 | //if it's not a string, we don't want it 157 | if(typeof string !== 'string') return string; 158 | 159 | //pretty mediocre way, but it's the only way to do this without *shivers* executing the html. 160 | string = _.unescape(string) 161 | 162 | //don't worry. this is just to make the summaries with html look a bit more nice. 163 | //we're never actually inserting any html into the dom. because that's dangerous af. chrome runtime access, son. i don't think so. 164 | return string.replace(/<[^>]*>?/g, ''); 165 | }, 166 | 167 | imageLoaded: function(e) { 168 | var path = e.target.getAttribute("src"); 169 | 170 | var img = this.shadowRoot.querySelector(".media .img") 171 | img.style.backgroundImage = "url("+path+")"; 172 | img.style.opacity = 1; 173 | 174 | e.target.remove(); //saves memory 175 | }, 176 | 177 | attached:function() { 178 | if('summary' in this.item) this.item.article = this.item.summary.content; 179 | else if('content' in this.item) this.item.article = this.item.content.content; 180 | } 181 | }); 182 | </script> 183 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/designernews/DesignerNews.coffee: -------------------------------------------------------------------------------- 1 | class Columns.DesignerNews extends Columns.FeedColumn 2 | name: "DesignerNews" 3 | width: 1 4 | thumb: "img/column-designernews.png" 5 | link: "https://www.designernews.co/" 6 | 7 | url: "https://api.designernews.co/api/v2/stories/" 8 | element: "dn-item" 9 | dataPath: "stories" 10 | 11 | tabbie.register "DesignerNews" -------------------------------------------------------------------------------- /src/columns/dribbble/Dribbble.coffee: -------------------------------------------------------------------------------- 1 | class Columns.Dribbble extends Columns.FeedColumn 2 | name: "Dribbble" 3 | width: 2 4 | thumb: "img/column-dribble.png" 5 | link: "https://dribbble.com" 6 | 7 | element: "dribbble-item" 8 | url: "https://api.dribbble.com/v1/shots?page={PAGENUM}&access_token=74f8fb9f92c1f79c4bc3662f708dfdce7cd05c3fc67ac84ae68ff47568b71a1f" 9 | 10 | infiniteScroll: true 11 | flex: true 12 | 13 | tabbie.register "Dribbble" -------------------------------------------------------------------------------- /src/columns/dribbble/dribbble-item.html: -------------------------------------------------------------------------------- 1 | 2 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 3 | <link rel="import" href="../../../bower_components/paper-ripple/paper-ripple.html"> 4 | <link rel="import" href="../../../bower_components/core-icon/core-icon.html"> 5 | <link rel="import" href="../../../bower_components/core-icons/communication-icons.html"> 6 | <link rel="import" href="../../../bower_components/core-icons/image-icons.html"> 7 | <link rel="import" href="../../time-ago.html"> 8 | 9 | <polymer-element name="dribbble-item" attributes="item"> 10 | <template> 11 | <style> 12 | :host { 13 | flex: 1 0 200px; 14 | font-size:10px; 15 | color:black; 16 | display:inline-block; 17 | margin: 10px 0 0 10px; 18 | position: relative; 19 | overflow:hidden; 20 | box-sizing:border-box; 21 | } 22 | 23 | h1 { 24 | font-size:14px; 25 | font-family: 'Roboto Slab', 'Roboto', sans-serif; 26 | } 27 | 28 | .img { 29 | width:100%; 30 | transition: transform 250ms; 31 | } 32 | 33 | a { 34 | color: rgba(0, 0, 0, 0.87); 35 | text-decoration: none; 36 | } 37 | 38 | .overlay { 39 | position: absolute; 40 | opacity:0; 41 | height: 100%; 42 | width:100%; 43 | padding:10px; 44 | background-color: rgba(255, 255, 255, .5); 45 | transition:opacity 250ms; 46 | z-index: 1; 47 | text-shadow:0 0 1px #fff; 48 | } 49 | 50 | paper-ripple { 51 | position: absolute; 52 | width: 100%; 53 | height:100%; 54 | z-index: 2; 55 | } 56 | 57 | a:hover > .overlay { 58 | opacity:1; 59 | } 60 | 61 | a:hover > .img { 62 | transform: scale(1.2) 63 | } 64 | 65 | .minigrid div { 66 | width:30%; 67 | float:left; 68 | text-align: center; 69 | color: #484848; 70 | } 71 | 72 | .minigrid div span { 73 | 74 | } 75 | 76 | .minigrid div core-icon { 77 | width:15px; 78 | height:15px; 79 | } 80 | 81 | .author { 82 | position: absolute; 83 | bottom: 25px; 84 | } 85 | 86 | .author img { 87 | width:25px; 88 | height:25px; 89 | border-radius:50%; 90 | float:left; 91 | margin-right:10px; 92 | } 93 | 94 | .author span { 95 | display:inline-block; 96 | padding-top:7px; 97 | } 98 | 99 | </style> 100 | 101 | <template if="{{item.image}}"> 102 | <a target="_blank" href="{{item.html_url}}"> 103 | <paper-ripple></paper-ripple> 104 | <div class="overlay"> 105 | <h1>{{item.title}}</h1> 106 | <div class="minigrid"> 107 | <div> 108 | <core-icon icon="favorite"></core-icon> 109 | <span>{{item.likes_count}}</span> 110 | </div> 111 | <div> 112 | <core-icon icon="communication:comment"></core-icon> 113 | <span>{{item.comments_count}}</span> 114 | </div> 115 | <div> 116 | <core-icon icon="image:remove-red-eye"></core-icon> 117 | <span>{{item.views_count}}</span> 118 | </div> 119 | </div> 120 | <div class="author"> 121 | <img src="{{item.user.avatar_url}}"> <span>{{item.user.name}}</span> 122 | </div> 123 | </div> 124 | <img src="{{item.image}}" class="img"> 125 | </a> 126 | </template> 127 | 128 | </template> 129 | <script> 130 | Polymer({ 131 | attached: function() { 132 | if(this.item && this.item.images) { 133 | if(this.item.images.hidpi) this.item.image = this.item.images.hidpi; 134 | else this.item.image = this.item.images.normal; 135 | } 136 | } 137 | }); 138 | </script> 139 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/github/GitHub.coffee: -------------------------------------------------------------------------------- 1 | class Columns.GitHub extends Columns.FeedColumn 2 | name: "GitHub" 3 | width: 1 4 | thumb: "img/column-github.png" 5 | link: "https://github.com" 6 | 7 | element: "github-item" 8 | dataPath: "items" 9 | dialog: "github-dialog" 10 | 11 | refresh: (columnElement, holderElement) -> 12 | if typeof @config.period is "undefined" then @config.period = 1 13 | 14 | switch @config.period 15 | when 0 then period = "month" 16 | when 1 then period = "week" 17 | when 2 then period = "day" 18 | 19 | if typeof @config.language is "undefined" then @config.language = 0 20 | 21 | switch @config.language 22 | when 0 then language = "" 23 | when 1 then language = "+language:CSS" 24 | when 2 then language = "+language:HTML" 25 | when 3 then language = "+language:Java" 26 | when 4 then language = "+language:JavaScript" 27 | when 5 then language = "+language:PHP" 28 | when 6 then language = "+language:Python" 29 | when 7 then language = "+language:Ruby" 30 | 31 | 32 | date = new moment 33 | date.subtract(1, period) 34 | @url = "https://api.github.com/search/repositories?q=created:>="+date.format("YYYY-MM-DD")+language+"&sort=stars&order=desc" 35 | super columnElement, holderElement 36 | 37 | tabbie.register "GitHub" -------------------------------------------------------------------------------- /src/columns/github/github-dialog.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link href="../../../bower_components/core-menu/core-menu.html" rel="import"> 3 | <link href="../../../bower_components/paper-dropdown/paper-dropdown.html" rel="import"> 4 | <link href="../../../bower_components/paper-item/paper-item.html" rel="import"> 5 | <link href="../../../bower_components/paper-dropdown-menu/paper-dropdown-menu.html" rel="import"> 6 | 7 | <polymer-element name="github-dialog" attributes="config"> 8 | <template> 9 | <tabbie-dialog style="width:400px; height: 600px;"> 10 | <div flex> 11 | <div style="padding-top: 20px; font-weight: bold;">Show language</div> 12 | <paper-dropdown-menu label="???" style="margin: 0"> 13 | <paper-dropdown class="dropdown"> 14 | <core-menu class="menu" selected="{{config.language}}"> 15 | <paper-item>All</paper-item> 16 | <paper-item>CSS</paper-item> 17 | <paper-item>HTML</paper-item> 18 | <paper-item>Java</paper-item> 19 | <paper-item>JavaScript</paper-item> 20 | <paper-item>PHP</paper-item> 21 | <paper-item>Python</paper-item> 22 | <paper-item>Ruby</paper-item> 23 | </core-menu> 24 | </paper-dropdown> 25 | </paper-dropdown-menu> 26 | <div style="padding-top: 30px; font-weight: bold">Show most popular this</div> 27 | <paper-dropdown-menu label="???" style="margin:0"> 28 | <paper-dropdown class="dropdown"> 29 | <core-menu class="menu" selected="{{config.period}}"> 30 | <paper-item>Month</paper-item> 31 | <paper-item>Week</paper-item> 32 | <paper-item>Day</paper-item> 33 | </core-menu> 34 | </paper-dropdown> 35 | </paper-dropdown-menu> 36 | </div> 37 | </tabbie-dialog> 38 | </template> 39 | <script> 40 | Polymer({}) 41 | </script> 42 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/github/github-item.html: -------------------------------------------------------------------------------- 1 | <!-- GH-ITEM --> 2 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 3 | <link rel="import" href="../../../bower_components/core-icon/core-icon.html"> 4 | <link rel="import" href="../../time-ago.html"> 5 | 6 | <polymer-element name="github-item" attributes="item"> 7 | <template> 8 | <link rel="stylesheet" href="../../../dist/css/feeditem.css"> 9 | <style> 10 | .spacing { 11 | min-width:60px; 12 | display:inline-block; 13 | } 14 | </style> 15 | <div> 16 | <h1><a href="{{item.html_url}}" target="_blank" >{{item.full_name}}</a></h1> 17 | <p>{{item.description}}</p> 18 | <p> 19 | <div class="spacing"> 20 | <core-icon icon="star"></core-icon> {{item.stargazers_count}} 21 | </div> 22 | <div class="spacing" style="min-width: 120px"> 23 | <core-icon icon="today"></core-icon> <time-ago datetime="{{item.created_at}}" epoch="false"></time-ago> 24 | </div> 25 | <template if="{{item.language}}"> 26 | <div class="spacing" style="min-width:100px;"> 27 | <core-icon icon="language"></core-icon> {{item.language}} 28 | </div> 29 | </template> 30 | </p> 31 | </div> 32 | </template> 33 | <script> 34 | Polymer({}); 35 | </script> 36 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/gmail/Gmail.coffee: -------------------------------------------------------------------------------- 1 | ` 2 | //util functions kindly stolen from https://github.com/ebidel/polymer-gmail/ 3 | 4 | function getValueForHeaderField(headers, field) { 5 | for (var i = 0, header; header = headers[i]; ++i) { 6 | if (header.name == field || header.name == field.toLowerCase()) { 7 | return header.value; 8 | } 9 | } 10 | return null; 11 | } 12 | 13 | function fixUpMessages(resp) { 14 | var messages = resp.result.messages; 15 | 16 | for (var j = 0, m; m = messages[j]; ++j) { 17 | var headers = m.payload.headers; 18 | var keep = ['subject', 'snippet', 'id', 'threadId'] 19 | message = {} 20 | keep.forEach(function(key) { 21 | message[key] = m[key] 22 | }) 23 | 24 | // Example: Thu Sep 25 2014 14:43:18 GMT-0700 (PDT) -> Sept 25. 25 | var date = new Date(getValueForHeaderField(headers, 'Date')); 26 | message.date = date.toDateString().split(' ').slice(1, 3).join(' '); 27 | message.to = getValueForHeaderField(headers, 'To'); 28 | message.subject = getValueForHeaderField(headers, 'Subject'); 29 | 30 | var fromHeaders = getValueForHeaderField(headers, 'From'); 31 | var fromHeaderMatches = fromHeaders.match(new RegExp(/"?(.*?)"?\s?<(.*)>/)); 32 | 33 | message.from = {}; 34 | 35 | // Use name if one was found. Otherwise, use email address. 36 | if (fromHeaderMatches) { 37 | // If no a name, use email address for displayName. 38 | message.from.name = fromHeaderMatches[1].length ? fromHeaderMatches[1] : fromHeaderMatches[2]; 39 | message.from.email = fromHeaderMatches[2]; 40 | } else { 41 | message.from.name = fromHeaders.split('@')[0]; 42 | message.from.email = fromHeaders; 43 | } 44 | message.from.name = message.from.name.split('@')[0]; // Ensure email is split. 45 | 46 | message.unread = m.labelIds.indexOf("UNREAD") != -1; 47 | message.starred = m.labelIds.indexOf("STARRED") != -1; 48 | 49 | messages[j] = message 50 | } 51 | 52 | return messages; 53 | } 54 | ` 55 | 56 | class Columns.Gmail extends Columns.Column 57 | name: "Gmail" 58 | thumb: "img/column-gmail.png" 59 | dialog: "gmail-dialog" 60 | link: "https://mail.google.com/" 61 | 62 | holderEl: undefined 63 | columnEl: undefined 64 | 65 | logOut: => 66 | #clear content, show spinner 67 | @holderEl.innerHTML = "" 68 | @loading = true 69 | delete @config.user 70 | @cache = [] 71 | tabbie.sync @ 72 | 73 | chrome.identity.getAuthToken 74 | interactive: false 75 | , (token) => 76 | if !chrome.runtime.lastError 77 | #step 1, remove token from local storage 78 | chrome.identity.removeCachedAuthToken 79 | token: token 80 | , => 81 | #step 2, revoke token @ google 82 | fetch("https://accounts.google.com/o/oauth2/revoke?token=" + token) 83 | .catch => 84 | #ok, so because we don't have permissions this we can't complete the request, but this doesn't matter, because the token is revoked eitherway. 85 | #wait 1 sec because else weird things start to happen (token not actually being revoked / getAuthToken returning a new token) 86 | setTimeout => 87 | @loading = false 88 | @refresh @columnEl, @holderEl 89 | , 1000 90 | 91 | draw: (data, holderElement) => 92 | @loading = false 93 | @refreshing = false 94 | 95 | holderElement.innerHTML = "" 96 | 97 | for item in data 98 | child = document.createElement "gmail-item" 99 | child.item = item 100 | holderElement.appendChild child 101 | 102 | render: (columnElement, holderElement) -> 103 | super columnElement, holderElement 104 | 105 | @columnEl = columnElement 106 | @holderEl = holderElement 107 | 108 | if Object.keys(@cache).length 109 | @draw @cache, holderElement 110 | @refresh columnElement, holderElement 111 | 112 | gapiLoaded: false 113 | errored: false 114 | 115 | refresh: (columnElement, holderElement) -> 116 | if not @config.colors then @config.colors = {} 117 | 118 | @refreshing = true 119 | gapiEl = document.createElement "google-client-api" 120 | columnElement.appendChild gapiEl 121 | 122 | setTimeout => 123 | #google-client-api doesn't have a event for errors ;_; so just wait 5 secs and see if gapi is available 124 | if not @gapiLoaded 125 | @errored = true 126 | @refreshing = false 127 | @loading = false 128 | @error holderElement 129 | , 5000 130 | 131 | gapiEl.addEventListener "api-load", => 132 | @gapiLoaded = true 133 | console.info 'gapi loaded' 134 | chrome.identity.getAuthToken 135 | interactive: false 136 | , (token) => 137 | if !chrome.runtime.lastError 138 | gapi.auth.setToken 139 | access_token: token, 140 | duration: "52000", 141 | state: "https://www.googleapis.com/auth/gmail.modify" 142 | 143 | console.log "Auth token data", gapi.auth.getToken() 144 | 145 | gapi.client.load "gmail", "v1", => 146 | if not @config.user 147 | gapi.client.load "plus", "v1", => 148 | console.info "gplus loaded" 149 | batch = gapi.client.newBatch() 150 | batch.add gapi.client.plus.people.get 151 | userId: 'me' 152 | batch.add gapi.client.gmail.users.getProfile 153 | userId: 'me' 154 | batch.then (resp) => 155 | @config.user = {} 156 | @config.user[key] = value for key, value of item.result for k, item of resp.result 157 | console.info "user", @config.user 158 | console.info "gmail loaded" 159 | gmail = gapi.client.gmail.users 160 | gmail.threads.list 161 | userId: 'me', 162 | q: 'in:inbox' 163 | .then (resp) => 164 | batch = gapi.client.newBatch() 165 | 166 | if not resp.result.threads 167 | @draw [], holderElement 168 | return 169 | 170 | resp.result.threads.forEach (thread) => 171 | req = gmail.threads.get 172 | userId: 'me', 173 | id: thread.id 174 | batch.add req 175 | 176 | batch.then (resp) => 177 | messages = [] 178 | for key, item of resp.result 179 | fixed = fixUpMessages item 180 | #grab first, add amount of messages in thread to message 181 | message = fixed[0] 182 | message.amount = fixed.length 183 | #check if we already generated a color for this recipient, if not, generate it & save it 184 | if not @config.colors[message.from.email] then message.color = @config.colors[message.from.email] = Please.make_color()[0] 185 | else message.color = @config.colors[message.from.email] 186 | messages.push message 187 | #batch returns threads in a random order, so resort them based on their hex id's 188 | messages = messages.sort (a, b) -> 189 | ax = parseInt a.id, 16 190 | bx = parseInt b.id, 16 191 | 192 | if ax > bx then return -1 193 | else return 1 194 | @cache = messages 195 | tabbie.sync @ 196 | @draw messages, holderElement 197 | else 198 | @loading = false 199 | @refreshing = false 200 | auth = document.createElement "gmail-auth" 201 | auth.addEventListener "sign-in", => 202 | @render columnElement, holderElement 203 | holderElement.innerHTML = "" 204 | holderElement.appendChild auth 205 | 206 | tabbie.register "Gmail" -------------------------------------------------------------------------------- /src/columns/gmail/gmail-auth.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/paper-button/paper-button.html"> 2 | <link rel="import" href="../../../bower_components/core-icon/core-icon.html"> 3 | <link rel="import" href="../../../bower_components/google-signin/google-icons.html"> 4 | <link rel="import" href="../../../bower_components/google-apis/google-apis.html"> 5 | 6 | <polymer-element name="gmail-auth" attributes="item"> 7 | <template> 8 | <style> 9 | div { 10 | display:flex; 11 | width:100%; 12 | height:100%; 13 | } 14 | 15 | div paper-button { 16 | margin:auto; 17 | background:#DC4434; 18 | color:#fff; 19 | } 20 | 21 | div paper-button core-icon { 22 | margin-right:10px; 23 | } 24 | </style> 25 | <div> 26 | <paper-button on-click="{{signIn}}" raised><core-icon icon="google:google"></core-icon> Sign in with Google</paper-button> 27 | </div> 28 | </template> 29 | <script> 30 | Polymer({ 31 | signIn: function() { 32 | var self = this; 33 | chrome.identity.getAuthToken({ 34 | interactive:true 35 | }, function(token) { 36 | self.fire("sign-in", token) 37 | console.log(self) 38 | }); 39 | } 40 | }) 41 | </script> 42 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/gmail/gmail-dialog.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link href="../../../bower_components/core-menu/core-menu.html" rel="import"> 3 | <link href="../../../bower_components/paper-dropdown/paper-dropdown.html" rel="import"> 4 | <link href="../../../bower_components/paper-item/paper-item.html" rel="import"> 5 | <link href="../../../bower_components/paper-dropdown-menu/paper-dropdown-menu.html" rel="import"> 6 | 7 | <polymer-element name="gmail-dialog" attributes="config"> 8 | <template> 9 | <tabbie-dialog style="width:300px;"> 10 | <style> 11 | paper-button { 12 | color:#F44336; 13 | float:left 14 | } 15 | img { 16 | float:left; 17 | width:40px; 18 | height:40px; 19 | border-radius: 50%; 20 | margin-right:10px; 21 | } 22 | 23 | .clear { 24 | clear:both; 25 | margin-bottom:20px; 26 | } 27 | 28 | .email { 29 | color:#616161; 30 | } 31 | 32 | .user { 33 | font-size:13px; 34 | } 35 | </style> 36 | <template if="{{config.user}}"> 37 | <div class="user"> 38 | <p>You are logged in as</p> 39 | <img src="{{config.user.image.url}}"> 40 | {{config.user.displayName}}<br> 41 | <span class="email">{{config.user.emailAddress}}</span> 42 | <div class="clear"></div> 43 | </div> 44 | </template> 45 | <template if="{{!config.user}}"> 46 | <div class="user"> 47 | <p>You are logged in as</p> 48 | <img src="../../../img/column-unknown.png"> 49 | Not logged in<br> 50 | <div class="clear"></div> 51 | </div> 52 | </template> 53 | <paper-button on-click="{{logOut}}" disabled?="{{!config.user}}">Log out</paper-button> 54 | </tabbie-dialog> 55 | </template> 56 | <script> 57 | Polymer({ 58 | logOut: function(e) { 59 | e.currentTarget.parentElement.toggle() 60 | this.column.logOut() 61 | } 62 | }) 63 | </script> 64 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/gmail/gmail-item.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../../../bower_components/paper-ripple/paper-ripple.html"> 3 | <link rel="import" href="../../time-ago.html"> 4 | 5 | <polymer-element name="gmail-item" attributes="item" noscript> 6 | <template> 7 | <style> 8 | :host { 9 | position: relative; 10 | width:100%; 11 | } 12 | .avatar { 13 | border-radius: 50%; 14 | background: gray; 15 | color: #fff; 16 | width: 30px; 17 | height: 30px; 18 | display: inline-block; 19 | text-align: center; 20 | padding-top: 4px; 21 | position: absolute; 22 | top: 12px; 23 | left: 21px; 24 | text-transform: uppercase; 25 | } 26 | * { 27 | box-sizing: border-box; 28 | text-decoration: none !important; 29 | } 30 | paper-ripple { 31 | position: absolute; 32 | top:0; 33 | left:0; 34 | width:100%; 35 | height:100%; 36 | } 37 | a { 38 | display: block; 39 | padding: 10px 10px 10px 75px; 40 | border-bottom: 1px solid #E0E0E0; 41 | color:#000; 42 | } 43 | a.unread h1, a.unread h2 { 44 | font-weight: bold; 45 | } 46 | h1 { 47 | font-size:16px; 48 | margin:0; 49 | font-weight:normal; 50 | } 51 | h1 .amount { 52 | color:#616161; 53 | font-weight: normal; 54 | } 55 | h2 { 56 | font-size:12px; 57 | margin:0; 58 | font-weight:normal; 59 | } 60 | p { 61 | font-size:12px; 62 | color:#616161; 63 | margin: 6px 0 0; 64 | } 65 | 66 | p.date { 67 | margin:0 0 0; 68 | float:right; 69 | } 70 | </style> 71 | 72 | <div class="avatar" style="background-color: {{item.color}}"> 73 | {{item.from.name[0]}} 74 | </div> 75 | <a target="_blank" href="https://mail.google.com/mail/u/0/#inbox/{{item.threadId}}" class="{{item.unread ? 'unread' : ''}}"> 76 | <paper-ripple></paper-ripple> 77 | <p class="date">{{item.date}}</p> 78 | <h1> 79 | {{item.from.name}} 80 | <template if="{{item.amount > 1}}"> 81 | <span class="amount">({{item.amount}})</span> 82 | </template> 83 | </h1> 84 | <h2>{{item.subject}}</h2> 85 | <p> 86 | {{item.snippet}} 87 | </p> 88 | </a> 89 | </template> 90 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/hackernews/HackerNews.coffee: -------------------------------------------------------------------------------- 1 | class Columns.HackerNews extends Columns.FeedColumn 2 | name: "HackerNews" 3 | thumb: "img/column-hackernews.png" 4 | url: "https://api.pnd.gs/v1/sources/hackerNews/popular" 5 | element: "hn-item" 6 | link: "https://news.ycombinator.com/" 7 | 8 | tabbie.register "HackerNews" -------------------------------------------------------------------------------- /src/columns/hackernews/hn-item.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../../time-ago.html"> 3 | 4 | <polymer-element name="hn-item" attributes="item"> 5 | <template> 6 | <link rel="stylesheet" href="../../../dist/css/feeditem.css"> 7 | <div> 8 | <h1><a href="{{item.source.sourceUrl}}" target="_blank" >{{item.title}} <span class="domain">{{item.hostname}}</span> </a></h1> 9 | <p> 10 | submitted by <a target="_blank" href="{{item.source.authorUrl}}">{{item.source.username}}</a> <time-ago datetime="{{item.source.createdAt}}" epoch="false"></time-ago> 11 | </p> 12 | <p> 13 | <a target="_blank" href="{{item.source.sourceUrl}}">{{item.source.commentsCount == null ? 0 : item.source.commentsCount}} comments</a>, {{item.source.likesCount}} points 14 | </p> 15 | </div> 16 | </template> 17 | <script> 18 | Polymer({ 19 | attached: function() { 20 | var match = this.item.url.target.match(/:\/\/(www[0-9]?\.)?(.[^/:]+)/i); 21 | if (match != null && match.length > 2 && typeof match[2] === 'string' && match[2].length > 0) this.item.hostname = match[2] 22 | } 23 | }); 24 | </script> 25 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/lobsters/Lobsters.coffee: -------------------------------------------------------------------------------- 1 | class Columns.Lobsters extends Columns.FeedColumn 2 | name: "Lobste.rs" 3 | width: 1 4 | thumb: "img/column-lobsters.png" 5 | link: "https://lobste.rs/" 6 | 7 | url: "https://lobste.rs/hottest.json" 8 | element: "lobsters-item" 9 | dialog: "lobsters-dialog" 10 | 11 | refresh: (holderEl, columnEl) => 12 | if not @config.listing then @config.listing = 0 13 | 14 | switch @config.listing 15 | when 0 then listing = "hottest" 16 | when 1 then listing = "newest" 17 | 18 | @url = "https://lobste.rs/"+listing+".json" 19 | super holderEl, columnEl 20 | 21 | tabbie.register "Lobsters" -------------------------------------------------------------------------------- /src/columns/lobsters/lobsters-dialog.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link href="../../../bower_components/core-menu/core-menu.html" rel="import"> 3 | <link href="../../../bower_components/paper-dropdown/paper-dropdown.html" rel="import"> 4 | <link href="../../../bower_components/paper-item/paper-item.html" rel="import"> 5 | <link href="../../../bower_components/paper-dropdown-menu/paper-dropdown-menu.html" rel="import"> 6 | 7 | <polymer-element name="lobsters-dialog" attributes="config"> 8 | <template> 9 | <tabbie-dialog style="width:400px; height: 223px;"> 10 | <paper-dropdown-menu label="Listing" style="width:100%; margin-bottom:20px;"> 11 | <paper-dropdown class="dropdown"> 12 | <core-menu class="menu" selected="{{config.listing}}"> 13 | <paper-item>Hot</paper-item> 14 | <paper-item>New</paper-item> 15 | </core-menu> 16 | </paper-dropdown> 17 | </paper-dropdown-menu> 18 | </tabbie-dialog> 19 | </template> 20 | <script> 21 | Polymer({}) 22 | </script> 23 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/lobsters/lobsters-item.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../../time-ago.html"> 3 | 4 | <polymer-element name="lobsters-item" attributes="item"> 5 | <template> 6 | <link rel="stylesheet" href="../../../dist/css/feeditem.css"> 7 | <style> 8 | .tag { 9 | background:#FFF59D; 10 | border-radius: 3px; 11 | font-size: 12px; 12 | font-family: Roboto, sans-serif; 13 | padding: 3px 6px; 14 | } 15 | </style> 16 | <div> 17 | <h1> 18 | <a href="{{item.url}}" target="_blank" >{{item.title}} <span class="domain">{{item.hostname}}</span></a> 19 | </h1> 20 | <template repeat="{{item.tags}}"> 21 | <a class="tag" href="https://lobste.rs/t/{{}}">{{}}</a> 22 | </template> 23 | <p> 24 | submitted by <a target="_blank" href="https://lobste.rs/u/{{item.submitter_user.username}}">{{item.submitter_user.username}}</a> <time-ago datetime="{{item.created_at}}" epoch="false"></time-ago> 25 | </p> 26 | <p> 27 | <a target="_blank" href="{{item.comments_url}}">{{item.comment_count}} comments</a>, {{item.score}} points 28 | </p> 29 | </div> 30 | </template> 31 | <script> 32 | Polymer({ 33 | attached: function() { 34 | var match = this.item.url.match(/:\/\/(www[0-9]?\.)?(.[^/:]+)/i); 35 | if (match != null && match.length > 2 && typeof match[2] === 'string' && match[2].length > 0) this.item.hostname = match[2] 36 | } 37 | }); 38 | </script> 39 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/producthunt/ProductHunt.coffee: -------------------------------------------------------------------------------- 1 | class Columns.ProductHunt extends Columns.FeedColumn 2 | name: "ProductHunt" 3 | thumb: "img/column-producthunt.png" 4 | element: "ph-item" 5 | dataPath: "posts" 6 | link: "https://www.producthunt.com" 7 | dialog: "ph-dialog" 8 | width: 1 9 | 10 | attemptAdd: (successCallback) -> 11 | chrome.permissions.request 12 | origins: ['https://api.producthunt.com/'] 13 | , (granted) => 14 | if granted and typeof successCallback is 'function' then successCallback() 15 | 16 | draw: (data, holderElement) -> 17 | if not @config.type then @config.type = "list" 18 | @element = if @config.type == "list" then "ph-item" else "ph-thumb" 19 | @flex = @element == "ph-thumb" 20 | 21 | data.posts = data.posts.map (item, index) -> item.index = index + 1; item 22 | 23 | super data, holderElement 24 | 25 | refresh: (columnElement, holderElement) => 26 | #producthunt api requires a request for a access token 27 | #when we've got access token, we can go on as usual 28 | 29 | fetch "https://api.producthunt.com/v1/oauth/token", 30 | method: "post", 31 | headers: 32 | "Accept": "application/json" 33 | "Content-Type": "application/json" 34 | body: JSON.stringify 35 | client_id: "6c7ae468245e828676be999f5a42e6e50e0101ca99480c4eefbeb981d56f310d", 36 | client_secret: "00825be2da634a7d80bc4dc8d3cbdd54bcaa46d4273101227c27dbd68accdb77", 37 | grant_type: "client_credentials" 38 | .then (response) -> 39 | if response.status is 200 then Promise.resolve response 40 | else Promise.reject new Error response.statusText 41 | .then (response) -> 42 | return response.json() 43 | .then (json) => 44 | @url = "https://api.producthunt.com/v1/posts?access_token="+json.access_token 45 | super columnElement, holderElement 46 | .catch (error) => 47 | console.error error 48 | @refreshing = false 49 | @loading = false 50 | 51 | if not @cache or @cache.length is 0 then @error holderElement 52 | 53 | 54 | tabbie.register "ProductHunt" -------------------------------------------------------------------------------- /src/columns/producthunt/ph-dialog.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link href="../../../bower_components/paper-button/paper-button.html" rel="import"> 3 | <link href="../../../bower_components/core-icon/core-icon.html" rel="import"> 4 | 5 | <polymer-element name="ph-dialog" attributes="config"> 6 | <template> 7 | <tabbie-dialog style="width:400px;"> 8 | <div layout horizontal style="padding-bottom: 20px;"> 9 | <paper-button class="choice active" raised on-click="{{setActive}}" focused?="{{config.type == 'list'}}" data-type="list"> 10 | <core-icon icon="view-stream"></core-icon> 11 | List 12 | </paper-button> 13 | <paper-button class="choice" raised on-click="{{setActive}}" focused?="{{config.type == 'grid'}}" data-type="grid"> 14 | <core-icon icon="view-module"></core-icon> 15 | Grid 16 | </paper-button> 17 | </div> 18 | </tabbie-dialog> 19 | </template> 20 | <script> 21 | Polymer({ 22 | setActive: function(e) { 23 | var target = e.target 24 | if(e.target.nodeName.toLowerCase() == "core-icon") target = e.target.parentElement 25 | this.config.type = target.getAttribute("data-type"); 26 | } 27 | }) 28 | </script> 29 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/producthunt/ph-item.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../../time-ago.html"> 3 | 4 | <polymer-element name="ph-item" attributes="item" noscript> 5 | <template> 6 | <!--<link rel="stylesheet" href="../../../dist/css/feeditem.css">--> 7 | <style> 8 | :host { 9 | position: relative; 10 | } 11 | 12 | a { 13 | color:#000; 14 | } 15 | 16 | .content { 17 | padding:10px 0; 18 | } 19 | 20 | .score { 21 | border-radius: 50%; 22 | background: gray; 23 | color: #fff; 24 | width: 30px; 25 | height: 30px; 26 | display: inline-block; 27 | text-align: center; 28 | padding-top: 4px; 29 | margin: 10px 15px 10px 25px; 30 | } 31 | 32 | h1 { 33 | font-size:14px; 34 | margin:0; 35 | } 36 | 37 | h5 { 38 | font-size:12px; 39 | color:#848484; 40 | margin:0; 41 | /*text-overflow: ellipsis;*/ 42 | /*white-space: nowrap;*/ 43 | /*width: 100%;*/ 44 | /*overflow: hidden;*/ 45 | } 46 | 47 | * { 48 | font-weight: normal; 49 | box-sizing: border-box; 50 | text-decoration: none !important; 51 | } 52 | 53 | .holder a { 54 | position: relative; 55 | } 56 | 57 | .comment { 58 | width:45px; 59 | display:inline-block; 60 | text-align: center; 61 | padding:8px 0; 62 | } 63 | 64 | .comment core-icon { 65 | margin-top:10px; 66 | margin-bottom:10px; 67 | color:#b9b9b9; 68 | width:18px; 69 | height:18px; 70 | } 71 | 72 | paper-ripple { 73 | z-index:1; 74 | } 75 | 76 | </style> 77 | <div class="holder" center layout horizontal> 78 | <a layout horizontal flex href="{{item.redirect_url}}" target="_blank"> 79 | <paper-ripple fit></paper-ripple> 80 | <h4 class="score">{{item.index}}</h4> 81 | <div class="content" flex> 82 | <h1>{{item.name}}</h1> 83 | <h5>{{item.tagline}}</h5> 84 | </div> 85 | </a> 86 | <a class="comment" href="{{item.discussion_url}}" target="_blank"> 87 | <paper-ripple fit></paper-ripple> 88 | <core-icon icon="communication:messenger"></core-icon> 89 | </a> 90 | </div> 91 | </template> 92 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/producthunt/ph-thumb.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../../../bower_components/paper-ripple/paper-ripple.html"> 3 | <link rel="import" href="../../../bower_components/core-icon/core-icon.html"> 4 | <link rel="import" href="../../../bower_components/core-icons/communication-icons.html"> 5 | <link rel="import" href="../../../bower_components/core-icons/image-icons.html"> 6 | <link rel="import" href="../../time-ago.html"> 7 | 8 | <polymer-element name="ph-thumb" attributes="item" noscript> 9 | <template center-justified layout> 10 | <style> 11 | :host { 12 | flex: 1 0 180px; 13 | height: 340px; 14 | max-width: 280px; 15 | font-size: 10px; 16 | color: black; 17 | display: inline-block; 18 | margin: 10px; 19 | padding: 10px; 20 | position: relative; 21 | overflow: hidden; 22 | background: #eceae9; 23 | box-sizing: border-box; 24 | border-radius: 4px; 25 | } 26 | :host(:hover) { 27 | background: #fcf5e2; 28 | -webkit-transition: background,.1s; 29 | -moz-transition: background,.1s; 30 | transition: background,.1s; 31 | } 32 | h1 { 33 | font-size: 24px; 34 | font-family: Verdana, Arial, sans-serif; 35 | font-weight: bold; 36 | text-align: left; 37 | } 38 | h3 { 39 | color: #534540; 40 | font-size: 16px; 41 | margin: 0 23px 5px 0; 42 | padding: 0 5px 0 0; 43 | } 44 | img { 45 | width: 100%; 46 | } 47 | .img { 48 | width: 100%; 49 | padding-bottom: 10px; 50 | } 51 | .img:hover { 52 | opacity: .8; 53 | -webkit-transition: opacity,.2s; 54 | transition: opacity,.2s; 55 | -moz-transition: opacity,.2s; 56 | } 57 | a { 58 | color: rgba(0, 0, 0, 0.87); 59 | text-decoration: none; 60 | } 61 | .overlay { 62 | position: absolute; 63 | opacity: 0; 64 | height: 100%; 65 | width: 100%; 66 | padding: 10px; 67 | background-color: rgba(255, 255, 255, .5); 68 | transition: opacity 250ms; 69 | z-index: 1; 70 | box-sizing:border-box; 71 | } 72 | .minigrid { 73 | position: relative; 74 | width: 100%; 75 | margin: 0 auto; 76 | /*display: flex;*/ 77 | } 78 | .minigrid div core-icon { 79 | width: 15px; 80 | height: 15px; 81 | } 82 | .votes { 83 | background-image: url(../../../img/arrow_up.svg); 84 | background-position: center 2px; 85 | background-repeat: no-repeat; 86 | border-radius: 3px; 87 | color: #534540; 88 | font-size: 13px; 89 | height: 44px; 90 | left: 0; 91 | margin-right: 10px; 92 | padding-top: 23px; 93 | position: absolute; 94 | text-align: center; 95 | top: 0; 96 | width: 33px; 97 | } 98 | .details { 99 | margin-left: 44px; 100 | margin-right: 10px; 101 | } 102 | .comments { 103 | background-image: url(../../../img/comment.svg); 104 | background-repeat: no-repeat; 105 | color: #a9a29f; 106 | height: 20px; 107 | min-width: 30px; 108 | /*padding-left: 20px;*/ 109 | position: absolute; 110 | right: 0; 111 | text-align: right; 112 | top: 0; 113 | } 114 | .comments:hover { 115 | background-image: url(../../../img/comment_hover.svg); 116 | background-repeat: no-repeat; 117 | color: #da552f; 118 | } 119 | p { 120 | color: #685f5c; 121 | font-size: 16px; 122 | margin: 0; 123 | padding: 0; 124 | } 125 | </style> 126 | <a class="product" target="_blank" href="{{item.redirect_url}}"> 127 | <div class="product" center-justified layout> 128 | <div class="img"> 129 | <img src="{{item.screenshot_url['300px']}}"> 130 | </div> 131 | 132 | <div class="minigrid"> 133 | <div class="votes"> 134 | {{item.votes_count}} 135 | </div> 136 | <div class="details"> 137 | <h3>{{item.name}}</h3> 138 | <p>{{item.tagline}}</p> 139 | </div> 140 | <a href="{{item.discussion_url}}" target="_blank"> 141 | <div class="comments"> 142 | {{item.comments_count}} 143 | </div> 144 | </a> 145 | </div> 146 | </div> 147 | </a> 148 | </template> 149 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/pushbullet/PushBullet.coffee: -------------------------------------------------------------------------------- 1 | class Columns.PushBullet extends Columns.Column 2 | name: "PushBullet" 3 | thumb: "img/column-pushbullet.png" 4 | dialog: "pushbullet-dialog" 5 | socket: undefined 6 | api: undefined 7 | colEl: undefined 8 | holEl: undefined 9 | link: "https://www.pushbullet.com" 10 | 11 | refresh: (columnElement, holderElement) => 12 | @refreshing = true 13 | @api.user (error, user) => 14 | @config.user = user 15 | tabbie.sync @ 16 | @api.pushHistory (error, history) => 17 | @refreshing = false 18 | user.myself = true 19 | item.from = user for item in history.pushes when item.direction is 'self' 20 | @cache = history 21 | tabbie.sync @ 22 | @draw history, holderElement 23 | 24 | draw: (data, holderElement) => 25 | @loading = false 26 | holderElement.innerHTML = "" 27 | for item in data.pushes 28 | if not item.active then continue 29 | el = document.createElement "pushbullet-item" 30 | el.item = item 31 | holderElement.appendChild el 32 | 33 | logOut: () => 34 | delete @config.access_token 35 | delete @config.user 36 | tabbie.sync @ 37 | @render @colEl, @holEl 38 | 39 | render: (columnElement, holderElement) => 40 | super columnElement, holderElement 41 | @api = window.PushBullet 42 | @colEl = columnElement 43 | @holEl = holderElement 44 | 45 | if not @config.access_token 46 | @loading = false 47 | @refreshing = false 48 | auth = document.createElement "pushbullet-auth" 49 | auth.addEventListener "sign-in", (event) => 50 | @config.access_token = event.detail 51 | tabbie.sync @ 52 | @render columnElement, holderElement 53 | holderElement.innerHTML = "" 54 | holderElement.appendChild auth 55 | else 56 | @api.APIKey = @config.access_token 57 | 58 | if Object.keys(@cache).length 59 | @draw @cache, holderElement 60 | @refresh columnElement, holderElement 61 | 62 | @socket = new WebSocket 'wss://stream.pushbullet.com/websocket/' + @config.access_token 63 | @socket.onmessage = (e) => 64 | data = JSON.parse e.data 65 | if data.type is "tickle" and data.subtype is "push" then @refresh columnElement, holderElement 66 | 67 | tabbie.register "PushBullet" -------------------------------------------------------------------------------- /src/columns/pushbullet/pushbullet-auth.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/paper-button/paper-button.html"> 2 | <link rel="import" href="../../../bower_components/core-icon/core-icon.html"> 3 | 4 | <polymer-element name="pushbullet-auth" attributes="item"> 5 | <template> 6 | <style> 7 | div { 8 | display:flex; 9 | width:100%; 10 | height:100%; 11 | } 12 | 13 | div paper-button { 14 | margin:auto; 15 | background:#4ab367; 16 | color:#fff; 17 | } 18 | 19 | div paper-button core-icon { 20 | margin-right:10px; 21 | } 22 | </style> 23 | <div> 24 | <paper-button on-click="{{signIn}}" raised><core-icon icon="open-in-new"></core-icon> Sign in</paper-button> 25 | </div> 26 | </template> 27 | <script> 28 | Polymer({ 29 | signIn: function() { 30 | var self = this; 31 | chrome.identity.launchWebAuthFlow({ 32 | url: 'https://www.pushbullet.com/authorize?client_id=weqYIl7GLNIOaep4PaTOpfnFUCBRl1IN&redirect_uri=https%3A%2F%2F'+chrome.runtime.id+'.chromiumapp.org%2FpushBullet&response_type=token', 33 | interactive: true 34 | }, function(redirect_url) { 35 | self.fire('sign-in', /access_token=([.a-zA-Z0-9]*)/.exec(redirect_url)[1]) 36 | }) 37 | } 38 | }) 39 | </script> 40 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/pushbullet/pushbullet-dialog.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link href="../../../bower_components/core-menu/core-menu.html" rel="import"> 3 | <link href="../../../bower_components/paper-dropdown/paper-dropdown.html" rel="import"> 4 | <link href="../../../bower_components/paper-item/paper-item.html" rel="import"> 5 | <link href="../../../bower_components/paper-dropdown-menu/paper-dropdown-menu.html" rel="import"> 6 | 7 | <polymer-element name="pushbullet-dialog" attributes="config"> 8 | <template> 9 | <tabbie-dialog style="width:300px;"> 10 | <style> 11 | paper-button { 12 | color:#F44336; 13 | float:left 14 | } 15 | img { 16 | float:left; 17 | width:40px; 18 | height:40px; 19 | border-radius: 50%; 20 | margin-right:10px; 21 | } 22 | 23 | .clear { 24 | clear:both; 25 | margin-bottom:20px; 26 | } 27 | 28 | .email { 29 | color:#616161; 30 | } 31 | 32 | .user { 33 | font-size:13px; 34 | } 35 | </style> 36 | <template if="{{config.user}}"> 37 | <div class="user"> 38 | <p>You are logged in as</p> 39 | <img src="{{config.user.image_url}}"> 40 | {{config.user.name}}<br> 41 | <span class="email">{{config.user.email}}</span> 42 | <div class="clear"></div> 43 | </div> 44 | </template> 45 | <template if="{{!config.user}}"> 46 | <div class="user"> 47 | <p>You are logged in as</p> 48 | <img src="../../../img/column-unknown.png"> 49 | Not logged in<br> 50 | <div class="clear"></div> 51 | </div> 52 | </template> 53 | <paper-button on-click="{{logOut}}" disabled?="{{!config.access_token}}">Log out</paper-button> 54 | </tabbie-dialog> 55 | </template> 56 | <script> 57 | Polymer({ 58 | logOut: function(e) { 59 | e.currentTarget.parentElement.toggle() 60 | this.column.logOut() 61 | } 62 | }) 63 | </script> 64 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/pushbullet/pushbullet-item.html: -------------------------------------------------------------------------------- 1 | <polymer-element name="pushbullet-item" attributes="item"> 2 | <template> 3 | <style> 4 | :host { 5 | padding:10px 10px; 6 | display:block; 7 | font-size:13px; 8 | border-bottom: 1px solid #d2d2d2; 9 | position: relative; 10 | } 11 | 12 | h4 { 13 | font-size:14px; 14 | margin: 10px 0 0; 15 | } 16 | 17 | a { 18 | color:#000; 19 | text-decoration: none; 20 | } 21 | 22 | .link { 23 | color:#009688; 24 | white-space: nowrap; 25 | text-overflow: ellipsis; 26 | overflow:hidden; 27 | width:100%; 28 | display:block; 29 | } 30 | 31 | p { 32 | margin:0; 33 | } 34 | 35 | .time { 36 | color:#8A9C9D; 37 | display:block; 38 | } 39 | .avatar { 40 | border-radius: 50%; 41 | float:left; 42 | width:40px; 43 | height:40px; 44 | margin-right:10px; 45 | } 46 | .clearfix { 47 | clear:both; 48 | } 49 | 50 | .image { 51 | position: relative; 52 | width:calc(100% + 11px); /* ew */ 53 | left:-10px; 54 | margin-top:10px; 55 | height:200px; 56 | background-position: center center; 57 | background-size: 100%; 58 | } 59 | </style> 60 | <a target="_blank"> 61 | <template if="{{item.from}}"> 62 | <img class="avatar" src="{{item.from.image_url}}"> 63 | From 64 | <template if="{{!item.from.myself}}">{{item.from.name}}</template> 65 | <template if="{{item.from.myself}}">Yourself</template> 66 | <time-ago datetime="{{item.created}}" class="time"></time-ago> 67 | <div class="clearfix"></div> 68 | </template> 69 | <template if="{{item.image_url}}"> 70 | <div class="image" style="background-image:url('{{item.image_url}}')"></div> 71 | </template> 72 | <h4>{{item.title}}</h4> 73 | <template if="{{item.body}}"> 74 | <p>{{item.body}}</p> 75 | </template> 76 | <template if="{{item.type == 'file' && !item.image_url}}"> 77 | <span class="link">{{item.file_name}}</span> 78 | </template> 79 | <template if="{{item.type == 'link'}}"> 80 | <span class="link">{{item.url}}</span> 81 | </template> 82 | 83 | <template if="{{item._url}}"> 84 | <paper-ripple fit></paper-ripple> 85 | </template> 86 | </a> 87 | 88 | </template> 89 | <script> 90 | Polymer({ 91 | attached: function() { 92 | switch(this.item.type) { 93 | case "note": 94 | this.item._url = false; 95 | break; 96 | case "file": 97 | this.item._url = this.item.file_url; 98 | break; 99 | case "link": 100 | this.item._url = this.item.url; 101 | } 102 | 103 | if(this.item._url) this.shadowRoot.querySelector("a").setAttribute("href", this.item._url) 104 | } 105 | }) 106 | </script> 107 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/reddit/Reddit.coffee: -------------------------------------------------------------------------------- 1 | class Columns.Reddit extends Columns.FeedColumn 2 | name: "Reddit" 3 | width: 1 4 | dialog: "reddit-dialog" 5 | thumb: "img/column-reddit.png" 6 | link: "https://www.reddit.com/" 7 | 8 | element: "reddit-item" 9 | dataPath: "data.children" 10 | childPath: "data" 11 | 12 | cid: "qr6drw45JFCXfw" 13 | 14 | refresh: (columnElement, holderElement) => 15 | @refreshing = true 16 | 17 | if not @config.subreddit 18 | @config = 19 | listing: 0 20 | subreddit: "funny" 21 | option: "subreddit", 22 | multireddit: "empw/m/electronicmusic" 23 | 24 | console.info "config.option", @config.option 25 | switch @config.option 26 | when "subreddit" 27 | switch @config.listing 28 | when 0 then listing = "hot" 29 | when 1 then listing = "new" 30 | when 2 then listing = "top" 31 | 32 | @url = "https://www.reddit.com/r/"+@config.subreddit+"/"+listing+".json" 33 | super columnElement, holderElement 34 | when "multireddit" 35 | @url = "https://www.reddit.com/user/"+@config.multireddit+".json" 36 | super columnElement, holderElement 37 | when "frontpage" 38 | cb = => 39 | fetch "https://oauth.reddit.com/.json", 40 | headers: 41 | "Authorization": "bearer "+@config.access_token 42 | "Accept": "application/json" 43 | .then (response) => 44 | if response.status is 200 then Promise.resolve response.json() 45 | else if response.status is 401 46 | @holderElement.innerHTML = "" 47 | @holderElement.appendChild document.createElement "reddit-error" 48 | delete @config.access_token 49 | delete @config.refresh_token 50 | delete @config.expire 51 | tabbie.sync @ 52 | Promise.reject new Error response.statusText 53 | else Promise.reject new Error response.statusText 54 | .then (data) => 55 | @refreshing = false 56 | @cache = data 57 | tabbie.sync @ 58 | holderElement.innerHTML = "" 59 | @draw data, holderElement 60 | .catch (error) => 61 | console.error error 62 | @refreshing = false 63 | @loading = false 64 | 65 | #check if token has expired 66 | if Math.floor(new Date().getTime() / 1000) > @config.expire 67 | @getToken false, cb 68 | else cb() 69 | else console.error "Reddit column: Invalid config.option ???" 70 | 71 | render: (@columnElement, @holderElement) => 72 | super @columnElement, @holderElement 73 | 74 | getToken: (code, cb) => 75 | #if we have a code, use the code, if not, assume we've got a refresh_token 76 | if code then body = "grant_type=authorization_code&code="+code+"&redirect_uri=https%3A%2F%2F"+chrome.runtime.id+".chromiumapp.org%2Freddit" 77 | else body = "grant_type=refresh_token&refresh_token="+@config.refresh_token+"&redirect_uri=https%3A%2F%2F"+chrome.runtime.id+".chromiumapp.org%2Freddit" 78 | 79 | fetch "https://www.reddit.com/api/v1/access_token", 80 | method: "post" 81 | headers: 82 | "Content-Type": "application/x-www-form-urlencoded", 83 | "Authorization": "Basic "+btoa(@cid+":") 84 | body: body 85 | .then (response) => 86 | if response.status is 200 then Promise.resolve response.json() 87 | else Promise.reject new Error response.statusText 88 | .then (data) => 89 | @config.access_token = data.access_token 90 | if code then @config.refresh_token = data.refresh_token 91 | @config.expire = Math.floor(new Date().getTime() / 1000) + data.expires_in 92 | console.info "access_token", @config.access_token, "refresh_token", @config.refresh_token, "expire time", @config.expire 93 | tabbie.sync @ 94 | if typeof cb is 'function' then cb() 95 | .catch (e) => 96 | console.error(e) 97 | 98 | login: => 99 | chrome.permissions.request 100 | origins: ['https://oauth.reddit.com/', 'https://www.reddit.com/'] 101 | , (granted) => 102 | if granted 103 | chrome.identity.launchWebAuthFlow 104 | url: "https://www.reddit.com/api/v1/authorize?client_id="+@cid+"&response_type=code&state=hellothisisaeasteregg&redirect_uri=https%3A%2F%2F"+chrome.runtime.id+".chromiumapp.org%2Freddit&duration=permanent&scope=read" 105 | interactive: true, 106 | , (redirect_url) => 107 | if redirect_url 108 | code = /code=([a-zA-Z0-9_\-]*)/.exec(redirect_url)[1] 109 | @getToken code 110 | 111 | logout: => 112 | delete @config.access_token 113 | tabbie.sync @ 114 | 115 | tabbie.register "Reddit" -------------------------------------------------------------------------------- /src/columns/reddit/reddit-dialog.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../../../bower_components/paper-input/paper-input.html"> 3 | <link rel="import" href="../../../bower_components/paper-input/paper-input-decorator.html"> 4 | <link href="../../../bower_components/core-menu/core-menu.html" rel="import"> 5 | <link href="../../../bower_components/paper-dropdown/paper-dropdown.html" rel="import"> 6 | <link href="../../../bower_components/paper-item/paper-item.html" rel="import"> 7 | <link href="../../../bower_components/paper-dropdown-menu/paper-dropdown-menu.html" rel="import"> 8 | <link href="../../../bower_components/paper-radio-button/paper-radio-button.html" rel="import"> 9 | <link href="../../../bower_components/paper-radio-group/paper-radio-group.html" rel="import"> 10 | 11 | <polymer-element name="reddit-dialog" attributes="config"> 12 | <template> 13 | <tabbie-dialog style="width:400px;"> 14 | <style> 15 | paper-input-decorator { 16 | display:block !important; 17 | } 18 | .listing { 19 | width:70px; 20 | margin-left:10px; 21 | padding-top: 0.75em 22 | } 23 | .listing paper-dropdown { 24 | width:90px; 25 | } 26 | 27 | .listing paper-dropdown-menu { 28 | margin-top:0; 29 | height: 22px; 30 | } 31 | 32 | .listing paper-dropdown paper-item { 33 | min-width: 0 !important; 34 | } 35 | 36 | .listing .label { 37 | color: #757575; 38 | display: block; 39 | font-size: 12px; 40 | } 41 | 42 | paper-radio-group { 43 | width:100%; 44 | padding-bottom:20px; 45 | } 46 | 47 | paper-radio-group > * { 48 | padding:0 !important; 49 | } 50 | 51 | .pad { 52 | padding-left:33px !important; 53 | } 54 | 55 | paper-button { 56 | float:left; 57 | } 58 | 59 | paper-button.green { 60 | color:#4CAF50; 61 | } 62 | 63 | paper-button.red { 64 | color:#F44336; 65 | } 66 | </style> 67 | <paper-radio-group selected="{{config.option}}"> 68 | <paper-radio-button name="subreddit" label="Display a subreddit"></paper-radio-button> 69 | <div layout horizontal class="pad"> 70 | <paper-input-decorator label="subreddit name" floatingLabel error="Subreddit name is required!" autoValidate flex> 71 | <input is="core-input" required value="{{config.subreddit}}" class="subreddit"> 72 | </paper-input-decorator> 73 | 74 | <div class="listing"> 75 | <span class="label">listing</span> 76 | <paper-dropdown-menu label="Listing"> 77 | <paper-dropdown class="dropdown"> 78 | <core-menu class="menu" selected="{{config.listing}}"> 79 | <paper-item>Hot</paper-item> 80 | <paper-item>New</paper-item> 81 | <paper-item>Top</paper-item> 82 | </core-menu> 83 | </paper-dropdown> 84 | </paper-dropdown-menu> 85 | </div> 86 | </div> 87 | <paper-radio-button name="multireddit" label="Display a multireddit"></paper-radio-button> 88 | <div class="pad"> 89 | <paper-input-decorator style="margin-right:20px;" label="multireddit url (for example empw/m/autos)" floatingLabel error="Url is required!" autoValidate flex> 90 | <input is="core-input" required value="{{config.multireddit}}" class="multi"> 91 | </paper-input-decorator> 92 | </div> 93 | <paper-radio-button name="frontpage" disabled?="{{!config.access_token}}" label="Display the frontpage"></paper-radio-button> 94 | </paper-radio-group> 95 | 96 | <template if="{{!config.access_token}}"> 97 | <paper-button on-click="{{logIn}}" class="green">Log in</paper-button> 98 | </template> 99 | <template if="{{config.access_token}}"> 100 | <paper-button on-click="{{logOut}}" class="red">Log out</paper-button> 101 | </template> 102 | </tabbie-dialog> 103 | </template> 104 | <script> 105 | Polymer({ 106 | dialog: null, 107 | attached: function() { 108 | this.dialog = this.shadowRoot.querySelector("tabbie-dialog") 109 | }, 110 | configChanged: function() { 111 | this.dialog.querySelector(".subreddit").parentElement.validate() 112 | this.dialog.querySelector(".multi").parentElement.validate() 113 | }, 114 | logIn: function() { 115 | this.column.login() 116 | }, 117 | logOut: function() { 118 | this.column.logout() 119 | }, 120 | close: function() { 121 | this.super(); 122 | console.log(this.config) 123 | } 124 | }) 125 | </script> 126 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/reddit/reddit-error.html: -------------------------------------------------------------------------------- 1 | <polymer-element name="reddit-error" noscript> 2 | <template> 3 | <style> 4 | :host { 5 | display:flex !important; 6 | height:100%; 7 | } 8 | h1 { 9 | margin:auto auto; 10 | color:#B71C1C; 11 | font-weight: lighter; 12 | font-size:14px; 13 | font-style: italic; 14 | text-align: center; 15 | } 16 | </style> 17 | <h1><core-icon icon="error"></core-icon><br>Reddit has revoked your session,<br>please log in again.</h1> 18 | </template> 19 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/reddit/reddit-item.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../../time-ago.html"> 3 | 4 | <polymer-element name="reddit-item" attributes="item"> 5 | <template> 6 | <link rel="stylesheet" href="../../../dist/css/feeditem.css"> 7 | <style> 8 | :host { 9 | padding: 0 0 0 90px; 10 | } 11 | 12 | :host(.nothumb) { 13 | padding:0 0 0 10px; 14 | } 15 | 16 | img { 17 | border-radius:50%; 18 | width:50px; 19 | height:50px; 20 | 21 | left:20px; 22 | top:10px; 23 | position: absolute; 24 | } 25 | .img { 26 | float:left; 27 | display:block; 28 | } 29 | </style> 30 | 31 | <template if="{{!item.nothumb}}"> 32 | <a class="img" target="_blank" href="{{item.url}}"><img src="{{item.thumbnail}}"></a> 33 | </template> 34 | <div> 35 | <h1><a href="{{item.url}}" target="_blank" >{{item.title}}</a></h1> 36 | <p> 37 | submitted by <a target="_blank" href="http://www.reddit.com/u/{{item.author}}">{{item.author}}</a> <time-ago epoch="true" datetime="{{item.created_utc}}"></time-ago> to <a href="https://www.reddit.com/r/{{item.subreddit}}">{{item.subreddit}}</a> 38 | </p> 39 | <p> 40 | <a target="_blank" href="https://reddit.com{{item.permalink}}">{{item.num_comments}} comments</a>, {{item.score}} points 41 | </p> 42 | </div> 43 | </template> 44 | <script> 45 | Polymer({ 46 | attached: function() { 47 | this.item.url = this.item.url.replace(/&/g, '&'); 48 | if(!this.item.thumbnail || this.item.thumbnail == "default" || this.item.thumbnail == "self" || this.item.thumbnail == "nsfw") { 49 | this.item.nothumb = true 50 | this.classList.add("nothumb"); 51 | } 52 | } 53 | }); 54 | </script> 55 | </polymer-element> -------------------------------------------------------------------------------- /src/columns/speeddial/SpeedDial.coffee: -------------------------------------------------------------------------------- 1 | class Columns.SpeedDial extends Columns.Column 2 | name: "SpeedDial" 3 | dialog: "speed-dial-dialog" 4 | thumb: "img/column-speeddial.png" 5 | flex: true 6 | element: "speed-dial-item" 7 | link: "" 8 | 9 | refresh: (columnElement, holderElement) -> 10 | holderElement.innerHTML = "" 11 | 12 | # initialize config 13 | if not @config.websites 14 | @config = 15 | websites: [] 16 | 17 | # create items 18 | for website in @config.websites 19 | app = document.createElement "speed-dial-item" 20 | try 21 | app.name = website.name 22 | app.icon = website.icon.url 23 | app.url = website.url 24 | catch e 25 | console.warn e 26 | 27 | holderElement.appendChild app 28 | 29 | #needed for proper flex 30 | for num in [0..10] when @flex 31 | hack = document.createElement "speed-dial-item" 32 | hack.className = "hack" 33 | holderElement.appendChild hack 34 | 35 | render: (columnElement, holderElement) -> 36 | super columnElement, holderElement 37 | @refreshing = false 38 | @loading = false 39 | 40 | @refresh columnElement, holderElement 41 | 42 | tabbie.register "SpeedDial" 43 | -------------------------------------------------------------------------------- /src/columns/speeddial/speed-dial-dialog.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../../../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../../../bower_components/paper-input/paper-input.html"> 3 | <link rel="import" href="../../../bower_components/paper-input/paper-input-decorator.html"> 4 | <link rel="import" href="../../../bower_components/paper-checkbox/paper-checkbox.html"> 5 | <link rel="import" href="../../../bower_components/core-icons/core-icons.html"> 6 | <link rel="import" href="../../../bower_components/core-icon/core-icon.html"> 7 | <link rel="import" href="../../../bower_components/core-icon-button/core-icon-button.html"> 8 | <link rel="import" href="../../../bower_components/paper-fab/paper-fab.html"> 9 | 10 | <polymer-element name="speed-dial-dialog" attributes="config"> 11 | <template> 12 | <tabbie-dialog id="speedDialSettingsDialog" style="width:600px; top:0;"> 13 | <style> 14 | paper-button.green { 15 | color:#4CAF50; 16 | } 17 | 18 | paper-button.red { 19 | color:#F44336; 20 | } 21 | 22 | paper-button.blue { 23 | color:#2196F3; 24 | } 25 | 26 | .container { 27 | height: 499px; 28 | overflow-y: auto; 29 | position: relative; 30 | overflow-x: hidden; 31 | } 32 | 33 | .page { 34 | position: absolute; 35 | top: 0; 36 | height: 100%; 37 | width: 100%; 38 | transition: 0.2s ease-out; 39 | } 40 | 41 | .list { 42 | 43 | } 44 | 45 | .list .entry { 46 | border-bottom: 1px solid rgba(0, 0, 0, 0.12); 47 | } 48 | 49 | .list > .entry:last-child { 50 | border-bottom: 0; 51 | } 52 | 53 | .list .entry > * { 54 | display: inline-block; 55 | vertical-align: middle; 56 | } 57 | 58 | .order-buttons { 59 | width: 46px; 60 | } 61 | 62 | .preview-thumb { 63 | width: 64px; 64 | text-align: center; 65 | } 66 | 67 | .preview-thumb > img { 68 | display: inline-block; 69 | max-width: 64px; 70 | height: 64px; 71 | border: none; 72 | } 73 | 74 | .text { 75 | width: calc(100% - 52px - 52px - 66px); 76 | } 77 | 78 | .text .name { 79 | font-size: 20px; 80 | margin-left: 5px; 81 | color: rgba(0, 0, 0, 0.87); 82 | } 83 | 84 | .text .url { 85 | font-size: 14px; 86 | color: rgba(0, 0, 0, 0.54); 87 | } 88 | 89 | .text .url > span { 90 | vertical-align: middle; 91 | } 92 | 93 | .edit-button { 94 | width: 48px; 95 | color: rgba(0, 0, 0, 0.54); 96 | } 97 | 98 | .settings { 99 | background: white; 100 | box-shadow: 0 2px 4px -1px rgba(0,0,0,.14),0 4px 5px 0 rgba(0,0,0,.098),0 1px 10px 0 rgba(0,0,0,.084); 101 | } 102 | 103 | .preview-item { 104 | width: 100%; 105 | text-align: center; 106 | } 107 | 108 | .preview-item > speed-dial-item { 109 | float: none; 110 | } 111 | 112 | .bottom-bar { 113 | position: absolute; 114 | left: 0; 115 | bottom: 0; 116 | padding: 20px; 117 | box-sizing: border-box; 118 | } 119 | 120 | .bottom-bar.middle { 121 | text-align: center; 122 | width: 100%; 123 | } 124 | 125 | .bottom-bar.middle > * { 126 | display: inline-block; 127 | } 128 | 129 | .placeholder { 130 | font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; 131 | text-align: center; 132 | padding-top: 200px; 133 | color:#646464; 134 | } 135 | 136 | .placeholder p { 137 | font-size: .9em; 138 | margin: 0.2em 0 0; 139 | } 140 | 141 | .placeholder core-icon { 142 | width:64px; 143 | height:64px; 144 | } 145 | </style> 146 | 147 | 148 | 149 | <div class="container"> 150 | 151 | 152 | 153 | <!-- List of all websites --> 154 | <div class="page list"> 155 | <template repeat="{{ website, index in config.websites }}"> 156 | 157 | <div class="entry"> 158 | 159 | <!-- Icon --> 160 | <div class="preview-thumb"> 161 | <img src="{{ website.icon.url }}"> 162 | </div> 163 | 164 | <!-- Text --> 165 | <div class="text"> 166 | <div class="name"> 167 | {{ website.name }} 168 | </div> 169 | <div class="url"> 170 | <core-icon icon="chevron-right"></core-icon> 171 | <span> {{ website.url }} </span> 172 | </div> 173 | </div> 174 | 175 | 176 | <!-- Settings --> 177 | <div class="edit-button"> 178 | <paper-icon-button icon="settings" on-click="{{openSettings}}" data-index={{index}}></paper-icon-button> 179 | </div> 180 | 181 | 182 | 183 | <!-- Reordering --> 184 | <div class="order-buttons"> 185 | <paper-icon-button icon="arrow-drop-down" style="transform: rotate(180deg);" 186 | title="Move entry up" aria-label="Move entry up" 187 | role="button" tabindex="0" 188 | on-click="{{moveUp}}" data-index="{{index}}" 189 | disabled?="{{ index <= 0 }}"> 190 | </paper-icon-button> 191 | <paper-icon-button icon="arrow-drop-down" 192 | title="Move entry down" aria-label="Move entry down" 193 | role="button" tabindex="0" 194 | on-click="{{moveDown}}" data-index="{{index}}" 195 | disabled?="{{ index >= config.websites.length - 1 }}"> 196 | </paper-icon-button> 197 | </div> 198 | 199 | </div> 200 | 201 | </template> 202 | 203 | <template if="{{ config.websites.length == 0 }}"> 204 | <div class="placeholder"> 205 | <core-icon icon="explore"></core-icon> 206 | <p>Create your first entry by clicking the button on the bottom right corner.</p> 207 | </div> 208 | </template> 209 | </div> 210 | 211 | 212 | 213 | <!-- Detailed settings page for selected website --> 214 | <div class="page settings" style="left: {{settingsOffset}}px;"> 215 | 216 | <div class="preview-item"> 217 | <speed-dial-item name="{{editedWebsite.name}}" url="#" icon="{{editedWebsite.icon.url}}"> 218 | </speed-dial-item> 219 | </div> 220 | 221 | <div class="field"> 222 | <paper-input-decorator label="Dialname" error="Required!" floatingLabel labelVisible isInvalid="{{!$.dialname.validity.valid}}"> 223 | <input id="dialname" is="core-input" required value="{{editedWebsite.name}}"> 224 | </paper-input-decorator> 225 | </div> 226 | 227 | <div class="field"> 228 | <paper-input-decorator label="Website URL" error="Invalid URL! (http:// required)" floatingLabel labelVisible isInvalid="{{!$.url.validity.valid}}"> 229 | <input id="url" is="core-input" required value="{{editedWebsite.url}}" pattern="((((ht|f)tp(s?))\:\/\/){1}\S+)"> 230 | </paper-input-decorator> 231 | </div> 232 | 233 | <div class="field"> 234 | <paper-input-decorator label="Custom Icon URL" error="Invalid Image URL!" floatingLabel labelVisible isInvalid="{{!$.iconurl.validity.valid}}"> 235 | <input id="iconurl" is="core-input" required value="{{editedWebsite.icon.url}}" pattern="^https?:\/\/(?:[a-z0-9\-]+\.)+[a-z]{2,6}(?:\/[^/#?]+)+\.(?:jp(e?)g|gif|png|ico)$" disabled?="{{$.chkToggleDefaultIcon.checked}}"> 236 | </paper-input-decorator> 237 | <paper-checkbox id="chkToggleDefaultIcon" on-change="{{toggleDefaultIcon}}" checked="{{isDefaultIcon}}" label="Default Icon"></paper-checkbox> 238 | </div> 239 | </div> 240 | </div> 241 | 242 | 243 | <!-- Dialog bottom buttons --> 244 | <div class="bottom-bar" hidden?="{{settingsOpen}}"> 245 | <paper-fab icon="add" class="fab-add" on-click="{{addWebsite}}"></paper-fab> 246 | </div> 247 | 248 | <div class="bottom-bar middle" hidden?="{{!settingsOpen}}"> 249 | <paper-fab icon="delete" class="fab-delete" on-click="{{removeSettingsWebsite}}"></paper-fab> 250 | <paper-fab icon="save" class="fab-save" style="background-color: #8BC34A;" on-click="{{saveSettingsWebsite}}"></paper-fab> 251 | </div> 252 | 253 | <div class="bottom-bar" hidden?="{{!settingsOpen}}"> 254 | <paper-fab icon="arrow-back" class="fab-arrow-back" on-click="{{closeSettings}}"></paper-fab> 255 | </div> 256 | 257 | </tabbie-dialog> 258 | </template> 259 | <script> 260 | 261 | function moveArrayElem(array, from, to) { 262 | array.splice(to, 0, array.splice(from, 1)[0]); 263 | } 264 | 265 | function existsWebsite(array, website) { 266 | var result = false; 267 | 268 | for(var item in array) { 269 | if(array[item].id == website.id) { 270 | result = true; 271 | break; 272 | } 273 | } 274 | 275 | return result; 276 | } 277 | 278 | Polymer({ 279 | removeSettingsWebsite: function() { 280 | var index = this.config.websites.indexOf(this.settingsWebsite); 281 | this.config.websites.splice(index, 1); 282 | this.closeSettings(); 283 | }, 284 | 285 | addWebsite: function() { 286 | var website = { 287 | id: Math.floor(Math.random() * 1000000), 288 | name: '', 289 | icon: { 290 | url: '' 291 | }, 292 | url: '' 293 | }; 294 | 295 | this.openSettingsDialog(website); 296 | }, 297 | 298 | saveSettingsWebsite: function(){ 299 | if(this.$.dialname.validity.valid && this.$.iconurl.validity.valid && this.$.url.validity.valid) { 300 | if(!existsWebsite(this.config.websites, this.settingsWebsite)) { // if this is new dial 301 | this.config.websites.push(this.editedWebsite); 302 | } 303 | this.settingsWebsite.name = this.editedWebsite.name; 304 | this.settingsWebsite.icon.url = this.editedWebsite.icon.url; 305 | this.settingsWebsite.url = this.editedWebsite.url; 306 | } else { 307 | alert("Please check the validity of the inputs!"); 308 | } 309 | }, 310 | 311 | moveDown: function(ev, detail, elem) { 312 | var index = elem.getAttribute("data-index"); 313 | var websites = this.config.websites; 314 | moveArrayElem(websites, index, Math.min(index + 1, websites.length - 1)); 315 | }, 316 | 317 | moveUp: function(ev, detail, elem) { 318 | var index = elem.getAttribute("data-index"); 319 | moveArrayElem(this.config.websites, index, Math.max(index - 1, 0)); 320 | }, 321 | 322 | openSettings: function(ev, detail, elem) { 323 | var index = elem.getAttribute("data-index"); 324 | this.openSettingsDialog(this.config.websites[index]); 325 | }, 326 | 327 | openSettingsDialog: function(website) { 328 | this.settingsOpen = true; 329 | this.settingsOffset = 0; 330 | this.settingsWebsite = website; 331 | this.editedWebsite = JSON.parse(JSON.stringify(website)); 332 | if(website.icon.url.startsWith('chrome-extension://')) { 333 | this.isDefaultIcon = true; 334 | } 335 | this.$.speedDialSettingsDialog.setAttribute('autoCloseDisabled', ''); 336 | }, 337 | 338 | closeSettings: function() { 339 | if(JSON.stringify(this.settingsWebsite) != JSON.stringify(this.editedWebsite)) { 340 | if(!confirm("Unsaved changes. Are you sure you want to quit?")) { 341 | return 0; 342 | } 343 | } 344 | 345 | this.settingsOffset = 600; 346 | this.settingsOpen = false; 347 | this.isDefaultIcon = false; 348 | this.$.speedDialSettingsDialog.removeAttribute('autoCloseDisabled'); 349 | }, 350 | 351 | toggleDefaultIcon: function(ev, detail, elem) { 352 | if(elem.checked) { 353 | this.editedWebsite.icon.url = chrome.extension.getURL("img/default-speeddialitem-icon.png"); 354 | } else { 355 | this.editedWebsite.icon.url = ""; 356 | } 357 | }, 358 | 359 | settingsOffset: 600, 360 | settingsOpen: false, 361 | isDefaultIcon: false, 362 | settingsWebsite: {}, 363 | editedWebsite: {} 364 | }) 365 | </script> 366 | </polymer-element> 367 | -------------------------------------------------------------------------------- /src/columns/speeddial/speed-dial-item.html: -------------------------------------------------------------------------------- 1 | <polymer-element name="speed-dial-item" attributes="name icon url" noscript> 2 | <template> 3 | <style> 4 | :host { 5 | width:50%; 6 | float:left; 7 | position: relative; 8 | padding:10px; 9 | cursor:pointer; 10 | flex: 1 0 150px; 11 | } 12 | h5 { 13 | width:100%; 14 | text-align: center; 15 | } 16 | img { 17 | width:64px; 18 | height:64px; 19 | display:block; 20 | margin:0 auto; 21 | } 22 | 23 | a, a:visited, a:hover, a:focus { 24 | text-decoration: none; 25 | color: black; 26 | } 27 | </style> 28 | 29 | <a href="{{url}}"> 30 | <paper-ripple fit></paper-ripple> 31 | <img src="{{icon}}"> 32 | <h5>{{name}}</h5> 33 | </a> 34 | </template> 35 | </polymer-element> 36 | -------------------------------------------------------------------------------- /src/columns/topsites/TopSites.coffee: -------------------------------------------------------------------------------- 1 | class Columns.TopSites extends Columns.Column 2 | name: "Top sites" 3 | thumb: "img/column-topsites.png" 4 | 5 | attemptAdd: (successCallback) => 6 | chrome.permissions.request 7 | permissions: ["topSites"], 8 | origins: ["chrome://favicon/*"] 9 | , (granted) => 10 | if granted 11 | if typeof successCallback is 'function' then successCallback() 12 | 13 | refresh: (columnElement, holderElement) -> 14 | chrome.topSites.get (sites) => 15 | holderElement.innerHTML = "" 16 | for site in sites 17 | paper = document.createElement "bookmark-item" 18 | paper.showdate = false 19 | paper.title = site.title 20 | paper.url = site.url 21 | holderElement.appendChild paper 22 | 23 | render: (columnElement, holderElement) -> 24 | super columnElement, holderElement 25 | @refreshing = false 26 | @loading = false 27 | @refresh columnElement, holderElement 28 | 29 | tabbie.register "TopSites" -------------------------------------------------------------------------------- /src/config.rb: -------------------------------------------------------------------------------- 1 | require 'compass/import-once/activate' 2 | # Require any additional compass plugins here. 3 | 4 | # Set this to the root of your project when deployed: 5 | http_path = "/" 6 | css_dir = "css" 7 | sass_dir = "sass" 8 | images_dir = "images" 9 | javascripts_dir = "js" 10 | 11 | # You can select your preferred output style here (can be overridden via the command line): 12 | # output_style = :expanded or :nested or :compact or :compressed 13 | 14 | # To enable relative paths to assets via compass helper functions. Uncomment: 15 | # relative_assets = true 16 | 17 | # To disable debugging comments that display the original location of your selectors. Uncomment: 18 | # line_comments = false 19 | 20 | 21 | # If you prefer the indented syntax, you might want to regenerate this 22 | # project again passing --syntax sass, or you can uncomment this: 23 | # preferred_syntax = :sass 24 | # and then run: 25 | # sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass 26 | -------------------------------------------------------------------------------- /src/fab-anim.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../bower_components/polymer/polymer.html"> 2 | <polymer-element name="fab-anim"> 3 | <template> 4 | <style> 5 | :host { 6 | position: fixed; 7 | width: 56px; 8 | height: 56px; 9 | background: #d23f31; 10 | border-radius: 50%; 11 | z-index: 9; 12 | transition: transform 350ms ease-in-out; 13 | } 14 | 15 | :host(.active) { 16 | transform:scale3d({{scale}}, {{scale}}, 1) 17 | } 18 | </style> 19 | 20 | </template> 21 | <script> 22 | Polymer({ 23 | play: function() { 24 | var animHeight = document.body.clientWidth; 25 | if(document.body.clientWidth < document.body.clientHeight) animHeight = document.body.clientHeight; 26 | this.scale = Math.round((animHeight / 56) * 3); 27 | console.log("fabanim scale", this.scale) 28 | this.classList.add("active") 29 | } 30 | }) 31 | </script> 32 | </polymer-element> -------------------------------------------------------------------------------- /src/font/Roboto-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/font/Roboto-Regular-webfont.woff -------------------------------------------------------------------------------- /src/font/RobotoSlab-Regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/font/RobotoSlab-Regular-webfont.ttf -------------------------------------------------------------------------------- /src/fullscreen-dialog.html: -------------------------------------------------------------------------------- 1 | <link rel="import" href="../bower_components/polymer/polymer.html"> 2 | <link rel="import" href="../bower_components/core-toolbar/core-toolbar.html"> 3 | <link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html"> 4 | 5 | <polymer-element name="fullscreen-dialog" attributes="heading"> 6 | <template> 7 | <style> 8 | :host { 9 | position:fixed; 10 | width:100%; 11 | height:100%; 12 | top:0; 13 | left:0; 14 | background: #fff; 15 | z-index: 10; 16 | opacity:0; 17 | transition:opacity 500ms; 18 | display:none; 19 | } 20 | 21 | .back-toolbar { 22 | position: absolute; 23 | left:0; 24 | top:0; 25 | width:100%; 26 | background: #d23f31; 27 | height:150px; 28 | color:#fff; 29 | } 30 | 31 | .card { 32 | width:70%; 33 | margin: 45px auto 0; 34 | background: #fff; 35 | border-radius:3px; 36 | } 37 | 38 | .card .content { 39 | padding:10px 20px; 40 | max-height: calc(100% - 162px); 41 | overflow-y:auto; 42 | } 43 | 44 | * { 45 | box-sizing: border-box; 46 | } 47 | 48 | .card-bar { 49 | border-top-left-radius: 3px; 50 | border-top-right-radius: 3px; 51 | background: #fff; 52 | padding:20px 10px; 53 | height:auto; 54 | border-bottom:1px solid #eee; 55 | } 56 | 57 | </style> 58 | 59 | <core-toolbar class="back-toolbar"> 60 | <paper-icon-button icon="arrow-back" on-click="{{toggle}}"></paper-icon-button> 61 | </core-toolbar> 62 | 63 | <paper-shadow z="2" class="card"> 64 | <core-toolbar class="card-bar"> 65 | <span flex>{{heading}}</span> 66 | </core-toolbar> 67 | 68 | <div class="content"> 69 | <content></content> 70 | </div> 71 | </paper-shadow> 72 | 73 | </template> 74 | <script> 75 | Polymer({ 76 | addButton: function(icon, callback) { 77 | var toolbar = this.shadowRoot.querySelector(".card-bar") 78 | var button = document.createElement("paper-icon-button"); 79 | button.icon = icon; 80 | toolbar.appendChild(button) 81 | button.addEventListener("click", function() { 82 | callback() 83 | }) 84 | }, 85 | 86 | replaceHeader: function(headerElements) { 87 | var toolbar = this.shadowRoot.querySelector(".card-bar"); 88 | toolbar.innerHTML = ""; 89 | headerElements.forEach(function(headerElement) { 90 | toolbar.appendChild(headerElement) 91 | }); 92 | }, 93 | 94 | toggle: function(afterFade, beforeFade) { 95 | if(this.style.opacity == 0) { 96 | this.style.display = "block" 97 | var self = this; 98 | this.async(function() { 99 | self.style.opacity = 1; 100 | }) 101 | } 102 | else { 103 | this.style.opacity = 0; 104 | } 105 | 106 | if (typeof beforeFade === 'function') beforeFade() 107 | 108 | var event = this.addEventListener("webkitTransitionEnd", function(e) { 109 | if(e.target.nodeName.toLowerCase() == "auto-suggestions") return; 110 | 111 | if(this.style.opacity == 0) this.style.display = "none" 112 | 113 | if(typeof afterFade === 'function') afterFade() 114 | }) 115 | 116 | } 117 | }) 118 | </script> 119 | </polymer-element> -------------------------------------------------------------------------------- /src/img/arrow_up.svg: -------------------------------------------------------------------------------- 1 | <svg width="15" height="14" viewBox="0 0 15 14" xmlns="http://www.w3.org/2000/svg"><title>Rectangle 24Created with Sketch. -------------------------------------------------------------------------------- /src/img/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/chrome.png -------------------------------------------------------------------------------- /src/img/column-apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-apps.png -------------------------------------------------------------------------------- /src/img/column-behance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-behance.png -------------------------------------------------------------------------------- /src/img/column-bookmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-bookmarks.png -------------------------------------------------------------------------------- /src/img/column-closedtabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-closedtabs.png -------------------------------------------------------------------------------- /src/img/column-codepen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-codepen.png -------------------------------------------------------------------------------- /src/img/column-designernews.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-designernews.png -------------------------------------------------------------------------------- /src/img/column-dribble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-dribble.png -------------------------------------------------------------------------------- /src/img/column-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-github.png -------------------------------------------------------------------------------- /src/img/column-gmail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-gmail.png -------------------------------------------------------------------------------- /src/img/column-hackernews.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-hackernews.png -------------------------------------------------------------------------------- /src/img/column-lobsters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-lobsters.png -------------------------------------------------------------------------------- /src/img/column-producthunt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-producthunt.png -------------------------------------------------------------------------------- /src/img/column-pushbullet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-pushbullet.png -------------------------------------------------------------------------------- /src/img/column-reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-reddit.png -------------------------------------------------------------------------------- /src/img/column-speeddial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-speeddial.png -------------------------------------------------------------------------------- /src/img/column-topsites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-topsites.png -------------------------------------------------------------------------------- /src/img/column-unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/column-unknown.png -------------------------------------------------------------------------------- /src/img/comment.svg: -------------------------------------------------------------------------------- 1 | Oval 28Created with Sketch. -------------------------------------------------------------------------------- /src/img/comment_hover.svg: -------------------------------------------------------------------------------- 1 | comment_hoverCreated with Sketch. -------------------------------------------------------------------------------- /src/img/default-speeddialitem-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/default-speeddialitem-icon.png -------------------------------------------------------------------------------- /src/img/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/icon_128.png -------------------------------------------------------------------------------- /src/img/icon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/icon_48.png -------------------------------------------------------------------------------- /src/img/icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/icon_64.png -------------------------------------------------------------------------------- /src/img/tour-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/tour-add.png -------------------------------------------------------------------------------- /src/img/tour-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/tour-edit.png -------------------------------------------------------------------------------- /src/img/tour-more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jariz/tabbie/ed34b27e88720ddb69d863bb7bbac143036c081f/src/img/tour-more.png -------------------------------------------------------------------------------- /src/item-card.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 28 | -------------------------------------------------------------------------------- /src/item-column.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 216 | 243 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tabbie - Material New Tab Page", 3 | "short_name": "Tabbie", 4 | "description": "A material, customizable, and hackable new tab extension.", 5 | "version": "1.1.3", 6 | "incognito": "split", 7 | "chrome_url_overrides": { 8 | "newtab": "tab.html" 9 | }, 10 | "permissions": [ 11 | "identity", 12 | "https://lobste.rs/*", 13 | "https://api.behance.net/*" 14 | ], 15 | "optional_permissions": [ 16 | "http://*/", 17 | "https://*/", 18 | 19 | "https://www.reddit.com/", 20 | "https://oauth.reddit.com/", 21 | "https://api.producthunt.com/", 22 | "http://storage.googleapis.com/", 23 | "https://feedly.com/", 24 | 25 | "management", 26 | "bookmarks", 27 | "chrome://favicon/*", 28 | "topSites", 29 | "sessions", 30 | "tabs" 31 | ], 32 | "oauth2": { 33 | "client_id": "2445668283-ovnfq4m0m03tmvkhh4rmj4qtu02l5tic.apps.googleusercontent.com", 34 | "scopes": [ 35 | "https://www.googleapis.com/auth/gmail.modify", 36 | "profile" 37 | ] 38 | }, 39 | "manifest_version": 2, 40 | "content_security_policy": "script-src 'self' https://apis.google.com/ https://accounts.google.com https://oauth.googleusercontent.com https://ssl.gstatic.com 'unsafe-eval'; object-src 'self'", 41 | "icons": { 42 | "48": "img/icon_48.png", 43 | "64": "img/icon_64.png", 44 | "128": "img/icon_128.png" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/recently-item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 38 | -------------------------------------------------------------------------------- /src/sass/feeditem.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 100%; 3 | margin-bottom:10px; 4 | 5 | padding: 0 0 0 10px; 6 | border-bottom: 1px solid #d2d2d2; 7 | display:block; 8 | font-size:12px; 9 | color:black; 10 | position: relative; 11 | 12 | box-sizing: border-box; 13 | } 14 | 15 | h1 { 16 | font-size:16px; 17 | font-family: 'Roboto Slab', 'Roboto', sans-serif; 18 | 19 | .domain { 20 | font-size:12px; 21 | color:#bdbdbd; 22 | font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; 23 | } 24 | } 25 | 26 | a { 27 | color: rgba(0, 0, 0, 0.78); 28 | text-decoration: none; 29 | } -------------------------------------------------------------------------------- /src/sass/screen.scss: -------------------------------------------------------------------------------- 1 | /* Welcome to Compass. 2 | * In this file you should write your main styles. (or centralize your imports) 3 | * Import this file using the following HTML or equivalent: 4 | * */ 5 | 6 | //@import "compass/reset"; 7 | @import "compass/css3/transform"; 8 | 9 | @font-face { 10 | font-family: 'Roboto'; 11 | src: url('../font/Roboto-Regular-webfont.woff') format('woff'); 12 | font-weight: normal; 13 | font-style: normal; 14 | } 15 | 16 | @font-face { 17 | font-family: 'Roboto Slab'; 18 | src: url('../font/RobotoSlab-Regular-webfont.ttf') format('woff'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | 23 | body { 24 | font-family: Roboto, sans-serif; 25 | font-size:16px; 26 | background: #eeeeee; 27 | overflow-x:hidden; 28 | overflow-y:auto; 29 | margin:0; 30 | padding:0; 31 | } 32 | 33 | * { 34 | box-sizing: border-box; 35 | } 36 | 37 | .fabs { 38 | position: fixed; 39 | bottom:0; 40 | right:0; 41 | height: 210px; 42 | width: 80px; 43 | z-index: 5; 44 | 45 | .fab-add { 46 | position: absolute; 47 | right:10px; 48 | bottom:20px; 49 | } 50 | 51 | .fab-update-wrapper { 52 | position: absolute; 53 | right: 19px; 54 | bottom: 211px; 55 | .fab-update { 56 | overflow: inherit; 57 | background-color: #FFC107; 58 | } 59 | } 60 | 61 | .fab-edit-wrapper { 62 | position: absolute; 63 | right: 19px; 64 | bottom: 94px; 65 | 66 | .fab-edit { 67 | overflow: inherit; 68 | background-color: #8BC34A; 69 | &.active { 70 | //ignore transitions 71 | opacity:1 !important; 72 | transform:translateZ(0) !important; 73 | background-color: #558B2F; 74 | } 75 | } 76 | } 77 | 78 | .fab-about-wrapper { 79 | position: absolute; 80 | right: 19px; 81 | bottom: 151px; 82 | 83 | .fab-about { 84 | overflow: inherit; 85 | background-color: #2196F3; 86 | } 87 | } 88 | } 89 | 90 | .fab-anim-add { 91 | right: 10px; 92 | bottom: 20px; 93 | } 94 | 95 | .fab-anim-update { 96 | right: 19px; 97 | bottom: 211px; 98 | background-color: #FFC107; 99 | width:40px; 100 | height:40px; 101 | } 102 | 103 | .fab-anim-about { 104 | right: 19px; 105 | bottom: 151px; 106 | background-color: #2196F3; 107 | width:40px; 108 | height:40px; 109 | } 110 | 111 | .fab-edit { 112 | /deep/ core-icon { 113 | transition:transform 250ms; 114 | } 115 | 116 | &.active /deep/ core-icon { 117 | transform:rotate(360deg); 118 | } 119 | } 120 | 121 | #about /deep/ .back-toolbar { 122 | background-color: #2196F3; 123 | } 124 | 125 | #update /deep/ .back-toolbar { 126 | background-color: #FFC107; 127 | } 128 | 129 | #searchdialog /deep/ .back-toolbar { 130 | background-color: #EEEEEE; 131 | } 132 | 133 | #searchdialog /deep/ paper-icon-button { 134 | color:#000; 135 | } 136 | 137 | #searchdialog /deep/ .card-bar .search-progress { 138 | position: absolute; 139 | top:15px; 140 | right:15px; 141 | } 142 | 143 | #searchdialog /deep/ .card-bar .search-bar { 144 | width:100%; 145 | } 146 | 147 | #searchdialog /deep/ .card .content { 148 | padding:0; 149 | } 150 | 151 | html /deep/ { 152 | paper-button.primary { 153 | background: #4285f4; 154 | color: #fff; 155 | } 156 | 157 | paper-button[autofocus] { 158 | color: #03a9f4; 159 | } 160 | } 161 | 162 | .ghost { 163 | background:#eee; 164 | } 165 | 166 | .column-holder { 167 | width: calc(100% - 85px); 168 | height:100% !important; //<< packery does weird things with the height that i don't understand, so force the height 169 | 170 | .grid-sizer { 171 | position: absolute; 172 | width: 25%; 173 | min-width: 250px; 174 | height:50%; 175 | } 176 | } 177 | 178 | .no-columns-container { 179 | display:flex; 180 | transition:500ms opacity; 181 | width:100%; 182 | height:100%; 183 | position:absolute; 184 | top:0; 185 | left:0; 186 | opacity:0; 187 | 188 | div { 189 | margin:auto; 190 | color: #757575; 191 | text-align: center; 192 | width: 554px; 193 | padding-top:140px; 194 | height: 355px; 195 | background-image:url('../img/chrome.png') 196 | } 197 | 198 | } 199 | 200 | #about { 201 | .about { 202 | h3 { 203 | font-weight: lighter; 204 | color: #424242; 205 | font-size: 22px; 206 | display: block; 207 | margin: 0; 208 | core-icon { 209 | margin-right: 10px; 210 | } 211 | } 212 | h4 { 213 | text-transform: uppercase; 214 | } 215 | p { 216 | text-transform: uppercase; 217 | color: #9E9E9E; 218 | } 219 | h5 { 220 | color: #9e9e9e; 221 | font-size: 14px; 222 | font-weight: lighter; 223 | } 224 | 225 | /* NO FLEX, ZONE! */ 226 | paper-item /deep/ .button-content { 227 | display: block; 228 | } 229 | 230 | .luv { 231 | width: 14px; 232 | } 233 | } 234 | 235 | .disclaimer { 236 | h5 { 237 | margin-bottom: 0; 238 | } 239 | .logo { 240 | color: #9e9e9e; 241 | float: right; 242 | padding-left: 10px; 243 | padding-top: 13px; 244 | core-icon { 245 | width: 18px; 246 | position: relative; 247 | top: -2px; 248 | } 249 | } 250 | } 251 | } 252 | 253 | .settings { 254 | position: fixed; 255 | right:0; 256 | top:0; 257 | z-index:9; 258 | transition: background 500ms, box-shadow 500ms, width 0ms 500ms; 259 | border-bottom-left-radius: 3px; 260 | padding:5px 9px; 261 | width: 65px; 262 | height: 55px; 263 | overflow: hidden; 264 | 265 | &:hover, &.force { 266 | width: 195px; 267 | transition-delay:0ms; 268 | box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37); 269 | background: #fff; 270 | 271 | .app-drawer-button, .bookmarks-drawer-button, .top-drawer-button, .recently-drawer-button { 272 | opacity:1; 273 | -webkit-animation: enter 500ms; 274 | } 275 | 276 | .more { 277 | opacity:0; 278 | } 279 | } 280 | 281 | &.force { 282 | transition: width 300ms; 283 | width:400px; 284 | } 285 | 286 | /deep/ core-icon { 287 | color: #424242; 288 | width:28px; 289 | height:28px; 290 | } 291 | 292 | .app-drawer-button, .bookmarks-drawer-button, .top-drawer-button, .recently-drawer-button { 293 | transition:opacity 500ms; 294 | opacity:0; 295 | float:right; 296 | -webkit-animation: leave 500ms; 297 | } 298 | 299 | .more { 300 | position: absolute; 301 | right:12px; 302 | top:5px; 303 | transition-delay: 250ms; 304 | transition: opacity 250ms ; 305 | opacity:1; 306 | } 307 | } 308 | 309 | @-webkit-keyframes enter { 310 | 0% { 311 | transform: translateX(24px) rotate(180deg); 312 | } 313 | 314 | 100% { 315 | transform: translateX(0) rotate(0deg); 316 | } 317 | } 318 | 319 | @-webkit-keyframes leave { 320 | 0% { 321 | transform: translateX(0) rotate(0deg); 322 | } 323 | 324 | 100% { 325 | transform: translateX(24px) rotate(180deg); 326 | } 327 | } 328 | 329 | paper-tabs.blue::shadow #selectionBar { 330 | background-color: #3F51B5; 331 | } 332 | 333 | paper-tabs.blue paper-tab::shadow #ink { 334 | color: #3F51B5; 335 | } 336 | 337 | app-drawer.bookmarks { 338 | core-animated-pages { 339 | height:calc(100% - 48px); 340 | div { 341 | height:100%; 342 | width:100%; 343 | overflow-y:auto; 344 | } 345 | } 346 | } 347 | 348 | .resize-preview { 349 | position: absolute; 350 | z-index:200; 351 | border:3px dotted #424242; 352 | min-width: 250px; 353 | } 354 | 355 | html /deep/ paper-button.choice { 356 | width: 50%; 357 | height: 150px; 358 | padding-top: 54px; 359 | transition:background-color 500ms, color 500ms; 360 | } 361 | -------------------------------------------------------------------------------- /src/tab.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tabbie 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 |

We're back with Tabbie 1.1, introducing the new speed dial, producthunt grid redesign and a whole bunch of bug fixes
Full changelog:

94 | 95 | 102 | 103 |

Huge shoutout to opensource contributors SebiH, erenhatirnaz and runofthemill

104 | 105 | 106 | Don't show this anymore 107 |
108 | 109 |
110 |
111 |

Welcome to Tabbie!

112 |

You have not added any columns yet :(
113 | Add one by clicking on the plus button at the bottom-right.

114 |
115 |
116 | 117 | 118 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 |
165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 |
176 | 177 | 178 | 179 | 180 | 181 | 191 | 192 | 193 | 194 | 195 |
196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 |
204 | 205 |
206 |
207 |
208 | 209 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /src/tabbie-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 50 | 58 | -------------------------------------------------------------------------------- /src/time-ago.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 10 | 11 | 54 | -------------------------------------------------------------------------------- /src/tour-step.html: -------------------------------------------------------------------------------- 1 | 2 | 55 | 100 | --------------------------------------------------------------------------------