App.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. <template>
  2. <template v-if="retrieved">
  3. <app-sidebar v-if="$route.name !== 'recording'" />
  4. <main :class="{ 'pl-16': $route.name !== 'recording' }">
  5. <router-view />
  6. </main>
  7. <ui-dialog>
  8. <template #auth>
  9. <div class="text-center">
  10. <p class="font-semibold text-xl">Oops!! 😬</p>
  11. <p class="mt-2 text-gray-600 dark:text-gray-200">
  12. {{ t('auth.text') }}
  13. </p>
  14. <ui-button
  15. tag="a"
  16. href="https://www.automa.site/auth"
  17. class="mt-6 w-full block"
  18. variant="accent"
  19. >
  20. {{ t('auth.signIn') }}
  21. </ui-button>
  22. </div>
  23. </template>
  24. </ui-dialog>
  25. <div
  26. v-if="isUpdated"
  27. class="p-4 shadow-2xl z-50 fixed bottom-8 left-1/2 -translate-x-1/2 rounded-lg bg-accent text-white dark:text-gray-900 flex items-center"
  28. >
  29. <v-remixicon name="riInformationLine" class="mr-3" />
  30. <p>
  31. {{ t('updateMessage.text1', { version: currentVersion }) }}
  32. </p>
  33. <a
  34. :href="`https://github.com/AutomaApp/automa/releases/latest`"
  35. target="_blank"
  36. rel="noopener"
  37. class="underline ml-1"
  38. >
  39. {{ t('updateMessage.text2') }}
  40. </a>
  41. <button
  42. class="ml-6 text-gray-200 dark:text-gray-600"
  43. @click="isUpdated = false"
  44. >
  45. <v-remixicon size="20" name="riCloseLine" />
  46. </button>
  47. </div>
  48. <shared-permissions-modal
  49. v-model="permissionState.showModal"
  50. :permissions="permissionState.items"
  51. />
  52. </template>
  53. <div v-else class="py-8 text-center">
  54. <ui-spinner color="text-accent" size="28" />
  55. </div>
  56. <app-survey />
  57. </template>
  58. <script setup>
  59. import { ref, reactive } from 'vue';
  60. import { useI18n } from 'vue-i18n';
  61. import { useRouter } from 'vue-router';
  62. import { compare } from 'compare-versions';
  63. import { useHead } from '@vueuse/head';
  64. import browser from 'webextension-polyfill';
  65. import { useStore } from '@/stores/main';
  66. import { useUserStore } from '@/stores/user';
  67. import { useFolderStore } from '@/stores/folder';
  68. import { usePackageStore } from '@/stores/package';
  69. import { useWorkflowStore } from '@/stores/workflow';
  70. import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
  71. import { useTheme } from '@/composable/theme';
  72. import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
  73. import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
  74. import { loadLocaleMessages, setI18nLanguage } from '@/lib/vueI18n';
  75. import { getUserWorkflows } from '@/utils/api';
  76. import { getWorkflowPermissions } from '@/utils/workflowData';
  77. import { sendMessage } from '@/utils/message';
  78. import { workflowState, startWorkflowExec } from '@/workflowEngine';
  79. import automa from '@business';
  80. import dbLogs from '@/db/logs';
  81. import dayjs from '@/lib/dayjs';
  82. import AppSurvey from '@/components/newtab/app/AppSurvey.vue';
  83. import AppSidebar from '@/components/newtab/app/AppSidebar.vue';
  84. import dataMigration from '@/utils/dataMigration';
  85. import iconFirefox from '@/assets/svg/logoFirefox.svg';
  86. import iconChrome from '@/assets/svg/logo.svg';
  87. import SharedPermissionsModal from '@/components/newtab/shared/SharedPermissionsModal.vue';
  88. let icon;
  89. if (window.location.protocol === 'moz-extension:') {
  90. icon = iconFirefox;
  91. } else {
  92. icon = iconChrome;
  93. }
  94. const iconElement = document.createElement('link');
  95. iconElement.rel = 'icon';
  96. iconElement.href = icon;
  97. document.head.appendChild(iconElement);
  98. const { t } = useI18n();
  99. const store = useStore();
  100. const theme = useTheme();
  101. const router = useRouter();
  102. const userStore = useUserStore();
  103. const folderStore = useFolderStore();
  104. const packageStore = usePackageStore();
  105. const workflowStore = useWorkflowStore();
  106. const teamWorkflowStore = useTeamWorkflowStore();
  107. const sharedWorkflowStore = useSharedWorkflowStore();
  108. const hostedWorkflowStore = useHostedWorkflowStore();
  109. theme.init();
  110. const retrieved = ref(false);
  111. const isUpdated = ref(false);
  112. const permissionState = reactive({
  113. permissions: [],
  114. showModal: false,
  115. });
  116. const currentVersion = browser.runtime.getManifest().version;
  117. const prevVersion = localStorage.getItem('ext-version') || '0.0.0';
  118. async function fetchUserData() {
  119. try {
  120. if (!userStore.user) return;
  121. const { backup, hosted } = await getUserWorkflows();
  122. userStore.hostedWorkflows = hosted || {};
  123. if (backup && backup.length > 0) {
  124. const { lastBackup } = browser.storage.local.get('lastBackup');
  125. if (!lastBackup) {
  126. const backupIds = backup.map(({ id }) => id);
  127. userStore.backupIds = backupIds;
  128. await browser.storage.local.set({
  129. backupIds,
  130. lastBackup: new Date().toISOString(),
  131. });
  132. }
  133. await workflowStore.insertOrUpdate(backup, { checkUpdateDate: true });
  134. }
  135. userStore.retrieved = true;
  136. } catch (error) {
  137. console.error(error);
  138. }
  139. }
  140. /* eslint-disable-next-line */
  141. function autoDeleteLogs() {
  142. const deleteAfter = store.settings.deleteLogAfter;
  143. if (deleteAfter === 'never') return;
  144. const lastCheck =
  145. +localStorage.getItem('checkDeleteLogs') || Date.now() - 8.64e7;
  146. const dayDiff = dayjs().diff(dayjs(lastCheck), 'day');
  147. if (dayDiff < 1) return;
  148. const aDayInMs = 8.64e7;
  149. const maxLogAge = Date.now() - aDayInMs * deleteAfter;
  150. dbLogs.items
  151. .where('endedAt')
  152. .below(maxLogAge)
  153. .toArray()
  154. .then((values) => {
  155. const ids = values.map(({ id }) => id);
  156. dbLogs.items.bulkDelete(ids);
  157. dbLogs.ctxData.where('logId').anyOf(ids).delete();
  158. dbLogs.logsData.where('logId').anyOf(ids).delete();
  159. dbLogs.histories.where('logId').anyOf(ids).delete();
  160. localStorage.setItem('checkDeleteLogs', Date.now());
  161. });
  162. }
  163. async function syncHostedWorkflows() {
  164. const hostIds = [];
  165. const userHosted = userStore.getHostedWorkflows;
  166. const hostedWorkflows = hostedWorkflowStore.workflows;
  167. Object.keys(hostedWorkflows).forEach((hostId) => {
  168. const isItsOwn = userHosted.find((item) => item.hostId === hostId);
  169. if (isItsOwn) return;
  170. hostIds.push({ hostId, updatedAt: hostedWorkflows[hostId].updatedAt });
  171. });
  172. if (hostIds.length === 0) return;
  173. await hostedWorkflowStore.fetchWorkflows(hostIds);
  174. }
  175. function stopRecording() {
  176. if (!window.stopRecording) return;
  177. window.stopRecording();
  178. }
  179. const messageEvents = {
  180. 'refresh-packages': function () {
  181. packageStore.loadData(true);
  182. },
  183. 'workflow:added': function (data) {
  184. if (data.source === 'team') {
  185. teamWorkflowStore.loadData().then(() => {
  186. router.push(
  187. `/teams/${data.teamId}/workflows/${data.workflowId}?permission=true`
  188. );
  189. });
  190. } else if (data.workflowData) {
  191. workflowStore
  192. .insert(data.workflowData, { duplicateId: true })
  193. .then(async () => {
  194. try {
  195. const permissions = await getWorkflowPermissions(data.workflowData);
  196. if (permissions.length === 0) return;
  197. permissionState.items = permissions;
  198. permissionState.showModal = true;
  199. } catch (error) {
  200. console.error(error);
  201. }
  202. })
  203. .catch((error) => {
  204. console.error(error);
  205. });
  206. }
  207. },
  208. 'workflow:execute': function ({ data }) {
  209. console.log(data);
  210. startWorkflowExec(data, data?.options ?? {});
  211. },
  212. 'recording:stop': stopRecording,
  213. 'background--recording:stop': stopRecording,
  214. };
  215. browser.runtime.onMessage.addListener(({ type, data }) => {
  216. if (!type || !messageEvents[type]) return;
  217. messageEvents[type](data);
  218. });
  219. browser.storage.local.onChanged.addListener(({ workflowStates }) => {
  220. if (!workflowStates) return;
  221. workflowStore.states = Object.values(workflowStates.newValue);
  222. });
  223. useHead(() => {
  224. const runningWorkflows = workflowStore.popupStates.length;
  225. return {
  226. title: 'Dashboard',
  227. titleTemplate:
  228. runningWorkflows > 0
  229. ? `%s (${runningWorkflows} Workflows Running) - Automa`
  230. : '%s - Automa',
  231. };
  232. });
  233. /* eslint-disable-next-line */
  234. window.onbeforeunload = () => {
  235. const runningWorkflows = workflowStore.popupStates.length;
  236. if (window.isDataChanged || runningWorkflows > 0) {
  237. return t('message.notSaved');
  238. }
  239. };
  240. window.addEventListener('message', ({ data }) => {
  241. if (data?.type !== 'automa-fetch') return;
  242. const sendResponse = (result) => {
  243. const sandbox = document.getElementById('sandbox');
  244. sandbox.contentWindow.postMessage(
  245. {
  246. type: 'fetchResponse',
  247. data: result,
  248. id: data.data.id,
  249. },
  250. '*'
  251. );
  252. };
  253. sendMessage('fetch', data.data, 'background')
  254. .then((result) => {
  255. sendResponse({ isError: false, result });
  256. })
  257. .catch((error) => {
  258. sendResponse({ isError: true, result: error.message });
  259. });
  260. });
  261. (async () => {
  262. try {
  263. workflowState.storage = {
  264. get() {
  265. return workflowStore.popupStates;
  266. },
  267. set(key, value) {
  268. workflowStore.popupStates = Object.values(value);
  269. },
  270. };
  271. const tabs = await browser.tabs.query({
  272. url: browser.runtime.getURL('/newtab.html'),
  273. });
  274. if (tabs.length > 1) {
  275. const firstTab = tabs.shift();
  276. await browser.tabs.update(firstTab.id, { active: true });
  277. await browser.tabs.remove(tabs.map((tab) => tab.id));
  278. return;
  279. }
  280. const { isFirstTime } = await browser.storage.local.get('isFirstTime');
  281. isUpdated.value = !isFirstTime && compare(currentVersion, prevVersion, '>');
  282. await Promise.allSettled([
  283. folderStore.load(),
  284. store.loadSettings(),
  285. workflowStore.loadData(),
  286. teamWorkflowStore.loadData(),
  287. hostedWorkflowStore.loadData(),
  288. packageStore.loadData(),
  289. ]);
  290. await loadLocaleMessages(store.settings.locale, 'newtab');
  291. await setI18nLanguage(store.settings.locale);
  292. await dataMigration();
  293. await userStore.loadUser({ useCache: false, ttl: 2 });
  294. await automa('app');
  295. retrieved.value = true;
  296. await Promise.allSettled([
  297. sharedWorkflowStore.fetchWorkflows(),
  298. fetchUserData(),
  299. syncHostedWorkflows(),
  300. ]);
  301. const { isRecording } = await browser.storage.local.get('isRecording');
  302. if (isRecording) {
  303. router.push('/recording');
  304. await browser.action.setBadgeBackgroundColor({ color: '#ef4444' });
  305. await browser.action.setBadgeText({ text: 'rec' });
  306. }
  307. autoDeleteLogs();
  308. } catch (error) {
  309. retrieved.value = true;
  310. console.error(error);
  311. }
  312. localStorage.setItem('ext-version', currentVersion);
  313. })();
  314. </script>
  315. <style>
  316. html,
  317. body {
  318. @apply bg-gray-50 dark:bg-gray-900 text-black dark:text-gray-100;
  319. }
  320. body {
  321. min-height: 100vh;
  322. }
  323. #app {
  324. height: 100%;
  325. }
  326. h1,
  327. h2,
  328. h3 {
  329. @apply dark:text-white;
  330. }
  331. </style>