Browse Source

Add text inlining and able to easily embed text

pull/46/head
coomdev 2 years ago
parent
commit
d73653e50f
  1. 7
      src/App.svelte
  2. 53
      src/Embedding.svelte
  3. 128
      src/PostOptions.svelte
  4. 4
      src/global.css
  5. 69
      src/main.ts
  6. 4
      src/thirdeye.ts
  7. 21
      src/utils.ts
  8. 9
      src/websites/index.ts

7
src/App.svelte

@ -113,13 +113,6 @@
Disable third-eye.
</label>
{#if !$settings.te}
<label>
<input type="checkbox" bind:checked={$settings.expte} />
<!-- svelte-ignore a11y-missing-attribute -->
Use Experimental Query API<a title="Can be up to 30% faster, reduces strain on boorus, may break">?</a
>
</label>
<h3>Booru sources</h3>
<div class="tagcont">
{#each $settings.rsources as source, i}

53
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 @@
/>
<!-- assoom videos will never be loaded from thumbnails -->
{/if}
{#if isText}
<!-- svelte-ignore a11y-media-has-caption -->
<!-- svelte-ignore a11y-missing-attribute -->
{#await content.text()}
<pre>Loading...</pre>
{:then con}
<pre>{con}</pre>
{/await}
<!-- assoom videos will never be loaded from thumbnails -->
{/if}
</div>
<div
bind:this={hoverElem}
@ -370,6 +405,16 @@
z-index: 9;
}
pre {
padding: 10px;
}
.contract pre {
max-width: 20ch;
text-overflow: ellipsis;
overflow: hidden;
}
.contract img,
.contract video {
max-width: 125px !important;

128
src/PostOptions.svelte

@ -0,0 +1,128 @@
<script lang="ts">
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()
}
</script>
<div class="root">
<!-- svelte-ignore a11y-missing-attribute -->
<a on:click={embedFile} title="Add a file">
<i class="fa fa-magnet"> {$appState.is4chanX ? '' : '🧲'} </i>
</a>
<div class="additionnal">
<!-- svelte-ignore a11y-missing-attribute -->
<a
on:click={embedText}
title="Add a message (this uses the content of the comment text box)"
>
<i class="fa fa-pencil"> {$appState.is4chanX ? '' : '🖉'} </i>
</a>
<!-- svelte-ignore a11y-missing-attribute -->
<a on:click={embedContent} title="Ready to Embed (Select a file before)">
<i class="fa fa-check"> {$appState.is4chanX ? '' : '✅'} </i>
</a>
{#if files.length}
<!-- svelte-ignore a11y-missing-attribute -->
<a on:click={() => (files = [])} title="Discard ALL selected content">
<i class="fa fa-times"> {$appState.is4chanX ? '' : '❌'} </i>
</a>
{/if}
</div>
</div>
<style scoped>
a {
cursor: pointer;
}
.root {
position: relative;
}
.additionnal {
display: none;
position: absolute;
flex-direction: column;
gap: 5px;
outline: 1px solid #ce3d08;
padding: 5px;
background-color: #fffdee;
border-radius: 5px;
left: 50%;
transform: translateX(-50%);
}
.root:hover > .additionnal {
display: flex;
}
</style>

4
src/global.css

@ -73,3 +73,7 @@ div.hasmultiple .catalog-host img {
display: flex;
gap: 20px;
}
#qr > form {
overflow: visible !important;
}

69
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<Buffer>);
data: EmbeddedFileWithoutPreview['data'] | ((lisn?: EventTarget) => Promise<Buffer>);
};
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<File>(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', <any>((e: CustomEvent<HTMLElement>) => {
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');

4
src/thirdeye.ts

@ -116,12 +116,12 @@ const shoujoFind = async (hex: string): Promise<ApiResult> => {
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}`);

21
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;
}));
};
};
export const getSelectedFile = () => {
return new Promise<File>(res => {
document.addEventListener('QRFile', e => res((e as any).detail), { once: true });
document.dispatchEvent(new CustomEvent('QRGetFile'));
});
};

9
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;
};
Loading…
Cancel
Save