[mod] alles mögliche verbessert

This commit is contained in:
roydfalk 2025-05-22 06:26:16 +00:00
parent 36814b5a7a
commit ae2e6a2142
16 changed files with 703 additions and 537 deletions

View file

@ -1,19 +1,10 @@
# Notizen
## Ziele
- soll eine kleine Web-Anwendung werden
- einzige Domäne im Modell ist die der Dokumente
- Eigenschaften von Dokumenten:
- Autor : Zeichenkette
- Titel : Zeichenkette
- Formulierung : Zeichenkette
- Begründung (optional) : Zeichenkette
- dafür soll es ein schlichtes [CRUD](https://de.wikipedia.org/wiki/CRUD) geben
- für jedes bestehende Dokument soll es Funktionen zum Aufbereiten und Herunderladen geben in den Formaten pdf und ogg
- für die Erstellung der Audio-Variante soll nach Möglichkeit [Piper](https://github.com/rhasspy/piper) verwendet werden
## Zu erledigen
- asynchrones Erstellen der Audio-Dateien (solange nicht fertig, eine Markierung in der Liste anzeigen)
- Persistenz mit SQLite
- Datum für Dokumente ergänzen
- eingebetter Media-Spieler?
- für jedes bestehende Dokument soll es Funktionen zum Aufbereiten und Herunderladen geben in den Formaten pdf/odt (pandoc nutzen) und ogg
- bei Löschung eines Dokuments auch die zugehörigen Dateien löschen

View file

@ -9,7 +9,7 @@ proof-of-concept für Partei-Arbeits-Dokumenten-Verwaltung, welche hörbare Vers
### Voraussetzungen
- curl
- curl (Debian-Paket-Name: `curl`)
### Anweisungen

View file

@ -1,268 +0,0 @@
<?php
namespace rosavox\helpers;
/**
*/
class cache_state
{
public static array $pool = [];
}
/**
*/
function cache_get(string $key, \Closure $retrieve)
{
if (\array_key_exists($key, cache_state::$pool))
{
$value = cache_state::$pool[$key];
}
else
{
$value = ($retrieve)();
cache_state::$pool[$key] = $value;
}
return $value;
}
/**
*/
function database_map_type(string $type)
{
switch ($type)
{
case 'integer': return \SQLITE3_INTEGER;
case 'string': return \SQLITE3_TEXT;
default: throw (new \Exception(\sprintf('unhandled type: %s', $type)));
}
}
/**
*/
function database_get(string $query_template, array $arguments) : array
{
$connection = new \SQLite3('data.sqlite');
$rows = $connection->query($query_template);
/*
$query = "SELECT * FROM books";
$result = $db->query($query);
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
*/
}
/**
*/
function database_put(string $query_template, array $arguments) : int
{
$connection = new \SQLite3('data.sqlite');
$statement = $connection->prepare($query);
foreach ($arguments as $key => $value)
{
$statement->bindValue(
\sprintf(':%s', $key),
$value['value'],
database_map_type($value['type'])
);
}
if ($statement->execute()) {
// SELECT last_insert_rowid()
echo "Buch erfolgreich hinzugefügt!";
}
else
{
echo "Fehler beim Hinzufügen des Buches: " . $db->lastErrorMsg();
}
}
/**
*/
function string_coin(string $template, array $arguments) : string
{
$result = $template;
foreach ($arguments as $key => $value)
{
$result = \str_replace(\sprintf('{{%s}}', $key), $value, $result);
}
return $result;
}
/**
*/
class class_crud_jsonfile
{
private string $path;
private \Closure $id_encode;
private \Closure $id_decode;
public function __construct(
string $path,
\Closure $id_encode,
\Closure $id_decode
)
{
$this->path = $path;
$this->id_encode = $id_encode;
$this->id_decode = $id_decode;
}
private function get() : array
{
$content = (\file_exists($this->path) ? \file_get_contents($this->path) : null);
return (
($content === null)
?
['last_id' => 0, 'entries' => []]
:
\json_decode($content, true)
);
}
private function put(array $data) : void
{
$content = \json_encode($data, \JSON_PRETTY_PRINT);
\file_put_contents($this->path, $content);
}
public function list_() : array
{
$data = $this->get();
return \array_map(
fn ($id_encoded) => [
'id' => ($this->id_decode)($id_encoded),
'value' => $data['entries'][$id_encoded],
],
\array_keys($data['entries'])
);
}
public function read(int $id)
{
$data = $this->get();
$id_encoded = ($this->id_encode)($id);
if (! \array_key_exists($id_encoded, $data['entries']))
{
throw (new \Exception('not found'));
}
else
{
return $data['entries'][$id_encoded];
}
}
public function create($value) : int
{
$data = $this->get();
$id = ($data['last_id'] + 1);
$id_encoded = ($this->id_encode)($id);
$data['last_id'] = $id;
$data['entries'][$id_encoded] = $value;
$this->put($data);
return $id;
}
public function update(int $id, $value) : void
{
$data = $this->get();
$id_encoded = ($this->id_encode)($id);
if (! \array_key_exists($id_encoded, $data['entries']))
{
throw (new \Exception('not found'));
}
else
{
$data['entries'][$id_encoded] = $value;
$this->put($data);
}
}
public function delete(int $id) : void
{
$data = $this->get();
$id_encoded = ($this->id_encode)($id);
if (! \array_key_exists($id_encoded, $data['entries']))
{
throw (new \Exception('not found'));
}
else
{
unset($data['entries'][$id_encoded]);
$this->put($data);
}
}
}
/**
*/
function render(string $template_name, array $arguments) : string
{
return string_coin(
cache_get(
string_coin(
'template.{{name}}',
[
'name' => $template_name,
]
),
fn() => \file_get_contents(
string_coin(
'{{directory}}/templates/{{name}}.html.tpl',
[
'directory' => __DIR__,
'name' => $template_name,
]
)
)
),
$arguments
);
}
/**
*/
function navigate(string $target) : void
{
\header('Location: ' . $target);
}
/**
*/
function generate_audio(string $name, string $input) : string
{
$path_wav = string_coin(
'/tmp/{{name}}.wav',
[
'name' => $name,
]
);
$path_ogg = string_coin(
'{{name}}.ogg',
[
'name' => $name,
]
);
$command = string_coin(
'echo "{{input}}" | piper/piper --model piper/voice.onnx --output_file {{path_wav}} ; ffmpeg -y -i {{path_wav}} {{path_ogg}}',
[
'input' => $input,
'path_wav' => $path_wav,
'path_ogg' => $path_ogg,
]
);
exec($command);
return $path_ogg;
}
?>

30
source/helpers/cache.php Normal file
View file

@ -0,0 +1,30 @@
<?php
namespace rosavox\helpers\cache;
/**
*/
class state
{
public static array $pool = [];
}
/**
*/
function get(string $key, \Closure $retrieve)
{
if (\array_key_exists($key, state::$pool))
{
$value = state::$pool[$key];
}
else
{
$value = ($retrieve)();
state::$pool[$key] = $value;
}
return $value;
}
?>

85
source/helpers/misc.php Normal file
View file

@ -0,0 +1,85 @@
<?php
namespace rosavox\helpers\misc;
require_once('string.php');
require_once('cache.php');
/**
*/
function render(string $template_name, array $arguments) : string
{
return \rosavox\helpers\string_\coin(
\rosavox\helpers\cache\get(
\rosavox\helpers\string_\coin(
'template.{{name}}',
[
'name' => $template_name,
]
),
fn() => \file_get_contents(
\rosavox\helpers\string_\coin(
'{{directory}}/templates/{{name}}.html.tpl',
[
'directory' => /*__DIR__*/'.',
'name' => $template_name,
]
)
)
),
$arguments
);
}
/**
*/
function navigate(string $target) : void
{
\header('Location: ' . $target);
}
/**
*/
function generate_audio(string $input, string $output_path) : void
{
$key = \hash('sha256', $input);
$path_wav = \rosavox\helpers\string_\coin(
'/tmp/{{key}}.wav',
[
'key' => $key,
]
);
$path_ogg = $output_path;
$command = \rosavox\helpers\string_\coin(
'echo "{{input}}" | piper/piper --model piper/voice.onnx --output_file {{path_wav}} && ffmpeg -y -i {{path_wav}} {{path_ogg}} ; rm -f {{path_wav}}',
[
'input' => $input,
'path_wav' => $path_wav,
'path_ogg' => $path_ogg,
]
);
\exec($command);
}
/**
*/
function translate(string $key, ?array $options = null) : string
{
$strings = \rosavox\helpers\cache\get(
'strings',
fn () => \json_decode(\file_get_contents('strings.json'), true)
);
$options = \array_merge(
[
'language' => 'de',
],
($options ?? [])
);
return ($strings[$options['language']][$key] ?? \sprintf('{%s}', $key));
}
?>

128
source/helpers/storage.php Normal file
View file

@ -0,0 +1,128 @@
<?php
namespace rosavox\helpers\storage;
/**
*/
interface interface_
{
public function list_() : array;
public function read(int $id);
public function create($value) : int;
public function update(int $id, $value) : void;
public function delete(int $id) : void;
}
/**
*/
class class_jsonfile implements interface_
{
private string $path;
private \Closure $id_encode;
private \Closure $id_decode;
public function __construct(
string $path,
\Closure $id_encode,
\Closure $id_decode
)
{
$this->path = $path;
$this->id_encode = $id_encode;
$this->id_decode = $id_decode;
}
private function get() : array
{
$content = (\file_exists($this->path) ? \file_get_contents($this->path) : null);
return (
($content === null)
?
['last_id' => 0, 'entries' => []]
:
\json_decode($content, true)
);
}
private function put(array $data) : void
{
$content = \json_encode($data, \JSON_PRETTY_PRINT);
\file_put_contents($this->path, $content);
}
public function list_() : array
{
$data = $this->get();
return \array_map(
fn ($id_encoded) => [
'id' => ($this->id_decode)($id_encoded),
'value' => $data['entries'][$id_encoded],
],
\array_keys($data['entries'])
);
}
public function read(int $id)
{
$data = $this->get();
$id_encoded = ($this->id_encode)($id);
if (! \array_key_exists($id_encoded, $data['entries']))
{
throw (new \Exception('not found'));
}
else
{
return $data['entries'][$id_encoded];
}
}
public function create($value) : int
{
$data = $this->get();
$id = ($data['last_id'] + 1);
$id_encoded = ($this->id_encode)($id);
$data['last_id'] = $id;
$data['entries'][$id_encoded] = $value;
$this->put($data);
return $id;
}
public function update(int $id, $value) : void
{
$data = $this->get();
$id_encoded = ($this->id_encode)($id);
if (! \array_key_exists($id_encoded, $data['entries']))
{
throw (new \Exception('not found'));
}
else
{
$data['entries'][$id_encoded] = $value;
$this->put($data);
}
}
public function delete(int $id) : void
{
$data = $this->get();
$id_encoded = ($this->id_encode)($id);
if (! \array_key_exists($id_encoded, $data['entries']))
{
throw (new \Exception('not found'));
}
else
{
unset($data['entries'][$id_encoded]);
$this->put($data);
}
}
}
?>

18
source/helpers/string.php Normal file
View file

@ -0,0 +1,18 @@
<?php
namespace rosavox\helpers\string_;
/**
*/
function coin(string $template, array $arguments) : string
{
$result = $template;
foreach ($arguments as $key => $value)
{
$result = \str_replace(\sprintf('{{%s}}', $key), $value, $result);
}
return $result;
}
?>

View file

@ -1,133 +1,181 @@
<?php
require_once(__DIR__ . '/helpers.php');
require_once(__DIR__ . '/logic.php');
require_once('helpers/string.php');
require_once('helpers/misc.php');
require_once('services/doc.php');
\rosavox\logic\docs_init();
\rosavox\services\doc\init();
$mode = ($_GET['mode'] ?? 'list');
$id_encoded = (! empty($_GET['id']) ? $_GET['id'] : null);
$id = (($id_encoded === null) ? null : \intval($id_encoded));
function nav(string $mode, array $args) : void
/**
*/
function make_link(string $mode, array $args) : string
{
$a = \array_merge($args, ['mode' => $mode]);
$target = \implode(
'&',
\array_map(
fn ($key) => \rosavox\helpers\string_coin(
'{{key}}={{value}}',
[
'key' => $key,
'value' => $a[$key],
]
),
\array_keys($a)
return (
'?'
.
\implode(
'&',
\array_map(
fn ($key) => \rosavox\helpers\string_\coin(
'{{key}}={{value}}',
[
'key' => $key,
'value' => $a[$key],
]
),
\array_keys($a)
)
)
);
\rosavox\helpers\navigate('?' . $target);
}
/**
*/
function nav(string $mode, array $args) : void
{
\rosavox\helpers\misc\navigate(make_link($mode, $args));
}
/**
*/
function render_list() : string
{
return \rosavox\helpers\misc\render(
'docs-list',
[
'label_make' => \rosavox\helpers\misc\translate('action.make'),
'link_make' => make_link('make', []),
'entries' => \implode(
"\n",
\array_map(
fn ($entry) => \rosavox\helpers\misc\render(
'docs-list-entry',
[
'label_read' => \rosavox\helpers\misc\translate('action.read'),
'label_hear' => \rosavox\helpers\misc\translate('action.hear'),
'value_link_open' => make_link('edit', ['id' => \sprintf('%u', $entry['id'])]),
'value_link_read' => \rosavox\services\doc\readable_path($entry['id']),
'value_link_hear' => \rosavox\services\doc\audio_path($entry['id']),
'value_name' => \rosavox\helpers\string_\coin(
'{{name}}.oga',
[
'name' => \rosavox\services\doc\name($entry['id']),
]
),
'value_text' => $entry['value']['title'],
]
),
\rosavox\services\doc\list_()
)
),
]
);
}
/**
*/
function render_edit(?int $id) : string
{
$doc = (
($id === null)
?
\rosavox\services\doc\empty_()
:
\rosavox\services\doc\read($id)
);
return \rosavox\helpers\misc\render(
'docs-edit',
[
'label_action_back' => \rosavox\helpers\misc\translate('action.back'),
'label_action_save' => \rosavox\helpers\misc\translate('action.save'),
'label_action_delete' => \rosavox\helpers\misc\translate('action.delete'),
'label_doc_title' => \rosavox\helpers\misc\translate('domain.doc.title'),
'label_doc_authors' => \rosavox\helpers\misc\translate('domain.doc.authors'),
'label_doc_content' => \rosavox\helpers\misc\translate('domain.doc.content'),
'label_doc_reasoning' => \rosavox\helpers\misc\translate('domain.doc.reasoning'),
'value_action_back' => make_link('list', []),
'value_action_save' => make_link('save', (($id === null) ? [] : ['id' => \strval($id)])),
'value_action_delete' => make_link('delete', (($id === null) ? [] : ['id' => \strval($id)])),
'value_doc_title' => $doc['title'],
'value_doc_authors' => \implode(', ', $doc['authors']),
'value_doc_content' => $doc['content'],
'value_doc_reasoning' => ($doc['reasoning'] ?? ''),
]
);
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<link rel="stylesheet" type="text/css" href="/style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
</head>
<body>
<h1>rosavox</h1>
<?php
switch ($mode)
{
case 'list':
case 'delete':
{
echo(
\rosavox\helpers\render(
'docs-list',
[
'entries' => \implode(
"\n",
\array_map(
fn ($entry) => \rosavox\helpers\render(
'docs-list-entry',
[
'link_open' => \rosavox\helpers\string_coin(
'?mode=edit&id={{id}}',
[
'id' => $entry['id'],
]
),
'link_read' => '#not_implemented',
'link_hear' => \rosavox\logic\docs_audio_path($entry['id']),
'text' => $entry['value']['title'],
]
),
\rosavox\logic\docs_list()
)
),
'link_new' => '?mode=make',
]
)
);
break;
}
case 'make':
{
echo(
\rosavox\helpers\render(
'docs-edit',
[
'id' => '',
'title' => '',
'authors' => '',
'content' => '',
'reasoning' => '',
'link_back' => '?mode=list',
]
)
);
break;
}
case 'edit':
{
$doc = \rosavox\logic\docs_read($id);
echo(
\rosavox\helpers\render(
'docs-edit',
[
'id' => $id_encoded,
'title' => $doc['title'],
'authors' => \implode(', ', $doc['authors']),
'content' => $doc['content'],
'reasoning' => $doc['reasoning'],
'link_back' => '?mode=list',
]
)
);
if ($id === null)
{
// do nothing
}
else
{
\rosavox\services\doc\delete($id);
}
nav('list', []);
break;
}
case 'save':
{
$doc = [
'title' => $_GET['title'],
'authors' => \explode(',', $_GET['authors']),
'content' => $_GET['content'],
'reasoning' => (empty($_GET['reasoning']) ? null : $_GET['reasoning']),
'title' => $_POST['title'],
'authors' => \explode(',', $_POST['authors']),
'content' => $_POST['content'],
'reasoning' => (empty($_POST['reasoning']) ? null : $_GET['reasoning']),
];
if ($id === null)
{
$id = \rosavox\logic\docs_create($doc);
$id = \rosavox\services\doc\create($doc);
}
else
{
\rosavox\logic\docs_update($id, $doc);
\rosavox\services\doc\update($id, $doc);
}
// nav('edit', ['id' => \rosavox\logic\docs_id_encode($id)]);
// nav('edit', ['id' => \rosavox\services\doc\id_encode($id)]);
nav('list', []);
break;
}
case 'list':
{
echo(render_list());
break;
}
case 'make':
{
echo(render_edit(null));
break;
}
case 'edit':
{
echo(render_edit($id));
break;
}
default:
{
throw (new \Exception(\sprintf('invalid mode: %s', $mode)));

View file

@ -1,146 +0,0 @@
<?php
namespace rosavox\logic;
require_once(__DIR__ . '/helpers.php');
/**
*/
class docs_state
{
public static ?\rosavox\helpers\class_crud_jsonfile $crud = null;
}
/**
*/
function docs_audio_name(int $id) : string
{
return \sprintf('%04u', $id);
}
/**
*/
function docs_audio_path(int $id) : string
{
return \sprintf('%s.ogg', docs_audio_name($id));
}
/**
*/
function docs_generate_audio(int $id, $doc) : void
{
$pause = ' . ';
\rosavox\helpers\generate_audio(
docs_audio_name($id),
\rosavox\helpers\string_coin(
"{{title}}{{pause}}Autoren: {{authors}}{{pause}}Formulierung: {{content}}{{macro_reasoning}}",
[
'pause' => $pause,
'title' => $doc['title'],
'authors' => implode(', ', $doc['authors']),
'content' => $doc['content'],
'macro_reasoning' => (
($doc['reasoning'] === null)
?
''
:
\rosavox\helpers\string_coin(
"{{pause}}Begründung: {{reasoning}}",
[
'pause' => $pause,
'reasoning' => $doc['reasoning'],
]
)
)
]
)
);
}
/**
*/
function docs_list() : array
{
return docs_state::$crud->list_();
}
/**
*/
function docs_read(int $id) : array
{
return docs_state::$crud->read($id);
}
/**
* @todo async generating
*/
function docs_create(array $doc) : int
{
$id = docs_state::$crud->create($doc);
docs_generate_audio($id, $doc);
return $id;
}
/**
*/
function docs_update(int $id, array $doc) : void
{
docs_state::$crud->update($id, $doc);
docs_generate_audio($id, $doc);
}
/**
*/
function docs_delete(int $id) : void
{
docs_state::$crud->delete($id);
}
/**
*/
function docs_add_examples() : void
{
docs_create(
[
'title' => 'Freibier bei Parteitagen',
'authors' => [
'Björn Biernot',
'Doreen Dauerdurst',
],
'content' => 'Der Landesverband möge beschließen, dass zu Beginn eines jeden Parteitags für jeden Deligierten mindestens zwei Flaschen Bier auf den zugehörigen Platz zu stellen sind.',
'reasoning' => 'Wir haben Durst!',
]
);
}
/**
*/
function docs_init() : void
{
docs_state::$crud = new \rosavox\helpers\class_crud_jsonfile(
'docs.json',
fn ($id) => \sprintf('%u', $id),
fn ($id_encoded) => \intval($id_encoded)
);
if (empty(docs_state::$crud->list_()))
{
docs_add_examples();
}
else
{
// do nothing
}
}
?>

246
source/services/doc.php Normal file
View file

@ -0,0 +1,246 @@
<?php
namespace rosavox\services\doc;
require_once('helpers/string.php');
require_once('helpers/storage.php');
require_once('helpers/misc.php');
/**
*/
class state
{
public static ?\rosavox\helpers\storage\interface_ $storage = null;
}
/**
*/
function name(int $id) : string
{
return \sprintf('doc-%04u', $id);
}
/**
*/
function audio_path(int $id) : string
{
return \sprintf('docs/%s.oga', name($id));
}
/**
*/
function readable_path(int $id) : string
{
return \sprintf('docs/%s.md', name($id));
}
/**
*/
function generate_readable(int $id, $doc) : void
{
$markdown = \rosavox\helpers\string_\coin(
"# {{value_title}}
## {{label_authors}}
{{value_authors}}
## {{label_content}}
{{value_content}}
{{macro_reasoning}}",
[
'label_title' => \rosavox\helpers\misc\translate('domain.doc.title'),
'value_title' => $doc['title'],
'label_authors' => \rosavox\helpers\misc\translate('domain.doc.authors'),
'value_authors' => \implode(
"\n",
\array_map(
fn ($author) => \rosavox\helpers\string_\coin(
'- {{author}}',
[
'author' => $author,
]
),
$doc['authors']
)
),
'label_content' => \rosavox\helpers\misc\translate('domain.doc.content'),
'value_content' => $doc['content'],
'macro_reasoning' => (
($doc['reasoning'] === null)
?
''
:
\rosavox\helpers\string_\coin(
"
## {{label_reasoning}}
{{value_reasoning}}",
[
'label_reasoning' => \rosavox\helpers\misc\translate('domain.doc.reasoning'),
'value_reasoning' => $doc['reasoning'],
]
)
)
]
);
\file_put_contents(
readable_path($id),
$markdown
);
}
/**
*/
function generate_audio(int $id, $doc) : void
{
$pause = " .\n";
$text = \rosavox\helpers\string_\coin(
"{{value_title}}{{pause}}{{label_authors}}: {{value_authors}}{{pause}}{{label_content}}: {{value_content}}{{macro_reasoning}}",
[
'pause' => $pause,
'label_title' => \rosavox\helpers\misc\translate('domain.doc.title'),
'value_title' => $doc['title'],
'label_authors' => \rosavox\helpers\misc\translate('domain.doc.authors'),
'value_authors' => \implode($pause, $doc['authors']),
'label_content' => \rosavox\helpers\misc\translate('domain.doc.content'),
'value_content' => $doc['content'],
'macro_reasoning' => (
($doc['reasoning'] === null)
?
''
:
\rosavox\helpers\string_\coin(
"{{pause}}{{label_reasoning}}: {{value_reasoning}}",
[
'pause' => $pause,
'label_reasoning' => \rosavox\helpers\misc\translate('domain.doc.reasoning'),
'value_reasoning' => $doc['reasoning'],
]
)
)
]
);
\rosavox\helpers\misc\generate_audio(
$text,
audio_path($id)
);
}
/**
*/
function empty_() : array
{
return [
'title' => '',
'authors' => [],
'content' => '',
'reasoning' => null,
];
}
/**
*/
function list_() : array
{
return state::$storage->list_();
}
/**
*/
function read(int $id) : array
{
return state::$storage->read($id);
}
/**
* @todo async generating
*/
function create(array $doc) : int
{
$id = state::$storage->create($doc);
generate_readable($id, $doc);
generate_audio($id, $doc);
return $id;
}
/**
*/
function update(int $id, array $doc) : void
{
state::$storage->update($id, $doc);
generate_readable($id, $doc);
generate_audio($id, $doc);
}
/**
*/
function delete(int $id) : void
{
state::$storage->delete($id);
}
/**
*/
function add_examples() : void
{
create(
[
'title' => 'Freibier bei Parteitagen',
'authors' => [
'Björn Biernot',
'Doreen Dauerdurst',
],
'content' => 'Der Landesverband möge beschließen, dass zu Beginn eines jeden Parteitags für jeden Deligierten mindestens zwei Flaschen Bier auf den zugehörigen Platz zu stellen sind.',
'reasoning' => 'Wir haben Durst!',
]
);
create(
[
'title' => 'Götterdämmerung',
'authors' => [
'Fenriswolf',
],
'content' => 'Der Allvater hat mich betrogen. Ich werde ihn und seine elende Asenbrut verschlingen.',
'reasoning' => null,
]
);
}
/**
*/
function init() : void
{
state::$storage = new \rosavox\helpers\storage\class_jsonfile(
'docs.json',
fn ($id) => \sprintf('%u', $id),
fn ($id_encoded) => \intval($id_encoded)
);
if (empty(state::$storage->list_()))
{
add_examples();
}
else
{
// do nothing
}
}
?>

14
source/strings.json Normal file
View file

@ -0,0 +1,14 @@
{
"de": {
"action.make": "neu",
"action.save": "speichern",
"action.back": "zurück",
"action.delete": "löschen",
"action.hear": "hören",
"action.read": "lesen",
"domain.doc.title": "Titel",
"domain.doc.authors": "Autoren",
"domain.doc.content": "Formulierung",
"domain.doc.reasoning": "Begründung"
}
}

View file

@ -9,6 +9,7 @@ html
color: hsl(var(--hue), 0%, 100%);
font-family: sans-serif;
font-size: 1.5em;
}
body
@ -48,7 +49,7 @@ label > span
margin-bottom: 4px;
}
input[type="text"]
input
{
min-width: 480px;
padding: 4px;
@ -56,6 +57,8 @@ input[type="text"]
background-color: hsl(var(--hue), 0%, 25%);
color: hsl(var(--hue), 0%, 100%);
font-size: 1.0em;
border: none;
}
@ -68,5 +71,18 @@ textarea
background-color: hsl(var(--hue), 0%, 25%);
color: hsl(var(--hue), 0%, 100%);
font-size: 1.0em;
border: none;
}
button
{
text-transform: capitalize;
font-size: 1.0em;
}
.docs-list-entry > *
{
vertical-align: middle;
}

View file

@ -1,26 +1,23 @@
<div id="docs-edit">
<form method="GET">
<input type="hidden" name="mode" value="save"/>
<input type="hidden" name="id" value="{{id}}"/>
<form method="POST">
<label>
<span>Titel</span>
<input type="text" name="title" value="{{title}}"/>
<span>{{label_doc_title}}</span>
<input type="text" name="title" value="{{value_doc_title}}"/>
</label>
<label>
<span>Autoren</span>
<input type="text" name="authors" value="{{authors}}"/>
<span>{{label_doc_authors}}</span>
<input type="text" name="authors" value="{{value_doc_authors}}"/>
</label>
<label>
<span>Formulierung</span>
<textarea name="content">{{content}}</textarea>
<span>{{label_doc_content}}</span>
<textarea name="content">{{value_doc_content}}</textarea>
</label>
<label>
<span>Begründung</span>
<textarea name="reasoning">{{reasoning}}</textarea>
</label>
<label>
<input type="submit" value="Speichern"/>
<span>{{label_doc_reasoning}}</span>
<textarea name="reasoning">{{value_doc_reasoning}}</textarea>
</label>
<button formaction="{{value_action_back}}">{{label_action_back}}</button>
<button formaction="{{value_action_save}}">{{label_action_save}}</button>
<button formaction="{{value_action_delete}}">{{label_action_delete}}</button>
</form>
<a href="{{link_back}}">zurück</a>
</div>

View file

@ -1,5 +1,9 @@
<li class="docs-list-entry">
<a href="{{link_open}}">{{text}}</a>
| <a href="{{link_read}}">[lesen]</a>
| <a href="{{link_hear}}">[hören]</a>
<a href="{{value_link_open}}">{{value_text}}</a>
|
<a href="{{value_link_read}}">[{{label_read}}]</a>
|
<a href="{{value_link_hear}}" download="{{value_name}}" type="audio/ogg">[{{label_hear}}]</a>
|
<audio controls="controls" src="{{value_link_hear}}"></audio>
</li>

View file

@ -1,6 +1,8 @@
<div id="docs-list">
<ul>
{{entries}}
{{entries}}
</ul>
<a href="{{link_new}}">Neu</a>
<form method="POST">
<button formaction="{{link_make}}">{{label_make}}</button>
</form>
</div>

View file

@ -12,3 +12,4 @@ mkdir -p ${dir_build}
cp -r -u lib/* ${dir_build}/
cp -r -u -v ${dir_source}/* ${dir_build}/
cd ${dir_build} && ln -f -s index.html.php index.php ; cd -
mkdir -p ${dir_build}/docs