Files
AstralRinth/apps/frontend/src/components/ui/servers/MOTDEditor.vue
Elizabeth 185dd47668 Pyro Integration (#2503)
* fix

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor(fileitem): optimize

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(fileitem): fixed width timestamp

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(fileitem): allow editing json5/jsonc

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: motd pt 1, auto backups scaffolding, editing navbar changes

* feat: fancy sidebar animations

* fix: files

* fix: files pt2

* fix: faulty name validation disallowing spaces in file names

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor: fileitem props

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: upload files not refreshing files list

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(imgviewer): handle invalid/empty images

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: return of the sticky files header

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: prevent servericon from shrinking

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: wtf were we thinking with this anyway

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: further mobile optimization

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: propagate margin

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: truncation fixes

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: track navbar with sentinel

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(files): a11y

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: improve inspector styles

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: console preformance improvements, decrease blur

* feat(mobile): new server header

* fix: linting

* fix: useless z indeces

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust file filter names

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(files): true breadcrumbs

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(marketing): make custom responsive

* fix(marketing): mobile file manager card

* feat: trackable navtabs

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: oh no

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: smartly truncate

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminal): z-indexes

* fix: autofocus more inputs

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: color

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust copy

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: backup modal usability improvements

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: padding

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: title

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(content): update banner mobile support

* fix: server listing icons

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: ignore clicks in server listing for labels

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(mobile): backup card

* fix(backups): make plural conditional

* fix: debounce file item selectitem

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: lint

Signed-off-by: Evan Song <theevansong@gmail.com>

* stuff

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: temp sidebar fix until i can be smart

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: explictly set button type in file modals

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: properly sort backups

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: add getautobackup method to pyroservers

Signed-off-by: Evan Song <theevansong@gmail.com>

* choer: update autobackup params

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: update autobackup methods (REALLY GUYS)

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: implement autobackups

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: implement backup-while-running preference

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: make server labels a component

* feat: implement 'All details' modal

* fix(mobile): server manage page

* feat(files): mobile compatible

* fix(info labels): wrap

* chore(inspector): clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(backup settings): swap + and -

* fix(manage): new -> plans instead of modal

* feat: more small mobile fixes

* fix(auto backup modal): manual input validation

* fix(file browse navbar): home margin

* feat(purchase modal): mobile support

* fix(marketing): faded line alignments

* feat: add servers to mobile nav

* feat(network): dns record fixes

* feat: make all settings work on mobile

* fix(loader settings): modpack mobile

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(marketing): add 'Manage your servers' button

* fix(marketing): only check servers if logged in

* fix(network): allocation edit & delete button

* fix(backups): use UiServersTeleportOverflowMenu

* chore: linting

* chore: but here comes the sentence case

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(marketing): make buttons consistent

* lint

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(loader): prevent multiline version names in dropdown

Signed-off-by: Evan Song <theevansong@gmail.com>

* lint

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: copy

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: sentence case

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: linting

* chore: rename dumbass preference key

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor: rewrite power action buttons

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: robust download logic

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(loader mobile): modpack dropdown width

* fix: sentence case

* fix(save & 'working on it'): look good on mobile

* fix(TeleportDropdown): width

* fix(inspecting error): mobile

* fix: show action button dropdown when installing

* fix(navtabs): temp fix for mobile scrolling issue

* fix(install error): mobile compatible

* chore: just remove tracking

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: cleanup

* fix: broken svg clr in checkbox when using experimental styles

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust vanilla icon

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust loader props

Signed-off-by: Evan Song <theevansong@gmail.com>

* revert changes to serversidebar

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: server properties flicker

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(backups): plural

* fix: cases where the telepoverflow would clash with viewport edge

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(backups): auto-backups label

* fix(network): titlecase

* feat(fileitem): new rename icon

* fix(properties): wiki proper noun

* fix: disable motd for the time being

* chore: adjust wording for power conifmration

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: "external" to billing

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: icon

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: add EULA checkbox

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* me and bro deciding which case rules to enforce

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(sftp): copy address & username, launch tooltip

* feat(files): better move

* chore: attempt to mitigate excessive stack depth type

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(loader): prevent versions 1.2.4 and below

* feat(dns table): placeholder improvements

* feat(pyroServer): error handling

* fix: intrinsic size on loader icon

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust wording

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: sentence case

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust wording

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: types

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: "implemented" key in preference

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(connection lost): redesign

* feat(connection error): make icon orange

* fix: cleanup

* chore(connection lost): redesign pt 2

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: OOOOHHH MY GOD

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: implement capacity api on marketing

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: update createdat backup type

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor: all of backups

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: update backup types

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor: backups pt 2

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: comically small icons

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: align designs

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: hide ram graph if ram as bytes enabled

Signed-off-by: Evan Song <theevansong@gmail.com>

* base add content page

* Fix conflict

* feat(content): mobile-compatible header, sticky

* fix(marketing): md instead of sm for custom

* fix: compiler macro warning

Signed-off-by: Evan Song <theevansong@gmail.com>

* again

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: loader type error

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: default uptime seconds prop

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: hydration errors on server listing

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: move custom URL to general

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: indiviudally checkj capacities

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: falsey

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: missing prop

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: Derive On That Thang

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust gap

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: add default name for backups

* fix: the backup number should PROBABLY be computed lol

* fix(backups): truncate text, mobile fixes

* fix(loader): modpack mobile fix

* feat(plans): add vcpus

* fix(backup modal): blank by default, maxlength

* fix(subdomain): separate length & valid chars

* feat: mrpack installs functionality (untested), forbidden handling, backups grammar

* feat(content): make responsive on mobile

* fix: disable plan buttons separately

* fix(backup modal): update name max length

* fix(purchase): wrapping on eula, eula link

* fix: move skeleton

* fix(server mobile header): truncation

* fix(server header): proper alignment

* Finish content page fixes

* fix: who up rinthing

Signed-off-by: Evan Song <theevansong@gmail.com>

* wip

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(staging & email banner): z-index

* feat: make eula tickbox more visible

* fix: move "powered by pyro" below buttons on hero

* fix: oops sorry ellie, also updated the main screenshot

* feat: update content screenshot

* fix: content page card should hide image on lg

* feat: hide total storage for now

* fix: terminal card now uses terminal icon

* fix(marketing): make medium plan card border solid

* feat: modloader card, move pyro BACK below buttons, beta release pill

* fix: spinning logo should be behind hero

* feat: surgically remove the hero's massive forehead

* feat(marketing): mobile UI screenshot

* fix(hero): z-index goes over mobile nav

* fix: consistent borders, files breakpoints

* chore: update turbo

* chore: adjust hero sizing

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: mention region restrictions

* chore: double check if we are at capcity

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: measure twice cut once

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: bro cut twice and measured once 💀

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(marketing): login first

* fix: out of capacity text when logged out

* fix(slider): reset some values for frontend

* feat: wip hero section

Signed-off-by: Evan Song <theevansong@gmail.com>

* New navigation to support the new products (#2879)

* Nav

* oops extra file

* feat: mrpack uploading with existing modpack, fix: choose modpack duplicate

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: update features section

Signed-off-by: Evan Song <theevansong@gmail.com>

* Nav adjustments

* fix: server manager empty state clashing with loading state

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: query param hard

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: do not count uptime if crashed

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: grammar

Signed-off-by: Evan Song <theevansong@gmail.com>

* hide hero img on lg breakpoints

* Make plugins a plug

* chore: prep for buffered text selection terminal

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: marketing responsive stuff, n fixes

* fix hoverable prop

* fix: edit mod spacing

* fix: type error for display name in dropdown

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: custom plans

* fix: no more console.log

* fix: properly linked prop label

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(install hero mobile): padding

* fix: prevent x overflow on servers page

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix lint oh ym fucking god yal

Signed-off-by: Evan Song <theevansong@gmail.com>

* Migrate modpack install to search

* fix(custom plan): warning icon variable

* fix: loading probally and modal loader things

* fix(marketing): login icon colours

* fix(marketing): responsiveness

* fix(marketing): responsiveness v2

* fix: sync button for icon tm

* fix(marketing): responsiveness v3

* fix: hero image

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: switch to cdn links

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: switch to cdn links

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: switch to cdn links

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: switch to cdn links

Signed-off-by: Evan Song <theevansong@gmail.com>

* Remove prod override

---------

Signed-off-by: Evan Song <theevansong@gmail.com>
Co-authored-by: Evan Song <theevansong@gmail.com>
Co-authored-by: TheWander02 <48934424+thewander02@users.noreply.github.com>
Co-authored-by: he3als <65787561+he3als@users.noreply.github.com>
Co-authored-by: Evan Song <52982404+ferothefox@users.noreply.github.com>
Co-authored-by: Lio <git@lio.cat>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: not-nullptr <needhelpwithrift@gmail.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: Prospector <prospectordev@gmail.com>
Co-authored-by: sticks <tanner@teamhydra.dev>
2024-11-02 21:14:00 -07:00

661 lines
21 KiB
Vue

<template>
<div class="flex items-center justify-center">
<div class="w-full overflow-hidden">
<div class="mb-4">
<div
v-for="(line, lineIndex) in motd"
:key="lineIndex"
class="relative mb-2 rounded bg-button-bg p-2"
>
<div
class="font-minecraft text-white"
:contenteditable="true"
spellcheck="false"
@input="handleInput($event, lineIndex)"
@keydown.enter.prevent
@paste.prevent="handlePaste($event, lineIndex)"
@mouseup="handleSelection(lineIndex)"
v-html="renderLine(line)"
></div>
<div class="text-sm text-gray-400">
{{ motd[lineIndex].reduce((sum, segment) => sum + segment.text.length, 0) }}/45
characters
</div>
</div>
</div>
</div>
<transition name="fade">
<div
v-if="showPopup"
:style="{ top: `${popupY}px`, left: `${popupX}px` }"
class="fixed z-10 flex flex-col items-end gap-2 transition-all duration-300 ease-in-out"
>
<div class="rounded-xl border bg-table-alternateRow p-2 shadow-lg">
<div class="flex space-x-2">
<Button
v-for="style in styles"
:key="style.name"
icon-only
transparent
@click="applyStyle({ [style.name]: !currentStyle[style.name] })"
>
<component :is="style.icon" class="h-4 w-4" />
</Button>
<div class="relative overflow-y-scroll">
<Button icon-only transparent :class="colorPicker ?? 'hidden'" @click="pickColor">
<PaintBrushIcon />
</Button>
</div>
</div>
</div>
<div
v-if="colorPicker"
icon-only
class="w-fit overflow-y-auto rounded-xl p-2 [&&]:bg-table-alternateRow"
>
<div :class="colorPicker ? `grid grid-flow-col grid-rows-4 gap-2` : '[&&]:hidden'">
<button
v-for="format in sortedFormatCodes()"
:key="format.code"
class="rounded-full p-3"
:style="{ backgroundColor: format.color }"
:title="format.description"
@click="applyStyle({ color: format.color })"
></button>
</div>
</div>
</div>
</transition>
</div>
</template>
<script setup>
import {
ItalicIcon,
BoldIcon,
StrikethroughIcon,
UnderlineIcon,
PaintBrushIcon,
ChevronLeftIcon,
} from "@modrinth/assets";
import { Button } from "@modrinth/ui";
const props = defineProps({
server: {
type: Object,
required: true,
},
});
const formatCodes = [
{ code: "§f", color: "white", description: "White" },
{ code: "§7", color: "#AAAAAA", description: "Gray" },
{ code: "§8", color: "#555555", description: "Dark Gray" },
{ code: "§0", color: "#000000", description: "Black" },
{ code: "§9", color: "#5555FF", description: "Blue" },
{ code: "§1", color: "#0000AA", description: "Dark Blue" },
{ code: "§b", color: "#55FFFF", description: "Aqua" },
{ code: "§3", color: "#00AAAA", description: "Dark Aqua" },
{ code: "§a", color: "#55FF55", description: "Green" },
{ code: "§2", color: "#00AA00", description: "Dark Green" },
{ code: "§e", color: "#FFFF55", description: "Yellow" },
{ code: "§6", color: "#FFAA00", description: "Gold" },
{ code: "§c", color: "#FF5555", description: "Red" },
{ code: "§4", color: "#AA0000", description: "Dark Red" },
{ code: "§d", color: "#FF55FF", description: "Light Purple" },
{ code: "§5", color: "#AA00AA", description: "Dark Purple" },
];
const sortedFormatCodes = () => {
const colors = formatCodes;
if (colors[0].description === "White") {
colors.reverse();
}
return colors;
};
const minecraftEmojis = [
{ char: "☺", name: "SMILING FACE" },
{ char: "☹", name: "FROWNING FACE" },
{ char: "☠", name: "SKULL AND CROSSBONES" },
{ char: "❣", name: "HEART EXCLAMATION" },
{ char: "❤", name: "RED HEART" },
{ char: "✌", name: "VICTORY HAND" },
{ char: "☝", name: "INDEX POINTING UP" },
{ char: "✍", name: "WRITING HAND" },
{ char: "♨", name: "HOT SPRINGS" },
{ char: "✈", name: "AIRPLANE" },
{ char: "⌛", name: "HOURGLASS DONE" },
{ char: "⌚", name: "WATCH" },
{ char: "☀", name: "SUN" },
{ char: "☁", name: "CLOUD" },
{ char: "☂", name: "UMBRELLA" },
{ char: "❄", name: "SNOWFLAKE" },
{ char: "☃", name: "SNOWMAN" },
{ char: "☄", name: "COMET" },
{ char: "♠", name: "SPADE SUIT" },
{ char: "♥", name: "HEART SUIT" },
{ char: "♦", name: "DIAMOND SUIT" },
{ char: "♣", name: "CLUB SUIT" },
{ char: "♟", name: "CHESS PAWN" },
{ char: "☎", name: "TELEPHONE" },
{ char: "⌨", name: "KEYBOARD" },
{ char: "✉", name: "ENVELOPE" },
{ char: "✏", name: "PENCIL" },
{ char: "✒", name: "BLACK PEN" },
{ char: "✂", name: "SCISSORS" },
{ char: "☢", name: "RADIOACTIVE" },
{ char: "☣", name: "BIOHAZARD" },
{ char: "⬆", name: "UP ARROW" },
{ char: "⬇", name: "DOWN ARROW" },
{ char: "➡", name: "RIGHT ARROW" },
{ char: "⬅", name: "LEFT ARROW" },
{ char: "↗", name: "UP-RIGHT ARROW" },
{ char: "↘", name: "DOWN-RIGHT ARROW" },
{ char: "↙", name: "DOWN-LEFT ARROW" },
{ char: "↖", name: "UP-LEFT ARROW" },
{ char: "↕", name: "UP-DOWN ARROW" },
{ char: "↔", name: "LEFT-RIGHT ARROW" },
{ char: "↩", name: "RIGHT ARROW CURVING LEFT" },
{ char: "↪", name: "LEFT ARROW CURVING RIGHT" },
{ char: "✡", name: "STAR OF DAVID" },
{ char: "☸", name: "WHEEL OF DHARMA" },
{ char: "☯", name: "YIN YANG" },
{ char: "✝", name: "LATIN CROSS" },
{ char: "☦", name: "ORTHODOX CROSS" },
{ char: "☪", name: "STAR AND CRESCENT" },
{ char: "☮", name: "PEACE SYMBOL" },
{ char: "♈", name: "ARIES" },
{ char: "♉", name: "TAURUS" },
{ char: "♊", name: "GEMINI" },
{ char: "♋", name: "CANCER" },
{ char: "♌", name: "LEO" },
{ char: "♍", name: "VIRGO" },
{ char: "♎", name: "LIBRA" },
{ char: "♏", name: "SCORPIO" },
{ char: "♐", name: "SAGITTARIUS" },
{ char: "♑", name: "CAPRICORN" },
{ char: "♒", name: "AQUARIUS" },
{ char: "♓", name: "PISCES" },
{ char: "▶", name: "PLAY BUTTON" },
{ char: "◀", name: "REVERSE BUTTON" },
{ char: "♀", name: "FEMALE SIGN" },
{ char: "♂", name: "MALE SIGN" },
{ char: "✖", name: "MULTIPLY" },
{ char: "‼", name: "DOUBLE EXCLAMATION MARK" },
{ char: "〰", name: "WAVY DASH" },
{ char: "☑", name: "CHECK BOX WITH CHECK" },
{ char: "✔", name: "CHECK MARK" },
{ char: "✳", name: "EIGHT-SPOKED ASTERISK" },
{ char: "✴", name: "EIGHT-POINTED STAR" },
{ char: "❇", name: "SPARKLE" },
{ char: "©", name: "COPYRIGHT" },
{ char: "®", name: "REGISTERED" },
{ char: "™", name: "TRADE MARK" },
{ char: "Ⓜ", name: "CIRCLED M" },
{ char: "㊗", name: 'JAPANESE "CONGRATULATIONS" BUTTON' },
{ char: "㊙", name: 'JAPANESE "SECRET" BUTTON' },
{ char: "▪", name: "BLACK SMALL SQUARE" },
{ char: "▫", name: "WHITE SMALL SQUARE" },
{ char: "☷", name: "TRIGRAM FOR EARTH" },
{ char: "☵", name: "TRIGRAM FOR WATER" },
{ char: "☶", name: "TRIGRAM FOR MOUNTAIN" },
{ char: "☋", name: "DESCENDING NODE" },
{ char: "☌", name: "CONJUNCTION" },
{ char: "♜", name: "BLACK CHESS ROOK" },
{ char: "♕", name: "WHITE CHESS QUEEN" },
{ char: "♡", name: "WHITE HEART SUIT" },
{ char: "♬", name: "BEAMED SIXTEENTH NOTES" },
{ char: "☚", name: "BLACK LEFT POINTING INDEX" },
{ char: "♮", name: "MUSIC NATURAL SIGN" },
{ char: "♝", name: "BLACK CHESS BISHOP" },
{ char: "♯", name: "SHARP" },
{ char: "☴", name: "TRIGRAM FOR WIND" },
{ char: "♭", name: "FLAT" },
{ char: "☓", name: "SALTIRE" },
{ char: "☛", name: "BLACK RIGHT POINTING INDEX" },
{ char: "☭", name: "HAMMER AND SICKLE" },
{ char: "♢", name: "WHITE DIAMOND SUIT" },
{ char: "✐", name: "UPPER RIGHT PENCIL" },
{ char: "♖", name: "WHITE CHESS ROOK" },
{ char: "☈", name: "THUNDERSTORM" },
{ char: "☒", name: "BALLOT BOX WITH X" },
{ char: "★", name: "BLACK STAR" },
{ char: "♚", name: "BLACK CHESS KING" },
{ char: "♛", name: "BLACK CHESS QUEEN" },
{ char: "✎", name: "LOWER RIGHT PENCIL" },
{ char: "♪", name: "EIGHTH NOTE" },
{ char: "☰", name: "TRIGRAM FOR HEAVEN" },
{ char: "☽", name: "FIRST QUARTER MOON" },
{ char: "☡", name: "CAUTION SIGN" },
{ char: "☼", name: "WHITE SUN WITH RAYS" },
{ char: "♅", name: "URANUS" },
{ char: "☐", name: "BALLOT BOX" },
{ char: "☟", name: "WHITE DOWN POINTING INDEX" },
{ char: "❦", name: "FLORAL HEART" },
{ char: "☊", name: "ASCENDING NODE" },
{ char: "☍", name: "OPPOSITION" },
{ char: "☬", name: "ADI SHAKTI" },
{ char: "♧", name: "WHITE CLUB SUIT" },
{ char: "☫", name: "FARSI SYMBOL" },
{ char: "☱", name: "TRIGRAM FOR LAKE" },
{ char: "☾", name: "LAST QUARTER MOON" },
{ char: "☤", name: "CADUCEUS" },
{ char: "❧", name: "ROTATED FLORAL HEART BULLET" },
{ char: "♄", name: "SATURN" },
{ char: "♁", name: "EARTH" },
{ char: "♔", name: "WHITE CHESS KING" },
{ char: "❥", name: "ROTATED HEAVY BLACK HEART BULLET" },
{ char: "☥", name: "ANKH" },
{ char: "☻", name: "BLACK SMILING FACE" },
{ char: "♤", name: "WHITE SPADE SUIT" },
{ char: "♞", name: "BLACK CHESS KNIGHT" },
{ char: "♆", name: "NEPTUNE" },
{ char: "#", name: "HASH SIGN" },
{ char: "♃", name: "JUPITER" },
{ char: "♩", name: "QUARTER NOTE" },
{ char: "☇", name: "LIGHTNING" },
{ char: "☞", name: "WHITE RIGHT POINTING INDEX" },
{ char: "♫", name: "BEAMED EIGHTH NOTES" },
{ char: "☏", name: "WHITE TELEPHONE" },
{ char: "♘", name: "WHITE CHESS KNIGHT" },
{ char: "☧", name: "CHI RHO" },
{ char: "☉", name: "SUN" },
{ char: "♇", name: "PLUTO" },
{ char: "☩", name: "CROSS OF JERUSALEM" },
{ char: "♙", name: "WHITE CHESS PAWN" },
{ char: "☜", name: "WHITE LEFT POINTING INDEX" },
{ char: "☲", name: "TRIGRAM FOR FIRE" },
{ char: "☨", name: "CROSS OF LORRAINE" },
{ char: "♗", name: "WHITE CHESS BISHOP" },
{ char: "☳", name: "TRIGRAM FOR THUNDER" },
{ char: "⚔", name: "CROSSED SWORDS" },
{ char: "⚀", name: "DICE ONE" },
];
const rawMotd = ref(props.server.general?.motd ?? "");
const motd = computed(() => {
const lines = rawMotd.value.split("\n");
return lines.map((line) => {
const segments = [];
let currentSegment = { text: "", color: "White" };
let i = 0;
while (i < line.length) {
if (line[i] === "§") {
if (currentSegment.text) {
segments.push({ ...currentSegment });
currentSegment = { text: "", color: "White" };
}
const formatCode = line.substr(i, 2);
const format = formatCodes.find((f) => f.code === formatCode);
console.log(format);
console.log(formatCode);
if (format) {
currentSegment.color = format.color;
i += 2;
continue;
} else if (formatCode === "§l") {
currentSegment.bold = true;
i += 2;
continue;
} else if (formatCode === "§o") {
currentSegment.italic = true;
i += 2;
continue;
} else if (formatCode === "§n") {
currentSegment.underline = true;
i += 2;
continue;
} else if (formatCode === "§m") {
currentSegment.strikethrough = true;
i += 2;
continue;
}
}
currentSegment.text += line[i];
i++;
}
if (currentSegment.text) {
segments.push(currentSegment);
}
return segments;
});
});
const styles = [
{
name: "bold",
icon: BoldIcon,
},
{
name: "italic",
icon: ItalicIcon,
},
{
name: "underline",
icon: UnderlineIcon,
},
{
name: "strikethrough",
icon: StrikethroughIcon,
},
];
const showPopup = ref(false);
const popupX = ref(0);
const popupY = ref(0);
const currentLineIndex = ref(0);
const selectionStart = ref(0);
const selectionEnd = ref(0);
const colorPicker = ref(false);
const pickColor = () => {
colorPicker.value = !colorPicker.value;
};
const totalCharacters = computed(() => {
return motd.value.reduce((sum, line) => {
return Math.max(
sum,
line.reduce((lineSum, segment) => lineSum + segment.text.length, 0),
);
}, 0);
});
const minecraftFormat = computed(() => {
return motd.value
.map((line) => {
return line
.map((segment) => {
let format = getColorCode(segment.color);
if (segment.bold) format += "§l";
if (segment.italic) format += "§o";
if (segment.underline) format += "§n";
if (segment.strikethrough) format += "§m";
return format + segment.text;
})
.join("");
})
.join("\n");
});
const currentStyle = computed(() => {
const line = motd.value[currentLineIndex.value];
if (!line) return {};
let start = 0;
for (const segment of line) {
if (start + segment.text.length > selectionStart.value) {
return {
color: segment.color || "White",
bold: segment.bold || false,
italic: segment.italic || false,
underline: segment.underline || false,
strikethrough: segment.strikethrough || false,
};
}
start += segment.text.length;
}
return {};
});
function getColorCode(color) {
const format = formatCodes.find((f) => f.description === color);
return format ? format.code : "§f";
}
function renderLine(line) {
return line
.map((segment) => {
let style = `color: ${segment.color};`;
if (segment.bold) style += "font-weight: 900;";
if (segment.italic) style += "font-style: italic;";
if (segment.underline) style += "text-decoration: underline;";
if (segment.strikethrough) style += "text-decoration: line-through;";
return `<span style="${style}">${segment.text}</span>`;
})
.join("");
}
function handleSelection(lineIndex) {
const selection = window.getSelection();
if (selection.toString().length > 0) {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
popupX.value = rect.left;
popupY.value = rect.bottom;
showPopup.value = true;
currentLineIndex.value = lineIndex;
const lineElement = document.querySelectorAll("[contenteditable]")[lineIndex];
const rangeClone = range.cloneRange();
rangeClone.selectNodeContents(lineElement);
rangeClone.setEnd(range.startContainer, range.startOffset);
selectionStart.value = rangeClone.toString().length;
selectionEnd.value = selectionStart.value + range.toString().length;
} else {
showPopup.value = false;
colorPicker.value = false;
}
}
function applyStyle(newStyle) {
const line = motd.value[currentLineIndex.value];
const newLine = [];
let currentPos = 0;
for (const segment of line) {
if (currentPos + segment.text.length <= selectionStart.value) {
newLine.push(segment);
} else if (currentPos >= selectionEnd.value) {
newLine.push(segment);
} else {
const beforeSelection = segment.text.slice(0, Math.max(0, selectionStart.value - currentPos));
const inSelection = segment.text.slice(
Math.max(0, selectionStart.value - currentPos),
Math.min(segment.text.length, selectionEnd.value - currentPos),
);
const afterSelection = segment.text.slice(
Math.min(segment.text.length, selectionEnd.value - currentPos),
);
console.log(beforeSelection);
console.log(inSelection);
console.log(afterSelection);
if (beforeSelection) newLine.push({ ...segment, text: beforeSelection });
if (inSelection) {
const mergedStyle = { ...segment, ...newStyle };
for (const key in newStyle) {
if (newStyle[key] === false) {
delete mergedStyle[key];
}
}
newLine.push({ ...mergedStyle, text: inSelection });
}
if (afterSelection) newLine.push({ ...segment, text: afterSelection });
}
currentPos += segment.text.length;
}
motd.value[currentLineIndex.value] = newLine;
showPopup.value = false;
colorPicker.value = false;
// Rerender the line to reflect the changes
nextTick(() => {
const lineElement = document.querySelectorAll("[contenteditable]")[currentLineIndex.value];
lineElement.innerHTML = renderLine(newLine);
});
}
function insertEmoji() {
const emoji = "☺";
if (totalCharacters.value + emoji.length <= 90) {
applyStyle({ text: emoji });
}
}
function handleInput(event, lineIndex) {
const newText = event.target.textContent;
const oldText = motd.value[lineIndex].reduce((acc, segment) => acc + segment.text, "");
const diff = newText.length - oldText.length;
if (newText.length <= 45) {
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const cursorOffset = getCursorOffset(event.target, range);
const newLine = [];
let currentPos = 0;
for (const segment of motd.value[lineIndex]) {
const segmentEnd = currentPos + segment.text.length;
const newSegmentText = newText.slice(currentPos, Math.min(segmentEnd, newText.length));
if (newSegmentText) {
newLine.push({ ...segment, text: newSegmentText });
}
currentPos = segmentEnd;
if (currentPos >= newText.length) break;
}
if (currentPos < newText.length) {
newLine.push({ text: newText.slice(currentPos), color: "White" });
}
motd.value[lineIndex] = newLine;
nextTick(() => {
const lineElement = event.target;
lineElement.innerHTML = renderLine(newLine);
const newRange = document.createRange();
const sel = window.getSelection();
const { node, offset } = getCursorNodeAndOffset(lineElement, cursorOffset);
if (node) {
newRange.setStart(node, offset);
newRange.collapse(true);
sel.removeAllRanges();
sel.addRange(newRange);
}
});
} else {
event.target.innerHTML = renderLine(motd.value[lineIndex]);
}
}
// Helper function to get cursor offset considering styled spans
function getCursorOffset(element, range) {
let offset = 0;
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
let node;
while ((node = walker.nextNode())) {
if (node === range.startContainer) {
return offset + range.startOffset;
}
offset += node.length;
}
return offset;
}
// Helper function to find the node and offset for cursor placement
function getCursorNodeAndOffset(element, targetOffset) {
let currentOffset = 0;
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
let node;
while ((node = walker.nextNode())) {
if (currentOffset + node.length >= targetOffset) {
return { node, offset: targetOffset - currentOffset };
}
currentOffset += node.length;
}
// If we've gone past the end, return the last possible position
const lastTextNode = element.lastChild?.lastChild;
return { node: lastTextNode, offset: lastTextNode?.length || 0 };
}
function handlePaste(event, lineIndex) {
event.preventDefault();
const pastedText = (event.clipboardData || window.clipboardData).getData("text");
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const startOffset = range.startOffset;
const currentText = motd.value[lineIndex].reduce((acc, segment) => acc + segment.text, "");
const newText = currentText.slice(0, startOffset) + pastedText + currentText.slice(startOffset);
if (newText.length <= 45) {
// Preserve existing styles by matching new text with old segments
const newLine = [];
let currentPos = 0;
for (const segment of motd.value[lineIndex]) {
if (currentPos < startOffset) {
const segmentEnd = Math.min(currentPos + segment.text.length, startOffset);
newLine.push({ ...segment, text: newText.slice(currentPos, segmentEnd) });
currentPos = segmentEnd;
} else if (currentPos >= startOffset + pastedText.length) {
newLine.push({ ...segment, text: newText.slice(currentPos) });
break;
}
}
// Insert pasted text as a new segment
if (currentPos < startOffset + pastedText.length) {
newLine.push({
text: newText.slice(currentPos, startOffset + pastedText.length),
color: "White",
});
}
motd.value[lineIndex] = newLine;
nextTick(() => {
const lineElement = document.querySelectorAll("[contenteditable]")[lineIndex];
lineElement.innerHTML = renderLine(newLine);
const newRange = document.createRange();
const sel = window.getSelection();
newRange.setStart(lineElement.childNodes[0], startOffset + pastedText.length);
newRange.collapse(true);
sel.removeAllRanges();
sel.addRange(newRange);
});
}
}
</script>
<style scoped>
.minecraft-font {
font-family: "Minecraft", monospace;
font-size: 16px;
line-height: 1.5;
}
[contenteditable] {
outline: none;
}
</style>
<style scoped>
@font-face {
font-family: "Monocraft";
src: url("/Monocraft.ttf") format("truetype");
}
.font-minecraft {
font-family: "Monocraft", monospace;
}
.mcbg {
background: url("@/assets/images/servers/minecraft-background-dark.png") repeat center center;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease-in-out;
}
.fade-enter, .fade-leave-to /* .fade-leave-active in <2.1.8 */ {
opacity: 0;
}
</style>