Home.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. <template>
  2. <div
  3. :class="[workflowHostKeys.length === 0 ? 'h-48' : 'h-56']"
  4. class="bg-accent rounded-b-2xl absolute top-0 left-0 w-full"
  5. ></div>
  6. <div
  7. :class="[workflowHostKeys.length === 0 ? 'mb-6' : 'mb-2']"
  8. class="dark placeholder-black relative z-10 text-white px-5 pt-8"
  9. >
  10. <div class="flex items-center mb-4">
  11. <h1 class="text-xl font-semibold text-white">Automa</h1>
  12. <div class="flex-grow"></div>
  13. <ui-button
  14. v-tooltip.group="t('home.record.title')"
  15. icon
  16. class="mr-2"
  17. @click="state.newRecordingModal = true"
  18. >
  19. <v-remixicon name="riRecordCircleLine" />
  20. </ui-button>
  21. <ui-button
  22. v-tooltip.group="
  23. t(`home.elementSelector.${state.haveAccess ? 'name' : 'noAccess'}`)
  24. "
  25. icon
  26. class="mr-2"
  27. @click="initElementSelector"
  28. >
  29. <v-remixicon name="riFocus3Line" />
  30. </ui-button>
  31. <ui-button
  32. v-tooltip.group="t('common.dashboard')"
  33. icon
  34. :title="t('common.dashboard')"
  35. @click="openDashboard"
  36. >
  37. <v-remixicon name="riHome5Line" />
  38. </ui-button>
  39. </div>
  40. <div class="flex">
  41. <ui-input
  42. v-model="state.query"
  43. :placeholder="`${t('common.search')}...`"
  44. prepend-icon="riSearch2Line"
  45. class="w-full search-input"
  46. />
  47. </div>
  48. <ui-tabs
  49. v-if="workflowHostKeys.length > 0"
  50. v-model="state.activeTab"
  51. fill
  52. class="mt-1"
  53. >
  54. <ui-tab v-for="type in workflowTypes" :key="type" :value="type">
  55. {{ t(`home.workflow.type.${type}`) }}
  56. </ui-tab>
  57. </ui-tabs>
  58. </div>
  59. <div class="px-5 pb-5 space-y-2">
  60. <ui-card v-if="Workflow.all().length === 0" class="text-center">
  61. <img src="@/assets/svg/alien.svg" />
  62. <p class="font-semibold">{{ t('message.empty') }}</p>
  63. <ui-button
  64. variant="accent"
  65. class="mt-6"
  66. @click="openDashboard('/workflows')"
  67. >
  68. {{ t('home.workflow.new') }}
  69. </ui-button>
  70. </ui-card>
  71. <home-workflow-card
  72. v-for="workflow in workflows"
  73. :key="workflow.id"
  74. :workflow="workflow"
  75. :tab="state.activeTab"
  76. @details="openDashboard(`/workflows/${$event.id}`)"
  77. @update="updateWorkflow(workflow.id, $event)"
  78. @execute="executeWorkflow"
  79. @rename="renameWorkflow"
  80. @delete="deleteWorkflow"
  81. />
  82. </div>
  83. <ui-modal v-model="state.newRecordingModal" custom-content>
  84. <ui-card
  85. :style="{ height: `${state.cardHeight}px` }"
  86. class="w-full recording-card overflow-hidden rounded-b-none"
  87. padding="p-0"
  88. >
  89. <div class="flex items-center px-4 pt-4 pb-2">
  90. <p class="flex-1 font-semibold">
  91. {{ t('home.record.title') }}
  92. </p>
  93. <v-remixicon
  94. class="text-gray-600 dark:text-gray-300 cursor-pointer"
  95. name="riCloseLine"
  96. size="20"
  97. @click="state.newRecordingModal = false"
  98. ></v-remixicon>
  99. </div>
  100. <home-start-recording
  101. @record="recordWorkflow"
  102. @close="state.newRecordingModal = false"
  103. @update="state.cardHeight = recordingCardHeight[$event] || 255"
  104. />
  105. </ui-card>
  106. </ui-modal>
  107. </template>
  108. <script setup>
  109. import { computed, onMounted, shallowReactive } from 'vue';
  110. import { useI18n } from 'vue-i18n';
  111. import { useStore } from 'vuex';
  112. import browser from 'webextension-polyfill';
  113. import { useDialog } from '@/composable/dialog';
  114. import { useGroupTooltip } from '@/composable/groupTooltip';
  115. import { sendMessage } from '@/utils/message';
  116. import Workflow from '@/models/workflow';
  117. import HomeWorkflowCard from '@/components/popup/home/HomeWorkflowCard.vue';
  118. import HomeStartRecording from '@/components/popup/home/HomeStartRecording.vue';
  119. const workflowTypes = ['local', 'host'];
  120. const recordingCardHeight = {
  121. new: 255,
  122. existing: 480,
  123. };
  124. const { t } = useI18n();
  125. const store = useStore();
  126. const dialog = useDialog();
  127. useGroupTooltip();
  128. const state = shallowReactive({
  129. query: '',
  130. cardHeight: 255,
  131. haveAccess: true,
  132. activeTab: 'local',
  133. newRecordingModal: false,
  134. });
  135. const workflowHostKeys = computed(() => Object.keys(store.state.workflowHosts));
  136. const workflowHosts = computed(() => {
  137. if (state.activeTab !== 'host') return [];
  138. return workflowHostKeys.value.reduce((acc, key) => {
  139. const workflow = store.state.workflowHosts[key];
  140. const isMatch = workflow.name
  141. .toLocaleLowerCase()
  142. .includes(state.query.toLocaleLowerCase());
  143. if (isMatch) acc.push({ ...workflow, id: key });
  144. return acc;
  145. }, []);
  146. });
  147. const localWorkflows = computed(() => {
  148. if (state.activeTab !== 'local') return [];
  149. return Workflow.query()
  150. .where(({ name }) =>
  151. name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())
  152. )
  153. .orderBy('createdAt', 'desc')
  154. .get();
  155. });
  156. const workflows = computed(() =>
  157. state.activeTab === 'local' ? localWorkflows.value : workflowHosts.value
  158. );
  159. function executeWorkflow(workflow) {
  160. sendMessage('workflow:execute', workflow, 'background');
  161. }
  162. function updateWorkflow(id, data) {
  163. return Workflow.update({
  164. where: id,
  165. data,
  166. });
  167. }
  168. function renameWorkflow({ id, name }) {
  169. dialog.prompt({
  170. title: t('home.workflow.rename'),
  171. placeholder: t('common.name'),
  172. okText: t('common.rename'),
  173. inputValue: name,
  174. onConfirm: (newName) => {
  175. updateWorkflow(id, { name: newName });
  176. },
  177. });
  178. }
  179. function deleteWorkflow({ id, name }) {
  180. dialog.confirm({
  181. title: t('home.workflow.delete'),
  182. okVariant: 'danger',
  183. body: t('message.delete', { name }),
  184. onConfirm: () => {
  185. if (state.activeTab === 'local') {
  186. Workflow.delete(id);
  187. } else {
  188. store.commit('deleteStateNested', `workflowHosts.${id}`);
  189. if (workflowHostKeys.value.length === 0) {
  190. state.activeTab = 'local';
  191. }
  192. browser.storage.local.set({ workflowHosts: store.state.workflowHosts });
  193. }
  194. },
  195. });
  196. }
  197. function openDashboard(url) {
  198. sendMessage('open:dashboard', url, 'background');
  199. }
  200. async function initElementSelector() {
  201. const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
  202. try {
  203. const result = await browser.tabs.sendMessage(tab.id, {
  204. type: 'automa-element-selector',
  205. });
  206. if (!result) throw new Error('not-found');
  207. window.close();
  208. } catch (error) {
  209. if (error.message.includes('Could not establish connection.')) {
  210. await browser.tabs.executeScript(tab.id, {
  211. allFrames: true,
  212. file: './elementSelector.bundle.js',
  213. });
  214. initElementSelector();
  215. }
  216. console.error(error);
  217. }
  218. }
  219. async function recordWorkflow(options = {}) {
  220. try {
  221. const flows = [];
  222. const [activeTab] = await browser.tabs.query({
  223. active: true,
  224. currentWindow: true,
  225. });
  226. if (activeTab && activeTab.url.startsWith('http')) {
  227. flows.push({
  228. id: 'new-tab',
  229. description: activeTab.url,
  230. data: { url: activeTab.url },
  231. });
  232. }
  233. await browser.storage.local.set({
  234. isRecording: true,
  235. recording: {
  236. flows,
  237. name: 'unnamed',
  238. activeTab: {
  239. id: activeTab.id,
  240. url: activeTab.url,
  241. },
  242. ...options,
  243. },
  244. });
  245. await browser.browserAction.setBadgeBackgroundColor({ color: '#ef4444' });
  246. await browser.browserAction.setBadgeText({ text: 'rec' });
  247. const tabs = await browser.tabs.query({});
  248. for (const tab of tabs) {
  249. if (tab.url.startsWith('http')) {
  250. await browser.tabs.executeScript(tab.id, {
  251. allFrames: true,
  252. file: 'recordWorkflow.bundle.js',
  253. });
  254. }
  255. }
  256. window.close();
  257. } catch (error) {
  258. console.error(error);
  259. }
  260. }
  261. onMounted(async () => {
  262. const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
  263. state.haveAccess = /^(https?)/.test(tab.url);
  264. });
  265. </script>
  266. <style>
  267. .recording-card {
  268. transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1) !important;
  269. }
  270. </style>