├── .gitignore ├── src ├── pages │ ├── popup │ │ ├── list-view.css │ │ ├── app.css │ │ ├── settings.css │ │ ├── search.css │ │ ├── index.js │ │ ├── item-view.css │ │ ├── app.js │ │ ├── test.js │ │ ├── ListView.js │ │ ├── Search.js │ │ ├── dashboard.css │ │ ├── ItemView.js │ │ ├── Settings.js │ │ └── Dashboard.js │ ├── background │ │ ├── reducers.js │ │ ├── reducers │ │ │ ├── animations.js │ │ │ ├── settings.js │ │ │ └── bookmarks.js │ │ ├── localStorage.js │ │ ├── index.js │ │ ├── store.js │ │ └── actions.js │ └── content │ │ ├── index.css │ │ └── index.js ├── assets │ ├── list.png │ ├── list128.png │ └── nothing.png └── manifest.json ├── images ├── cover.png ├── cover2.png ├── image1.png └── image2.png ├── logo-readme.png ├── coverage ├── lcov-report │ ├── sort-arrow-sprite.png │ ├── prettify.css │ ├── background │ │ ├── index.html │ │ └── actions.js.html │ ├── index.html │ ├── popup │ │ ├── App.js.html │ │ ├── ListView.js.html │ │ ├── index.html │ │ ├── Search.js.html │ │ ├── ItemView.js.html │ │ ├── Settings.js.html │ │ └── Dashboard.js.html │ ├── sorter.js │ ├── base.css │ └── prettify.js ├── lcov.info ├── clover.xml └── coverage-final.json ├── .babelrc ├── LICENSE ├── README.md ├── webpack.config.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | Dist/ 3 | dist.zip 4 | logo/ -------------------------------------------------------------------------------- /src/pages/popup/list-view.css: -------------------------------------------------------------------------------- 1 | .empty-list { 2 | margin: 5px 3px; 3 | } 4 | -------------------------------------------------------------------------------- /images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierroberto/Pin-Tabs/HEAD/images/cover.png -------------------------------------------------------------------------------- /logo-readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierroberto/Pin-Tabs/HEAD/logo-readme.png -------------------------------------------------------------------------------- /images/cover2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierroberto/Pin-Tabs/HEAD/images/cover2.png -------------------------------------------------------------------------------- /images/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierroberto/Pin-Tabs/HEAD/images/image1.png -------------------------------------------------------------------------------- /images/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierroberto/Pin-Tabs/HEAD/images/image2.png -------------------------------------------------------------------------------- /src/assets/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierroberto/Pin-Tabs/HEAD/src/assets/list.png -------------------------------------------------------------------------------- /src/pages/popup/app.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | margin: 15px 20px; 3 | margin-bottom: 80px; 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/list128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierroberto/Pin-Tabs/HEAD/src/assets/list128.png -------------------------------------------------------------------------------- /src/assets/nothing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierroberto/Pin-Tabs/HEAD/src/assets/nothing.png -------------------------------------------------------------------------------- /coverage/lcov-report/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierroberto/Pin-Tabs/HEAD/coverage/lcov-report/sort-arrow-sprite.png -------------------------------------------------------------------------------- /src/pages/popup/settings.css: -------------------------------------------------------------------------------- 1 | .fa-check:hover { 2 | color: #33c3f0; 3 | } 4 | 5 | label { 6 | margin-right: 10px; 7 | } 8 | 9 | ul { 10 | list-style-type: none; 11 | line-height: 3.5; 12 | padding: 0; 13 | } 14 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-2" 6 | ], 7 | "plugins": [ 8 | "transform-class-properties", 9 | "transform-async-to-generator", 10 | "react-html-attrs" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/background/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import bookmark from "./reducers/bookmarks"; 3 | import settings from "./reducers/settings"; 4 | import animation from "./reducers/animations"; 5 | 6 | export default combineReducers({ 7 | bookmark: bookmark, 8 | settings: settings, 9 | animation: animation 10 | }); 11 | -------------------------------------------------------------------------------- /src/pages/popup/search.css: -------------------------------------------------------------------------------- 1 | .hide { 2 | visibility: hidden; 3 | } 4 | 5 | input { 6 | border: none; 7 | } 8 | 9 | input:focus { 10 | outline: none; 11 | border: none; 12 | margin-right: 5px; 13 | margin-bottom: 10px; 14 | padding: 5px 0 10px; 15 | font-size: 16px; 16 | transition: padding 0.5s; 17 | transition: font-size 0.5s; 18 | } 19 | 20 | .show { 21 | visibility: visible; 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/background/reducers/animations.js: -------------------------------------------------------------------------------- 1 | const defaultState = { 2 | buttonCog: false 3 | }; 4 | 5 | const animation = (state = defaultState, action) => { 6 | switch (action.type) { 7 | case "TOGGLE-COG": 8 | return { 9 | ...state, 10 | buttonCog: action.buttonCog 11 | }; 12 | case "TOGGLE-SEARCH": 13 | return { 14 | ...state, 15 | toggleSearch: action.toggleSearch 16 | }; 17 | default: 18 | return state; 19 | } 20 | }; 21 | 22 | export default animation; 23 | -------------------------------------------------------------------------------- /src/pages/background/localStorage.js: -------------------------------------------------------------------------------- 1 | export const saveState = state => { 2 | try { 3 | const serializedState = JSON.stringify(state); 4 | localStorage.setItem("state", serializedState); 5 | } catch (e) { 6 | console.error("Error saving state", e); 7 | } 8 | }; 9 | 10 | export const loadState = () => { 11 | try { 12 | const serializedState = localStorage.getItem("state"); 13 | if (serializedState === null) { 14 | return undefined; 15 | } 16 | return JSON.parse(serializedState); 17 | } catch (e) {} 18 | }; 19 | -------------------------------------------------------------------------------- /src/pages/popup/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import { Store } from "react-chrome-redux"; 5 | import App from "./App"; 6 | 7 | const store = new Store({ 8 | portName: "COUNTING" 9 | }); 10 | 11 | store.ready().then(() => { 12 | const mountNode = document.createElement("div"); 13 | document.body.appendChild(mountNode); 14 | 15 | ReactDOM.render( 16 | 17 | 18 | , 19 | mountNode 20 | ); 21 | }); 22 | -------------------------------------------------------------------------------- /src/pages/background/index.js: -------------------------------------------------------------------------------- 1 | import store from "./store"; 2 | 3 | store.subscribe(() => { 4 | store.getState().bookmark.tabs.map(infoTab => { 5 | chrome.alarms.create(infoTab.tab[0].url, { 6 | when: infoTab.expiry + store.getState().settings.expireDate 7 | }); 8 | }); 9 | }); 10 | 11 | chrome.alarms.onAlarm.addListener(function(data) { 12 | const elementExpired = store.getState().bookmark.tabs.filter(el => { 13 | return el.tab[0].url === data.name; 14 | }); 15 | store.dispatch({ type: "EXPIRY", url: elementExpired[0].tab[0].url }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/pages/popup/item-view.css: -------------------------------------------------------------------------------- 1 | .content-container { 2 | display: flex; 3 | align-items: end; 4 | justify-content: space-between; 5 | padding: 15px 0; 6 | border-bottom: 1px solid #f2f2f2; 7 | } 8 | 9 | .content-container:last-child { 10 | border-bottom: 0; 11 | padding-bottom: 0; 12 | } 13 | 14 | .col-1-1 { 15 | width: 16px; 16 | height: 16px; 17 | margin-right: 5px; 18 | } 19 | 20 | .col-1-2 { 21 | color: #099ecf; 22 | } 23 | 24 | .icon { 25 | width: 16px; 26 | height: 16px; 27 | } 28 | 29 | .row { 30 | display: flex; 31 | } 32 | 33 | .title { 34 | text-decoration: none; 35 | } 36 | 37 | .warning { 38 | font-size: 10px; 39 | } 40 | -------------------------------------------------------------------------------- /coverage/lcov-report/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /src/pages/background/reducers/settings.js: -------------------------------------------------------------------------------- 1 | const defaultState = { 2 | button: false, 3 | buttonHystory: true, 4 | expireDate: 86400000 // 1 day 5 | }; 6 | 7 | const settings = (state = defaultState, action) => { 8 | switch (action.type) { 9 | case "TOGGLE-BUTTON": 10 | return { 11 | ...state, 12 | button: action.toggleButton 13 | }; 14 | case "TOGGLE-BUTTON-HISTORY": 15 | return { 16 | ...state, 17 | buttonHistory: action.toggleButtonHistory 18 | }; 19 | case "UPDATE-DATE": 20 | return { 21 | ...state, 22 | expireDate: action.expireDate 23 | }; 24 | } 25 | return state; 26 | }; 27 | 28 | export default settings; 29 | -------------------------------------------------------------------------------- /src/pages/popup/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import "./app.css"; 4 | import ListView from "./ListView.js"; 5 | import Settings from "./Settings"; 6 | import Dashboard from "./Dashboard"; 7 | import { Route, BrowserRouter as Router, Switch } from "react-router-dom"; 8 | 9 | class App extends React.Component { 10 | render() { 11 | return ( 12 | 13 |
14 | 15 | 16 | 17 | 18 |
19 |
20 | ); 21 | } 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /src/pages/background/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from "redux"; 2 | import { wrapStore, alias } from "react-chrome-redux"; 3 | import { createLogger } from "redux-logger"; 4 | import thunk from "redux-thunk"; 5 | import reducer from "./reducers"; 6 | import throttle from "lodash/throttle"; 7 | import { saveState, loadState } from "./localStorage"; 8 | const store = createStore(reducer, loadState()); 9 | 10 | store.subscribe( 11 | throttle(() => { 12 | saveState({ 13 | bookmark: store.getState().bookmark, 14 | settings: store.getState().settings, 15 | animation: store.getState().animation 16 | }); 17 | }), 18 | 1000 19 | ); 20 | 21 | wrapStore(store, { 22 | portName: "COUNTING" 23 | }); 24 | 25 | export default store; 26 | -------------------------------------------------------------------------------- /src/pages/popup/test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as actions from "../background/actions"; 3 | import { shallow } from "enzyme"; 4 | import Adapter from "enzyme-adapter-react-15"; 5 | import App from "./App"; 6 | import { configure } from "enzyme"; 7 | 8 | configure({ adapter: new Adapter() }); 9 | describe("", () => { 10 | it("renders 1 component", () => { 11 | const component = shallow(); 12 | expect(component).toHaveLength(1); 13 | }); 14 | }); 15 | describe("actions", () => { 16 | it("schould add a new tab", () => { 17 | const urlList = "http://www.google.com"; 18 | const add = { 19 | type: "ADD", 20 | urlList, 21 | expiry: new Date().getTime() 22 | }; 23 | expect(actions.addBookmark(urlList)).toEqual(add); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/pages/popup/ListView.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./list-view.css"; 3 | import randomId from "uuid/v4"; 4 | import ItemView from "./ItemView"; 5 | 6 | export default class ListView extends React.Component { 7 | renderTabs() { 8 | if (this.props.tabs.length < 1) { 9 | return

No tabs to show 😔

; 10 | } 11 | return this.props.tabs.map((tab) => { 12 | return ( 13 | 23 | ); 24 | }); 25 | } 26 | 27 | render() { 28 | return
{this.renderTabs()}
; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pier Roberto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Pin tabs - Tab Manager", 4 | "short_name": "Keep tabs history", 5 | "version": "1.3.1", 6 | "description": "Pin Tabs allows the user to pin one or more tabs for short periods, keep track of the expired tabs in the history and save energy", 7 | "browser_action": { 8 | "default_title": "Show the list of pinned tabs", 9 | "default_popup": "pages/popup.html" 10 | }, 11 | "author": "Pier Roberto Lucisano", 12 | "background": { 13 | "page": "pages/background.html" 14 | }, 15 | "content_scripts": [ 16 | { 17 | "matches": [""], 18 | "js": ["pages/index.js"], 19 | "run_at": "document_end", 20 | "exclude_matches": ["https://www.youtube.com/*"] 21 | } 22 | ], 23 | "permissions": [ 24 | "tabs", 25 | "activeTab", 26 | "http://*/*", 27 | "https://*/*", 28 | "storage", 29 | "alarms" 30 | ], 31 | "minimum_chrome_version": "60", 32 | "icons": { 33 | "16": "assets/list.png", 34 | "48": "assets/list.png", 35 | "128": "assets/list128.png" 36 | }, 37 | "content_security_policy": "script-src 'self' https://ssl.google-analytics.com; object-src 'self'" 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | [![license](https://badgen.now.sh/badge/license/MIT)](./LICENSE) 6 | 7 | A Google Chrome extension that allows the user to pin browser tabs and keep track of them for short periods of time. 8 | You can add tabs to your favourites either by clicking on the extension's icon, or through a keyboard shortcut (mac: `CTRL + OPT + s`, Windows: `ALT + s`, Linux: `CTRL + ALT + s`). 9 | 10 | Pinned tabs expire after a user-set timing (from 1 hour to 1 week). The last few expired tabs are still accessible through chronology. 11 | 12 |

13 | 14 |

15 | 16 | ## Installation 17 | 18 | * Clone the repo and cd into it 19 | * Run ```npm install``` 20 | * Run ```npm run build``` 21 | 22 | ## Tech Stack 23 | 24 | * [React](https://reactjs.org/) 25 | * [Redux](https://redux.js.org/) 26 | * [React-Chrome-Redux](https://github.com/tshaddix/react-chrome-redux/wiki) 27 | 28 | ## License 29 | 30 | Pin Tabs is licensed under the [MIT](https://opensource.org/licenses/mit-license.php) license. 31 | -------------------------------------------------------------------------------- /src/pages/background/actions.js: -------------------------------------------------------------------------------- 1 | //BOOKMARKS ACTIONS 2 | export const refreshBookmark = (data, time) => ({ 3 | type: "REFRESH", 4 | urlList: data, 5 | expiry: time 6 | }); 7 | 8 | export const deleteAllBookmark = () => ({ 9 | type: "DELETE-ALL" 10 | }); 11 | 12 | export const deleteOneBookmark = url => ({ 13 | type: "DELETE-ONE", 14 | url: url 15 | }); 16 | 17 | export const addBookmark = url => ({ 18 | type: "ADD", 19 | urlList: url, 20 | expiry: new Date().getTime() 21 | }); 22 | 23 | export const addFromButton = flag => ({ 24 | type: "DELETE-ONE", 25 | addFromButton: flag 26 | }); 27 | 28 | export const searchBookmark = text => ({ 29 | type: "SEARCH", 30 | textSearched: text 31 | }); 32 | 33 | export const emptySearch = () => ({ 34 | type: "EMPTY-SEARCH" 35 | }); 36 | 37 | //SETTINGS actions 38 | 39 | export const toggleButton = flag => ({ 40 | type: "TOGGLE-BUTTON", 41 | toggleButton: flag 42 | }); 43 | 44 | export const expireDate = date => ({ 45 | type: "UPDATE-DATE", 46 | expireDate: date 47 | }); 48 | 49 | export const toggleButtonHistory = flag => ({ 50 | type: "TOGGLE-BUTTON-HISTORY", 51 | toggleButtonHistory: flag 52 | }); 53 | 54 | // ANIMATION ACTIONS 55 | 56 | export const buttonCog = flag => ({ 57 | type: "TOGGLE-COG", 58 | buttonCog: flag 59 | }); 60 | 61 | export const toggleSearch = classValue => ({ 62 | type: "TOGGLE-SEARCH", 63 | toggleSearch: classValue 64 | }); 65 | -------------------------------------------------------------------------------- /src/pages/popup/Search.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import "./search.css"; 4 | import { 5 | searchBookmark, 6 | emptySearch, 7 | toggleSearch 8 | } from "../background/actions"; 9 | import ListView from "./ListView"; 10 | class Search extends React.Component { 11 | findTab = () => { 12 | if (this.props.bookmark.search) { 13 | return ( 14 |
15 |

Search results

16 | 22 |
23 | ); 24 | } 25 | }; 26 | 27 | componentDidMount() { 28 | this.props.resetSearch(); 29 | } 30 | 31 | // ================== RENDERING 32 | render() { 33 | return ( 34 |
35 | this.props.searchString(e.target.value)} 38 | placeholder="search..." 39 | /> 40 | {this.findTab()} 41 |
42 | ); 43 | } 44 | } 45 | 46 | const mapStateToProps = state => ({ 47 | bookmark: state.bookmark, 48 | settings: state.settings, 49 | animation: state.animation 50 | }); 51 | 52 | const mapDispatchToProps = dispatch => ({ 53 | searchString: text => dispatch(searchBookmark(text)), 54 | resetSearch: () => dispatch(emptySearch()), 55 | displaySearch: classValue => dispatch(toggleSearch(classValue)) 56 | }); 57 | 58 | export default connect( 59 | mapStateToProps, 60 | mapDispatchToProps 61 | )(Search); 62 | -------------------------------------------------------------------------------- /src/pages/content/index.css: -------------------------------------------------------------------------------- 1 | .circle { 2 | position: fixed; 3 | right: 20px; 4 | bottom: 20px; 5 | height: 50px; 6 | width: 50px; 7 | box-shadow: 6px 6px 18px 0 rgba(0, 0, 0, 0.3); 8 | border-radius: 100%; 9 | background-color: #33c3f0; 10 | cursor: pointer; 11 | z-index: 99999; 12 | } 13 | 14 | .hidden { 15 | visibility: hidden; 16 | } 17 | 18 | .popup { 19 | position: absolute; 20 | background-color: white; 21 | border-radius: 4px; 22 | padding: 10px 20px; 23 | right: 70px; 24 | top: 50%; 25 | color: #33c3f0; 26 | box-shadow: 6px 6px 18px 0 rgba(0, 0, 0, 0.1); 27 | font-size: 12px; 28 | text-transform: uppercase; 29 | letter-spacing: 3px; 30 | transform: translate(0, -50%); 31 | border: 1px solid #f3f3f3; 32 | } 33 | 34 | .popup:before { 35 | content: ""; 36 | position: absolute; 37 | top: 50%; 38 | right: 0; 39 | transform: translate(100%, -50%); 40 | width: 0; 41 | height: 0; 42 | border-top: 7px solid transparent; 43 | border-bottom: 7px solid transparent; 44 | border-left: 7px solid white; 45 | } 46 | 47 | .popup-visibility-show { 48 | opacity: 1; 49 | visibility: visible; 50 | transition: all 0.35s ease; 51 | } 52 | 53 | .popup-visibility-hide { 54 | transition: all 0.35s ease; 55 | opacity: 0; 56 | visibility: hidden; 57 | } 58 | 59 | .visible { 60 | visibility: visible; 61 | } 62 | 63 | .x-line { 64 | position: absolute; 65 | top: 50%; 66 | left: 50%; 67 | transform: translate(-50%, -50%); 68 | width: 2px; 69 | height: 20px; 70 | background-color: white; 71 | } 72 | 73 | .y-line { 74 | position: absolute; 75 | top: 50%; 76 | left: 50%; 77 | transform: translate(-50%, -50%); 78 | width: 20px; 79 | height: 2px; 80 | background-color: white; 81 | } 82 | -------------------------------------------------------------------------------- /src/pages/popup/dashboard.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Roboto:300"); 2 | @import url("https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"); 3 | 4 | html, 5 | body { 6 | height: auto; 7 | width: 400px; 8 | margin: 0; 9 | padding: 0; 10 | font-family: "Roboto", sans-serif; 11 | } 12 | 13 | button { 14 | background-color: #33c3f0; 15 | border-radius: 3px; 16 | color: white; 17 | border: none; 18 | margin-right: 5px; 19 | margin-bottom: 10px; 20 | padding: 5px 10px; 21 | } 22 | 23 | .fa-cog:hover { 24 | color: #33c3f0; 25 | } 26 | 27 | .fa-trash-o:hover { 28 | color: red; 29 | cursor: pointer; 30 | } 31 | 32 | .footer { 33 | position: fixed; 34 | height: 40px; 35 | width: 360px; 36 | display: flex; 37 | align-items: center; 38 | justify-content: space-between; 39 | bottom: 0; 40 | background-color: #fff; 41 | } 42 | 43 | .footer__author { 44 | width: 50%; 45 | } 46 | 47 | .footer__github { 48 | width: 60px; 49 | display: flex; 50 | justify-content: space-between; 51 | } 52 | 53 | .footer__link { 54 | text-decoration: none; 55 | color: #000; 56 | } 57 | 58 | .footer__social { 59 | display: flex; 60 | justify-content: space-between; 61 | } 62 | 63 | h2 { 64 | margin-top: 0; 65 | } 66 | 67 | .header { 68 | display: flex; 69 | margin-top: 0; 70 | align-items: center; 71 | justify-content: space-between; 72 | font-size: 10px; 73 | } 74 | 75 | .history { 76 | margin-top: 20px; 77 | } 78 | 79 | .hidden { 80 | display: none; 81 | } 82 | 83 | .nav { 84 | display: flex; 85 | flex-direction: row-reverse; 86 | justify-content: flex-end; 87 | align-items: flex-start; 88 | /*justify-content: space-between;*/ 89 | } 90 | 91 | .visible { 92 | display: block; 93 | } 94 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const CopyPlugin = require('copy-webpack-plugin') 3 | const HtmlPlugin = require('html-webpack-plugin') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | 6 | require("babel-core/register"); 7 | require("babel-polyfill"); 8 | 9 | const PAGES_PATH = './src/pages' 10 | 11 | function generateHtmlPlugins(items) { 12 | return items.map( (name) => new HtmlPlugin( 13 | { 14 | filename: `./${name}.html`, 15 | chunks: [ name ], 16 | } 17 | )) 18 | } 19 | 20 | module.exports = { 21 | entry: { 22 | background: [ 23 | 'babel-polyfill', 24 | `${PAGES_PATH}/background`, 25 | ], 26 | popup: [ 27 | 'babel-polyfill', 28 | `${PAGES_PATH}/popup`, 29 | ], 30 | index: [ 31 | 'babel-polyfill', 32 | `${PAGES_PATH}/content`, 33 | ] 34 | }, 35 | output: { 36 | path: path.resolve('dist/pages'), 37 | filename: '[name].js' 38 | }, 39 | 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.js$/, 44 | use: [ 'babel-loader' ] 45 | }, 46 | { 47 | test: /\.jpe?g$|\.gif$|\.png$|\.ttf$|\.eot$|\.svg$/, 48 | use: 'file-loader?name=[name].[ext]?[hash]' 49 | }, 50 | { 51 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, 52 | loader: 'url-loader?limit=10000&mimetype=application/fontwoff' 53 | }, 54 | { 55 | test: /\.css$/, 56 | loaders: ["style-loader","css-loader"] 57 | } 58 | ] 59 | }, 60 | plugins: [ 61 | new ExtractTextPlugin( 62 | { 63 | filename: '[name].[contenthash].css', 64 | } 65 | ), 66 | new CopyPlugin( 67 | [ 68 | { 69 | from: 'src', 70 | to: path.resolve('dist'), 71 | ignore: [ 'pages/**/*' ] 72 | } 73 | ] 74 | ), 75 | ...generateHtmlPlugins( 76 | [ 77 | 'background', 78 | 'popup' 79 | ] 80 | ) 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-redux-tutorial", 3 | "jest": { 4 | "verbose": true, 5 | "moduleNameMapper": { 6 | "\\.(css|less)$": "identity-obj-proxy" 7 | }, 8 | "roots": [ 9 | "/src/pages/popup/" 10 | ] 11 | }, 12 | "description": "Redux Chrome extension example", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/pierroberto/chrome-navigator.git" 16 | }, 17 | "version": "0.0.1", 18 | "author": { 19 | "name": "Your Name", 20 | "email": "user@server.com" 21 | }, 22 | "license": "MIT", 23 | "scripts": { 24 | "build": "webpack --watch --progress", 25 | "test": "jest --coverage", 26 | "test:watch": "npm test --watch" 27 | }, 28 | "dependencies": { 29 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 30 | "babel-polyfill": "^6.23.0", 31 | "babel-preset-es2015": "^6.24.1", 32 | "babel-preset-stage-2": "^6.24.1", 33 | "file-loader": "^0.11.2", 34 | "lodash": "^4.17.11", 35 | "react": "^15.4.1", 36 | "react-chrome-redux": "^1.3.3", 37 | "react-dom": "^15.4.1", 38 | "react-redux": "^4.4.6", 39 | "react-router": "^4.2.0", 40 | "react-router-dom": "^4.2.2", 41 | "redux": "^3.6.0", 42 | "redux-logger": "^3.0.6", 43 | "redux-thunk": "^2.3.0", 44 | "truncate": "^2.0.0", 45 | "url-loader": "^0.5.9", 46 | "uuid": "^3.1.0", 47 | "webpack-css-loaders": "^1.0.0" 48 | }, 49 | "devDependencies": { 50 | "babel-core": "^6.26.3", 51 | "babel-jest": "^22.0.4", 52 | "babel-loader": "^7.1.1", 53 | "babel-plugin-react-html-attrs": "^2.0.0", 54 | "babel-plugin-transform-async-to-generator": "^6.24.1", 55 | "babel-preset-react": "^6.24.1", 56 | "copy-webpack-plugin": "^4.0.1", 57 | "enzyme": "^3.9.0", 58 | "enzyme-adapter-react-15": "^1.3.1", 59 | "extract-text-webpack-plugin": "^3.0.0", 60 | "html-webpack-plugin": "^2.30.1", 61 | "identity-obj-proxy": "^3.0.0", 62 | "jest": "^22.0.4", 63 | "merge": "^1.2.1", 64 | "mime": "^1.4.1", 65 | "react-addons-test-utils": "^15.6.2", 66 | "react-test-renderer": "^15.5.4", 67 | "webpack": "^3.5.3", 68 | "write-file-webpack-plugin": "^3.4.2" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/pages/background/reducers/bookmarks.js: -------------------------------------------------------------------------------- 1 | const defaultState = { 2 | tabs: [], 3 | chronology: [], 4 | addFromButton: false, 5 | search: "", 6 | searchResult: [] 7 | }; 8 | 9 | const bookmark = (state = defaultState, action) => { 10 | switch (action.type) { 11 | case "ADD": 12 | return { 13 | ...state, 14 | tabs: [{ tab: action.urlList, expiry: action.expiry }, ...state.tabs] 15 | }; 16 | case "ADD-FROM-BUTTON": 17 | return { 18 | ...state, 19 | addFromButton: action.addFromButton 20 | }; 21 | case "REFRESH": 22 | return { 23 | ...state, 24 | tabs: [{ tab: action.urlList, expiry: action.expiry }, ...state.tabs] 25 | }; 26 | case "DELETE-ALL": 27 | return { 28 | ...state, 29 | tabs: [] 30 | }; 31 | case "DELETE-ONE": 32 | return { 33 | ...state, 34 | tabs: [ 35 | ...state.tabs.filter(element => element.tab[0].url !== action.url) 36 | ], 37 | addFromButton: false 38 | }; 39 | case "EXPIRY": 40 | return { 41 | ...state, 42 | tabs: [ 43 | ...state.tabs.filter(element => element.tab[0].url !== action.url) 44 | ], 45 | chronology: [ 46 | ...state.tabs.filter(element => element.tab[0].url === action.url), 47 | ...state.chronology 48 | ] 49 | }; 50 | case "SEARCH": 51 | return { 52 | ...state, 53 | searchResult: [ 54 | ...state.tabs.filter( 55 | element => 56 | element.tab[0].title 57 | .toLowerCase() 58 | .indexOf(action.textSearched.toLowerCase()) !== -1 59 | ), 60 | ...state.chronology.filter( 61 | element => 62 | element.tab[0].title 63 | .toLowerCase() 64 | .indexOf(action.textSearched.toLowerCase()) !== -1 65 | ) 66 | ], 67 | search: action.textSearched 68 | }; 69 | case "EMPTY-SEARCH": 70 | return { 71 | ...state, 72 | searchResult: [], 73 | search: "" 74 | }; 75 | } 76 | return state; 77 | }; 78 | 79 | export default bookmark; 80 | -------------------------------------------------------------------------------- /src/pages/content/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { render } from "react-dom"; 3 | import "./index.css"; 4 | import { Store } from "react-chrome-redux"; 5 | import { Provider } from "react-redux"; 6 | import { connect } from "react-redux"; 7 | 8 | const store = new Store({ 9 | portName: "COUNTING" 10 | }); 11 | 12 | export default class InjectApp extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.classButtonDetail = ""; 16 | this.classPopupDetail = ""; 17 | } 18 | render() { 19 | if (!this.props.settings || !this.props.animation) return null; 20 | if (this.props.animation.buttonCog) { 21 | this.classButtonDetail = "circle faa-tada "; 22 | this.classPopupDetail = "popup popup-visibility-show "; 23 | setTimeout(function() { 24 | store.dispatch({ type: "TOGGLE-COG", buttonCog: false }); 25 | }, 3500); 26 | } 27 | if (!this.props.animation.buttonCog) { 28 | this.classButtonDetail = "circle "; 29 | this.classPopupDetail = "popup popup-visibility-hide"; 30 | } 31 | return ( 32 |
33 |
{ 41 | store.dispatch({ type: "ADD-FROM-BUTTON", addFromButton: true }); 42 | store.dispatch({ type: "TOGGLE-COG", buttonCog: true }); 43 | }} 44 | > 45 |
Saved
46 |
47 |
48 |
49 |
50 | ); 51 | } 52 | } 53 | 54 | const mapStateToProps = state => ({ 55 | settings: state.settings, 56 | animation: state.animation 57 | }); 58 | 59 | const ConnectedInjectApp = connect(mapStateToProps)(InjectApp); 60 | 61 | window.addEventListener("load", () => { 62 | const injectDOM = document.createElement("div"); 63 | injectDOM.className = "inject-react"; 64 | injectDOM.style.textAlign = "center"; 65 | document.body.appendChild(injectDOM); 66 | render( 67 | 68 | 69 | , 70 | injectDOM 71 | ); 72 | }); 73 | -------------------------------------------------------------------------------- /src/pages/popup/ItemView.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./item-view.css"; 3 | 4 | export default class ItemView extends React.Component { 5 | checkDeleteButton() { 6 | if (this.props.deleteTab) { 7 | return ( 8 |
9 | this.props.deleteTab(this.props.tab_url)} 12 | /> 13 |
14 | ); 15 | } 16 | } 17 | 18 | checkExpireDate() { 19 | let timeLeft; 20 | const time = new Date( 21 | this.props.expirySettings - (Date.now() - this.props.expiry) 22 | ); 23 | const seconds = (time / 1000).toFixed(1); 24 | if (this.props.expired || seconds <= 0) 25 | return
Expired
; 26 | const minutes = (time / (1000 * 60)).toFixed(1); 27 | const hours = (time / (1000 * 60 * 60)).toFixed(1); 28 | const days = (time / (1000 * 60 * 60 * 24)).toFixed(1); 29 | 30 | if (seconds < 60) { 31 | Math.floor(seconds) === 1 32 | ? (timeLeft = Math.floor(seconds) + " second") 33 | : (timeLeft = Math.floor(seconds) + " seconds"); 34 | } else if (minutes < 60) { 35 | Math.floor(minutes) === 1 36 | ? (timeLeft = Math.floor(minutes) + " minute") 37 | : (timeLeft = Math.floor(minutes) + " minutes"); 38 | } else if (hours < 24) { 39 | Math.floor(hours) === 1 40 | ? (timeLeft = Math.floor(hours) + " hour") 41 | : (timeLeft = Math.floor(hours) + " hours"); 42 | } else { 43 | Math.floor(days) === 1 44 | ? (timeLeft = Math.floor(days) + " day") 45 | : (timeLeft = Math.floor(days) + " days"); 46 | } 47 | return
Expires in {timeLeft}
; 48 | } 49 | 50 | render() { 51 | return ( 52 |
53 |
54 |
55 |
56 | 57 |
58 | 63 |
64 |
65 |
66 | {this.checkExpireDate()} 67 |
68 |
69 | {this.checkDeleteButton()} 70 |
71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/pages/popup/Settings.js: -------------------------------------------------------------------------------- 1 | import { Store } from "react-chrome-redux"; 2 | import React, { Component } from "react"; 3 | import "./settings.css"; 4 | import "./App"; 5 | import { Link } from "react-router-dom"; 6 | import { connect } from "react-redux"; 7 | import { 8 | toggleButton, 9 | toggleButtonHistory, 10 | expireDate 11 | } from "../background/actions"; 12 | 13 | class Settings extends React.Component { 14 | // ======================== RENDERING 15 | 16 | render() { 17 | return ( 18 |
19 |
20 |

Settings

21 | 22 | 23 | 24 |
25 | 26 |
27 |
    28 |
  • 29 | 30 | 44 |
  • 45 |
  • 46 | 47 | this.props.toggle(e.target.checked)} 51 | /> 52 |
  • 53 |
  • 54 | 55 | this.props.toggleHistory(e.target.checked)} 59 | /> 60 |
  • 61 |
62 |
63 |
64 | ); 65 | } 66 | } 67 | 68 | const mapStateToProps = state => ({ 69 | bookmark: state.bookmark, 70 | settings: state.settings 71 | }); 72 | 73 | const mapDispatchToProps = dispatch => ({ 74 | toggle: flag => dispatch(toggleButton(flag)), 75 | toggleHistory: flag => dispatch(toggleButtonHistory(flag)), 76 | expire: date => dispatch(expireDate(date)) 77 | }); 78 | 79 | export default connect( 80 | mapStateToProps, 81 | mapDispatchToProps 82 | )(Settings); 83 | -------------------------------------------------------------------------------- /coverage/lcov-report/background/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for background 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files background 20 |

21 |
22 |
23 | 54.17% 24 | Statements 25 | 13/24 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 8.33% 34 | Functions 35 | 1/12 36 |
37 |
38 | 100% 39 | Lines 40 | 12/12 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
FileStatementsBranchesFunctionsLines
actions.js
54.17%13/24100%0/08.33%1/12100%12/12
76 |
77 |
78 | 82 |
83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /coverage/lcov-report/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for All files 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files 20 |

21 |
22 |
23 | 18.02% 24 | Statements 25 | 20/111 26 |
27 |
28 | 0% 29 | Branches 30 | 0/38 31 |
32 |
33 | 3.45% 34 | Functions 35 | 2/58 36 |
37 |
38 | 21.11% 39 | Lines 40 | 19/90 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
FileStatementsBranchesFunctionsLines
background
54.17%13/24100%0/08.33%1/12100%12/12
popup
8.05%7/870%0/382.17%1/468.97%7/78
89 |
90 |
91 | 95 | 96 | 97 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /coverage/lcov-report/popup/App.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for popup/App.js 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / popup App.js 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 1/1 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 100% 34 | Functions 35 | 1/1 36 |
37 |
38 | 100% 39 | Lines 40 | 1/1 41 |
42 |
43 |
44 |
45 |

 46 | 
125 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 26 72 | 27  73 |   74 |   75 |   76 |   77 |   78 |   79 |   80 |   81 |   82 |   83 |   84 | 1x 85 |   86 |   87 |   88 |   89 |   90 |   91 |   92 |   93 |   94 |   95 |   96 |   97 |   98 |  
import React, { Component } from 'react';
 99 | import { connect } from 'react-redux';
100 | import './app.css';
101 | import ListView from './ListView.js';
102 | import Settings from './Settings';
103 | import Dashboard from './Dashboard';
104 | import {Route, BrowserRouter as Router,Switch} from 'react-router-dom'
105 |  
106 |  
107 | class App extends React.Component {
108 |  
109 |   render () {
110 |     return (
111 |       <Router>
112 |         <div className='wrapper'>
113 |           <Switch>
114 |               <Route exact path='/pages/popup.html' component={Dashboard} />
115 |               <Route path='/pages/settings' component={Settings} />
116 |           </Switch>
117 |       </div>
118 |       </Router>
119 |     )
120 |   }
121 | }
122 |  
123 | export default App;
124 |  
126 |
127 |
128 | 132 | 133 | 134 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/pages/popup/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { 4 | refreshBookmark, 5 | deleteAllBookmark, 6 | deleteOneBookmark, 7 | addBookmark, 8 | addFromButton 9 | } from "../background/actions"; 10 | import "./dashboard.css"; 11 | import ListView from "./ListView.js"; 12 | import Settings from "./Settings"; 13 | import Search from "./Search"; 14 | import truncate from "truncate"; 15 | import { Link } from "react-router-dom"; 16 | 17 | class Dashboard extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | } 21 | 22 | saveBookmark() { 23 | return new Promise((resolved, rejected) => { 24 | chrome.tabs.query({ active: true, lastFocusedWindow: true }, data => { 25 | resolved(data); 26 | }); 27 | }).then(link => { 28 | if (!link[0].favIconUrl) link[0].favIconUrl = "../assets/nothing.png"; 29 | if (link[0].title.length > 10) 30 | link[0].title = truncate(link[0].title.toString(), 50); 31 | const flag = this.checkUrl(link); 32 | if (flag) { 33 | this.props.add(link); 34 | } else { 35 | //If it already exists remove the old link and add a new one 36 | this.props.deleteOne(link[0].url); 37 | this.props.add(link); 38 | } 39 | }); 40 | } 41 | 42 | checkUrl(link) { 43 | for (let i = 0; i < this.props.bookmark.tabs.length; i++) { 44 | if (this.props.bookmark.tabs[i].tab[0].url === link[0].url) return false; 45 | } 46 | return true; 47 | } 48 | 49 | clearAll() { 50 | this.props.deleteAll(); 51 | } 52 | 53 | deleteTab = url => { 54 | this.props.deleteOne(url); 55 | return data; 56 | }; 57 | 58 | componentDidMount() { 59 | // VERSION 1.0.2 Double check if the link has expireDate 60 | 61 | this.props.bookmark.tabs.map(tab => { 62 | if (tab.expiry >= this.props.settings.expireDate + Date.now()) { 63 | store.dispatch({ type: "EXPIRY", url: tab.tab[0].url }); 64 | } 65 | }); 66 | } 67 | 68 | checkSearch() { 69 | if (!this.props.bookmark.search) { 70 | return ( 71 |
72 | 78 |
81 |

History

82 | 88 |
89 |
90 | ); 91 | } 92 | } 93 | 94 | // ====================== RENDERING 95 | 96 | render() { 97 | // I check if the button in the content script has been clicked 98 | if (this.props.bookmark.addFromButton) { 99 | this.saveBookmark(); 100 | this.props.addThroughButton(false); 101 | } 102 | 103 | return ( 104 |
105 |
106 |

Pin Tabs

107 | 108 | 109 | 110 |
111 | 112 |
113 |
114 | 115 | {/* */} 116 | 117 |
118 | 119 |
120 |
121 | 122 | {this.checkSearch()} 123 | 124 |
125 |
Pier Roberto Lucisano 📌 2020
126 | 135 |
136 |
137 |