Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ strong {
@apply text-xl;
}

.tiptap-minirenderer {
@apply h-full w-full p-2;
@apply focus:outline-none;
font-family: Minecraft;
@apply text-xl;
}

.tiptap p.is-editor-empty:first-child::before {
color: #7f7f7f;
content: attr(data-placeholder);
Expand Down Expand Up @@ -92,6 +99,11 @@ strong {
filter: blur(3px);
}

.export-button {
@apply items-center p-1 px-1.5 bg-zinc-900 text-zinc-200 hover:text-white rounded-md font-lexend text-xs h-6;
@apply w-0 invisible sm:w-fit sm:visible; /* hide on mobile */
}

/* Useful */

.nomob {
Expand Down
115 changes: 115 additions & 0 deletions src/lib/components/modals/ExportSelectionModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<script lang="ts">
import Modal from "$lib/components/Modal.svelte";
import { Editor } from "@tiptap/core";
import Key from "../Key.svelte";
import MiniRenderer from "../text/MiniRenderer.svelte";
import { appSettings } from "$lib/settings";
import { convert } from "$lib/text/nbt/export";
import { json, typescript } from "svelte-highlight/languages";
import Highlight from "svelte-highlight";

import IconCopy from "~icons/tabler/copy";
import IconCheck from "~icons/tabler/check";
import CheckBox from "../CheckBox.svelte";

let recentlyCopied: boolean = $state(false)
let jsonOutput: boolean = $state(false)

let {
exportSelectionDialog = $bindable(),
editor,
shouldOptimise = true
}: {
exportSelectionDialog: Modal,
editor: Editor,
shouldOptimise: boolean
} = $props();

let value: object = $state({})

function onOpen() {
let content = editor.state.doc.slice(editor.state.selection.from, editor.state.selection.to).content.toJSON()

// Fix if only one line
if (content[0].type !== "paragraph") {
content = [{
type: "paragraph",
content: content
}]
}

// If last paragraph is empty, remove
if (content.length > 0 && !("content" in content[content.length - 1])) {
content.pop();
}

// Format in document
value = {
type: "doc",
content: content
};
}
</script>

<Modal title="Export selection" bind:this={exportSelectionDialog} key="Q" onOpen={onOpen}>
Comment thread
Silabear marked this conversation as resolved.
<div class="flex w-full flex-col space-y-1">
{#key value}
<p class="w-full">Selected text:</p>
<div class="bg-zinc-900 rounded-md px-1">
<MiniRenderer value={value} />
</div>

<p class="w-full mt-2">Output:</p>
<code class="max-h-56 w-full space-x-3 overflow-auto rounded-lg bg-zinc-950 p-3">
{#if $appSettings.syntaxHighlight}
<Highlight
language={jsonOutput ? json : typescript}
code={
convert(
value,
shouldOptimise,
"standard",
jsonOutput,
)
} />
{:else}
<pre class="inline break-all whitespace-pre-wrap">
{editor ? convert(
value,
shouldOptimise,
"standard",
jsonOutput
) : "Loading..."}
</pre>
{/if}
</code>

<div class="flex items-center space-x-2 mb-2 mt-1">
<CheckBox label="bg" bind:value={jsonOutput} />
<label for="bg">JSON output</label>
</div>

<button
class="btn flex items-center space-x-2"
onclick={() => {
navigator.clipboard.writeText(
convert(
value,
shouldOptimise
)
);
recentlyCopied = true;
setTimeout(() => (recentlyCopied = false), 2000);
}}>
{#if !recentlyCopied}
<IconCopy />
<span>Copy</span>
{:else}
<IconCheck />
<span>Copied!</span>
{/if}
</button>

{/key}
</div>
</Modal>
2 changes: 1 addition & 1 deletion src/lib/components/modals/topbar/ExportModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { Highlight } from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";
import { appSettings } from "$lib/settings";
import { json } from "svelte-highlight/languages";

// Icons
import IconItem from "~icons/tabler/swords";
Expand All @@ -17,7 +18,6 @@
import IconCopy from "~icons/tabler/copy";
import IconCheck from "~icons/tabler/check";
import IconDownload from "~icons/tabler/download";
import { json } from "svelte-highlight/languages";

// UI
let showOutput: string | null = $state(null);
Expand Down
9 changes: 9 additions & 0 deletions src/lib/components/modals/topbar/SettingsModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@
</label>
</div>

<div class="flex items-center space-x-2">
<CheckBox label="realisticLineHeight" bind:value={$appSettings.hideSelectionExport} />
<label for="hideSelectionExport" class="flex flex-col">
<span>Hide "Export this" button</span>
<span class="text-xs text-zinc-500"
>If enabled, the selection export feature ("export this" button) will be disabled</span>
</label>
</div>

<div class="flex items-center space-x-2">
<select
name="fontSize"
Expand Down
5 changes: 5 additions & 0 deletions src/lib/components/text/MiniRenderer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
FontsExtension,
],
content: value,
editorProps: {
attributes: {
class: 'tiptap-minirenderer',
},
},
}).setEditable(false);

appSettings.subscribe(() => {
Expand Down
35 changes: 12 additions & 23 deletions src/lib/components/toolbar/Toolbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,16 @@
Icon={IconGradient}
onClick={gradientDialog.open}
ariaLabel="Color Gradient" />
<div id="colorBtns">
<div id="colorBtns" class="flex items-center space-x-0 mx-1">
{#each colourMap as colour}
<ToolbarButton
Icon={IconSquare}
onClick={() => editor!.chain().focus().setColor(colour.value).run()}
styleVar={editor.isActive("textStyle", { color: colour.value })}
colour={colour.value}
ariaLabel={toTitleCase(colour.name.replace("_", " "))} />
<button
aria-label={toTitleCase(colour.name.replace("_", " "))}
onclick={() => editor!.chain().focus().setColor(colour.value).run()}
{@attach tooltip}
style="color: {colour.value || 'inherit'}"
class="rounded-md py-1 px-0 text-lg font-medium hover:bg-white/3 {editor.isActive("textStyle", { color: colour.value }) ? ' bg-zinc-800' : ''}">
<IconSquare />
</button>
{/each}
</div>
{#if editor.getAttributes("textStyle").color}
Expand Down Expand Up @@ -260,22 +262,9 @@
<ToolbarButton onClick={() => editor?.commands.redo()} ariaLabel="Redo" Icon={IconRedo} />

<div class="grow"></div>

<button
{@attach tooltip}
class="toolbar-btn nomob"
onclick={insertImageDialog?.open}
aria-label="Insert Image"><IconUploadImage /></button>
<button
{@attach tooltip}
class="toolbar-btn nomob"
onclick={fontUploadModal?.open}
aria-label="Upload Font"><IconUploadFont /></button>
<button
{@attach tooltip}
class="toolbar-btn nomob"
onclick={keybindDialog?.open}
aria-label="Keybinds"><IconKeybinds /></button>

<ToolbarButton onClick={insertImageDialog?.open} ariaLabel="Insert Image" Icon={IconUploadImage} desktopOnly />
<ToolbarButton onClick={keybindDialog?.open} ariaLabel="Keybinds" Icon={IconKeybinds} desktopOnly />
{/if}
</div>

Expand Down
5 changes: 3 additions & 2 deletions src/lib/components/toolbar/ToolbarButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@
ariaLabel: string;
Icon: Component;
colour?: string;
desktopOnly?: boolean
};

const { onClick, styleVar, Icon, ariaLabel, colour }: Props = $props();
const { onClick, styleVar, Icon, ariaLabel, colour, desktopOnly }: Props = $props();
</script>

<button
aria-label={ariaLabel}
onclick={onClick}
{@attach tooltip}
style="color: {colour || 'inherit'}"
class="toolbar-btn{styleVar ? ' bg-zinc-800' : ''}">
class="toolbar-btn{styleVar ? ' bg-zinc-800' : ''} {desktopOnly ? 'nomob' : ''}">
<Icon />
</button>
2 changes: 2 additions & 0 deletions src/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type Settings = {
showCharacterCount: boolean; // whether the character count is shown
syntaxHighlight: boolean; // whether to show highlighted output text
realisticLineHeight: boolean; // whether to use smaller line height
hideSelectionExport: boolean; // whether to hide the export this button
fontSize: number; // 0 = small, 1 = default, 2 = large
};

Expand Down Expand Up @@ -32,5 +33,6 @@ export const appSettings: Writable<Settings> = createPersistentStore("settings",
showCharacterCount: true,
syntaxHighlight: true,
realisticLineHeight: false,
hideSelectionExport: true,
fontSize: 1,
});
79 changes: 79 additions & 0 deletions src/lib/tiptap/extensions/ExportButton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Extension } from '@tiptap/core';
import { Plugin, PluginKey } from '@tiptap/pm/state';
import { EditorView } from '@tiptap/pm/view';
import { appSettings } from "$lib/settings";
import { get } from 'svelte/store';

// Define the shape of configuration options for type safety
export interface ExportButtonOptions {
onClick: () => void;
}

export const ExportButtonExtension = Extension.create<ExportButtonOptions>({
name: 'ExportButton',

addOptions() {
return {
onClick: () => { },
};
},

addProseMirrorPlugins() {
const { onClick } = this.options;
let buttonDom: HTMLButtonElement | null = null;

return [
new Plugin({
key: new PluginKey('ExportButton'),
view(editorView: EditorView) {
return {
update(view: EditorView) {
if (get(appSettings).hideSelectionExport == true) {
return;
}

const { state } = view;
const { selection } = state;

const parentNode = view.dom.parentNode as HTMLElement | null;
if (!parentNode) return;

if ((selection.empty || !view.hasFocus()) && buttonDom) {
buttonDom.style.display = 'none';
return;
}

if (!buttonDom) {
buttonDom = document.createElement('button');
buttonDom.innerText = '↪ Export this';
buttonDom.className = 'export-button';
buttonDom.style.position = 'absolute';
buttonDom.style.zIndex = '10';

buttonDom.addEventListener('mousedown', (e: MouseEvent) => e.preventDefault());
buttonDom.addEventListener('click', onClick);

parentNode.appendChild(buttonDom);
}

const coords = view.coordsAtPos(selection.to);
const top = coords.top + 19;
const left = coords.left - 90;

buttonDom.style.display = 'block';
buttonDom.style.top = `${top}px`;
buttonDom.style.left = `${left + 5}px`;
},

destroy() {
if (buttonDom) {
buttonDom.remove();
buttonDom = null;
}
}
};
},
}),
];
},
});
Loading