Skip to content

Commit 04a15ba

Browse files
Julien FroidefondJulien Froidefond
Julien Froidefond
authored and
Julien Froidefond
committedMay 5, 2020
init
1 parent 9544e4e commit 04a15ba

30 files changed

+7621
-20
lines changed
 

‎.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
build.zip
3+
.DS_Store
4+
build

‎.prettierignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
src/jquery.js
2+
yarn.lock
3+
*.png

‎.prettierrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"arrowParens": "avoid",
3+
"trailingComma": "all"
4+
}

‎README.md

+114-20
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,114 @@
1-
# Introduction
2-
TODO: Give a short introduction of your project. Let this section explain the objectives or the motivation behind this project.
3-
4-
# Getting Started
5-
TODO: Guide users through getting your code up and running on their own system. In this section you can talk about:
6-
1. Installation process
7-
2. Software dependencies
8-
3. Latest releases
9-
4. API references
10-
11-
# Build and Test
12-
TODO: Describe and show how to build your code and run the tests.
13-
14-
# Contribute
15-
TODO: Explain how other users and developers can contribute to make your code better.
16-
17-
If you want to learn more about creating good readme files then refer the following [guidelines](https://www.visualstudio.com/en-us/docs/git/create-a-readme). You can also seek inspiration from the below readme files:
18-
- [ASP.NET Core](https://github.com/aspnet/Home)
19-
- [Visual Studio Code](https://github.com/Microsoft/vscode)
20-
- [Chakra Core](https://github.com/Microsoft/ChakraCore)
1+
# XP clients links
2+
3+
## Developing a new extension
4+
5+
_I'll assume that you already read the [Webpack docs](https://webpack.js.org) and the [Chrome Extension](https://developer.chrome.com/extensions/getstarted) docs._
6+
7+
1. Check if your Node.js version is >= 6.
8+
2. Clone the repository.
9+
3. Install [yarn](https://yarnpkg.com/lang/en/docs/install/).
10+
4. Run `yarn`.
11+
5. Run `yarn start`
12+
6. Load your extension on Chrome following:
13+
1. Access `chrome://extensions/`
14+
2. Check `Developer mode`
15+
3. Click on `Load unpacked extension`
16+
4. Select the `build` folder.
17+
18+
## Structure
19+
20+
All your extension's development code must be placed in `src` folder, including the extension manifest.
21+
22+
The boilerplate is already prepared to have a popup, a options page and a background page. You can easily customize this.
23+
24+
Each page has its own [assets package defined](https://github.com/samuelsimoes/chrome-extension-webpack-boilerplate/blob/master/webpack.config.js#L16-L20). So, to code on popup you must start your code on `src/js/popup.js`, for example.
25+
26+
You must use the [ES6 modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) to a better code organization. The boilerplate is already prepared to that and [here you have a little example](https://github.com/samuelsimoes/chrome-extension-webpack-boilerplate/blob/master/src/js/popup.js#L2-L4).
27+
28+
## Webpack auto-reload and HRM
29+
30+
To make your workflow much more efficient this boilerplate uses the [webpack server](https://webpack.github.io/docs/webpack-dev-server.html) to development (started with `yarn run server`) with auto reload feature that reloads the browser automatically every time that you save some file o your editor.
31+
32+
You can run the dev mode on other port if you want. Just specify the env var `port` like this:
33+
34+
```
35+
$ PORT=6002 yarn run start
36+
```
37+
38+
## Content Scripts
39+
40+
Although this boilerplate uses the webpack dev server, it's also prepared to write all your bundles files on the disk at every code change, so you can point, on your extension manifest, to your bundles that you want to use as [content scripts](https://developer.chrome.com/extensions/content_scripts), but you need to exclude these entry points from hot reloading [(why?)](https://github.com/samuelsimoes/chrome-extension-webpack-boilerplate/issues/4#issuecomment-261788690). To do so you need to expose which entry points are content scripts on the `webpack.config.js` using the `chromeExtensionBoilerplate -> notHotReload` config. Look the example below.
41+
42+
Let's say that you want use the `myContentScript` entry point as content script, so on your `webpack.config.js` you will configure the entry point and exclude it from hot reloading, like this:
43+
44+
```js
45+
{
46+
47+
entry: {
48+
myContentScript: "./src/js/myContentScript.js"
49+
},
50+
chromeExtensionBoilerplate: {
51+
notHotReload: ["myContentScript"]
52+
}
53+
54+
}
55+
```
56+
57+
and on your `src/manifest.json`:
58+
59+
```json
60+
{
61+
"content_scripts": [
62+
{
63+
"matches": ["https://www.google.com/*"],
64+
"js": ["myContentScript.bundle.js"]
65+
}
66+
]
67+
}
68+
```
69+
70+
## Packing
71+
72+
After the development of your extension run the command
73+
74+
```
75+
$ NODE_ENV=production yarn run build
76+
```
77+
78+
Now, the content of `build` folder will be the extension ready to be submitted to the Chrome Web Store. Just take a look at the [official guide](https://developer.chrome.com/webstore/publish) to more infos about publishing.
79+
80+
## Secrets
81+
82+
If you are developing an extension that talks with some API you probably are using different keys for testing and production. Is a good practice you not commit your secret keys and expose to anyone that have access to the repository.
83+
84+
To this task this boilerplate import the file `./secrets.<THE-NODE_ENV>.js` on your modules through the module named as `secrets`, so you can do things like this:
85+
86+
_./secrets.development.js_
87+
88+
```js
89+
export default { key: "123" };
90+
```
91+
92+
_./src/popup.js_
93+
94+
```js
95+
import secrets from "secrets";
96+
ApiCall({ key: secrets.key });
97+
```
98+
99+
:point_right: The files with name `secrets.*.js` already are ignored on the repository.
100+
101+
## With React.js
102+
103+
:bulb: If you want use [React.js](https://facebook.github.io/react/) with this boilerplate, check the **[react branch](https://github.com/samuelsimoes/chrome-extension-webpack-boilerplate/tree/react)**.
104+
105+
## Contributing
106+
107+
1. **Please!! Do not create a pull request without an issue before discussing the problem.**
108+
2. On your PR make sure that you are following the current codebase style.
109+
3. Your PR must be single purpose. Resolve just one problem on your PR.
110+
4. Make sure to commit in the same style that we are committing until now on the project.
111+
112+
---
113+
114+
Samuel Simões ~ [@samuelsimoes](https://twitter.com/samuelsimoes) ~ [Blog](http://blog.samuelsimoes.com/)

‎package.json

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"devDependencies": {
3+
"clean-webpack-plugin": "3.0.0",
4+
"copy-webpack-plugin": "5.0.5",
5+
"css-loader": "3.2.0",
6+
"file-loader": "4.3.0",
7+
"fs-extra": "8.1.0",
8+
"html-loader": "0.5.5",
9+
"html-webpack-plugin": "3.2.0",
10+
"husky": "^4.2.1",
11+
"prettier": "^2.0.5",
12+
"pretty-quick": "^2.0.1",
13+
"style-loader": "1.0.0",
14+
"webpack": "4.41.2",
15+
"webpack-cli": "3.3.10",
16+
"webpack-dev-server": "3.9.0",
17+
"write-file-webpack-plugin": "4.5.1"
18+
},
19+
"scripts": {
20+
"build": "node utils/build.js",
21+
"start": "node utils/webserver.js"
22+
},
23+
"husky": {
24+
"hooks": {
25+
"pre-commit": "pretty-quick --staged"
26+
}
27+
},
28+
"dependencies": {
29+
"firebase": "^7.8.0",
30+
"jquery": "^3.4.1"
31+
}
32+
}

‎src/background.html

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title></title>
6+
</head>
7+
<body></body>
8+
</html>

‎src/css/options.css

Whitespace-only changes.

‎src/css/popup.css

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#mainPopup {
2+
padding: 10px;
3+
width: 400px;
4+
font-family: Helvetica, Ubuntu, Arial, sans-serif;
5+
height: 70vh;
6+
font-weight: 500;
7+
font-family: "Roboto", sans-serif;
8+
-webkit-font-smoothing: antialiased;
9+
font-size: 12px;
10+
}
11+
12+
h1 {
13+
font-size: 1em;
14+
}
15+
16+
.badge {
17+
color: #fff;
18+
background-color: #6c757d;
19+
display: inline-block;
20+
padding: 0.25em 0.4em;
21+
font-size: 100%;
22+
margin-bottom: 5px;
23+
font-weight: 700;
24+
line-height: 1;
25+
text-align: center;
26+
white-space: nowrap;
27+
vertical-align: baseline;
28+
border-radius: 0.25rem;
29+
margin-left: 5px;
30+
cursor: pointer;
31+
}
32+
.badge.checked {
33+
background: #40474e;
34+
color: #eff4fa;
35+
}
36+
label {
37+
display: inline-block;
38+
max-width: 100%;
39+
margin-bottom: 5px;
40+
font-weight: 700;
41+
}
42+
.form-group {
43+
margin-bottom: 15px;
44+
}
45+
.form-control {
46+
display: block;
47+
width: 90%;
48+
height: 34px;
49+
padding: 0px 10px;
50+
font-size: 14px;
51+
line-height: 1.42857143;
52+
color: #555;
53+
background-color: #fff;
54+
background-image: none;
55+
border: 1px solid #ccc;
56+
border-radius: 4px;
57+
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
58+
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
59+
-webkit-transition: border-color ease-in-out 0.15s,
60+
box-shadow ease-in-out 0.15s;
61+
-o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
62+
-webkit-transition: border-color ease-in-out 0.15s,
63+
-webkit-box-shadow ease-in-out 0.15s;
64+
transition: border-color ease-in-out 0.15s,
65+
-webkit-box-shadow ease-in-out 0.15s;
66+
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
67+
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s,
68+
-webkit-box-shadow ease-in-out 0.15s;
69+
}
70+
.form-control:focus {
71+
border-color: #66afe9;
72+
outline: 0;
73+
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
74+
0 0 8px rgba(102, 175, 233, 0.6);
75+
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
76+
0 0 8px rgba(102, 175, 233, 0.6);
77+
}
78+
.btn {
79+
display: inline-block;
80+
margin-bottom: 0;
81+
font-weight: 400;
82+
text-align: center;
83+
white-space: nowrap;
84+
vertical-align: middle;
85+
-ms-touch-action: manipulation;
86+
touch-action: manipulation;
87+
cursor: pointer;
88+
background-image: none;
89+
border: 1px solid transparent;
90+
padding: 6px 12px;
91+
font-size: 14px;
92+
line-height: 1.42857143;
93+
border-radius: 4px;
94+
-webkit-user-select: none;
95+
-moz-user-select: none;
96+
-ms-user-select: none;
97+
user-select: none;
98+
}
99+
.btn-default {
100+
color: #333;
101+
background-color: #fff;
102+
border-color: #ccc;
103+
}
104+
.btn-default:hover {
105+
color: #333;
106+
background-color: #e6e6e6;
107+
border-color: #adadad;
108+
}
109+
hr {
110+
margin-top: 20px;
111+
margin-bottom: 20px;
112+
border: 0;
113+
border-top: 1px solid #eee;
114+
}
115+
#submit-form {
116+
display: none;
117+
}

‎src/img/screenshot.jpg

42.8 KB
Loading

‎src/img/xpclient-1024.png

113 KB
Loading

‎src/img/xpclient-128.png

14.3 KB
Loading

‎src/img/xpclient-16.png

1017 Bytes
Loading

‎src/img/xpclient-256.png

29.7 KB
Loading

‎src/img/xpclient-32.png

2.62 KB
Loading

‎src/img/xpclient-48.png

4.4 KB
Loading

‎src/img/xpclient-512.png

64.5 KB
Loading

‎src/img/xpclient-64.png

6.42 KB
Loading

‎src/js/background.js

+383
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
import "../img/xpclient-16.png";
2+
import "../img/xpclient-48.png";
3+
import "../img/xpclient-128.png";
4+
5+
import firebaseHandler from "./firebase";
6+
7+
let links;
8+
let currentIdMappings = {};
9+
let rootBookmarkName = "xp client";
10+
let nodeChangedToSync = [];
11+
12+
const syncRootBookmarkName = () => {
13+
chrome.storage.local.get(["customRootBookmarkName"], result => {
14+
const rootName = result.customRootBookmarkName;
15+
if (rootName) rootBookmarkName = rootName;
16+
});
17+
};
18+
syncRootBookmarkName();
19+
20+
/**
21+
* Syncing the tags selected
22+
*/
23+
const syncByPreferences = () => {
24+
chrome.storage.local.get(["currentIdMappings"], settings => {
25+
currentIdMappings = settings.currentIdMappings || {};
26+
syncBookmarks();
27+
updateBookmarks();
28+
});
29+
};
30+
31+
/**
32+
* Connect to popup and launch the sync by pref
33+
*/
34+
chrome.extension.onConnect.addListener(function (port) {
35+
console.log("Connected .....");
36+
port.onMessage.addListener(function (msg) {
37+
if (msg.event === "syncBookmarks") {
38+
console.log("syncBookmarks");
39+
syncByPreferences();
40+
} else if (msg.event == "updateRootBookmarkName") {
41+
syncRootBookmarkName();
42+
}
43+
});
44+
});
45+
46+
/**
47+
* Recursively check links for changes
48+
* @param {array} linksInMemory Old links
49+
* @param {array} linksFromStorage New links
50+
*/
51+
const detectChildrenChanges = (linksInMemory, linksFromStorage) => {
52+
if (linksInMemory.length !== linksFromStorage.length) return true;
53+
for (let i in linksFromStorage) {
54+
const linkFromStorage = linksFromStorage[i];
55+
const link = linksInMemory[i];
56+
if (!link) return true;
57+
if (link) {
58+
if (
59+
linkFromStorage.title !== link.title ||
60+
linkFromStorage.url !== link.url ||
61+
linkFromStorage.id !== link.id
62+
) {
63+
return true;
64+
}
65+
if (linkFromStorage.children)
66+
return detectChildrenChanges(link.children, linkFromStorage.children);
67+
}
68+
}
69+
return false;
70+
};
71+
72+
/**
73+
* Detect if a change has been made
74+
* @param {array} linksInMemory Old links
75+
* @param {array} linksFromStorage New links
76+
*/
77+
const detectChanges = (linksInMemory, linksFromStorage) => {
78+
//TODO : Before to set the new links; we must compare it to detect changes. And in case of changes, we update the right rootnode
79+
const rootNodeChanged = [];
80+
if (linksInMemory) {
81+
for (let i in linksFromStorage) {
82+
const linkFromStorage = linksFromStorage[i];
83+
const link = linksInMemory[i];
84+
const hasChange = detectChildrenChanges([link], [linkFromStorage]);
85+
if (hasChange) rootNodeChanged.push(linkFromStorage);
86+
}
87+
}
88+
return rootNodeChanged;
89+
};
90+
91+
/**
92+
* Sync datas from firebase
93+
*/
94+
firebaseHandler.getBookmarks(linksFromStorage => {
95+
nodeChangedToSync = detectChanges(links, linksFromStorage);
96+
97+
links = linksFromStorage;
98+
99+
//Trying to reconstruct idMappings
100+
chrome.bookmarks.search({ title: rootBookmarkName }, rootNodes => {
101+
const rootNode = rootNodes[0];
102+
for (let i in links) {
103+
const item = links[i];
104+
chrome.bookmarks.search({ title: item.title }, searches => {
105+
const search = searches[0];
106+
if (search) {
107+
chrome.storage.local.get(["currentIdMappings"], result => {
108+
let currentIdMappings = result.currentIdMappings || {};
109+
if (
110+
search.parentId === rootNode.id &&
111+
!currentIdMappings[item.id]
112+
) {
113+
console.log("force selection", search);
114+
currentIdMappings = {
115+
...currentIdMappings,
116+
[item.id]: search.id,
117+
};
118+
chrome.storage.local.set({
119+
currentIdMappings,
120+
});
121+
}
122+
});
123+
}
124+
});
125+
}
126+
});
127+
128+
syncByPreferences();
129+
});
130+
131+
/**
132+
* Determine if is a tag. If is a root node; it is a tag.
133+
* @param {string} id
134+
*/
135+
const isATag = id => {
136+
for (let i in links) {
137+
const link = links[i];
138+
if (id === link.id) {
139+
return true;
140+
}
141+
}
142+
return false;
143+
};
144+
145+
/**
146+
* Get a bookmark by id
147+
* @param {string} id
148+
*/
149+
const getBookmarkById = id => {
150+
for (let i in links) {
151+
if (links[i].id === id) return links[i];
152+
}
153+
};
154+
155+
/**
156+
* Creating the root bookmark. "xp client"
157+
* @param {array} selectedTags
158+
* @param {function} callback
159+
*/
160+
const createRootBookmarkNode = (selectedTags, callback) => {
161+
// if (Object.keys(selectedTags).length > 0) {
162+
chrome.storage.local.get(["rootBookmarkId"], result => {
163+
// if (result.rootBookmarkId) {
164+
// callback(result.rootBookmarkId);
165+
// } else {
166+
chrome.bookmarks.search({ title: rootBookmarkName, url: null }, res => {
167+
const rootNode = res[0];
168+
if (res.length > 0) {
169+
callback(rootNode.id);
170+
} else {
171+
chrome.bookmarks.create(
172+
{
173+
parentId: "1",
174+
title: rootBookmarkName,
175+
index: 0,
176+
},
177+
rootNode => {
178+
console.log("root bookmark created =>", rootNode.id);
179+
//register id
180+
chrome.storage.local.set({
181+
rootBookmarkId: rootNode.id,
182+
});
183+
callback(rootNode.id);
184+
},
185+
);
186+
}
187+
});
188+
// }
189+
});
190+
// }
191+
};
192+
193+
/**
194+
* Check the existence of a sub root bookmark
195+
* @param {*} bookmark
196+
* @param {*} cb
197+
*/
198+
const bookmarkExists = (bookmark, cb) => {
199+
chrome.bookmarks.search({ title: rootBookmarkName }, rootNodes => {
200+
const rootNode = rootNodes[0];
201+
chrome.bookmarks.search({ title: bookmark.title }, searches => {
202+
const res = searches[0];
203+
if (res && res.parentId === rootNode.id) cb(true);
204+
else cb(false);
205+
});
206+
});
207+
};
208+
209+
/**
210+
* Recursive function to create bookmarks and subsequent childs.
211+
* @param {string} rootNodeId
212+
* @param {Array} bookmarks
213+
*/
214+
const createSubBookMarks = (rootNodeId, bookmarks) => {
215+
for (let i in bookmarks) {
216+
const bookmark = bookmarks[i];
217+
//TODO: check if already exists by name / url + parentId = root
218+
bookmarkExists(bookmark, exists => {
219+
if (!exists)
220+
chrome.bookmarks.create(
221+
{
222+
parentId: rootNodeId.toString(),
223+
title: bookmark.title,
224+
url: bookmark.url,
225+
},
226+
bMark => {
227+
console.log("bookmark created =>", bMark.id, bookmark.id);
228+
if (isATag(bookmark.id)) currentIdMappings[bookmark.id] = bMark.id;
229+
chrome.storage.local.set({ currentIdMappings });
230+
if (bookmark.children && bMark)
231+
createSubBookMarks(bMark.id, bookmark.children);
232+
},
233+
);
234+
});
235+
}
236+
};
237+
238+
/**
239+
* Ensure to delete a sub root node even if a async has come
240+
* @param {*} bookmark
241+
* @param {*} cb
242+
*/
243+
const getNodeToDelete = (bookmark, cb) => {
244+
chrome.bookmarks.search({ title: rootBookmarkName }, rootNodes => {
245+
const rootNode = rootNodes[0];
246+
chrome.bookmarks.search({ title: bookmark.title }, searches => {
247+
for (let i in searches) {
248+
const search = searches[i];
249+
if (search.parentId === rootNode.id) cb(search);
250+
}
251+
});
252+
});
253+
};
254+
255+
/**
256+
* Remove e bookmark by id
257+
* @param {string} bookmarkIdTodel
258+
*/
259+
const removeBookmarks = bookmarkIdTodel => {
260+
const bookmarkId = currentIdMappings[bookmarkIdTodel];
261+
if (bookmarkId) {
262+
getNodeToDelete(getBookmarkById(bookmarkIdTodel), res => {
263+
if (res) chrome.bookmarks.removeTree(res.id);
264+
});
265+
chrome.bookmarks.removeTree(bookmarkId);
266+
delete currentIdMappings[bookmarkIdTodel];
267+
chrome.storage.local.set({ currentIdMappings });
268+
console.log("bookmark deleted =>", bookmarkId, bookmarkIdTodel);
269+
}
270+
};
271+
272+
/**
273+
* Doing all the synchronization job
274+
*/
275+
const syncBookmarks = () => {
276+
chrome.storage.local.get(["selectedTags"], result => {
277+
const selectedTags = result.selectedTags || {};
278+
// console.log("currentIdMappings", currentIdMappings);
279+
// console.log("selectedTags", selectedTags);
280+
createRootBookmarkNode(selectedTags, rootNodeId => {
281+
//Here we just create if root node is not in scope; we have to update if firebase has change.
282+
for (let i in links) {
283+
const link = links[i];
284+
if (selectedTags[link.id]) {
285+
//has to be : To create if is not already done
286+
if (!currentIdMappings[link.id]) {
287+
createSubBookMarks(rootNodeId, { [link.id]: link });
288+
} else {
289+
//TODO : update if needed ?
290+
}
291+
} else {
292+
//To delete if already added
293+
if (currentIdMappings[link.id]) {
294+
removeBookmarks(link.id);
295+
}
296+
}
297+
}
298+
});
299+
});
300+
};
301+
302+
/**
303+
* Update bookmarks
304+
*/
305+
const updateBookmarks = () => {
306+
if (nodeChangedToSync.length > 0) {
307+
for (let i in nodeChangedToSync) {
308+
const node = nodeChangedToSync[i];
309+
removeBookmarks(node.id);
310+
}
311+
createRootBookmarkNode(null, rootNodeId => {
312+
//Here we just create if root node is not in scope; we have to update if firebase has change.
313+
for (let i in nodeChangedToSync) {
314+
const node = nodeChangedToSync[i];
315+
createSubBookMarks(rootNodeId, { [node.id]: node });
316+
}
317+
});
318+
}
319+
nodeChangedToSync = [];
320+
};
321+
322+
/**
323+
* Find the user by the email : check existence
324+
* @param {array} users Users
325+
* @param {string} email Email of user
326+
*/
327+
const findUserByEmail = (users, email) => {
328+
if (!email) return { user: null, position: null };
329+
for (let i in users) {
330+
const user = users[i];
331+
if (user.email === email) {
332+
return { user, position: i };
333+
}
334+
}
335+
return { user: null, position: null };
336+
};
337+
338+
/**
339+
* Log in frebase the users by chrome instance id
340+
*/
341+
const logUserByInstanceId = () => {
342+
//Log user by instanceId
343+
firebaseHandler.getUsers(users => {
344+
chrome.instanceID.getID(instanceId => {
345+
chrome.identity.getProfileUserInfo(userProfileInfo => {
346+
const { email, id } = userProfileInfo;
347+
const { user, position } = findUserByEmail(users, email);
348+
if (!user) {
349+
const createdAt = new Date();
350+
users.push({
351+
lastInstanceId: instanceId,
352+
email,
353+
id,
354+
createdAt,
355+
});
356+
} else {
357+
if (email !== "" && id !== "")
358+
users[position] = {
359+
...users[position],
360+
lastInstanceId: instanceId,
361+
email,
362+
id,
363+
};
364+
365+
const updatedAt = new Date();
366+
users[position] = { ...users[position], updatedAt };
367+
}
368+
firebaseHandler.setUsers(users);
369+
});
370+
});
371+
});
372+
};
373+
374+
chrome.runtime.onInstalled.addListener(() => {
375+
logUserByInstanceId();
376+
createRootBookmarkNode(null, () => {});
377+
});
378+
379+
//reset prefs :::
380+
// const reset = () => {
381+
// currentIdMappings = {};
382+
// chrome.storage.local.set({ currentIdMappings });
383+
// }();

‎src/js/firebase.js

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import * as firebase from "firebase/app";
2+
import "firebase/firestore";
3+
4+
// Your web app's Firebase configuration
5+
let firebaseConfig = {
6+
apiKey: "AIzaSyARBRD6lLUN4DIHjjTp3_D_SbLdfh2Hpkc",
7+
authDomain: "bookmarkssync.firebaseapp.com",
8+
databaseURL: "https://bookmarkssync.firebaseio.com",
9+
projectId: "bookmarkssync",
10+
storageBucket: "bookmarkssync.appspot.com",
11+
messagingSenderId: "33268123377",
12+
appId: "1:33268123377:web:f372519e2ab973a8430b50"
13+
};
14+
const docId = "Q1IYmUTGBzouvIiwVOlj";
15+
16+
// Initialize Firebase
17+
firebase.initializeApp(firebaseConfig);
18+
19+
let getBookmarks = callback => {
20+
let db = firebase.firestore();
21+
db.collection("bookmarks")
22+
.doc(docId)
23+
.onSnapshot(doc => {
24+
if (doc.exists) {
25+
console.log("Get firebase ::: Document data:", doc.data());
26+
callback(doc.data().bookmarks);
27+
} else {
28+
// doc.data() will be undefined in this case
29+
console.log("No such document!");
30+
}
31+
});
32+
};
33+
34+
let setBookmarks = bookmarks => {
35+
let db = firebase.firestore();
36+
db.collection("bookmarks")
37+
.doc(docId)
38+
.set(bookmarks)
39+
.then(error => {
40+
if (!error) {
41+
console.log("Set firebase ::: Document data:", doc.data());
42+
callback(doc.data());
43+
}
44+
})
45+
.catch(function(error) {
46+
console.log("Error setting document:", error);
47+
});
48+
};
49+
50+
let getUsers = callback => {
51+
let db = firebase.firestore();
52+
db.collection("bookmarks")
53+
.doc("aSudfknWtis2QFRKkAOH")
54+
.get()
55+
.then(doc => {
56+
if (doc.exists) {
57+
console.log("Get firebase ::: Users data:", doc.data());
58+
callback(doc.data().users);
59+
} else {
60+
// doc.data() will be undefined in this case
61+
console.log("No such users document!");
62+
}
63+
});
64+
};
65+
66+
let setUsers = users => {
67+
let db = firebase.firestore();
68+
db.collection("bookmarks")
69+
.doc("aSudfknWtis2QFRKkAOH")
70+
.set({ users })
71+
.then(error => {
72+
if (!error) {
73+
console.log("Set firebase ::: Users");
74+
}
75+
})
76+
.catch(function(error) {
77+
console.log("Error setting document:", error);
78+
});
79+
};
80+
const suggestionExists = (suggestions, suggestion) => {
81+
if (!suggestions) return false;
82+
for (let i in suggestions) {
83+
if (suggestions[i].url === suggestion.url) {
84+
return true;
85+
}
86+
}
87+
return false;
88+
};
89+
90+
let sendSuggestion = (suggestion, callback) => {
91+
let db = firebase.firestore();
92+
db.collection("bookmarks")
93+
.doc("5UuO2xqnW1Qpl7WTptai")
94+
.get()
95+
.then(docs => {
96+
const { suggestions } = docs.data();
97+
if (!suggestionExists(suggestions, suggestion)) {
98+
db.collection("bookmarks")
99+
.doc("5UuO2xqnW1Qpl7WTptai")
100+
.set({ suggestions: [...suggestions, suggestion] })
101+
.then(error => {
102+
if (!error) callback(true);
103+
})
104+
.catch(function(error) {
105+
console.log("Error setting document:", error);
106+
});
107+
} else {
108+
callback(false);
109+
}
110+
});
111+
};
112+
113+
export default {
114+
getBookmarks,
115+
setBookmarks,
116+
getUsers,
117+
setUsers,
118+
sendSuggestion
119+
};

‎src/js/initData.js

+1,105
Large diffs are not rendered by default.

‎src/js/options.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import "../css/options.css";

‎src/js/popup.js

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import $ from "jquery";
2+
3+
import "../img/screenshot.jpg";
4+
import "../css/popup.css";
5+
import firebaseHandler from "./firebase";
6+
import initData from "./initData";
7+
8+
let rootBookmarkName = "xp client";
9+
10+
chrome.storage.local.get(["customRootBookmarkName"], result => {
11+
const rootName = result.customRootBookmarkName;
12+
if (rootName) rootBookmarkName = rootName;
13+
});
14+
15+
var port = chrome.extension.connect({
16+
name: "Bookmarks Communication",
17+
});
18+
19+
const checkItems = selectedTags => {
20+
$(".tag").removeClass("checked");
21+
for (let i in selectedTags) {
22+
$("[data-id=" + i + "]").addClass("checked");
23+
}
24+
};
25+
26+
const parseMenu = (rootDomNode, menu) => {
27+
for (let i in menu) {
28+
const item = menu[i];
29+
if (!item.parentId)
30+
$(rootDomNode).append(
31+
`<span class="badge tag" data-id="${item.id}">${item.title}</span>`,
32+
);
33+
}
34+
$(rootDomNode)
35+
.find(".tag")
36+
.each((index, element) => {
37+
$(element).click(e => {
38+
e.preventDefault();
39+
const id = $(e.target).attr("data-id");
40+
const label = $(e.target).text();
41+
//Remind preferences
42+
chrome.storage.local.get(["selectedTags"], result => {
43+
let selectedTags = result.selectedTags || {};
44+
if (selectedTags[id]) {
45+
//removeBookmarks(id);
46+
delete selectedTags[id];
47+
} else {
48+
//createBookmarks(id);
49+
selectedTags = { ...selectedTags, [id]: label };
50+
}
51+
console.log("selectedTags", selectedTags);
52+
chrome.storage.local.set({ selectedTags });
53+
checkItems(selectedTags);
54+
55+
port.postMessage({ event: "syncBookmarks" });
56+
});
57+
});
58+
});
59+
};
60+
61+
//reset ::: chrome.storage.local.set({ selectedTags: {} });
62+
// const menu = $("#menu-tags");
63+
// parseMenu(menu, links);
64+
// chrome.storage.local.get(["selectedTags"], result => {
65+
// const selectedTags = result.selectedTags || {};
66+
// checkItems(selectedTags);
67+
// });
68+
let timeout = null;
69+
$(document).ready(() => {
70+
chrome.storage.local.get(["customRootBookmarkName"], result => {
71+
const rootName = result.customRootBookmarkName;
72+
if (rootName) $("#custom-root-name").val(rootName);
73+
});
74+
$("#custom-root-name").keydown(e => {
75+
clearTimeout(timeout);
76+
77+
timeout = setTimeout(function () {
78+
chrome.storage.local.set(
79+
{
80+
customRootBookmarkName: $(e.currentTarget).val(),
81+
},
82+
() => port.postMessage({ event: "updateRootBookmarkName" }),
83+
);
84+
}, 1000);
85+
});
86+
});
87+
88+
firebaseHandler.getBookmarks(linksFromStorage => {
89+
const menu = $("#menu-tags");
90+
console.log(linksFromStorage);
91+
parseMenu(menu, linksFromStorage);
92+
chrome.storage.local.get(["selectedTags"], result => {
93+
const selectedTags = result.selectedTags || {};
94+
checkItems(selectedTags);
95+
});
96+
97+
//Trying to check already present items
98+
chrome.bookmarks.search({ title: rootBookmarkName }, rootNodes => {
99+
const rootNode = rootNodes[0];
100+
for (let i in linksFromStorage) {
101+
const item = linksFromStorage[i];
102+
chrome.bookmarks.search({ title: item.title }, searches => {
103+
const search = searches[0];
104+
if (search) {
105+
chrome.storage.local.get(["selectedTags"], result => {
106+
let selectedTags = result.selectedTags || {};
107+
if (search.parentId === rootNode.id && !selectedTags[i]) {
108+
console.log("force selection", search);
109+
$("[data-id=" + item.id + "]").addClass("checked");
110+
selectedTags = {
111+
...selectedTags,
112+
[item.id]: search.title,
113+
};
114+
chrome.storage.local.set({
115+
selectedTags,
116+
});
117+
}
118+
});
119+
}
120+
});
121+
}
122+
});
123+
port.postMessage({ event: "syncBookmarks" });
124+
125+
$(".all-badges").click(e => {
126+
chrome.storage.local.get(["selectedTags"], result => {
127+
if (
128+
!result.selectedTags ||
129+
Object.keys(result.selectedTags).length == 0
130+
) {
131+
let selectedTags = {};
132+
for (let i in linksFromStorage) {
133+
const item = linksFromStorage[i];
134+
const label = item.title;
135+
//Remind preferences
136+
selectedTags = {
137+
...selectedTags,
138+
[item.id]: label,
139+
};
140+
}
141+
chrome.storage.local.set({
142+
selectedTags,
143+
});
144+
$(".tag").addClass("checked");
145+
} else {
146+
chrome.storage.local.set({ selectedTags: {} });
147+
$(".tag").removeClass("checked");
148+
}
149+
port.postMessage({ event: "syncBookmarks" });
150+
});
151+
port.postMessage({ event: "syncBookmarks" });
152+
});
153+
154+
$(".push").click(() => {
155+
firebaseHandler.setBookmarks(initData);
156+
port.postMessage({ event: "syncBookmarks" });
157+
});
158+
$("#show-submit").click(e => {
159+
$("#submit-form").show();
160+
$(e.target).hide();
161+
});
162+
$("#submit-form").submit(e => {
163+
e.preventDefault();
164+
firebaseHandler.sendSuggestion(
165+
{
166+
title: $("#title").val(),
167+
url: $("#url").val(),
168+
},
169+
hasBeenSent => {
170+
$("#submit-form").html(
171+
hasBeenSent
172+
? "<p>Thank you ! Suggestion registered</p>"
173+
: "<p>Suggestion already present in db</p>",
174+
);
175+
},
176+
);
177+
});
178+
});

‎src/manifest.json

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "Xp client : liens",
3+
"version": "0.1.7",
4+
"manifest_version": 2,
5+
"description": "This extension synchronize awesome links of client experience direction",
6+
"homepage_url": "http://cdiscount.com",
7+
"icons": {
8+
"16": "xpclient-16.png",
9+
"48": "xpclient-48.png",
10+
"128": "xpclient-128.png"
11+
},
12+
"browser_action": {
13+
"default_icon": "xpclient-128.png",
14+
"default_title": "Xp client links",
15+
"default_popup": "popup.html"
16+
},
17+
"permissions": ["bookmarks", "storage", "gcm", "identity", "identity.email"],
18+
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
19+
"background": {
20+
"page": "background.html"
21+
}
22+
}

‎src/options.html

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title></title>
6+
</head>
7+
<body></body>
8+
</html>

‎src/popup.html

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!DOCTYPE html>
2+
3+
<div id="mainPopup">
4+
<h1>Xp client Links</h1>
5+
<p>
6+
Here are some links for xp client.
7+
</p>
8+
<div id="menu-tags"></div>
9+
<span class="badge all-badges" data-id="all">Tous</span>
10+
11+
<button class="push">PUSH</button>
12+
13+
<br />
14+
<hr />
15+
<input
16+
type="text"
17+
class="form-control"
18+
id="custom-root-name"
19+
placeholder="Custom root name (xp client by default)"
20+
/>
21+
22+
<br />
23+
<hr />
24+
<button class="btn btn-default" id="show-submit">
25+
Want to submit a new link ?
26+
</button>
27+
<form id="submit-form">
28+
<div class="form-group">
29+
<label for="title">Title</label>
30+
<input type="text" class="form-control" id="title" placeholder="Title" />
31+
</div>
32+
<div class="form-group">
33+
<label for="url">URL</label>
34+
<input type="url" class="form-control" id="url" placeholder="URL" />
35+
</div>
36+
<button type="submit" class="btn btn-default">Submit a new link</button>
37+
</form>
38+
<br />
39+
<hr />
40+
41+
<div>
42+
<h2>Help</h2>
43+
By selecting tags, you will get in your bookmarks a new directory named "xp
44+
client" and under it, you will have, by tags, sub directories with all fresh
45+
links.
46+
<img style="width: 100%;" src="./img/screenshot.jpg" />
47+
</div>
48+
</div>

‎utils/build.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
var webpack = require("webpack"),
2+
config = require("../webpack.config");
3+
4+
delete config.chromeExtensionBoilerplate;
5+
6+
webpack(config, function(err) {
7+
if (err) throw err;
8+
});

‎utils/env.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// tiny wrapper with default env vars
2+
module.exports = {
3+
NODE_ENV: process.env.NODE_ENV || "development",
4+
PORT: process.env.PORT || 3000
5+
};

‎utils/webserver.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
var WebpackDevServer = require("webpack-dev-server"),
2+
webpack = require("webpack"),
3+
config = require("../webpack.config"),
4+
env = require("./env"),
5+
path = require("path");
6+
7+
var options = config.chromeExtensionBoilerplate || {};
8+
var excludeEntriesToHotReload = options.notHotReload || [];
9+
10+
for (var entryName in config.entry) {
11+
if (excludeEntriesToHotReload.indexOf(entryName) === -1) {
12+
config.entry[entryName] = [
13+
"webpack-dev-server/client?http://localhost:" + env.PORT,
14+
"webpack/hot/dev-server"
15+
].concat(config.entry[entryName]);
16+
}
17+
}
18+
19+
config.plugins = [new webpack.HotModuleReplacementPlugin()].concat(
20+
config.plugins || []
21+
);
22+
23+
delete config.chromeExtensionBoilerplate;
24+
25+
var compiler = webpack(config);
26+
27+
var server = new WebpackDevServer(compiler, {
28+
hot: true,
29+
contentBase: path.join(__dirname, "../build"),
30+
sockPort: env.PORT,
31+
headers: {
32+
"Access-Control-Allow-Origin": "*"
33+
},
34+
disableHostCheck: true
35+
});
36+
37+
server.listen(env.PORT);

‎webpack.config.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
var webpack = require("webpack"),
2+
path = require("path"),
3+
fileSystem = require("fs"),
4+
env = require("./utils/env"),
5+
CleanWebpackPlugin = require("clean-webpack-plugin").CleanWebpackPlugin,
6+
CopyWebpackPlugin = require("copy-webpack-plugin"),
7+
HtmlWebpackPlugin = require("html-webpack-plugin"),
8+
WriteFilePlugin = require("write-file-webpack-plugin");
9+
10+
// load the secrets
11+
var alias = {};
12+
13+
var secretsPath = path.join(__dirname, "secrets." + env.NODE_ENV + ".js");
14+
15+
var fileExtensions = [
16+
"jpg",
17+
"jpeg",
18+
"png",
19+
"gif",
20+
"eot",
21+
"otf",
22+
"svg",
23+
"ttf",
24+
"woff",
25+
"woff2"
26+
];
27+
28+
if (fileSystem.existsSync(secretsPath)) {
29+
alias["secrets"] = secretsPath;
30+
}
31+
32+
var options = {
33+
mode: process.env.NODE_ENV || "development",
34+
entry: {
35+
popup: path.join(__dirname, "src", "js", "popup.js"),
36+
options: path.join(__dirname, "src", "js", "options.js"),
37+
background: path.join(__dirname, "src", "js", "background.js")
38+
},
39+
devtool: "inline-source-map",
40+
output: {
41+
path: path.join(__dirname, "build"),
42+
filename: "[name].bundle.js"
43+
},
44+
module: {
45+
rules: [
46+
{
47+
test: /\.css$/,
48+
loader: "style-loader!css-loader",
49+
exclude: /node_modules/
50+
},
51+
{
52+
test: new RegExp(".(" + fileExtensions.join("|") + ")$"),
53+
loader: "file-loader?name=[name].[ext]",
54+
exclude: /node_modules/
55+
},
56+
{
57+
test: /\.html$/,
58+
loader: "html-loader",
59+
exclude: /node_modules/
60+
}
61+
]
62+
},
63+
resolve: {
64+
alias: alias
65+
},
66+
plugins: [
67+
// clean the build folder
68+
new CleanWebpackPlugin(),
69+
// expose and write the allowed env vars on the compiled bundle
70+
new webpack.EnvironmentPlugin(["NODE_ENV"]),
71+
new CopyWebpackPlugin([
72+
{
73+
from: "src/manifest.json",
74+
transform: function(content, path) {
75+
// generates the manifest file using the package.json informations
76+
return Buffer.from(
77+
JSON.stringify({
78+
description: process.env.npm_package_description,
79+
version: process.env.npm_package_version,
80+
...JSON.parse(content.toString())
81+
})
82+
);
83+
}
84+
}
85+
]),
86+
new HtmlWebpackPlugin({
87+
template: path.join(__dirname, "src", "popup.html"),
88+
filename: "popup.html",
89+
chunks: ["popup"]
90+
}),
91+
new HtmlWebpackPlugin({
92+
template: path.join(__dirname, "src", "options.html"),
93+
filename: "options.html",
94+
chunks: ["options"]
95+
}),
96+
new HtmlWebpackPlugin({
97+
template: path.join(__dirname, "src", "background.html"),
98+
filename: "background.html",
99+
chunks: ["background"]
100+
}),
101+
new WriteFilePlugin()
102+
]
103+
};
104+
105+
if (env.NODE_ENV === "development") {
106+
options.devtool = "cheap-module-eval-source-map";
107+
}
108+
109+
module.exports = options;

‎yarn.lock

+5,316
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.