vm-export.vue 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. <template>
  2. <div class="export">
  3. <div class="flex flex-wrap center-items mr-1c">
  4. <button v-text="i18n('buttonExportData')" @click="handleExport" :disabled="exporting"/>
  5. <setting-text name="exportNameTemplate" ref="tpl" has-reset :has-save="false" :rows="1"
  6. class="tpl flex flex-1 center-items mr-1c"/>
  7. <tooltip :content="i18n('msgDateFormatInfo', dateTokens)" placement="left">
  8. <a href="https://momentjs.com/docs/#/displaying/format/" target="_blank">
  9. <icon name="info"/>
  10. </a>
  11. </tooltip>
  12. <span hidden v-text="getFileName()"/>
  13. </div>
  14. <div class="mt-1">
  15. <setting-check name="exportValues" :label="i18n('labelExportScriptData')" />
  16. </div>
  17. <modal
  18. v-if="store.ffDownload"
  19. transition="in-out"
  20. :visible="!!store.ffDownload.url"
  21. @close="store.ffDownload = {}">
  22. <div class="modal-content">
  23. <a :download="store.ffDownload.name" :href="store.ffDownload.url">
  24. Right click and save as<br />
  25. <strong>scripts.zip</strong>
  26. </a>
  27. </div>
  28. </modal>
  29. </div>
  30. </template>
  31. <script>
  32. import Modal from 'vueleton/lib/modal/bundle';
  33. import Tooltip from 'vueleton/lib/tooltip/bundle';
  34. import Icon from '#/common/ui/icon';
  35. import { getScriptName, sendCmdDirectly } from '#/common';
  36. import { formatDate, DATE_FMT } from '#/common/date';
  37. import { objectGet } from '#/common/object';
  38. import options from '#/common/options';
  39. import ua from '#/common/ua';
  40. import SettingCheck from '#/common/ui/setting-check';
  41. import SettingText from '#/common/ui/setting-text';
  42. import { downloadBlob } from '#/common/download';
  43. import loadZip from '#/common/zip';
  44. import { store } from '../../utils';
  45. /**
  46. * Note:
  47. * - Firefox does not support multiline <select>
  48. */
  49. if (ua.isFirefox) store.ffDownload = {};
  50. export default {
  51. components: {
  52. SettingCheck,
  53. SettingText,
  54. Icon,
  55. Modal,
  56. Tooltip,
  57. },
  58. data() {
  59. return {
  60. store,
  61. dateTokens: Object.keys(DATE_FMT).join(', '),
  62. exporting: false,
  63. };
  64. },
  65. methods: {
  66. async handleExport() {
  67. try {
  68. this.exporting = true;
  69. download(await exportData(), this.getFileName());
  70. } finally {
  71. this.exporting = false;
  72. }
  73. },
  74. getFileName() {
  75. const { tpl } = this.$refs;
  76. return tpl && `${formatDate(tpl.value?.trim() || tpl.defaultValue)}.zip`;
  77. },
  78. },
  79. };
  80. function download(blob, fileName) {
  81. /* Old FF can't download blobs https://bugzil.la/1420419, fixed by enabling OOP:
  82. * v56 in Windows https://bugzil.la/1357486
  83. * v61 in MacOS https://bugzil.la/1385403
  84. * v63 in Linux https://bugzil.la/1357487 */
  85. const FF = ua.isFirefox;
  86. // eslint-disable-next-line no-nested-ternary
  87. if (FF && (ua.os === 'win' ? FF < 56 : ua.os === 'mac' ? FF < 61 : FF < 63)) {
  88. const reader = new FileReader();
  89. reader.onload = () => {
  90. store.ffDownload = {
  91. name: fileName,
  92. url: reader.result,
  93. };
  94. };
  95. reader.readAsDataURL(blob);
  96. } else {
  97. downloadBlob(blob, fileName);
  98. }
  99. }
  100. function normalizeFilename(name) {
  101. return name.replace(/[\\/:*?"<>|]/g, '-');
  102. }
  103. async function exportData() {
  104. const withValues = options.get('exportValues');
  105. const data = await sendCmdDirectly('ExportZip', {
  106. values: withValues,
  107. });
  108. const names = {};
  109. const vm = {
  110. scripts: {},
  111. settings: options.get(),
  112. };
  113. delete vm.settings.sync;
  114. if (withValues) vm.values = {};
  115. const files = (objectGet(data, 'items') || []).map(({ script, code }) => {
  116. let name = normalizeFilename(getScriptName(script));
  117. if (names[name]) {
  118. names[name] += 1;
  119. name = `${name}_${names[name]}`;
  120. } else names[name] = 1;
  121. const { lastModified, lastUpdated } = script.props;
  122. const info = {
  123. custom: script.custom,
  124. config: script.config,
  125. position: script.props.position,
  126. lastModified,
  127. lastUpdated,
  128. };
  129. if (withValues) {
  130. // `values` are related to scripts by `props.id` in Violentmonkey,
  131. // but by the global `props.uri` when exported.
  132. const values = data.values[script.props.id];
  133. if (values) vm.values[script.props.uri] = values;
  134. }
  135. vm.scripts[name] = info;
  136. return {
  137. name: `${name}.user.js`,
  138. content: code,
  139. lastModDate: new Date(lastUpdated || lastModified),
  140. };
  141. });
  142. files.push({
  143. name: 'violentmonkey',
  144. content: JSON.stringify(vm),
  145. });
  146. const zip = await loadZip();
  147. const blobWriter = new zip.BlobWriter('application/zip');
  148. const writer = new zip.ZipWriter(blobWriter, { bufferedWrite: true, keepOrder: false });
  149. await Promise.all(files.map(file => writer.add(file.name, new zip.TextReader(file.content), {
  150. lastModDate: file.lastModDate,
  151. })));
  152. const blob = await writer.close();
  153. return blob;
  154. }
  155. </script>
  156. <style>
  157. .export {
  158. .modal-content {
  159. width: 13rem;
  160. }
  161. .icon {
  162. width: 16px;
  163. height: 16px;
  164. fill: var(--fg);
  165. }
  166. .tpl {
  167. max-width: 30em;
  168. &:focus-within ~ [hidden] {
  169. display: initial;
  170. }
  171. textarea {
  172. height: auto;
  173. resize: none;
  174. white-space: nowrap;
  175. overflow: hidden;
  176. min-width: 10em;
  177. }
  178. }
  179. }
  180. </style>