mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
748 lines
20 KiB
Svelte
748 lines
20 KiB
Svelte
<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>
|