vm-export.vue 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. <template>
  2. <section>
  3. <h3 v-text="i18n('labelDataExport')"></h3>
  4. <div class="export-list">
  5. <div
  6. v-for="(item, index) in items"
  7. :key="index"
  8. class="ellipsis"
  9. :class="{active: item.active}"
  10. @click="item.active = !item.active"
  11. v-text="getName(item)"
  12. />
  13. </div>
  14. <button v-text="i18n('buttonAllNone')" @click="toggleSelection()"></button>
  15. <button v-text="i18n('buttonExportData')" @click="exportData" :disabled="exporting"></button>
  16. <label class="ml-1">
  17. <setting-check name="exportValues" />
  18. <span v-text="i18n('labelExportScriptData')"></span>
  19. </label>
  20. <modal
  21. v-if="store.ffDownload"
  22. transition="in-out"
  23. :visible="!!store.ffDownload.url"
  24. @close="store.ffDownload = {}">
  25. <div class="export-modal modal-content">
  26. <a :download="store.ffDownload.name" :href="store.ffDownload.url">
  27. Right click and save as<br />
  28. <strong>scripts.zip</strong>
  29. </a>
  30. </div>
  31. </modal>
  32. </section>
  33. </template>
  34. <script>
  35. import Modal from 'vueleton/lib/modal';
  36. import { sendMessage, getLocaleString } from '#/common';
  37. import { objectGet } from '#/common/object';
  38. import options from '#/common/options';
  39. import { isFirefox } from '#/common/ua';
  40. import SettingCheck from '#/common/ui/setting-check';
  41. import { downloadBlob } from '#/common/download';
  42. import { store } from '../../utils';
  43. /**
  44. * Note:
  45. * - Firefox does not support multiline <select>
  46. */
  47. if (isFirefox) store.ffDownload = {};
  48. export default {
  49. components: {
  50. SettingCheck,
  51. Modal,
  52. },
  53. data() {
  54. return {
  55. isFirefox,
  56. store,
  57. exporting: false,
  58. items: [],
  59. };
  60. },
  61. watch: {
  62. 'store.scripts': 'initItems',
  63. },
  64. computed: {
  65. selectedIds() {
  66. return this.items.filter(item => item.active).map(item => item.script.props.id);
  67. },
  68. },
  69. created() {
  70. this.initItems();
  71. },
  72. methods: {
  73. initItems() {
  74. this.items = (store.scripts || [])
  75. .filter(({ config: { removed } }) => !removed)
  76. .map(script => ({
  77. script,
  78. active: true,
  79. }));
  80. },
  81. toggleSelection() {
  82. if (!store.scripts.length) return;
  83. const active = this.selectedIds.length < store.scripts.length;
  84. this.items.forEach(item => { item.active = active; });
  85. },
  86. exportData() {
  87. this.exporting = true;
  88. Promise.resolve(exportData(this.selectedIds))
  89. .then(download)
  90. .catch(err => {
  91. console.error(err);
  92. })
  93. .then(() => {
  94. this.exporting = false;
  95. });
  96. },
  97. getName(item) {
  98. return item.script.custom.name || getLocaleString(item.script.meta, 'name');
  99. },
  100. },
  101. };
  102. function getWriter() {
  103. return new Promise(resolve => {
  104. zip.createWriter(new zip.BlobWriter(), writer => {
  105. resolve(writer);
  106. });
  107. });
  108. }
  109. function addFile(writer, file) {
  110. return new Promise(resolve => {
  111. writer.add(file.name, new zip.TextReader(file.content), () => {
  112. resolve(writer);
  113. });
  114. });
  115. }
  116. function leftpad(src, length, pad = '0') {
  117. let str = `${src}`;
  118. while (str.length < length) str = pad + str;
  119. return str;
  120. }
  121. function getTimestamp() {
  122. const date = new Date();
  123. return `${
  124. date.getFullYear()
  125. }-${
  126. leftpad(date.getMonth() + 1, 2)
  127. }-${
  128. leftpad(date.getDate(), 2)
  129. }_${
  130. leftpad(date.getHours(), 2)
  131. }.${
  132. leftpad(date.getMinutes(), 2)
  133. }.${
  134. leftpad(date.getSeconds(), 2)
  135. }`;
  136. }
  137. function getExportname() {
  138. return `scripts_${getTimestamp()}.zip`;
  139. }
  140. function download(blob) {
  141. // Known issue: does not work on Firefox
  142. // https://bugzilla.mozilla.org/show_bug.cgi?id=1331176
  143. if (isFirefox) {
  144. const reader = new FileReader();
  145. reader.onload = () => {
  146. store.ffDownload = {
  147. name: getExportname(),
  148. url: reader.result,
  149. };
  150. };
  151. reader.readAsDataURL(blob);
  152. } else {
  153. downloadBlob(blob, getExportname());
  154. }
  155. }
  156. function exportData(selectedIds) {
  157. const withValues = options.get('exportValues');
  158. return (selectedIds.length ? sendMessage({
  159. cmd: 'ExportZip',
  160. data: {
  161. values: withValues,
  162. ids: selectedIds,
  163. },
  164. }) : Promise.resolve())
  165. .then(data => {
  166. const names = {};
  167. const vm = {
  168. scripts: {},
  169. settings: options.get(),
  170. };
  171. delete vm.settings.sync;
  172. if (withValues) vm.values = {};
  173. const files = (objectGet(data, 'items') || []).map(({ script, code }) => {
  174. let name = script.custom.name || script.meta.name || script.props.id;
  175. if (names[name]) {
  176. names[name] += 1;
  177. name = `${name}_${names[name]}`;
  178. } else names[name] = 1;
  179. const info = {
  180. custom: script.custom,
  181. config: script.config,
  182. position: script.props.position,
  183. };
  184. if (withValues) {
  185. // `values` are related to scripts by `props.id` in Violentmonkey,
  186. // but by the global `props.uri` when exported.
  187. const values = data.values[script.props.id];
  188. if (values) vm.values[script.props.uri] = values;
  189. }
  190. vm.scripts[name] = info;
  191. return {
  192. name: `${name}.user.js`,
  193. content: code,
  194. };
  195. });
  196. files.push({
  197. name: 'violentmonkey',
  198. content: JSON.stringify(vm),
  199. });
  200. return files;
  201. })
  202. .then(files => files.reduce((result, file) => (
  203. result.then(writer => addFile(writer, file))
  204. ), getWriter()))
  205. .then(writer => new Promise(resolve => {
  206. writer.close(blob => {
  207. resolve(blob);
  208. });
  209. }));
  210. }
  211. </script>
  212. <style>
  213. .export-list {
  214. display: block;
  215. min-height: 4rem;
  216. max-height: 20rem;
  217. overflow-y: auto;
  218. padding: .3rem;
  219. white-space: normal;
  220. border: 1px solid #ddd;
  221. > .ellipsis {
  222. display: inline-block;
  223. width: 13rem;
  224. max-width: 100%;
  225. line-height: 1.5;
  226. margin-right: .2rem;
  227. margin-bottom: .1rem;
  228. padding: 0 .3rem;
  229. border: 1px solid #bbb;
  230. border-radius: 3px;
  231. cursor: pointer;
  232. &.active {
  233. border-color: #2c82c9;
  234. background: #3498db;
  235. color: white;
  236. }
  237. }
  238. }
  239. .export-modal {
  240. width: 13rem;
  241. }
  242. </style>