mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-01 18:22:41 -08:00
initial commit
This commit is contained in:
747
vendor/texthooker-ui/src/components/App.svelte
vendored
Normal file
747
vendor/texthooker-ui/src/components/App.svelte
vendored
Normal file
@@ -0,0 +1,747 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
mdiArrowULeftTop,
|
||||
mdiCancel,
|
||||
mdiCog,
|
||||
mdiDelete,
|
||||
mdiDeleteForever,
|
||||
mdiNoteEdit,
|
||||
mdiPause,
|
||||
mdiPlay,
|
||||
mdiWindowMaximize,
|
||||
mdiWindowRestore,
|
||||
} from '@mdi/js';
|
||||
import { debounceTime, filter, fromEvent, map, NEVER, switchMap, tap } from 'rxjs';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { quintInOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import {
|
||||
actionHistory$,
|
||||
allowNewLineDuringPause$,
|
||||
allowPasteDuringPause$,
|
||||
autoStartTimerDuringPause$,
|
||||
autoStartTimerDuringPausePaste$,
|
||||
blockCopyOnPage$,
|
||||
customCSS$,
|
||||
dialogOpen$,
|
||||
displayVertical$,
|
||||
enabledReplacements$,
|
||||
enablePaste$,
|
||||
filterNonCJKLines$,
|
||||
flashOnMissedLine$,
|
||||
flashOnPauseTimeout$,
|
||||
fontSize$,
|
||||
isPaused$,
|
||||
lastPipHeight$,
|
||||
lastPipWidth$,
|
||||
lineData$,
|
||||
maxLines$,
|
||||
maxPipLines$,
|
||||
mergeEqualLineStarts$,
|
||||
newLine$,
|
||||
notesOpen$,
|
||||
onlineFont$,
|
||||
openDialog$,
|
||||
preventGlobalDuplicate$,
|
||||
preventLastDuplicate$,
|
||||
removeAllWhitespace$,
|
||||
replacements$,
|
||||
reverseLineOrder$,
|
||||
secondaryWebsocketUrl$,
|
||||
showConnectionIcon$,
|
||||
showSpinner$,
|
||||
theme$,
|
||||
websocketUrl$,
|
||||
} from '../stores/stores';
|
||||
import { LineType, OnlineFont, Theme, type LineItem, type LineItemEditEvent } from '../types';
|
||||
import {
|
||||
applyAfkBlur,
|
||||
applyCustomCSS,
|
||||
applyReplacements,
|
||||
generateRandomUUID,
|
||||
newLineCharacter,
|
||||
reduceToEmptyString,
|
||||
updateScroll,
|
||||
} from '../util';
|
||||
import DialogManager from './DialogManager.svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
import Line from './Line.svelte';
|
||||
import Notes from './Notes.svelte';
|
||||
import Presets from './Presets.svelte';
|
||||
import Settings from './Settings.svelte';
|
||||
import SocketConnector from './SocketConnector.svelte';
|
||||
import Spinner from './Spinner.svelte';
|
||||
import Stats from './Stats.svelte';
|
||||
|
||||
let isSmFactor = false;
|
||||
let settingsComponent: Settings;
|
||||
let selectedLineIds: string[] = [];
|
||||
let settingsContainer: HTMLElement;
|
||||
let settingsElement: SVGElement;
|
||||
let settingsOpen = false;
|
||||
let lineContainer: HTMLElement;
|
||||
let lineElements: Line[] = [];
|
||||
let lineInEdit = false;
|
||||
let blockNextExternalLine = false;
|
||||
let wakeLock = null;
|
||||
let pipContainer: HTMLElement;
|
||||
let pipWindow: Window | undefined;
|
||||
let pipResizeTimeout: number;
|
||||
let hasPipFocus = false;
|
||||
|
||||
const wakeLockAvailable = 'wakeLock' in navigator;
|
||||
|
||||
const cjkCharacters = /[\p{scx=Hira}\p{scx=Kana}\p{scx=Han}]/imu;
|
||||
|
||||
const uniqueLines$ = preventGlobalDuplicate$.pipe(
|
||||
map((preventGlobalDuplicate) =>
|
||||
preventGlobalDuplicate ? new Set<string>($lineData$.map((line) => line.text)) : new Set<string>(),
|
||||
),
|
||||
);
|
||||
|
||||
const handleLine$ = newLine$.pipe(
|
||||
filter(([_, lineType]) => {
|
||||
const isPaste = lineType === LineType.PASTE;
|
||||
const hasNoUserInteraction = !isPaste || (!$notesOpen$ && !$dialogOpen$ && !settingsOpen && !lineInEdit);
|
||||
const skipExternalLine = blockNextExternalLine && lineType === LineType.EXTERNAL;
|
||||
|
||||
if (skipExternalLine) {
|
||||
blockNextExternalLine = false;
|
||||
}
|
||||
|
||||
if (
|
||||
(!$isPaused$ ||
|
||||
(($allowPasteDuringPause$ || $autoStartTimerDuringPausePaste$) && isPaste) ||
|
||||
(($allowNewLineDuringPause$ || $autoStartTimerDuringPause$) && !isPaste)) &&
|
||||
hasNoUserInteraction &&
|
||||
!skipExternalLine
|
||||
) {
|
||||
if (
|
||||
$isPaused$ &&
|
||||
(($autoStartTimerDuringPausePaste$ && isPaste) || ($autoStartTimerDuringPause$ && !isPaste))
|
||||
) {
|
||||
$isPaused$ = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!skipExternalLine && hasNoUserInteraction && $flashOnMissedLine$) {
|
||||
handleMissedLine();
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
tap((newLine: [string, LineType]) => {
|
||||
const [lineContent] = newLine;
|
||||
const text = transformLine(lineContent);
|
||||
|
||||
if (text) {
|
||||
$lineData$ = applyEqualLineStartMerge([
|
||||
...applyMaxLinesAndGetRemainingLineData(1),
|
||||
{ id: generateRandomUUID(), text },
|
||||
]);
|
||||
}
|
||||
}),
|
||||
reduceToEmptyString(),
|
||||
);
|
||||
|
||||
const pasteHandler$ = enablePaste$.pipe(
|
||||
switchMap((enablePaste) => (enablePaste ? fromEvent(document, 'paste') : NEVER)),
|
||||
tap((event: ClipboardEvent) => newLine$.next([event.clipboardData.getData('text/plain'), LineType.PASTE])),
|
||||
reduceToEmptyString(),
|
||||
);
|
||||
|
||||
const visibilityHandler$ = fromEvent(document, 'visibilitychange').pipe(
|
||||
tap(() => {
|
||||
if (wakeLockAvailable && wakeLock !== null && document.visibilityState === 'visible') {
|
||||
wakeLock = navigator.wakeLock
|
||||
.request('screen')
|
||||
.then((lock) => {
|
||||
return lock;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Unable to aquire screen lock: ${error.message}`);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}),
|
||||
reduceToEmptyString(),
|
||||
);
|
||||
|
||||
const copyBlocker$ = blockCopyOnPage$.pipe(
|
||||
switchMap((blockCopyOnPage) => {
|
||||
blockNextExternalLine = false;
|
||||
|
||||
return blockCopyOnPage ? fromEvent(document, 'copy') : NEVER;
|
||||
}),
|
||||
tap(() => (blockNextExternalLine = true)),
|
||||
reduceToEmptyString(),
|
||||
);
|
||||
|
||||
const resizeHandler$ = fromEvent(window, 'resize').pipe(
|
||||
debounceTime(500),
|
||||
tap(mountFunction),
|
||||
reduceToEmptyString(),
|
||||
);
|
||||
|
||||
$: iconSize = isSmFactor ? '1.5rem' : '1.25rem';
|
||||
|
||||
$: $enabledReplacements$ = $replacements$.filter((replacment) => replacment.enabled);
|
||||
|
||||
$: pipAvailable = 'documentPictureInPicture' in window && !!pipContainer;
|
||||
|
||||
$: pipLines = pipAvailable && $lineData$ ? $lineData$.slice(-$maxPipLines$) : [];
|
||||
|
||||
$: if (pipWindow) {
|
||||
pipWindow.document.body.dataset.theme = $theme$;
|
||||
|
||||
applyCustomCSS(pipWindow.document, $customCSS$);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mountFunction();
|
||||
if (wakeLockAvailable) {
|
||||
wakeLock = navigator.wakeLock
|
||||
.request('screen')
|
||||
.then((lock) => {
|
||||
return lock;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Unable to aquire screen lock: ${error.message}`);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function mountFunction() {
|
||||
isSmFactor = window.matchMedia('(min-width: 640px)').matches;
|
||||
executeUpdateScroll();
|
||||
}
|
||||
|
||||
function handleKeyPress(event: KeyboardEvent) {
|
||||
if ($notesOpen$ || $dialogOpen$ || settingsOpen || lineInEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = (event.key || '')?.toLowerCase();
|
||||
|
||||
if (key === 'delete') {
|
||||
if (window.getSelection()?.toString().trim()) {
|
||||
const range = window.getSelection().getRangeAt(0);
|
||||
|
||||
for (let index = 0, { length } = lineElements; index < length; index += 1) {
|
||||
const lineElement = lineElements[index];
|
||||
const selectedId = lineElement?.getIdIfSelected(range);
|
||||
|
||||
if (selectedId) {
|
||||
selectedLineIds.push(selectedId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedLineIds.length) {
|
||||
removeLines();
|
||||
} else if (event.altKey) {
|
||||
removeLastLine();
|
||||
}
|
||||
} else if (selectedLineIds.length && key === 'escape') {
|
||||
deselectLines();
|
||||
} else if (event.altKey && key === 'a') {
|
||||
settingsComponent.handleReset(false);
|
||||
} else if (event.altKey && key === 'q') {
|
||||
settingsComponent.handleReset(true);
|
||||
} else if ((event.ctrlKey || event.metaKey) && key === ' ') {
|
||||
$isPaused$ = !$isPaused$;
|
||||
} else if (event.altKey && key === 'g') {
|
||||
$showConnectionIcon$ = !$showConnectionIcon$;
|
||||
}
|
||||
}
|
||||
|
||||
async function undoLastAction() {
|
||||
if (!$actionHistory$.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linesToRevert = $actionHistory$.pop();
|
||||
|
||||
let lineToRevert = linesToRevert.pop();
|
||||
|
||||
while (lineToRevert) {
|
||||
const text = transformLine(lineToRevert.text, false);
|
||||
|
||||
if (text) {
|
||||
const { id, index } = lineToRevert;
|
||||
|
||||
if (index > $lineData$.length - 1) {
|
||||
$lineData$.push({ id, text });
|
||||
} else if ($lineData$[index].id === id) {
|
||||
$lineData$[index] = { id, text };
|
||||
} else {
|
||||
$lineData$.splice(index, 0, { id, text });
|
||||
}
|
||||
}
|
||||
|
||||
lineToRevert = linesToRevert.pop();
|
||||
}
|
||||
|
||||
await tick();
|
||||
|
||||
$lineData$ = applyEqualLineStartMerge(applyMaxLinesAndGetRemainingLineData());
|
||||
$actionHistory$ = $actionHistory$;
|
||||
}
|
||||
|
||||
function removeLastLine() {
|
||||
if (!$lineData$.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [removedLine] = $lineData$.splice($lineData$.length - 1, 1);
|
||||
|
||||
selectedLineIds = selectedLineIds.filter((selectedLineId) => selectedLineId !== removedLine.id);
|
||||
$lineData$ = $lineData$;
|
||||
$actionHistory$ = [...$actionHistory$, [{ ...removedLine, index: $lineData$.length }]];
|
||||
|
||||
$uniqueLines$.delete(removedLine.text);
|
||||
}
|
||||
|
||||
function removeLines() {
|
||||
const linesToDelete = new Set(selectedLineIds);
|
||||
const newActionHistory: LineItem[] = [];
|
||||
|
||||
$lineData$ = $lineData$.filter((oldLine, index) => {
|
||||
const hasLine = linesToDelete.has(oldLine.id);
|
||||
|
||||
linesToDelete.delete(oldLine.id);
|
||||
|
||||
if (hasLine) {
|
||||
newActionHistory.push({ ...oldLine, index: index - newActionHistory.length });
|
||||
$uniqueLines$.delete(oldLine.text);
|
||||
}
|
||||
|
||||
return !hasLine;
|
||||
});
|
||||
|
||||
selectedLineIds = linesToDelete.size ? [...linesToDelete] : [];
|
||||
|
||||
if (newActionHistory.length) {
|
||||
$actionHistory$ = [...$actionHistory$, newActionHistory];
|
||||
}
|
||||
}
|
||||
|
||||
function deselectLines() {
|
||||
for (let index = 0, { length } = lineElements; index < length; index += 1) {
|
||||
lineElements[index]?.deselect();
|
||||
}
|
||||
|
||||
selectedLineIds = [];
|
||||
}
|
||||
|
||||
async function handlePipAction() {
|
||||
if (pipWindow) {
|
||||
return pipWindow.close();
|
||||
}
|
||||
|
||||
pipWindow = await window.documentPictureInPicture
|
||||
.requestWindow(
|
||||
$lastPipHeight$ > 0 && $lastPipWidth$ > 0
|
||||
? { height: $lastPipHeight$, width: $lastPipWidth$, preferInitialWindowPlacement: false }
|
||||
: { preferInitialWindowPlacement: false },
|
||||
)
|
||||
.catch(({ message }) => {
|
||||
$openDialog$ = {
|
||||
message: `Error opening floating window: ${message}`,
|
||||
showCancel: false,
|
||||
};
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (!pipWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
pipWindow.document.body.appendChild(pipContainer);
|
||||
|
||||
pipWindow.addEventListener('pagehide', onPipHide, { once: true });
|
||||
pipWindow.addEventListener('resize', onPipResize, false);
|
||||
pipWindow.addEventListener('blur', onPipFocusBlur, false);
|
||||
pipWindow.addEventListener('focus', onPipFocusBlur, false);
|
||||
|
||||
[...document.styleSheets].forEach((styleSheet) => {
|
||||
if (styleSheet.ownerNode instanceof Element && styleSheet.ownerNode.id === 'user-css') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join('');
|
||||
const style = document.createElement('style');
|
||||
|
||||
style.textContent = cssRules;
|
||||
pipWindow.document.head.appendChild(style);
|
||||
} catch (_error) {
|
||||
const link = document.createElement('link');
|
||||
|
||||
link.rel = 'stylesheet';
|
||||
link.type = styleSheet.type;
|
||||
link.media = styleSheet.media.toString();
|
||||
link.href = styleSheet.href;
|
||||
pipWindow.document.head.appendChild(link);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onPipHide() {
|
||||
updatePipDimensions();
|
||||
|
||||
pipWindow.removeEventListener('resize', onPipResize, false);
|
||||
pipWindow.removeEventListener('blur', onPipFocusBlur, false);
|
||||
pipWindow.removeEventListener('focus', onPipFocusBlur, false);
|
||||
|
||||
hasPipFocus = false;
|
||||
pipWindow = undefined;
|
||||
}
|
||||
|
||||
function onPipResize() {
|
||||
window.clearTimeout(pipResizeTimeout);
|
||||
|
||||
pipResizeTimeout = window.setTimeout(updatePipDimensions, 500);
|
||||
}
|
||||
|
||||
function updatePipDimensions() {
|
||||
if (!pipWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lastPipHeight$ = pipWindow.document.body.clientHeight;
|
||||
$lastPipWidth$ = pipWindow.document.body.clientWidth;
|
||||
}
|
||||
|
||||
function onPipFocusBlur(event: Event) {
|
||||
hasPipFocus = event.type === 'focus';
|
||||
}
|
||||
|
||||
function onAfkBlur({ detail: isAfk }: CustomEvent<boolean>) {
|
||||
applyAfkBlur(document, isAfk);
|
||||
|
||||
if (pipWindow) {
|
||||
applyAfkBlur(pipWindow.document, isAfk);
|
||||
}
|
||||
}
|
||||
|
||||
function executeUpdateScroll() {
|
||||
updateScroll(window, lineContainer, $reverseLineOrder$, $displayVertical$);
|
||||
|
||||
if (pipWindow) {
|
||||
updateScroll(pipWindow, pipContainer, $reverseLineOrder$, false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMissedLine() {
|
||||
clearTimeout($flashOnPauseTimeout$);
|
||||
|
||||
if ($theme$ === Theme.GARDEN) {
|
||||
settingsContainer.classList.add('bg-base-200');
|
||||
settingsContainer.classList.remove('bg-base-100');
|
||||
document.body.classList.add('bg-base-200');
|
||||
}
|
||||
|
||||
document.body.classList.add('animate-[pulse_0.5s_cubic-bezier(0.4,0,0.6,1)_1]');
|
||||
|
||||
$flashOnPauseTimeout$ = window.setTimeout(() => {
|
||||
if ($theme$ === Theme.GARDEN) {
|
||||
settingsContainer.classList.add('bg-base-100');
|
||||
settingsContainer.classList.remove('bg-base-200');
|
||||
document.body.classList.remove('bg-base-200');
|
||||
}
|
||||
|
||||
document.body.classList.remove('animate-[pulse_0.5s_cubic-bezier(0.4,0,0.6,1)_1]');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function transformLine(text: string, useReplacements = true) {
|
||||
const textToAppend = useReplacements ? applyReplacements(text, $enabledReplacements$) : text;
|
||||
|
||||
let canAppend = true;
|
||||
let lineToAppend = $removeAllWhitespace$ ? textToAppend.replace(/\s/gm, '').trim() : textToAppend;
|
||||
|
||||
if ($filterNonCJKLines$ && !lineToAppend.match(cjkCharacters)) {
|
||||
lineToAppend = '';
|
||||
}
|
||||
|
||||
if (!lineToAppend) {
|
||||
canAppend = false;
|
||||
} else if ($preventGlobalDuplicate$) {
|
||||
canAppend = !$uniqueLines$.has(lineToAppend);
|
||||
$uniqueLines$.add(lineToAppend);
|
||||
} else if ($preventLastDuplicate$ && $lineData$.length) {
|
||||
canAppend = $lineData$.slice(-$preventLastDuplicate$).every((line) => line.text !== lineToAppend);
|
||||
}
|
||||
|
||||
return canAppend ? lineToAppend : undefined;
|
||||
}
|
||||
|
||||
function handleLineEdit(event) {
|
||||
const { inEdit, data } = event.detail as LineItemEditEvent;
|
||||
|
||||
if (data && data.originalText !== data.newText) {
|
||||
const text = transformLine(data.newText);
|
||||
|
||||
$lineData$[data.lineIndex] = {
|
||||
id: data.line.id,
|
||||
text,
|
||||
};
|
||||
|
||||
if (text) {
|
||||
$actionHistory$ = [...$actionHistory$, [{ ...data.line, index: data.lineIndex }]];
|
||||
$uniqueLines$.delete(data.originalText);
|
||||
$uniqueLines$.add(text);
|
||||
} else {
|
||||
tick().then(
|
||||
() =>
|
||||
($lineData$[data.lineIndex] = {
|
||||
id: data.line.id,
|
||||
text: data.originalText,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
lineInEdit = inEdit;
|
||||
}
|
||||
|
||||
function applyMaxLinesAndGetRemainingLineData(diffMod = 0) {
|
||||
const oldLinesToRemove = new Set<string>();
|
||||
const startIndex = $maxLines$ ? $lineData$.length - $maxLines$ + diffMod : 0;
|
||||
const remainingLineData =
|
||||
startIndex > 0
|
||||
? $lineData$.filter((oldLine, index) => {
|
||||
if (index < startIndex) {
|
||||
oldLinesToRemove.add(oldLine.id);
|
||||
|
||||
$uniqueLines$.delete(oldLine.text);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
: $lineData$;
|
||||
|
||||
if (oldLinesToRemove.size) {
|
||||
selectedLineIds = selectedLineIds.filter((selectedLineId) => !oldLinesToRemove.has(selectedLineId));
|
||||
}
|
||||
|
||||
return remainingLineData;
|
||||
}
|
||||
|
||||
function updateLineData(executeUpdate: boolean) {
|
||||
if (!executeUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
$showSpinner$ = true;
|
||||
|
||||
try {
|
||||
for (let index = 0, { length } = $lineData$; index < length; index += 1) {
|
||||
const line = $lineData$[index];
|
||||
const newText = transformLine(line.text);
|
||||
|
||||
if (newText && newText !== line.text) {
|
||||
$uniqueLines$.delete(line.text);
|
||||
|
||||
$lineData$[index] = { ...line, text: newText };
|
||||
}
|
||||
}
|
||||
|
||||
$openDialog$ = {
|
||||
message: `Operation executed`,
|
||||
showCancel: false,
|
||||
};
|
||||
} catch ({ message }) {
|
||||
$openDialog$ = {
|
||||
type: 'error',
|
||||
message: `An Error occured: ${message}`,
|
||||
showCancel: false,
|
||||
};
|
||||
}
|
||||
|
||||
$lineData$ = applyEqualLineStartMerge(applyMaxLinesAndGetRemainingLineData());
|
||||
|
||||
$showSpinner$ = false;
|
||||
}
|
||||
|
||||
function applyEqualLineStartMerge(currentLineData: LineItem[]) {
|
||||
if (!$mergeEqualLineStarts$ || currentLineData.length < 2) {
|
||||
return currentLineData;
|
||||
}
|
||||
|
||||
const lastIndex = currentLineData.length - 1;
|
||||
const comparisonIndex = lastIndex - 1;
|
||||
const lastLine = currentLineData[lastIndex];
|
||||
const comparisonLine = currentLineData[comparisonIndex].text;
|
||||
|
||||
if (lastLine.text.startsWith(comparisonLine)) {
|
||||
$uniqueLines$.delete(comparisonLine);
|
||||
|
||||
selectedLineIds = selectedLineIds.filter(
|
||||
(selectedLineId) => selectedLineId !== currentLineData[comparisonIndex].id,
|
||||
);
|
||||
|
||||
currentLineData.splice(comparisonIndex, 2, lastLine);
|
||||
}
|
||||
|
||||
return currentLineData;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keyup={handleKeyPress} />
|
||||
|
||||
{$visibilityHandler$ ?? ''}
|
||||
{$handleLine$ ?? ''}
|
||||
{$pasteHandler$ ?? ''}
|
||||
{$copyBlocker$ ?? ''}
|
||||
{$resizeHandler$ ?? ''}
|
||||
|
||||
{#if $showSpinner$}
|
||||
<Spinner />
|
||||
{/if}
|
||||
|
||||
<DialogManager />
|
||||
|
||||
<header class="fixed top-0 right-0 flex justify-end items-center p-2 bg-base-100" bind:this={settingsContainer}>
|
||||
<Stats on:afkBlur={onAfkBlur} />
|
||||
{#if $websocketUrl$}
|
||||
<SocketConnector />
|
||||
{/if}
|
||||
{#if $secondaryWebsocketUrl$}
|
||||
<SocketConnector isPrimary={false} />
|
||||
{/if}
|
||||
{#if $isPaused$}
|
||||
<div
|
||||
role="button"
|
||||
title="Continue"
|
||||
class="mr-1 animate-[pulse_1.25s_cubic-bezier(0.4,0,0.6,1)_infinite] hover:text-primary sm:mr-2"
|
||||
>
|
||||
<Icon path={mdiPlay} width={iconSize} height={iconSize} on:click={() => ($isPaused$ = false)} />
|
||||
</div>
|
||||
{:else}
|
||||
<div role="button" title="Pause" class="mr-1 hover:text-primary sm:mr-2">
|
||||
<Icon path={mdiPause} width={iconSize} height={iconSize} on:click={() => ($isPaused$ = true)} />
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
role="button"
|
||||
title="Delete last Line"
|
||||
class="mr-1 hover:text-primary sm:mr-2"
|
||||
class:opacity-50={!$lineData$.length}
|
||||
class:cursor-not-allowed={!$lineData$.length}
|
||||
class:hover:text-primary={$lineData$.length}
|
||||
>
|
||||
<Icon path={mdiDeleteForever} width={iconSize} height={iconSize} on:click={removeLastLine} />
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
title="Undo last Action"
|
||||
class="mr-1 hover:text-primary sm:mr-2"
|
||||
class:opacity-50={!$actionHistory$.length}
|
||||
class:cursor-not-allowed={!$actionHistory$.length}
|
||||
class:hover:text-primary={$actionHistory$.length}
|
||||
>
|
||||
<Icon path={mdiArrowULeftTop} width={iconSize} height={iconSize} on:click={undoLastAction} />
|
||||
</div>
|
||||
{#if selectedLineIds.length}
|
||||
<div role="button" title="Remove selected Lines" class="mr-1 hover:text-primary sm:mr-2">
|
||||
<Icon path={mdiDelete} width={iconSize} height={iconSize} on:click={removeLines} />
|
||||
</div>
|
||||
<div role="button" title="Deselect Lines" class="mr-1 hover:text-primary sm:mr-2">
|
||||
<Icon path={mdiCancel} width={iconSize} height={iconSize} on:click={deselectLines} />
|
||||
</div>
|
||||
{/if}
|
||||
<div role="button" title="Open Notes" class="mr-1 hover:text-primary sm:mr-2">
|
||||
<Icon path={mdiNoteEdit} width={iconSize} height={iconSize} on:click={() => ($notesOpen$ = true)} />
|
||||
</div>
|
||||
{#if pipAvailable}
|
||||
<div
|
||||
role="button"
|
||||
class="mr-1 hover:text-primary sm:mr-2"
|
||||
title={pipWindow ? 'Close Floating Window' : 'Open Floating Window'}
|
||||
>
|
||||
<Icon
|
||||
width={iconSize}
|
||||
height={iconSize}
|
||||
path={pipWindow ? mdiWindowMaximize : mdiWindowRestore}
|
||||
on:click={handlePipAction}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Icon
|
||||
class="cursor-pointer mr-1 hover:text-primary md:mr-2"
|
||||
path={mdiCog}
|
||||
width={iconSize}
|
||||
height={iconSize}
|
||||
bind:element={settingsElement}
|
||||
on:click={() => (settingsOpen = !settingsOpen)}
|
||||
/>
|
||||
<Settings
|
||||
{settingsElement}
|
||||
{pipAvailable}
|
||||
bind:settingsOpen
|
||||
bind:selectedLineIds
|
||||
bind:this={settingsComponent}
|
||||
on:applyReplacements={() => updateLineData(!!$enabledReplacements$.length)}
|
||||
on:layoutChange={executeUpdateScroll}
|
||||
on:maxLinesChange={() => ($lineData$ = applyMaxLinesAndGetRemainingLineData())}
|
||||
/>
|
||||
<Presets isQuickSwitch={true} on:layoutChange={executeUpdateScroll} />
|
||||
</header>
|
||||
<main
|
||||
class="flex flex-col flex-1 break-all px-4 w-full h-full overflow-auto"
|
||||
class:py-16={!$displayVertical$}
|
||||
class:py-8={$displayVertical$}
|
||||
class:opacity-50={$notesOpen$}
|
||||
class:flex-col-reverse={$reverseLineOrder$}
|
||||
style:font-size={`${$fontSize$}px`}
|
||||
style:font-family={$onlineFont$ !== OnlineFont.OFF ? $onlineFont$ : undefined}
|
||||
style:writing-mode={$displayVertical$ ? 'vertical-rl' : 'horizontal-tb'}
|
||||
bind:this={lineContainer}
|
||||
>
|
||||
{@html newLineCharacter}
|
||||
{#each $lineData$ as line, index (line.id)}
|
||||
<Line
|
||||
{line}
|
||||
{index}
|
||||
isLast={$lineData$.length - 1 === index}
|
||||
bind:this={lineElements[index]}
|
||||
on:selected={({ detail }) => {
|
||||
selectedLineIds = [...selectedLineIds, detail];
|
||||
}}
|
||||
on:deselected={({ detail }) => {
|
||||
selectedLineIds = selectedLineIds.filter((selectedLineId) => selectedLineId !== detail);
|
||||
}}
|
||||
on:edit={handleLineEdit}
|
||||
/>
|
||||
{/each}
|
||||
</main>
|
||||
{#if $notesOpen$}
|
||||
<div
|
||||
class="bg-base-200 fixed top-0 right-0 z-[60] flex h-full w-full max-w-3xl flex-col justify-between"
|
||||
in:fly|local={{ x: 100, duration: 100, easing: quintInOut }}
|
||||
>
|
||||
<Notes />
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
id="pip-container"
|
||||
class="flex flex-col flex-1 flex flex-col break-all px-4 w-full h-full overflow-auto"
|
||||
class:flex-col-reverse={$reverseLineOrder$}
|
||||
class:hidden={!pipWindow}
|
||||
style:font-size={`${$fontSize$}px`}
|
||||
style:font-family={$onlineFont$ !== OnlineFont.OFF ? $onlineFont$ : undefined}
|
||||
bind:this={pipContainer}
|
||||
>
|
||||
{#if pipWindow}
|
||||
{#each pipLines as line, index (line.id)}
|
||||
<Line {line} {index} {pipWindow} isLast={pipLines.length - 1 === index} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user