improve the import/export process to allow for non-ascii characters.

This commit is contained in:
arabcoders 2025-06-19 18:11:19 +03:00
parent 746b490216
commit 86da6c2b06
10 changed files with 39 additions and 78 deletions

View file

@ -209,6 +209,7 @@
<script setup>
import { useStorage } from '@vueuse/core'
const emitter = defineEmits(['cancel', 'submit'])
import { decode } from '~/utils/importer'
const props = defineProps({
reference: {
@ -321,18 +322,8 @@ const importItem = async () => {
return
}
if (false === val.startsWith('{')) {
try {
val = base64UrlDecode(val)
} catch (e) {
console.error(e)
toast.error(`Failed to decode string. ${e.message}`)
return
}
}
try {
const item = JSON.parse(val)
const item = decode(val)
if (!item?._type || 'condition' !== item._type) {
toast.error(`Invalid import string. Expected type 'condition', got '${item._type ?? 'unknown'}'.`)

View file

@ -267,6 +267,8 @@
<script setup>
import { useStorage } from '@vueuse/core'
import { decode } from '~/utils/importer'
const emitter = defineEmits(['cancel', 'submit']);
const toast = useNotification();
const props = defineProps({
@ -339,18 +341,8 @@ const importItem = async () => {
return
}
if (false === val.startsWith('{')) {
try {
val = base64UrlDecode(val)
} catch (e) {
console.error(e)
toast.error(`Failed to decode string. ${e.message}`)
return
}
}
try {
const item = JSON.parse(val)
const item = decode(val)
if ('notification' !== item._type) {
toast.error(`Invalid import string. Expected type 'notification', got '${item._type}'.`)

View file

@ -232,6 +232,8 @@
<script setup>
import { useStorage } from '@vueuse/core'
import { decode } from '~/utils/importer'
const emitter = defineEmits(['cancel', 'submit']);
const props = defineProps({
@ -351,18 +353,8 @@ const importItem = async () => {
return
}
if (false === val.startsWith('{')) {
try {
val = base64UrlDecode(val)
} catch (e) {
console.error(e)
toast.error(`Failed to decode string. ${e.message}`)
return
}
}
try {
const item = JSON.parse(val)
const item = decode(val)
if (!item?._type || 'preset' !== item._type) {
toast.error(`Invalid import string. Expected type 'preset', got '${item._type ?? 'unknown'}'.`)

View file

@ -271,6 +271,7 @@
<script setup>
import { useStorage } from '@vueuse/core'
import { CronExpressionParser } from 'cron-parser'
import { decode } from '~/utils/importer'
const props = defineProps({
reference: {
@ -352,18 +353,8 @@ const importItem = async () => {
return
}
if (false === val.startsWith('{')) {
try {
val = base64UrlDecode(val)
} catch (e) {
console.error(e)
toast.error(`Failed to decode string. ${e.message}`)
return
}
}
try {
const item = JSON.parse(val)
const item = decode(val)
if ('task' !== item._type) {
toast.error(`Invalid import string. Expected type 'task', got '${item._type}'.`)

View file

@ -116,6 +116,7 @@
<script setup>
import { request } from '~/utils/index'
import { encode } from '~/utils/importer'
const toast = useNotification()
const config = useConfigStore()
@ -286,6 +287,6 @@ const exportItem = item => {
userData['_type'] = 'condition'
userData['_version'] = '1.0'
return copyText(base64UrlEncode(JSON.stringify(userData)))
return copyText(encode(userData))
}
</script>

View file

@ -139,6 +139,7 @@ div.is-centered {
<script setup>
import { request } from '~/utils/index'
import { encode } from '~/utils/importer'
const toast = useNotification()
const config = useConfigStore()
@ -337,6 +338,6 @@ const exportItem = async item => {
data['_type'] = 'notification'
data['_version'] = '1.0'
return copyText(base64UrlEncode(JSON.stringify(data)))
return copyText(encode(data))
}
</script>

View file

@ -213,6 +213,7 @@ div.is-centered {
<script setup>
import { useStorage } from '@vueuse/core'
import { request } from '~/utils/index'
import { encode } from '~/utils/importer'
const toast = useNotification()
const config = useConfigStore()
@ -400,7 +401,7 @@ const exportItem = item => {
userData['_type'] = 'preset'
userData['_version'] = '2.5'
return copyText(base64UrlEncode(JSON.stringify(userData)))
return copyText(encode(userData))
}
const calcPath = (path) => {

View file

@ -243,6 +243,7 @@ import moment from 'moment'
import { useStorage } from '@vueuse/core'
import { CronExpressionParser } from 'cron-parser'
import { request } from '~/utils/index'
import { encode } from '~/utils/importer'
import type { task_item, exported_task, error_response } from '~/@types/tasks'
const box = useConfirm()
@ -496,8 +497,7 @@ const exportItem = async (item: task_item) => {
data._type = 'task'
data._version = '2.0'
return copyText(base64UrlEncode(JSON.stringify(data)));
return copyText(encode(data));
}
</script>

21
ui/utils/importer.ts Normal file
View file

@ -0,0 +1,21 @@
const encode = (obj: Record<string, any>): string => {
const jsonStr = JSON.stringify(obj);
const utf8Bytes = new TextEncoder().encode(jsonStr);
const binary = String.fromCharCode(...utf8Bytes);
const base64 = btoa(binary);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
const decode = (str: string): object => {
const base64 = str
.replace(/-/g, '+')
.replace(/_/g, '/')
.padEnd(str.length + (4 - str.length % 4) % 4, '=');
const binary = atob(base64);
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
const jsonStr = new TextDecoder().decode(bytes);
return JSON.parse(jsonStr);
}
export { encode, decode }

View file

@ -406,33 +406,6 @@ const convertCliOptions = async opts => {
return data
}
/**
* URL Safe Base64 Encode.
*
* @param {String} data The data to encode
*
* @returns {String} The encoded data
*/
const base64UrlEncode = data => btoa(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
/**
* URL Safe Base64 Decode.
*
* @param {String} input The input string to decode
*
* @returns {String} The decoded data
*/
const base64UrlDecode = input => {
let base64 = input.replace(/-/g, '+').replace(/_/g, '/');
const pad = base64.length % 4;
if (pad) {
base64 += '='.repeat(4 - pad);
}
return atob(base64);
}
/**
* Check if array or object has data.
*
@ -539,8 +512,6 @@ export {
makeDownload,
formatBytes,
convertCliOptions,
base64UrlEncode,
base64UrlDecode,
has_data,
toggleClass,
cleanObject,