mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 17:50:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b29d71ac5 | ||
|
|
46360ca6ea | ||
|
|
03b1c51e27 | ||
|
|
fc628fb9a0 | ||
|
|
91391b1dd7 | ||
|
|
d61a7b4c92 | ||
|
|
2dabd93809 | ||
|
|
ee5024678d |
No files matched your search
@@ -37,6 +37,7 @@ Run the following commands to verify your changes:
|
||||
- **Dependencies**: if you add a new Python library to `requirements.txt`, you MUST run `voxvera vendorize` to include it in the portable distribution.
|
||||
- **Layout Integrity**: VoxVera uses visual width validation. If you add new UI elements, ensure they respect the physical boundaries of an 8.5"x11" flyer.
|
||||
- **Tests**: Ensure all tests pass: `pytest tests/`. Run the integrity suite specifically when modifying locales: `pytest tests/test_integrity.py`.
|
||||
- **Recommended local check**: Run `bash scripts/check.sh` before opening a PR. It mirrors the main lint/test/build checks in one command.
|
||||
- **Style**: Follow the existing coding style (PEP 8 for Python).
|
||||
- **Documentation**: If adding a new feature, update the master templates in `docs/templates/` and run `voxvera build-docs`.
|
||||
|
||||
|
||||
@@ -30,6 +30,13 @@ pip install .
|
||||
voxvera vendorize # Optional: pull dependencies locally for offline use
|
||||
```
|
||||
|
||||
### 5. Local Validation
|
||||
Run the full local validation sweep with:
|
||||
```bash
|
||||
bash scripts/check.sh
|
||||
```
|
||||
This runs linting, Python tests, Electron smoke tests, dependency checks, and the docs/site rebuild steps used during development.
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
+13
-4
@@ -15,9 +15,12 @@ echo "Starting E2E verification at $(date)"
|
||||
if [ -n "${VOXVERA_BIN:-}" ] && [ -f "$VOXVERA_BIN" ]; then
|
||||
voxvera_cmd="$VOXVERA_BIN"
|
||||
echo "Using provided VoxVera binary: $voxvera_cmd" >&2
|
||||
elif command -v voxvera >/dev/null 2>&1; then
|
||||
voxvera_cmd="$(command -v voxvera)"
|
||||
echo "Using existing VoxVera on PATH: $voxvera_cmd" >&2
|
||||
else
|
||||
echo "Installing VoxVera in editable mode..."
|
||||
python3 -m pip install --break-system-packages -e ".[e2e]"
|
||||
python3 -m pip install --break-system-packages --no-build-isolation -e ".[e2e]"
|
||||
voxvera_cmd="$(command -v voxvera || echo "$HOME/.local/bin/voxvera")"
|
||||
fi
|
||||
|
||||
@@ -30,16 +33,22 @@ DemoUser
|
||||
voxvera
|
||||
EOI
|
||||
|
||||
config_path="$HOME/host/voxvera/config.json"
|
||||
if [ ! -f "$config_path" ]; then
|
||||
echo "CRITICAL: Expected config file not found after init: $config_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building demo flyer..."
|
||||
"$voxvera_cmd" --lang en build
|
||||
"$voxvera_cmd" --lang en --config "$config_path" build
|
||||
|
||||
# Verify build output exists in host/
|
||||
folder_name="voxvera"
|
||||
host_dir="voxvera/host/$folder_name"
|
||||
host_dir="$HOME/host/$folder_name"
|
||||
|
||||
if [ ! -d "$host_dir" ]; then
|
||||
echo "CRITICAL: Host directory $host_dir not found!" >&2
|
||||
ls -R voxvera/host/ || true
|
||||
ls -R "$HOME/host/" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
+10
-5
@@ -4,6 +4,7 @@ const path = require('path');
|
||||
const which = require('which');
|
||||
const fs = require('fs');
|
||||
const { launchTor } = require('./tor.js');
|
||||
const { extractOnionUrl } = require('./runtime-utils');
|
||||
|
||||
let mainWindow;
|
||||
let onionProc;
|
||||
@@ -74,8 +75,12 @@ async function runServe (retry = false) {
|
||||
|
||||
onionProc.stdout.on('data', buf => {
|
||||
const line = buf.toString();
|
||||
const m = line.match(/OnionShare is hosting at (http.*\.onion)/);
|
||||
if (m) { gotURL = true; clearTimeout(softTimeout); }
|
||||
const onionUrl = extractOnionUrl(line);
|
||||
if (onionUrl) {
|
||||
gotURL = true;
|
||||
clearTimeout(softTimeout);
|
||||
mainWindow.webContents.send('onion-url', onionUrl);
|
||||
}
|
||||
mainWindow.webContents.send('log', { text: line });
|
||||
});
|
||||
|
||||
@@ -134,9 +139,9 @@ ipcMain.handle('run-quickstart', async (_, config) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('log', { text: line, isError: false });
|
||||
}
|
||||
const m = line.match(/Onion URL:\s*(https?:\/\/[a-z0-9.-]+\.onion)/i);
|
||||
if (m && mainWindow) {
|
||||
mainWindow.webContents.send('onion-url', m[1]);
|
||||
const onionUrl = extractOnionUrl(line);
|
||||
if (onionUrl && mainWindow) {
|
||||
mainWindow.webContents.send('onion-url', onionUrl);
|
||||
}
|
||||
});
|
||||
proc.stderr.on('data', data => {
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"lint": "echo 'lint pass'"
|
||||
"lint": "echo 'lint pass'",
|
||||
"test": "node --test runtime-utils.test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^29.0.0",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
function extractOnionUrl(line) {
|
||||
const match = line.match(/Onion URL:\s*(https?:\/\/[a-z0-9.-]+\.onion)/i)
|
||||
|| line.match(/OnionShare is hosting at\s*(https?:\/\/[a-z0-9.-]+\.onion)/i);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function platformDirName(platform = process.platform) {
|
||||
if (platform === 'win32') return 'win';
|
||||
if (platform === 'darwin') return 'mac';
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extractOnionUrl,
|
||||
platformDirName,
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { extractOnionUrl, platformDirName } = require('./runtime-utils');
|
||||
|
||||
test('extractOnionUrl handles current CLI output', () => {
|
||||
assert.equal(
|
||||
extractOnionUrl('[voxvera] Onion URL: http://test123.onion'),
|
||||
'http://test123.onion'
|
||||
);
|
||||
});
|
||||
|
||||
test('extractOnionUrl handles legacy OnionShare output', () => {
|
||||
assert.equal(
|
||||
extractOnionUrl('OnionShare is hosting at http://legacy456.onion'),
|
||||
'http://legacy456.onion'
|
||||
);
|
||||
});
|
||||
|
||||
test('extractOnionUrl returns null when no onion URL is present', () => {
|
||||
assert.equal(extractOnionUrl('still bootstrapping'), null);
|
||||
});
|
||||
|
||||
test('platformDirName maps bundled resource folders correctly', () => {
|
||||
assert.equal(platformDirName('linux'), 'linux');
|
||||
assert.equal(platformDirName('darwin'), 'mac');
|
||||
assert.equal(platformDirName('win32'), 'win');
|
||||
});
|
||||
+4
-2
@@ -3,14 +3,16 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const which = require('which');
|
||||
const getPort = (...args) => import('get-port').then(m => m.default(...args));
|
||||
const { platformDirName } = require('./runtime-utils');
|
||||
|
||||
async function launchTor() {
|
||||
const socks = await getPort();
|
||||
const control = await getPort();
|
||||
const torBase = path.join(__dirname, '..', '..', 'voxvera', 'resources', 'tor');
|
||||
let exe = path.join(torBase, process.platform,
|
||||
const platformDir = platformDirName(process.platform);
|
||||
let exe = path.join(torBase, platformDir,
|
||||
process.platform === 'win32' ? 'tor.exe' : 'tor');
|
||||
let obfs4 = path.join(torBase, process.platform,
|
||||
let obfs4 = path.join(torBase, platformDir,
|
||||
process.platform === 'win32' ? 'obfs4proxy.exe' : 'obfs4proxy');
|
||||
|
||||
const missing = p => {
|
||||
|
||||
+24
@@ -182,6 +182,23 @@ if [ -d "$HOME/imports" ] && ls "$HOME/imports"/*.json &>/dev/null; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean generated files from existing site directories so rebuild-all
|
||||
# regenerates them from the current template. Preserves:
|
||||
# config.json — site content and settings
|
||||
# .onionshare-session — Tor domain identity key
|
||||
# server.pid — running process marker
|
||||
# onionshare.log — current log (may be in use)
|
||||
# Everything else is regenerated by build_assets().
|
||||
if [ -d "$HOME/host" ]; then
|
||||
for site_dir in "$HOME"/host/*/; do
|
||||
[ -f "${site_dir}config.json" ] || continue
|
||||
msg "Cleaning generated files in ${site_dir}..."
|
||||
rm -f "${site_dir}index.html" 2>/dev/null || true
|
||||
rm -f "${site_dir}qrcode-content.png" "${site_dir}qrcode-tear-offs.png" 2>/dev/null || true
|
||||
rm -rf "${site_dir}download" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
# --- Helper: build pip flags for this distro ---
|
||||
pip_flags() {
|
||||
local flags="--user --upgrade"
|
||||
@@ -364,6 +381,13 @@ for strategy in install_tarball install_shallow install_pipx install_pip_git ins
|
||||
fi
|
||||
# Clear bash's cached command path so the new binary is found immediately
|
||||
hash -r 2>/dev/null || true
|
||||
|
||||
# Rebuild all existing sites so they pick up template/CSS changes
|
||||
if command_exists voxvera; then
|
||||
msg "Rebuilding existing sites with updated template..."
|
||||
voxvera rebuild-all 2>/dev/null || true
|
||||
fi
|
||||
|
||||
msg "\nVoxVera installed/updated successfully. Run 'voxvera check' to verify."
|
||||
# Detect if the parent shell will still have a stale hash
|
||||
msg "If 'voxvera' is not found, run: hash -r (or open a new terminal)"
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ voxvera = ["templates/**", "src/**", "resources/**", "locales/**", "resources/bi
|
||||
|
||||
[project]
|
||||
name = "voxvera"
|
||||
version = "0.3.1"
|
||||
version = "0.3.4"
|
||||
description = "VoxVera CLI utilities"
|
||||
readme = "README.md"
|
||||
authors = [{name = "VoxVera"}]
|
||||
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "==> flake8"
|
||||
python3 -m flake8 voxvera tests \
|
||||
--jobs=1 \
|
||||
--exclude=voxvera/vendor \
|
||||
--ignore=E501,W503,W293,E402,E306,E704,E261,F841,E302,F401,E401,F541
|
||||
|
||||
echo "==> pytest"
|
||||
python3 -m pytest tests/ -v
|
||||
|
||||
echo "==> electron smoke tests"
|
||||
(cd gui/electron && npm test)
|
||||
|
||||
echo "==> dependency check"
|
||||
python3 -m voxvera.cli --lang en check
|
||||
|
||||
echo "==> build docs"
|
||||
python3 -m voxvera.cli build-docs
|
||||
|
||||
echo "==> build site"
|
||||
python3 -m voxvera.cli build-site
|
||||
|
||||
echo "All local checks passed."
|
||||
Binary file not shown.
Binary file not shown.
+10
-6
@@ -169,20 +169,21 @@
|
||||
|
||||
.content {
|
||||
width: calc(8.5in - 3.75in);
|
||||
padding: 20px;
|
||||
padding: 15px 20px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
page-break-inside: avoid;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
margin-bottom: 10px;
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -192,7 +193,10 @@
|
||||
|
||||
.distribute {
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 6px;
|
||||
margin: 2px 0;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -204,8 +208,8 @@
|
||||
}
|
||||
|
||||
.message {
|
||||
height: 6in; /* Fixed height to prevent footer push-out */
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
@@ -265,7 +269,7 @@
|
||||
border: none;
|
||||
border-top: 1px solid #000;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
||||
@@ -126,6 +126,21 @@ def test_import_preserves_session(tmp_path, monkeypatch):
|
||||
assert (host_dir / "index.html").exists()
|
||||
|
||||
|
||||
def test_import_rejects_zip_path_traversal(tmp_path, monkeypatch, capsys):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
malicious_zip = tmp_path / "malicious.zip"
|
||||
with zipfile.ZipFile(malicious_zip, "w") as zf:
|
||||
zf.writestr("../escape.txt", "owned")
|
||||
zf.writestr("config.json", json.dumps({"folder_name": "evil"}))
|
||||
|
||||
cli.import_site(str(malicious_zip))
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Refusing to extract path outside destination" in captured.out
|
||||
assert not (tmp_path / "escape.txt").exists()
|
||||
assert not ((tmp_path / "data" / "host" / "evil").exists())
|
||||
|
||||
|
||||
def test_check_all_present(capsys, monkeypatch):
|
||||
monkeypatch.setattr(shutil, "which", lambda cmd: "/usr/bin/" + cmd)
|
||||
cli.main(["check"])
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.3.1"
|
||||
__version__ = "0.3.4"
|
||||
|
||||
# Add the vendor directory to sys.path ONLY for PyInstaller (frozen) builds.
|
||||
# For pip/pipx installs the system/venv packages are correct and the vendored
|
||||
|
||||
+118
-6
@@ -28,6 +28,8 @@ from InquirerPy import prompt, inquirer
|
||||
from rich.console import Console
|
||||
from voxvera import __version__
|
||||
|
||||
GITHUB_RECOMMENDED_BINARY_LIMIT = 95 * 1024 * 1024
|
||||
|
||||
# package root (contains bundled templates and src/)
|
||||
if getattr(sys, 'frozen', False):
|
||||
ROOT = Path(sys._MEIPASS).joinpath("voxvera")
|
||||
@@ -398,15 +400,15 @@ def interactive_update(config_path: str):
|
||||
data.update(prompt(meta_qs))
|
||||
|
||||
console.rule(t("cli.init_body"))
|
||||
# Start with existing/default content; preserve user edits across retries
|
||||
body = _br_to_newlines(get_default("content"))
|
||||
while True:
|
||||
# Use existing content if present, else fallback to localized landing default
|
||||
initial_content = _br_to_newlines(get_default("content"))
|
||||
|
||||
body = open_editor(initial_content)
|
||||
body = open_editor(body)
|
||||
length = len(body)
|
||||
if length > 1000:
|
||||
console.print(f"{t('cli.body_length')}: {length}/1000 {t('cli.body_limit_exceeded')}", style="red")
|
||||
if not inquirer.confirm(message=t("cli.edit_again"), default=True).execute():
|
||||
console.print(t("cli.body_over_limit_warning"), style="yellow")
|
||||
break
|
||||
else:
|
||||
console.print(f"{t('cli.body_length')}: {length}/1000", style="green")
|
||||
@@ -731,6 +733,12 @@ def serve(config_path: str) -> str | None:
|
||||
if not dir_path.is_dir():
|
||||
print(f"Directory {dir_path} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Auto-rebuild if index.html is missing (e.g. after install cleanup)
|
||||
if not (dir_path / "index.html").exists():
|
||||
print(f"Rebuilding {folder_name} from current template...")
|
||||
build_assets(config_path)
|
||||
|
||||
logfile = dir_path / "onionshare.log"
|
||||
|
||||
# Set environment variables for OnionShare to use the detected Tor ports
|
||||
@@ -847,10 +855,24 @@ def _clear_host_dir(dir_path: Path):
|
||||
session.write_bytes(saved)
|
||||
|
||||
|
||||
import base64
|
||||
import zipfile
|
||||
import tempfile
|
||||
|
||||
|
||||
def _safe_extract_zip(zipf: zipfile.ZipFile, dest_dir: Path) -> None:
|
||||
"""Extract a ZIP archive without allowing path traversal."""
|
||||
dest_dir = dest_dir.resolve()
|
||||
for member in zipf.infolist():
|
||||
member_path = Path(member.filename)
|
||||
if member_path.is_absolute():
|
||||
raise ValueError(f"Refusing to extract absolute path from archive: {member.filename}")
|
||||
target_path = (dest_dir / member_path).resolve()
|
||||
if not str(target_path).startswith(str(dest_dir) + os.sep) and target_path != dest_dir:
|
||||
raise ValueError(f"Refusing to extract path outside destination: {member.filename}")
|
||||
zipf.extractall(dest_dir)
|
||||
|
||||
|
||||
def get_export_dir() -> Path:
|
||||
"""Get the default export directory (~/voxvera-exports)."""
|
||||
export_dir = Path.home() / "voxvera-exports"
|
||||
@@ -904,7 +926,11 @@ def import_site(zip_path: str):
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with zipfile.ZipFile(zip_path, 'r') as zipf:
|
||||
zipf.extractall(tmpdir)
|
||||
try:
|
||||
_safe_extract_zip(zipf, Path(tmpdir))
|
||||
except ValueError as e:
|
||||
print(f"Error: Invalid export file. {e}")
|
||||
return
|
||||
|
||||
tmp_path = Path(tmpdir)
|
||||
config_file = tmp_path / "config.json"
|
||||
@@ -954,6 +980,58 @@ def import_multiple_sites(source_dir: str = None):
|
||||
print("\nImport multiple complete.")
|
||||
|
||||
|
||||
def export_keys(folder_name: str):
|
||||
"""Export the onion service session key as base64 for easy copy/paste."""
|
||||
console = Console()
|
||||
session_file = DATA_DIR / "host" / folder_name / ".onionshare-session"
|
||||
if not session_file.exists():
|
||||
console.print(f"[red]{t('cli.keys_not_found', name=folder_name)}[/red]")
|
||||
return
|
||||
raw = session_file.read_bytes()
|
||||
encoded = base64.b64encode(raw).decode("ascii")
|
||||
console.print(f"\n[bold]{t('cli.keys_export_header', name=folder_name)}[/bold]")
|
||||
console.print("[yellow]" + "─" * 60 + "[/yellow]")
|
||||
console.print(encoded)
|
||||
console.print("[yellow]" + "─" * 60 + "[/yellow]")
|
||||
console.print(f"[dim]{t('cli.keys_export_hint')}[/dim]\n")
|
||||
|
||||
|
||||
def import_keys(folder_name: str):
|
||||
"""Import an onion service session key from base64 pasted by the user."""
|
||||
console = Console()
|
||||
site_dir = DATA_DIR / "host" / folder_name
|
||||
if not site_dir.exists():
|
||||
console.print(f"[red]{t('cli.keys_site_not_found', name=folder_name)}[/red]")
|
||||
return
|
||||
|
||||
session_file = site_dir / ".onionshare-session"
|
||||
if session_file.exists():
|
||||
overwrite = inquirer.confirm(message=t("cli.keys_overwrite_confirm", name=folder_name), default=False).execute()
|
||||
if not overwrite:
|
||||
console.print(f"[yellow]{t('cli.keys_import_cancelled')}[/yellow]")
|
||||
return
|
||||
|
||||
console.print(f"\n[bold]{t('cli.keys_import_prompt')}[/bold]")
|
||||
try:
|
||||
raw_input = input("> ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
console.print(f"\n[yellow]{t('cli.keys_import_cancelled')}[/yellow]")
|
||||
return
|
||||
|
||||
if not raw_input:
|
||||
console.print(f"[red]{t('cli.keys_import_empty')}[/red]")
|
||||
return
|
||||
|
||||
try:
|
||||
decoded = base64.b64decode(raw_input)
|
||||
except Exception:
|
||||
console.print(f"[red]{t('cli.keys_import_invalid')}[/red]")
|
||||
return
|
||||
|
||||
session_file.write_bytes(decoded)
|
||||
console.print(f"[green]{t('cli.keys_import_success', name=folder_name)}[/green]")
|
||||
|
||||
|
||||
def build_docs():
|
||||
"""Generate localized documentation from templates and locale data."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
@@ -1126,11 +1204,15 @@ def build_site():
|
||||
# Bundle source version
|
||||
bundle_source(download_dir / "voxvera-source.zip")
|
||||
|
||||
# Copy standalone binaries if they exist
|
||||
# Copy standalone binaries if they exist, but avoid writing oversized
|
||||
# artifacts into the tracked site tree where GitHub rejects >100 MB files.
|
||||
bin_dir = ROOT / "resources" / "bin"
|
||||
if bin_dir.exists():
|
||||
for bin_file in bin_dir.iterdir():
|
||||
if bin_file.is_file():
|
||||
if bin_file.stat().st_size > GITHUB_RECOMMENDED_BINARY_LIMIT:
|
||||
print(f"Skipping oversized site download artifact: {bin_file.name}")
|
||||
continue
|
||||
shutil.copy(bin_file, download_dir / bin_file.name)
|
||||
|
||||
print("site/ directory successfully synchronized.")
|
||||
@@ -1259,6 +1341,27 @@ def _seed_bundled_sites():
|
||||
shutil.copytree(src, dest)
|
||||
|
||||
|
||||
def rebuild_all_sites():
|
||||
"""Rebuild index.html for all existing sites from the current master template.
|
||||
|
||||
Preserves config.json, .onionshare-session keys, and download/ content.
|
||||
Only regenerates the HTML and QR codes.
|
||||
"""
|
||||
servers = get_servers()
|
||||
if not servers:
|
||||
print("No sites found to rebuild.")
|
||||
return
|
||||
print(f"Rebuilding {len(servers)} site(s) from current template...")
|
||||
for name in servers:
|
||||
config_path = DATA_DIR / "host" / name / "config.json"
|
||||
try:
|
||||
build_assets(str(config_path))
|
||||
print(f" Rebuilt: {name}")
|
||||
except Exception as e:
|
||||
print(f" Failed to rebuild {name}: {e}")
|
||||
print("Rebuild complete.")
|
||||
|
||||
|
||||
def get_servers() -> list[str]:
|
||||
"""Return server names found in DATA_DIR/host only."""
|
||||
host_dir = DATA_DIR / "host"
|
||||
@@ -1597,6 +1700,8 @@ def manage_servers():
|
||||
Choice("toggle", t("cli.manage_action_toggle_stop") if running else t("cli.manage_action_toggle_start")),
|
||||
Choice("edit", t("cli.manage_action_edit")),
|
||||
Choice("export", t("cli.manage_action_export")),
|
||||
Choice("export_keys", t("cli.manage_action_export_keys")),
|
||||
Choice("import_keys", t("cli.manage_action_import_keys")),
|
||||
Choice("delete", t("cli.manage_action_delete")),
|
||||
Choice(None, t("cli.manage_action_back"))
|
||||
]
|
||||
@@ -1629,6 +1734,10 @@ def manage_servers():
|
||||
console.print("\n[yellow]Update cancelled.[/yellow]")
|
||||
elif action == "export":
|
||||
export_site(selected)
|
||||
elif action == "export_keys":
|
||||
export_keys(selected)
|
||||
elif action == "import_keys":
|
||||
import_keys(selected)
|
||||
elif action == "delete":
|
||||
confirm = inquirer.confirm(message=t("cli.manage_delete_confirm", name=selected)).execute()
|
||||
if confirm:
|
||||
@@ -1733,6 +1842,7 @@ def main(argv=None):
|
||||
sub.add_parser("start-all", help="Start all configured sites (non-interactive)")
|
||||
sub.add_parser("stop-all", help="Stop all running sites (non-interactive)")
|
||||
sub.add_parser("autostart", help="Install/remove a systemd user service to start sites on boot")
|
||||
sub.add_parser("rebuild-all", help="Rebuild all existing sites from the current template (preserves keys and config)")
|
||||
sub.add_parser("_internal_onionshare", help=argparse.SUPPRESS)
|
||||
|
||||
args, unknown_args = parser.parse_known_args(argv)
|
||||
@@ -1832,6 +1942,8 @@ def main(argv=None):
|
||||
build_docs()
|
||||
elif args.command == "build-site":
|
||||
build_site()
|
||||
elif args.command == "rebuild-all":
|
||||
rebuild_all_sites()
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=850">
|
||||
<title id="site-title">Vox Vera Printable Flyers</title>
|
||||
<title id="site-title">{{name}}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Courier New', Courier, 'Nimbus Mono L', 'Liberation Mono', 'Noto Sans Arabic', 'Noto Sans Hebrew', 'Noto Sans Devanagari', 'Noto Sans JP', 'Noto Sans SC', 'Noto Sans Arabic UI', monospace;
|
||||
@@ -169,20 +169,21 @@
|
||||
|
||||
.content {
|
||||
width: calc(8.5in - 3.75in);
|
||||
padding: 20px;
|
||||
padding: 15px 20px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
page-break-inside: avoid;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
margin-bottom: 10px;
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -192,7 +193,10 @@
|
||||
|
||||
.distribute {
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 6px;
|
||||
margin: 2px 0;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -204,8 +208,8 @@
|
||||
}
|
||||
|
||||
.message {
|
||||
height: 6in; /* Fixed height to prevent footer push-out */
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
@@ -265,7 +269,7 @@
|
||||
border: none;
|
||||
border-top: 1px solid #000;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
@@ -378,7 +382,7 @@
|
||||
</style>
|
||||
<script>
|
||||
// VoxVera Multi-Language Engine
|
||||
const locales = {"ar": {"meta": {"language_name": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", "flag": "\ud83c\uddf8\ud83c\udde6", "direction": "rtl"}, "web": {"tear_off_tor_browser": "\u0627\u0633\u062a\u062e\u062f\u0645 \u0645\u062a\u0635\u0641\u062d Tor", "tear_off_desc": "\u0644\u0644\u0645\u0633\u0627\u0639\u062f\u0629 \u0641\u064a \u0646\u0634\u0631 \u0647\u0630\u0647 \u0627\u0644\u0631\u0633\u0627\u0644\u0629\u060c \u0627\u0646\u062a\u0642\u0644 \u0625\u0644\u0649", "tear_off_footer": "\u0627\u0646\u0642\u0631 \u0641\u0648\u0642 \u0627\u0644\u0632\u0631 \u0623\u062f\u0646\u0627\u0647 \u0644\u0644\u0637\u0628\u0627\u0639\u0629", "built_with": "\u0628\u0646\u064a \u0628\u0648\u0627\u0633\u0637\u0629", "print_button": "\u0627\u0637\u0628\u0639 \u0627\u0644\u0645\u0646\u0634\u0648\u0631", "download_button": "\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0623\u062f\u0627\u0629 \u0648\u0627\u0644\u0643\u0648\u062f", "download_attachment": "Download Attachment"}, "landing": {"title": "\u0633\u0631\u064a \u0644\u0644\u063a\u0627\u064a\u0629", "subtitle": "~~\u0644\u0627~~ \u0644\u0644\u0646\u0634\u0631", "headline": "\u0639\u0645\u0644\u064a\u0629 VOX VERA", "content": "\u062a\u062d\u0631\u0631 \u0645\u0646 \u0627\u0644\u0631\u0642\u0627\u0628\u0629 \u0645\u0639 VoxVera. \u0623\u062f\u0627\u0629 \u062a\u0633\u0648\u064a\u0642 \u0639\u0635\u0627\u0628\u0627\u062a \u0645\u062c\u0647\u0648\u0644\u0629 \u0648\u0646\u0634\u0631 \u0631\u0633\u0627\u0626\u0644. \u0633\u0648\u0627\u0621 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a \u0623\u0648 \u0641\u064a \u0627\u0644\u0639\u0627\u0644\u0645 \u0627\u0644\u0645\u0627\u062f\u064a\u060c \u064a\u0645\u0643\u0646\u0647\u0627 \u062a\u0645\u0643\u064a\u0646\u0643 \u0645\u0646 \u0646\u0634\u0631 \u0623\u0641\u0643\u0627\u0631\u0643 \u0628\u062c\u0631\u0623\u0629\u060c \u0645\u062d\u0645\u064a\u0629 \u0628\u0625\u062e\u0641\u0627\u0621 \u0627\u0644\u0647\u0648\u064a\u0629 \u0627\u0644\u0643\u0627\u0645\u0644 (\u0625\u0630\u0627 \u0643\u0646\u062a \u062a\u0633\u062a\u0636\u064a\u0641 \u0639\u0628\u0631 Tor). \u0642\u0645 \u0628\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0643\u0648\u062f\u060c \u0648\u0635\u0645\u0645 \u0645\u0648\u0642\u0639 \u0645\u0646\u0634\u0648\u0631\u060c \u0648\u0627\u0633\u062a\u0636\u064a\u0641\u0647 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a \u0648\u0642\u0645 \u0628\u062a\u0636\u062e\u064a\u0645 \u0631\u0633\u0627\u0644\u062a\u0643.<br><br>\u0627\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u0642\u0648\u0629 \u0627\u0644\u0645\u064a\u0645\u064a\u0629 \u0644\u0645\u0634\u0627\u0631\u0643\u0629 \u0623\u0641\u0643\u0627\u0631\u0643 \u0641\u064a \u0645\u062f\u0631\u0633\u062a\u0643\u060c \u0623\u0648 \u0645\u0643\u0627\u0646 \u0639\u0645\u0644\u0643\u060c \u0623\u0648 \u0645\u062c\u062a\u0645\u0639\u0627\u062a\u0643 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a\u060c \u0623\u0648 \u062d\u062a\u0649 \u0639\u0644\u0649 \u0645\u0633\u062a\u0648\u0649 \u0627\u0644\u0639\u0627\u0644\u0645. \u062a\u0636\u0645\u0646 VoxVera \u062a\u0631\u062f\u062f \u0635\u062f\u0649 \u0631\u0633\u0627\u0644\u062a\u0643 \u0628\u0639\u064a\u062f\u0627\u064b \u0648\u0639\u0644\u0649 \u0646\u0637\u0627\u0642 \u0648\u0627\u0633\u0639\u060c \u0645\u0639 \u0623\u0642\u0633\u0627\u0645 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u0646\u0632\u0639 \u062a\u062a\u0645\u064a\u0632 \u0628\u0631\u0648\u0627\u0628\u0637 \u0641\u0631\u064a\u062f\u0629 \u0648\u0623\u0643\u0648\u0627\u062f QR \u0644\u0633\u0647\u0648\u0644\u0629 \u0625\u0639\u0627\u062f\u0629 \u0627\u0644\u0637\u0628\u0627\u0639\u0629.<br><br>\u064a\u0645\u0643\u0646 \u0627\u0644\u062d\u0641\u0627\u0638 \u0639\u0644\u0649 \u0627\u0644\u062e\u0635\u0648\u0635\u064a\u0629. \u064a\u0645\u0643\u0646 \u0645\u0634\u0627\u0631\u0643\u0629 \u0627\u0644\u0645\u0646\u0634\u0648\u0631\u0627\u062a \u0639\u0628\u0631 \u0634\u0628\u0643\u0629 Tor\u060c \u0645\u0645\u0627 \u064a\u062d\u0645\u064a \u0627\u0644\u0645\u0636\u064a\u0641 \u0648\u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645\u064a\u0646 \u0645\u0646 \u0627\u0644\u0631\u0642\u0627\u0628\u0629.<br><br>\u0627\u0646\u0636\u0645 \u0625\u0644\u064a\u0646\u0627 \u0641\u064a \u062b\u0648\u0631\u0629 \u062a\u0642\u062f\u0631 \u0627\u0644\u062d\u0642\u064a\u0642\u0629 \u0648\u0627\u0644\u0634\u0641\u0627\u0641\u064a\u0629. \u0645\u0639\u0627\u064b\u060c \u064a\u0645\u0643\u0646\u0646\u0627 \u0628\u0646\u0627\u0621 \u0634\u0628\u0643\u0629 \u0645\u0646 \u0627\u0644\u0645\u0648\u0627\u0637\u0646\u064a\u0646 \u0627\u0644\u0645\u0633\u062a\u0646\u064a\u0631\u064a\u0646 \u0627\u0644\u0630\u064a\u0646 \u0644\u0627 \u064a\u062e\u0634\u0648\u0646 \u0627\u0644\u062a\u062d\u062f\u062b.", "url_message": "\u0627\u062d\u0635\u0644 \u0639\u0644\u0649 VoxVera \u0648\u0627\u0628\u062f\u0623 \u0628\u0646\u0634\u0631 \u0631\u0633\u0627\u0644\u062a\u0643.", "name": "\u0645\u0646\u0634\u0648\u0631\u0627\u062a Vox Vera \u0627\u0644Line truncated
|
||||
const locales = {{locales}}; // Injected at build time
|
||||
|
||||
function t(path, lang) {
|
||||
const parts = path.split('.');
|
||||
@@ -441,10 +445,10 @@
|
||||
<div class="left-tear-offs">
|
||||
<div class="tear-off">
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
|
||||
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
|
||||
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
<img src="qrcode-tear-offs.png" alt="QR Code">
|
||||
@@ -453,10 +457,10 @@
|
||||
<!-- Repeat for other tear-offs -->
|
||||
<div class="tear-off">
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
|
||||
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
|
||||
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
<img src="qrcode-tear-offs.png" alt="QR Code">
|
||||
@@ -464,10 +468,10 @@
|
||||
</div>
|
||||
<div class="tear-off">
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
|
||||
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
|
||||
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
<img src="qrcode-tear-offs.png" alt="QR Code">
|
||||
@@ -475,10 +479,10 @@
|
||||
</div>
|
||||
<div class="tear-off">
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
|
||||
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
|
||||
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
<img src="qrcode-tear-offs.png" alt="QR Code">
|
||||
@@ -486,10 +490,10 @@
|
||||
</div>
|
||||
<div class="tear-off">
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
|
||||
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
|
||||
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
<img src="qrcode-tear-offs.png" alt="QR Code">
|
||||
@@ -497,10 +501,10 @@
|
||||
</div>
|
||||
<div class="tear-off">
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
|
||||
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
|
||||
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
<img src="qrcode-tear-offs.png" alt="QR Code">
|
||||
@@ -508,10 +512,10 @@
|
||||
</div>
|
||||
<div class="tear-off">
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
|
||||
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
|
||||
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
<img src="qrcode-tear-offs.png" alt="QR Code">
|
||||
@@ -519,10 +523,10 @@
|
||||
</div>
|
||||
<div class="tear-off">
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
|
||||
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
|
||||
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
<img src="qrcode-tear-offs.png" alt="QR Code">
|
||||
@@ -530,10 +534,10 @@
|
||||
</div>
|
||||
<div class="tear-off">
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
|
||||
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
|
||||
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
<img src="qrcode-tear-offs.png" alt="QR Code">
|
||||
@@ -541,10 +545,10 @@
|
||||
</div>
|
||||
<div class="tear-off">
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
<span data-i18n="web.tear_off_tor_browser">{{t_web_tear_off_tor_browser}}</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">{{t_web_tear_off_desc}}</span><br>
|
||||
<a class="tear-off-link" href="{{tear_off_link}}" target="_blank">{{tear_off_link}}</a><br>
|
||||
<span data-i18n="web.tear_off_footer">{{t_web_tear_off_footer}}</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
<img src="qrcode-tear-offs.png" alt="QR Code">
|
||||
@@ -552,32 +556,32 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h1 id="top-secret-title" data-i18n="landing.title">TOP SECRET</h1>
|
||||
<div class="distribute" id="do-not-distribute" data-i18n="landing.subtitle">DO <span class="redacted">NOT</span> DISTRIBUTE</div>
|
||||
<h1 id="operation-title-2" data-i18n="landing.headline">OPERATION VOX VERA</h1>
|
||||
<h1 id="top-secret-title" data-i18n="landing.title">{{t_landing_title}}</h1>
|
||||
<div class="distribute" id="do-not-distribute" data-i18n="landing.subtitle">{{t_landing_subtitle}}</div>
|
||||
<h1 id="operation-title-2" data-i18n="landing.headline">{{t_landing_headline}}</h1>
|
||||
<hr>
|
||||
<div class="message">
|
||||
<div class="text-container" id="main-content" contenteditable="false" data-i18n="landing.content">Break free from censorship with VoxVera. An anonymous guerrilla marketing and message-spreading tool. Whether online or in the physical world, it can empower you to spread your ideas boldly, shielded by complete anonymity (if you host over Tor). Download the code, design a flyer site, host it online and amplify your message.<br><br>Use memetic power to share your ideas in your school, workplace, online communities, or even globally. VoxVera ensures your message resonates far and wide, with tear-off sections featuring unique URLs and QR codes for easy reprinting.<br><br>Privacy can be maintained. Flyers can be shared via the Tor network, protecting hoster and users from censorship.<br><br>Join us in a revolution that values truth and transparency. Together, we can build a network of informed citizens who are unafraid to speak out.</div>
|
||||
<div class="text-container" id="main-content" contenteditable="false" data-i18n="landing.content">{{t_landing_content}}</div>
|
||||
<div class="qr-code-body">
|
||||
|
||||
<div class="qr-code-url">
|
||||
<span class="url-message" data-i18n="landing.url_message">Get VoxVera and start spreading your message.</span><br><br>
|
||||
<a id="qr-link-2" href="https://github.com/PR0M3TH3AN/VoxVera" target="_blank">https://github.com/PR0M3TH3AN/VoxVera</a>
|
||||
<span class="url-message" data-i18n="landing.url_message">{{t_landing_url_message}}</span><br><br>
|
||||
<a id="qr-link-2" href="{{url}}" target="_blank">{{url}}</a>
|
||||
</div>
|
||||
<img src="qrcode-content.png" alt="QR Code">
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="footer">
|
||||
<p class="credit"><span data-i18n="web.built_with">built with</span> <a href="https://github.com/PR0M3TH3AN/VoxVera">voxvera</a></p>
|
||||
<p class="binary" id="binary-message">0110010 0101011 0110010 0111101 0110100</p>
|
||||
<p class="credit"><span data-i18n="web.built_with">{{t_web_built_with}}</span> <a href="https://github.com/PR0M3TH3AN/VoxVera">voxvera</a></p>
|
||||
<p class="binary" id="binary-message">{{binary_message}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<button class="print-button" onclick="window.print()" data-i18n="web.print_button">Print this page</button>
|
||||
<button class="print-button" onclick="window.print()" data-i18n="web.print_button">{{t_web_print_button}}</button>
|
||||
<a id="download-link" href="download/voxvera-source.zip" download>
|
||||
<button class="download-button" data-i18n="web.download_button">Download Source Code</button>
|
||||
<button class="download-button" data-i18n="web.download_button">{{t_web_download_button}}</button>
|
||||
</a>
|
||||
<select class="lang-select" id="lang-selector" onchange="setLanguage(this.value)"></select>
|
||||
</div>
|
||||
|
||||
+14
-1
@@ -76,7 +76,20 @@
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_templates": "--- إنشاء من قالب ---",
|
||||
"template_select": "اختر قالباً:"
|
||||
"template_select": "اختر قالباً:",
|
||||
"body_over_limit_warning": "تحذير: نص المحتوى يتجاوز 1000 حرف. قد يتم قطعه في المنشور.",
|
||||
"manage_action_export_keys": "تصدير المفاتيح",
|
||||
"manage_action_import_keys": "استيراد المفاتيح",
|
||||
"keys_not_found": "لم يتم العثور على مفتاح جلسة لـ '{name}'. قم بتشغيل الموقع أولاً لتوليد المفاتيح.",
|
||||
"keys_export_header": "مفتاح الجلسة لـ '{name}':",
|
||||
"keys_export_hint": "انسخ هذا المفتاح واحفظه بأمان. استخدم 'استيراد المفاتيح' في نسخة أخرى للاستعادة.",
|
||||
"keys_site_not_found": "الموقع '{name}' غير موجود.",
|
||||
"keys_overwrite_confirm": "الموقع '{name}' لديه مفتاح بالفعل. هل تريد الكتابة فوقه؟",
|
||||
"keys_import_cancelled": "تم إلغاء الاستيراد.",
|
||||
"keys_import_prompt": "الصق مفتاح الجلسة base64 أدناه:",
|
||||
"keys_import_empty": "لم يتم تقديم مفتاح.",
|
||||
"keys_import_invalid": "تنسيق المفتاح غير صالح. يجب أن يكون سلسلة base64 صالحة.",
|
||||
"keys_import_success": "تم استيراد مفتاح الجلسة لـ '{name}'. أعد تشغيل الموقع لاستخدام المفتاح الجديد."
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "استخدم متصفح Tor",
|
||||
|
||||
+14
-1
@@ -75,7 +75,20 @@
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_templates": "--- Aus Vorlage erstellen ---",
|
||||
"template_select": "Wählen Sie eine Vorlage:"
|
||||
"template_select": "Wählen Sie eine Vorlage:",
|
||||
"body_over_limit_warning": "Warnung: Der Textkörper überschreitet 1000 Zeichen. Er könnte auf dem Flyer abgeschnitten werden.",
|
||||
"manage_action_export_keys": "Schlüssel exportieren",
|
||||
"manage_action_import_keys": "Schlüssel importieren",
|
||||
"keys_not_found": "Kein Sitzungsschlüssel für '{name}' gefunden. Starten Sie die Seite zuerst, um Schlüssel zu generieren.",
|
||||
"keys_export_header": "Sitzungsschlüssel für '{name}':",
|
||||
"keys_export_hint": "Kopieren Sie diesen Schlüssel und bewahren Sie ihn sicher auf. Verwenden Sie 'Schlüssel importieren' auf einer anderen Instanz zur Wiederherstellung.",
|
||||
"keys_site_not_found": "Seite '{name}' nicht gefunden.",
|
||||
"keys_overwrite_confirm": "Seite '{name}' hat bereits einen Schlüssel. Überschreiben?",
|
||||
"keys_import_cancelled": "Import abgebrochen.",
|
||||
"keys_import_prompt": "Fügen Sie den base64-Sitzungsschlüssel unten ein:",
|
||||
"keys_import_empty": "Kein Schlüssel angegeben.",
|
||||
"keys_import_invalid": "Ungültiges Schlüsselformat. Muss eine gültige base64-Zeichenkette sein.",
|
||||
"keys_import_success": "Sitzungsschlüssel für '{name}' importiert. Starten Sie die Seite neu, um den neuen Schlüssel zu verwenden."
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Tor-Browser verwenden",
|
||||
|
||||
+14
-1
@@ -75,7 +75,20 @@
|
||||
"no_urls_qr_skip": "No URLs configured yet. QR codes will be generated after 'voxvera serve'",
|
||||
"input_file_not_found": "Input file {path} does not exist.",
|
||||
"manage_templates": "--- Create From Template ---",
|
||||
"template_select": "Select a template:"
|
||||
"template_select": "Select a template:",
|
||||
"body_over_limit_warning": "Warning: Body text exceeds 1000 characters. It may be cut off on the flyer.",
|
||||
"manage_action_export_keys": "Export Keys",
|
||||
"manage_action_import_keys": "Import Keys",
|
||||
"keys_not_found": "No session key found for '{name}'. Start the site first to generate keys.",
|
||||
"keys_export_header": "Session key for '{name}':",
|
||||
"keys_export_hint": "Copy this key and save it securely. Use 'Import Keys' on another instance to restore.",
|
||||
"keys_site_not_found": "Site '{name}' not found.",
|
||||
"keys_overwrite_confirm": "Site '{name}' already has a key. Overwrite?",
|
||||
"keys_import_cancelled": "Import cancelled.",
|
||||
"keys_import_prompt": "Paste the base64 session key below:",
|
||||
"keys_import_empty": "No key provided.",
|
||||
"keys_import_invalid": "Invalid key format. Must be a valid base64 string.",
|
||||
"keys_import_success": "Session key imported for '{name}'. Restart the site to use the new key."
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Use Tor Browser",
|
||||
|
||||
+14
-1
@@ -75,7 +75,20 @@
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_templates": "--- Crear Desde Plantilla ---",
|
||||
"template_select": "Seleccione una plantilla:"
|
||||
"template_select": "Seleccione una plantilla:",
|
||||
"body_over_limit_warning": "Advertencia: El texto del cuerpo supera los 1000 caracteres. Puede cortarse en el volante.",
|
||||
"manage_action_export_keys": "Exportar Claves",
|
||||
"manage_action_import_keys": "Importar Claves",
|
||||
"keys_not_found": "No se encontró clave de sesión para '{name}'. Inicie el sitio primero para generar claves.",
|
||||
"keys_export_header": "Clave de sesión para '{name}':",
|
||||
"keys_export_hint": "Copie esta clave y guárdela de forma segura. Use 'Importar Claves' en otra instancia para restaurar.",
|
||||
"keys_site_not_found": "Sitio '{name}' no encontrado.",
|
||||
"keys_overwrite_confirm": "El sitio '{name}' ya tiene una clave. ¿Sobrescribir?",
|
||||
"keys_import_cancelled": "Importación cancelada.",
|
||||
"keys_import_prompt": "Pegue la clave de sesión base64 a continuación:",
|
||||
"keys_import_empty": "No se proporcionó clave.",
|
||||
"keys_import_invalid": "Formato de clave no válido. Debe ser una cadena base64 válida.",
|
||||
"keys_import_success": "Clave de sesión importada para '{name}'. Reinicie el sitio para usar la nueva clave."
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Use el Navegador Tor",
|
||||
|
||||
+14
-1
@@ -76,7 +76,20 @@
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_templates": "--- ایجاد از قالب ---",
|
||||
"template_select": "یک قالب انتخاب کنید:"
|
||||
"template_select": "یک قالب انتخاب کنید:",
|
||||
"body_over_limit_warning": "هشدار: متن بدنه از 1000 کاراکتر بیشتر است. ممکن است در بروشور بریده شود.",
|
||||
"manage_action_export_keys": "صادرات کلیدها",
|
||||
"manage_action_import_keys": "واردات کلیدها",
|
||||
"keys_not_found": "کلید جلسه برای '{name}' یافت نشد. ابتدا سایت را اجرا کنید تا کلیدها تولید شوند.",
|
||||
"keys_export_header": "کلید جلسه برای '{name}':",
|
||||
"keys_export_hint": "این کلید را کپی کرده و به صورت امن ذخیره کنید. از 'واردات کلیدها' در نمونه دیگر برای بازیابی استفاده کنید.",
|
||||
"keys_site_not_found": "سایت '{name}' یافت نشد.",
|
||||
"keys_overwrite_confirm": "سایت '{name}' از قبل کلید دارد. رونویسی شود؟",
|
||||
"keys_import_cancelled": "واردات لغو شد.",
|
||||
"keys_import_prompt": "کلید جلسه base64 را در زیر جایگذاری کنید:",
|
||||
"keys_import_empty": "کلیدی ارائه نشد.",
|
||||
"keys_import_invalid": "فرمت کلید نامعتبر است. باید یک رشته base64 معتبر باشد.",
|
||||
"keys_import_success": "کلید جلسه برای '{name}' وارد شد. سایت را مجدداً راهاندازی کنید تا از کلید جدید استفاده شود."
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "از مرورگر Tor استفاده کنید",
|
||||
|
||||
+14
-1
@@ -76,7 +76,20 @@
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_templates": "--- Créer à partir d'un modèle ---",
|
||||
"template_select": "Sélectionnez un modèle :"
|
||||
"template_select": "Sélectionnez un modèle :",
|
||||
"body_over_limit_warning": "Avertissement : Le texte dépasse 1000 caractères. Il pourrait être coupé sur le tract.",
|
||||
"manage_action_export_keys": "Exporter les clés",
|
||||
"manage_action_import_keys": "Importer les clés",
|
||||
"keys_not_found": "Aucune clé de session trouvée pour '{name}'. Démarrez le site d'abord pour générer les clés.",
|
||||
"keys_export_header": "Clé de session pour '{name}' :",
|
||||
"keys_export_hint": "Copiez cette clé et conservez-la en sécurité. Utilisez 'Importer les clés' sur une autre instance pour restaurer.",
|
||||
"keys_site_not_found": "Site '{name}' introuvable.",
|
||||
"keys_overwrite_confirm": "Le site '{name}' a déjà une clé. Écraser ?",
|
||||
"keys_import_cancelled": "Importation annulée.",
|
||||
"keys_import_prompt": "Collez la clé de session base64 ci-dessous :",
|
||||
"keys_import_empty": "Aucune clé fournie.",
|
||||
"keys_import_invalid": "Format de clé invalide. Doit être une chaîne base64 valide.",
|
||||
"keys_import_success": "Clé de session importée pour '{name}'. Redémarrez le site pour utiliser la nouvelle clé."
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Utilisez le navigateur Tor",
|
||||
|
||||
+14
-1
@@ -76,7 +76,20 @@
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_templates": "--- יצירה מתבנית ---",
|
||||
"template_select": "בחר תבנית:"
|
||||
"template_select": "בחר תבנית:",
|
||||
"body_over_limit_warning": "אזהרה: טקסט הגוף חורג מ-1000 תווים. הוא עלול להיחתך בעלון.",
|
||||
"manage_action_export_keys": "ייצוא מפתחות",
|
||||
"manage_action_import_keys": "ייבוא מפתחות",
|
||||
"keys_not_found": "לא נמצא מפתח הפעלה עבור '{name}'. הפעל את האתר תחילה ליצירת מפתחות.",
|
||||
"keys_export_header": "מפתח הפעלה עבור '{name}':",
|
||||
"keys_export_hint": "העתק מפתח זה ושמור אותו בבטחה. השתמש ב'ייבוא מפתחות' במופע אחר לשחזור.",
|
||||
"keys_site_not_found": "האתר '{name}' לא נמצא.",
|
||||
"keys_overwrite_confirm": "לאתר '{name}' כבר יש מפתח. לדרוס?",
|
||||
"keys_import_cancelled": "הייבוא בוטל.",
|
||||
"keys_import_prompt": "הדבק את מפתח ההפעלה base64 למטה:",
|
||||
"keys_import_empty": "לא סופק מפתח.",
|
||||
"keys_import_invalid": "פורמט מפתח לא חוקי. חייב להיות מחרוזת base64 חוקית.",
|
||||
"keys_import_success": "מפתח הפעלה יובא עבור '{name}'. הפעל מחדש את האתר כדי להשתמש במפתח החדש."
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "השתמש בדפדפן Tor",
|
||||
|
||||
+14
-1
@@ -76,7 +76,20 @@
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_templates": "--- टेम्पलेट से बनाएं ---",
|
||||
"template_select": "एक टेम्पलेट चुनें:"
|
||||
"template_select": "एक टेम्पलेट चुनें:",
|
||||
"body_over_limit_warning": "चेतावनी: मुख्य पाठ 1000 अक्षरों से अधिक है। यह पर्चे पर कटा हुआ दिख सकता है।",
|
||||
"manage_action_export_keys": "कुंजियाँ निर्यात करें",
|
||||
"manage_action_import_keys": "कुंजियाँ आयात करें",
|
||||
"keys_not_found": "'{name}' के लिए सत्र कुंजी नहीं मिली। कुंजियाँ उत्पन्न करने के लिए पहले साइट शुरू करें।",
|
||||
"keys_export_header": "'{name}' के लिए सत्र कुंजी:",
|
||||
"keys_export_hint": "इस कुंजी को कॉपी करें और सुरक्षित रूप से सहेजें। पुनर्स्थापित करने के लिए किसी अन्य इंस्टेंस पर 'कुंजियाँ आयात करें' का उपयोग करें।",
|
||||
"keys_site_not_found": "साइट '{name}' नहीं मिली।",
|
||||
"keys_overwrite_confirm": "साइट '{name}' में पहले से कुंजी है। अधिलेखित करें?",
|
||||
"keys_import_cancelled": "आयात रद्द किया गया।",
|
||||
"keys_import_prompt": "नीचे base64 सत्र कुंजी चिपकाएँ:",
|
||||
"keys_import_empty": "कोई कुंजी प्रदान नहीं की गई।",
|
||||
"keys_import_invalid": "अमान्य कुंजी प्रारूप। एक वैध base64 स्ट्रिंग होनी चाहिए।",
|
||||
"keys_import_success": "'{name}' के लिए सत्र कुंजी आयात की गई। नई कुंजी का उपयोग करने के लिए साइट को पुनः प्रारंभ करें।"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Tor Browser का उपयोग करें",
|
||||
|
||||
+14
-1
@@ -76,7 +76,20 @@
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_templates": "--- テンプレートから作成 ---",
|
||||
"template_select": "テンプレートを選択してください:"
|
||||
"template_select": "テンプレートを選択してください:",
|
||||
"body_over_limit_warning": "警告:本文が1000文字を超えています。チラシ上で途切れる可能性があります。",
|
||||
"manage_action_export_keys": "鍵をエクスポート",
|
||||
"manage_action_import_keys": "鍵をインポート",
|
||||
"keys_not_found": "'{name}' のセッションキーが見つかりません。まずサイトを起動してキーを生成してください。",
|
||||
"keys_export_header": "'{name}' のセッションキー:",
|
||||
"keys_export_hint": "このキーをコピーして安全に保管してください。別のインスタンスで「鍵をインポート」を使用して復元できます。",
|
||||
"keys_site_not_found": "サイト '{name}' が見つかりません。",
|
||||
"keys_overwrite_confirm": "サイト '{name}' にはすでにキーがあります。上書きしますか?",
|
||||
"keys_import_cancelled": "インポートがキャンセルされました。",
|
||||
"keys_import_prompt": "base64セッションキーを以下に貼り付けてください:",
|
||||
"keys_import_empty": "キーが提供されていません。",
|
||||
"keys_import_invalid": "無効なキー形式です。有効なbase64文字列である必要があります。",
|
||||
"keys_import_success": "'{name}' のセッションキーがインポートされました。新しいキーを使用するにはサイトを再起動してください。"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Tor Browserを使用してください",
|
||||
|
||||
+14
-1
@@ -76,7 +76,20 @@
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_templates": "--- Criar a partir de Modelo ---",
|
||||
"template_select": "Selecione um modelo:"
|
||||
"template_select": "Selecione um modelo:",
|
||||
"body_over_limit_warning": "Aviso: O texto do corpo excede 1000 caracteres. Pode ser cortado no panfleto.",
|
||||
"manage_action_export_keys": "Exportar Chaves",
|
||||
"manage_action_import_keys": "Importar Chaves",
|
||||
"keys_not_found": "Nenhuma chave de sessão encontrada para '{name}'. Inicie o site primeiro para gerar as chaves.",
|
||||
"keys_export_header": "Chave de sessão para '{name}':",
|
||||
"keys_export_hint": "Copie esta chave e guarde-a com segurança. Use 'Importar Chaves' em outra instância para restaurar.",
|
||||
"keys_site_not_found": "Site '{name}' não encontrado.",
|
||||
"keys_overwrite_confirm": "O site '{name}' já possui uma chave. Sobrescrever?",
|
||||
"keys_import_cancelled": "Importação cancelada.",
|
||||
"keys_import_prompt": "Cole a chave de sessão base64 abaixo:",
|
||||
"keys_import_empty": "Nenhuma chave fornecida.",
|
||||
"keys_import_invalid": "Formato de chave inválido. Deve ser uma string base64 válida.",
|
||||
"keys_import_success": "Chave de sessão importada para '{name}'. Reinicie o site para usar a nova chave."
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Use o Tor Browser",
|
||||
|
||||
+14
-1
@@ -76,7 +76,20 @@
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_templates": "--- Создать из шаблона ---",
|
||||
"template_select": "Выберите шаблон:"
|
||||
"template_select": "Выберите шаблон:",
|
||||
"body_over_limit_warning": "Предупреждение: Текст превышает 1000 символов. Он может быть обрезан на листовке.",
|
||||
"manage_action_export_keys": "Экспорт ключей",
|
||||
"manage_action_import_keys": "Импорт ключей",
|
||||
"keys_not_found": "Сессионный ключ для '{name}' не найден. Сначала запустите сайт для генерации ключей.",
|
||||
"keys_export_header": "Сессионный ключ для '{name}':",
|
||||
"keys_export_hint": "Скопируйте этот ключ и сохраните его в безопасности. Используйте 'Импорт ключей' на другом экземпляре для восстановления.",
|
||||
"keys_site_not_found": "Сайт '{name}' не найден.",
|
||||
"keys_overwrite_confirm": "У сайта '{name}' уже есть ключ. Перезаписать?",
|
||||
"keys_import_cancelled": "Импорт отменён.",
|
||||
"keys_import_prompt": "Вставьте сессионный ключ base64 ниже:",
|
||||
"keys_import_empty": "Ключ не предоставлен.",
|
||||
"keys_import_invalid": "Неверный формат ключа. Должна быть допустимая строка base64.",
|
||||
"keys_import_success": "Сессионный ключ импортирован для '{name}'. Перезапустите сайт для использования нового ключа."
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Используйте Tor Browser",
|
||||
|
||||
+14
-1
@@ -76,7 +76,20 @@
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_templates": "--- Tengeneza Kutoka Kiolezo ---",
|
||||
"template_select": "Chagua kiolezo:"
|
||||
"template_select": "Chagua kiolezo:",
|
||||
"body_over_limit_warning": "Onyo: Maandishi ya mwili yanazidi herufi 1000. Yanaweza kukatwa kwenye kipeperushi.",
|
||||
"manage_action_export_keys": "Hamisha Funguo",
|
||||
"manage_action_import_keys": "Ingiza Funguo",
|
||||
"keys_not_found": "Hakuna funguo ya kipindi iliyopatikana kwa '{name}'. Anzisha tovuti kwanza ili kuzalisha funguo.",
|
||||
"keys_export_header": "Funguo ya kipindi kwa '{name}':",
|
||||
"keys_export_hint": "Nakili funguo hii na uihifadhi kwa usalama. Tumia 'Ingiza Funguo' kwenye mfano mwingine kurejesha.",
|
||||
"keys_site_not_found": "Tovuti '{name}' haijapatikana.",
|
||||
"keys_overwrite_confirm": "Tovuti '{name}' tayari ina funguo. Andika juu yake?",
|
||||
"keys_import_cancelled": "Uingizaji umeghairiwa.",
|
||||
"keys_import_prompt": "Bandika funguo ya kipindi ya base64 hapa chini:",
|
||||
"keys_import_empty": "Hakuna funguo iliyotolewa.",
|
||||
"keys_import_invalid": "Muundo wa funguo si sahihi. Lazima iwe mfuatano halali wa base64.",
|
||||
"keys_import_success": "Funguo ya kipindi imeingizwa kwa '{name}'. Anzisha upya tovuti ili kutumia funguo mpya."
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Tumia Kivinjari cha Tor",
|
||||
|
||||
+14
-1
@@ -76,7 +76,20 @@
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_templates": "--- Şablondan Oluştur ---",
|
||||
"template_select": "Bir şablon seçin:"
|
||||
"template_select": "Bir şablon seçin:",
|
||||
"body_over_limit_warning": "Uyarı: Gövde metni 1000 karakteri aşıyor. El ilanında kesilebilir.",
|
||||
"manage_action_export_keys": "Anahtarları Dışa Aktar",
|
||||
"manage_action_import_keys": "Anahtarları İçe Aktar",
|
||||
"keys_not_found": "'{name}' için oturum anahtarı bulunamadı. Anahtarları oluşturmak için önce siteyi başlatın.",
|
||||
"keys_export_header": "'{name}' için oturum anahtarı:",
|
||||
"keys_export_hint": "Bu anahtarı kopyalayın ve güvenli bir şekilde saklayın. Geri yüklemek için başka bir örnekte 'Anahtarları İçe Aktar' kullanın.",
|
||||
"keys_site_not_found": "Site '{name}' bulunamadı.",
|
||||
"keys_overwrite_confirm": "Site '{name}' zaten bir anahtara sahip. Üzerine yazılsın mı?",
|
||||
"keys_import_cancelled": "İçe aktarma iptal edildi.",
|
||||
"keys_import_prompt": "Base64 oturum anahtarını aşağıya yapıştırın:",
|
||||
"keys_import_empty": "Anahtar sağlanmadı.",
|
||||
"keys_import_invalid": "Geçersiz anahtar biçimi. Geçerli bir base64 dizesi olmalıdır.",
|
||||
"keys_import_success": "'{name}' için oturum anahtarı içe aktarıldı. Yeni anahtarı kullanmak için siteyi yeniden başlatın."
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Tor Tarayıcı Kullanın",
|
||||
|
||||
+14
-1
@@ -76,7 +76,20 @@
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_templates": "--- 从模板创建 ---",
|
||||
"template_select": "选择一个模板:"
|
||||
"template_select": "选择一个模板:",
|
||||
"body_over_limit_warning": "警告:正文超过1000个字符。传单上可能会被截断。",
|
||||
"manage_action_export_keys": "导出密钥",
|
||||
"manage_action_import_keys": "导入密钥",
|
||||
"keys_not_found": "未找到 '{name}' 的会话密钥。请先启动站点以生成密钥。",
|
||||
"keys_export_header": "'{name}' 的会话密钥:",
|
||||
"keys_export_hint": "复制此密钥并安全保存。在另一个实例上使用\u201c导入密钥\u201d来恢复。",
|
||||
"keys_site_not_found": "未找到站点 '{name}'。",
|
||||
"keys_overwrite_confirm": "站点 '{name}' 已有密钥。是否覆盖?",
|
||||
"keys_import_cancelled": "导入已取消。",
|
||||
"keys_import_prompt": "请在下方粘贴 base64 会话密钥:",
|
||||
"keys_import_empty": "未提供密钥。",
|
||||
"keys_import_invalid": "密钥格式无效。必须是有效的 base64 字符串。",
|
||||
"keys_import_success": "已导入 '{name}' 的会话密钥。请重启站点以使用新密钥。"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "使用 Tor 浏览器",
|
||||
|
||||
@@ -169,20 +169,21 @@
|
||||
|
||||
.content {
|
||||
width: calc(8.5in - 3.75in);
|
||||
padding: 20px;
|
||||
padding: 15px 20px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
page-break-inside: avoid;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
margin-bottom: 10px;
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -192,7 +193,10 @@
|
||||
|
||||
.distribute {
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 6px;
|
||||
margin: 2px 0;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -204,8 +208,8 @@
|
||||
}
|
||||
|
||||
.message {
|
||||
height: 6in; /* Fixed height to prevent footer push-out */
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
@@ -265,7 +269,7 @@
|
||||
border: none;
|
||||
border-top: 1px solid #000;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<title id="site-title">{{name}}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-family: 'Courier New', Courier, 'Nimbus Mono L', 'Liberation Mono', 'Noto Sans Arabic', 'Noto Sans Hebrew', 'Noto Sans Devanagari', 'Noto Sans JP', 'Noto Sans SC', 'Noto Sans Arabic UI', monospace;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
@@ -19,6 +19,45 @@
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* RTL Layout Overrides */
|
||||
[dir="rtl"] .container {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
[dir="rtl"] .left-tear-offs {
|
||||
border-right: none;
|
||||
border-left: 1px dashed #000;
|
||||
}
|
||||
|
||||
[dir="rtl"] .tear-off {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
[dir="rtl"] .qr-code {
|
||||
margin-left: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .qr-code-body {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
[dir="rtl"] .qr-code-url {
|
||||
text-align: right;
|
||||
margin-right: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .message {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[dir="rtl"] .text-container {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ... existing styles ... */
|
||||
|
||||
a {
|
||||
color: #b80000;
|
||||
text-decoration: none;
|
||||
@@ -87,12 +126,14 @@
|
||||
|
||||
.tear-off {
|
||||
display: flex;
|
||||
height: 1in; /* Fixed height for consistency */
|
||||
padding: 5px;
|
||||
border: 1px dashed #000;
|
||||
font-size: 10px;
|
||||
box-sizing: border-box;
|
||||
page-break-inside: avoid;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tear-off-text {
|
||||
@@ -128,26 +169,38 @@
|
||||
|
||||
.content {
|
||||
width: calc(8.5in - 3.75in);
|
||||
padding: 20px;
|
||||
padding: 15px 20px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
page-break-inside: avoid;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
margin-bottom: 10px;
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.distribute {
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 6px;
|
||||
margin: 2px 0;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.redacted {
|
||||
@@ -156,14 +209,14 @@
|
||||
|
||||
.message {
|
||||
flex: 1;
|
||||
margin-top: 10px;
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
overflow-wrap: break-word;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
justify-content: flex-start;
|
||||
white-space: pre-wrap;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
@@ -216,7 +269,7 @@
|
||||
border: none;
|
||||
border-top: 1px solid #000;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
@@ -281,6 +334,51 @@
|
||||
padding: 20px 0 30px;
|
||||
}
|
||||
|
||||
/* Dropdown styles */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
background-color: #f9f9f9;
|
||||
min-width: 200px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
border: 1px solid #000;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
color: black;
|
||||
padding: 12px 16px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.dropdown-content a:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dropdown-content a:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.dropdown:hover .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown:hover .download-button {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
// VoxVera Multi-Language Engine
|
||||
@@ -299,9 +397,25 @@
|
||||
function setLanguage(lang) {
|
||||
if (!locales[lang]) lang = 'en';
|
||||
localStorage.setItem('voxvera_lang', lang);
|
||||
|
||||
// Set text direction
|
||||
const direction = locales[lang].meta.direction || 'ltr';
|
||||
document.documentElement.dir = direction;
|
||||
document.documentElement.lang = lang;
|
||||
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const path = el.getAttribute('data-i18n');
|
||||
el.innerText = t(path, lang);
|
||||
let translation = t(path, lang);
|
||||
if (translation) {
|
||||
// Support Markdown-style redaction ~~text~~ -> <span class="redacted">text</span>
|
||||
translation = translation.replace(/~~(.*?)~~/g, '<span class="redacted">$1</span>');
|
||||
|
||||
if (path.startsWith('landing.') || path.includes('subtitle') || path.includes('content')) {
|
||||
el.innerHTML = translation;
|
||||
} else {
|
||||
el.innerText = translation;
|
||||
}
|
||||
}
|
||||
});
|
||||
document.getElementById('lang-selector').value = lang;
|
||||
document.documentElement.lang = lang;
|
||||
@@ -449,6 +563,7 @@
|
||||
<div class="message">
|
||||
<div class="text-container" id="main-content" contenteditable="false" data-i18n="landing.content">{{t_landing_content}}</div>
|
||||
<div class="qr-code-body">
|
||||
|
||||
<div class="qr-code-url">
|
||||
<span class="url-message" data-i18n="landing.url_message">{{t_landing_url_message}}</span><br><br>
|
||||
<a id="qr-link-2" href="{{url}}" target="_blank">{{url}}</a>
|
||||
@@ -465,7 +580,9 @@
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<button class="print-button" onclick="window.print()" data-i18n="web.print_button">{{t_web_print_button}}</button>
|
||||
<a class="download-button" href="download/download.zip" download data-i18n="web.download_button">{{t_web_download_button}}</a>
|
||||
<a id="download-link" href="download/voxvera-source.zip" download>
|
||||
<button class="download-button" data-i18n="web.download_button">{{t_web_download_button}}</button>
|
||||
</a>
|
||||
<select class="lang-select" id="lang-selector" onchange="setLanguage(this.value)"></select>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
Reference in new issue
Block a user