[mod] alles mögliche verbessert
This commit is contained in:
parent
36814b5a7a
commit
ae2e6a2142
16 changed files with 703 additions and 537 deletions
19
notizen.md
19
notizen.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
30
source/helpers/cache.php
Normal 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
85
source/helpers/misc.php
Normal 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
128
source/helpers/storage.php
Normal 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
18
source/helpers/string.php
Normal 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;
|
||||
}
|
||||
|
||||
?>
|
|
@ -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)));
|
||||
|
|
146
source/logic.php
146
source/logic.php
|
@ -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
246
source/services/doc.php
Normal 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
14
source/strings.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue