app.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. <template>
  2. <div
  3. class="page-popup"
  4. :data-failure-reason="failureReason">
  5. <div class="flex menu-buttons">
  6. <div class="logo" :class="{disabled:!options.isApplied}">
  7. <img src="/public/images/icon128.png">
  8. </div>
  9. <div
  10. class="flex-1 ext-name"
  11. :class="{disabled:!options.isApplied}"
  12. v-text="i18n('extName')"
  13. />
  14. <tooltip
  15. class="menu-area"
  16. :class="{disabled:!options.isApplied}"
  17. :content="options.isApplied ? i18n('menuScriptEnabled') : i18n('menuScriptDisabled')"
  18. placement="bottom"
  19. align="end"
  20. @click.native="onToggle">
  21. <icon :name="getSymbolCheck(options.isApplied)"></icon>
  22. </tooltip>
  23. <tooltip
  24. class="menu-area"
  25. :content="i18n('menuDashboard')"
  26. placement="bottom"
  27. align="end"
  28. @click.native="onManage">
  29. <icon name="cog"></icon>
  30. </tooltip>
  31. <tooltip
  32. class="menu-area"
  33. :content="i18n('menuNewScript')"
  34. placement="bottom"
  35. align="end"
  36. @click.native="onCreateScript">
  37. <icon name="plus"></icon>
  38. </tooltip>
  39. </div>
  40. <div class="menu" v-if="store.injectable" v-show="store.domain">
  41. <div class="menu-item menu-area menu-find" @click="onFindSameDomainScripts">
  42. <icon name="search"></icon>
  43. <div class="flex-1" v-text="i18n('menuFindScripts')"></div>
  44. </div>
  45. </div>
  46. <div class="failure-reason" v-if="failureReasonText">
  47. <span v-text="failureReasonText"/>
  48. <code v-text="store.blacklisted" v-if="store.blacklisted" class="ellipsis inline-block"/>
  49. </div>
  50. <div
  51. v-for="scope in injectionScopes"
  52. class="menu menu-scripts"
  53. :class="{ expand: activeMenu === scope.name }"
  54. :data-type="scope.name"
  55. :key="scope.name">
  56. <div class="menu-item menu-area menu-group" @click="toggleMenu(scope.name)">
  57. <div class="flex-auto" v-text="scope.title" :data-totals="scope.totals" />
  58. <icon name="arrow" class="icon-collapse"></icon>
  59. </div>
  60. <div class="submenu">
  61. <div
  62. v-for="(item, index) in scope.list"
  63. :key="index"
  64. :class="{ disabled: !item.data.config.enabled }"
  65. @mouseenter="message = item.name"
  66. @mouseleave="message = ''">
  67. <div
  68. class="menu-item menu-area"
  69. @click="onToggleScript(item)">
  70. <img class="script-icon" :src="item.data.safeIcon" @error="scriptIconError">
  71. <icon :name="getSymbolCheck(item.data.config.enabled)"></icon>
  72. <div class="flex-auto ellipsis" v-text="item.name"
  73. :class="{failed: item.data.failed}"
  74. @click.ctrl.exact.stop="onEditScript(item)"
  75. @contextmenu.exact.stop="onEditScript(item)"
  76. @mousedown.middle.exact.stop="onEditScript(item)" />
  77. </div>
  78. <div class="submenu-buttons">
  79. <!-- Using a standard tooltip that's shown after a delay to avoid nagging the user -->
  80. <div class="submenu-button" @click="onEditScript(item)"
  81. :title="i18n('buttonEditClickHint')">
  82. <icon name="code"></icon>
  83. </div>
  84. </div>
  85. <div class="submenu-commands">
  86. <div
  87. class="menu-item menu-area"
  88. v-for="(cap, i) in store.commands[item.data.props.id]"
  89. :key="i"
  90. @click="onCommand(item.data.props.id, cap)"
  91. @mouseenter="message = cap"
  92. @mouseleave="message = item.name">
  93. <icon name="command" />
  94. <div class="flex-auto ellipsis" v-text="cap" />
  95. </div>
  96. </div>
  97. </div>
  98. </div>
  99. </div>
  100. <div class="failure-reason" v-if="store.injectionFailure">
  101. <div v-text="i18n('menuInjectionFailed')"/>
  102. <a v-text="i18n('menuInjectionFailedFix')" href="#"
  103. v-if="store.injectionFailure.fixable"
  104. @click.prevent="onInjectionFailureFix"/>
  105. </div>
  106. <div class="incognito"
  107. v-if="store.currentTab && store.currentTab.incognito"
  108. v-text="i18n('msgIncognitoChanges')"/>
  109. <footer>
  110. <span @click="onVisitWebsite" v-text="i18n('visitWebsite')" />
  111. </footer>
  112. <div class="message" v-if="message">
  113. <div v-text="message"></div>
  114. </div>
  115. </div>
  116. </template>
  117. <script>
  118. import Tooltip from 'vueleton/lib/tooltip/bundle';
  119. import { INJECT_AUTO } from '#/common/consts';
  120. import options from '#/common/options';
  121. import { getLocaleString, i18n, makePause, sendCmd, sendTabCmd } from '#/common';
  122. import Icon from '#/common/ui/icon';
  123. import { store } from '../utils';
  124. const optionsData = {
  125. isApplied: options.get('isApplied'),
  126. filtersPopup: options.get('filtersPopup') || {},
  127. };
  128. options.hook((changes) => {
  129. if ('isApplied' in changes) {
  130. optionsData.isApplied = changes.isApplied;
  131. }
  132. if ('filtersPopup' in changes) {
  133. optionsData.filtersPopup = {
  134. ...optionsData.filtersPopup,
  135. ...changes.filtersPopup,
  136. };
  137. }
  138. });
  139. export default {
  140. components: {
  141. Icon,
  142. Tooltip,
  143. },
  144. data() {
  145. return {
  146. store,
  147. options: optionsData,
  148. activeMenu: 'scripts',
  149. message: null,
  150. };
  151. },
  152. computed: {
  153. injectionScopes() {
  154. const { sort, enabledFirst, hideDisabled } = this.options.filtersPopup;
  155. const isSorted = sort === 'alpha' || enabledFirst;
  156. return [
  157. store.injectable && ['scripts', i18n('menuMatchedScripts')],
  158. ['frameScripts', i18n('menuMatchedFrameScripts')],
  159. ]
  160. .filter(Boolean)
  161. .map(([name, title]) => {
  162. let list = this.store[name];
  163. const numTotal = list.length;
  164. const numEnabled = list.reduce((num, script) => num + script.config.enabled, 0);
  165. if (hideDisabled) list = list.filter(script => script.config.enabled);
  166. list = list.map((script, i) => {
  167. const scriptName = script.custom.name || getLocaleString(script.meta, 'name');
  168. return {
  169. name: scriptName,
  170. data: script,
  171. key: isSorted && `${
  172. enabledFirst && +!script.config.enabled
  173. }${
  174. sort === 'alpha' ? scriptName.toLowerCase() : `${1e6 + i}`.slice(1)
  175. }`,
  176. };
  177. });
  178. if (isSorted) {
  179. list.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key));
  180. }
  181. return numTotal && {
  182. name,
  183. title,
  184. list,
  185. totals: numEnabled < numTotal
  186. ? `${numEnabled} / ${numTotal}`
  187. : `${numTotal}`,
  188. };
  189. }).filter(Boolean);
  190. },
  191. failureReason() {
  192. return [
  193. !store.injectable && 'noninjectable',
  194. store.blacklisted && 'blacklisted',
  195. // undefined means the data isn't ready yet
  196. optionsData.isApplied === false && 'scripts-disabled',
  197. ].filter(Boolean).join(' ');
  198. },
  199. failureReasonText() {
  200. return (
  201. !store.injectable && i18n('failureReasonNoninjectable')
  202. || store.blacklisted && i18n('failureReasonBlacklisted')
  203. || optionsData.isApplied === false && i18n('menuScriptDisabled')
  204. || ''
  205. );
  206. },
  207. },
  208. methods: {
  209. toggleMenu(name) {
  210. this.activeMenu = this.activeMenu === name ? null : name;
  211. },
  212. getSymbolCheck(bool) {
  213. return `toggle-${bool ? 'on' : 'off'}`;
  214. },
  215. scriptIconError(event) {
  216. event.target.removeAttribute('src');
  217. },
  218. onToggle() {
  219. options.set('isApplied', !this.options.isApplied);
  220. this.checkReload();
  221. },
  222. onManage() {
  223. browser.runtime.openOptionsPage();
  224. window.close();
  225. },
  226. onVisitWebsite() {
  227. sendCmd('TabOpen', {
  228. url: 'https://violentmonkey.github.io/',
  229. });
  230. window.close();
  231. },
  232. onEditScript(item) {
  233. sendCmd('TabOpen', {
  234. url: `/options/index.html#scripts/${item.data.props.id}`,
  235. maybeInWindow: true,
  236. });
  237. window.close();
  238. },
  239. onFindSameDomainScripts() {
  240. sendCmd('TabOpen', {
  241. url: `https://greasyfork.org/scripts/by-site/${encodeURIComponent(this.store.domain)}`,
  242. });
  243. window.close();
  244. },
  245. onCommand(id, cap) {
  246. sendTabCmd(store.currentTab.id, 'Command', `${id}:${cap}`);
  247. },
  248. onToggleScript(item) {
  249. const { data } = item;
  250. const enabled = !data.config.enabled;
  251. sendCmd('UpdateScriptInfo', {
  252. id: data.props.id,
  253. config: { enabled },
  254. })
  255. .then(() => {
  256. data.config.enabled = enabled;
  257. this.checkReload();
  258. });
  259. },
  260. checkReload() {
  261. if (options.get('autoReload')) browser.tabs.reload(this.store.currentTab.id);
  262. },
  263. async onCreateScript() {
  264. const { currentTab, domain } = this.store;
  265. const id = domain && await sendCmd('CacheNewScript', {
  266. url: currentTab.url.split(/[#?]/)[0],
  267. name: `- ${domain}`,
  268. });
  269. sendCmd('TabOpen', {
  270. url: `/options/index.html#scripts/_new${id ? `/${id}` : ''}`,
  271. maybeInWindow: true,
  272. });
  273. window.close();
  274. },
  275. async onInjectionFailureFix() {
  276. // TODO: promisify options.set, resolve on storage write, await it instead of makePause
  277. options.set('defaultInjectInto', INJECT_AUTO);
  278. await makePause(100);
  279. await browser.tabs.reload();
  280. window.close();
  281. },
  282. },
  283. };
  284. </script>
  285. <style src="../style.css"></style>