vm-export.vue 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. <template>
  2. <section>
  3. <h3 v-text="i18n('labelDataExport')"></h3>
  4. <button v-text="i18n('buttonExportData')" @click="handleExport" :disabled="exporting"></button>
  5. <div class="mt-1">
  6. <setting-check name="exportValues" :label="i18n('labelExportScriptData')" />
  7. </div>
  8. <modal
  9. v-if="store.ffDownload"
  10. transition="in-out"
  11. :visible="!!store.ffDownload.url"
  12. @close="store.ffDownload = {}">
  13. <div class="export-modal modal-content">
  14. <a :download="store.ffDownload.name" :href="store.ffDownload.url">
  15. Right click and save as<br />
  16. <strong>scripts.zip</strong>
  17. </a>
  18. </div>
  19. </modal>
  20. </section>
  21. </template>
  22. <script>
  23. import Modal from 'vueleton/lib/modal/bundle';
  24. import { getScriptName, sendCmd } from '#/common';
  25. import { objectGet } from '#/common/object';
  26. import options from '#/common/options';
  27. import ua from '#/common/ua';
  28. import SettingCheck from '#/common/ui/setting-check';
  29. import { downloadBlob } from '#/common/download';
  30. import loadZip from '#/common/zip';
  31. import { store } from '../../utils';
  32. /**
  33. * Note:
  34. * - Firefox does not support multiline <select>
  35. */
  36. if (ua.isFirefox) store.ffDownload = {};
  37. export default {
  38. components: {
  39. SettingCheck,
  40. Modal,
  41. },
  42. data() {
  43. return {
  44. store,
  45. exporting: false,
  46. };
  47. },
  48. methods: {
  49. async handleExport() {
  50. this.exporting = true;
  51. try {
  52. const blob = await exportData();
  53. download(blob);
  54. } catch (err) {
  55. console.error(err);
  56. }
  57. this.exporting = false;
  58. },
  59. },
  60. };
  61. function leftpad(src, length, pad = '0') {
  62. let str = `${src}`;
  63. while (str.length < length) str = pad + str;
  64. return str;
  65. }
  66. function getTimestamp() {
  67. const date = new Date();
  68. return `${
  69. date.getFullYear()
  70. }-${
  71. leftpad(date.getMonth() + 1, 2)
  72. }-${
  73. leftpad(date.getDate(), 2)
  74. }_${
  75. leftpad(date.getHours(), 2)
  76. }.${
  77. leftpad(date.getMinutes(), 2)
  78. }.${
  79. leftpad(date.getSeconds(), 2)
  80. }`;
  81. }
  82. function getExportname() {
  83. return `scripts_${getTimestamp()}.zip`;
  84. }
  85. function download(blob) {
  86. /* Old FF can't download blobs https://bugzil.la/1420419, fixed by enabling OOP:
  87. * v56 in Windows https://bugzil.la/1357486
  88. * v61 in MacOS https://bugzil.la/1385403
  89. * v63 in Linux https://bugzil.la/1357487 */
  90. const FF = ua.isFirefox;
  91. // eslint-disable-next-line no-nested-ternary
  92. if (FF && (ua.os === 'win' ? FF < 56 : ua.os === 'mac' ? FF < 61 : FF < 63)) {
  93. const reader = new FileReader();
  94. reader.onload = () => {
  95. store.ffDownload = {
  96. name: getExportname(),
  97. url: reader.result,
  98. };
  99. };
  100. reader.readAsDataURL(blob);
  101. } else {
  102. downloadBlob(blob, getExportname());
  103. }
  104. }
  105. function normalizeFilename(name) {
  106. return name.replace(/[\\/:*?"<>|]/g, '-');
  107. }
  108. async function exportData() {
  109. const withValues = options.get('exportValues');
  110. const data = await sendCmd('ExportZip', {
  111. values: withValues,
  112. });
  113. const names = {};
  114. const vm = {
  115. scripts: {},
  116. settings: options.get(),
  117. };
  118. delete vm.settings.sync;
  119. if (withValues) vm.values = {};
  120. const files = (objectGet(data, 'items') || []).map(({ script, code }) => {
  121. let name = normalizeFilename(getScriptName(script));
  122. if (names[name]) {
  123. names[name] += 1;
  124. name = `${name}_${names[name]}`;
  125. } else names[name] = 1;
  126. const { lastModified, lastUpdated } = script.props;
  127. const info = {
  128. custom: script.custom,
  129. config: script.config,
  130. position: script.props.position,
  131. lastModified,
  132. lastUpdated,
  133. };
  134. if (withValues) {
  135. // `values` are related to scripts by `props.id` in Violentmonkey,
  136. // but by the global `props.uri` when exported.
  137. const values = data.values[script.props.id];
  138. if (values) vm.values[script.props.uri] = values;
  139. }
  140. vm.scripts[name] = info;
  141. return {
  142. name: `${name}.user.js`,
  143. content: code,
  144. lastModDate: new Date(lastUpdated || lastModified),
  145. };
  146. });
  147. files.push({
  148. name: 'violentmonkey',
  149. content: JSON.stringify(vm),
  150. });
  151. const zip = await loadZip();
  152. const blobWriter = new zip.BlobWriter('application/zip');
  153. const writer = new zip.ZipWriter(blobWriter, { bufferedWrite: true, keepOrder: false });
  154. await Promise.all(files.map(file => writer.add(file.name, new zip.TextReader(file.content), {
  155. lastModDate: file.lastModDate,
  156. })));
  157. const blob = await writer.close();
  158. return blob;
  159. }
  160. </script>
  161. <style>
  162. .export-modal {
  163. width: 13rem;
  164. }
  165. </style>