8080 <ui-checkbox v-model =" state.encrypt" class =" mt-12 mb-4" >
8181 {{ t('settings.backupWorkflows.backup.encrypt') }}
8282 </ui-checkbox >
83- <ui-button class =" w-full" @click =" backupWorkflows" >
84- {{ t('settings.backupWorkflows.backup.button') }}
85- </ui-button >
83+ <div class =" flex items-center gap-2" >
84+ <ui-button class =" flex-1" @click =" backupWorkflows" >
85+ {{ t('settings.backupWorkflows.backup.button') }}
86+ </ui-button >
87+ <ui-popover @close =" registerScheduleBackup" >
88+ <template #trigger >
89+ <ui-button
90+ v-tooltip =" t('settings.backupWorkflows.backup.schedule')"
91+ icon
92+ :class =" { 'text-primary': localBackupSchedule.schedule }"
93+ >
94+ <v-remixicon name =" riCalendarLine" />
95+ </ui-button >
96+ </template >
97+ <div class =" min-w-[14rem]" >
98+ <p class =" mb-2" >
99+ {{ t('settings.backupWorkflows.backup.schedule') }}
100+ </p >
101+ <template v-if =" ! downloadPermission .has .downloads " >
102+ <p class =" text-gray-600 dark:text-gray-300" >
103+ Automa requires the "Downloads" permission for this feature to
104+ work
105+ </p >
106+ <ui-button
107+ class =" mt-4 w-full"
108+ @click =" downloadPermission.request()"
109+ >
110+ Allow "Downloads" permission
111+ </ui-button >
112+ </template >
113+ <template v-else >
114+ <ui-select
115+ v-model =" localBackupSchedule.schedule"
116+ label =" Schedule"
117+ class =" w-full"
118+ >
119+ <option value =" " >Never</option >
120+ <option
121+ v-for =" (value, key) in BACKUP_SCHEDULES"
122+ :key =" key"
123+ :value =" key"
124+ >
125+ {{ value }}
126+ </option >
127+ <option value =" custom" >Custom</option >
128+ </ui-select >
129+ <template v-if =" localBackupSchedule .schedule === ' custom' " >
130+ <ui-input
131+ v-model =" localBackupSchedule.customSchedule"
132+ label =" Cron Expression"
133+ class =" w-full mt-2"
134+ placeholder =" 0 8 * * *"
135+ />
136+ <p className =" text-sm text-gray-600 dark:text-gray-300" >
137+ {{ getBackupScheduleCron() }}
138+ </p >
139+ </template >
140+ <ui-input
141+ v-model =" localBackupSchedule.folderName"
142+ label =" Folder name"
143+ class =" w-full mt-2"
144+ placeholder =" backup-folder"
145+ />
146+ <p
147+ v-if =" localBackupSchedule.lastBackup"
148+ class =" text-gray-600 dark:text-gray-300 text-sm mt-4"
149+ >
150+ Last backup:
151+ {{ dayjs(localBackupSchedule.lastBackup).fromNow() }}
152+ </p >
153+ </template >
154+ </div >
155+ </ui-popover >
156+ </div >
86157 </div >
87158 <div class =" w-6/12 rounded-lg border p-4 dark:border-gray-700" >
88159 <div class =" text-center" >
111182 </ui-modal >
112183</template >
113184<script setup>
114- import { reactive , onMounted } from ' vue' ;
185+ import { reactive , toRaw , onMounted } from ' vue' ;
115186import { useI18n } from ' vue-i18n' ;
116187import { useToast } from ' vue-toastification' ;
117188import dayjs from ' dayjs' ;
118189import AES from ' crypto-js/aes' ;
190+ import cronParser from ' cron-parser' ;
119191import encUtf8 from ' crypto-js/enc-utf8' ;
120192import browser from ' webextension-polyfill' ;
121193import hmacSHA256 from ' crypto-js/hmac-sha256' ;
122194import { useDialog } from ' @/composable/dialog' ;
195+ import { readableCron } from ' @/lib/cronstrue' ;
123196import { useUserStore } from ' @/stores/user' ;
124197import { getUserWorkflows } from ' @/utils/api' ;
125198import { useWorkflowStore } from ' @/stores/workflow' ;
199+ import { useHasPermissions } from ' @/composable/hasPermissions' ;
126200import { fileSaver , openFilePicker , parseJSON } from ' @/utils/helper' ;
127201import SettingsCloudBackup from ' @/components/newtab/settings/SettingsCloudBackup.vue' ;
128202
203+ const BACKUP_SCHEDULES = {
204+ ' 0 8 * * *' : ' Every day' ,
205+ ' 0 8 * * 0' : ' Every week' ,
206+ };
207+
129208const { t } = useI18n ();
130209const toast = useToast ();
131210const dialog = useDialog ();
132211const userStore = useUserStore ();
133212const workflowStore = useWorkflowStore ();
213+ const downloadPermission = useHasPermissions ([' downloads' ]);
134214
135215const state = reactive ({
136216 lastSync: null ,
@@ -144,7 +224,46 @@ const backupState = reactive({
144224 modal: false ,
145225 loading: false ,
146226});
227+ const localBackupSchedule = reactive ({
228+ schedule: ' ' ,
229+ lastBackup: null ,
230+ customSchedule: ' ' ,
231+ folderName: ' automa-backup' ,
232+ });
233+
234+ async function registerScheduleBackup () {
235+ try {
236+ if (! localBackupSchedule .schedule .trim ()) {
237+ await browser .alarms .clear (' schedule-local-backup' );
238+ } else {
239+ const expression =
240+ localBackupSchedule .schedule === ' custom'
241+ ? localBackupSchedule .customSchedule
242+ : localBackupSchedule .schedule ;
243+ const parsedExpression = cronParser .parseExpression (expression).next ();
244+ if (! parsedExpression) return ;
245+
246+ await browser .alarms .create (' schedule-local-backup' , {
247+ when: parsedExpression .getTime (),
248+ });
249+ }
147250
251+ browser .storage .local .set ({
252+ scheduleLocalBackup: toRaw (localBackupSchedule),
253+ });
254+ } catch (error) {
255+ console .error (error);
256+ }
257+ }
258+ function getBackupScheduleCron () {
259+ try {
260+ const expression = localBackupSchedule .customSchedule ;
261+
262+ return ` ${ readableCron (expression)} ` ;
263+ } catch (error) {
264+ return error .message ;
265+ }
266+ }
148267function formatDate (date ) {
149268 if (! date) return ' null' ;
150269
@@ -301,10 +420,14 @@ async function restoreWorkflows() {
301420}
302421
303422onMounted (async () => {
304- const { lastBackup , lastSync } = await browser .storage .local .get ([
305- ' lastBackup' ,
306- ' lastSync' ,
307- ]);
423+ const { lastBackup , lastSync , scheduleLocalBackup } =
424+ await browser .storage .local .get ([
425+ ' lastSync' ,
426+ ' lastBackup' ,
427+ ' scheduleLocalBackup' ,
428+ ]);
429+
430+ Object .assign (localBackupSchedule, scheduleLocalBackup || {});
308431
309432 state .lastSync = lastSync;
310433 state .lastBackup = lastBackup;
0 commit comments