diff --git a/components.d.ts b/components.d.ts index e31119b3..b021643a 100644 --- a/components.d.ts +++ b/components.d.ts @@ -132,6 +132,7 @@ declare module '@vue/runtime-core' { NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NDivider: typeof import('naive-ui')['NDivider'] NEllipsis: typeof import('naive-ui')['NEllipsis'] + NForm: typeof import('naive-ui')['NForm'] NFormItem: typeof import('naive-ui')['NFormItem'] NGi: typeof import('naive-ui')['NGi'] NGrid: typeof import('naive-ui')['NGrid'] @@ -143,8 +144,12 @@ declare module '@vue/runtime-core' { NLayout: typeof import('naive-ui')['NLayout'] NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NMenu: typeof import('naive-ui')['NMenu'] + NRadio: typeof import('naive-ui')['NRadio'] + NRadioGroup: typeof import('naive-ui')['NRadioGroup'] NScrollbar: typeof import('naive-ui')['NScrollbar'] + NSpace: typeof import('naive-ui')['NSpace'] NSpin: typeof import('naive-ui')['NSpin'] + NSwitch: typeof import('naive-ui')['NSwitch'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] @@ -159,6 +164,7 @@ declare module '@vue/runtime-core' { RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default'] + SafelinkDecoder: typeof import('./src/tools/safelink-decoder/safelink-decoder.vue')['default'] SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default'] SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default'] SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default'] diff --git a/package.json b/package.json index fd6c02e6..853381c2 100644 --- a/package.json +++ b/package.json @@ -51,16 +51,20 @@ "change-case": "^4.1.2", "colord": "^2.9.3", "composerize-ts": "^0.6.2", + "countries-and-timezones": "^3.6.0", "country-code-lookup": "^0.1.0", + "cron-parser": "^4.9.0", "cron-validator": "^1.3.1", "cronstrue": "^2.26.0", "crypto-js": "^4.1.1", "date-fns": "^2.29.3", "dompurify": "^3.0.6", "emojilib": "^3.0.10", + "event-cron-parser": "^1.0.34", "figlet": "^1.7.0", "figue": "^1.2.0", "fuse.js": "^6.6.2", + "get-timezone-offset": "^1.0.5", "highlight.js": "^11.7.0", "iarna-toml-esm": "^3.0.5", "ibantools": "^4.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd6c38c9..5b2b413d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,9 +53,15 @@ dependencies: composerize-ts: specifier: ^0.6.2 version: 0.6.2 + countries-and-timezones: + specifier: ^3.6.0 + version: 3.6.0 country-code-lookup: specifier: ^0.1.0 version: 0.1.0 + cron-parser: + specifier: ^4.9.0 + version: 4.9.0 cron-validator: specifier: ^1.3.1 version: 1.3.1 @@ -74,6 +80,9 @@ dependencies: emojilib: specifier: ^3.0.10 version: 3.0.10 + event-cron-parser: + specifier: ^1.0.34 + version: 1.0.34 figlet: specifier: ^1.7.0 version: 1.7.0 @@ -83,6 +92,9 @@ dependencies: fuse.js: specifier: ^6.6.2 version: 6.6.2 + get-timezone-offset: + specifier: ^1.0.5 + version: 1.0.5 highlight.js: specifier: ^11.7.0 version: 11.7.0 @@ -3351,7 +3363,7 @@ packages: dependencies: '@unhead/dom': 0.5.1 '@unhead/schema': 0.5.1 - '@vueuse/shared': 10.7.2(vue@3.3.4) + '@vueuse/shared': 11.1.0(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 transitivePeerDependencies: @@ -3993,10 +4005,10 @@ packages: - vue dev: false - /@vueuse/shared@10.7.2(vue@3.3.4): - resolution: {integrity: sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==} + /@vueuse/shared@11.1.0(vue@3.3.4): + resolution: {integrity: sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==} dependencies: - vue-demi: 0.14.6(vue@3.3.4) + vue-demi: 0.14.10(vue@3.3.4) transitivePeerDependencies: - '@vue/composition-api' - vue @@ -4613,6 +4625,11 @@ packages: browserslist: 4.22.1 dev: true + /countries-and-timezones@3.6.0: + resolution: {integrity: sha512-8/nHBCs1eKeQ1jnsZVGdqrLYxS8nPcfJn8PnmxdJXWRLZdXsGFR8gnVhRjatGDBjqmPm7H+FtYpBYTPWd0Eiqg==} + engines: {node: '>=8.x', npm: '>=5.x'} + dev: false + /country-code-lookup@0.1.0: resolution: {integrity: sha512-IOI66HEG+8bXfWPy+sTzuN7161vmDZOHg1wgIPFf3WfD73FeLajnn6C+fnxOIa9RL1WRBDMXQQWW/FOaOYaQ3w==} dev: false @@ -4621,6 +4638,13 @@ packages: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} dev: false + /cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + dependencies: + luxon: 3.5.0 + dev: false + /cron-validator@1.3.1: resolution: {integrity: sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A==} dev: false @@ -5512,6 +5536,12 @@ packages: engines: {node: '>=0.10.0'} dev: true + /event-cron-parser@1.0.34: + resolution: {integrity: sha512-ytqZmMrNfSvzHWriiHdoNOpYKFr4d2fDoC4Rgq0F8lEA37abCWkYhSsqslC/kngWwnTGq7L0Q9VlMreKe6EJbQ==} + dependencies: + number-to-words: 1.2.4 + dev: false + /event-stream@3.3.4: resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} dependencies: @@ -5806,6 +5836,11 @@ packages: get-intrinsic: 1.2.2 dev: true + /get-timezone-offset@1.0.5: + resolution: {integrity: sha512-+B+/vEJ9qJgZheDVNmuY+4il8sJhTFXRvSiiqyRfwiCEhTaZqn/yCoNToDzQL+Mv9DLKlyO1bSIP5nUCJQN9Aw==} + engines: {node: '>=4.0.0'} + dev: false + /get-tsconfig@4.7.2: resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} dependencies: @@ -6753,6 +6788,11 @@ packages: dependencies: yallist: 4.0.0 + /luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + dev: false + /magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} dependencies: @@ -7106,6 +7146,10 @@ packages: boolbase: 1.0.0 dev: true + /number-to-words@1.2.4: + resolution: {integrity: sha512-/fYevVkXRcyBiZDg6yzZbm0RuaD6i0qRfn8yr+6D0KgBMOndFPxuW10qCHpzs50nN8qKuv78k8MuotZhcVX6Pw==} + dev: false + /nwsapi@2.2.7: resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} dev: true @@ -9136,8 +9180,8 @@ packages: vue: 3.3.4 dev: false - /vue-demi@0.14.5(vue@3.3.4): - resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} + /vue-demi@0.14.10(vue@3.3.4): + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} hasBin: true requiresBuild: true @@ -9151,8 +9195,8 @@ packages: vue: 3.3.4 dev: false - /vue-demi@0.14.6(vue@3.3.4): - resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} + /vue-demi@0.14.5(vue@3.3.4): + resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} engines: {node: '>=12'} hasBin: true requiresBuild: true @@ -9442,6 +9486,7 @@ packages: /workbox-google-analytics@7.0.0: resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} + deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained dependencies: workbox-background-sync: 7.0.0 workbox-core: 7.0.0 diff --git a/src/composable/queryParams.ts b/src/composable/queryParams.ts index 9699abbc..7cc8cc0d 100644 --- a/src/composable/queryParams.ts +++ b/src/composable/queryParams.ts @@ -1,7 +1,8 @@ import { useRouteQuery } from '@vueuse/router'; import { computed } from 'vue'; +import { useStorage } from '@vueuse/core'; -export { useQueryParam }; +export { useQueryParam, useQueryParamOrStorage }; const transformers = { number: { @@ -16,6 +17,12 @@ const transformers = { fromQuery: (value: string) => value.toLowerCase() === 'true', toQuery: (value: boolean) => (value ? 'true' : 'false'), }, + object: { + fromQuery: (value: string) => { + return JSON.parse(value); + }, + toQuery: (value: object) => JSON.stringify(value), + }, }; function useQueryParam({ name, defaultValue }: { name: string; defaultValue: T }) { @@ -33,3 +40,27 @@ function useQueryParam({ name, defaultValue }: { name: string; defaultValue: }, }); } + +function useQueryParamOrStorage({ name, storageName, defaultValue }: { name: string; storageName: string; defaultValue: T }) { + const type = typeof defaultValue; + const transformer = transformers[type as keyof typeof transformers] ?? transformers.string; + + const storageRef = useStorage(storageName, defaultValue); + const proxyDefaultValue = transformer.toQuery(defaultValue as never); + const proxy = useRouteQuery(name, proxyDefaultValue); + + const r = ref(defaultValue); + + watch(r, + (value) => { + proxy.value = transformer.toQuery(value as never); + storageRef.value = value as never; + }, + { deep: true }); + + r.value = (proxy.value && proxy.value !== proxyDefaultValue + ? transformer.fromQuery(proxy.value) as unknown as T + : storageRef.value as T) as never; + + return r; +} diff --git a/src/tools/crontab-generator/crontab-generator.service.test.ts b/src/tools/crontab-generator/crontab-generator.service.test.ts new file mode 100644 index 00000000..388a92bf --- /dev/null +++ b/src/tools/crontab-generator/crontab-generator.service.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { getCronType, getLastExecutionTimes, isCronValid } from './crontab-generator.service'; + +describe('crontab-generator', () => { + describe('isCronValid', () => { + it('should return true for all valid formats', () => { + expect(isCronValid('0 0 * * 1-5')).toBe(true); + expect(isCronValid('23 0-20/2 * * *')).toBe(true); + + // AWS formats + expect(isCronValid('0 11-22 ? * MON-FRI *')).toBe(true); + expect(isCronValid('0 0 ? * 1 *')).toBe(true); + }); + it('should return false for all invalid formats', () => { + expect(isCronValid('aert')).toBe(false); + expect(isCronValid('40 *')).toBe(false); + }); + }); + + describe('getCronType', () => { + it('should return right type', () => { + expect(getCronType('0 0 * * 1-5')).toBe('standard'); + expect(getCronType('23 0-20/2 * * *')).toBe('standard'); + + // AWS formats + expect(getCronType('0 11-22 ? * MON-FRI *')).toBe('aws'); + expect(getCronType('0 0 ? * 1 *')).toBe('aws'); + + expect(getCronType('aert')).toBe(false); + expect(getCronType('40 *')).toBe(false); + }); + }); + + describe('getLastExecutionTimes', () => { + it('should return next valid datetimes', () => { + expect(getLastExecutionTimes('0 0 * * 1-5')).toHaveLength(5); + expect(getLastExecutionTimes('23 0-20/2 * * *')).toHaveLength(5); + + // AWS formats + expect(getLastExecutionTimes('0 11-22 ? * MON-FRI *')).toHaveLength(5); + expect(getLastExecutionTimes('0 0 ? * 1 *')).toHaveLength(5); + }); + }); +}); diff --git a/src/tools/crontab-generator/crontab-generator.service.ts b/src/tools/crontab-generator/crontab-generator.service.ts new file mode 100644 index 00000000..1d44405b --- /dev/null +++ b/src/tools/crontab-generator/crontab-generator.service.ts @@ -0,0 +1,44 @@ +import { parseExpression } from 'cron-parser'; +import EventCronParser from 'event-cron-parser'; + +export function getLastExecutionTimes(cronExpression: string, tz: string | undefined = undefined, count: number = 5) { + if (getCronType(cronExpression) === 'standard') { + const interval = parseExpression(cronExpression, { tz }); + const times = []; + for (let i = 0; i < count; i++) { + times.push(interval.next().toJSON()); + } + return times; + } + if (getCronType(cronExpression) === 'aws') { + const parsed = new EventCronParser(cronExpression); + const times = []; + for (let i = 0; i < count; i++) { + times.push(JSON.stringify(parsed.next())); + } + return times; + } + + return []; +} + +export function isCronValid(v: string) { + return !!getCronType(v); +} + +export function getCronType(v: string) { + try { + parseExpression(v); + return 'standard'; + } + catch (_) { + try { + const parsed = new EventCronParser(v); + parsed.validate(); + return 'aws'; + } + catch (_) { + } + } + return false; +} diff --git a/src/tools/crontab-generator/crontab-generator.vue b/src/tools/crontab-generator/crontab-generator.vue index 97503e7d..f5f060b5 100644 --- a/src/tools/crontab-generator/crontab-generator.vue +++ b/src/tools/crontab-generator/crontab-generator.vue @@ -1,11 +1,10 @@ @@ -191,4 +326,12 @@ pre { overflow: auto; padding: 10px 0; } + +.cron-execution-string{ + text-align: center; + font-size: 14px; + opacity: 0.8; + margin: 5px 0 15px; + white-space: pre-wrap; +} diff --git a/src/tools/crontab-generator/get-timezone-offset.d.ts b/src/tools/crontab-generator/get-timezone-offset.d.ts new file mode 100644 index 00000000..3aa8e8db --- /dev/null +++ b/src/tools/crontab-generator/get-timezone-offset.d.ts @@ -0,0 +1,3 @@ +declare module "get-timezone-offset" { + export default function(timeZoneName: string, date: Date); +} diff --git a/src/tools/crontab-generator/index.ts b/src/tools/crontab-generator/index.ts index 429d6e14..c08c73ef 100644 --- a/src/tools/crontab-generator/index.ts +++ b/src/tools/crontab-generator/index.ts @@ -20,6 +20,7 @@ export const tool = defineTool({ 'day', 'minute', 'second', + 'aws', ], component: () => import('./crontab-generator.vue'), icon: Alarm,