This commit is contained in:
Egor Aristov 2025-02-23 13:54:21 +03:00
parent e477f8fc91
commit 6db3af59e3
Signed by: egor3f
GPG Key ID: 40482A264AAEC85F
4 changed files with 74 additions and 16 deletions

View File

@ -2,7 +2,7 @@
import Field from "@/components/Field.vue"; import Field from "@/components/Field.vue";
import {type Field as FieldSpec} from "@/urlmaker/specs"; import {type Field as FieldSpec} from "@/urlmaker/specs";
import {validateUrl} from "@/urlmaker/validators.ts"; import {validateOr, validatePreset, validateUrl} from "@/urlmaker/validators.ts";
import Btn from "@/components/Btn.vue"; import Btn from "@/components/Btn.vue";
import {onMounted, onUnmounted, ref, watch} from "vue"; import {onMounted, onUnmounted, ref, watch} from "vue";
import Modal from "@/components/Modal.vue"; import Modal from "@/components/Modal.vue";
@ -10,10 +10,10 @@ import Modal from "@/components/Modal.vue";
const field: FieldSpec = { const field: FieldSpec = {
name: '', name: '',
input_type: 'url', input_type: 'url',
label: 'URL of feed for editing', label: 'URL of feed or preset',
default: '', default: '',
required: true, required: true,
validate: validateUrl, validate: validateOr(validateUrl, validatePreset),
} }
const visible = defineModel('visible'); const visible = defineModel('visible');

View File

@ -4,20 +4,23 @@ import {ref, watch} from "vue";
import Btn from "@/components/Btn.vue"; import Btn from "@/components/Btn.vue";
import Copyable from "@/components/Copyable.vue"; import Copyable from "@/components/Copyable.vue";
import EditUrlModal from "@/components/EditUrlModal.vue"; import EditUrlModal from "@/components/EditUrlModal.vue";
import {decodeUrl, encodeUrl, getScreenshotUrl} from "@/urlmaker"; import {decodePreset, decodeUrl, encodePreset, encodeUrl, getScreenshotUrl} from "@/urlmaker";
import {useWizardStore} from "@/stores/wizard.ts"; import {useWizardStore} from "@/stores/wizard.ts";
import {debounce} from "es-toolkit"; import {debounce} from "es-toolkit";
import {validatePreset, validateUrl} from "@/urlmaker/validators.ts";
const store = useWizardStore(); const store = useWizardStore();
const existingLink = ref(""); const existingLink = ref("");
const link = ref(""); const resultLink = ref("");
const resultPreset = ref("");
const editModalVisible = ref(false); const editModalVisible = ref(false);
watch(existingLink, async (value) => { watch(existingLink, async (value) => {
if(!value) return; if(!value) return;
existingLink.value = ""; existingLink.value = "";
try { try {
store.updateSpecs(await decodeUrl(value)); if(validateUrl(value).ok) store.updateSpecs(await decodeUrl(value));
else if (validatePreset(value).ok) store.updateSpecs(await decodePreset(value));
} catch (e) { } catch (e) {
console.log(e); console.log(e);
alert(`Decoding error: ${e}`); alert(`Decoding error: ${e}`);
@ -26,17 +29,18 @@ watch(existingLink, async (value) => {
watch(store.specs, debounce(() => { watch(store.specs, debounce(() => {
if (store.formValid) { if (store.formValid) {
generateLink(); generate();
} else { } else {
link.value = ""; resultLink.value = "";
} }
}, 100), }, 100),
{immediate: true} {immediate: true}
); );
async function generateLink() { async function generate() {
try { try {
link.value = await encodeUrl(store.specs); resultLink.value = await encodeUrl(store.specs);
resultPreset.value = await encodePreset(store.specs);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
alert(`Encoding error: ${e}`); alert(`Encoding error: ${e}`);
@ -44,8 +48,10 @@ async function generateLink() {
} }
function screenshot() { function screenshot() {
if(store.formValid) {
window.open(getScreenshotUrl(store.specs.url)); window.open(getScreenshotUrl(store.specs.url));
} }
}
</script> </script>
@ -54,9 +60,12 @@ function screenshot() {
<SpecsForm class="specs-form"></SpecsForm> <SpecsForm class="specs-form"></SpecsForm>
<!-- <Btn :active="store.formValid" @click="generateLink">Generate link</Btn>--> <!-- <Btn :active="store.formValid" @click="generateLink">Generate link</Btn>-->
<Btn :active="store.formValid" @click="screenshot">Screenshot</Btn> <Btn :active="store.formValid" @click="screenshot">Screenshot</Btn>
<Btn @click="editModalVisible = true">Edit existing task</Btn> <Btn @click="editModalVisible = true">Edit existing task / import preset</Btn>
<Btn @click="store.reset">Reset Form</Btn> <Btn @click="store.reset">Reset Form</Btn>
<Copyable v-if="link" :contents="link" class="link-view"></Copyable> <div v-if="resultLink" class="link-label">Link for RSS reader:</div>
<Copyable v-if="resultLink" :contents="resultLink" class="link-view"></Copyable>
<div v-if="resultPreset" class="link-label">Preset for sharing:</div>
<Copyable v-if="resultPreset" :contents="resultPreset" class="link-view"></Copyable>
<EditUrlModal v-model:visible="editModalVisible" v-model="existingLink"></EditUrlModal> <EditUrlModal v-model:visible="editModalVisible" v-model="existingLink"></EditUrlModal>
</div> </div>
</template> </template>
@ -66,13 +75,14 @@ div.wrapper {
width: 100%; width: 100%;
max-width: 600px; max-width: 600px;
margin: auto; margin: auto;
padding-bottom: 50px;
} }
.specs-form { .specs-form {
margin-bottom: 15px; margin-bottom: 15px;
} }
.link-view { .link-label {
margin-top: 15px !important; margin-top: 15px;
} }
</style> </style>

View File

@ -3,6 +3,7 @@ import type {Specs} from "@/urlmaker/specs.ts";
const apiBase = import.meta.env.VITE_API_BASE || document.location.origin; const apiBase = import.meta.env.VITE_API_BASE || document.location.origin;
const renderEndpoint = '/api/v1/render/'; // trailing slash const renderEndpoint = '/api/v1/render/'; // trailing slash
const screenshotEndpoint = '/api/v1/screenshot'; // no trailing slash const screenshotEndpoint = '/api/v1/screenshot'; // no trailing slash
export const presetPrefix = 'rssalchemy:';
export async function decodeUrl(url: string): Promise<Specs> { export async function decodeUrl(url: string): Promise<Specs> {
const splitUrl = url.split(renderEndpoint); const splitUrl = url.split(renderEndpoint);
@ -10,6 +11,18 @@ export async function decodeUrl(url: string): Promise<Specs> {
throw 'Split failed'; throw 'Split failed';
} }
let encodedData = splitUrl[1]; let encodedData = splitUrl[1];
return decodeSpecsPart(encodedData);
}
export async function decodePreset(preset: string): Promise<Specs> {
if(!preset.startsWith(presetPrefix)) {
throw 'Invalid preset';
}
let encodedData = preset.substring(presetPrefix.length);
return decodeSpecsPart(encodedData);
}
export async function decodeSpecsPart(encodedData: string): Promise<Specs> {
console.log('Data len=' + encodedData.length); console.log('Data len=' + encodedData.length);
const m = encodedData.match(/(\d*):?([A-Za-z0-9+/=]+)/); const m = encodedData.match(/(\d*):?([A-Za-z0-9+/=]+)/);
if(!m) { if(!m) {
@ -28,12 +41,20 @@ export async function decodeUrl(url: string): Promise<Specs> {
} }
export async function encodeUrl(specs: Specs): Promise<string> { export async function encodeUrl(specs: Specs): Promise<string> {
return `${apiBase}${renderEndpoint}${await encodeSpecsPart(specs)}`
}
export async function encodePreset(specs: Specs): Promise<string> {
return `${presetPrefix}${await encodeSpecsPart(specs)}`;
}
export async function encodeSpecsPart(specs: Specs): Promise<string> {
const jsonData = JSON.stringify(specs); const jsonData = JSON.stringify(specs);
const buf = await compress(jsonData); const buf = await compress(jsonData);
const encodedData = b64encode(buf); const encodedData = b64encode(buf);
console.log('Data len=' + encodedData.length); console.log('Data len=' + encodedData.length);
const version = 0; const version = 0;
return `${apiBase}${renderEndpoint}${version}:${encodedData}` return `${version}:${encodedData}`;
} }
export function getScreenshotUrl(url: string): string { export function getScreenshotUrl(url: string): string {
@ -55,7 +76,9 @@ async function compress(s: string): Promise<Uint8Array> {
let byteArray = new TextEncoder().encode(s); let byteArray = new TextEncoder().encode(s);
let cs = new CompressionStream('deflate-raw'); let cs = new CompressionStream('deflate-raw');
let writer = cs.writable.getWriter(); let writer = cs.writable.getWriter();
// noinspection ES6MissingAwait
writer.write(byteArray); writer.write(byteArray);
// noinspection ES6MissingAwait
writer.close(); writer.close();
let response = new Response(cs.readable); let response = new Response(cs.readable);
return new Uint8Array(await response.arrayBuffer()); return new Uint8Array(await response.arrayBuffer());
@ -64,7 +87,9 @@ async function compress(s: string): Promise<Uint8Array> {
async function decompress(buf: Uint8Array): Promise<string> { async function decompress(buf: Uint8Array): Promise<string> {
let ds = new DecompressionStream('deflate-raw'); let ds = new DecompressionStream('deflate-raw');
let writer = ds.writable.getWriter(); let writer = ds.writable.getWriter();
// noinspection ES6MissingAwait
writer.write(buf); writer.write(buf);
// noinspection ES6MissingAwait
writer.close(); writer.close();
let response = new Response(ds.readable); let response = new Response(ds.readable);
return response.text(); return response.text();

View File

@ -1,3 +1,5 @@
import {presetPrefix} from "@/urlmaker/index.ts";
type validResult = { ok: boolean, error?: string }; type validResult = { ok: boolean, error?: string };
export type validator = (v: string) => validResult export type validator = (v: string) => validResult
@ -14,6 +16,27 @@ export function validateUrl(s: string): validResult {
} }
} }
export function validatePreset(s: string): validResult {
if(!s.startsWith(presetPrefix)) {
return {
ok: false,
error: 'Not a preset'
}
}
return {ok: true}
}
export function validateOr(...validators: validator[]): validator {
return function(s: string): validResult {
return validators.reduce<validResult>((res, v) => {
let r = v(s);
if(r.ok) res.ok = true;
else res.error += r.error + '; ';
return res;
}, {ok: false, error: ''});
}
}
export function validateSelector(s: string): validResult { export function validateSelector(s: string): validResult {
try { try {
document.createDocumentFragment().querySelector(s); document.createDocumentFragment().querySelector(s);