Skip to content

Commit 79c6506

Browse files
matheusfaustinoMottie
authored andcommitted
Implement Dropbox export (#82) (#393)
* Implement Dropbox export (#82) * Remove wrong dropbox api key * Improve implementation of Dropbox by using identity.launchWebAuthFlow api and get rid of web_accessible_resources * We don't need a dropbox receiver anymore, remove constante with the html file * Implement compression in dropbox export * Add LICENSE file from dropbox and zipjs * Fix code style error * Fix code style and folder structure of the feature * Fix eslint error in dropbox implementation * Add real dropbox api key from stylus dropbox account * For test only: fixed addon's ID on firefox * Change the file not found message to a better one * Add dropdown style on export and import buttons * Changes arrow from buttons to svg * Remove applications entry on manifest.json * Remove unnecessary break line
1 parent 5c6cf72 commit 79c6506

14 files changed

+10907
-2
lines changed

_locales/en/messages.json

+33
Original file line numberDiff line numberDiff line change
@@ -1294,5 +1294,38 @@
12941294
"writeStyleForURL": {
12951295
"message": "this URL",
12961296
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
1297+
},
1298+
"syncDropboxStyles": {
1299+
"message": "Dropbox Export"
1300+
},
1301+
"retrieveDropboxSync": {
1302+
"message": "Dropbox Import"
1303+
},
1304+
"overwriteFileExport": {
1305+
"message": "Do you want to overwrite an existing file?"
1306+
},
1307+
"exportSavedSuccess": {
1308+
"message": "File saved with success"
1309+
},
1310+
"noFileToImport": {
1311+
"message": "To import your styles, you should export it first."
1312+
},
1313+
"connectingDropbox": {
1314+
"message": "Connecting Dropbox..."
1315+
},
1316+
"gettingStyles": {
1317+
"message": "Getting all styles..."
1318+
},
1319+
"zipStyles": {
1320+
"message": "Zipping styles..."
1321+
},
1322+
"unzipStyles": {
1323+
"message": "Unzipping styles..."
1324+
},
1325+
"readingStyles": {
1326+
"message": "Reading styles..."
1327+
},
1328+
"uploadingFile": {
1329+
"message": "Uploading File..."
12971330
}
12981331
}

manage.html

+31-2
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,17 @@ <h2 class="style-name">
161161
<script src="manage/updater-ui.js" async></script>
162162
<script src="manage/object-diff.js" async></script>
163163
<script src="manage/import-export.js" async></script>
164+
164165
<script src="manage/incremental-search.js" async></script>
165166
<script src="msgbox/msgbox.js" async></script>
166167
<script src="js/sections-equal.js" async></script>
167168
<script src="js/storage-util.js" async></script>
169+
170+
<script src="sync/vendor/dropbox/dropbox-sdk.js" async></script>
171+
<script src="sync/vendor/zipjs/zip.js" defer></script>
172+
<script src="sync/compress-text.js" defer></script>
173+
<script src="sync/cross-browser-functions.js" defer></script>
174+
<script src="sync/import-export-dropbox.js" async></script>
168175
</head>
169176

170177
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
@@ -364,8 +371,30 @@ <h2 i18n-text="manageFilters">:
364371
<summary><h2 id="backup-title" i18n-text="backupButtons"></h2></summary>
365372
<span id="backup-message" i18n-text="backupMessage"></span>
366373
<div id="backup-buttons">
367-
<button id="file-all-styles" i18n-text="bckpInstStyles"></button>
368-
<button id="unfile-all-styles" i18n-text="retrieveBckp"></button>
374+
<div class="dropdown">
375+
<button class="dropbtn">
376+
<span>Export</span>
377+
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
378+
</button>
379+
380+
<div class="dropdown-content">
381+
<a href="#" id="file-all-styles" i18n-text="bckpInstStyles"></a>
382+
<a href="#" id="sync-dropbox-export" i18n-text="syncDropboxStyles"></a>
383+
</div>
384+
</div>
385+
386+
<div class="dropdown">
387+
<button class="dropbtn">
388+
<span>Import</span>
389+
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
390+
</button>
391+
392+
<div class="dropdown-content">
393+
<a href="#" id="unfile-all-styles" i18n-text="retrieveBckp"></a>
394+
<a href="#" id="sync-dropbox-import" i18n-text="retrieveDropboxSync"></a>
395+
</div>
396+
</div>
397+
369398
</div>
370399
</details>
371400

manage/manage.css

+48
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,54 @@ input[id^="manage.newUI"] {
10471047
text-overflow: ellipsis;
10481048
}
10491049

1050+
/* export/import buttons */
1051+
#backup-buttons .dropbtn {
1052+
padding: 3px 7px;
1053+
cursor: pointer;
1054+
text-overflow: inherit;
1055+
}
1056+
1057+
#backup-buttons .dropbtn span {
1058+
display: inline-block;
1059+
margin-right: 7px;
1060+
}
1061+
1062+
#backup-buttons .dropdown {
1063+
position: relative;
1064+
display: inline-block;
1065+
}
1066+
1067+
#backup-buttons .dropdown-content {
1068+
display: none;
1069+
position: absolute;
1070+
background-color: #f9f9f9;
1071+
min-width: 160px;
1072+
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
1073+
z-index: 1;
1074+
}
1075+
1076+
#backup-buttons .dropdown-content a {
1077+
color: black;
1078+
padding: 12px 16px;
1079+
text-decoration: none;
1080+
display: block;
1081+
}
1082+
1083+
#backup-buttons .dropdown-content a:hover {
1084+
/* background-color: #f2f2f2 */
1085+
background-color: #e9e9e9
1086+
}
1087+
1088+
#backup-buttons .dropdown:hover .dropdown-content {
1089+
display: block;
1090+
}
1091+
1092+
#backup-buttons .dropdown:hover .dropbtn {
1093+
background-color: hsl(0, 0%, 95%);
1094+
border-color: hsl(0, 0%, 52%);
1095+
/* background-color: #3e8e41; */
1096+
}
1097+
10501098
/* sort font */
10511099
@font-face {
10521100
font-family: 'sorticon';

manifest.json

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"contextMenus",
2020
"storage",
2121
"alarms",
22+
"identity",
2223
"<all_urls>"
2324
],
2425
"background": {

sync/compress-text.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* global messageBox */
2+
/* global zip */
3+
'use strict';
4+
5+
onDOMready().then(() => {
6+
zip.workerScriptsPath = '/sync/vendor/zipjs/';
7+
});
8+
9+
/**
10+
* @param {String} filename
11+
* @param {String} text content of the file as text
12+
* @returns {Promise<Blob>} resolves to a blob object representing the zip file
13+
*/
14+
function createZipFileFromText(filename, text) {
15+
return new Promise((resolve, reject) => {
16+
zip.createWriter(new zip.BlobWriter('application/zip'), writer => {
17+
writer.add(filename, new zip.TextReader(text), function () {
18+
writer.close(blob => {
19+
resolve(blob);
20+
});
21+
});
22+
}, reject);
23+
});
24+
}
25+
26+
/**
27+
* @param {Object} blob object of zip file
28+
* @returns {Promise<String>} resolves to a string the content of the first file of the zip
29+
*/
30+
function readZipFileFromBlob(blob) {
31+
return new Promise((resolve, reject) => {
32+
zip.createReader(new zip.BlobReader(blob), zipReader => {
33+
zipReader.getEntries(entries => {
34+
entries[0].getData(new zip.BlobWriter('text/plain'), data => {
35+
zipReader.close();
36+
resolve(data);
37+
});
38+
});
39+
}, reject);
40+
});
41+
}

sync/cross-browser-functions.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict';
2+
3+
/**
4+
* @returns {String} returns a redirect URL to be used in |launchWebAuthFlow|
5+
*/
6+
function getRedirectUrlAuthFlow() {
7+
const browserApi = typeof browser === 'undefined' ? chrome : browser;
8+
return browserApi.identity.getRedirectURL();
9+
}
10+
11+
/**
12+
* @param {Object} details based on chrome api
13+
* @param {string} details.url url that initiates the auth flow
14+
* @param {boolean} details.interactive if it is true a window will be displayed
15+
* @return {Promise} returns the url containing the token for extraction
16+
*/
17+
function launchWebAuthFlow(details) {
18+
if (typeof browser === 'undefined') {
19+
return new Promise(resolve => {
20+
chrome.identity.launchWebAuthFlow(details, resolve);
21+
});
22+
}
23+
return browser.identity.launchWebAuthFlow(details);
24+
}

sync/import-export-dropbox.js

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/* global messageBox Dropbox createZipFileFromText readZipFileFromBlob launchWebAuthFlow getRedirectUrlAuthFlow importFromString resolve */
2+
'use strict';
3+
4+
const DROPBOX_API_KEY = 'zg52vphuapvpng9';
5+
const FILENAME_ZIP_FILE = 'stylus.json';
6+
const DROPBOX_FILE = 'stylus.zip';
7+
const API_ERROR_STATUS_FILE_NOT_FOUND = 409;
8+
const HTTP_STATUS_CANCEL = 499;
9+
10+
function messageProgressBar(data) {
11+
return messageBox({
12+
title: `${data.title}`,
13+
className: 'config-dialog',
14+
contents: [
15+
$create('p', data.text)
16+
],
17+
buttons: [{
18+
textContent: t('confirmClose'),
19+
dataset: {cmd: 'close'},
20+
}],
21+
}).then(() => {
22+
document.body.style.minWidth = '';
23+
document.body.style.minHeight = '';
24+
});
25+
}
26+
27+
function hasDropboxAccessToken() {
28+
return chromeLocal.getValue('dropbox_access_token');
29+
}
30+
31+
function requestDropboxAccessToken() {
32+
const client = new Dropbox.Dropbox({clientId: DROPBOX_API_KEY});
33+
const authUrl = client.getAuthenticationUrl(getRedirectUrlAuthFlow());
34+
return launchWebAuthFlow({url: authUrl, interactive: true})
35+
.then(urlReturned => {
36+
const params = new URLSearchParams(new URL(urlReturned).hash.replace('#', ''));
37+
chromeLocal.setValue('dropbox_access_token', params.get('access_token'));
38+
return params.get('access_token');
39+
});
40+
}
41+
42+
function uploadFileDropbox(client, stylesText) {
43+
return client.filesUpload({path: '/' + DROPBOX_FILE, contents: stylesText});
44+
}
45+
46+
$('#sync-dropbox-export').onclick = () => {
47+
const title = t('syncDropboxStyles');
48+
messageProgressBar({title: title, text: t('connectingDropbox')});
49+
50+
hasDropboxAccessToken()
51+
.then(token => token || requestDropboxAccessToken())
52+
.then(token => {
53+
const client = new Dropbox.Dropbox({
54+
clientId: DROPBOX_API_KEY,
55+
accessToken: token
56+
});
57+
return client.filesDownload({path: '/' + DROPBOX_FILE})
58+
.then(() => messageBox.confirm(t('overwriteFileExport')))
59+
.then(ok => {
60+
// deletes file if user want to
61+
if (!ok) {
62+
return Promise.reject({status: HTTP_STATUS_CANCEL});
63+
}
64+
return client.filesDelete({path: '/' + DROPBOX_FILE});
65+
})
66+
// file deleted with success, get styles and create a file
67+
.then(() => {
68+
messageProgressBar({title: title, text: t('gettingStyles')});
69+
return API.getStyles().then(styles => JSON.stringify(styles, null, '\t'));
70+
})
71+
// create zip file
72+
.then(stylesText => {
73+
messageProgressBar({title: title, text: t('zipStyles')});
74+
return createZipFileFromText(FILENAME_ZIP_FILE, stylesText);
75+
})
76+
// create file dropbox
77+
.then(zipedText => {
78+
messageProgressBar({title: title, text: t('uploadingFile')});
79+
return uploadFileDropbox(client, zipedText);
80+
})
81+
// gives feedback to user
82+
.then(() => messageProgressBar({title: title, text: t('exportSavedSuccess')}))
83+
// handle not found cases and cancel action
84+
.catch(error => {
85+
console.log(error);
86+
// saving file first time
87+
if (error.status === API_ERROR_STATUS_FILE_NOT_FOUND) {
88+
API.getStyles()
89+
.then(styles => {
90+
messageProgressBar({title: title, text: t('gettingStyles')});
91+
return JSON.stringify(styles, null, '\t');
92+
})
93+
.then(stylesText => {
94+
messageProgressBar({title: title, text: t('zipStyles')});
95+
return createZipFileFromText(FILENAME_ZIP_FILE, stylesText);
96+
})
97+
.then(zipedText => {
98+
messageProgressBar({title: title, text: t('uploadingFile')});
99+
return uploadFileDropbox(client, zipedText);
100+
})
101+
.then(() => messageProgressBar({title: title, text: t('exportSavedSuccess')}))
102+
.catch(err => messageBox.alert(err));
103+
return;
104+
}
105+
106+
// user cancelled the flow
107+
if (error.status === HTTP_STATUS_CANCEL) {
108+
return;
109+
}
110+
111+
console.error(error);
112+
});
113+
});
114+
};
115+
116+
$('#sync-dropbox-import').onclick = () => {
117+
const title = t('retrieveDropboxSync');
118+
messageProgressBar({title: title, text: t('connectingDropbox')});
119+
120+
hasDropboxAccessToken()
121+
.then(token => token || requestDropboxAccessToken())
122+
.then(token => {
123+
const client = new Dropbox.Dropbox({
124+
clientId: DROPBOX_API_KEY,
125+
accessToken: token
126+
});
127+
return client.filesDownload({path: '/' + DROPBOX_FILE})
128+
.then(response => {
129+
messageProgressBar({title: title, text: t('unzipStyles')});
130+
return readZipFileFromBlob(response.fileBlob);
131+
})
132+
.then(zipedFileBlob => {
133+
messageProgressBar({title: title, text: t('readingStyles')});
134+
document.body.style.cursor = 'wait';
135+
const fReader = new FileReader();
136+
fReader.onloadend = event => {
137+
const text = event.target.result;
138+
const maybeUsercss = !/^[\s\r\n]*\[/.test(text) &&
139+
(text.includes('==UserStyle==') || /==UserStyle==/i.test(text));
140+
(!maybeUsercss ?
141+
importFromString(text) :
142+
getOwnTab().then(tab => {
143+
tab.url = URL.createObjectURL(new Blob([text], {type: 'text/css'}));
144+
return API.installUsercss({direct: true, tab})
145+
.then(() => URL.revokeObjectURL(tab.url));
146+
})
147+
).then(numStyles => {
148+
document.body.style.cursor = '';
149+
resolve(numStyles);
150+
});
151+
};
152+
fReader.readAsText(zipedFileBlob, 'utf-8');
153+
})
154+
.catch(error => {
155+
// no file
156+
if (error.status === API_ERROR_STATUS_FILE_NOT_FOUND) {
157+
messageBox.alert(t('noFileToImport'));
158+
return;
159+
}
160+
messageBox.alert(error);
161+
});
162+
});
163+
};

0 commit comments

Comments
 (0)