├── .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 ` ` just write down - as usual - :
166 | ```html
167 | {{item.title}}
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 ` [](https://gitter.im/jariz/tabbie?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://travis-ci.org/jariz/tabbie) [](https://chrome.google.com/webstore/detail/tabbie/kckhddfnffeofnfjcpdffpeiljicclbd)
13 |
14 | 
15 | 
16 | 
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 "
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 |
2 |
3 |
4 |
5 |
6 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
128 |
129 |
--------------------------------------------------------------------------------
/src/app-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
23 |
24 |
25 |
26 | {{name}}
27 |
28 |
--------------------------------------------------------------------------------
/src/auto-suggestions.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
72 |
73 |
74 |
75 |
76 |
{{suggestion.website | formatWebsite}}
77 |
{{suggestion.title}}
78 |
{{suggestion.description}}
79 |
80 |
81 |
82 |
83 |
84 |
85 |
Unable to get your sites. Is your internet down?
86 |
87 |
88 |
89 |
90 |
91 |
92 |
144 |
--------------------------------------------------------------------------------
/src/bookmark-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
33 |
34 |
35 |
36 | {{title}}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {{title}}
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
{{column.name}}
63 |
64 |
65 |
66 |
67 |
68 |
69 | This will remove the column from your overview, you will have to add it again if you want it back.
70 |
71 | No
72 | Yes
73 |
74 |
75 |
76 |
110 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
98 |
99 |
100 |
101 |
102 |
{{item.name}}
103 |
104 |
105 |
106 |
107 | {{item.stats.appreciations}}
108 |
109 |
110 |
111 | {{item.stats.comments}}
112 |
113 |
114 |
115 | {{item.stats.views}}
116 |
117 |
118 |
119 |
{{item.owners[0].first_name}} {{item.owners[0].last_name}}
120 |
121 |
122 |
123 |
124 |
125 |
126 |
130 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
31 |
32 | RECENT
33 | ALL
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
69 |
70 |
71 |
72 |
73 |
74 |
{{item.title}}
75 |
{{item['dc:creator']}}
76 |
77 |
78 |
79 |
80 |
81 |
82 |
92 |
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 |
2 |
3 |
4 |
5 |
6 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
105 |
106 |
107 |
108 |
109 |
110 | {{item.title}}
111 |
112 |
113 |
{{item.article | filterHtml | summarize}}
114 |
115 |
125 |
126 |
127 |
128 |
129 |
183 |
--------------------------------------------------------------------------------
/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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
100 |
101 |
102 |
103 |
104 |
105 |
{{item.title}}
106 |
107 |
108 |
109 | {{item.likes_count}}
110 |
111 |
112 |
113 | {{item.comments_count}}
114 |
115 |
116 |
117 | {{item.views_count}}
118 |
119 |
120 |
121 |
{{item.user.name}}
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
139 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
Show language
12 |
13 |
14 |
24 |
25 |
26 |
Show most popular this
27 |
28 |
29 |
34 |
35 |
36 |
37 |
38 |
39 |
42 |
--------------------------------------------------------------------------------
/src/columns/github/github-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
15 |
16 |
17 |
{{item.description}}
18 |
19 |
20 | {{item.stargazers_count}}
21 |
22 |
23 |
24 |
25 |
26 |
27 | {{item.language}}
28 |
29 |
30 |
31 |
32 |
33 |
36 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
25 |
26 |
Sign in with Google
27 |
28 |
29 |
42 |
--------------------------------------------------------------------------------
/src/columns/gmail/gmail-dialog.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
36 |
37 |
38 |
You are logged in as
39 |
40 | {{config.user.displayName}}
41 |
{{config.user.emailAddress}}
42 |
43 |
44 |
45 |
46 |
47 |
You are logged in as
48 |
49 | Not logged in
50 |
51 |
52 |
53 | Log out
54 |
55 |
56 |
64 |
--------------------------------------------------------------------------------
/src/columns/gmail/gmail-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
71 |
72 |
73 | {{item.from.name[0]}}
74 |
75 |
76 |
77 | {{item.date}}
78 |
79 | {{item.from.name}}
80 |
81 | ({{item.amount}})
82 |
83 |
84 | {{item.subject}}
85 |
86 | {{item.snippet}}
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
17 |
25 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
23 |
--------------------------------------------------------------------------------
/src/columns/lobsters/lobsters-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
30 |
31 |
39 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | List
12 |
13 |
14 |
15 | Grid
16 |
17 |
18 |
19 |
20 |
29 |
--------------------------------------------------------------------------------
/src/columns/producthunt/ph-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
77 |
91 |
92 |
--------------------------------------------------------------------------------
/src/columns/producthunt/ph-thumb.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | {{item.votes_count}}
135 |
136 |
137 |
{{item.name}}
138 |
{{item.tagline}}
139 |
140 |
141 |
144 |
145 |
146 |
147 |
148 |
149 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
6 |
23 |
26 |
27 |
40 |
--------------------------------------------------------------------------------
/src/columns/pushbullet/pushbullet-dialog.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
36 |
37 |
38 |
You are logged in as
39 |
40 | {{config.user.name}}
41 |
{{config.user.email}}
42 |
43 |
44 |
45 |
46 |
47 |
You are logged in as
48 |
49 | Not logged in
50 |
51 |
52 |
53 | Log out
54 |
55 |
56 |
64 |
--------------------------------------------------------------------------------
/src/columns/pushbullet/pushbullet-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
60 |
61 |
62 |
63 | From
64 | {{item.from.name}}
65 | Yourself
66 |
67 |
68 |
69 |
70 |
71 |
72 | {{item.title}}
73 |
74 | {{item.body}}
75 |
76 |
77 | {{item.file_name}}
78 |
79 |
80 | {{item.url}}
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
107 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
listing
76 |
77 |
78 |
83 |
84 |
85 |
86 |
87 |
88 |
93 |
94 |
95 |
96 |
97 | Log in
98 |
99 |
100 | Log out
101 |
102 |
103 |
104 |
126 |
--------------------------------------------------------------------------------
/src/columns/reddit/reddit-error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 | Reddit has revoked your session, please log in again.
18 |
19 |
--------------------------------------------------------------------------------
/src/columns/reddit/reddit-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
30 |
31 |
32 |
33 |
34 |
43 |
44 |
55 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 | {{ website.name }}
168 |
169 |
170 |
171 | {{ website.url }}
172 |
173 |
174 |
175 |
176 |
177 |
180 |
181 |
182 |
183 |
184 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
Create your first entry by clicking the button on the bottom right corner.
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
226 |
227 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
247 |
248 |
252 |
253 |
256 |
257 |
258 |
259 |
366 |
367 |
--------------------------------------------------------------------------------
/src/columns/speeddial/speed-dial-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
28 |
29 |
30 |
31 |
32 | {{name}}
33 |
34 |
35 |
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 |
2 |
3 |
4 |
19 |
20 |
21 |
32 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
6 |
7 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | {{heading}}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
119 |
--------------------------------------------------------------------------------
/src/img/arrow_up.svg:
--------------------------------------------------------------------------------
1 | Rectangle 24 Created 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 28 Created with Sketch.
--------------------------------------------------------------------------------
/src/img/comment_hover.svg:
--------------------------------------------------------------------------------
1 | comment_hover Created 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 |
4 |
19 | {{title}}
20 | {{description}}
21 |
22 |
28 |
--------------------------------------------------------------------------------
/src/item-column.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
Unable to update {{column.name}}. Is your internet down?
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
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 |
6 |
27 |
28 |
29 | {{title}}
30 |
31 |
32 |
33 |
34 | {{tab_count}} Tabs
35 |
36 |
37 |
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 |
55 |
56 | Looking for your bookmarks, apps and topsites?
57 | You can access them from this handy menu here.
58 | Got it
59 | End tour
60 |
61 |
62 | Start by adding a column of your choice
63 | The most important part of Tabbie are its columns. Add one here.
64 | Got it
65 | End tour
66 |
67 |
68 | Make yourself at home.
69 | You can modify columns to fit your needs as well.
70 |
71 | Move a column by holding down your mouse button on the move icon.
72 | Resize a column by dragging its bottom right handle.
73 | Customize a column by clicking on the settings icon.
74 | Remove a column by clicking on its delete icon.
75 | Rename a column by clicking on it's name while editing.
76 |
77 | Got it
78 | End tour
79 |
80 |
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 |
96 | (finally) fixed problem with custom columns going to the next page instead of refreshing
97 | The often requested speeddial is added! Add your favorite websites to this column.
98 | Fixed designernews domain related issues
99 | Redesigned ProductHunt's grid (enable in column settings)
100 | Temporarily disabled animations on menu drawer because of it not showing anymore after opening it the 2nd time.
101 |
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 |
119 |
120 |
121 | Development and design
122 | Jari Zwarts
123 |
124 |
Made with
125 |
126 | Polymer {{polymerVersion}}
127 |
128 |
129 | CoffeeScript
130 |
131 |
132 | Vanilla.js
133 |
134 |
Links
135 |
136 | Official site
137 |
138 |
139 | Project page
140 |
141 |
142 | Report a bug
143 |
144 |
145 | Request a column/feature
146 |
147 |
148 |
149 | Tabbie {{version}}
150 |
151 |
Tabbie is opensource and will never contain ads. Please consider contributing if you want to see more features.
152 | Tabbie is a product of pure .
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 | RECENT
184 | ALL
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
208 |
209 |
210 |
211 | Undo
212 |
213 |
214 |
215 |
216 |
217 |
--------------------------------------------------------------------------------
/src/tabbie-dialog.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
34 |
35 |
36 |
37 |
38 |
49 |
50 |
58 |
--------------------------------------------------------------------------------
/src/time-ago.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
54 |
--------------------------------------------------------------------------------
/src/tour-step.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
44 |
54 |
55 |
100 |
--------------------------------------------------------------------------------