More things work now
All checks were successful
Build and Deploy Nuxt / build (push) Successful in 34s

This commit is contained in:
2026-04-27 00:42:14 +02:00
parent 2b07cc98a6
commit c7aac117c7
16 changed files with 607 additions and 59 deletions

View File

@@ -6,6 +6,7 @@ import WindowManager from './components/managers/WindowManager.vue';
import { CreateWindow } from '@/services/Windows'
import { GetUser, HasAdmin } from './services/User';
import TooltipManager from './components/managers/TooltipManager.vue';
import ContextMenuManager from './components/managers/ContextMenuManager.vue';
async function start(){
if(GetUser()){
@@ -41,6 +42,7 @@ onMounted(() => {
<div class="viewer">
<ToastManager></ToastManager>
<TooltipManager></TooltipManager>
<ContextMenuManager></ContextMenuManager>
<WindowManager></WindowManager>
<ContentManager></ContentManager>
<!-- Managers -->

View File

@@ -0,0 +1,61 @@
<script setup>
import { onMounted, ref, watch } from 'vue';
import { AddContextMenu, HideContextMenu } from '@/services/ContextMenu';
const props = defineProps(['options', 'onselect', 'selected']);
const options = props.options;
const selectCallback = props.onselect;
const initialSelect = props.selected;
const dropdown = ref(null);
const selected = ref(initialSelect);
onMounted(() => {
let context = [];
if(props.selected == undefined) selected.value = "undefined";
watch(() => props.selected, () => {
selected.value = props.selected;
});
options.forEach(name => {
context.push({
icon: selected.value == name ? 'icons/iconoir/regular/check.svg' : false,
name,
action: () => {
HideContextMenu();
selected.value = name;
if(selectCallback) selectCallback(name);
}
});
});
AddContextMenu(dropdown.value, context, {dropdown: true});
});
</script>
<template>
<div class="dropdown" ref="dropdown">
<span>{{ selected }}</span>
<img class="icon" src="/icons/iconoir/regular/nav-arrow-down.svg" draggable="false" ref="closeButton">
</div>
</template>
<style scoped lang="scss">
.dropdown {
flex-grow: 1;
display: flex;
background-color: var(--color-background-softer);
border: none;
padding: 4px 8px 4px 8px;
margin: 0 6px 0px 6px;
border-radius: 6px;
color: var(--color-text);
transition: 300ms background-color;
border: solid 1px var(--color-border);
.icon {
margin-left: auto;
justify-content: right;
}
}
</style>

View File

@@ -0,0 +1,101 @@
<script setup>
import { ref } from 'vue';
const props = defineProps(['rows']);
const rowDict = {}
for(let i = 0; i < props.rows.length; i++) rowDict[props.rows[i].id] = i;
let selectedTab = ref(props.rows[0].id);
function SelectTab(row){
selectedTab.value = row;
}
</script>
<template>
<div class="tab-container">
<div class="row">
<div class="toggler" :class="{ selected: row.id == selectedTab }" v-for="row in rows" v-on:click.prevent="SelectTab(row.id)">
{{ $t(row.value) }}
</div>
</div>
<div class="tab-container-outer">
<div v-for="row in rows" class="tab-content">
<TransitionGroup name="tab">
<div class="tab-content-inner" v-show="row.id == selectedTab" :key="row.id">
<slot :name="row.id" />
</div>
</TransitionGroup>
</div>
</div>
</div>
</template>
<style lang="scss">
.toggler.selected {
color: var(--color-text);
background-color: var(--color-background-softer);
}
</style>
<style scoped lang="scss">
.tab-container {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.tab-container-outer {
flex-grow: 1;
display: flex;
flex-direction: column;
position: relative;
pointer-events: none;
}
.tab-enter-active,
.tab-leave-active {
transition: all 0.15s ease;
}
.tab-enter-from,
.tab-leave-to {
opacity: 0;
transform: translateY(15px);
}
.tab-content {
width: 100%;
height: 100%;
position: absolute;
overflow-y: auto;
}
.tab-content-inner {
width: 100%;
height: 100%;
pointer-events: auto;
}
.toggler {
flex-grow: 1;
flex-basis: 0;
font-weight: bold;
padding: 3px 12px 3px 12px;
font-size: 16px;
color: #9c9c9c;
border-left: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
border-top: 1px solid var(--color-border);
transition: color 0.2s, background-color 0.2s;
&:first-child {
border-left: none;
}
&.active {
color: var(--color-text);
}
}
</style>

View File

@@ -0,0 +1,72 @@
<script setup>
import { onMounted, watch, ref } from 'vue';
import { SetupContextMenu } from '../../services/ContextMenu';
onMounted(() => {
SetupContextMenu();
});
</script>
<template>
<div id="context-menu" class="context-menu">
<!--
<div class="context-menu-element">
<span>Hola</span> <img src="/icons/iconoir/regular/nav-arrow-right.svg">
</div>
<div class="context-menu-element">
<span>Holaa</span>
</div>
<div class="context-menu-element">
<span>Holaa</span>
</div>
<div class="context-menu-element">
<span>Holaaaaaaa</span>
</div>
-->
</div>
</template>
<style lang="scss">
.context-menu {
position: absolute;
z-index: 214748363;
flex-direction: column;
.context-menu-element {
&:last-child {
border-width: 1px 1px 1px 1px;
}
border: solid 1px var(--color-border);
border-width: 1px 1px 0px 1px;
padding: 3px 5px 3px 5px;
cursor: default;
user-select: none;
background-color: var(--tooltip-background);
transition: background-color 100ms;
display: flex;
align-items: center;
position: relative;
span {
flex-grow: 1;
padding-right: 20px;
white-space: nowrap;
}
img {
filter: invert(1);
width: 18px;
height: 18px;
}
&:hover {
background-color: var(--color-background-softest);
}
}
}
</style>

View File

@@ -55,12 +55,12 @@ function EditProfile(){
}
function EditSettings(){
ClearWindow('main_menu');
ClearWindow({type: 'main_menu'});
CreateWindow('settings', {
id: 'settings',
type: 'settings',
title: 'settings.title',
back: () => { ClearWindow('settings'); CreateWindow('main_menu'); }
back: () => { ClearWindow({type: 'settings'}); CreateWindow({type: 'main_menu'}); }
});
}

View File

@@ -1,6 +1,6 @@
<script setup>
import { onMounted, ref } from 'vue';
import { SetupHandle, SetSize, ResetPosition } from '@/services/Windows';
import { SetupHandle, SetSize, ResetPosition, Top } from '@/services/Windows';
import Server from '@/services/Server'
import { SetMinSize, SetResizable } from '@/services/Windows';
@@ -16,12 +16,14 @@ const data = props.data;
const userIcon = ref("");
const handle = ref(null);
const wrapper = ref(null);
const isAdmin = ref(false);
let id = data.id;
console.log(data);
onMounted(() => {
Top(wrapper);
SetupHandle(id, handle);
SetSize(id, {width: 500, height: 480});
ResetPosition(id, "center");
@@ -44,7 +46,7 @@ function RemoveUser(){
<template>
<div class="window-wrapper" :id="'window-wrapper-' + id">
<div class="window-wrapper" :id="'window-wrapper-' + id" ref="wrapper">
<WindowHandle :window="id" ref="handle"></WindowHandle>
<BigIconTemplate :title="data.user.username" :img="userIcon">

View File

@@ -1,10 +1,11 @@
<script setup>
import { onMounted, ref } from 'vue';
import { SetupHandle, SetSize, ResetPosition } from '@/services/Windows';
import { SetupHandle, SetSize, ResetPosition, Top } from '@/services/Windows';
import WindowHandle from './partials/WindowHandle.vue';
const handle = ref(null);
const wrapper = ref(null);
const props = defineProps(['data']);
const data = props.data;
@@ -12,6 +13,7 @@ const data = props.data;
let id = data.id;
onMounted(() => {
Top(wrapper);
SetupHandle(id, handle);
SetSize(id, {width: 500, height: 380});
ResetPosition(id, "center");
@@ -20,7 +22,7 @@ onMounted(() => {
<template>
<div class="window-wrapper" :id="'window-wrapper-' + id">
<div class="window-wrapper" :id="'window-wrapper-' + id" ref="wrapper">
<WindowHandle :window="id" ref="handle"></WindowHandle>
<!-- Body -->

View File

@@ -5,9 +5,9 @@ import {
SetSize,
ResetPosition,
SetResizable,
SetMovable,
ClearWindow,
CreateWindow,
Top,
} from '@/services/Windows';
import WindowHandle from './partials/WindowHandle.vue';
@@ -17,6 +17,7 @@ import { SetUser } from '~/services/User';
import Spinner from '../partials/Spinner.vue';
const handle = ref(null);
const wrapper = ref(null);
const props = defineProps(['data']);
const data = props.data;
@@ -29,6 +30,7 @@ const password = ref("");
const loading = ref(false);
onMounted(() => {
Top(wrapper);
SetupHandle(id, handle);
SetSize(id, {width: 450, height: 480});
SetResizable(id, false);
@@ -37,7 +39,7 @@ onMounted(() => {
function ShowMainMenu(){
CreateWindow('main_menu');
ClearWindow('login');
ClearWindow({type: 'login'});
}
function login() {
@@ -61,13 +63,13 @@ function login() {
function toRegister(){
CreateWindow('register');
ClearWindow('login');
ClearWindow({type: 'login'});
}
</script>
<template>
<div class="window-wrapper" :id="'window-wrapper-' + id">
<div class="window-wrapper" :id="'window-wrapper-' + id" ref="wrapper">
<WindowHandle :window="id" ref="handle"></WindowHandle>
<!-- Body -->

View File

@@ -1,12 +1,13 @@
<script setup>
import { onMounted, ref } from 'vue';
import { SetupHandle, SetSize, ResetPosition } from '@/services/Windows';
import { SetupHandle, SetSize, ResetPosition, Top } from '@/services/Windows';
import WindowHandle from './partials/WindowHandle.vue';
import VersionRender from '../partials/VersionRender.vue';
import EditUserPartial from '../partials/EditUserPartial.vue';
const handle = ref(null);
const wrapper = ref(null);
const props = defineProps(['data']);
const data = props.data;
@@ -14,6 +15,7 @@ const data = props.data;
let id = data.id;
onMounted(() => {
Top(wrapper);
SetupHandle(id, handle);
SetSize(id, {width: 500, height: 460});
ResetPosition(id, "center");
@@ -22,7 +24,7 @@ onMounted(() => {
<template>
<div class="window-wrapper" :id="'window-wrapper-' + id">
<div class="window-wrapper" :id="'window-wrapper-' + id" ref="wrapper">
<WindowHandle :window="id" ref="handle"></WindowHandle>
<EditUserPartial></EditUserPartial>
@@ -33,7 +35,6 @@ onMounted(() => {
<button class="btn-primary button-expand sound-click" v-on:click="OpenCampaigns" ref="campaignButton">{{ $t("main-menu.campaigns") }}</button>
</div>
<VersionRender></VersionRender>
</div>
</template>

View File

@@ -1,6 +1,6 @@
<script setup>
import { onMounted, ref } from 'vue';
import { SetupHandle, SetSize, ResetPosition, CreateWindow, ClearWindow } from '@/services/Windows';
import { SetupHandle, SetSize, ResetPosition, CreateWindow, ClearWindow, Top } from '@/services/Windows';
import WindowHandle from './partials/WindowHandle.vue';
import Spinner from '../partials/Spinner.vue';
@@ -9,6 +9,7 @@ import Server from '~/services/Server';
import { errorMessages } from 'vue/compiler-sfc';
const handle = ref(null);
const wrapper = ref(null);
const props = defineProps(['data']);
const data = props.data;
@@ -34,6 +35,7 @@ const images = [
const splashSource = ref("");
onMounted(() => {
Top(wrapper);
SetupHandle(id, handle);
SetSize(id, {width: 500});
ResetPosition(id, "center");
@@ -46,7 +48,7 @@ onMounted(() => {
function toLogin(){
CreateWindow('login');
ClearWindow('register');
ClearWindow({type: 'register'});
}
function register(){
@@ -93,7 +95,7 @@ function register(){
<template>
<div class="window-wrapper" :id="'window-wrapper-' + id">
<div class="window-wrapper" :id="'window-wrapper-' + id" ref="wrapper">
<WindowHandle :window="id" ref="handle"></WindowHandle>
<!-- Body -->

View File

@@ -0,0 +1,144 @@
<script setup>
import { onMounted, ref } from 'vue';
import WindowHandle from './partials/WindowHandle.vue';
import Tabs from '../layouts/Tabs.vue';
import Dropdown from '../layouts/Dropdown.vue';
import { GetUser, GetUserSetting, SetUserSetting } from '@/services/User';
import { SetupHandle, SetSize, ResetPosition, Top, ClearWindow, CreateWindow, SetMinSize, SetResizable } from '@/services/Windows';
const handle = ref(null);
const wrapper = ref(null);
const props = defineProps(['data']);
const data = props.data;
const id = data.id;
const rows = ref([{id: "account-settings", value: "settings.tabs.account-settings"}]);
/* TODO
const languageOptions = ref(["English", "Spanish", "Catalan"])
const langSelector = ref(null);
const currentLanguage = ref("");
*/
onBeforeMount(() => {
/*
let codes = {
"en-US": "English",
"es-ES": "Spanish",
"ca": "Catalan"
}
GetUserSetting('lang').then(value => {
currentLanguage.value = codes[value ?? 'en']
console.log(currentLanguage.value)
});
*/
if(GetUser().admin) rows.value.push({
id: "site-administration",
value: "settings.tabs.site-administration"
});
});
onMounted(() => {
Top(wrapper);
SetupHandle(id, handle);
SetSize(id, {width: 400, height: 480});
ResetPosition(id, "center");
SetResizable(id, true);
SetMinSize(id, {width: 350, height: 280});
});
function OpenManageAccounts(){
ClearWindow('settings');
CreateWindow('account_management', {
type: 'account_management',
title: 'settings.site-administration.manage-accounts.title',
id: 'account-management',
back: () => {
ClearWindow('account-management')
CreateWindow('settings', {
id: 'settings',
type: 'settings',
title: 'settings.title',
back: () => { ClearWindow('settings'); CreateWindow('main_menu'); }
});
}
})
}
function OpenManagePlugins(){
ClearWindow('settings');
CreateWindow('plugin_management', {
type: 'plugin_management',
title: 'settings.site-administration.manage-plugins.title',
id: 'plugin-management',
back: () => {
ClearWindow('plugin-management')
CreateWindow('settings', {
id: 'settings',
type: 'settings',
title: 'settings.title',
back: () => { ClearWindow('settings'); CreateWindow('main_menu'); }
});
}
})
}
</script>
<template>
<div class="window-wrapper" :id="'window-wrapper-' + id" ref="wrapper">
<WindowHandle :window="id" ref="handle"></WindowHandle>
<!-- Body -->
<Tabs :rows="rows">
<template #account-settings>
<div class="form-container">
<div class="form-element">
<label>{{ $t('settings.account.language') }}</label>
</div>
</div>
</template>
<template #site-administration>
<div class="form-element centered">
<button v-on:click.prevent="OpenManageAccounts">{{ $t('settings.site-administration.manage-accounts-button') }}</button>
</div>
<div class="form-element centered">
<button v-on:click.prevent="OpenManagePlugins">{{ $t('settings.site-administration.manage-plugins-button') }}</button>
</div>
</template>
</Tabs>
</div>
</template>
<style scoped>
.window-wrapper {
width: 100%;
display: flex;
align-items: center;
}
.splash-image {
width: 600px;
height: 250px;
}
.form-field {
padding: 10px;
display: flex;
align-items: left;
flex-direction: column;
justify-content: left;
width: 600px;
}
label {
text-align: left;
}
</style>

View File

@@ -74,13 +74,13 @@ defineExpose({
<div class="window-handle" :id="'window-handle-' + id">
<div class="left" v-if="def">
<img class="icon icon-add-margin" :src="ArrowLeftIcon" draggable="false" ref="backButton" v-if="hasBack" v-on:click="backFunction">
<img class="icon-handle icon-add-margin" :src="ArrowLeftIcon" draggable="false" ref="backButton" v-if="hasBack" v-on:click="backFunction">
</div>
<div class="center" v-if="def">
<span>{{ title }}</span>
</div>
<div class="right">
<img class="icon" :src="XMarkIcon" draggable="false" ref="closeButton" v-if="close" v-on:click="CloseButton">
<img class="icon-handle" :src="XMarkIcon" draggable="false" ref="closeButton" v-if="close" v-on:click="CloseButton">
</div>
<!-- span>{{ title }}</span>
@@ -139,4 +139,10 @@ defineExpose({
background-color: var(--color-window-handle-background);
}
.icon-handle {
width: 24px;
height: 24px;
filter: invert(var(--color-icon-invert));
}
</style>

View File

@@ -0,0 +1,148 @@
// You should hide the context menu when the element that has the
// event gets removed
let margin = -3;
let cursorX = 0;
let cursorY = 0;
let arrowIcon = "icons/iconoir/regular/nav-arrow-right.svg";
import { animate } from 'motion'
function Show(){
let contextMenu = document.getElementById('context-menu');
contextMenu.style.display = "flex";
contextMenu.style.top = (cursorY + margin) + "px";
contextMenu.style.left = (cursorX + margin) + "px";
}
function HideContextMenu(){
let contextMenu = document.getElementById('context-menu');
contextMenu.style.display = "none";
}
function PopulateContext(val){
let children = [];
let elementNum = 0;
val.forEach(element => {
let contextMenuElement = document.createElement('div');
contextMenuElement.classList.add("context-menu-element");
if(element.action)
contextMenuElement.addEventListener("click", element.action);
let spanInfo = document.createElement('span');
spanInfo.innerHTML = element.name;
contextMenuElement.appendChild(spanInfo);
if(element.icon){
let iconContextElement = document.createElement('img');
iconContextElement.src = element.icon;
contextMenuElement.appendChild(iconContextElement);
}
if(element.context){
let iconContextElement = document.createElement('img');
iconContextElement.src = arrowIcon;
contextMenuElement.appendChild(iconContextElement);
let childContextMenuElement = document.createElement('div');
childContextMenuElement.classList.add("context-menu");
childContextMenuElement.style.left = "100%";
childContextMenuElement.style.top = "0";
childContextMenuElement.style.display = "none";
let childChildren = PopulateContext(element.context);
childChildren.forEach((child) => childContextMenuElement.appendChild(child));
contextMenuElement.addEventListener("mouseenter", () => {
childContextMenuElement.style.display = "flex";
});
contextMenuElement.addEventListener("mouseleave", () => {
childContextMenuElement.style.display = "none";
})
contextMenuElement.appendChild(childContextMenuElement);
}
children.push(contextMenuElement);
animate(contextMenuElement, {
opacity: [0, 1],
translateY: [-20, -2]
}, {duration: 0.15}).finished.then(() => {
});
elementNum++;
});
return children;
}
function PopulateContextMenu(val){
let contextMenu = document.getElementById('context-menu');
let children = PopulateContext(val);
contextMenu.replaceChildren();
children.forEach((el) => contextMenu.appendChild(el));
}
function AddContextMenu(element, val, options = {}){
element._dr_context = val;
function show(e){
e.preventDefault();
PopulateContextMenu(val);
Show();
if(options.dropdown){
let rect = element.getBoundingClientRect();
let contextMenu = document.getElementById('context-menu');
contextMenu.style.top = rect.bottom + "px";
contextMenu.style.left = rect.left + "px";
}
}
element.addEventListener('contextmenu', show);
if(options.dropdown) element.addEventListener('click', show);
}
function UpdateVisibility(){
let contextMenu = document.getElementById('context-menu');
let element = document.elementFromPoint(cursorX, cursorY);
let mustHide = true;
while(element){
if(element == contextMenu){
mustHide = false;
break;
}
element = element.parentElement;
}
if(mustHide) HideContextMenu();
}
function SetupContextMenu(){
HideContextMenu();
document.addEventListener('mousemove', (e) => {
cursorX = e.clientX;
cursorY = e.clientY;
});
document.addEventListener('mousedown', UpdateVisibility);
}
function ShowContextMenu(val){
PopulateContextMenu(val);
Show();
}
export {
SetupContextMenu,
AddContextMenu,
ShowContextMenu,
HideContextMenu
};

View File

@@ -1,9 +1,13 @@
import { ClearWindowsWithType, GetFirstWindowId } from './Windows'
import { ClearWindow, GetFirstWindowId } from './Windows'
/*
Put here all dragonroll windows
*/
const defWindows = {
example: {
title: 'windows.example',
component: () => import('~/components/windows/ExampleWindow.vue'),
},
login: {
title: 'windows.login',
movable: false,
@@ -18,16 +22,18 @@ const defWindows = {
title: 'windows.main-menu',
component: () => import('~/components/windows/MainMenuWindow.vue'),
},
example: {
title: 'windows.example',
component: () => import('~/components/windows/ExampleWindow.vue'),
},
edit_profile: {
title: "windows.edit-profile",
component: () => import('~/components/windows/EditProfileWindow.vue'),
close: () => ClearWindowsWithType(GetFirstWindowId('edit_profile')),
close: () => ClearWindow({type: 'edit_profile'}),
movable: true
},
settings: {
title: "windows.settings",
component: () => import('~/components/windows/SettingsWindow.vue'),
close: () => ClearWindow({type: 'settings'}),
movable: true
}
}
export {

View File

@@ -23,7 +23,6 @@ let currentId = 0;
function SetupHandle(id, handle) {
// Update window info with handle info
console.log(id);
let win = GetWindowWithId(id);
let currentWindowId = "window-wrapper-" + id;
@@ -189,7 +188,6 @@ function GetPosition(id) {
}
function ResetPosition(id, pos) {
console.log("The id: ", id)
let win = GetWindowWithId(id);
let data = { x: win.x, y: win.y };
@@ -202,9 +200,6 @@ function ResetPosition(id, pos) {
function CreateWindow(type, data = {}) {
console.log("Creating window")
console.log(windows.value);
let finalData = { ...{ type, id: currentId }, ...defWindows[type], ...data }
currentId++;
@@ -217,25 +212,19 @@ function CreateWindow(type, data = {}) {
}
if (!contains) {
windows.value.push(finalData);
console.log(windows.value)
setTimeout(() => {
SetOnTop(finalData.id);
if (finalData.create) finalData.create();
}, 0);
}
}
function CreateChildWindow(parentId, type, data = {}) {
console.log("Creating child window")
console.log(parentId, type, data);
let finalData = { ...{ type }, ...defWindows[type], ...data }
const newId = currentId;
let parent = GetWindowWithId(parentId);
console.log(parent);
if (parent.children) parent.children.push(finalData.type);
else parent.children = [finalData.type];
if (parent.children) parent.children.push(newId); // We will create the child window right now
else parent.children = [newId];
CreateWindow(type, data);
console.log(windows.value);
}
function GetFirstWindowId(type) {
@@ -250,26 +239,29 @@ function ClearAll() {
});
}
function ClearWindows(data) {
for (let i = 0; i < windows.value.length; i++) {
ClearWindow(windows.value[i].type);
}
// reload.value += 1;
}
function ClearWindowsWithType(type) {
const index = windows.value.findIndex(w => w.type === type);
if (index !== -1) ClearWindow(windows.value[index].id)
// reload.value += 1;
}
function ClearWindow(id) {
function clearWindowById(id){
let win = GetWindowWithId(id);
console.log(win);
if (!win) return;
if (win.children) for (let i = 0; i < win.children.length; i++) ClearWindow(win.children[i].id);
const index = windows.value.findIndex(w => w.type === id)
if (win.children) for (let i = 0; i < win.children.length; i++) clearWindowById(win.children[i]);
const index = windows.value.findIndex(w => w.id === id)
if (index !== -1) windows.value.splice(index, 1)
}
function ClearWindow(selector) {
if(selector.type !== undefined) {
const type = selector.type;
for(let i = 0; i < windows.value.length; i++) {
if(windows.value[i].type == type) {
clearWindowById(windows.value[i].id);
break;
}
}
}
if(selector.id !== undefined) {
const id = selector.id;
clearWindowById(id);
}
// reload.value += 1;
}
@@ -300,6 +292,13 @@ function SetOnTop(id) {
} catch(e) {}
}
function Top(element) {
try {
currentIndex += 1;
element.value.style.zIndex = currentIndex;
} catch(e) {}
}
export {
windows,
SetupHandle,
@@ -311,7 +310,6 @@ export {
SetMovable,
ResetPosition,
WindowMap,
ClearWindows,
CreateWindow,
CreateChildWindow,
GetFirstWindowId,
@@ -320,7 +318,7 @@ export {
SaveWindowPos,
GetPosition,
ClearWindow,
ClearWindowsWithType,
ClearAll,
Top,
getComponent
}

View File

@@ -4,7 +4,8 @@
"register": "Register",
"main-menu": "Dragonroll",
"example": "Example Window",
"edit-profile": "Edit Profile"
"edit-profile": "Edit Profile",
"settings": "Settings"
},
"login": {
"username": "Username or email",