diff --git a/TODO-css-improvements.md b/TODO-css-improvements.md index 90de97f..f741250 100644 --- a/TODO-css-improvements.md +++ b/TODO-css-improvements.md @@ -13,10 +13,11 @@ ## Donation / Open-Source Banner -- [ ] Add a persistent but dismissible banner or notification in the dashboard UI informing users that the project is open-source and under active development, and that donations are highly appreciated -- [ ] Include a link to the donation page (GitHub Sponsors, Ko-fi, or similar — decide on platform) -- [ ] Remember dismissal in localStorage so it doesn't reappear every session -- [ ] Add i18n keys for the banner text (`en.json`, `ru.json`, `zh.json`) +- [x] Add a persistent but dismissible banner or notification in the dashboard UI informing users that the project is open-source and under active development, and that donations are highly appreciated +- [x] Include a link to the donation page (GitHub Sponsors, Ko-fi, or similar — decide on platform) +- [x] Remember dismissal in localStorage so it doesn't reappear every session +- [x] Add i18n keys for the banner text (`en.json`, `ru.json`, `zh.json`) +- [ ] Configure `DONATE_URL` and `REPO_URL` constants in `donation.ts` once platform is chosen --- diff --git a/server/src/wled_controller/__init__.py b/server/src/wled_controller/__init__.py index d40a6a8..36ff477 100644 --- a/server/src/wled_controller/__init__.py +++ b/server/src/wled_controller/__init__.py @@ -10,3 +10,9 @@ except PackageNotFoundError: __author__ = "Alexei Dolgolyov" __email__ = "dolgolyov.alexei@gmail.com" + +# ─── Project links ─────────────────────────────────────────── +GITEA_BASE_URL = "https://git.dolgolyov-family.by" +GITEA_REPO = "alexei.dolgolyov/wled-screen-controller-mixed" +REPO_URL = f"{GITEA_BASE_URL}/{GITEA_REPO}" +DONATE_URL = "" # TODO: set once donation platform is chosen diff --git a/server/src/wled_controller/api/routes/system.py b/server/src/wled_controller/api/routes/system.py index c8e11ad..c195882 100644 --- a/server/src/wled_controller/api/routes/system.py +++ b/server/src/wled_controller/api/routes/system.py @@ -13,7 +13,7 @@ from typing import Optional import psutil from fastapi import APIRouter, Depends, HTTPException, Query -from wled_controller import __version__ +from wled_controller import __version__, REPO_URL, DONATE_URL from wled_controller.api.auth import AuthRequired, is_auth_enabled from wled_controller.api.dependencies import ( get_audio_source_store, @@ -54,7 +54,11 @@ logger = get_logger(__name__) psutil.cpu_percent(interval=None) # GPU monitoring (initialized once in utils.gpu, shared with metrics_history) -from wled_controller.utils.gpu import nvml_available as _nvml_available, nvml as _nvml, nvml_handle as _nvml_handle # noqa: E402 +from wled_controller.utils.gpu import ( # noqa: E402 + nvml_available as _nvml_available, + nvml as _nvml, + nvml_handle as _nvml_handle, +) def _get_cpu_name() -> str | None: @@ -77,9 +81,7 @@ def _get_cpu_name() -> str | None: return line.split(":")[1].strip() elif platform.system() == "Darwin": return ( - subprocess.check_output( - ["sysctl", "-n", "machdep.cpu.brand_string"] - ) + subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"]) .decode() .strip() ) @@ -107,6 +109,8 @@ async def health_check(): version=__version__, demo_mode=get_config().demo, auth_required=is_auth_enabled(), + repo_url=REPO_URL, + donate_url=DONATE_URL, ) @@ -131,12 +135,22 @@ async def list_all_tags(_: AuthRequired): """Get all tags used across all entities.""" all_tags: set[str] = set() from wled_controller.api.dependencies import get_asset_store + store_getters = [ - get_device_store, get_output_target_store, get_color_strip_store, - get_picture_source_store, get_audio_source_store, get_value_source_store, - get_sync_clock_store, get_automation_store, get_scene_preset_store, - get_template_store, get_audio_template_store, get_pp_template_store, - get_pattern_template_store, get_asset_store, + get_device_store, + get_output_target_store, + get_color_strip_store, + get_picture_source_store, + get_audio_source_store, + get_value_source_store, + get_sync_clock_store, + get_automation_store, + get_scene_preset_store, + get_template_store, + get_audio_template_store, + get_pp_template_store, + get_pattern_template_store, + get_asset_store, ] for getter in store_getters: try: @@ -209,15 +223,11 @@ async def get_displays( except EntityNotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) - except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Failed to get displays: %s", e, exc_info=True) - raise HTTPException( - status_code=500, - detail="Internal server error" - ) + raise HTTPException(status_code=500, detail="Internal server error") @router.get("/api/v1/system/processes", response_model=ProcessListResponse, tags=["Config"]) @@ -235,10 +245,7 @@ async def get_running_processes(_: AuthRequired): return ProcessListResponse(processes=sorted_procs, count=len(sorted_procs)) except Exception as e: logger.error("Failed to get processes: %s", e, exc_info=True) - raise HTTPException( - status_code=500, - detail="Internal server error" - ) + raise HTTPException(status_code=500, detail="Internal server error") @router.get( @@ -260,9 +267,7 @@ def get_system_performance(_: AuthRequired): try: util = _nvml.nvmlDeviceGetUtilizationRates(_nvml_handle) mem_info = _nvml.nvmlDeviceGetMemoryInfo(_nvml_handle) - temp = _nvml.nvmlDeviceGetTemperature( - _nvml_handle, _nvml.NVML_TEMPERATURE_GPU - ) + temp = _nvml.nvmlDeviceGetTemperature(_nvml_handle, _nvml.NVML_TEMPERATURE_GPU) gpu = GpuInfo( name=_nvml.nvmlDeviceGetName(_nvml_handle), utilization=float(util.gpu), diff --git a/server/src/wled_controller/api/schemas/system.py b/server/src/wled_controller/api/schemas/system.py index c8260be..75d5f2c 100644 --- a/server/src/wled_controller/api/schemas/system.py +++ b/server/src/wled_controller/api/schemas/system.py @@ -13,7 +13,11 @@ class HealthResponse(BaseModel): timestamp: datetime = Field(description="Current server time") version: str = Field(description="Application version") demo_mode: bool = Field(default=False, description="Whether demo mode is active") - auth_required: bool = Field(default=True, description="Whether API key authentication is required") + auth_required: bool = Field( + default=True, description="Whether API key authentication is required" + ) + repo_url: str = Field(default="", description="Source code repository URL") + donate_url: str = Field(default="", description="Donation page URL") class VersionResponse(BaseModel): @@ -84,6 +88,7 @@ class RestoreResponse(BaseModel): # ─── Auto-backup schemas ────────────────────────────────────── + class AutoBackupSettings(BaseModel): """Settings for automatic backup.""" @@ -119,6 +124,7 @@ class BackupListResponse(BaseModel): # ─── MQTT schemas ────────────────────────────────────────────── + class MQTTSettingsResponse(BaseModel): """MQTT broker settings response (password is masked).""" @@ -138,17 +144,22 @@ class MQTTSettingsRequest(BaseModel): broker_host: str = Field(description="MQTT broker hostname or IP") broker_port: int = Field(ge=1, le=65535, description="MQTT broker port") username: str = Field(default="", description="MQTT username (empty = anonymous)") - password: str = Field(default="", description="MQTT password (empty = keep existing if omitted)") + password: str = Field( + default="", description="MQTT password (empty = keep existing if omitted)" + ) client_id: str = Field(default="ledgrab", description="MQTT client ID") base_topic: str = Field(default="ledgrab", description="Base topic prefix") # ─── External URL schema ─────────────────────────────────────── + class ExternalUrlResponse(BaseModel): """External URL setting response.""" - external_url: str = Field(description="External base URL (e.g. https://myserver.example.com:8080). Empty = use auto-detected URL.") + external_url: str = Field( + description="External base URL (e.g. https://myserver.example.com:8080). Empty = use auto-detected URL." + ) class ExternalUrlRequest(BaseModel): @@ -159,10 +170,13 @@ class ExternalUrlRequest(BaseModel): # ─── Log level schemas ───────────────────────────────────────── + class LogLevelResponse(BaseModel): """Current log level response.""" - level: str = Field(description="Current effective log level name (e.g. DEBUG, INFO, WARNING, ERROR, CRITICAL)") + level: str = Field( + description="Current effective log level name (e.g. DEBUG, INFO, WARNING, ERROR, CRITICAL)" + ) class LogLevelRequest(BaseModel): diff --git a/server/src/wled_controller/main.py b/server/src/wled_controller/main.py index 52d53e8..b02eba0 100644 --- a/server/src/wled_controller/main.py +++ b/server/src/wled_controller/main.py @@ -12,11 +12,14 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from starlette.requests import Request -from wled_controller import __version__ +from wled_controller import __version__, GITEA_BASE_URL, GITEA_REPO from wled_controller.api import router from wled_controller.api.dependencies import init_dependencies from wled_controller.config import get_config -from wled_controller.core.processing.processor_manager import ProcessorDependencies, ProcessorManager +from wled_controller.core.processing.processor_manager import ( + ProcessorDependencies, + ProcessorManager, +) from wled_controller.storage import DeviceStore from wled_controller.storage.template_store import TemplateStore from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore @@ -31,7 +34,9 @@ from wled_controller.storage.value_source_store import ValueSourceStore from wled_controller.storage.automation_store import AutomationStore from wled_controller.storage.scene_preset_store import ScenePresetStore from wled_controller.storage.sync_clock_store import SyncClockStore -from wled_controller.storage.color_strip_processing_template_store import ColorStripProcessingTemplateStore +from wled_controller.storage.color_strip_processing_template_store import ( + ColorStripProcessingTemplateStore, +) from wled_controller.storage.gradient_store import GradientStore from wled_controller.storage.weather_source_store import WeatherSourceStore from wled_controller.storage.asset_store import AssetStore @@ -61,6 +66,7 @@ db = Database(config.storage.database_file) # Seed demo data after DB is ready (first-run only) if config.demo: from wled_controller.core.demo_seed import seed_demo_data + seed_demo_data(db) # Initialize storage and processing @@ -148,7 +154,8 @@ async def lifespan(app: FastAPI): # Create automation engine (needs processor_manager + mqtt_service + stores for scene activation) automation_engine = AutomationEngine( - automation_store, processor_manager, + automation_store, + processor_manager, mqtt_service=mqtt_service, scene_preset_store=scene_preset_store, target_store=output_target_store, @@ -165,8 +172,8 @@ async def lifespan(app: FastAPI): # Create update service (checks for new releases) _release_provider = GiteaReleaseProvider( - base_url="https://git.dolgolyov-family.by", - repo="alexei.dolgolyov/wled-screen-controller-mixed", + base_url=GITEA_BASE_URL, + repo=GITEA_REPO, ) update_service = UpdateService( provider=_release_provider, @@ -177,7 +184,9 @@ async def lifespan(app: FastAPI): # Initialize API dependencies init_dependencies( - device_store, template_store, processor_manager, + device_store, + template_store, + processor_manager, database=db, pp_template_store=pp_template_store, pattern_template_store=pattern_template_store, @@ -309,6 +318,7 @@ async def lifespan(app: FastAPI): except Exception as e: logger.error(f"Error stopping MQTT service: {e}") + # Create FastAPI application app = FastAPI( title="LED Grab", @@ -363,6 +373,7 @@ async def _no_cache_static(request: Request, call_next): return response return await call_next(request) + # Mount static files static_path = Path(__file__).parent / "static" if static_path.exists(): diff --git a/server/src/wled_controller/static/css/layout.css b/server/src/wled_controller/static/css/layout.css index 742fd48..de69aaf 100644 --- a/server/src/wled_controller/static/css/layout.css +++ b/server/src/wled_controller/static/css/layout.css @@ -158,6 +158,58 @@ h2 { background: var(--border-color); } +/* ── Donation banner ── */ +.donation-banner { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 6px 16px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + color: var(--text-color); + font-size: 0.85rem; + animation: bannerSlideDown 0.3s var(--ease-out); +} + +.donation-banner-text { + display: flex; + align-items: center; + gap: 6px; + color: var(--text-secondary); +} + +.donation-banner-text .icon { + width: 14px; + height: 14px; + color: #e25555; + flex-shrink: 0; +} + +.donation-banner-action { + padding: 4px; + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + border-radius: var(--radius-sm); + transition: color 0.15s, background 0.15s; +} + +.donation-banner-action:hover { + color: var(--primary-color); + background: var(--border-color); +} + +.donation-banner-donate { + color: #e25555; +} + +.donation-banner-donate:hover { + color: #ff6b6b; + background: rgba(226, 85, 85, 0.1); +} + @keyframes bannerSlideDown { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } diff --git a/server/src/wled_controller/static/css/modal.css b/server/src/wled_controller/static/css/modal.css index cb397a5..9a58810 100644 --- a/server/src/wled_controller/static/css/modal.css +++ b/server/src/wled_controller/static/css/modal.css @@ -352,21 +352,23 @@ .settings-tab-bar { display: flex; + justify-content: center; gap: 0; border-bottom: 2px solid var(--border-color); - padding: 0 1.25rem; + padding: 0 0.75rem; } .settings-tab-btn { background: none; border: none; - padding: 8px 16px; - font-size: 0.9rem; + padding: 8px 12px; + font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; + white-space: nowrap; transition: color 0.2s ease, border-color 0.25s ease; } @@ -388,6 +390,102 @@ animation: tabFadeIn 0.25s ease-out; } +/* ── About panel ──────────────────────────────────────────── */ +.about-section { + text-align: center; + padding: 8px 0 4px; +} + +.about-logo { + margin-bottom: 8px; +} + +.about-logo .icon { + width: 36px; + height: 36px; + color: var(--primary-color); +} + +.about-title { + margin: 0 0 2px; + font-size: 1.1rem; + color: var(--text-color); +} + +.about-version { + display: inline-block; + margin-bottom: 8px; + padding: 2px 10px; + border-radius: 10px; + background: var(--bg-tertiary); + color: var(--text-secondary); + font-size: 0.8rem; + font-family: var(--font-mono, monospace); +} + +.about-text { + margin: 0 0 12px; + color: var(--text-secondary); + font-size: 0.85rem; + line-height: 1.4; +} + +.about-license { + margin: 10px 0 0; + color: var(--text-secondary); + font-size: 0.8rem; + opacity: 0.7; +} + +.about-links { + display: flex; + flex-direction: column; + gap: 8px; + max-width: 280px; + margin: 0 auto; +} + +.about-link { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + border-radius: var(--radius); + background: var(--bg-secondary); + border: 1px solid var(--border-color); + color: var(--text-color); + text-decoration: none; + font-size: 0.9rem; + transition: border-color 0.15s, background 0.15s; +} + +.about-link:hover { + border-color: var(--primary-color); + background: var(--bg-tertiary); +} + +.about-link .icon { + width: 18px; + height: 18px; + flex-shrink: 0; +} + +.about-link .icon:last-child { + width: 14px; + height: 14px; + margin-left: auto; + color: var(--text-secondary); +} + +.about-link span { + flex: 1; + text-align: left; +} + +.about-link-donate .icon:first-child { + color: #e25555; +} + /* ── Log viewer overlay (full-screen) ──────────────────────── */ .log-overlay { diff --git a/server/src/wled_controller/static/js/app.ts b/server/src/wled_controller/static/js/app.ts index 0850306..3fd4d09 100644 --- a/server/src/wled_controller/static/js/app.ts +++ b/server/src/wled_controller/static/js/app.ts @@ -8,7 +8,7 @@ import { Modal } from './core/modal.ts'; import { queryEl } from './core/dom-utils.ts'; // Layer 1: api, i18n -import { loadServerInfo, loadDisplays, configureApiKey, startConnectionMonitor, stopConnectionMonitor } from './core/api.ts'; +import { loadServerInfo, loadDisplays, configureApiKey, startConnectionMonitor, stopConnectionMonitor, serverRepoUrl, serverDonateUrl } from './core/api.ts'; import { t, initLocale, changeLocale } from './core/i18n.ts'; // Layer 1.5: visual effects @@ -205,6 +205,9 @@ import { initUpdateSettingsPanel, applyUpdate, openReleaseNotes, closeReleaseNotes, } from './features/update.ts'; +import { + initDonationBanner, dismissDonation, snoozeDonation, renderAboutPanel, setProjectUrls, +} from './features/donation.ts'; // ─── Register all HTML onclick / onchange / onfocus globals ─── @@ -576,6 +579,11 @@ Object.assign(window, { openReleaseNotes, closeReleaseNotes, + // donation + dismissDonation, + snoozeDonation, + renderAboutPanel, + // appearance applyStylePreset, applyBgEffect, @@ -723,6 +731,10 @@ document.addEventListener('DOMContentLoaded', async () => { initUpdateListener(); loadUpdateStatus(); + // Show donation banner (after a few sessions) + setProjectUrls(serverRepoUrl, serverDonateUrl); + initDonationBanner(); + // Show getting-started tutorial on first visit if (!localStorage.getItem('tour_completed')) { setTimeout(() => startGettingStartedTutorial(), 600); diff --git a/server/src/wled_controller/static/js/core/api.ts b/server/src/wled_controller/static/js/core/api.ts index 44e1e99..5b051f1 100644 --- a/server/src/wled_controller/static/js/core/api.ts +++ b/server/src/wled_controller/static/js/core/api.ts @@ -236,6 +236,8 @@ function _setConnectionState(online: boolean) { } export let demoMode = false; +export let serverRepoUrl = ''; +export let serverDonateUrl = ''; export async function loadServerInfo() { try { @@ -259,6 +261,10 @@ export async function loadServerInfo() { setAuthRequired(authNeeded); (window as any)._authRequired = authNeeded; + // Project URLs (repo, donate) + if (data.repo_url) serverRepoUrl = data.repo_url; + if (data.donate_url) serverDonateUrl = data.donate_url; + // Demo mode detection if (data.demo_mode && !demoMode) { demoMode = true; diff --git a/server/src/wled_controller/static/js/core/icon-paths.ts b/server/src/wled_controller/static/js/core/icon-paths.ts index 0b1706e..1252d30 100644 --- a/server/src/wled_controller/static/js/core/icon-paths.ts +++ b/server/src/wled_controller/static/js/core/icon-paths.ts @@ -87,3 +87,5 @@ export const xIcon = ''; export const fileUp = ''; export const fileAudio = ''; export const packageIcon = ''; +export const heart = ''; +export const github = ''; diff --git a/server/src/wled_controller/static/js/core/icons.ts b/server/src/wled_controller/static/js/core/icons.ts index f4cdd9c..51c90dd 100644 --- a/server/src/wled_controller/static/js/core/icons.ts +++ b/server/src/wled_controller/static/js/core/icons.ts @@ -189,6 +189,8 @@ export const ICON_X = _svg(P.xIcon); export const ICON_FILE_UP = _svg(P.fileUp); export const ICON_FILE_AUDIO = _svg(P.fileAudio); export const ICON_ASSET = _svg(P.packageIcon); +export const ICON_HEART = _svg(P.heart); +export const ICON_GITHUB = _svg(P.github); /** Asset type → icon (fallback: file) */ export function getAssetTypeIcon(assetType: string): string { diff --git a/server/src/wled_controller/static/js/features/donation.ts b/server/src/wled_controller/static/js/features/donation.ts new file mode 100644 index 0000000..cfd7cef --- /dev/null +++ b/server/src/wled_controller/static/js/features/donation.ts @@ -0,0 +1,143 @@ +/** + * Donation banner — shows a dismissible open-source/donation notice + * after the user has had a few sessions with the app. + */ + +import { t } from '../core/i18n.ts'; +import { ICON_HEART, ICON_EXTERNAL_LINK, ICON_X, ICON_GITHUB } from '../core/icons.ts'; + +// ─── Config ───────────────────────────────────────────────── + +/** URLs are set from the server /health response via setProjectUrls(). */ +let _donateUrl = ''; +let _repoUrl = ''; + +/** Minimum number of app opens before showing the banner. */ +const MIN_SESSIONS = 3; + +/** "Remind me later" snooze duration (30 days). */ +const SNOOZE_MS = 30 * 24 * 60 * 60 * 1000; + +// ─── localStorage keys ────────────────────────────────────── + +const LS_DISMISSED = 'donation-banner-dismissed'; +const LS_SNOOZE = 'donation-banner-snooze-until'; +const LS_SESSIONS = 'donation-banner-sessions'; + +// ─── Public API ───────────────────────────────────────────── + +/** Set project URLs from server health response. Call before initDonationBanner. */ +export function setProjectUrls(repoUrl: string, donateUrl: string): void { + _repoUrl = repoUrl || ''; + _donateUrl = donateUrl || ''; +} + +/** Call once on app init. Increments session count and shows banner if conditions met. */ +export function initDonationBanner(): void { + // No URLs configured — nothing to show + if (!_donateUrl && !_repoUrl) return; + + const sessions = (parseInt(localStorage.getItem(LS_SESSIONS) || '0', 10) || 0) + 1; + localStorage.setItem(LS_SESSIONS, String(sessions)); + + if (sessions < MIN_SESSIONS) return; + if (localStorage.getItem(LS_DISMISSED) === '1') return; + + const snoozeUntil = parseInt(localStorage.getItem(LS_SNOOZE) || '0', 10) || 0; + if (snoozeUntil > Date.now()) return; + + _showBanner(); +} + +/** Dismiss forever. */ +export function dismissDonation(): void { + localStorage.setItem(LS_DISMISSED, '1'); + _hideBanner(); +} + +/** Snooze for 30 days. */ +export function snoozeDonation(): void { + localStorage.setItem(LS_SNOOZE, String(Date.now() + SNOOZE_MS)); + _hideBanner(); +} + +/** Render the About panel content in settings modal. */ +export function renderAboutPanel(): void { + const container = document.getElementById('about-panel-content'); + if (!container) return; + + const version = document.getElementById('version-number')?.textContent || ''; + + let links = ''; + + if (_repoUrl) { + links += ` + ${ICON_GITHUB} + ${t('donation.view_source')} + ${ICON_EXTERNAL_LINK} + `; + } + + if (_donateUrl) { + links += ` + ${ICON_HEART} + ${t('donation.about_donate')} + ${ICON_EXTERNAL_LINK} + `; + } + + container.innerHTML = ` +
+ +

${t('donation.about_title')}

+ ${version ? `${version}` : ''} +

${t('donation.about_opensource')}

+ ${links ? `` : ''} +

${t('donation.about_license')}

+
+ `; +} + +// ─── Internal ─────────────────────────────────────────────── + +function _showBanner(): void { + const banner = document.getElementById('donation-banner'); + if (!banner) return; + + let actions = ''; + + if (_donateUrl) { + actions += ` + ${ICON_HEART} + `; + } + + if (_repoUrl) { + actions += ` + ${ICON_GITHUB} + `; + } + + actions += ``; + + banner.innerHTML = ` + + ${ICON_HEART} + ${t('donation.message')} + + ${actions} + `; + banner.style.display = 'flex'; +} + +function _hideBanner(): void { + const banner = document.getElementById('donation-banner'); + if (banner) banner.style.display = 'none'; +} diff --git a/server/src/wled_controller/static/js/features/settings.ts b/server/src/wled_controller/static/js/features/settings.ts index 90b2ec1..c2c491b 100644 --- a/server/src/wled_controller/static/js/features/settings.ts +++ b/server/src/wled_controller/static/js/features/settings.ts @@ -81,6 +81,10 @@ export function switchSettingsTab(tabId: string): void { (window as any).initUpdateSettingsPanel(); (window as any).loadUpdateSettings(); } + // Lazy-render the about panel content + if (tabId === 'about' && typeof (window as any).renderAboutPanel === 'function') { + (window as any).renderAboutPanel(); + } } // ─── Log Viewer ──────────────────────────────────────────── diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 69714b4..8254b31 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -1940,6 +1940,7 @@ "appearance.bg.applied": "Background effect applied", "settings.tab.updates": "Updates", + "settings.tab.about": "About", "update.status_label": "Update Status", "update.current_version": "Current version:", "update.badge_tooltip": "New version available — click for details", @@ -2009,5 +2010,15 @@ "asset.type.video": "Video", "asset.type.other": "Other", "streams.group.assets": "Assets", - "section.empty.assets": "No assets yet. Click + to upload one." + "section.empty.assets": "No assets yet. Click + to upload one.", + + "donation.message": "LedGrab is free & open-source. If it's useful to you, consider supporting development.", + "donation.support": "Support the project", + "donation.view_source": "View source code", + "donation.later": "Remind me later", + "donation.dismiss": "Don't show again", + "donation.about_title": "About LedGrab", + "donation.about_opensource": "LedGrab is open-source software, free to use and modify.", + "donation.about_donate": "Support development", + "donation.about_license": "MIT License" } \ No newline at end of file diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 52be816..2a8e9e5 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -1869,6 +1869,7 @@ "appearance.bg.applied": "Фоновый эффект применён", "settings.tab.updates": "Обновления", + "settings.tab.about": "О программе", "update.status_label": "Статус обновления", "update.current_version": "Текущая версия:", "update.badge_tooltip": "Доступна новая версия — нажмите для подробностей", @@ -1938,5 +1939,15 @@ "asset.type.video": "Видео", "asset.type.other": "Другое", "streams.group.assets": "Ресурсы", - "section.empty.assets": "Ресурсов пока нет. Нажмите +, чтобы загрузить." + "section.empty.assets": "Ресурсов пока нет. Нажмите +, чтобы загрузить.", + + "donation.message": "LedGrab — бесплатный проект с открытым кодом. Если он вам полезен, поддержите разработку.", + "donation.support": "Поддержать проект", + "donation.view_source": "Исходный код", + "donation.later": "Напомнить позже", + "donation.dismiss": "Больше не показывать", + "donation.about_title": "О LedGrab", + "donation.about_opensource": "LedGrab — программа с открытым исходным кодом, бесплатная для использования и модификации.", + "donation.about_donate": "Поддержать разработку", + "donation.about_license": "Лицензия MIT" } \ No newline at end of file diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index 15555b7..0bb6d36 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -1867,6 +1867,7 @@ "appearance.bg.applied": "背景效果已应用", "settings.tab.updates": "更新", + "settings.tab.about": "关于", "update.status_label": "更新状态", "update.current_version": "当前版本:", "update.badge_tooltip": "有新版本可用 — 点击查看详情", @@ -1936,5 +1937,15 @@ "asset.type.video": "视频", "asset.type.other": "其他", "streams.group.assets": "资源", - "section.empty.assets": "暂无资源。点击 + 上传一个。" + "section.empty.assets": "暂无资源。点击 + 上传一个。", + + "donation.message": "LedGrab 是免费开源软件。如果它对您有帮助,请考虑支持开发。", + "donation.support": "支持项目", + "donation.view_source": "查看源代码", + "donation.later": "稍后提醒", + "donation.dismiss": "不再显示", + "donation.about_title": "关于 LedGrab", + "donation.about_opensource": "LedGrab 是开源软件,可免费使用和修改。", + "donation.about_donate": "支持开发", + "donation.about_license": "MIT 许可证" } \ No newline at end of file diff --git a/server/src/wled_controller/templates/index.html b/server/src/wled_controller/templates/index.html index 36cee3f..8a55a7c 100644 --- a/server/src/wled_controller/templates/index.html +++ b/server/src/wled_controller/templates/index.html @@ -105,6 +105,7 @@ +
@@ -170,7 +171,7 @@

Created by Alexei Dolgolyovdolgolyov.alexei@gmail.com - • Source Code + • About

diff --git a/server/src/wled_controller/templates/modals/settings.html b/server/src/wled_controller/templates/modals/settings.html index 7bcca36..4723fc2 100644 --- a/server/src/wled_controller/templates/modals/settings.html +++ b/server/src/wled_controller/templates/modals/settings.html @@ -13,6 +13,7 @@ +
@@ -282,11 +283,13 @@
+ +
+
+
+ -