install-usercss.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. /* global CodeMirror semverCompare makeLink closeCurrentTab */
  2. /* global messageBox download chromeLocal */
  3. 'use strict';
  4. (() => {
  5. const params = new URLSearchParams(location.search);
  6. let liveReload = false;
  7. let installed = false;
  8. const tabId = Number(params.get('tabId'));
  9. let port;
  10. if (params.has('direct')) {
  11. $('.live-reload').remove();
  12. getCodeDirectly();
  13. } else {
  14. port = chrome.tabs.connect(tabId);
  15. port.postMessage({method: 'getSourceCode'});
  16. port.onMessage.addListener(msg => {
  17. switch (msg.method) {
  18. case 'getSourceCodeResponse':
  19. if (msg.error) {
  20. messageBox.alert(msg.error);
  21. } else {
  22. initSourceCode(msg.sourceCode);
  23. }
  24. break;
  25. case 'sourceCodeChanged':
  26. if (msg.error) {
  27. messageBox.alert(msg.error);
  28. } else {
  29. liveReloadUpdate(msg.sourceCode);
  30. }
  31. break;
  32. }
  33. });
  34. port.onDisconnect.addListener(closeCurrentTab);
  35. }
  36. const cm = CodeMirror($('.main'), {readOnly: true});
  37. let liveReloadPending = Promise.resolve();
  38. window.addEventListener('resize', adjustCodeHeight);
  39. setTimeout(() => {
  40. if (!installed) {
  41. const div = $element({});
  42. $('.header').appendChild($element({
  43. className: 'lds-spinner',
  44. appendChild: new Array(12).fill(div).map(e => e.cloneNode()),
  45. }));
  46. }
  47. }, 200);
  48. function liveReloadUpdate(sourceCode) {
  49. liveReloadPending = liveReloadPending.then(() => {
  50. const scrollInfo = cm.getScrollInfo();
  51. const cursor = cm.getCursor();
  52. cm.setValue(sourceCode);
  53. cm.setCursor(cursor);
  54. cm.scrollTo(scrollInfo.left, scrollInfo.top);
  55. return sendMessage({
  56. id: installed.id,
  57. method: 'saveUsercss',
  58. reason: 'update',
  59. sourceCode
  60. }).then(updateMeta)
  61. .catch(showError);
  62. });
  63. }
  64. function updateMeta(style, dup) {
  65. const data = style.usercssData;
  66. const dupData = dup && dup.usercssData;
  67. const versionTest = dup && semverCompare(data.version, dupData.version);
  68. // update editor
  69. cm.setPreprocessor(data.preprocessor);
  70. // update metas
  71. document.title = `${installButtonLabel()} ${data.name}`;
  72. $('.install').textContent = installButtonLabel();
  73. $('.set-update-url').title = dup && dup.updateUrl && t('installUpdateFrom', dup.updateUrl) || '';
  74. $('.meta-name').textContent = data.name;
  75. $('.meta-version').textContent = data.version;
  76. $('.meta-description').textContent = data.description;
  77. if (data.author) {
  78. $('.meta-author').parentNode.style.display = '';
  79. $('.meta-author').textContent = '';
  80. $('.meta-author').appendChild(makeAuthor(data.author));
  81. } else {
  82. $('.meta-author').parentNode.style.display = 'none';
  83. }
  84. $('.meta-license').parentNode.style.display = data.license ? '' : 'none';
  85. $('.meta-license').textContent = data.license;
  86. $('.applies-to').textContent = '';
  87. getAppliesTo(style).forEach(pattern =>
  88. $('.applies-to').appendChild($element({tag: 'li', textContent: pattern}))
  89. );
  90. $('.external-link').textContent = '';
  91. const externalLink = makeExternalLink();
  92. if (externalLink) {
  93. $('.external-link').appendChild(externalLink);
  94. }
  95. $('.header').classList.add('meta-init');
  96. $('.header').classList.remove('meta-init-error');
  97. setTimeout(() => $('.lds-spinner') && $('.lds-spinner').remove(), 1000);
  98. showError('');
  99. requestAnimationFrame(adjustCodeHeight);
  100. function makeAuthor(text) {
  101. const match = text.match(/^(.+?)(?:\s+<(.+?)>)?(?:\s+\((.+?)\))$/);
  102. if (!match) {
  103. return document.createTextNode(text);
  104. }
  105. const [, name, email, url] = match;
  106. const frag = document.createDocumentFragment();
  107. if (email) {
  108. frag.appendChild(makeLink(`mailto:${email}`, name));
  109. } else {
  110. frag.appendChild($element({
  111. tag: 'span',
  112. textContent: name
  113. }));
  114. }
  115. if (url) {
  116. frag.appendChild(makeLink(
  117. url,
  118. $element({
  119. tag: 'svg#svg',
  120. viewBox: '0 0 20 20',
  121. class: 'icon',
  122. appendChild: $element({
  123. tag: 'svg#path',
  124. d: 'M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z'
  125. })
  126. })
  127. ));
  128. }
  129. return frag;
  130. }
  131. function makeExternalLink() {
  132. const urls = [];
  133. if (data.homepageURL) {
  134. urls.push([data.homepageURL, t('externalHomepage')]);
  135. }
  136. if (data.supportURL) {
  137. urls.push([data.supportURL, t('externalSupport')]);
  138. }
  139. if (urls.length) {
  140. return $element({appendChild: [
  141. $element({tag: 'h3', textContent: t('externalLink')}),
  142. $element({tag: 'ul', appendChild: urls.map(args =>
  143. $element({tag: 'li', appendChild: makeLink(...args)})
  144. )})
  145. ]});
  146. }
  147. }
  148. function installButtonLabel() {
  149. return t(
  150. installed ? 'installButtonInstalled' :
  151. !dup ? 'installButton' :
  152. versionTest > 0 ? 'installButtonUpdate' : 'installButtonReinstall'
  153. );
  154. }
  155. }
  156. function showError(err) {
  157. $('.warnings').textContent = '';
  158. if (err) {
  159. $('.warnings').appendChild(buildWarning(err));
  160. }
  161. $('.warnings').classList.toggle('visible', Boolean(err));
  162. $('.container').classList.toggle('has-warnings', Boolean(err));
  163. adjustCodeHeight();
  164. }
  165. function install(style) {
  166. installed = style;
  167. $$('.warning')
  168. .forEach(el => el.remove());
  169. $('.install').disabled = true;
  170. $('.install').classList.add('installed');
  171. $('.set-update-url input[type=checkbox]').disabled = true;
  172. $('.set-update-url').title = style.updateUrl ?
  173. t('installUpdateFrom', style.updateUrl) : '';
  174. updateMeta(style);
  175. sendMessage({method: 'openEditor', id: style.id});
  176. if (!liveReload) {
  177. chrome.runtime.sendMessage({method: 'closeTab'});
  178. }
  179. window.dispatchEvent(new CustomEvent('installed'));
  180. }
  181. function initSourceCode(sourceCode) {
  182. cm.setValue(sourceCode);
  183. cm.refresh();
  184. sendMessage({method: 'buildUsercss', sourceCode, checkDup: true})
  185. .then(init)
  186. .catch(err => {
  187. $('.header').classList.add('meta-init-error');
  188. showError(err);
  189. });
  190. }
  191. function buildWarning(err) {
  192. return $element({className: 'warning', appendChild: [
  193. t('parseUsercssError'),
  194. $element({tag: 'pre', textContent: String(err)})
  195. ]});
  196. }
  197. function init({style, dup}) {
  198. const data = style.usercssData;
  199. const dupData = dup && dup.usercssData;
  200. const versionTest = dup && semverCompare(data.version, dupData.version);
  201. updateMeta(style, dup);
  202. // update UI
  203. if (versionTest < 0) {
  204. $('.actions').parentNode.insertBefore(
  205. $element({className: 'warning', textContent: t('versionInvalidOlder')}),
  206. $('.actions')
  207. );
  208. }
  209. $('button.install').onclick = () => {
  210. (!dup ?
  211. Promise.resolve(true) :
  212. messageBox.confirm(t('styleInstallOverwrite', [
  213. data.name,
  214. dupData.version,
  215. data.version,
  216. ]))
  217. ).then(ok => ok &&
  218. sendMessage(Object.assign(style, {method: 'saveUsercss', reason: 'update'}))
  219. .then(install)
  220. .catch(err => messageBox.alert(t('styleInstallFailed', err))));
  221. };
  222. // set updateUrl
  223. const setUpdate = $('.set-update-url input[type=checkbox]');
  224. const updateUrl = new URL(params.get('updateUrl'));
  225. $('.set-update-url > span').textContent = t('installUpdateFromLabel');
  226. if (dup && dup.updateUrl === updateUrl.href) {
  227. setUpdate.checked = true;
  228. // there is no way to "unset" updateUrl, you can only overwrite it.
  229. setUpdate.disabled = true;
  230. } else if (updateUrl.protocol !== 'file:') {
  231. setUpdate.checked = true;
  232. style.updateUrl = updateUrl.href;
  233. }
  234. setUpdate.onchange = e => {
  235. if (e.target.checked) {
  236. style.updateUrl = updateUrl.href;
  237. } else {
  238. delete style.updateUrl;
  239. }
  240. };
  241. if (!port) {
  242. return;
  243. }
  244. // live reload
  245. const setLiveReload = $('.live-reload input[type=checkbox]');
  246. if (updateUrl.protocol !== 'file:') {
  247. setLiveReload.parentNode.remove();
  248. } else {
  249. setLiveReload.addEventListener('change', () => {
  250. liveReload = setLiveReload.checked;
  251. if (installed) {
  252. const method = 'liveReload' + (liveReload ? 'Start' : 'Stop');
  253. port.postMessage({method});
  254. }
  255. });
  256. window.addEventListener('installed', () => {
  257. if (liveReload) {
  258. port.postMessage({method: 'liveReloadStart'});
  259. }
  260. });
  261. }
  262. }
  263. function getAppliesTo(style) {
  264. function *_gen() {
  265. for (const section of style.sections) {
  266. for (const type of ['urls', 'urlPrefixes', 'domains', 'regexps']) {
  267. if (section[type]) {
  268. yield *section[type];
  269. }
  270. }
  271. }
  272. }
  273. const result = [..._gen()];
  274. if (!result.length) {
  275. result.push(chrome.i18n.getMessage('appliesToEverything'));
  276. }
  277. return result;
  278. }
  279. function adjustCodeHeight() {
  280. // Chrome-only bug (apparently): it doesn't limit the scroller element height
  281. const scroller = cm.display.scroller;
  282. const prevWindowHeight = adjustCodeHeight.prevWindowHeight;
  283. if (scroller.scrollHeight === scroller.clientHeight ||
  284. prevWindowHeight && window.innerHeight !== prevWindowHeight) {
  285. adjustCodeHeight.prevWindowHeight = window.innerHeight;
  286. cm.setSize(null, $('.main').offsetHeight - $('.warnings').offsetHeight);
  287. }
  288. }
  289. function getCodeDirectly() {
  290. // FF applies page CSP even to content scripts, https://bugzil.la/1267027
  291. // To circumvent that, the bg process downloads the code directly
  292. const key = 'tempUsercssCode' + tabId;
  293. chrome.storage.local.get(key, data => {
  294. const code = data && data[key];
  295. // bg already downloaded the code
  296. if (typeof code === 'string') {
  297. initSourceCode(code);
  298. chrome.storage.local.remove(key);
  299. return;
  300. }
  301. // bg still downloads the code
  302. if (code && code.loading) {
  303. const waitForCodeInStorage = (changes, area) => {
  304. if (area === 'local' && key in changes) {
  305. initSourceCode(changes[key].newValue);
  306. chrome.storage.onChanged.removeListener(waitForCodeInStorage);
  307. chrome.storage.local.remove(key);
  308. }
  309. };
  310. chrome.storage.onChanged.addListener(waitForCodeInStorage);
  311. return;
  312. }
  313. // on the off-chance dbExecChromeStorage.getAll ran right after bg download was saved
  314. download(params.get('updateUrl'))
  315. .then(initSourceCode)
  316. .catch(err => messageBox.alert(t('styleInstallFailed', String(err))));
  317. });
  318. }
  319. })();