mirror of
https://github.com/arabine/open-story-teller.git
synced 2025-12-06 17:09:06 +01:00
First web server route, use of preact for raylib html version
This commit is contained in:
parent
b2fdf5c03b
commit
432d72c80c
13 changed files with 933 additions and 189 deletions
|
|
@ -30,7 +30,7 @@ MainWindow::MainWindow()
|
|||
, m_nodeEditorWindow(*this)
|
||||
, m_libraryWindow(*this, m_libraryManager)
|
||||
, m_player(*this)
|
||||
|
||||
, m_webServer(m_libraryManager)
|
||||
{
|
||||
// VM Initialize
|
||||
m_chip32_ctx.stack_size = 512;
|
||||
|
|
|
|||
|
|
@ -1,175 +1,68 @@
|
|||
|
||||
#include <string.h>
|
||||
#include "json.hpp"
|
||||
|
||||
#include "web_server.h"
|
||||
|
||||
#define DOCUMENT_ROOT "."
|
||||
#define PORT "8081"
|
||||
|
||||
class FooHandler : public CivetHandler
|
||||
bool HandlerBase::Reply(struct mg_connection *conn, const nlohmann::json &json)
|
||||
{
|
||||
public:
|
||||
bool
|
||||
handleGet(CivetServer *server, struct mg_connection *conn)
|
||||
{
|
||||
/* Handler may access the request info using mg_get_request_info */
|
||||
const struct mg_request_info *req_info = mg_get_request_info(conn);
|
||||
std::stringstream ss;
|
||||
|
||||
mg_printf(conn,
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: "
|
||||
"text/html\r\nConnection: close\r\n\r\n");
|
||||
std::string data = json.dump();
|
||||
|
||||
mg_printf(conn, "<html><body>\n");
|
||||
mg_printf(conn, "<h2>This is the Foo GET handler!!!</h2>\n");
|
||||
mg_printf(conn,
|
||||
"<p>The request was:<br><pre>%s %s HTTP/%s</pre></p>\n",
|
||||
req_info->request_method,
|
||||
req_info->request_uri,
|
||||
req_info->http_version);
|
||||
mg_printf(conn, "</body></html>\n");
|
||||
/* Send HTTP message header (+1 for \n) */
|
||||
mg_send_http_ok(conn, "application/json; charset=utf-8", data.size() + 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
bool
|
||||
handlePost(CivetServer *server, struct mg_connection *conn)
|
||||
{
|
||||
/* Handler may access the request info using mg_get_request_info */
|
||||
const struct mg_request_info *req_info = mg_get_request_info(conn);
|
||||
long long rlen, wlen;
|
||||
long long nlen = 0;
|
||||
long long tlen = req_info->content_length;
|
||||
char buf[1024];
|
||||
/* Send HTTP message content */
|
||||
mg_write(conn, data.c_str(), data.size());
|
||||
|
||||
mg_printf(conn,
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: "
|
||||
"text/html\r\nConnection: close\r\n\r\n");
|
||||
/* Add a newline. This is not required, but the result is more
|
||||
* human-readable in a debugger. */
|
||||
mg_write(conn, "\n", 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
mg_printf(conn, "<html><body>\n");
|
||||
mg_printf(conn, "<h2>This is the Foo POST handler!!!</h2>\n");
|
||||
mg_printf(conn,
|
||||
"<p>The request was:<br><pre>%s %s HTTP/%s</pre></p>\n",
|
||||
req_info->request_method,
|
||||
req_info->request_uri,
|
||||
req_info->http_version);
|
||||
mg_printf(conn, "<p>Content Length: %li</p>\n", (long)tlen);
|
||||
mg_printf(conn, "<pre>\n");
|
||||
bool LibraryManagerHandler::handleGet(CivetServer *server, struct mg_connection *conn)
|
||||
{
|
||||
const struct mg_request_info *req_info = mg_get_request_info(conn);
|
||||
nlohmann::json json;
|
||||
|
||||
while (nlen < tlen) {
|
||||
rlen = tlen - nlen;
|
||||
if (rlen > sizeof(buf)) {
|
||||
rlen = sizeof(buf);
|
||||
}
|
||||
rlen = mg_read(conn, buf, (size_t)rlen);
|
||||
if (rlen <= 0) {
|
||||
break;
|
||||
}
|
||||
wlen = mg_write(conn, buf, (size_t)rlen);
|
||||
if (wlen != rlen) {
|
||||
break;
|
||||
}
|
||||
nlen += wlen;
|
||||
}
|
||||
|
||||
mg_printf(conn, "\n</pre>\n");
|
||||
mg_printf(conn, "</body></html>\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#define fopen_recursive fopen
|
||||
|
||||
bool
|
||||
handlePut(CivetServer *server, struct mg_connection *conn)
|
||||
for (auto &s : m_libraryManager)
|
||||
{
|
||||
/* Handler may access the request info using mg_get_request_info */
|
||||
const struct mg_request_info *req_info = mg_get_request_info(conn);
|
||||
long long rlen, wlen;
|
||||
long long nlen = 0;
|
||||
long long tlen = req_info->content_length;
|
||||
FILE * f;
|
||||
char buf[1024];
|
||||
int fail = 0;
|
||||
|
||||
#ifdef _WIN32
|
||||
_snprintf(buf, sizeof(buf), "D:\\somewhere\\%s\\%s", req_info->remote_user, req_info->local_uri);
|
||||
buf[sizeof(buf)-1] = 0;
|
||||
if (strlen(buf)>255) {
|
||||
/* Windows will not work with path > 260 (MAX_PATH), unless we use
|
||||
* the unicode API. However, this is just an example code: A real
|
||||
* code will probably never store anything to D:\\somewhere and
|
||||
* must be adapted to the specific needs anyhow. */
|
||||
fail = 1;
|
||||
f = NULL;
|
||||
} else {
|
||||
f = fopen_recursive(buf, "wb");
|
||||
}
|
||||
#else
|
||||
snprintf(buf, sizeof(buf), "~/somewhere/%s/%s", req_info->remote_user, req_info->local_uri);
|
||||
buf[sizeof(buf)-1] = 0;
|
||||
if (strlen(buf)>1020) {
|
||||
/* The string is too long and probably truncated. Make sure an
|
||||
* UTF-8 string is never truncated between the UTF-8 code bytes.
|
||||
* This example code must be adapted to the specific needs. */
|
||||
fail = 1;
|
||||
f = NULL;
|
||||
} else {
|
||||
f = fopen_recursive(buf, "w");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!f) {
|
||||
fail = 1;
|
||||
} else {
|
||||
while (nlen < tlen) {
|
||||
rlen = tlen - nlen;
|
||||
if (rlen > sizeof(buf)) {
|
||||
rlen = sizeof(buf);
|
||||
}
|
||||
rlen = mg_read(conn, buf, (size_t)rlen);
|
||||
if (rlen <= 0) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
wlen = fwrite(buf, 1, (size_t)rlen, f);
|
||||
if (wlen != rlen) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
nlen += wlen;
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
if (fail) {
|
||||
mg_printf(conn,
|
||||
"HTTP/1.1 409 Conflict\r\n"
|
||||
"Content-Type: text/plain\r\n"
|
||||
"Connection: close\r\n\r\n");
|
||||
} else {
|
||||
mg_printf(conn,
|
||||
"HTTP/1.1 201 Created\r\n"
|
||||
"Content-Type: text/plain\r\n"
|
||||
"Connection: close\r\n\r\n");
|
||||
}
|
||||
|
||||
return true;
|
||||
nlohmann::json story = {
|
||||
{"title", s->GetName() },
|
||||
{"uuid", s->GetUuid() },
|
||||
};
|
||||
json.push_back(story);
|
||||
|
||||
}
|
||||
};
|
||||
return Reply(conn, json);
|
||||
}
|
||||
|
||||
|
||||
static const char *options[] = {
|
||||
"document_root", DOCUMENT_ROOT,
|
||||
"listening_ports", PORT,
|
||||
0
|
||||
"access_control_allow_origin", "*",
|
||||
"access_control_allow_methods", "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"access_control_allow_headers", "Content-Type",
|
||||
0
|
||||
};
|
||||
|
||||
WebServer::WebServer()
|
||||
: m_server(options)
|
||||
static const std::string gRestBase = "/api/v1";
|
||||
|
||||
WebServer::WebServer(LibraryManager &libraryManager)
|
||||
: m_libraryManager(libraryManager)
|
||||
, m_server(options)
|
||||
, m_libraryManagerHandler(libraryManager)
|
||||
{
|
||||
mg_init_library(0);
|
||||
|
||||
FooHandler h_foo;
|
||||
m_server.addHandler("**.foo", h_foo);
|
||||
printf("Browse files at http://localhost:%s/\n", PORT);
|
||||
m_server.addHandler(gRestBase + "/library/list", m_libraryManagerHandler);
|
||||
// printf("Browse files at http://localhost:%s/\n", PORT);
|
||||
}
|
||||
|
||||
WebServer::~WebServer()
|
||||
|
|
|
|||
|
|
@ -1,16 +1,169 @@
|
|||
#pragma once
|
||||
|
||||
#include "CivetServer.h"
|
||||
#include "library_manager.h"
|
||||
|
||||
class HandlerBase : public CivetHandler
|
||||
{
|
||||
public:
|
||||
|
||||
protected:
|
||||
// Utility methods for children
|
||||
|
||||
bool Reply(struct mg_connection *conn, const nlohmann::json &json);
|
||||
};
|
||||
|
||||
class LibraryManagerHandler : public HandlerBase
|
||||
{
|
||||
public:
|
||||
LibraryManagerHandler(LibraryManager &libraryManager)
|
||||
: m_libraryManager(libraryManager)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
bool handleGet(CivetServer *server, struct mg_connection *conn);
|
||||
|
||||
bool
|
||||
handlePost(CivetServer *server, struct mg_connection *conn)
|
||||
{
|
||||
/* Handler may access the request info using mg_get_request_info */
|
||||
const struct mg_request_info *req_info = mg_get_request_info(conn);
|
||||
long long rlen, wlen;
|
||||
long long nlen = 0;
|
||||
long long tlen = req_info->content_length;
|
||||
char buf[1024];
|
||||
|
||||
mg_printf(conn,
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: "
|
||||
"text/html\r\nConnection: close\r\n\r\n");
|
||||
|
||||
mg_printf(conn, "<html><body>\n");
|
||||
mg_printf(conn, "<h2>This is the Foo POST handler!!!</h2>\n");
|
||||
mg_printf(conn,
|
||||
"<p>The request was:<br><pre>%s %s HTTP/%s</pre></p>\n",
|
||||
req_info->request_method,
|
||||
req_info->request_uri,
|
||||
req_info->http_version);
|
||||
mg_printf(conn, "<p>Content Length: %li</p>\n", (long)tlen);
|
||||
mg_printf(conn, "<pre>\n");
|
||||
|
||||
while (nlen < tlen) {
|
||||
rlen = tlen - nlen;
|
||||
if (rlen > sizeof(buf)) {
|
||||
rlen = sizeof(buf);
|
||||
}
|
||||
rlen = mg_read(conn, buf, (size_t)rlen);
|
||||
if (rlen <= 0) {
|
||||
break;
|
||||
}
|
||||
wlen = mg_write(conn, buf, (size_t)rlen);
|
||||
if (wlen != rlen) {
|
||||
break;
|
||||
}
|
||||
nlen += wlen;
|
||||
}
|
||||
|
||||
mg_printf(conn, "\n</pre>\n");
|
||||
mg_printf(conn, "</body></html>\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#define fopen_recursive fopen
|
||||
|
||||
bool
|
||||
handlePut(CivetServer *server, struct mg_connection *conn)
|
||||
{
|
||||
/* Handler may access the request info using mg_get_request_info */
|
||||
const struct mg_request_info *req_info = mg_get_request_info(conn);
|
||||
long long rlen, wlen;
|
||||
long long nlen = 0;
|
||||
long long tlen = req_info->content_length;
|
||||
FILE * f;
|
||||
char buf[1024];
|
||||
int fail = 0;
|
||||
|
||||
#ifdef _WIN32
|
||||
_snprintf(buf, sizeof(buf), "D:\\somewhere\\%s\\%s", req_info->remote_user, req_info->local_uri);
|
||||
buf[sizeof(buf)-1] = 0;
|
||||
if (strlen(buf)>255) {
|
||||
/* Windows will not work with path > 260 (MAX_PATH), unless we use
|
||||
* the unicode API. However, this is just an example code: A real
|
||||
* code will probably never store anything to D:\\somewhere and
|
||||
* must be adapted to the specific needs anyhow. */
|
||||
fail = 1;
|
||||
f = NULL;
|
||||
} else {
|
||||
f = fopen_recursive(buf, "wb");
|
||||
}
|
||||
#else
|
||||
snprintf(buf, sizeof(buf), "~/somewhere/%s/%s", req_info->remote_user, req_info->local_uri);
|
||||
buf[sizeof(buf)-1] = 0;
|
||||
if (strlen(buf)>1020) {
|
||||
/* The string is too long and probably truncated. Make sure an
|
||||
* UTF-8 string is never truncated between the UTF-8 code bytes.
|
||||
* This example code must be adapted to the specific needs. */
|
||||
fail = 1;
|
||||
f = NULL;
|
||||
} else {
|
||||
f = fopen_recursive(buf, "w");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!f) {
|
||||
fail = 1;
|
||||
} else {
|
||||
while (nlen < tlen) {
|
||||
rlen = tlen - nlen;
|
||||
if (rlen > sizeof(buf)) {
|
||||
rlen = sizeof(buf);
|
||||
}
|
||||
rlen = mg_read(conn, buf, (size_t)rlen);
|
||||
if (rlen <= 0) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
wlen = fwrite(buf, 1, (size_t)rlen, f);
|
||||
if (wlen != rlen) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
nlen += wlen;
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
if (fail) {
|
||||
mg_printf(conn,
|
||||
"HTTP/1.1 409 Conflict\r\n"
|
||||
"Content-Type: text/plain\r\n"
|
||||
"Connection: close\r\n\r\n");
|
||||
} else {
|
||||
mg_printf(conn,
|
||||
"HTTP/1.1 201 Created\r\n"
|
||||
"Content-Type: text/plain\r\n"
|
||||
"Connection: close\r\n\r\n");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
LibraryManager &m_libraryManager;
|
||||
};
|
||||
|
||||
class WebServer : public CivetHandler
|
||||
{
|
||||
|
||||
public:
|
||||
WebServer();
|
||||
WebServer(LibraryManager &libraryManager);
|
||||
~WebServer();
|
||||
|
||||
private:
|
||||
|
||||
LibraryManager &m_libraryManager;
|
||||
|
||||
CivetServer m_server;
|
||||
LibraryManagerHandler m_libraryManagerHandler;
|
||||
|
||||
};
|
||||
|
|
|
|||
39
story-player-raylib/app.js
Normal file
39
story-player-raylib/app.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import apiClient from './classes/api-client.js'
|
||||
import eventBus from './classes/event-bus.js';
|
||||
import storage from './classes/storage.js';
|
||||
|
||||
|
||||
import { render } from 'preact';
|
||||
import { html } from 'htm/preact';
|
||||
import TopMenu from './components/TopMenu.js'
|
||||
import ParametersDialog from './components/ParametersDialog.js';
|
||||
|
||||
export function App() {
|
||||
|
||||
this.params = storage.getItem('server') || {
|
||||
serverUrl: '127.0.0.1',
|
||||
serverPort: 8081,
|
||||
};
|
||||
|
||||
storage.setItem('server', this.params);
|
||||
|
||||
// try to connect to the server
|
||||
apiClient.setBaseUrl(`http://${this.params.serverUrl}:${this.params.serverPort}/api/v1`);
|
||||
apiClient.get('/library/list')
|
||||
.then(data => {
|
||||
console.log('Server is up and running', data);
|
||||
eventBus.publish('server-state-changed', {connected: true});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Server is down', error);
|
||||
eventBus.publish('server-state-changed', {connected: false});
|
||||
});
|
||||
|
||||
|
||||
return html`
|
||||
<${TopMenu} />
|
||||
<${ParametersDialog} />
|
||||
`;
|
||||
}
|
||||
|
||||
render(html`<${App} />`, document.getElementById('app'));
|
||||
54
story-player-raylib/classes/api-client.js
Normal file
54
story-player-raylib/classes/api-client.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
class ApiClient {
|
||||
constructor(baseURL) {
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
setBaseUrl(baseURL) {
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
async request(endpoint, method = 'GET', data = null, headers = {}) {
|
||||
const config = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers
|
||||
}
|
||||
};
|
||||
|
||||
if (data) {
|
||||
config.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseURL}${endpoint}`, config);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || 'Something went wrong');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API request error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
get(endpoint, headers = {}) {
|
||||
return this.request(endpoint, 'GET', null, headers);
|
||||
}
|
||||
|
||||
post(endpoint, data, headers = {}) {
|
||||
return this.request(endpoint, 'POST', data, headers);
|
||||
}
|
||||
|
||||
put(endpoint, data, headers = {}) {
|
||||
return this.request(endpoint, 'PUT', data, headers);
|
||||
}
|
||||
|
||||
delete(endpoint, headers = {}) {
|
||||
return this.request(endpoint, 'DELETE', null, headers);
|
||||
}
|
||||
}
|
||||
// Export de l'instance ApiClient pour l'importer facilement
|
||||
const apiClient = new ApiClient('127.0.0.1:8081');
|
||||
export default apiClient;
|
||||
27
story-player-raylib/classes/event-bus.js
Normal file
27
story-player-raylib/classes/event-bus.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
class EventBus {
|
||||
constructor() {
|
||||
this.events = {};
|
||||
this.id = Math.floor(Math.random() * 10000);
|
||||
}
|
||||
|
||||
subscribe(event, listener) {
|
||||
if (!this.events[event]) {
|
||||
this.events[event] = [];
|
||||
}
|
||||
this.events[event].push(listener);
|
||||
}
|
||||
|
||||
unsubscribe(event, listener) {
|
||||
if (this.events[event]) {
|
||||
this.events[event] = this.events[event].filter(l => l !== listener);
|
||||
}
|
||||
}
|
||||
|
||||
publish(event, data) {
|
||||
if (this.events[event]) {
|
||||
this.events[event].forEach(listener => listener(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new EventBus();
|
||||
48
story-player-raylib/classes/storage.js
Normal file
48
story-player-raylib/classes/storage.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
class Storage {
|
||||
constructor(prefix = '') {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
try {
|
||||
const data = JSON.stringify(value);
|
||||
localStorage.setItem(this.prefix + key, data);
|
||||
} catch (error) {
|
||||
console.error('Error saving to localStorage', error);
|
||||
}
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
try {
|
||||
const data = localStorage.getItem(this.prefix + key);
|
||||
return data ? JSON.parse(data) : null;
|
||||
} catch (error) {
|
||||
console.error('Error reading from localStorage', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
removeItem(key) {
|
||||
try {
|
||||
localStorage.removeItem(this.prefix + key);
|
||||
} catch (error) {
|
||||
console.error('Error removing from localStorage', error);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
try {
|
||||
const keys = Object.keys(localStorage);
|
||||
keys.forEach(key => {
|
||||
if (key.startsWith(this.prefix)) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error clearing localStorage', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exemple d'utilisation
|
||||
export default new Storage('ost_player_v1_');
|
||||
82
story-player-raylib/components/ParametersDialog.js
Normal file
82
story-player-raylib/components/ParametersDialog.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { render } from 'preact';
|
||||
import { html } from 'htm/preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import eventBus from '../classes/event-bus.js';
|
||||
|
||||
function ParametersDialog() {
|
||||
|
||||
const [serverUrl, setServerUrl] = useState('127.0.0.1:8081');
|
||||
|
||||
// Function to show the modal
|
||||
function showModal() {
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
// Function to hide the modal
|
||||
function hideModal() {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
|
||||
// Event listener for the close button
|
||||
function handleCloseClick() {
|
||||
hideModal();
|
||||
}
|
||||
|
||||
// Event listener for the submit button
|
||||
function handleOkClick () {
|
||||
const urlInput = document.getElementById('url-input').value;
|
||||
console.log('URL entered:', urlInput);
|
||||
hideModal();
|
||||
}
|
||||
|
||||
eventBus.subscribe('show-modal', function(data) {
|
||||
showModal();
|
||||
});
|
||||
|
||||
return html`
|
||||
|
||||
<style>
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
display: none; /* Hidden by default */
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */
|
||||
}
|
||||
|
||||
|
||||
.close-button {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close-button:hover,
|
||||
.close-button:focus {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<div id="modal" class="modal">
|
||||
<div class="block fixed">
|
||||
<span class="close-button" onClick="${handleCloseClick}">⨯</span>
|
||||
<label for="url-input">URL du serveur:</label>
|
||||
<div class="wrapper block">
|
||||
<input type="text" id="url-input" name="url-input" placeholder="127.0.0.1:8080" value="${serverUrl}" />
|
||||
</div>
|
||||
<button id="submit-button" class="block accent" onClick="${handleOkClick}">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
}
|
||||
|
||||
export default ParametersDialog;
|
||||
65
story-player-raylib/components/TopMenu.js
Normal file
65
story-player-raylib/components/TopMenu.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { html } from 'htm/preact';
|
||||
import eventBus from '../classes/event-bus.js';
|
||||
import { useState } from 'preact/hooks';
|
||||
|
||||
|
||||
function MessageComponent({ show, message }) {
|
||||
return html`
|
||||
<div>
|
||||
${show
|
||||
? html`<div class="themed">
|
||||
<div class="block fixed accent">
|
||||
${message}
|
||||
</div>
|
||||
</div>`
|
||||
: html`<p />`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
}
|
||||
|
||||
|
||||
function TopMenu() {
|
||||
|
||||
const [message, setMessage] = useState('Erreur ');
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
function handleClick() {
|
||||
eventBus.publish('show-modal', { type: 'parameters' });
|
||||
|
||||
eventBus.subscribe('server-state-changed', function(data) {
|
||||
if (data.connected) {
|
||||
setMessage('Connecté au serveur');
|
||||
} else {
|
||||
setMessage('Serveur non trouvé');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<style>
|
||||
|
||||
.flexRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.themed {
|
||||
--block-accent-color: #EA471A;
|
||||
|
||||
<!-- background: #abcdef; -->
|
||||
}
|
||||
|
||||
</style>
|
||||
<div class="flexRow">
|
||||
<${MessageComponent} show=${error} message=${message} />
|
||||
<div class="block accent" onClick="${handleClick}">
|
||||
Paramètres
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
export default TopMenu;
|
||||
|
|
@ -11,29 +11,44 @@
|
|||
|
||||
<title>OpenStoryTeller Web Player</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="main.css">
|
||||
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"preact": "https://esm.sh/preact@10.23.1",
|
||||
"preact/hooks": "https://esm.sh/preact@10.23.1/hooks",
|
||||
"htm/preact": "https://esm.sh/htm@3.1.1/preact?external=preact"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="shortcut icon" href="https://www.raylib.com/favicon.ico">
|
||||
<style>
|
||||
/* body {
|
||||
margin: 0px;
|
||||
text-align: center;
|
||||
} */
|
||||
canvas.emscripten { border: 0px none; background-color: black; display: inline; }
|
||||
body {
|
||||
line-height: 1;
|
||||
font-family: 'Inter', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
canvas.emscripten { border: 0px none; background-color: black; display: inline; }
|
||||
#canvas-container {
|
||||
width: 100%;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="container-fluid">
|
||||
<vertical-menu></vertical-menu>
|
||||
<body style="min-width: min-content;">
|
||||
<main>
|
||||
<div id="app"></div>
|
||||
|
||||
<div id="canvas-container">
|
||||
<div id="canvas-container" class="block fixed">
|
||||
<canvas class=emscripten id=canvas oncontextmenu=event.preventDefault() tabindex=-1></canvas>
|
||||
</div>
|
||||
<p id="output" />
|
||||
|
|
@ -64,7 +79,6 @@
|
|||
};
|
||||
</script>
|
||||
<script async type="text/javascript" src="bin/story-player.js"></script>
|
||||
<script src="vertical-menu.js" type="module"></script>
|
||||
|
||||
<script src="app.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
54
story-player-raylib/main.css
Normal file
54
story-player-raylib/main.css
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/* Extra styles to make the demo look nicer.
|
||||
* Feel free to look around! */
|
||||
|
||||
main {
|
||||
margin: 4px auto;
|
||||
max-width: 800px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 824px) {
|
||||
main {
|
||||
margin: 4px 12px;
|
||||
width: calc(100% - 24px);
|
||||
}
|
||||
iframe {
|
||||
width: 90vw;
|
||||
height: 56vw;
|
||||
}
|
||||
.preWrapper {
|
||||
width: 100%;
|
||||
}
|
||||
pre {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 32px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #222;
|
||||
}
|
||||
|
||||
h2:first-child {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 28px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
border: 0;
|
||||
}
|
||||
342
story-player-raylib/style.css
Normal file
342
story-player-raylib/style.css
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
/* Basic CSS reset */
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, sans-serif;
|
||||
color: #222;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
font-size: 1em;
|
||||
box-sizing: border-box;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
button,
|
||||
code,
|
||||
kbd,
|
||||
pre {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre {
|
||||
font-family: 'Menlo', 'Monaco', monospace;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
padding: 2px 4px 1px 4px;
|
||||
background: rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
/* util.css
|
||||
* custom utility CSS library for projects by @thesephist */
|
||||
|
||||
/* margins */
|
||||
|
||||
.m0 {margin: 0}
|
||||
|
||||
.mt0, .my0 {margin-top: 0}
|
||||
.mt1, .my1 {margin-top: .25rem}
|
||||
.mt2, .my2 {margin-top: .5rem}
|
||||
.mt3, .my3 {margin-top: .75rem}
|
||||
.mt4, .my4 {margin-top: 1.5rem}
|
||||
.mt5, .my5 {margin-top: 2.25rem}
|
||||
.mt6, .my6 {margin-top: 3.25rem}
|
||||
|
||||
.mb0, .my0 {margin-bottom: 0}
|
||||
.mb1, .my1 {margin-bottom: .25rem}
|
||||
.mb2, .my2 {margin-bottom: .5rem}
|
||||
.mb3, .my3 {margin-bottom: .75rem}
|
||||
.mb4, .my4 {margin-bottom: 1.5rem}
|
||||
.mb5, .my5 {margin-bottom: 2.25rem}
|
||||
.mb6, .my6 {margin-bottom: 3.25rem}
|
||||
|
||||
.ml0, .mx0 {margin-left: 0}
|
||||
.ml1, .mx1 {margin-left: .25rem}
|
||||
.ml2, .mx2 {margin-left: .5rem}
|
||||
.ml3, .mx3 {margin-left: .75rem}
|
||||
.ml4, .mx4 {margin-left: 1.5rem}
|
||||
.ml5, .mx5 {margin-left: 2.25rem}
|
||||
.ml6, .mx6 {margin-left: 3.25rem}
|
||||
|
||||
.mr0, .mx0 {margin-right: 0}
|
||||
.mr1, .mx1 {margin-right: .25rem}
|
||||
.mr2, .mx2 {margin-right: .5rem}
|
||||
.mr3, .mx3 {margin-right: .75rem}
|
||||
.mr4, .mx4 {margin-right: 1.5rem}
|
||||
.mr5, .mx5 {margin-right: 2.25rem}
|
||||
.mr6, .mx6 {margin-right: 3.25rem}
|
||||
|
||||
/* paddings */
|
||||
|
||||
.p0 {padding: 0}
|
||||
|
||||
.pt0, .py0 {padding-top: 0}
|
||||
.pt1, .py1 {padding-top: .25rem}
|
||||
.pt2, .py2 {padding-top: .5rem}
|
||||
.pt3, .py3 {padding-top: .75rem}
|
||||
.pt4, .py4 {padding-top: 1rem}
|
||||
|
||||
.pb0, .py0 {padding-bottom: 0}
|
||||
.pb1, .py1 {padding-bottom: .25rem}
|
||||
.pb2, .py2 {padding-bottom: .5rem}
|
||||
.pb3, .py3 {padding-bottom: .75rem}
|
||||
.pb4, .py4 {padding-bottom: 1rem}
|
||||
|
||||
.pl0, .px0 {padding-left: 0}
|
||||
.pl1, .px1 {padding-left: .25rem}
|
||||
.pl2, .px2 {padding-left: .5rem}
|
||||
.pl3, .px3 {padding-left: .75rem}
|
||||
.pl4, .px4 {padding-left: 1rem}
|
||||
|
||||
.pr0, .px0 {padding-right: 0}
|
||||
.pr1, .px1 {padding-right: .25rem}
|
||||
.pr2, .px2 {padding-right: .5rem}
|
||||
.pr3, .px3 {padding-right: .75rem}
|
||||
.pr4, .px4 {padding-right: 1rem}
|
||||
|
||||
/* typography */
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {font-weight: 400}
|
||||
.bold {font-weight: bold}
|
||||
.clean {text-decoration: none}
|
||||
.underline {text-decoration: underline}
|
||||
.f0 {font-size: .75rem}
|
||||
.f1 {font-size: 1rem}
|
||||
.f2 {font-size: 1.25rem}
|
||||
.f3 {font-size: 1.6rem}
|
||||
.f4 {font-size: 2rem}
|
||||
.f5 {font-size: 3rem}
|
||||
.text-start {text-align: start}
|
||||
.text-end {text-align: end}
|
||||
.text-center {text-align: center}
|
||||
|
||||
/* flex & layout */
|
||||
|
||||
.flex {display: flex}
|
||||
.flex-row {flex-direction: row}
|
||||
.flex-column {flex-direction: column}
|
||||
.flex-wrap {flex-wrap: wrap}
|
||||
.align-center {align-items: center}
|
||||
.align-start {align-items: flex-start}
|
||||
.align-end {align-items: flex-end}
|
||||
.justify-center {justify-content: center}
|
||||
.justify-start {justify-content: flex-start}
|
||||
.justify-end {justify-content: flex-end}
|
||||
.fill, .fill-width {width: 100%}
|
||||
.fill, .fill-height {height: 100%}
|
||||
|
||||
/* page widths */
|
||||
|
||||
.page-xs {max-width: 480px}
|
||||
.page-s {max-width: 600px}
|
||||
.page-m {max-width: 800px}
|
||||
.page-l {max-width: 1200px}
|
||||
.page-xl {max-width: 1600px}
|
||||
|
||||
/* shadows */
|
||||
|
||||
.shadow1 {box-shadow: 0 2px 3px rgba(0, 0, 0, .36)}
|
||||
.shadow2 {box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .34)}
|
||||
.shadow3 {box-shadow: 0 4px 7px -1px rgba(0, 0, 0, .32)}
|
||||
.shadow4 {box-shadow: 0 5px 9px -2px rgba(0, 0, 0, .3)}
|
||||
.shadow5 {box-shadow: 0 7px 12px -3px rgba(0, 0, 0, .28)}
|
||||
|
||||
/* list */
|
||||
|
||||
ul.list-reset,
|
||||
ol.list-reset {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* other utility */
|
||||
|
||||
.borderless {border: 0 solid transparent}
|
||||
.rounded {border-radius: 4px}
|
||||
.block {display: block}
|
||||
.inline-block {display: inline-block}
|
||||
.inline {display: inline}
|
||||
.hidden {display: none}
|
||||
.pointer {cursor: pointer}
|
||||
.auto-center {margin-left: auto; margin-right: auto}
|
||||
|
||||
|
||||
/* DEFAULT VARIABLES */
|
||||
body {
|
||||
--block-text-color: #222;
|
||||
--block-background-color: #fff;
|
||||
--block-accent-color: #00ae86;
|
||||
--block-shadow-color: #444;
|
||||
}
|
||||
|
||||
/* BASIC BLOCK STYLES */
|
||||
.block {
|
||||
display: block;
|
||||
color: var(--block-text-color);
|
||||
border: 3px solid var(--block-text-color);
|
||||
border-radius: 3px;
|
||||
padding: 4px 8px;
|
||||
background: var(--block-background-color);
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
|
||||
position: relative;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
transition: transform 0.2s;
|
||||
margin: 8px 6px 10px 6px;
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.block.wrapper,
|
||||
.block.wrapper.inline {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.block.wrapper > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* INTERACTIVE BLOCK STYLES */
|
||||
.block::before {
|
||||
content: "";
|
||||
background: var(--block-background-color);
|
||||
border: 3px solid var(--block-text-color);
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: -3px;
|
||||
height: calc(100% + 6px);
|
||||
width: calc(100% + 6px);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.block:hover,
|
||||
.block:focus {
|
||||
transform: translate(2px, 2px);
|
||||
}
|
||||
|
||||
.block::after {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
background: var(--block-shadow-color);
|
||||
border: 3px solid var(--block-text-color);
|
||||
border-radius: 3px;
|
||||
height: calc(100% + 6px);
|
||||
width: calc(100% + 6px);
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
right: 0;
|
||||
z-index: -2;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.block:hover::after,
|
||||
.block:focus::after {
|
||||
transform: translate(-2px, -3px);
|
||||
}
|
||||
|
||||
.block:active {
|
||||
color: var(--block-text-color);
|
||||
transform: translate(3px, 3px);
|
||||
}
|
||||
|
||||
.block:active::after {
|
||||
transform: translate(-4px, -4px);
|
||||
}
|
||||
|
||||
.block:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.block.fixed {
|
||||
cursor: initial !important;
|
||||
}
|
||||
|
||||
/* FIXED STYLES */
|
||||
.block.fixed:hover,
|
||||
.block.fixed:hover::before,
|
||||
.block.fixed:hover::after,
|
||||
.block.fixed:active,
|
||||
.block.fixed:active::before,
|
||||
.block.fixed:active::after,
|
||||
.block.fixed:focus,
|
||||
.block.fixed:focus::before,
|
||||
.block.fixed:focus::after {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* ACCENT STYLES */
|
||||
.block.accent {
|
||||
color: var(--block-background-color);
|
||||
background: var(--block-accent-color);
|
||||
}
|
||||
|
||||
.block.accent::before {
|
||||
background: var(--block-accent-color);
|
||||
}
|
||||
|
||||
/* INLINE STYLES */
|
||||
.block.inline {
|
||||
display: inline-block;
|
||||
font-size: 0.75em;
|
||||
padding: 0 6px;
|
||||
margin: 3px 2px 1px 4px;
|
||||
}
|
||||
.block.inline::after {
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
}
|
||||
.block.inline:hover,
|
||||
.block.inline:focus {
|
||||
transform: translate(1px, 1px);
|
||||
}
|
||||
.block.inline:hover::after,
|
||||
.block.inline:focus::after {
|
||||
transform: translate(-1px, -1px);
|
||||
}
|
||||
.block.inline:active {
|
||||
transform: translate(2px, 2px);
|
||||
}
|
||||
|
||||
/* ROUND STYLES */
|
||||
.block.round,
|
||||
.block.round::before,
|
||||
.block.round::after {
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.block.round::after {
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
class VerticalMenu extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Create a wrapper nav element using Bulma
|
||||
this.innerHTML = `
|
||||
<nav>
|
||||
<ul>
|
||||
<li><strong>OpenStoryTeller</strong></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="#">Paramètres</a></li>
|
||||
<li><a href="#">À propos</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
`;
|
||||
|
||||
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Define the new element
|
||||
customElements.define('vertical-menu', VerticalMenu);
|
||||
Loading…
Reference in a new issue