1
0
mirror of git://f0xx.org/ac/ac-platform-php synced 2026-07-29 02:18:27 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-23 12:29:28 +02:00
commit f4b39bea26
11 changed files with 2095 additions and 0 deletions

57
platform/README.md Normal file
View File

@@ -0,0 +1,57 @@
# Shared platform pieces
## Footer (`footer.php`)
Flexible site-wide footer for hub, crashes, tickets, graphs, and builder consoles.
**Configure:** edit `footer.config.php` (copy from `footer.config.example.php`).
### Layout
```
┌─────────────────────────────────────────────┐
│ top (optional, full width) │
├──────────────────────────┬──────────────────┤
│ left (optional / auto) │ right (optional) │
├──────────────────────────┴──────────────────┤
│ bottom (optional, full width) │
└─────────────────────────────────────────────┘
```
When `left` is null, the left column is built from `copyright_symbol`, `holder`, and year settings.
| Key | Purpose |
|-----|---------|
| `top` | Full-width line above main row (`null` = hidden) |
| `left` | Main left text; `null` = auto copyright line |
| `right` | Main right text (e.g. build id, links) |
| `bottom` | Full-width line below main row |
| `holder` | Copyright name (default left) |
| `copyright_symbol` | `©` (U+00A9) or empty |
| `show_year` | Append year / range on default left |
| `year` | Fixed end year, or `null` for current |
| `year_start` | Start year (integer). With `year_through_current``2026 - current`; else range `2026{end}` |
| `year_through_current` | If true with `year_start`, year label is `{start} - current` (not a rolling end year) |
| `use_i18n` | `data-i18n` on auto-built left column |
| `footer_class` | Extra CSS class (always adds `platform-footer`) |
**Per-page override** when rendering:
```php
platform_render_footer([
'right' => 'OTA: staging',
'bottom' => 'Internal only',
]);
```
**In PHP views:** `<?php platform_render_footer(); ?>` (loaded via each app `bootstrap.php`).
**Hub:** `examples/app_hub/index.php` (landing nginx runs it through PHP-FPM).
**Static HTML export:**
```bash
php examples/platform/scripts/render-footer-html.php
```
**Gitea** (`/git/`) uses the upstream Gitea UI; this footer does not apply there unless you customize Gitea templates separately.

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* Site-wide footer (hub, crashes, tickets, graphs, builder).
* Copy to footer.config.php and edit; footer.config.php is optional (defaults apply).
*
* Layout (empty/null row = hidden):
* top — full-width line above main row
* main — left | right (flex); invisible gap between columns
* bottom — full-width line below main row
*
* Left column: set `left` to a custom string, or leave null to build from holder + year below.
*/
return [
// --- Optional rows (static or PHP-dynamic strings) ---
'top' => null,
'left' => null,
'right' => null,
'bottom' => null,
// --- Default left column when `left` is null ---
// Use holder_name + holder_url for a safe link (holder HTML is escaped if used alone).
'holder_name' => 'Anton Afanaasyeu',
'holder_url' => 'https://f0xx.org',
'holder' => null,
'copyright_symbol' => "\u{00A9}",
'show_year' => false,
// null = current calendar year at render time (end of range).
'year' => null,
// Integer start year. With year_through_current: "2026 - current" (literal suffix).
// Without it: "20262027" range (en dash) or lone "2026" when start equals end year.
'year_start' => 2026,
'year_through_current' => true,
// CSS class on <footer> (platform-footer layout classes are always added).
'footer_class' => 'bottom-bar',
// When true, consoles with i18n.js replace default left text via data-i18n / data-year.
'use_i18n' => true,
'i18n_key' => 'footer.copyright',
];

View File

@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
return require __DIR__ . '/footer.config.example.php';

241
platform/footer.php Normal file
View File

@@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
/**
* Shared footer fragment for Android Cast web consoles and hub.
*
* Configure: examples/platform/footer.config.php (see footer.config.example.php).
* Usage: <?php platform_render_footer(); ?> or platform_render_footer(['use_i18n' => false]);
*
* Layout (each row omitted when empty):
* [ top — full width ]
* [ left | right ] — main row (copyright defaults on left when left unset)
* [ bottom — full width ]
*/
function platform_footer_h(mixed $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
/** @return array<string, mixed> */
function platform_footer_config(): array
{
static $loaded = null;
if ($loaded !== null) {
return $loaded;
}
$path = __DIR__ . '/footer.config.php';
if (!is_file($path)) {
$path = __DIR__ . '/footer.config.example.php';
}
$loaded = require $path;
return is_array($loaded) ? $loaded : [];
}
/** @param array<string, mixed> $cfg */
function platform_footer_year_end(array $cfg): int
{
if (array_key_exists('year', $cfg) && $cfg['year'] !== null) {
return (int) $cfg['year'];
}
return (int) date('Y');
}
/**
* Year label: single year or range (e.g. 20262026) when year_start is set.
*
* @param array<string, mixed> $cfg
*/
function platform_footer_year_label(array $cfg): string
{
$end = platform_footer_year_end($cfg);
$start = isset($cfg['year_start']) ? (int) $cfg['year_start'] : 0;
if ($start > 0) {
if (!empty($cfg['year_through_current'])) {
return $start . ' - current';
}
if ($start === $end) {
return (string) $start;
}
return $start . "\u{2013}" . $end;
}
return (string) $end;
}
/**
* Default left line: © holder[, year|year-range].
*
* @param array<string, mixed> $cfg
*/
function platform_footer_default_left(array $cfg): string
{
$symbol = trim((string) ($cfg['copyright_symbol'] ?? "\u{00A9}"));
$holderName = trim((string) ($cfg['holder_name'] ?? ''));
$holderUrl = trim((string) ($cfg['holder_url'] ?? ''));
$holder = trim((string) ($cfg['holder'] ?? ''));
$showYear = (bool) ($cfg['show_year'] ?? true);
$parts = [];
if ($symbol !== '') {
$parts[] = $symbol;
}
if ($holderName !== '') {
$parts[] = $holderName;
} elseif ($holder !== '') {
$parts[] = $holder;
}
$line = implode(' ', $parts);
if (!$showYear) {
return $line;
}
$yearLabel = platform_footer_year_label($cfg);
if ($line === '') {
return $yearLabel;
}
return $line . ', ' . $yearLabel;
}
/** Render left footer HTML (holder link when configured). */
function platform_footer_render_left_html(array $cfg, array $opts): ?string
{
$raw = platform_footer_resolve_left($cfg, $opts);
if ($raw === null) {
return null;
}
$holderName = trim((string) ($cfg['holder_name'] ?? ''));
$holderUrl = trim((string) ($cfg['holder_url'] ?? ''));
if (platform_footer_field($cfg, $opts, 'left') === null
&& $holderName !== '' && $holderUrl !== '') {
$text = platform_footer_h($raw);
$name = platform_footer_h($holderName);
$link = '<a href="' . platform_footer_h($holderUrl) . '">' . $name . '</a>';
if (str_contains($text, $name)) {
return substr_replace($text, $link, strpos($text, $name), strlen($name));
}
return $link . ($text !== '' ? ' ' . $text : '');
}
return platform_footer_h($raw);
}
/** True when default left uses holder_name + holder_url (skip i18n text replacement). */
function platform_footer_left_has_link(array $cfg, array $opts): bool
{
if (platform_footer_field($cfg, $opts, 'left') !== null) {
return false;
}
$holderName = trim((string) ($cfg['holder_name'] ?? ''));
$holderUrl = trim((string) ($cfg['holder_url'] ?? ''));
return $holderName !== '' && $holderUrl !== '';
}
/**
* Resolve optional row text (null/empty = omit row). Supports per-render overrides in $opts.
*
* @param array<string, mixed> $cfg
* @param array<string, mixed> $opts
*/
function platform_footer_field(array $cfg, array $opts, string $key): ?string
{
if (array_key_exists($key, $opts)) {
$v = $opts[$key];
if ($v === null || $v === false) {
return null;
}
$t = trim((string) $v);
return $t === '' ? null : $t;
}
if (!array_key_exists($key, $cfg)) {
return null;
}
$v = $cfg[$key];
if ($v === null || $v === false) {
return null;
}
$t = trim((string) $v);
return $t === '' ? null : $t;
}
/**
* @param array<string, mixed> $cfg
* @param array<string, mixed> $opts
*/
function platform_footer_resolve_left(array $cfg, array $opts): ?string
{
$custom = platform_footer_field($cfg, $opts, 'left');
if ($custom !== null) {
return $custom;
}
$built = platform_footer_default_left($cfg);
return $built === '' ? null : $built;
}
/** @deprecated Use platform_footer_default_left(); kept for scripts/tests. */
function platform_footer_plain_text(array $cfg, int $year): string
{
$cfg = array_merge($cfg, ['year' => $year]);
return platform_footer_default_left($cfg);
}
/**
* @param array{use_i18n?: bool, extra_class?: string, top?: ?string, left?: ?string, right?: ?string, bottom?: ?string} $opts
*/
function platform_render_footer(array $opts = []): void
{
$cfg = platform_footer_config();
$top = platform_footer_field($cfg, $opts, 'top');
$left = platform_footer_resolve_left($cfg, $opts);
$right = platform_footer_field($cfg, $opts, 'right');
$bottom = platform_footer_field($cfg, $opts, 'bottom');
$baseClass = (string) ($opts['extra_class'] ?? ($cfg['footer_class'] ?? 'bottom-bar'));
$classes = trim($baseClass . ' platform-footer');
if ($top === null && $bottom === null && $right === null) {
$classes .= ' platform-footer--single-row';
}
$useI18n = $opts['use_i18n'] ?? (bool) ($cfg['use_i18n'] ?? true);
$i18nKey = (string) ($cfg['i18n_key'] ?? 'footer.copyright');
$leftUsesDefault = platform_footer_field($cfg, $opts, 'left') === null;
$leftHasLink = platform_footer_left_has_link($cfg, $opts);
$yearEnd = platform_footer_year_end($cfg);
$leftHtml = platform_footer_render_left_html($cfg, $opts);
echo '<footer class="' . platform_footer_h($classes) . '">';
if ($top !== null) {
echo '<div class="platform-footer__top">' . platform_footer_h($top) . '</div>';
}
if ($left !== null || $right !== null) {
echo '<div class="platform-footer__main">';
if ($left !== null) {
echo '<div class="platform-footer__left"';
if ($useI18n && $i18nKey !== '' && $leftUsesDefault && !$leftHasLink) {
echo ' data-i18n="' . platform_footer_h($i18nKey) . '"'
. ' data-year="' . platform_footer_h(platform_footer_year_label($cfg)) . '"'
. ' data-year-end="' . $yearEnd . '"';
}
echo '>' . ($leftHtml ?? platform_footer_h($left)) . '</div>';
}
if ($right !== null) {
echo '<div class="platform-footer__right">' . platform_footer_h($right) . '</div>';
}
echo '</div>';
}
if ($bottom !== null) {
echo '<div class="platform-footer__bottom">' . platform_footer_h($bottom) . '</div>';
}
echo '</footer>';
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* Emit a static footer.html from footer.config.php (for docs or static-only hosts).
*
* php examples/platform/scripts/render-footer-html.php > /path/to/footer.html
*/
require_once dirname(__DIR__) . '/footer.php';
ob_start();
platform_render_footer(['use_i18n' => false]);
$footer = ob_get_clean();
echo $footer . "\n";

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* Shared session for all Android Cast web consoles (crashes, build, …).
* Same cookie path + session name => one login across sub-apps.
*/
function platform_start_session(string $sessionName = 'ac_crash_sess', string $cookiePath = '/app/androidcast_project'): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
session_name($sessionName);
session_set_cookie_params([
'lifetime' => 0,
'path' => $cookiePath,
'httponly' => true,
'samesite' => 'Lax',
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
]);
session_start();
}