diff --git a/webapp-maker/README.md b/webapp-maker/README.md new file mode 100644 index 00000000..510d3694 --- /dev/null +++ b/webapp-maker/README.md @@ -0,0 +1,94 @@ +# Webapp Maker + +Turn websites into desktop applications: fill in a name and a URL, and get a +launcher entry with the site's own icon that opens in a dedicated browser app +window — no tabs, no URL bar. The same panel lists every web app it created, +with one-click removal. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `umedbazarov/webapp-maker` | +| Entries | Panel: `panel` | + +## Requirements + +- A chromium-family browser (Chromium, Chrome, Brave, Vivaldi, Edge, …) for + the app windows — that engine family is the only one with an `--app` mode. + If your default browser is one of them it is used; otherwise the first + installed one is picked. Firefox alone is not enough. +- `bash`, `chmod`, `cp`, `curl`, `file`, `grep`, `head`, `mkdir`, `rm`, + `sed`, `setsid`, `tr` and `xdg-settings`, required — used by the bundled + scripts to fetch the site icon, write the `.desktop` entry, resolve the + default browser and launch the app window. +- `gtk-update-icon-cache`, `update-desktop-database` and `notify-send`, + optional — icon-cache/database refresh and the fallback "no browser found" + notification; each is skipped when absent. + +## Usage + +The plugin has no bar widget. Open the panel from the plugin's row in +Settings, or bind it in your compositor: + +```sh +noctalia msg panel-toggle umedbazarov/webapp-maker:panel +``` + +Fill in the form: + +- **Name** and **URL** are required (`https://` is added automatically). +- **Chromium launch flags** (optional) are stored in the launcher and passed + to the browser on every start — e.g. `--proxy-server=http://127.0.0.1:1080` + to route one site through a proxy, or `--incognito`. +- **Icon** (optional): empty fetches the site's own icon (apple-touch-icon, + then the well-known path, then a favicon service); or give an image URL, a + local file path, or the name of an installed theme icon. + +**Create** writes `~/.local/share/applications/.desktop` and installs +the icon into the hicolor theme; the app immediately appears in Noctalia's +launcher and any other application menu. The **Installed** section at the +bottom lists every web app this mechanism created (and only those — regular +applications are never touched); the trash button removes the launcher and +its icon. + +The `.desktop` entries execute a launch script the plugin copies into its +own data directory, so launchers keep working across plugin updates. If the +plugin is uninstalled, already-created web apps keep working too; removing +them afterwards is a matter of deleting their `.desktop` files. + +## Settings + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `env_file` | `string` | *(empty)* | A file sourced before the install command — e.g. one exporting `HTTPS_PROXY` so site icons download through a proxy. Empty disables it. | + +## Notes + +- **Commands spawned.** Creating: the bundled `scripts/webapp-install.sh` + (`curl` for the site page and icon — the only network access, `file` to + verify the download is an image, `sed`/`tr`/`grep` for parsing and + freedesktop escaping). Listing: `grep -l` over + `~/.local/share/applications/*.desktop`. Removing: the bundled + `scripts/webapp-remove.sh` (`rm` of the entry and icon). Launching (from + the created `.desktop`, not from the panel): `scripts/webapp-launch.sh` — + `xdg-settings` to resolve the default browser, then `setsid + --app=` plus the stored flags. +- **Files written.** `~/.local/share/applications/.desktop`, the icon + under `~/.local/share/icons/hicolor/256x256/apps/`, and a copy of the + launch script in the plugin's data directory. Values written into + `.desktop` files are escaped per the freedesktop spec (both string and + Exec quoting), and app names may not contain `/`. +- **No privileges.** Everything runs as the user; nothing touches system + configuration. +- Removal only ever deletes launchers whose `Exec` runs the plugin's launch + script, so a name clash with a real application cannot delete it. + +## Credits + +Inspired by Omarchy's `omarchy-webapp-install` tooling (MIT), rebuilt as a +self-contained Noctalia panel plugin. + +## License + +MIT. diff --git a/webapp-maker/panel.luau b/webapp-maker/panel.luau new file mode 100644 index 00000000..47853e47 --- /dev/null +++ b/webapp-maker/panel.luau @@ -0,0 +1,297 @@ +--!nonstrict +-- Webapp Maker: a floating centered panel - a form that creates web-app +-- launchers, plus the list of installed web apps with one-click removal. +-- No bar widget; open the panel via IPC (bind it in your compositor): +-- noctalia msg panel-open umedbazarov/webapp-maker:panel +-- +-- All real work happens in the bundled shell scripts (scripts/) through +-- noctalia.runAsync - the panel handlers only assemble commands, keeping +-- well inside the plugin CPU budget. webapp-launch.sh is copied into the +-- plugin's data directory on open, and .desktop entries reference that +-- stable copy, so launchers keep working across plugin updates. +-- +-- ui.input is uncontrolled: value seeds once per formRev, edits flow +-- through named global onChange handlers into the `form` table. + +local form = { name = "", url = "", flags = "", icon = "" } +local formRev = 0 +local busy = false +local status = nil -- { ok = boolean, text = string } + +local apps = {} -- installed web-app names (basename of the .desktop) +local deleting = {} -- name -> true while its removal runs + +local render + +local function tr(key, args) + return noctalia.tr(key, args) +end + +local function cfg(key, fallback) + -- getConfig returns nil until the setting is materialized in + -- settings.toml; the manifest default is NOT substituted by the runtime. + local value = noctalia.getConfig(key) + if value == nil then + return fallback + end + return value +end + +local function trim(value) + return (tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")) +end + +local function shellQuote(value) + return "'" .. tostring(value):gsub("'", "'\\''") .. "'" +end + +local function normalizeUrl(url) + if url ~= "" and url:match("^%a[%w+.%-]*:") == nil then + return "https://" .. url + end + return url +end + +local function scriptPath(name) + return (noctalia.pluginDir() or ".") .. "/scripts/" .. name +end + +-- The stable copy of the launch script that .desktop entries point at: the +-- data directory survives plugin updates, the materialized code dir may not. +local function launcherPath() + local dir = noctalia.pluginDataDir() + return dir ~= nil and (dir .. "/webapp-launch") or nil +end + +local launcherReady = false + +local function ensureLauncher() + local target = launcherPath() + if target == nil then + return + end + noctalia.runAsync("cp -f " .. shellQuote(scriptPath("webapp-launch.sh")) .. " " .. shellQuote(target) + .. " && chmod +x " .. shellQuote(target), function(result) + launcherReady = (result.exitCode or 1) == 0 + end) +end + +-- Optional environment file sourced before install/launch commands (the +-- env_file setting) - e.g. to export HTTPS_PROXY for icon downloads. +local function envPrefix() + local envFile = trim(tostring(cfg("env_file", ""))) + if envFile == "" then + return "" + end + return ". " .. shellQuote(envFile) .. " 2>/dev/null; " +end + +-- ── Installed list ────────────────────────────────────────────────────────── + +local function refreshApps() + local cmd = "grep -l '^Exec=.*webapp-launch' ~/.local/share/applications/*.desktop 2>/dev/null | tr '\\r' '\\n'" + noctalia.runAsync(cmd, function(result) + apps = {} + for line in (result.stdout or ""):gmatch("[^\n]+") do + local name = line:match("([^/]+)%.desktop%s*$") + if name ~= nil then + table.insert(apps, name) + end + end + table.sort(apps) + deleting = {} + render() + end) +end + +local function removeApp(name) + if deleting[name] == true then + return + end + deleting[name] = true + render() + noctalia.runAsync("bash " .. shellQuote(scriptPath("webapp-remove.sh")) .. " " .. shellQuote(name) .. " 2>&1", function(result) + if (result.exitCode or 1) ~= 0 then + local out = trim(result.stdout) + status = { ok = false, text = out ~= "" and out or tr("err_remove_failed") } + else + status = { ok = true, text = tr("ok_removed", { name = name }) } + end + refreshApps() + end) +end + +-- ── Field handlers (uncontrolled inputs accumulate into `form`) ───────────── + +function onFld_name(v) form.name = tostring(v or "") end +function onFld_url(v) form.url = tostring(v or "") end +function onFld_flags(v) form.flags = tostring(v or "") end +function onFld_icon(v) form.icon = tostring(v or "") end + +-- ── Creating ──────────────────────────────────────────────────────────────── + +local function create() + if busy then + return + end + + local name = trim(form.name) + local url = normalizeUrl(trim(form.url)) + if name == "" or url == "" then + status = { ok = false, text = tr("err_required") } + render() + return + end + if name:find("/", 1, true) ~= nil then + status = { ok = false, text = tr("err_slash") } + render() + return + end + + busy = true + status = nil + render() + + local launcher = launcherPath() + if launcher == nil then + busy = false + status = { ok = false, text = tr("err_failed") } + render() + return + end + if not launcherReady then + -- The copy from onOpen may still be in flight on a very fresh open; + -- fire it again so the .desktop never points at a missing file. + ensureLauncher() + end + + local args = { "--launcher " .. shellQuote(launcher) } + local flags = trim(form.flags) + if flags ~= "" then + table.insert(args, "--flags " .. shellQuote(flags)) + end + table.insert(args, shellQuote(name)) + table.insert(args, shellQuote(url)) + if trim(form.icon) ~= "" then + table.insert(args, shellQuote(trim(form.icon))) + end + + -- The script's output is a single error line at most, so 2>&1 is safe + -- for the status label. env_file (optional) may export HTTPS_PROXY etc. + -- for the icon download. + local cmd = "{ " .. envPrefix() .. "bash " .. shellQuote(scriptPath("webapp-install.sh")) .. " " + .. table.concat(args, " ") .. "; } 2>&1" + + noctalia.runAsync(cmd, function(result) + busy = false + if result.timedOut then + status = { ok = false, text = tr("err_timeout") } + elseif (result.exitCode or 1) ~= 0 then + local out = trim(result.stdout) + if #out > 200 then + out = out:sub(-200) + end + status = { ok = false, text = out ~= "" and out or tr("err_failed") } + else + status = { ok = true, text = tr("ok_created", { name = name }) } + -- A clean form for the next app; bumping formRev resets the + -- inputs' identity, or the uncontrolled fields keep the old text. + form = { name = "", url = "", flags = "", icon = "" } + formRev += 1 + end + refreshApps() + end) +end + +-- ── Render ────────────────────────────────────────────────────────────────── + +local function field(key, labelKey, phKey) + return ui.column({ gap = 2, align = "stretch" }, { + ui.label({ text = tr(labelKey), fontSize = 11, color = "on_surface_variant" }), + ui.input({ + key = "fld-" .. key .. "-" .. formRev, + value = form[key], + placeholder = tr(phKey), + onChange = "onFld_" .. key, + }), + }) +end + +render = function() + local children = { + ui.row({ gap = 8, align = "center" }, { + ui.glyph({ name = "world-plus", size = 18, color = "primary" }), + ui.label({ text = tr("title"), fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }), + ui.button({ + glyph = "close", variant = "ghost", tooltip = tr("tip_close"), + onClick = function() + panel.close() + end, + }), + }), + field("name", "field_name", "field_name_ph"), + field("url", "field_url", "field_url_ph"), + field("flags", "field_flags", "field_flags_ph"), + field("icon", "field_icon", "field_icon_ph"), + } + + if busy then + table.insert(children, ui.label({ text = tr("status_busy"), fontSize = 11, color = "secondary" })) + elseif status ~= nil then + table.insert(children, ui.label({ + text = status.text, + fontSize = 11, + color = status.ok and "primary" or "error", + maxLines = 3, + })) + end + + table.insert(children, ui.button({ + key = "create" .. (busy and "-off" or ""), + text = tr("btn_create"), + enabled = not busy, + onClick = function() + create() + end, + })) + + table.insert(children, ui.separator({})) + table.insert(children, ui.label({ text = tr("installed_title"), fontSize = 11, color = "on_surface_variant" })) + + if #apps == 0 then + table.insert(children, ui.label({ text = tr("installed_empty"), fontSize = 12, color = "on_surface_variant" })) + table.insert(children, ui.spacer({ flexGrow = 1 })) + else + local rows = {} + for _, name in ipairs(apps) do + table.insert(rows, ui.row({ key = "app-" .. name, gap = 8, align = "center" }, { + ui.label({ text = name, fontSize = 12, color = "on_surface", flexGrow = 1, maxLines = 1 }), + ui.button({ + key = "del-" .. name .. (deleting[name] and "-off" or ""), + glyph = "trash", + variant = "ghost", + enabled = deleting[name] ~= true, + tooltip = tr("tip_remove"), + onClick = function() + removeApp(name) + end, + }), + })) + end + table.insert(children, ui.scroll({ key = "apps", flexGrow = 1, gap = 4 }, rows)) + end + + panel.render(ui.column({ flexGrow = 1, gap = 10, align = "stretch" }, children)) +end + +function onOpen(_context) + -- A fresh form on every open: the panel is summoned to make a new web + -- app, so previous input is not worth keeping. + form = { name = "", url = "", flags = "", icon = "" } + busy = false + status = nil + formRev += 1 + render() + ensureLauncher() + refreshApps() +end diff --git a/webapp-maker/plugin.toml b/webapp-maker/plugin.toml new file mode 100644 index 00000000..923d5d97 --- /dev/null +++ b/webapp-maker/plugin.toml @@ -0,0 +1,26 @@ +id = "umedbazarov/webapp-maker" +name = "Webapp Maker" +version = "1.0.0" +plugin_api = 9 +author = "umedbazarov" +license = "MIT" +icon = "world-plus" +description = "Turn websites into desktop apps: a launcher entry with the site's icon, opening in its own browser app window." +dependencies = ["bash", "chmod", "cp", "curl", "file", "grep", "head", "mkdir", "rm", "sed", "setsid", "tr", "xdg-settings"] +tags = ["panel", "utility"] + +[[setting]] +key = "env_file" +type = "string" +label_key = "settings.env_file.label" +description_key = "settings.env_file.description" +default = "" +advanced = true + +[[panel]] +id = "panel" +entry = "panel.luau" +width = 440 +height = 560 +placement = "floating" +position = "center" diff --git a/webapp-maker/scripts/webapp-install.sh b/webapp-maker/scripts/webapp-install.sh new file mode 100755 index 00000000..cb30673c --- /dev/null +++ b/webapp-maker/scripts/webapp-install.sh @@ -0,0 +1,173 @@ +#!/bin/bash + +# Create a desktop launcher for a web app: fetch the site's icon, write a +# .desktop entry whose Exec opens the URL as an app window via the launcher +# script this plugin installed into its data directory. +# +# Usage: webapp-install.sh --launcher [--flags ""] [icon-url-or-file] +# +# Non-interactive by design: the Noctalia panel is the UI. Exit non-zero with +# a one-line message on stderr/stdout when something is wrong. + +set -e + +ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps" +DESKTOP_DIR="$HOME/.local/share/applications" + +LAUNCHER="" +EXTRA_FLAGS="" +args=() +while (($#)); do + case "$1" in + --launcher) + LAUNCHER=${2:?--launcher needs a value} + shift 2 + ;; + --flags) + EXTRA_FLAGS=${2:?--flags needs a value} + shift 2 + ;; + *) + args+=("$1") + shift + ;; + esac +done +set -- "${args[@]}" + +APP_NAME="${1:-}" +APP_URL="${2:-}" +ICON_REF="${3:-}" + +if [[ -z $LAUNCHER || -z $APP_NAME || -z $APP_URL ]]; then + echo "usage: webapp-install.sh --launcher [--flags \"...\"] [icon]" >&2 + exit 1 +fi + +safe_icon_name() { + printf '%s\n' "$1" \ + | tr '[:upper:]' '[:lower:]' \ + | sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//' +} + +# The name becomes a filename. A slash would turn it into directory levels, +# so the launcher lands somewhere webapp-remove cannot address; a leading +# ../ leaves the applications directory altogether. Most often it is a URL +# typed into the name field. +if [[ $APP_NAME == */* ]]; then + echo "App name cannot contain '/': $APP_NAME" + exit 1 +fi + +# Chromium's --app= treats javascript:, file:, and data: as a document to +# run. Prefix schemeless input with https, then refuse anything not http(s). +if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then + APP_URL="https://$APP_URL" +fi +if [[ $APP_URL =~ [[:space:]] ]]; then + echo "Error: web app URL must not contain whitespace." >&2 + exit 1 +fi +if [[ ! ${APP_URL,,} =~ ^https?:// ]]; then + echo "Error: web app URL must be http or https." >&2 + exit 1 +fi + +download_icon() { + curl -fsSL --max-time 10 -o "$2" "$1" 2>/dev/null && + [[ -s $2 && $(file -b --mime-type "$2") == image/* ]] +} + +# Prefer the site's own high-res icon (apple-touch-icon is typically 180px+), +# then the well-known path, then a favicon service as a last resort. +fetch_site_icon() { + local site_url="$1" dest="$2" + local origin page icon_url + origin=$(sed -E 's|^(https?://[^/]+).*|\1|' <<<"$site_url") + + page=$(curl -fsSL --max-time 5 "$site_url" 2>/dev/null | head -c 100000 | tr '\n' ' ') + icon_url=$(grep -oiE "]*rel=[\"'][^\"']*apple-touch-icon[^\"']*[\"'][^>]*>" <<<"$page" | + grep -oiE "href=[\"'][^\"']+" | head -1 | sed -E "s/^href=[\"']//") + + case $icon_url in + http://* | https://*) ;; + //*) icon_url="https:$icon_url" ;; + /*) icon_url="$origin$icon_url" ;; + ?*) icon_url="$origin/$icon_url" ;; + esac + + { [[ -n $icon_url ]] && download_icon "$icon_url" "$dest"; } || + download_icon "$origin/apple-touch-icon.png" "$dest" || + download_icon "https://www.google.com/s2/favicons?domain=${site_url}&sz=256" "$dest" +} + +mkdir -p "$ICON_DIR" +ICON_VALUE=$(safe_icon_name "$APP_NAME") +if [[ -z $ICON_REF ]]; then + if ! fetch_site_icon "$APP_URL" "$ICON_DIR/$ICON_VALUE.png"; then + echo "Error: could not download an icon for $APP_URL." + exit 1 + fi +elif [[ $ICON_REF =~ ^https?:// ]]; then + if ! download_icon "$ICON_REF" "$ICON_DIR/$ICON_VALUE.png"; then + echo "Error: could not download the icon." + exit 1 + fi +elif [[ -f $ICON_REF ]]; then + cp "$ICON_REF" "$ICON_DIR/$ICON_VALUE.png" +else + # The name of an icon already installed in the system theme. + ICON_VALUE=$ICON_REF +fi +command -v gtk-update-icon-cache >/dev/null && gtk-update-icon-cache "$HOME/.local/share/icons/hicolor" >/dev/null 2>&1 || true + +desktop_string_escape() { + # Desktop Entry "string" value (freedesktop spec): a raw newline would + # start a new key line and let a value inject a second Exec=. Escape + # backslash first, then tab/CR/LF and a leading space. + local value="$1" + value=${value//\\/\\\\} + value=${value//$'\t'/\\t} + value=${value//$'\r'/\\r} + value=${value//$'\n'/\\n} + [[ $value == " "* ]] && value="\\s${value# }" + printf '%s' "$value" +} + +desktop_exec_arg() { + # One Exec argument, double-quoted per the freedesktop Exec spec: inside + # quotes " ` $ \ take a backslash and a literal % becomes %%. + local escaped + escaped=$(printf '%s' "$1" \ + | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/`/\\`/g' -e 's/\$/\\$/g' -e 's/%/%%/g') + printf '"%s"' "$escaped" +} + +EXEC_COMMAND="$(desktop_exec_arg "$LAUNCHER") $(desktop_exec_arg "$APP_URL")" +# Flags split on whitespace; each becomes its own quoted Exec argument. +for flag in $EXTRA_FLAGS; do + EXEC_COMMAND+=" $(desktop_exec_arg "$flag")" +done + +mkdir -p "$DESKTOP_DIR" +DESKTOP_FILE="$DESKTOP_DIR/$APP_NAME.desktop" + +name_field=$(desktop_string_escape "$APP_NAME") +exec_field=$(desktop_string_escape "$EXEC_COMMAND") +icon_field=$(desktop_string_escape "$ICON_VALUE") + +cat >"$DESKTOP_FILE" </dev/null && update-desktop-database "$DESKTOP_DIR" >/dev/null 2>&1 || true +exit 0 diff --git a/webapp-maker/scripts/webapp-launch.sh b/webapp-maker/scripts/webapp-launch.sh new file mode 100755 index 00000000..5386edc3 --- /dev/null +++ b/webapp-maker/scripts/webapp-launch.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Launch a URL as a web-app window (no tabs, no URL bar). +# Only chromium-family browsers have an --app mode; when the default browser +# is not one of them (e.g. Firefox), the first installed chromium-family +# browser is used instead. Extra arguments are passed to the browser as-is. + +url=${1:?usage: webapp-launch [browser flags...]} +shift + +browser=$(xdg-settings get default-web-browser 2>/dev/null) +case $browser in +chromium* | chrome* | google-chrome* | brave* | microsoft-edge* | opera* | vivaldi* | helium*) ;; +*) browser="" ;; +esac + +exec_bin="" +if [[ -n $browser ]]; then + exec_bin=$(sed -n 's/^Exec=\([^ ]*\).*/\1/p' \ + "$HOME/.local/share/applications/$browser" "/usr/share/applications/$browser" 2>/dev/null | head -1) +fi +if [[ -z $exec_bin ]]; then + for candidate in chromium google-chrome-stable google-chrome brave vivaldi microsoft-edge-stable; do + if command -v "$candidate" >/dev/null; then + exec_bin=$candidate + break + fi + done +fi +if [[ -z $exec_bin ]]; then + command -v notify-send >/dev/null && notify-send "Web app" "No chromium-family browser found for app windows" + exit 1 +fi + +exec setsid "$exec_bin" --app="$url" "$@" diff --git a/webapp-maker/scripts/webapp-remove.sh b/webapp-maker/scripts/webapp-remove.sh new file mode 100755 index 00000000..4cf39b0c --- /dev/null +++ b/webapp-maker/scripts/webapp-remove.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Remove a web-app launcher created by webapp-install.sh: its .desktop entry +# and the icon it installed. Only launchers whose Exec runs webapp-launch are +# touched, so a name clash with a regular application cannot delete it. + +set -e + +name=${1:?usage: webapp-remove } + +ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps" +DESKTOP_DIR="$HOME/.local/share/applications" + +desktop_file="$DESKTOP_DIR/$name.desktop" +if [[ ! -f $desktop_file ]] || ! grep -q '^Exec=.*webapp-launch' "$desktop_file"; then + echo "Not a web app: $name" >&2 + exit 1 +fi + +icon_name=$(printf '%s\n' "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//') +rm -f "$desktop_file" "$ICON_DIR/$icon_name.png" "$ICON_DIR/$name.png" + +command -v update-desktop-database >/dev/null && update-desktop-database "$DESKTOP_DIR" >/dev/null 2>&1 || true +exit 0 diff --git a/webapp-maker/thumbnail.webp b/webapp-maker/thumbnail.webp new file mode 100644 index 00000000..bb8b3073 Binary files /dev/null and b/webapp-maker/thumbnail.webp differ diff --git a/webapp-maker/translations/en.json b/webapp-maker/translations/en.json new file mode 100644 index 00000000..b3650248 --- /dev/null +++ b/webapp-maker/translations/en.json @@ -0,0 +1,30 @@ +{ + "btn_create": "Create", + "err_failed": "webapp-install failed, see the Noctalia log", + "err_remove_failed": "Removal failed, see the Noctalia log", + "err_required": "Name and URL are required", + "err_slash": "Name cannot contain \"/\"", + "err_timeout": "webapp-install timed out", + "field_flags": "Chromium launch flags (optional)", + "field_flags_ph": "--proxy-server=http://127.0.0.1:1080 --incognito …", + "field_icon": "Icon: URL or file path (empty = fetch from site)", + "field_icon_ph": "https://…/icon.png or /path/icon.png", + "field_name": "Name", + "field_name_ph": "My web app", + "field_url": "URL", + "field_url_ph": "example.com (https is added automatically)", + "installed_empty": "Nothing created yet", + "installed_title": "Installed", + "ok_created": "\"{name}\" created — it is in the launcher now", + "ok_removed": "\"{name}\" removed", + "settings": { + "env_file": { + "label": "Environment file", + "description": "Sourced before the install and launch commands — e.g. a file exporting HTTPS_PROXY so site icons download through a proxy. Empty = none." + } + }, + "status_busy": "Creating — fetching the icon…", + "tip_close": "Close", + "tip_remove": "Remove the launcher and icon", + "title": "New web app" +} diff --git a/webapp-maker/translations/ru.json b/webapp-maker/translations/ru.json new file mode 100644 index 00000000..b9db7979 --- /dev/null +++ b/webapp-maker/translations/ru.json @@ -0,0 +1,30 @@ +{ + "btn_create": "Создать", + "err_failed": "webapp-install завершился с ошибкой, подробности в логе Noctalia", + "err_remove_failed": "Не удалось удалить, подробности в логе Noctalia", + "err_required": "Название и URL обязательны", + "err_slash": "В названии нельзя «/»", + "err_timeout": "webapp-install не уложился в таймаут", + "field_flags": "Параметры запуска chromium (опционально)", + "field_flags_ph": "--proxy-server=http://127.0.0.1:1080 --incognito …", + "field_icon": "Иконка: URL или путь (пусто = с сайта)", + "field_icon_ph": "https://…/icon.png или /путь/icon.png", + "field_name": "Название", + "field_name_ph": "Мой webapp", + "field_url": "URL", + "field_url_ph": "example.com (https допишется сам)", + "installed_empty": "Пока ничего не создано", + "installed_title": "Установленные", + "ok_created": "«{name}» создано — уже в лаунчере", + "ok_removed": "«{name}» удалено", + "settings": { + "env_file": { + "label": "Файл окружения", + "description": "Сорсится перед командами создания/запуска — например, файл с export HTTPS_PROXY, чтобы иконки сайтов качались через прокси. Пусто = не нужен." + } + }, + "status_busy": "Создание — скачивается иконка…", + "tip_close": "Закрыть", + "tip_remove": "Удалить ярлык и иконку", + "title": "Новое веб-приложение" +}