diff --git a/src/App.svelte b/src/App.svelte index c48973c..c08a0d8 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -113,13 +113,6 @@ Disable third-eye. {#if !$settings.te} - -

Booru sources

{#each $settings.rsources as source, i} diff --git a/src/Embedding.svelte b/src/Embedding.svelte index c51fadd..8ffaee7 100644 --- a/src/Embedding.svelte +++ b/src/Embedding.svelte @@ -5,6 +5,9 @@ import type { EmbeddedFile } from './main' import { createEventDispatcher } from 'svelte' import { GM_head, headerStringToObject } from '../dist/requests' + import { text } from 'svelte/internal' + import App from './App.svelte' + import { Buffer } from 'buffer' export const dispatch = createEventDispatcher() @@ -12,6 +15,7 @@ let isVideo = false let isImage = false let isAudio = false + let isText = false let url = '' let settled = false let contracted = true @@ -38,6 +42,7 @@ return contracted } + let content: Blob beforeUpdate(async () => { if (settled) return settled = true @@ -46,7 +51,15 @@ let type: FileTypeResult | undefined if (typeof thumb != 'string') { type = await fileTypeFromBuffer(thumb) - url = URL.createObjectURL(new Blob([thumb], { type: type?.mime })) + if ( + !type && + file.filename.endsWith('.txt') && + file.filename.startsWith('message') + ) { + type = { ext: 'txt', mime: 'text/plain' } as any + } + content = new Blob([thumb], { type: type?.mime }) + url = URL.createObjectURL(content) if (!type) return } else { let head = headerStringToObject(await GM_head(thumb, undefined)) @@ -56,6 +69,7 @@ isVideo = type.mime.startsWith('video/') isAudio = type.mime.startsWith('audio/') isImage = type.mime.startsWith('image/') + isText = type.mime.startsWith('text/plain') dispatch('fileinfo', { type }) if (isImage) { @@ -97,9 +111,17 @@ lisn.addEventListener('progress', (e: any) => { progress = e.detail }) - let full = await file.data(lisn) + let full = Buffer.isBuffer(file.data) ? file.data : await file.data(lisn) type = await fileTypeFromBuffer(full) - furl = URL.createObjectURL(new Blob([full], { type: type?.mime })) + if ( + !type && + file.filename.endsWith('.txt') && + file.filename.startsWith('message') + ) { + type = { ext: 'txt', mime: 'text/plain' } as any + } + content = new Blob([full], { type: type?.mime }) + furl = URL.createObjectURL(content) } else { url = file.data furl = file.data @@ -110,6 +132,7 @@ isVideo = type.mime.startsWith('video/') isAudio = type.mime.startsWith('audio/') isImage = type.mime.startsWith('image/') + isText = type.mime.startsWith('text/plain') unzipping = false dispatch('fileinfo', { type }) @@ -186,7 +209,7 @@ let [iw, ih] = [0, 0] if (isImage) { ;[iw, ih] = [imgElem.naturalWidth, imgElem.naturalHeight] - } else { + } else if (isVideo) { ;[iw, ih] = [videoElem.videoWidth, videoElem.videoHeight] } let scale = Math.min(1, sw / iw, sh / ih) @@ -197,6 +220,7 @@ } async function hoverStart(ev?: MouseEvent) { + if (!(isVideo || isImage)) return if ($settings.dh) return if (file.thumbnail && !furl) { unzip() @@ -231,6 +255,7 @@ lastev = lastev || ev if ($settings.dh) return if (!contracted) return + if (!(isVideo || isImage)) return recompute() // yeah I gave up const [sw, sh] = [visualViewport.width, visualViewport.height] // shamelessly stolen from 4chanX @@ -310,6 +335,16 @@ /> {/if} + {#if isText} + + + {#await content.text()} +
Loading...
+ {:then con} +
{con}
+ {/await} + + {/if}
+ import { appState } from './stores' + import type { ImageProcessor } from './main' + + import { fireNotification, getSelectedFile } from './utils' + + export let processors: ImageProcessor[] = [] + export let textinput: HTMLTextAreaElement + + let files: File[] = [] + + const addContent = (...newfiles: File[]) => { + files = [...files, ...newfiles] + if (files.length > 5) { + fireNotification( + 'warning', + 'Can only add up to 5 attachments, further attachments will be dropped', + ) + files = files.slice(0, 5) + } + } + + const embedText = async (e: Event) => { + if (textinput.value == '') return + if (textinput.value.length > 2000) { + fireNotification("error", "Message attachments are limited to 2000 characters") + return; + } + addContent( + new File( + [new Blob([textinput.value], { type: 'text/plain' })], + `message${files.length}.txt`, + ), + ) + textinput.value = '' + } + + const embedContent = async (e: Event) => { + const file = await getSelectedFile() + if (!file) return + const type = file.type + try { + const proc = processors + .filter((e) => e.inject) + .find((e) => e.match(file.name)) + if (!proc) throw new Error('Container filetype not supported') + const buff = await proc.inject!(file, [...files].slice(0, 5)) + document.dispatchEvent( + new CustomEvent('QRSetFile', { + //detail: { file: new Blob([buff]), name: file.name, type: file.type } + detail: { file: new Blob([buff], { type }), name: file.name }, + }), + ) + fireNotification( + 'success', + `File${files.length > 1 ? 's' : ''} successfully embedded!`, + ) + } catch (err) { + const e = err as Error + fireNotification('error', "Couldn't embed file: " + e.message) + } + } + + const embedFile = async (e: Event) => { + const input = document.createElement('input') as HTMLInputElement + input.setAttribute('type', 'file') + input.multiple = true + input.onchange = async (ev) => { + if (input.files) { + addContent(...input.files) + } + } + input.click() + } + + +
+ + + {$appState.is4chanX ? '' : '🧲'} + + +
+ + diff --git a/src/global.css b/src/global.css index fac8fe6..e1a61e3 100644 --- a/src/global.css +++ b/src/global.css @@ -73,3 +73,7 @@ div.hasmultiple .catalog-host img { display: flex; gap: 20px; } + +#qr > form { + overflow: visible !important; +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index daf5260..f978b55 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,7 @@ import { GM_fetch, GM_head, headerStringToObject } from "./requests"; import App from "./App.svelte"; import ScrollHighlighter from "./ScrollHighlighter.svelte"; +import PostOptions from "./PostOptions.svelte"; import SettingsButton from './SettingsButton.svelte'; //import Embedding from './Embedding.svelte'; import Embeddings from './Embeddings.svelte'; @@ -77,7 +78,7 @@ type EmbeddedFileWithPreview = { source?: string; // can be like a twitter post this was posted in originally thumbnail: Buffer; filename: string; - data: string | ((lisn?: EventTarget) => Promise); + data: EmbeddedFileWithoutPreview['data'] | ((lisn?: EventTarget) => Promise); }; type EmbeddedFileWithoutPreview = { @@ -244,9 +245,8 @@ const scrapeBoard = async (self: HTMLButtonElement) => { fireNotification("success", "Processing finished!"); }; -const startup = async () => { - if (typeof (window as any)['FCX'] != "undefined") - appState.set({ ...cappState, is4chanX: true }); +const startup = async (is4chanX = true) => { + appState.set({ ...cappState, is4chanX }); if (csettings.vercheck) versionCheck(); @@ -296,11 +296,12 @@ const startup = async () => { if (cappState.isCatalog) { const opts = document.getElementById('index-options') as HTMLDivElement; - const button = document.createElement('button'); - button.textContent = "おもらし"; - button.onclick = () => scrapeBoard(button); - opts.insertAdjacentElement("beforebegin", button); - + if (opts) { + const button = document.createElement('button'); + button.textContent = "おもらし"; + button.onclick = () => scrapeBoard(button); + opts.insertAdjacentElement("beforebegin", button); + } } const n = 7; @@ -315,15 +316,8 @@ const startup = async () => { //await Promise.all(posts.map(e => processPost(e as any))); }; -const getSelectedFile = () => { - return new Promise(res => { - document.addEventListener('QRFile', e => res((e as any).detail), { once: true }); - document.dispatchEvent(new CustomEvent('QRGetFile')); - }); -}; - //if (cappState!.is4chanX) -document.addEventListener('4chanXInitFinished', startup); +document.addEventListener('4chanXInitFinished', () => startup(true)); /*else { document.addEventListener("QRGetFile", (e) => { const qr = document.getElementById('qrFile') as HTMLInputElement | null; @@ -353,51 +347,22 @@ if (cappState!.is4chanX) { } document.addEventListener('QRDialogCreation', ((e: CustomEvent) => { - const a = document.createElement('a'); - const i = document.createElement('i'); - i.className = "fa fa-magnet"; - a.appendChild(i); - a.title = "Embed File (Select a file before...)"; + const a = document.createElement('span'); let target; - if (cappState.is4chanX) { - i.innerText = "🧲"; + if (!cappState.is4chanX) { target = e.detail; target.querySelector("input[type=submit]")?.insertAdjacentElement("beforebegin", a); } else { target = e.target as HTMLDivElement; + new PostOptions({ + target: a, + props: { processors, textinput: target.querySelector('textarea')! } + }); target.querySelector('#qr-filename-container')?.appendChild(a); } - a.onclick = async (e) => { - const file = await getSelectedFile(); - if (!file) - return; - const input = document.createElement('input') as HTMLInputElement; - input.setAttribute("type", "file"); - const type = file.type; - input.multiple = true; - input.onchange = (async ev => { - if (input.files) { - try { - const proc = processors.filter(e => e.inject).find(e => e.match(file.name)); - if (!proc) - throw new Error("Container filetype not supported"); - const buff = await proc.inject!(file, [...input.files].slice(0, 5)); - document.dispatchEvent(new CustomEvent('QRSetFile', { - //detail: { file: new Blob([buff]), name: file.name, type: file.type } - detail: { file: new Blob([buff], { type }), name: file.name } - })); - fireNotification('success', `File${input.files.length > 1 ? 's' : ''} successfully embedded!`); - } catch (err) { - const e = err as Error; - fireNotification('error', "Couldn't embed file: " + e.message); - } - } - }); - input.click(); - }; }), { once: !cappState!.is4chanX }); // 4chan's normal extension destroys the QR form everytime const customStyles = document.createElement('style'); diff --git a/src/thirdeye.ts b/src/thirdeye.ts index 37b1d76..90e6bf1 100644 --- a/src/thirdeye.ts +++ b/src/thirdeye.ts @@ -116,12 +116,12 @@ const shoujoFind = async (hex: string): Promise => { const findFileFrom = async (b: Booru, hex: string, abort?: EventTarget) => { try { - if (experimentalApi) { +/* if (experimentalApi) { const res = await shoujoFind(hex); if (!res) debugger; return hex in res ? (res[hex][b.domain] || []) : []; - } + }*/ if (b.domain in cache && hex in cache[b.domain]) return cache[b.domain][hex] as BooruMatch[]; const res = await GM_fetch(`https://${b.domain}${b.endpoint}${hex}`); diff --git a/src/utils.ts b/src/utils.ts index 99a87b1..5c94d1d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -60,7 +60,7 @@ export const buildPeeFile = async (f: File) => { const namebuf = Buffer.from(f.name); const ret = Buffer.alloc(4 /* Magic */ + 1 /* Flags */ + namebuf.byteLength + 1 + - (4 + thumbnail.byteLength) /* TSize + Thumbnail */ + + (thumbnail.byteLength != 0 ? (4 + thumbnail.byteLength) : 0) /* TSize + Thumbnail */ + f.size /*Teh file*/); let ptr = 0; ret.write('PEE\0', 0); @@ -126,12 +126,16 @@ export const decodeCoom3Payload = async (buff: Buffer) => { let thumbsize = 0; if (hasThumbnail) { thumbsize = header.readInt32LE(ptr); - thumb = Buffer.from(await (await GM_fetch(pee, { headers: { 'user-agent': '', range: `bytes=${ptr + 4}-${ptr + 4 + thumbsize}` } })).arrayBuffer()); + ptr += 4; + thumb = Buffer.from(await (await GM_fetch(pee, { headers: { 'user-agent': '', range: `bytes=${ptr}-${ptr + thumbsize}` } })).arrayBuffer()); + ptr += thumbsize; } + const unzip = async (lsn?: EventTarget) => + Buffer.from(await (await GM_fetch(pee, { headers: { 'user-agent': '', range: `bytes=${ptr}-${size - 1}` } }, lsn)).arrayBuffer()); return { filename: fn, - data: async (lsn) => - Buffer.from(await (await GM_fetch(pee, { headers: { 'user-agent': '', range: `bytes=${ptr + 4 + thumbsize}-${size - 1}` } }, lsn)).arrayBuffer()), + // if file is small, then just get it fully + data: size < 3072 ? await unzip() : unzip, thumbnail: thumb, } as EmbeddedFile; } catch (e) { @@ -175,4 +179,11 @@ export const uploadFiles = async (injs: File[]) => { fireNotification('info', `Uploaded files [${++total}/${injs.length}] ${ret}`); return ret; })); -}; \ No newline at end of file +}; + +export const getSelectedFile = () => { + return new Promise(res => { + document.addEventListener('QRFile', e => res((e as any).detail), { once: true }); + document.dispatchEvent(new CustomEvent('QRGetFile')); + }); +}; diff --git a/src/websites/index.ts b/src/websites/index.ts new file mode 100644 index 0000000..508e7a6 --- /dev/null +++ b/src/websites/index.ts @@ -0,0 +1,9 @@ +export type QueryProcessor = { + thumbnailSelector: string; + md5Selector: string; + filenameSelector: string; + linkSelector: string; + postWithFileSelector: string; + postContainerSelector: string; + controlHostSelector: string; +};