This commit is contained in:
2026-04-26 00:08:27 +02:00
parent 92074e7f60
commit c3e5448597
40 changed files with 1783 additions and 54 deletions

View File

@@ -1,9 +1,7 @@
<script setup lang="ts">
import ContentManager from './components/managers/ContentManager.vue';
import ToastManager from './components/managers/ToastManager.vue';
import WindowManager from './components/managers/WindowManager.vue';
import Content from './components/viewer/content/Content.vue';
import StatusBar from './components/viewer/statusbar/StatusBar.vue';
import TopBar from './components/viewer/TopBar.vue';
import { CreateWindow } from '@/services/Windows'
@@ -14,19 +12,17 @@ async function start(){
}
onMounted(() => {
setupTheme();
setTheme('dark');
start();
})
</script>
<template>
<div class="viewer">
<ToastManager></ToastManager>
<WindowManager></WindowManager>
<TopBar></TopBar>
<Content></Content>
<StatusBar></StatusBar>
<ContentManager></ContentManager>
<!-- Managers -->
</div>
</template>

View File

@@ -3,27 +3,53 @@
$themes: (
dark: (
background: #141414,
background-light: #202020,
background-line: #202324,
background-fore: #10141f,
window-handle-background: #191919,
window-background: #141414,
window-border: #202324,
window-shadow: #00000077,
button-background: #20202077,
button-hover: #202020aa,
button-active: #202020cc,
hover: #21262d,
selected: #4a4a4b,
border-color: #819796,
border: #202324,
text: #ebede9,
container-shadow: #151d28,
sticky-header-bg: #20202077
sticky-header-bg: #20202077,
icon-invert: 100%
),
light: (
background: #ffffff,
background-light: #f9f9f9,
background-line: #f0f0f0,
background-fore: #ffffff,
window-handle-background: #f0f0f0,
window-background: #ffffff,
window-border: #e0e0e0,
window-shadow: #d4d4d4,
button-background: #f0f0f0,
button-hover: #e9e9e9,
button-active: #d4d4d4,
border-color: #e0e0e0,
border: #f0f0f0,
hover: #e9e9e9,
selected: #d4d4d4,
text: #1e1e1e,
container-shadow: #5f6774,
sticky-header-bg: #fff
sticky-header-bg: #fff,
icon-invert: 0%
)
);

View File

@@ -1,25 +1,31 @@
body {
color: var(--text-color);
color: var(--color-text);
font-family: "BookInsanityRemake", Arial, Helvetica, sans-serif;
}
body {
background-color: var(--background-color);
background-color: var(--color-background);
margin: 0;
}
* {
color: var(--text-color);
color: var(--color-text);
}
a {
color: var(--link-color);
color: var(--color-link);
}
.icon {
height: 12px;
filter: invert(var(--color-icon-invert));
}
* {
font-family: BookInsanityRemake;
}
*::-webkit-scrollbar
{
width: 6px;
@@ -39,6 +45,141 @@ a {
color: var(--error-link);
}
.buttons-row {
width: 100%;
padding-right: 10px;
padding-left: 10px;
display: flex;
flex-direction: row;
justify-content: center;
}
.button-row {
margin-left: 5px;
margin-right: 5px;
flex-grow: 1;
}
.form-field {
padding-bottom: 10px;
display: flex;
align-items: left;
flex-direction: column;
justify-content: left;
}
hr {
border: 0;
height: 1px;
width: 30%;
overflow: visible;
position: relative;
margin: 16px auto 16px auto;
background-color: var(--separator);
}
hr:before {
content: "";
display: inline-block;
width: 8px;
height: 8px;
background-color: var(--separator);
position: absolute;
transform: rotate(45deg);
top: -2.5px;
left: 50%;
margin: -1px 0 0 -1px;
}
input[type=text], input[type=password], input[type=email] {
background-color: var(--color-background-softer);
border: none;
padding: 8px;
border-radius: 6px;
color: var(--color-text);
transition: 300ms background-color;
border: solid 1px var(--color-border);
}
textarea {
background-color: var(--color-background-softer);
padding: 12px;
color: var(--color-text);
border: none;
}
input[type=text]:focus, input[type=password]:focus, input[type=email]:focus {
outline: none;
background-color: var(--color-background-softest);
}
textarea:focus {
outline: none;
}
button {
margin-top: 5px;
margin-bottom: 5px;
padding: 14px;
font-size: 15px;
border-radius: 6px;
outline: none;
border: solid 1px var(--color-border);
-webkit-box-shadow: 0px 0px 10px -2px rgba(0,0,0,0.25);
-moz-box-shadow: 0px 0px 10px -2px rgba(0,0,0,0.25);
box-shadow: 0px 0px 10px -2px rgba(0,0,0,0.25);
transition: 300ms background-color;
background-color: var(--color-button-background);
color: var(--color-text);
}
button:hover {
background-color: var(--color-button-hover);
}
button:active {
background-color: var(--color-button-active);
}
.render-image {
max-width: 600px;
margin-left: auto;
margin-right: auto;
display: block;
}
.confirm-form-button {
margin-top: 15px;
}
.parameters {
display: flex;
flex-direction: column;
width: 100%;
padding: 10px;
}
.param-element {
width: 100%;
display: flex;
flex-direction: row;
}
.param-text {
margin-right: auto;
}
.param-value {
margin-left: auto;
}
.centered {
text-align: center;
}
.window-wrapper {
display: flex;
@@ -51,4 +192,166 @@ a {
-webkit-box-shadow: 0px 0px 10px -2px var(--shadow-color);
-moz-box-shadow: 0px 0px 10px -2px var(--shadow-color);
box-shadow: 0px 0px 10px -2px var(--shadow-color);
}
}
.document {
text-align: left;
width: 100%;
}
.document.centered {
text-align: center;
justify-content: center;
}
.document.item {
text-align: center;
width: 220px;
}
.document.item img {
width: 64px;
height: 64px;
}
.document h1 {
font-weight: normal;
font-size: 32px;
}
.document b {
font-weight: bold;
}
.text-icon {
height: 18px;
width: 18px;
margin-bottom: -4px;
}
.invert {
filter: invert(0.9);
}
.main-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.row {
width: 100%;
display: flex;
overflow-x: auto;
scrollbar-width: thin;
}
span.important {
font-family: NodestoCapsCondensed;
font-size: 24px;
line-height: 32px;
}
span.common {
color: var(--color-common);
}
span.uncommon {
color: var(--color-uncommon);
}
span.rare {
color: var(--color-rare);
}
span.very-rare {
color: var(--color-very-rare);
}
span.legendary {
color: var(--color-legendary);
}
span.artifact {
color: var(--color-artifact);
}
.form-container {
width: 100%;
}
.form-element {
padding: 10px 0 10px 0;
margin: 0 10px 0 10px;
display: flex;
align-items: center;
border-bottom: 1px dashed var(--color-border);
}
.form-element label {
flex-grow: 0;
margin-right: 6px;
margin-left: 6px;
}
.form-element.centered {
justify-content: center;
}
.grow {
flex-grow: 1;
}
.subsection.border:first-child {
border-left: none;
}
.subsection.border {
border-left: 1px solid var(--color-border);
}
.subsection {
margin-left: 5px;
margin-right: 5px;
height: 32px;
display: flex;
align-items: left;
justify-content: left;
}
.subsection.left {
align-items: left;
justify-content: left;
}
.subsection.right {
align-items: right;
justify-content: right;
}
.subsection.center {
align-items: center;
justify-content: center;
}
.window-enter-active,
.window-leave-active {
transition: all 0.15s ease;
}
.window-enter-from,
.window-leave-to {
opacity: 0;
transform: translateY(15px);
}
.window-wrapper {
background-color: var(--window-background);
/* backdrop-filter: blur(10px); */
position: fixed;
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,18 @@
<script setup>
import Content from '../viewer/content/Content.vue';
import StatusBar from '../viewer/statusbar/StatusBar.vue';
import TopBar from '../viewer/TopBar.vue';
import { ShowContent } from '../../services/Content.js';
</script>
<template>
<div v-show="ShowContent">
<TopBar></TopBar>
<Content></Content>
<StatusBar></StatusBar>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,120 @@
<script setup>
import { ref } from 'vue';
import { emitter } from '@/services/Emitter';
const text = ref("");
const toast = ref(null);
let toastQueue = [];
let displayingToast = false;
function DisplayToast(){
if(displayingToast) return;
if(toastQueue.length == 0) return;
displayingToast = true;
let data = toastQueue.pop();
text.value = data.text;
toast.value.classList.add(data.color);
toast.value.classList.add("show");
setTimeout(() => {
toast.value.classList.add("sliding");
setTimeout(() => {
toast.value.style = {};
toast.value.classList.remove("show");
toast.value.classList.remove("sliding");
toast.value.classList.remove(data.color);
displayingToast = false;
DisplayToast();
}, 400);
}, data.duration);
}
emitter.on('toast', data => {
toastQueue.push(data);
DisplayToast();
});
</script>
<template>
<div class="toast" ref="toast">
<div class="toast-container">{{ text }}</div>
</div>
</template>
<style scoped lang="scss">
.toast-container {
height: 100%;
background-color: var(--color-background-soft);
padding: 10px;
margin-left: 5px;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
transform: translate(2px,0px)
}
.toast {
position: absolute;
display: none;
top: 10px;
left: 50%;
transform: translate(-50%, 0);
min-width: 400px;
min-height: 40px;
border-radius: 6px;
text-align: center;
z-index: 9999999;
animation: slide-in 0.4s ease-in-out;
@keyframes slide-in {
0% {
transform: translate(-50%,-50px);
opacity: 0;
}
100% {
opacity: 1;
}
}
&.sliding {
@keyframes slide-out {
0% {
opacity: 1;
}
100% {
transform: translate(-50%,-50px);
opacity: 0;
}
}
animation: slide-out .4s ease-in-out forwards;
}
&.show {
display: block;
}
/* Colors!!!! */
&.red {
background-color: rgb(243, 68, 68);
}
&.green {
background-color: rgb(92, 199, 92);
}
&.aqua {
background-color: rgb(113, 250, 250);
}
}
</style>

View File

@@ -29,7 +29,7 @@ const windows = Windows();
}
.window-wrapper {
background-color: var(--window-background);
background-color: var(--color-window-background);
/* backdrop-filter: blur(10px); */
position: fixed;
@@ -37,6 +37,15 @@ const windows = Windows();
display: flex;
flex-direction: column;
}
align-items: center;
border: solid 1px var(--color-window-border);
/* opacity: 0; */
user-select: none;
-webkit-box-shadow: 0px 0px 10px -2px var(--color-window-shadow);
-moz-box-shadow: 0px 0px 10px -2px var(--color-window-shadow);
box-shadow: 0px 0px 10px -2px var(--color-window-shadow);
}
</style>

View File

@@ -21,7 +21,7 @@ import TopSearchBar from './topbar/TopSearchBar.vue';
flex-shrink: 0;
min-height: 40px;
width: 100%;
background-color: var(--top-bar-background-color);
background-color: var(--color-background-light);
display: flex;
}

View File

@@ -1,8 +1,7 @@
<script setup>
import { ref, onMounted } from 'vue';
import Note from './Note.vue';
const emitter = useEmitter();
import { emitter } from '~/services/Emitter';
let noteData = ref([]);
@@ -61,6 +60,7 @@ emitter.on("delete-note", (key) => {
height: 100%;
margin: 0;
height: 100%;
background-color: var(--color-background);
}
</style>

View File

@@ -1,6 +1,7 @@
<script setup>
import { ref,onMounted } from 'vue';
const emitter = useEmitter();
import { emitter } from '~/services/Emitter';
const statusIcon = ref(null);
const statusMessage = ref(null);

View File

@@ -22,7 +22,7 @@ import FetchStatus from './FetchStatus.vue';
min-height: 24px;
max-height: 24px;
width: 100%;
background-color: var(--top-bar-background-color);
background-color: var(--color-background-light);
display: flex;
font-size: 14px;
}

View File

@@ -0,0 +1,42 @@
<script setup>
import { onMounted, ref } from 'vue';
import { SetupHandle, SetSize, ResetPosition } from '@/services/Windows';
import WindowHandle from './partials/WindowHandle.vue';
const handle = ref(null);
const props = defineProps(['data']);
const data = props.data;
let id = data.id;
const test = ref(null)
onMounted(() => {
SetupHandle(id, handle);
SetSize(id, {width: 500, height: 380});
ResetPosition(id, "center");
});
</script>
<template>
<div class="window-wrapper" :id="'window-wrapper-' + id">
<WindowHandle :window="id" ref="handle"></WindowHandle>
<!-- Body -->
<div ref="test"></div>
</div>
</template>
<style scoped>
.window-wrapper {
display: flex;
align-items: center;
}
</style>

View File

@@ -1,8 +1,19 @@
<script setup>
import { onMounted, ref } from 'vue';
import { SetupHandle, SetSize, ResetPosition } from '@/services/Windows';
import {
SetupHandle,
SetSize,
ResetPosition,
SetResizable,
SetMovable,
ClearWindow,
CreateWindow,
} from '@/services/Windows';
import WindowHandle from './partials/WindowHandle.vue';
import { DisplayToast } from '~/services/Toaster';
import Server from '~/services/Server';
import { SetUser } from '~/services/User';
const handle = ref(null);
@@ -11,13 +22,44 @@ const data = props.data;
let id = data.id;
const test = ref(null)
const username = ref("");
const password = ref("");
onMounted(() => {
SetupHandle(id, handle);
SetSize(id, {width: 500, height: 380});
SetSize(id, {width: 450, height: 480});
SetMovable(id, false);
SetResizable(id, false);
ResetPosition(id, "center");
});
function login() {
Server().post('/user/login', { username: username.value, password: password.value }).then((response) => {
const data = response.data;
console.log(data);
if(data.status == "error"){
DisplayToast('red', "Wrong username or password", 3000)
} else {
SetUser(data.token);
ShowMainMenu();
}
}).catch((error) => {
console.log(error);
if(error.response.status == 429){
// errorMessage.value = error.response.data;
} else {
// errorMessage.value = "Hi ha hagut un error intern, torna'ho a provar més tard";
console.log(error);
}
});
}
function toRegister(){
CreateWindow('register');
ClearWindow('login');
}
</script>
@@ -26,8 +68,30 @@ onMounted(() => {
<WindowHandle :window="id" ref="handle"></WindowHandle>
<!-- Body -->
<div ref="test">
<p>Hola</p>
<div class="vert-expand">
<picture align="center">
<source media="(prefers-color-scheme: dark)" srcset="/img/logo-splash.png">
<source media="(prefers-color-scheme: light)" srcset="/img/logo-splash-light.png">
<img alt="Dragonroll logo" src="/img/logo-splash.png" class="splash-image" draggable="false">
</picture>
<form v-on:submit.prevent="login">
<div class="form-field">
<label for="username">Username</label>
<input id="username-field" type="text" placeholder="Enter your username here..." name="username" v-model="username" autocomplete="off" >
</div>
<div class="form-field">
<label for="password">Password</label>
<input id="password-field" type="password" placeholder="Enter your password..." name="password" v-model="password" autocomplete="off" >
</div>
<div class="form-field">
<button class="btn-primary sound-click">Log in</button>
</div>
<div class="form-field center">
<p>You don't have an account? <a href="#" @click.prevent="toRegister">Register</a></p>
</div>
</form>
</div>
</div>
@@ -35,10 +99,41 @@ onMounted(() => {
<style scoped>
p {
user-select: none;
}
.vert-expand {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
.window-wrapper {
user-select: none;
display: flex;
align-items: center;
}
.splash-image {
width: 450px;
}
form {
margin-left: 30px;
margin-right: 30px;
}
label {
text-align: left;
}
.center {
text-align: center;
}
</style>

View File

@@ -0,0 +1,42 @@
<script setup>
import { onMounted, ref } from 'vue';
import { SetupHandle, SetSize, ResetPosition } from '@/services/Windows';
import WindowHandle from './partials/WindowHandle.vue';
const handle = ref(null);
const props = defineProps(['data']);
const data = props.data;
let id = data.id;
const test = ref(null)
onMounted(() => {
SetupHandle(id, handle);
SetSize(id, {width: 500, height: 380});
ResetPosition(id, "center");
});
</script>
<template>
<div class="window-wrapper" :id="'window-wrapper-' + id">
<WindowHandle :window="id" ref="handle"></WindowHandle>
<!-- Body -->
<div ref="test"></div>
</div>
</template>
<style scoped>
.window-wrapper {
display: flex;
align-items: center;
}
</style>

View File

@@ -133,7 +133,7 @@ defineExpose({
display: flex;
background-color: var(--color-handler);
background-color: var(--color-window-handle-background);
}
</style>

View File

@@ -0,0 +1,47 @@
import { ref, onMounted, watch } from 'vue'
type Theme = 'light' | 'dark'
type Accent = 'katlum'
const theme = ref<Theme>('light')
const accent = ref<Accent>('katlum')
const applyTheme = () => {
document.documentElement.setAttribute('data-theme', theme.value)
document.documentElement.setAttribute('data-accent', accent.value)
}
const setTheme = (value: Theme) => {
theme.value = value
localStorage.setItem('theme', value)
applyTheme();
}
const setAccent = (value: Accent) => {
accent.value = value
localStorage.setItem('accent', value)
applyTheme();
}
const setupTheme = () => {
const savedTheme = localStorage.getItem('theme') as Theme | null
const savedAccent = localStorage.getItem('accent') as Accent | null
const media = window.matchMedia('(prefers-color-scheme: dark)')
theme.value = savedTheme || (media.matches ? 'dark' : 'light')
accent.value = savedAccent || 'katlum'
applyTheme()
media.addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
theme.value = e.matches ? 'dark' : 'light'
applyTheme()
}
})
};
watch([theme, accent], applyTheme)
export { theme, accent, setTheme, setAccent, setupTheme}

View File

@@ -1,11 +0,0 @@
import mitt from 'mitt'
export default defineNuxtPlugin(() => {
const emitter = mitt()
return {
provide: {
emitter
}
}
})

View File

@@ -0,0 +1,11 @@
var backendUrl = ''
if (import.meta.env.PROD) {
backendUrl = 'https://api.aranroig.com/';
} else {
backendUrl = 'http://localhost:5000/'
}
export {
backendUrl
};

View File

@@ -0,0 +1,12 @@
import { ref } from 'vue';
const ShowContent = ref(false);
function SetShowContent(value) {
ShowContent.value = value;
}
export {
ShowContent,
SetShowContent
}

View File

@@ -0,0 +1,3 @@
import mitt from 'mitt'
export const emitter = mitt();

View File

@@ -0,0 +1,21 @@
import axios from 'axios';
import { backendUrl } from './BackendURL';
const server = axios.create({
baseURL: backendUrl,
headers: {
"Access-Control-Allow-Origin": "*",
}
});
// Attach token dynamically on each request via interceptor
server.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export default () => server;

View File

@@ -0,0 +1,9 @@
import { emitter } from './Emitter';
function DisplayToast(color, text, duration = 1000){
emitter.emit("toast", {color, text, duration});
}
export {
DisplayToast,
}

View File

@@ -0,0 +1,80 @@
import { ref } from 'vue';
import Server from './Server';
const UserStatus = ref(0);
function parseJwt(token) {
return JSON.parse(atob(token.split('.')[1]));
}
function SetUser(token){
localStorage.setItem('token', token);
UserStatus.value = 1;
}
async function HasAdmin(){
let response = await Server().get('/user/has-admin');
return response.data.status != "init";
}
async function SetUserSetting(key, value){
let user = GetUser();
if(!user.settings) user.settings = {};
user.settings[key] = value;
const response = await Server().post('/user/update-settings', { settings: user.settings });
return response.data.settings;
}
async function GetUserSetting(key){
const response = await Server().get('/user/get-settings');
if (response.data.settings)
return response.data.settings[key];
return undefined;
}
function GetUser(){
const token = localStorage.getItem('token');
if(token){
const data = parseJwt(token);
// Check if token is expired
const now = Date.now() / 1000;
if(now > data.exp){
LogoutUser();
return undefined;
}
return data;
}
return undefined;
}
function IsAdmin(){
const user = GetUser();
if(user){
return user.admin;
}
}
function LoadUser(){
const token = localStorage.getItem('token');
if(token) UserStatus.value = 1;
}
function LogoutUser(){
localStorage.removeItem("token");
UserStatus.value = 0;
}
export {
UserStatus,
GetUser,
SetUser,
LoadUser,
IsAdmin,
LogoutUser,
HasAdmin,
GetUserSetting,
SetUserSetting
}

View File

@@ -3,22 +3,29 @@ import { ref } from 'vue'
const windows = ref([]);
import LoginWindow from '~/components/windows/LoginWindow.vue';
import RegisterWindow from '~/components/windows/RegisterWindow.vue';
import ExampleWindow from '~/components/windows/ExampleWindow.vue';
let windowMap = {
login: LoginWindow
login: LoginWindow,
register: RegisterWindow,
example: ExampleWindow
};
async function InjectWindow(window_type, plugin, window_component) {
let systemWidows = {};
systemWidows[window_type] = (await import(`../../plugins/${plugin}/views/${window_component}.vue`)).default;
windowMap = { ...windowMap, ...systemWidows };
}
// Presets
const defValues = {
'example': {
id: "example",
title: "Example",
close: () => ClearWindow('example')
},
'login': {
id: 'login',
title: 'Login',
},
'register': {
id: 'register',
title: 'Register'
}
}
@@ -59,7 +66,9 @@ function SetupHandle(id, handle) {
SetOnTop(id);
});
// Move window listeners
handler.addEventListener("mousedown", (event) => {
if(win.noMove) return;
draggingWindow = true;
let windowRect = currentWindow.getBoundingClientRect();
@@ -67,8 +76,8 @@ function SetupHandle(id, handle) {
offsetY = windowRect.top - event.clientY;
})
// Move window listeners
document.addEventListener("mousemove", (event) => {
if(win.noMove) return;
if (!draggingWindow) return;
if (event.clientX + offsetX < -currentWindow.getBoundingClientRect().width + 20) currentWindow.style.left = (-currentWindow.getBoundingClientRect().width + 20) + "px";
@@ -81,6 +90,7 @@ function SetupHandle(id, handle) {
})
document.addEventListener("mouseup", (event) => {
if(win.noMove) return;
draggingWindow = false;
// ummm suposo que no pots tancar mentres mous?
SaveWindowPos({ id, x: parseInt(currentWindow.style.left, 10), y: parseInt(currentWindow.style.top, 10) });
@@ -126,6 +136,11 @@ function SetResizable(id, resizable) {
win.resizable = resizable;
}
function SetMovable(id, movable) {
let win = GetWindowWithId(id);
win.noMove = !movable;
}
function SetSize(id, size) {
let currentWindowId = "window-wrapper-" + id;
let currentWindow = document.getElementById(currentWindowId);
@@ -284,10 +299,10 @@ export {
SetMaxSize,
SetMinSize,
SetPosition,
SetMovable,
ResetPosition,
Windows,
WindowMap,
InjectWindow,
ReloadRef,
ClearWindows,
CreateWindow,