mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
i18n batch 1/6: wire device-detail + settings (~242 keys)
- device-detail.js: tabs, draft banner, layout selector, info cards, uptime timeline, controls, remote tab, playlist items, copy/assign modals, all toasts and confirms - settings.js: account, change password, license, user management, white-label, server info, setup guide, your data export/import, language selector, about - es/fr/de/pt all at 425/425 key parity; hi skeleton untouched - Native review still recommended before publicizing as fully supported Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
8e7a093150
commit
eccf4b7af1
|
|
@ -194,4 +194,250 @@ export default {
|
||||||
'content.toast.folder_deleted': 'Ordner gelöscht',
|
'content.toast.folder_deleted': 'Ordner gelöscht',
|
||||||
'content.toast.moved': 'Verschoben',
|
'content.toast.moved': 'Verschoben',
|
||||||
'content.toast.moved_to_root': 'In den Hauptordner verschoben',
|
'content.toast.moved_to_root': 'In den Hauptordner verschoben',
|
||||||
|
|
||||||
|
// Device detail
|
||||||
|
'device.back': 'Zurück zu Bildschirmen',
|
||||||
|
'device.owner_label': 'Besitzer: {owner}',
|
||||||
|
'device.rename': 'Umbenennen',
|
||||||
|
'device.screenshot_btn': 'Screenshot',
|
||||||
|
'device.remove': 'Entfernen',
|
||||||
|
'device.click_to_confirm': 'Erneut klicken zum Bestätigen',
|
||||||
|
'device.prompt_new_name': 'Neuen Namen eingeben:',
|
||||||
|
'device.confirm_discard_draft': 'Alle nicht veröffentlichten Änderungen verwerfen und zur letzten veröffentlichten Version zurückkehren?',
|
||||||
|
'device.failed_load': 'Gerät konnte nicht geladen werden',
|
||||||
|
'device.no_screenshot': 'Kein Screenshot verfügbar. Klicken Sie auf „Screenshot", um einen aufzunehmen.',
|
||||||
|
'device.no_content_assigned': 'Kein Inhalt zugewiesen',
|
||||||
|
'device.now_playing_id': 'Wiedergabe: {id}',
|
||||||
|
'device.playlist_count_one': '1 Element in der Playlist',
|
||||||
|
'device.playlist_count_other': '{n} Elemente in der Playlist',
|
||||||
|
'device.tab.now_playing': 'Aktuelle Wiedergabe',
|
||||||
|
'device.tab.now_playing_tip': 'Live-Screenshot dessen, was aktuell auf diesem Gerät angezeigt wird.',
|
||||||
|
'device.tab.playlist': 'Playlist',
|
||||||
|
'device.tab.playlist_tip': 'Diesem Gerät zugewiesener Inhalt. Ziehen zum Sortieren. Medien, Widgets oder Kioskseiten hinzufügen.',
|
||||||
|
'device.tab.info': 'Geräteinfos',
|
||||||
|
'device.tab.info_tip': 'Hardware-Telemetrie, Ausrichtung, Notizen und Gerätesteuerung.',
|
||||||
|
'device.tab.remote': 'Fernsteuerung',
|
||||||
|
'device.tab.remote_tip': 'Bildschirm in Echtzeit anzeigen und Tastendrücke senden. Funktioniert mit Android-APK und Web-Player.',
|
||||||
|
'device.draft.banner_title': 'Unveröffentlichte Änderungen',
|
||||||
|
'device.draft.devices_showing_published': 'Geräte zeigen weiterhin die zuletzt veröffentlichte Version.',
|
||||||
|
'device.draft.never_published': 'Diese Playlist wurde noch nie veröffentlicht. Geräte zeigen nichts an, bis Sie veröffentlichen.',
|
||||||
|
'device.draft.discard': 'Verwerfen',
|
||||||
|
'device.draft.publish': 'Veröffentlichen',
|
||||||
|
'device.draft.publishing': 'Wird veröffentlicht...',
|
||||||
|
'device.layout.label': 'Bildschirm-Layout',
|
||||||
|
'device.layout.fullscreen_default': 'Vollbild (Standard)',
|
||||||
|
'device.layout.zones_count': '{name} ({n} Zonen)',
|
||||||
|
'device.layout.template_zones_count': '[Vorlage] {name} ({n} Zonen)',
|
||||||
|
'device.layout.apply': 'Anwenden',
|
||||||
|
'device.playlist.label': 'Playlist',
|
||||||
|
'device.playlist.no_playlist': 'Keine Playlist',
|
||||||
|
'device.playlist.copy_to_btn': 'Kopieren nach...',
|
||||||
|
'device.playlist.add_content_btn': 'Inhalt hinzufügen',
|
||||||
|
'device.playlist.empty_title': 'Kein Inhalt zugewiesen',
|
||||||
|
'device.playlist.empty_desc': 'Fügen Sie Inhalt aus Ihrer Bibliothek zur Playlist dieses Bildschirms hinzu.',
|
||||||
|
'device.playlist_picker.with_count': '{name} — {n} Elemente',
|
||||||
|
'device.playlist_picker.with_auto': '{name} (auto) — {n} Elemente',
|
||||||
|
'device.info.status': 'Status',
|
||||||
|
'device.info.ip_address': 'IP-Adresse',
|
||||||
|
'device.info.battery': 'Akku',
|
||||||
|
'device.info.storage': 'Speicher',
|
||||||
|
'device.info.size_free': '{size} frei',
|
||||||
|
'device.info.player_type': 'Player-Typ',
|
||||||
|
'device.info.web_player': 'Web-Player',
|
||||||
|
'device.info.wifi': 'WLAN',
|
||||||
|
'device.info.uptime': 'Betriebszeit',
|
||||||
|
'device.info.android_version': 'Android-Version',
|
||||||
|
'device.info.app_version': 'App-Version',
|
||||||
|
'device.info.screen_resolution': 'Bildschirmauflösung',
|
||||||
|
'device.info.ram': 'RAM',
|
||||||
|
'device.info.cpu_usage': 'CPU-Auslastung',
|
||||||
|
'device.timeline.title': 'Verfügbarkeit (letzte 24 Stunden)',
|
||||||
|
'device.timeline.h24_ago': 'vor 24h',
|
||||||
|
'device.timeline.now': 'Jetzt',
|
||||||
|
'device.timeline.online': 'Online',
|
||||||
|
'device.timeline.offline': 'Offline',
|
||||||
|
'device.timeline.no_data': 'Keine Daten',
|
||||||
|
'device.timeline.uptime_pct_tracked': '{pct}% Verfügbarkeit ({n}min erfasst)',
|
||||||
|
'device.timeline.uptime_pct_no_data': '{pct}% Verfügbarkeit (keine Daten)',
|
||||||
|
'device.form.orientation_label': 'Ausrichtung / Drehung',
|
||||||
|
'device.form.orientation.landscape': 'Querformat (0°)',
|
||||||
|
'device.form.orientation.portrait': 'Hochformat (90° im UZS)',
|
||||||
|
'device.form.orientation.landscape_flipped': 'Querformat gedreht (180°)',
|
||||||
|
'device.form.orientation.portrait_flipped': 'Hochformat gedreht (270° im UZS)',
|
||||||
|
'device.form.default_content_label': 'Standardinhalt',
|
||||||
|
'device.form.default_content_none': 'Keiner ("Warten..." anzeigen)',
|
||||||
|
'device.form.notes_label': 'Notizen',
|
||||||
|
'device.form.notes_placeholder': 'Standort, Setup-Details usw.',
|
||||||
|
'device.form.save_settings': 'Einstellungen speichern',
|
||||||
|
'device.ctl.reboot_device': 'Gerät neu starten',
|
||||||
|
'device.ctl.screen_off': 'Bildschirm aus',
|
||||||
|
'device.ctl.screen_on': 'Bildschirm an',
|
||||||
|
'device.ctl.launch_player': 'Player starten',
|
||||||
|
'device.ctl.force_update': 'Update erzwingen',
|
||||||
|
'device.ctl.shutdown': 'Herunterfahren',
|
||||||
|
'device.remote.start_prompt': 'Auf „Fernsteuerung starten" klicken zum Beginnen',
|
||||||
|
'device.remote.start': 'Fernsteuerung starten',
|
||||||
|
'device.remote.stop': 'Fernsteuerung beenden',
|
||||||
|
'device.remote.vol_up': 'Vol +',
|
||||||
|
'device.remote.vol_down': 'Vol -',
|
||||||
|
'device.remote.home': 'Start',
|
||||||
|
'device.remote.back': 'Zurück',
|
||||||
|
'device.remote.recents': 'Zuletzt',
|
||||||
|
'device.remote.power': 'Power',
|
||||||
|
'device.remote.ok': 'OK',
|
||||||
|
'device.remote.settings': 'Einstellungen',
|
||||||
|
'device.remote.scrn_off': 'Bs. aus',
|
||||||
|
'device.remote.scrn_on': 'Bs. an',
|
||||||
|
'device.remote.enable_system_view': 'Systemansicht aktivieren',
|
||||||
|
'device.remote.system_view_tooltip': 'Fordert den Gerätenutzer auf, Vollbildaufnahme zu erlauben - aktiviert Fernansicht von Startbildschirm, Einstellungen und anderen Apps',
|
||||||
|
'device.remote.system_view_hint': 'Einmalige Genehmigung am Gerät erforderlich',
|
||||||
|
'device.remote.waiting_for_approval': 'Warte auf Genehmigung am Gerät...',
|
||||||
|
'device.remote.system_view_enabled': 'Systemansicht aktiviert',
|
||||||
|
'device.remote.unlocked_hint': 'Navigation und Systemsteuerung freigeschaltet',
|
||||||
|
'device.pl_item.widget_with_type': 'Widget ({type})',
|
||||||
|
'device.pl_item.youtube': 'YouTube',
|
||||||
|
'device.pl_item.video': 'Video',
|
||||||
|
'device.pl_item.image': 'Bild',
|
||||||
|
'device.pl_item.zone_label': 'Zone: {id}',
|
||||||
|
'device.pl_item.no_zone': 'Keine Zone',
|
||||||
|
'device.pl_item.mute': 'Stummschalten',
|
||||||
|
'device.pl_item.unmute': 'Stummschaltung aufheben',
|
||||||
|
'device.pl_item.remove': 'Entfernen',
|
||||||
|
'device.copy.no_other_devices': 'Keine anderen Geräte zum Kopieren',
|
||||||
|
'device.copy.prompt': 'Playlist auf welches Gerät kopieren?\n\n{list}\n\nNummer eingeben:',
|
||||||
|
'device.copy.invalid_selection': 'Ungültige Auswahl',
|
||||||
|
'device.copy.toast': '{n} Elemente nach {device} kopiert',
|
||||||
|
'device.assign.empty_all': 'Noch keine Inhalte, Widgets oder Kioskseiten. Erstellen Sie zuerst etwas!',
|
||||||
|
'device.assign.modal_title': 'Zur Playlist hinzufügen',
|
||||||
|
'device.assign.zone_label': 'Zone',
|
||||||
|
'device.assign.zone_default': 'Standard (Vollbild)',
|
||||||
|
'device.assign.duration_label': 'Anzeigedauer (Sekunden, für Bilder/Widgets)',
|
||||||
|
'device.assign.tab.media': 'Medien ({n})',
|
||||||
|
'device.assign.tab.widgets': 'Widgets ({n})',
|
||||||
|
'device.assign.tab.kiosk': 'Kiosk ({n})',
|
||||||
|
'device.assign.no_media': 'Noch keine Medien hochgeladen',
|
||||||
|
'device.assign.no_widgets': 'Noch keine Widgets erstellt.',
|
||||||
|
'device.assign.no_kiosk': 'Noch keine Kioskseiten.',
|
||||||
|
'device.assign.create_one': 'Eines erstellen',
|
||||||
|
'device.assign.add_selected': 'Auswahl hinzufügen',
|
||||||
|
'device.assign.select_first': 'Erst etwas auswählen',
|
||||||
|
'device.assign.kiosk_widget_name': 'Kiosk: {name}',
|
||||||
|
'device.toast.screenshot_requested': 'Screenshot angefordert',
|
||||||
|
'device.toast.renamed': 'Bildschirm umbenannt',
|
||||||
|
'device.toast.removing': 'Wird entfernt...',
|
||||||
|
'device.toast.removed': 'Bildschirm entfernt',
|
||||||
|
'device.toast.settings_saved': 'Einstellungen gespeichert',
|
||||||
|
'device.toast.published': 'Playlist veröffentlicht — Geräte aktualisiert',
|
||||||
|
'device.toast.draft_discarded': 'Entwurfsänderungen verworfen',
|
||||||
|
'device.toast.playlist_changed': 'Playlist geändert',
|
||||||
|
'device.toast.layout_applied': 'Layout angewendet',
|
||||||
|
'device.toast.switched_to_fullscreen': 'Auf Vollbild umgestellt',
|
||||||
|
'device.toast.added_to_playlist': 'Zur Playlist hinzugefügt',
|
||||||
|
'device.toast.unmuted': 'Stummschaltung aufgehoben',
|
||||||
|
'device.toast.muted': 'Stummgeschaltet',
|
||||||
|
'device.toast.zone_updated': 'Zone aktualisiert',
|
||||||
|
'device.toast.removed_from_playlist': 'Inhalt aus Playlist entfernt',
|
||||||
|
'device.toast.playlist_reordered': 'Playlist neu sortiert',
|
||||||
|
'device.toast.reboot_sent': 'Neustart-Befehl gesendet',
|
||||||
|
'device.toast.shutdown_sent': 'Herunterfahren-Befehl gesendet',
|
||||||
|
'device.toast.screen_off_sent': 'Befehl Bildschirm aus gesendet',
|
||||||
|
'device.toast.screen_on_sent': 'Befehl Bildschirm an gesendet',
|
||||||
|
'device.toast.launch_sent': 'Start-Befehl gesendet',
|
||||||
|
'device.toast.update_triggered': 'Update-Prüfung ausgelöst',
|
||||||
|
'device.toast.remote_started': 'Fernsteuerungssitzung gestartet',
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
'settings.title': 'Einstellungen',
|
||||||
|
'settings.subtitle': 'Serverkonfiguration und Setup-Informationen',
|
||||||
|
'settings.account': 'Konto',
|
||||||
|
'settings.save_profile': 'Profil speichern',
|
||||||
|
'settings.change_password': 'Passwort ändern',
|
||||||
|
'settings.password_min_8': 'Muss mindestens 8 Zeichen lang sein.',
|
||||||
|
'settings.current_password': 'Aktuelles Passwort',
|
||||||
|
'settings.new_password': 'Neues Passwort',
|
||||||
|
'settings.confirm_new_password': 'Neues Passwort bestätigen',
|
||||||
|
'settings.sso_note': 'Sie melden sich über {provider} an. Verwalten Sie Ihr Passwort dort.',
|
||||||
|
'settings.license': 'Lizenz',
|
||||||
|
'settings.license_mit': 'MIT-Lizenz - alle Funktionen enthalten.',
|
||||||
|
'settings.platform_admin_link': 'Plattform-Admin-Tools sind auf der Seite',
|
||||||
|
'settings.platform_admin_page_suffix': '.',
|
||||||
|
'settings.user_management': 'Benutzerverwaltung',
|
||||||
|
'settings.loading_users': 'Benutzer werden geladen...',
|
||||||
|
'settings.white_label': 'White Label / Branding',
|
||||||
|
'settings.white_label_desc': 'Passen Sie das Erscheinungsbild des Dashboards und Players für Ihre Kunden an.',
|
||||||
|
'settings.brand_name': 'Markenname',
|
||||||
|
'settings.logo_url': 'Logo-URL',
|
||||||
|
'settings.primary_color': 'Primärfarbe',
|
||||||
|
'settings.bg_color': 'Hintergrundfarbe',
|
||||||
|
'settings.custom_domain': 'Benutzerdefinierte Domain',
|
||||||
|
'settings.favicon_url': 'Favicon-URL',
|
||||||
|
'settings.custom_css': 'Benutzerdefiniertes CSS (optional)',
|
||||||
|
'settings.hide_branding': '„ScreenTinker"-Branding ausblenden',
|
||||||
|
'settings.save_branding': 'Branding speichern',
|
||||||
|
'settings.preview': 'Vorschau',
|
||||||
|
'settings.white_label_enterprise_only': 'Benutzerdefiniertes Branding ist im Enterprise-Plan verfügbar',
|
||||||
|
'settings.view_plans': 'Pläne ansehen',
|
||||||
|
'settings.server_info': 'Serverinformationen',
|
||||||
|
'settings.server_url': 'Server-URL',
|
||||||
|
'settings.api_endpoint': 'API-Endpunkt',
|
||||||
|
'settings.server_url_hint': 'Verwenden Sie diese URL beim Einrichten der Android-App',
|
||||||
|
'settings.setup_guide': 'Setup-Anleitung',
|
||||||
|
'settings.setup_step_1': 'Installieren Sie die ScreenTinker APK auf Ihrem TV via Sideloading',
|
||||||
|
'settings.setup_step_2_prefix': 'Öffnen Sie die App und geben Sie diese Server-URL ein:',
|
||||||
|
'settings.setup_step_3': 'Die App zeigt einen 6-stelligen Kopplungscode an',
|
||||||
|
'settings.setup_step_4': 'Klicken Sie auf „Bildschirm hinzufügen" und geben Sie den Code ein',
|
||||||
|
'settings.setup_step_5': 'Inhalte in die Inhaltsbibliothek hochladen',
|
||||||
|
'settings.setup_step_6': 'Inhalte der Playlist des Bildschirms zuweisen',
|
||||||
|
'settings.your_data': 'Ihre Daten',
|
||||||
|
'settings.your_data_desc': 'Exportieren oder importieren Sie Ihre Geräte, Inhalte, Layouts, Zeitpläne und alle Einstellungen. Nutzen Sie dies zur Migration zwischen Cloud und Selfhosted.',
|
||||||
|
'settings.export_my_data': 'Meine Daten exportieren',
|
||||||
|
'settings.include_media_zip': 'Mediendateien einschließen (ZIP)',
|
||||||
|
'settings.import_data': 'Daten importieren',
|
||||||
|
'settings.language': 'Sprache',
|
||||||
|
'settings.about': 'Über',
|
||||||
|
'settings.about_tagline': 'Digital-Signage-Verwaltungssystem.',
|
||||||
|
'settings.third_party_licenses': 'Drittanbieter-Lizenzen',
|
||||||
|
'settings.import.reading_file': 'Datei wird gelesen...',
|
||||||
|
'settings.import.zip_detected': 'ZIP-Export erkannt: <strong>{name}</strong> ({size} MB)<br>Enthält Daten + Mediendateien.',
|
||||||
|
'settings.import.confirm': 'Import bestätigen',
|
||||||
|
'settings.import.invalid_file': 'Ungültige Datei. Muss ein ScreenTinker-Export-JSON oder ZIP sein.',
|
||||||
|
'settings.import.summary_devices': '{n} Geräte',
|
||||||
|
'settings.import.summary_content': '{n} Inhalte',
|
||||||
|
'settings.import.summary_widgets': '{n} Widgets',
|
||||||
|
'settings.import.summary_layouts': '{n} Layouts',
|
||||||
|
'settings.import.summary_schedules': '{n} Zeitpläne',
|
||||||
|
'settings.import.summary_walls': '{n} Videowände',
|
||||||
|
'settings.import.summary_kiosk': '{n} Kioskseiten',
|
||||||
|
'settings.import.found_summary': 'Gefunden: {summary}.<br>Von: {email} (exportiert {date})',
|
||||||
|
'settings.import.empty_export': 'leerer Export',
|
||||||
|
'settings.import.uploading_zip': 'Wird hochgeladen und importiert... Bei großen Dateien kann dies einen Moment dauern.',
|
||||||
|
'settings.import.importing': 'Wird importiert...',
|
||||||
|
'settings.import.complete': 'Import abgeschlossen: {imported}.',
|
||||||
|
'settings.import.pairing_codes_title': 'Geräte-Kopplungscodes:',
|
||||||
|
'settings.import.pairing_codes_hint': 'Geben Sie diese Codes an jedem Gerät ein, um sie neu zu verknüpfen. Zuweisungen und Zeitpläne bleiben erhalten.',
|
||||||
|
'settings.import.failed': 'Import fehlgeschlagen',
|
||||||
|
'settings.import.failed_with_error': 'Import fehlgeschlagen: {error}',
|
||||||
|
'settings.import.read_failed': 'Datei konnte nicht gelesen werden: {error}',
|
||||||
|
'settings.toast.support_token_generated': 'Support-Token erstellt (gültig {hours}h)',
|
||||||
|
'settings.toast.import_success': 'Daten erfolgreich importiert',
|
||||||
|
'settings.toast.name_required': 'Name darf nicht leer sein',
|
||||||
|
'settings.toast.profile_saved': 'Profil gespeichert',
|
||||||
|
'settings.toast.current_password_required': 'Geben Sie Ihr aktuelles Passwort ein',
|
||||||
|
'settings.toast.new_password_min_8': 'Neues Passwort muss mindestens 8 Zeichen lang sein',
|
||||||
|
'settings.toast.passwords_dont_match': 'Neue Passwörter stimmen nicht überein',
|
||||||
|
'settings.toast.password_changed': 'Passwort geändert',
|
||||||
|
'settings.toast.branding_saved': 'Branding gespeichert',
|
||||||
|
'settings.toast.preview_applied': 'Vorschau angewendet (zum Zurücksetzen neu laden)',
|
||||||
|
'settings.toast.plan_updated': 'Plan aktualisiert',
|
||||||
|
'settings.toast.user_removed': 'Benutzer entfernt',
|
||||||
|
'settings.user.col_user': 'Benutzer',
|
||||||
|
'settings.user.col_auth': 'Auth',
|
||||||
|
'settings.user.col_role': 'Rolle',
|
||||||
|
'settings.user.col_plan': 'Plan',
|
||||||
|
'settings.user.col_actions': 'Aktionen',
|
||||||
|
'settings.user.remove': 'Entfernen',
|
||||||
|
'settings.user.you': 'Sie',
|
||||||
|
'settings.user.confirm': 'Bestätigen?',
|
||||||
|
'settings.user.count_one': '1 Benutzer registriert',
|
||||||
|
'settings.user.count_other': '{n} Benutzer registriert',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -205,4 +205,266 @@ export default {
|
||||||
'content.toast.folder_deleted': 'Folder deleted',
|
'content.toast.folder_deleted': 'Folder deleted',
|
||||||
'content.toast.moved': 'Moved',
|
'content.toast.moved': 'Moved',
|
||||||
'content.toast.moved_to_root': 'Moved to root',
|
'content.toast.moved_to_root': 'Moved to root',
|
||||||
|
|
||||||
|
// Device detail
|
||||||
|
'device.back': 'Back to Displays',
|
||||||
|
'device.owner_label': 'Owner: {owner}',
|
||||||
|
'device.rename': 'Rename',
|
||||||
|
'device.screenshot_btn': 'Screenshot',
|
||||||
|
'device.remove': 'Remove',
|
||||||
|
'device.click_to_confirm': 'Click again to confirm',
|
||||||
|
'device.prompt_new_name': 'Enter new name:',
|
||||||
|
'device.confirm_discard_draft': 'Discard all unpublished changes and revert to the last published version?',
|
||||||
|
'device.failed_load': 'Failed to load device',
|
||||||
|
'device.no_screenshot': 'No screenshot available. Click "Screenshot" to capture one.',
|
||||||
|
'device.no_content_assigned': 'No content assigned',
|
||||||
|
'device.now_playing_id': 'Playing: {id}',
|
||||||
|
'device.playlist_count_one': '1 item in playlist',
|
||||||
|
'device.playlist_count_other': '{n} items in playlist',
|
||||||
|
// Tabs
|
||||||
|
'device.tab.now_playing': 'Now Playing',
|
||||||
|
'device.tab.now_playing_tip': "Live screenshot of what's currently displaying on this device.",
|
||||||
|
'device.tab.playlist': 'Playlist',
|
||||||
|
'device.tab.playlist_tip': 'Content assigned to this device. Drag items to reorder. Add media, widgets, or kiosk pages.',
|
||||||
|
'device.tab.info': 'Device Info',
|
||||||
|
'device.tab.info_tip': 'Hardware telemetry, orientation settings, notes, and device controls.',
|
||||||
|
'device.tab.remote': 'Remote Control',
|
||||||
|
'device.tab.remote_tip': 'View the device screen in real-time and send key presses. Works on Android APK and web player.',
|
||||||
|
// Draft banner
|
||||||
|
'device.draft.banner_title': 'Unpublished changes',
|
||||||
|
'device.draft.devices_showing_published': 'Devices are still showing the last published version.',
|
||||||
|
'device.draft.never_published': 'This playlist has never been published. Devices will show nothing until you publish.',
|
||||||
|
'device.draft.discard': 'Discard',
|
||||||
|
'device.draft.publish': 'Publish',
|
||||||
|
'device.draft.publishing': 'Publishing...',
|
||||||
|
// Layout selector
|
||||||
|
'device.layout.label': 'Screen Layout',
|
||||||
|
'device.layout.fullscreen_default': 'Fullscreen (default)',
|
||||||
|
'device.layout.zones_count': '{name} ({n} zones)',
|
||||||
|
'device.layout.template_zones_count': '[Template] {name} ({n} zones)',
|
||||||
|
'device.layout.apply': 'Apply',
|
||||||
|
// Playlist tab
|
||||||
|
'device.playlist.label': 'Playlist',
|
||||||
|
'device.playlist.no_playlist': 'No playlist',
|
||||||
|
'device.playlist.copy_to_btn': 'Copy To...',
|
||||||
|
'device.playlist.add_content_btn': 'Add Content',
|
||||||
|
'device.playlist.empty_title': 'No content assigned',
|
||||||
|
'device.playlist.empty_desc': "Add content from your library to this display's playlist.",
|
||||||
|
'device.playlist_picker.with_count': '{name} — {n} items',
|
||||||
|
'device.playlist_picker.with_auto': '{name} (auto) — {n} items',
|
||||||
|
// Info cards
|
||||||
|
'device.info.status': 'Status',
|
||||||
|
'device.info.ip_address': 'IP Address',
|
||||||
|
'device.info.battery': 'Battery',
|
||||||
|
'device.info.storage': 'Storage',
|
||||||
|
'device.info.size_free': '{size} free',
|
||||||
|
'device.info.player_type': 'Player Type',
|
||||||
|
'device.info.web_player': 'Web Player',
|
||||||
|
'device.info.wifi': 'WiFi',
|
||||||
|
'device.info.uptime': 'Uptime',
|
||||||
|
'device.info.android_version': 'Android Version',
|
||||||
|
'device.info.app_version': 'App Version',
|
||||||
|
'device.info.screen_resolution': 'Screen Resolution',
|
||||||
|
'device.info.ram': 'RAM',
|
||||||
|
'device.info.cpu_usage': 'CPU Usage',
|
||||||
|
// Uptime timeline
|
||||||
|
'device.timeline.title': 'Uptime Timeline (Last 24 Hours)',
|
||||||
|
'device.timeline.h24_ago': '24h ago',
|
||||||
|
'device.timeline.now': 'Now',
|
||||||
|
'device.timeline.online': 'Online',
|
||||||
|
'device.timeline.offline': 'Offline',
|
||||||
|
'device.timeline.no_data': 'No data',
|
||||||
|
'device.timeline.uptime_pct_tracked': '{pct}% uptime ({n}min tracked)',
|
||||||
|
'device.timeline.uptime_pct_no_data': '{pct}% uptime (no data)',
|
||||||
|
// Form
|
||||||
|
'device.form.orientation_label': 'Orientation / Rotation',
|
||||||
|
'device.form.orientation.landscape': 'Landscape (0°)',
|
||||||
|
'device.form.orientation.portrait': 'Portrait (90° CW)',
|
||||||
|
'device.form.orientation.landscape_flipped': 'Landscape Flipped (180°)',
|
||||||
|
'device.form.orientation.portrait_flipped': 'Portrait Flipped (270° CW)',
|
||||||
|
'device.form.default_content_label': 'Default Content',
|
||||||
|
'device.form.default_content_none': 'None (show "Waiting...")',
|
||||||
|
'device.form.notes_label': 'Notes',
|
||||||
|
'device.form.notes_placeholder': 'Location, setup details, etc.',
|
||||||
|
'device.form.save_settings': 'Save Settings',
|
||||||
|
// Control buttons
|
||||||
|
'device.ctl.reboot_device': 'Reboot Device',
|
||||||
|
'device.ctl.screen_off': 'Screen Off',
|
||||||
|
'device.ctl.screen_on': 'Screen On',
|
||||||
|
'device.ctl.launch_player': 'Launch Player',
|
||||||
|
'device.ctl.force_update': 'Force Update',
|
||||||
|
'device.ctl.shutdown': 'Shutdown',
|
||||||
|
// Remote tab
|
||||||
|
'device.remote.start_prompt': 'Click "Start Remote" to begin',
|
||||||
|
'device.remote.start': 'Start Remote',
|
||||||
|
'device.remote.stop': 'Stop Remote',
|
||||||
|
'device.remote.vol_up': 'Vol +',
|
||||||
|
'device.remote.vol_down': 'Vol -',
|
||||||
|
'device.remote.home': 'Home',
|
||||||
|
'device.remote.back': 'Back',
|
||||||
|
'device.remote.recents': 'Recents',
|
||||||
|
'device.remote.power': 'Power',
|
||||||
|
'device.remote.ok': 'OK',
|
||||||
|
'device.remote.settings': 'Settings',
|
||||||
|
'device.remote.scrn_off': 'Scrn Off',
|
||||||
|
'device.remote.scrn_on': 'Scrn On',
|
||||||
|
'device.remote.enable_system_view': 'Enable System View',
|
||||||
|
'device.remote.system_view_tooltip': 'Prompts the device user to allow full screen capture - enables remote view of home screen, settings, and other apps',
|
||||||
|
'device.remote.system_view_hint': 'Requires one-time approval on device',
|
||||||
|
'device.remote.waiting_for_approval': 'Waiting for device approval...',
|
||||||
|
'device.remote.system_view_enabled': 'System View Enabled',
|
||||||
|
'device.remote.unlocked_hint': 'Navigation and system controls unlocked',
|
||||||
|
// Playlist item
|
||||||
|
'device.pl_item.widget_with_type': 'Widget ({type})',
|
||||||
|
'device.pl_item.youtube': 'YouTube',
|
||||||
|
'device.pl_item.video': 'Video',
|
||||||
|
'device.pl_item.image': 'Image',
|
||||||
|
'device.pl_item.zone_label': 'Zone: {id}',
|
||||||
|
'device.pl_item.no_zone': 'No zone',
|
||||||
|
'device.pl_item.mute': 'Mute',
|
||||||
|
'device.pl_item.unmute': 'Unmute',
|
||||||
|
'device.pl_item.remove': 'Remove',
|
||||||
|
// Copy playlist
|
||||||
|
'device.copy.no_other_devices': 'No other devices to copy to',
|
||||||
|
'device.copy.prompt': 'Copy playlist to which device?\n\n{list}\n\nEnter number:',
|
||||||
|
'device.copy.invalid_selection': 'Invalid selection',
|
||||||
|
'device.copy.toast': 'Copied {n} items to {device}',
|
||||||
|
// Add-content modal
|
||||||
|
'device.assign.empty_all': 'No content, widgets, or kiosk pages yet. Create something first!',
|
||||||
|
'device.assign.modal_title': 'Add to Playlist',
|
||||||
|
'device.assign.zone_label': 'Zone',
|
||||||
|
'device.assign.zone_default': 'Default (fullscreen)',
|
||||||
|
'device.assign.duration_label': 'Display Duration (seconds, for images/widgets)',
|
||||||
|
'device.assign.tab.media': 'Media ({n})',
|
||||||
|
'device.assign.tab.widgets': 'Widgets ({n})',
|
||||||
|
'device.assign.tab.kiosk': 'Kiosk ({n})',
|
||||||
|
'device.assign.no_media': 'No media uploaded yet',
|
||||||
|
'device.assign.no_widgets': 'No widgets created yet.',
|
||||||
|
'device.assign.no_kiosk': 'No kiosk pages yet.',
|
||||||
|
'device.assign.create_one': 'Create one',
|
||||||
|
'device.assign.add_selected': 'Add Selected',
|
||||||
|
'device.assign.select_first': 'Select something first',
|
||||||
|
'device.assign.kiosk_widget_name': 'Kiosk: {name}',
|
||||||
|
// Toasts
|
||||||
|
'device.toast.screenshot_requested': 'Screenshot requested',
|
||||||
|
'device.toast.renamed': 'Display renamed',
|
||||||
|
'device.toast.removing': 'Removing...',
|
||||||
|
'device.toast.removed': 'Display removed',
|
||||||
|
'device.toast.settings_saved': 'Settings saved',
|
||||||
|
'device.toast.published': 'Playlist published — devices updated',
|
||||||
|
'device.toast.draft_discarded': 'Draft changes discarded',
|
||||||
|
'device.toast.playlist_changed': 'Playlist changed',
|
||||||
|
'device.toast.layout_applied': 'Layout applied',
|
||||||
|
'device.toast.switched_to_fullscreen': 'Switched to fullscreen',
|
||||||
|
'device.toast.added_to_playlist': 'Added to playlist',
|
||||||
|
'device.toast.unmuted': 'Unmuted',
|
||||||
|
'device.toast.muted': 'Muted',
|
||||||
|
'device.toast.zone_updated': 'Zone updated',
|
||||||
|
'device.toast.removed_from_playlist': 'Content removed from playlist',
|
||||||
|
'device.toast.playlist_reordered': 'Playlist reordered',
|
||||||
|
'device.toast.reboot_sent': 'Reboot command sent',
|
||||||
|
'device.toast.shutdown_sent': 'Shutdown command sent',
|
||||||
|
'device.toast.screen_off_sent': 'Screen off command sent',
|
||||||
|
'device.toast.screen_on_sent': 'Screen on command sent',
|
||||||
|
'device.toast.launch_sent': 'Launch command sent',
|
||||||
|
'device.toast.update_triggered': 'Update check triggered',
|
||||||
|
'device.toast.remote_started': 'Remote session started',
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
'settings.title': 'Settings',
|
||||||
|
'settings.subtitle': 'Server configuration and setup information',
|
||||||
|
'settings.account': 'Account',
|
||||||
|
'settings.save_profile': 'Save Profile',
|
||||||
|
'settings.change_password': 'Change Password',
|
||||||
|
'settings.password_min_8': 'Must be at least 8 characters.',
|
||||||
|
'settings.current_password': 'Current Password',
|
||||||
|
'settings.new_password': 'New Password',
|
||||||
|
'settings.confirm_new_password': 'Confirm New Password',
|
||||||
|
'settings.sso_note': 'You sign in via {provider}. Manage your password there.',
|
||||||
|
'settings.license': 'License',
|
||||||
|
'settings.license_mit': 'MIT License - all features included.',
|
||||||
|
'settings.platform_admin_link': 'Platform admin tools are in the',
|
||||||
|
'settings.platform_admin_page_suffix': 'page.',
|
||||||
|
'settings.user_management': 'User Management',
|
||||||
|
'settings.loading_users': 'Loading users...',
|
||||||
|
'settings.white_label': 'White Label / Branding',
|
||||||
|
'settings.white_label_desc': 'Customize the look of your dashboard and player for your clients.',
|
||||||
|
'settings.brand_name': 'Brand Name',
|
||||||
|
'settings.logo_url': 'Logo URL',
|
||||||
|
'settings.primary_color': 'Primary Color',
|
||||||
|
'settings.bg_color': 'Background Color',
|
||||||
|
'settings.custom_domain': 'Custom Domain',
|
||||||
|
'settings.favicon_url': 'Favicon URL',
|
||||||
|
'settings.custom_css': 'Custom CSS (optional)',
|
||||||
|
'settings.hide_branding': 'Hide "ScreenTinker" branding',
|
||||||
|
'settings.save_branding': 'Save Branding',
|
||||||
|
'settings.preview': 'Preview',
|
||||||
|
'settings.white_label_enterprise_only': 'Custom branding is available on the Enterprise plan',
|
||||||
|
'settings.view_plans': 'View Plans',
|
||||||
|
'settings.server_info': 'Server Information',
|
||||||
|
'settings.server_url': 'Server URL',
|
||||||
|
'settings.api_endpoint': 'API Endpoint',
|
||||||
|
'settings.server_url_hint': 'Use this URL when setting up the Android app',
|
||||||
|
'settings.setup_guide': 'Setup Guide',
|
||||||
|
'settings.setup_step_1': 'Install the ScreenTinker APK on your TV via sideloading',
|
||||||
|
'settings.setup_step_2_prefix': 'Open the app and enter this server URL:',
|
||||||
|
'settings.setup_step_3': 'The app will display a 6-digit pairing code',
|
||||||
|
'settings.setup_step_4': 'Click "Add Display" on the dashboard and enter the pairing code',
|
||||||
|
'settings.setup_step_5': 'Upload content in the Content Library',
|
||||||
|
'settings.setup_step_6': "Assign content to the display's Playlist",
|
||||||
|
'settings.your_data': 'Your Data',
|
||||||
|
'settings.your_data_desc': 'Export or import your devices, content, layouts, schedules, and all settings. Use this to migrate between cloud and self-hosted instances.',
|
||||||
|
'settings.export_my_data': 'Export My Data',
|
||||||
|
'settings.include_media_zip': 'Include media files (ZIP)',
|
||||||
|
'settings.import_data': 'Import Data',
|
||||||
|
'settings.language': 'Language',
|
||||||
|
'settings.about': 'About',
|
||||||
|
'settings.about_tagline': 'Digital signage management system.',
|
||||||
|
'settings.third_party_licenses': 'Third-Party Licenses',
|
||||||
|
// Import flow
|
||||||
|
'settings.import.reading_file': 'Reading file...',
|
||||||
|
'settings.import.zip_detected': 'ZIP export detected: <strong>{name}</strong> ({size} MB)<br>Contains data + media files.',
|
||||||
|
'settings.import.confirm': 'Confirm Import',
|
||||||
|
'settings.import.invalid_file': 'Invalid file. Must be a ScreenTinker export JSON or ZIP.',
|
||||||
|
'settings.import.summary_devices': '{n} devices',
|
||||||
|
'settings.import.summary_content': '{n} content items',
|
||||||
|
'settings.import.summary_widgets': '{n} widgets',
|
||||||
|
'settings.import.summary_layouts': '{n} layouts',
|
||||||
|
'settings.import.summary_schedules': '{n} schedules',
|
||||||
|
'settings.import.summary_walls': '{n} video walls',
|
||||||
|
'settings.import.summary_kiosk': '{n} kiosk pages',
|
||||||
|
'settings.import.found_summary': 'Found: {summary}.<br>From: {email} (exported {date})',
|
||||||
|
'settings.import.empty_export': 'empty export',
|
||||||
|
'settings.import.uploading_zip': 'Uploading and importing... This may take a moment for large files.',
|
||||||
|
'settings.import.importing': 'Importing...',
|
||||||
|
'settings.import.complete': 'Import complete: {imported}.',
|
||||||
|
'settings.import.pairing_codes_title': 'Device Pairing Codes:',
|
||||||
|
'settings.import.pairing_codes_hint': 'Enter these codes on each device to re-link them. All assignments and schedules will be preserved.',
|
||||||
|
'settings.import.failed': 'Import failed',
|
||||||
|
'settings.import.failed_with_error': 'Import failed: {error}',
|
||||||
|
'settings.import.read_failed': 'Failed to read file: {error}',
|
||||||
|
// Settings toasts
|
||||||
|
'settings.toast.support_token_generated': 'Support token generated (valid {hours}h)',
|
||||||
|
'settings.toast.import_success': 'Data imported successfully',
|
||||||
|
'settings.toast.name_required': 'Name cannot be empty',
|
||||||
|
'settings.toast.profile_saved': 'Profile saved',
|
||||||
|
'settings.toast.current_password_required': 'Enter your current password',
|
||||||
|
'settings.toast.new_password_min_8': 'New password must be at least 8 characters',
|
||||||
|
'settings.toast.passwords_dont_match': 'New passwords do not match',
|
||||||
|
'settings.toast.password_changed': 'Password changed',
|
||||||
|
'settings.toast.branding_saved': 'Branding saved',
|
||||||
|
'settings.toast.preview_applied': 'Preview applied (refresh to reset)',
|
||||||
|
'settings.toast.plan_updated': 'Plan updated',
|
||||||
|
'settings.toast.user_removed': 'User removed',
|
||||||
|
// User management table
|
||||||
|
'settings.user.col_user': 'User',
|
||||||
|
'settings.user.col_auth': 'Auth',
|
||||||
|
'settings.user.col_role': 'Role',
|
||||||
|
'settings.user.col_plan': 'Plan',
|
||||||
|
'settings.user.col_actions': 'Actions',
|
||||||
|
'settings.user.remove': 'Remove',
|
||||||
|
'settings.user.you': 'You',
|
||||||
|
'settings.user.confirm': 'Confirm?',
|
||||||
|
'settings.user.count_one': '1 user registered',
|
||||||
|
'settings.user.count_other': '{n} users registered',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -193,4 +193,250 @@ export default {
|
||||||
'content.toast.folder_deleted': 'Carpeta eliminada',
|
'content.toast.folder_deleted': 'Carpeta eliminada',
|
||||||
'content.toast.moved': 'Movido',
|
'content.toast.moved': 'Movido',
|
||||||
'content.toast.moved_to_root': 'Movido a la raíz',
|
'content.toast.moved_to_root': 'Movido a la raíz',
|
||||||
|
|
||||||
|
// Device detail
|
||||||
|
'device.back': 'Volver a Pantallas',
|
||||||
|
'device.owner_label': 'Propietario: {owner}',
|
||||||
|
'device.rename': 'Renombrar',
|
||||||
|
'device.screenshot_btn': 'Captura',
|
||||||
|
'device.remove': 'Eliminar',
|
||||||
|
'device.click_to_confirm': 'Haz clic de nuevo para confirmar',
|
||||||
|
'device.prompt_new_name': 'Ingresa el nuevo nombre:',
|
||||||
|
'device.confirm_discard_draft': '¿Descartar todos los cambios no publicados y volver a la última versión publicada?',
|
||||||
|
'device.failed_load': 'Error al cargar el dispositivo',
|
||||||
|
'device.no_screenshot': 'No hay captura disponible. Haz clic en "Captura" para tomar una.',
|
||||||
|
'device.no_content_assigned': 'Sin contenido asignado',
|
||||||
|
'device.now_playing_id': 'Reproduciendo: {id}',
|
||||||
|
'device.playlist_count_one': '1 elemento en la lista',
|
||||||
|
'device.playlist_count_other': '{n} elementos en la lista',
|
||||||
|
'device.tab.now_playing': 'Reproducción actual',
|
||||||
|
'device.tab.now_playing_tip': 'Captura en vivo de lo que se muestra ahora en este dispositivo.',
|
||||||
|
'device.tab.playlist': 'Lista',
|
||||||
|
'device.tab.playlist_tip': 'Contenido asignado a este dispositivo. Arrastra para reordenar. Agrega medios, widgets o páginas de kiosco.',
|
||||||
|
'device.tab.info': 'Información del dispositivo',
|
||||||
|
'device.tab.info_tip': 'Telemetría de hardware, orientación, notas y controles del dispositivo.',
|
||||||
|
'device.tab.remote': 'Control remoto',
|
||||||
|
'device.tab.remote_tip': 'Visualiza la pantalla del dispositivo en tiempo real y envía pulsaciones. Funciona en la APK de Android y el reproductor web.',
|
||||||
|
'device.draft.banner_title': 'Cambios sin publicar',
|
||||||
|
'device.draft.devices_showing_published': 'Los dispositivos siguen mostrando la última versión publicada.',
|
||||||
|
'device.draft.never_published': 'Esta lista nunca se ha publicado. Los dispositivos no mostrarán nada hasta que la publiques.',
|
||||||
|
'device.draft.discard': 'Descartar',
|
||||||
|
'device.draft.publish': 'Publicar',
|
||||||
|
'device.draft.publishing': 'Publicando...',
|
||||||
|
'device.layout.label': 'Diseño de pantalla',
|
||||||
|
'device.layout.fullscreen_default': 'Pantalla completa (predeterminado)',
|
||||||
|
'device.layout.zones_count': '{name} ({n} zonas)',
|
||||||
|
'device.layout.template_zones_count': '[Plantilla] {name} ({n} zonas)',
|
||||||
|
'device.layout.apply': 'Aplicar',
|
||||||
|
'device.playlist.label': 'Lista',
|
||||||
|
'device.playlist.no_playlist': 'Sin lista',
|
||||||
|
'device.playlist.copy_to_btn': 'Copiar a...',
|
||||||
|
'device.playlist.add_content_btn': 'Agregar contenido',
|
||||||
|
'device.playlist.empty_title': 'Sin contenido asignado',
|
||||||
|
'device.playlist.empty_desc': 'Agrega contenido de tu biblioteca a la lista de esta pantalla.',
|
||||||
|
'device.playlist_picker.with_count': '{name} — {n} elementos',
|
||||||
|
'device.playlist_picker.with_auto': '{name} (auto) — {n} elementos',
|
||||||
|
'device.info.status': 'Estado',
|
||||||
|
'device.info.ip_address': 'Dirección IP',
|
||||||
|
'device.info.battery': 'Batería',
|
||||||
|
'device.info.storage': 'Almacenamiento',
|
||||||
|
'device.info.size_free': '{size} libres',
|
||||||
|
'device.info.player_type': 'Tipo de reproductor',
|
||||||
|
'device.info.web_player': 'Reproductor web',
|
||||||
|
'device.info.wifi': 'WiFi',
|
||||||
|
'device.info.uptime': 'Tiempo activo',
|
||||||
|
'device.info.android_version': 'Versión de Android',
|
||||||
|
'device.info.app_version': 'Versión de la app',
|
||||||
|
'device.info.screen_resolution': 'Resolución de pantalla',
|
||||||
|
'device.info.ram': 'RAM',
|
||||||
|
'device.info.cpu_usage': 'Uso de CPU',
|
||||||
|
'device.timeline.title': 'Línea de tiempo (últimas 24 horas)',
|
||||||
|
'device.timeline.h24_ago': 'hace 24h',
|
||||||
|
'device.timeline.now': 'Ahora',
|
||||||
|
'device.timeline.online': 'En línea',
|
||||||
|
'device.timeline.offline': 'Desconectado',
|
||||||
|
'device.timeline.no_data': 'Sin datos',
|
||||||
|
'device.timeline.uptime_pct_tracked': '{pct}% activo ({n}min registrados)',
|
||||||
|
'device.timeline.uptime_pct_no_data': '{pct}% activo (sin datos)',
|
||||||
|
'device.form.orientation_label': 'Orientación / Rotación',
|
||||||
|
'device.form.orientation.landscape': 'Horizontal (0°)',
|
||||||
|
'device.form.orientation.portrait': 'Vertical (90° SR)',
|
||||||
|
'device.form.orientation.landscape_flipped': 'Horizontal invertido (180°)',
|
||||||
|
'device.form.orientation.portrait_flipped': 'Vertical invertido (270° SR)',
|
||||||
|
'device.form.default_content_label': 'Contenido predeterminado',
|
||||||
|
'device.form.default_content_none': 'Ninguno (mostrar "Esperando...")',
|
||||||
|
'device.form.notes_label': 'Notas',
|
||||||
|
'device.form.notes_placeholder': 'Ubicación, detalles de instalación, etc.',
|
||||||
|
'device.form.save_settings': 'Guardar configuración',
|
||||||
|
'device.ctl.reboot_device': 'Reiniciar dispositivo',
|
||||||
|
'device.ctl.screen_off': 'Apagar pantalla',
|
||||||
|
'device.ctl.screen_on': 'Encender pantalla',
|
||||||
|
'device.ctl.launch_player': 'Iniciar reproductor',
|
||||||
|
'device.ctl.force_update': 'Forzar actualización',
|
||||||
|
'device.ctl.shutdown': 'Apagar',
|
||||||
|
'device.remote.start_prompt': 'Haz clic en "Iniciar control remoto" para comenzar',
|
||||||
|
'device.remote.start': 'Iniciar control remoto',
|
||||||
|
'device.remote.stop': 'Detener control remoto',
|
||||||
|
'device.remote.vol_up': 'Vol +',
|
||||||
|
'device.remote.vol_down': 'Vol -',
|
||||||
|
'device.remote.home': 'Inicio',
|
||||||
|
'device.remote.back': 'Atrás',
|
||||||
|
'device.remote.recents': 'Recientes',
|
||||||
|
'device.remote.power': 'Encendido',
|
||||||
|
'device.remote.ok': 'OK',
|
||||||
|
'device.remote.settings': 'Ajustes',
|
||||||
|
'device.remote.scrn_off': 'Pant. off',
|
||||||
|
'device.remote.scrn_on': 'Pant. on',
|
||||||
|
'device.remote.enable_system_view': 'Habilitar vista de sistema',
|
||||||
|
'device.remote.system_view_tooltip': 'Solicita al usuario del dispositivo permitir captura de pantalla completa - habilita ver pantalla de inicio, ajustes y otras apps',
|
||||||
|
'device.remote.system_view_hint': 'Requiere aprobación única en el dispositivo',
|
||||||
|
'device.remote.waiting_for_approval': 'Esperando aprobación del dispositivo...',
|
||||||
|
'device.remote.system_view_enabled': 'Vista de sistema habilitada',
|
||||||
|
'device.remote.unlocked_hint': 'Navegación y controles del sistema desbloqueados',
|
||||||
|
'device.pl_item.widget_with_type': 'Widget ({type})',
|
||||||
|
'device.pl_item.youtube': 'YouTube',
|
||||||
|
'device.pl_item.video': 'Video',
|
||||||
|
'device.pl_item.image': 'Imagen',
|
||||||
|
'device.pl_item.zone_label': 'Zona: {id}',
|
||||||
|
'device.pl_item.no_zone': 'Sin zona',
|
||||||
|
'device.pl_item.mute': 'Silenciar',
|
||||||
|
'device.pl_item.unmute': 'Activar audio',
|
||||||
|
'device.pl_item.remove': 'Eliminar',
|
||||||
|
'device.copy.no_other_devices': 'No hay otros dispositivos a los que copiar',
|
||||||
|
'device.copy.prompt': '¿A qué dispositivo copiar la lista?\n\n{list}\n\nIngresa el número:',
|
||||||
|
'device.copy.invalid_selection': 'Selección no válida',
|
||||||
|
'device.copy.toast': 'Se copiaron {n} elementos a {device}',
|
||||||
|
'device.assign.empty_all': 'Aún no hay contenido, widgets ni páginas de kiosco. ¡Crea algo primero!',
|
||||||
|
'device.assign.modal_title': 'Agregar a la lista',
|
||||||
|
'device.assign.zone_label': 'Zona',
|
||||||
|
'device.assign.zone_default': 'Predeterminada (pantalla completa)',
|
||||||
|
'device.assign.duration_label': 'Duración (segundos, para imágenes/widgets)',
|
||||||
|
'device.assign.tab.media': 'Medios ({n})',
|
||||||
|
'device.assign.tab.widgets': 'Widgets ({n})',
|
||||||
|
'device.assign.tab.kiosk': 'Kiosco ({n})',
|
||||||
|
'device.assign.no_media': 'Aún no se ha subido ningún medio',
|
||||||
|
'device.assign.no_widgets': 'Aún no hay widgets creados.',
|
||||||
|
'device.assign.no_kiosk': 'Aún no hay páginas de kiosco.',
|
||||||
|
'device.assign.create_one': 'Crea uno',
|
||||||
|
'device.assign.add_selected': 'Agregar selección',
|
||||||
|
'device.assign.select_first': 'Primero selecciona algo',
|
||||||
|
'device.assign.kiosk_widget_name': 'Kiosco: {name}',
|
||||||
|
'device.toast.screenshot_requested': 'Captura solicitada',
|
||||||
|
'device.toast.renamed': 'Pantalla renombrada',
|
||||||
|
'device.toast.removing': 'Eliminando...',
|
||||||
|
'device.toast.removed': 'Pantalla eliminada',
|
||||||
|
'device.toast.settings_saved': 'Configuración guardada',
|
||||||
|
'device.toast.published': 'Lista publicada — dispositivos actualizados',
|
||||||
|
'device.toast.draft_discarded': 'Cambios del borrador descartados',
|
||||||
|
'device.toast.playlist_changed': 'Lista cambiada',
|
||||||
|
'device.toast.layout_applied': 'Diseño aplicado',
|
||||||
|
'device.toast.switched_to_fullscreen': 'Cambiado a pantalla completa',
|
||||||
|
'device.toast.added_to_playlist': 'Agregado a la lista',
|
||||||
|
'device.toast.unmuted': 'Audio activado',
|
||||||
|
'device.toast.muted': 'Silenciado',
|
||||||
|
'device.toast.zone_updated': 'Zona actualizada',
|
||||||
|
'device.toast.removed_from_playlist': 'Contenido eliminado de la lista',
|
||||||
|
'device.toast.playlist_reordered': 'Lista reordenada',
|
||||||
|
'device.toast.reboot_sent': 'Comando de reinicio enviado',
|
||||||
|
'device.toast.shutdown_sent': 'Comando de apagado enviado',
|
||||||
|
'device.toast.screen_off_sent': 'Comando para apagar pantalla enviado',
|
||||||
|
'device.toast.screen_on_sent': 'Comando para encender pantalla enviado',
|
||||||
|
'device.toast.launch_sent': 'Comando de inicio enviado',
|
||||||
|
'device.toast.update_triggered': 'Verificación de actualización iniciada',
|
||||||
|
'device.toast.remote_started': 'Sesión de control remoto iniciada',
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
'settings.title': 'Configuración',
|
||||||
|
'settings.subtitle': 'Configuración del servidor e información de instalación',
|
||||||
|
'settings.account': 'Cuenta',
|
||||||
|
'settings.save_profile': 'Guardar perfil',
|
||||||
|
'settings.change_password': 'Cambiar contraseña',
|
||||||
|
'settings.password_min_8': 'Debe tener al menos 8 caracteres.',
|
||||||
|
'settings.current_password': 'Contraseña actual',
|
||||||
|
'settings.new_password': 'Contraseña nueva',
|
||||||
|
'settings.confirm_new_password': 'Confirmar contraseña nueva',
|
||||||
|
'settings.sso_note': 'Inicias sesión con {provider}. Gestiona tu contraseña ahí.',
|
||||||
|
'settings.license': 'Licencia',
|
||||||
|
'settings.license_mit': 'Licencia MIT - todas las funciones incluidas.',
|
||||||
|
'settings.platform_admin_link': 'Las herramientas de administración están en la',
|
||||||
|
'settings.platform_admin_page_suffix': '.',
|
||||||
|
'settings.user_management': 'Gestión de usuarios',
|
||||||
|
'settings.loading_users': 'Cargando usuarios...',
|
||||||
|
'settings.white_label': 'Marca blanca / Branding',
|
||||||
|
'settings.white_label_desc': 'Personaliza la apariencia del panel y el reproductor para tus clientes.',
|
||||||
|
'settings.brand_name': 'Nombre de la marca',
|
||||||
|
'settings.logo_url': 'URL del logotipo',
|
||||||
|
'settings.primary_color': 'Color principal',
|
||||||
|
'settings.bg_color': 'Color de fondo',
|
||||||
|
'settings.custom_domain': 'Dominio personalizado',
|
||||||
|
'settings.favicon_url': 'URL del favicon',
|
||||||
|
'settings.custom_css': 'CSS personalizado (opcional)',
|
||||||
|
'settings.hide_branding': 'Ocultar la marca "ScreenTinker"',
|
||||||
|
'settings.save_branding': 'Guardar branding',
|
||||||
|
'settings.preview': 'Previsualizar',
|
||||||
|
'settings.white_label_enterprise_only': 'El branding personalizado está disponible en el plan Enterprise',
|
||||||
|
'settings.view_plans': 'Ver planes',
|
||||||
|
'settings.server_info': 'Información del servidor',
|
||||||
|
'settings.server_url': 'URL del servidor',
|
||||||
|
'settings.api_endpoint': 'Endpoint de la API',
|
||||||
|
'settings.server_url_hint': 'Usa esta URL al configurar la app de Android',
|
||||||
|
'settings.setup_guide': 'Guía de instalación',
|
||||||
|
'settings.setup_step_1': 'Instala el APK de ScreenTinker en tu TV mediante sideloading',
|
||||||
|
'settings.setup_step_2_prefix': 'Abre la app e ingresa esta URL del servidor:',
|
||||||
|
'settings.setup_step_3': 'La app mostrará un código de vinculación de 6 dígitos',
|
||||||
|
'settings.setup_step_4': 'Haz clic en "Agregar pantalla" en el panel e ingresa el código',
|
||||||
|
'settings.setup_step_5': 'Sube contenido en la Biblioteca de contenido',
|
||||||
|
'settings.setup_step_6': 'Asigna contenido a la lista de la pantalla',
|
||||||
|
'settings.your_data': 'Tus datos',
|
||||||
|
'settings.your_data_desc': 'Exporta o importa tus dispositivos, contenido, diseños, horarios y toda la configuración. Úsalo para migrar entre instancias en la nube y autoalojadas.',
|
||||||
|
'settings.export_my_data': 'Exportar mis datos',
|
||||||
|
'settings.include_media_zip': 'Incluir archivos multimedia (ZIP)',
|
||||||
|
'settings.import_data': 'Importar datos',
|
||||||
|
'settings.language': 'Idioma',
|
||||||
|
'settings.about': 'Acerca de',
|
||||||
|
'settings.about_tagline': 'Sistema de gestión de señalización digital.',
|
||||||
|
'settings.third_party_licenses': 'Licencias de terceros',
|
||||||
|
'settings.import.reading_file': 'Leyendo archivo...',
|
||||||
|
'settings.import.zip_detected': 'Exportación ZIP detectada: <strong>{name}</strong> ({size} MB)<br>Contiene datos + archivos multimedia.',
|
||||||
|
'settings.import.confirm': 'Confirmar importación',
|
||||||
|
'settings.import.invalid_file': 'Archivo no válido. Debe ser un JSON o ZIP de exportación de ScreenTinker.',
|
||||||
|
'settings.import.summary_devices': '{n} dispositivos',
|
||||||
|
'settings.import.summary_content': '{n} elementos de contenido',
|
||||||
|
'settings.import.summary_widgets': '{n} widgets',
|
||||||
|
'settings.import.summary_layouts': '{n} diseños',
|
||||||
|
'settings.import.summary_schedules': '{n} horarios',
|
||||||
|
'settings.import.summary_walls': '{n} muros de video',
|
||||||
|
'settings.import.summary_kiosk': '{n} páginas de kiosco',
|
||||||
|
'settings.import.found_summary': 'Encontrado: {summary}.<br>De: {email} (exportado {date})',
|
||||||
|
'settings.import.empty_export': 'exportación vacía',
|
||||||
|
'settings.import.uploading_zip': 'Subiendo e importando... Esto puede tardar para archivos grandes.',
|
||||||
|
'settings.import.importing': 'Importando...',
|
||||||
|
'settings.import.complete': 'Importación completa: {imported}.',
|
||||||
|
'settings.import.pairing_codes_title': 'Códigos de vinculación:',
|
||||||
|
'settings.import.pairing_codes_hint': 'Ingresa estos códigos en cada dispositivo para volver a vincularlos. Las asignaciones y horarios se conservarán.',
|
||||||
|
'settings.import.failed': 'Error al importar',
|
||||||
|
'settings.import.failed_with_error': 'Error al importar: {error}',
|
||||||
|
'settings.import.read_failed': 'Error al leer el archivo: {error}',
|
||||||
|
'settings.toast.support_token_generated': 'Token de soporte generado (válido {hours}h)',
|
||||||
|
'settings.toast.import_success': 'Datos importados correctamente',
|
||||||
|
'settings.toast.name_required': 'El nombre no puede estar vacío',
|
||||||
|
'settings.toast.profile_saved': 'Perfil guardado',
|
||||||
|
'settings.toast.current_password_required': 'Ingresa tu contraseña actual',
|
||||||
|
'settings.toast.new_password_min_8': 'La nueva contraseña debe tener al menos 8 caracteres',
|
||||||
|
'settings.toast.passwords_dont_match': 'Las nuevas contraseñas no coinciden',
|
||||||
|
'settings.toast.password_changed': 'Contraseña cambiada',
|
||||||
|
'settings.toast.branding_saved': 'Branding guardado',
|
||||||
|
'settings.toast.preview_applied': 'Previsualización aplicada (recarga para restablecer)',
|
||||||
|
'settings.toast.plan_updated': 'Plan actualizado',
|
||||||
|
'settings.toast.user_removed': 'Usuario eliminado',
|
||||||
|
'settings.user.col_user': 'Usuario',
|
||||||
|
'settings.user.col_auth': 'Auth',
|
||||||
|
'settings.user.col_role': 'Rol',
|
||||||
|
'settings.user.col_plan': 'Plan',
|
||||||
|
'settings.user.col_actions': 'Acciones',
|
||||||
|
'settings.user.remove': 'Eliminar',
|
||||||
|
'settings.user.you': 'Tú',
|
||||||
|
'settings.user.confirm': '¿Confirmar?',
|
||||||
|
'settings.user.count_one': '1 usuario registrado',
|
||||||
|
'settings.user.count_other': '{n} usuarios registrados',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -194,4 +194,250 @@ export default {
|
||||||
'content.toast.folder_deleted': 'Dossier supprimé',
|
'content.toast.folder_deleted': 'Dossier supprimé',
|
||||||
'content.toast.moved': 'Déplacé',
|
'content.toast.moved': 'Déplacé',
|
||||||
'content.toast.moved_to_root': 'Déplacé à la racine',
|
'content.toast.moved_to_root': 'Déplacé à la racine',
|
||||||
|
|
||||||
|
// Device detail
|
||||||
|
'device.back': 'Retour aux écrans',
|
||||||
|
'device.owner_label': 'Propriétaire : {owner}',
|
||||||
|
'device.rename': 'Renommer',
|
||||||
|
'device.screenshot_btn': 'Capture',
|
||||||
|
'device.remove': 'Retirer',
|
||||||
|
'device.click_to_confirm': 'Cliquez à nouveau pour confirmer',
|
||||||
|
'device.prompt_new_name': 'Saisissez le nouveau nom :',
|
||||||
|
'device.confirm_discard_draft': 'Annuler toutes les modifications non publiées et revenir à la dernière version publiée ?',
|
||||||
|
'device.failed_load': 'Échec du chargement de l\'appareil',
|
||||||
|
'device.no_screenshot': 'Aucune capture disponible. Cliquez sur « Capture » pour en prendre une.',
|
||||||
|
'device.no_content_assigned': 'Aucun contenu attribué',
|
||||||
|
'device.now_playing_id': 'En lecture : {id}',
|
||||||
|
'device.playlist_count_one': '1 élément dans la liste',
|
||||||
|
'device.playlist_count_other': '{n} éléments dans la liste',
|
||||||
|
'device.tab.now_playing': 'En cours de lecture',
|
||||||
|
'device.tab.now_playing_tip': 'Capture en direct de ce qui s\'affiche sur cet appareil.',
|
||||||
|
'device.tab.playlist': 'Liste de lecture',
|
||||||
|
'device.tab.playlist_tip': 'Contenu attribué à cet appareil. Glissez pour réorganiser. Ajoutez des médias, widgets ou pages kiosque.',
|
||||||
|
'device.tab.info': 'Infos appareil',
|
||||||
|
'device.tab.info_tip': 'Télémétrie matérielle, orientation, notes et contrôles de l\'appareil.',
|
||||||
|
'device.tab.remote': 'Contrôle à distance',
|
||||||
|
'device.tab.remote_tip': 'Visualisez l\'écran en temps réel et envoyez des touches. Fonctionne sur l\'APK Android et le lecteur web.',
|
||||||
|
'device.draft.banner_title': 'Modifications non publiées',
|
||||||
|
'device.draft.devices_showing_published': 'Les appareils affichent encore la dernière version publiée.',
|
||||||
|
'device.draft.never_published': 'Cette liste n\'a jamais été publiée. Les appareils n\'afficheront rien jusqu\'à publication.',
|
||||||
|
'device.draft.discard': 'Annuler',
|
||||||
|
'device.draft.publish': 'Publier',
|
||||||
|
'device.draft.publishing': 'Publication...',
|
||||||
|
'device.layout.label': 'Mise en page',
|
||||||
|
'device.layout.fullscreen_default': 'Plein écran (par défaut)',
|
||||||
|
'device.layout.zones_count': '{name} ({n} zones)',
|
||||||
|
'device.layout.template_zones_count': '[Modèle] {name} ({n} zones)',
|
||||||
|
'device.layout.apply': 'Appliquer',
|
||||||
|
'device.playlist.label': 'Liste de lecture',
|
||||||
|
'device.playlist.no_playlist': 'Aucune liste',
|
||||||
|
'device.playlist.copy_to_btn': 'Copier vers...',
|
||||||
|
'device.playlist.add_content_btn': 'Ajouter du contenu',
|
||||||
|
'device.playlist.empty_title': 'Aucun contenu attribué',
|
||||||
|
'device.playlist.empty_desc': 'Ajoutez du contenu de votre bibliothèque à la liste de cet écran.',
|
||||||
|
'device.playlist_picker.with_count': '{name} — {n} éléments',
|
||||||
|
'device.playlist_picker.with_auto': '{name} (auto) — {n} éléments',
|
||||||
|
'device.info.status': 'Statut',
|
||||||
|
'device.info.ip_address': 'Adresse IP',
|
||||||
|
'device.info.battery': 'Batterie',
|
||||||
|
'device.info.storage': 'Stockage',
|
||||||
|
'device.info.size_free': '{size} libres',
|
||||||
|
'device.info.player_type': 'Type de lecteur',
|
||||||
|
'device.info.web_player': 'Lecteur web',
|
||||||
|
'device.info.wifi': 'WiFi',
|
||||||
|
'device.info.uptime': 'Disponibilité',
|
||||||
|
'device.info.android_version': 'Version d\'Android',
|
||||||
|
'device.info.app_version': 'Version de l\'app',
|
||||||
|
'device.info.screen_resolution': 'Résolution',
|
||||||
|
'device.info.ram': 'RAM',
|
||||||
|
'device.info.cpu_usage': 'Utilisation CPU',
|
||||||
|
'device.timeline.title': 'Disponibilité (24 dernières heures)',
|
||||||
|
'device.timeline.h24_ago': 'il y a 24h',
|
||||||
|
'device.timeline.now': 'Maintenant',
|
||||||
|
'device.timeline.online': 'En ligne',
|
||||||
|
'device.timeline.offline': 'Hors ligne',
|
||||||
|
'device.timeline.no_data': 'Aucune donnée',
|
||||||
|
'device.timeline.uptime_pct_tracked': '{pct}% disponible ({n}min suivies)',
|
||||||
|
'device.timeline.uptime_pct_no_data': '{pct}% disponible (aucune donnée)',
|
||||||
|
'device.form.orientation_label': 'Orientation / Rotation',
|
||||||
|
'device.form.orientation.landscape': 'Paysage (0°)',
|
||||||
|
'device.form.orientation.portrait': 'Portrait (90° SH)',
|
||||||
|
'device.form.orientation.landscape_flipped': 'Paysage retourné (180°)',
|
||||||
|
'device.form.orientation.portrait_flipped': 'Portrait retourné (270° SH)',
|
||||||
|
'device.form.default_content_label': 'Contenu par défaut',
|
||||||
|
'device.form.default_content_none': 'Aucun (afficher « En attente... »)',
|
||||||
|
'device.form.notes_label': 'Notes',
|
||||||
|
'device.form.notes_placeholder': 'Emplacement, détails d\'installation, etc.',
|
||||||
|
'device.form.save_settings': 'Enregistrer les paramètres',
|
||||||
|
'device.ctl.reboot_device': 'Redémarrer l\'appareil',
|
||||||
|
'device.ctl.screen_off': 'Éteindre l\'écran',
|
||||||
|
'device.ctl.screen_on': 'Allumer l\'écran',
|
||||||
|
'device.ctl.launch_player': 'Lancer le lecteur',
|
||||||
|
'device.ctl.force_update': 'Forcer la mise à jour',
|
||||||
|
'device.ctl.shutdown': 'Arrêter',
|
||||||
|
'device.remote.start_prompt': 'Cliquez sur « Démarrer » pour commencer',
|
||||||
|
'device.remote.start': 'Démarrer',
|
||||||
|
'device.remote.stop': 'Arrêter',
|
||||||
|
'device.remote.vol_up': 'Vol +',
|
||||||
|
'device.remote.vol_down': 'Vol -',
|
||||||
|
'device.remote.home': 'Accueil',
|
||||||
|
'device.remote.back': 'Retour',
|
||||||
|
'device.remote.recents': 'Récents',
|
||||||
|
'device.remote.power': 'Marche',
|
||||||
|
'device.remote.ok': 'OK',
|
||||||
|
'device.remote.settings': 'Paramètres',
|
||||||
|
'device.remote.scrn_off': 'Écr. off',
|
||||||
|
'device.remote.scrn_on': 'Écr. on',
|
||||||
|
'device.remote.enable_system_view': 'Activer la vue système',
|
||||||
|
'device.remote.system_view_tooltip': 'Demande à l\'utilisateur d\'autoriser la capture plein écran - permet de voir l\'écran d\'accueil, les paramètres et d\'autres apps',
|
||||||
|
'device.remote.system_view_hint': 'Approbation unique requise sur l\'appareil',
|
||||||
|
'device.remote.waiting_for_approval': 'Attente de l\'approbation de l\'appareil...',
|
||||||
|
'device.remote.system_view_enabled': 'Vue système activée',
|
||||||
|
'device.remote.unlocked_hint': 'Navigation et contrôles système débloqués',
|
||||||
|
'device.pl_item.widget_with_type': 'Widget ({type})',
|
||||||
|
'device.pl_item.youtube': 'YouTube',
|
||||||
|
'device.pl_item.video': 'Vidéo',
|
||||||
|
'device.pl_item.image': 'Image',
|
||||||
|
'device.pl_item.zone_label': 'Zone : {id}',
|
||||||
|
'device.pl_item.no_zone': 'Aucune zone',
|
||||||
|
'device.pl_item.mute': 'Muet',
|
||||||
|
'device.pl_item.unmute': 'Réactiver le son',
|
||||||
|
'device.pl_item.remove': 'Retirer',
|
||||||
|
'device.copy.no_other_devices': 'Aucun autre appareil vers lequel copier',
|
||||||
|
'device.copy.prompt': 'Copier la liste vers quel appareil ?\n\n{list}\n\nSaisissez le numéro :',
|
||||||
|
'device.copy.invalid_selection': 'Sélection invalide',
|
||||||
|
'device.copy.toast': '{n} éléments copiés vers {device}',
|
||||||
|
'device.assign.empty_all': 'Pas encore de contenu, widgets ou pages kiosque. Créez-en un d\'abord !',
|
||||||
|
'device.assign.modal_title': 'Ajouter à la liste',
|
||||||
|
'device.assign.zone_label': 'Zone',
|
||||||
|
'device.assign.zone_default': 'Par défaut (plein écran)',
|
||||||
|
'device.assign.duration_label': 'Durée (secondes, pour images/widgets)',
|
||||||
|
'device.assign.tab.media': 'Médias ({n})',
|
||||||
|
'device.assign.tab.widgets': 'Widgets ({n})',
|
||||||
|
'device.assign.tab.kiosk': 'Kiosque ({n})',
|
||||||
|
'device.assign.no_media': 'Aucun média téléversé',
|
||||||
|
'device.assign.no_widgets': 'Aucun widget créé.',
|
||||||
|
'device.assign.no_kiosk': 'Aucune page kiosque.',
|
||||||
|
'device.assign.create_one': 'Créez-en un',
|
||||||
|
'device.assign.add_selected': 'Ajouter la sélection',
|
||||||
|
'device.assign.select_first': 'Sélectionnez d\'abord un élément',
|
||||||
|
'device.assign.kiosk_widget_name': 'Kiosque : {name}',
|
||||||
|
'device.toast.screenshot_requested': 'Capture demandée',
|
||||||
|
'device.toast.renamed': 'Écran renommé',
|
||||||
|
'device.toast.removing': 'Suppression...',
|
||||||
|
'device.toast.removed': 'Écran retiré',
|
||||||
|
'device.toast.settings_saved': 'Paramètres enregistrés',
|
||||||
|
'device.toast.published': 'Liste publiée — appareils mis à jour',
|
||||||
|
'device.toast.draft_discarded': 'Modifications du brouillon annulées',
|
||||||
|
'device.toast.playlist_changed': 'Liste modifiée',
|
||||||
|
'device.toast.layout_applied': 'Mise en page appliquée',
|
||||||
|
'device.toast.switched_to_fullscreen': 'Passé en plein écran',
|
||||||
|
'device.toast.added_to_playlist': 'Ajouté à la liste',
|
||||||
|
'device.toast.unmuted': 'Son réactivé',
|
||||||
|
'device.toast.muted': 'Son coupé',
|
||||||
|
'device.toast.zone_updated': 'Zone mise à jour',
|
||||||
|
'device.toast.removed_from_playlist': 'Contenu retiré de la liste',
|
||||||
|
'device.toast.playlist_reordered': 'Liste réorganisée',
|
||||||
|
'device.toast.reboot_sent': 'Commande de redémarrage envoyée',
|
||||||
|
'device.toast.shutdown_sent': 'Commande d\'arrêt envoyée',
|
||||||
|
'device.toast.screen_off_sent': 'Commande d\'extinction de l\'écran envoyée',
|
||||||
|
'device.toast.screen_on_sent': 'Commande d\'allumage de l\'écran envoyée',
|
||||||
|
'device.toast.launch_sent': 'Commande de lancement envoyée',
|
||||||
|
'device.toast.update_triggered': 'Vérification de mise à jour déclenchée',
|
||||||
|
'device.toast.remote_started': 'Session de contrôle à distance démarrée',
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
'settings.title': 'Paramètres',
|
||||||
|
'settings.subtitle': 'Configuration du serveur et informations d\'installation',
|
||||||
|
'settings.account': 'Compte',
|
||||||
|
'settings.save_profile': 'Enregistrer le profil',
|
||||||
|
'settings.change_password': 'Changer le mot de passe',
|
||||||
|
'settings.password_min_8': 'Doit contenir au moins 8 caractères.',
|
||||||
|
'settings.current_password': 'Mot de passe actuel',
|
||||||
|
'settings.new_password': 'Nouveau mot de passe',
|
||||||
|
'settings.confirm_new_password': 'Confirmer le nouveau mot de passe',
|
||||||
|
'settings.sso_note': 'Vous vous connectez via {provider}. Gérez votre mot de passe là-bas.',
|
||||||
|
'settings.license': 'Licence',
|
||||||
|
'settings.license_mit': 'Licence MIT - toutes les fonctionnalités incluses.',
|
||||||
|
'settings.platform_admin_link': 'Les outils d\'administration sont sur la page',
|
||||||
|
'settings.platform_admin_page_suffix': '.',
|
||||||
|
'settings.user_management': 'Gestion des utilisateurs',
|
||||||
|
'settings.loading_users': 'Chargement des utilisateurs...',
|
||||||
|
'settings.white_label': 'Marque blanche / Branding',
|
||||||
|
'settings.white_label_desc': 'Personnalisez l\'apparence du tableau de bord et du lecteur pour vos clients.',
|
||||||
|
'settings.brand_name': 'Nom de la marque',
|
||||||
|
'settings.logo_url': 'URL du logo',
|
||||||
|
'settings.primary_color': 'Couleur principale',
|
||||||
|
'settings.bg_color': 'Couleur de fond',
|
||||||
|
'settings.custom_domain': 'Domaine personnalisé',
|
||||||
|
'settings.favicon_url': 'URL du favicon',
|
||||||
|
'settings.custom_css': 'CSS personnalisé (facultatif)',
|
||||||
|
'settings.hide_branding': 'Masquer la marque « ScreenTinker »',
|
||||||
|
'settings.save_branding': 'Enregistrer le branding',
|
||||||
|
'settings.preview': 'Aperçu',
|
||||||
|
'settings.white_label_enterprise_only': 'Le branding personnalisé est disponible sur le plan Enterprise',
|
||||||
|
'settings.view_plans': 'Voir les plans',
|
||||||
|
'settings.server_info': 'Informations du serveur',
|
||||||
|
'settings.server_url': 'URL du serveur',
|
||||||
|
'settings.api_endpoint': 'Point de terminaison API',
|
||||||
|
'settings.server_url_hint': 'Utilisez cette URL pour configurer l\'app Android',
|
||||||
|
'settings.setup_guide': 'Guide d\'installation',
|
||||||
|
'settings.setup_step_1': 'Installez l\'APK ScreenTinker sur votre TV via le sideloading',
|
||||||
|
'settings.setup_step_2_prefix': 'Ouvrez l\'app et saisissez cette URL du serveur :',
|
||||||
|
'settings.setup_step_3': 'L\'app affichera un code d\'appairage à 6 chiffres',
|
||||||
|
'settings.setup_step_4': 'Cliquez sur « Ajouter un écran » et saisissez le code',
|
||||||
|
'settings.setup_step_5': 'Téléversez du contenu dans la Bibliothèque',
|
||||||
|
'settings.setup_step_6': 'Attribuez du contenu à la liste de l\'écran',
|
||||||
|
'settings.your_data': 'Vos données',
|
||||||
|
'settings.your_data_desc': 'Exportez ou importez vos appareils, contenu, mises en page, calendriers et tous les paramètres. Utilisez cela pour migrer entre cloud et auto-hébergé.',
|
||||||
|
'settings.export_my_data': 'Exporter mes données',
|
||||||
|
'settings.include_media_zip': 'Inclure les fichiers médias (ZIP)',
|
||||||
|
'settings.import_data': 'Importer des données',
|
||||||
|
'settings.language': 'Langue',
|
||||||
|
'settings.about': 'À propos',
|
||||||
|
'settings.about_tagline': 'Système de gestion d\'affichage dynamique.',
|
||||||
|
'settings.third_party_licenses': 'Licences tierces',
|
||||||
|
'settings.import.reading_file': 'Lecture du fichier...',
|
||||||
|
'settings.import.zip_detected': 'Export ZIP détecté : <strong>{name}</strong> ({size} Mo)<br>Contient données + fichiers médias.',
|
||||||
|
'settings.import.confirm': 'Confirmer l\'import',
|
||||||
|
'settings.import.invalid_file': 'Fichier invalide. Doit être un JSON ou ZIP d\'export ScreenTinker.',
|
||||||
|
'settings.import.summary_devices': '{n} appareils',
|
||||||
|
'settings.import.summary_content': '{n} contenus',
|
||||||
|
'settings.import.summary_widgets': '{n} widgets',
|
||||||
|
'settings.import.summary_layouts': '{n} mises en page',
|
||||||
|
'settings.import.summary_schedules': '{n} calendriers',
|
||||||
|
'settings.import.summary_walls': '{n} murs vidéo',
|
||||||
|
'settings.import.summary_kiosk': '{n} pages kiosque',
|
||||||
|
'settings.import.found_summary': 'Trouvé : {summary}.<br>De : {email} (exporté {date})',
|
||||||
|
'settings.import.empty_export': 'export vide',
|
||||||
|
'settings.import.uploading_zip': 'Téléversement et import... Cela peut prendre du temps pour les gros fichiers.',
|
||||||
|
'settings.import.importing': 'Import...',
|
||||||
|
'settings.import.complete': 'Import terminé : {imported}.',
|
||||||
|
'settings.import.pairing_codes_title': 'Codes d\'appairage :',
|
||||||
|
'settings.import.pairing_codes_hint': 'Saisissez ces codes sur chaque appareil pour les relier. Les attributions et calendriers seront préservés.',
|
||||||
|
'settings.import.failed': 'Échec de l\'import',
|
||||||
|
'settings.import.failed_with_error': 'Échec de l\'import : {error}',
|
||||||
|
'settings.import.read_failed': 'Échec de lecture du fichier : {error}',
|
||||||
|
'settings.toast.support_token_generated': 'Jeton de support généré (valide {hours}h)',
|
||||||
|
'settings.toast.import_success': 'Données importées avec succès',
|
||||||
|
'settings.toast.name_required': 'Le nom ne peut pas être vide',
|
||||||
|
'settings.toast.profile_saved': 'Profil enregistré',
|
||||||
|
'settings.toast.current_password_required': 'Saisissez votre mot de passe actuel',
|
||||||
|
'settings.toast.new_password_min_8': 'Le nouveau mot de passe doit comporter au moins 8 caractères',
|
||||||
|
'settings.toast.passwords_dont_match': 'Les nouveaux mots de passe ne correspondent pas',
|
||||||
|
'settings.toast.password_changed': 'Mot de passe changé',
|
||||||
|
'settings.toast.branding_saved': 'Branding enregistré',
|
||||||
|
'settings.toast.preview_applied': 'Aperçu appliqué (rechargez pour réinitialiser)',
|
||||||
|
'settings.toast.plan_updated': 'Plan mis à jour',
|
||||||
|
'settings.toast.user_removed': 'Utilisateur retiré',
|
||||||
|
'settings.user.col_user': 'Utilisateur',
|
||||||
|
'settings.user.col_auth': 'Auth',
|
||||||
|
'settings.user.col_role': 'Rôle',
|
||||||
|
'settings.user.col_plan': 'Plan',
|
||||||
|
'settings.user.col_actions': 'Actions',
|
||||||
|
'settings.user.remove': 'Retirer',
|
||||||
|
'settings.user.you': 'Vous',
|
||||||
|
'settings.user.confirm': 'Confirmer ?',
|
||||||
|
'settings.user.count_one': '1 utilisateur inscrit',
|
||||||
|
'settings.user.count_other': '{n} utilisateurs inscrits',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -194,4 +194,250 @@ export default {
|
||||||
'content.toast.folder_deleted': 'Pasta excluída',
|
'content.toast.folder_deleted': 'Pasta excluída',
|
||||||
'content.toast.moved': 'Movido',
|
'content.toast.moved': 'Movido',
|
||||||
'content.toast.moved_to_root': 'Movido para a raiz',
|
'content.toast.moved_to_root': 'Movido para a raiz',
|
||||||
|
|
||||||
|
// Device detail
|
||||||
|
'device.back': 'Voltar para Telas',
|
||||||
|
'device.owner_label': 'Proprietário: {owner}',
|
||||||
|
'device.rename': 'Renomear',
|
||||||
|
'device.screenshot_btn': 'Captura',
|
||||||
|
'device.remove': 'Remover',
|
||||||
|
'device.click_to_confirm': 'Clique novamente para confirmar',
|
||||||
|
'device.prompt_new_name': 'Digite o novo nome:',
|
||||||
|
'device.confirm_discard_draft': 'Descartar todas as alterações não publicadas e voltar à última versão publicada?',
|
||||||
|
'device.failed_load': 'Falha ao carregar o dispositivo',
|
||||||
|
'device.no_screenshot': 'Sem captura disponível. Clique em "Captura" para tirar uma.',
|
||||||
|
'device.no_content_assigned': 'Sem conteúdo atribuído',
|
||||||
|
'device.now_playing_id': 'Reproduzindo: {id}',
|
||||||
|
'device.playlist_count_one': '1 item na playlist',
|
||||||
|
'device.playlist_count_other': '{n} itens na playlist',
|
||||||
|
'device.tab.now_playing': 'Reproduzindo agora',
|
||||||
|
'device.tab.now_playing_tip': 'Captura ao vivo do que está sendo exibido neste dispositivo.',
|
||||||
|
'device.tab.playlist': 'Playlist',
|
||||||
|
'device.tab.playlist_tip': 'Conteúdo atribuído a este dispositivo. Arraste para reordenar. Adicione mídia, widgets ou páginas de quiosque.',
|
||||||
|
'device.tab.info': 'Informações do dispositivo',
|
||||||
|
'device.tab.info_tip': 'Telemetria de hardware, orientação, notas e controles do dispositivo.',
|
||||||
|
'device.tab.remote': 'Controle remoto',
|
||||||
|
'device.tab.remote_tip': 'Visualize a tela em tempo real e envie pressionamentos de tecla. Funciona no APK do Android e no player web.',
|
||||||
|
'device.draft.banner_title': 'Alterações não publicadas',
|
||||||
|
'device.draft.devices_showing_published': 'Os dispositivos ainda exibem a última versão publicada.',
|
||||||
|
'device.draft.never_published': 'Esta playlist nunca foi publicada. Os dispositivos não exibirão nada até você publicar.',
|
||||||
|
'device.draft.discard': 'Descartar',
|
||||||
|
'device.draft.publish': 'Publicar',
|
||||||
|
'device.draft.publishing': 'Publicando...',
|
||||||
|
'device.layout.label': 'Layout da tela',
|
||||||
|
'device.layout.fullscreen_default': 'Tela cheia (padrão)',
|
||||||
|
'device.layout.zones_count': '{name} ({n} zonas)',
|
||||||
|
'device.layout.template_zones_count': '[Modelo] {name} ({n} zonas)',
|
||||||
|
'device.layout.apply': 'Aplicar',
|
||||||
|
'device.playlist.label': 'Playlist',
|
||||||
|
'device.playlist.no_playlist': 'Sem playlist',
|
||||||
|
'device.playlist.copy_to_btn': 'Copiar para...',
|
||||||
|
'device.playlist.add_content_btn': 'Adicionar conteúdo',
|
||||||
|
'device.playlist.empty_title': 'Sem conteúdo atribuído',
|
||||||
|
'device.playlist.empty_desc': 'Adicione conteúdo da sua biblioteca à playlist desta tela.',
|
||||||
|
'device.playlist_picker.with_count': '{name} — {n} itens',
|
||||||
|
'device.playlist_picker.with_auto': '{name} (auto) — {n} itens',
|
||||||
|
'device.info.status': 'Status',
|
||||||
|
'device.info.ip_address': 'Endereço IP',
|
||||||
|
'device.info.battery': 'Bateria',
|
||||||
|
'device.info.storage': 'Armazenamento',
|
||||||
|
'device.info.size_free': '{size} livres',
|
||||||
|
'device.info.player_type': 'Tipo de player',
|
||||||
|
'device.info.web_player': 'Player web',
|
||||||
|
'device.info.wifi': 'Wi-Fi',
|
||||||
|
'device.info.uptime': 'Tempo ativo',
|
||||||
|
'device.info.android_version': 'Versão do Android',
|
||||||
|
'device.info.app_version': 'Versão do app',
|
||||||
|
'device.info.screen_resolution': 'Resolução',
|
||||||
|
'device.info.ram': 'RAM',
|
||||||
|
'device.info.cpu_usage': 'Uso de CPU',
|
||||||
|
'device.timeline.title': 'Linha do tempo (últimas 24 horas)',
|
||||||
|
'device.timeline.h24_ago': 'há 24h',
|
||||||
|
'device.timeline.now': 'Agora',
|
||||||
|
'device.timeline.online': 'Online',
|
||||||
|
'device.timeline.offline': 'Offline',
|
||||||
|
'device.timeline.no_data': 'Sem dados',
|
||||||
|
'device.timeline.uptime_pct_tracked': '{pct}% ativo ({n}min monitorados)',
|
||||||
|
'device.timeline.uptime_pct_no_data': '{pct}% ativo (sem dados)',
|
||||||
|
'device.form.orientation_label': 'Orientação / Rotação',
|
||||||
|
'device.form.orientation.landscape': 'Paisagem (0°)',
|
||||||
|
'device.form.orientation.portrait': 'Retrato (90° SH)',
|
||||||
|
'device.form.orientation.landscape_flipped': 'Paisagem invertida (180°)',
|
||||||
|
'device.form.orientation.portrait_flipped': 'Retrato invertido (270° SH)',
|
||||||
|
'device.form.default_content_label': 'Conteúdo padrão',
|
||||||
|
'device.form.default_content_none': 'Nenhum (mostrar "Aguardando...")',
|
||||||
|
'device.form.notes_label': 'Notas',
|
||||||
|
'device.form.notes_placeholder': 'Localização, detalhes de instalação, etc.',
|
||||||
|
'device.form.save_settings': 'Salvar configurações',
|
||||||
|
'device.ctl.reboot_device': 'Reiniciar dispositivo',
|
||||||
|
'device.ctl.screen_off': 'Desligar tela',
|
||||||
|
'device.ctl.screen_on': 'Ligar tela',
|
||||||
|
'device.ctl.launch_player': 'Iniciar player',
|
||||||
|
'device.ctl.force_update': 'Forçar atualização',
|
||||||
|
'device.ctl.shutdown': 'Desligar',
|
||||||
|
'device.remote.start_prompt': 'Clique em "Iniciar controle remoto" para começar',
|
||||||
|
'device.remote.start': 'Iniciar controle remoto',
|
||||||
|
'device.remote.stop': 'Parar controle remoto',
|
||||||
|
'device.remote.vol_up': 'Vol +',
|
||||||
|
'device.remote.vol_down': 'Vol -',
|
||||||
|
'device.remote.home': 'Início',
|
||||||
|
'device.remote.back': 'Voltar',
|
||||||
|
'device.remote.recents': 'Recentes',
|
||||||
|
'device.remote.power': 'Energia',
|
||||||
|
'device.remote.ok': 'OK',
|
||||||
|
'device.remote.settings': 'Configurações',
|
||||||
|
'device.remote.scrn_off': 'Tela off',
|
||||||
|
'device.remote.scrn_on': 'Tela on',
|
||||||
|
'device.remote.enable_system_view': 'Ativar visão do sistema',
|
||||||
|
'device.remote.system_view_tooltip': 'Solicita ao usuário do dispositivo permitir captura de tela cheia - habilita visualização remota da tela inicial, configurações e outros apps',
|
||||||
|
'device.remote.system_view_hint': 'Aprovação única necessária no dispositivo',
|
||||||
|
'device.remote.waiting_for_approval': 'Aguardando aprovação do dispositivo...',
|
||||||
|
'device.remote.system_view_enabled': 'Visão do sistema ativada',
|
||||||
|
'device.remote.unlocked_hint': 'Navegação e controles do sistema desbloqueados',
|
||||||
|
'device.pl_item.widget_with_type': 'Widget ({type})',
|
||||||
|
'device.pl_item.youtube': 'YouTube',
|
||||||
|
'device.pl_item.video': 'Vídeo',
|
||||||
|
'device.pl_item.image': 'Imagem',
|
||||||
|
'device.pl_item.zone_label': 'Zona: {id}',
|
||||||
|
'device.pl_item.no_zone': 'Sem zona',
|
||||||
|
'device.pl_item.mute': 'Silenciar',
|
||||||
|
'device.pl_item.unmute': 'Ativar áudio',
|
||||||
|
'device.pl_item.remove': 'Remover',
|
||||||
|
'device.copy.no_other_devices': 'Não há outros dispositivos para copiar',
|
||||||
|
'device.copy.prompt': 'Copiar playlist para qual dispositivo?\n\n{list}\n\nDigite o número:',
|
||||||
|
'device.copy.invalid_selection': 'Seleção inválida',
|
||||||
|
'device.copy.toast': '{n} itens copiados para {device}',
|
||||||
|
'device.assign.empty_all': 'Ainda não há conteúdo, widgets ou páginas de quiosque. Crie algo primeiro!',
|
||||||
|
'device.assign.modal_title': 'Adicionar à playlist',
|
||||||
|
'device.assign.zone_label': 'Zona',
|
||||||
|
'device.assign.zone_default': 'Padrão (tela cheia)',
|
||||||
|
'device.assign.duration_label': 'Duração (segundos, para imagens/widgets)',
|
||||||
|
'device.assign.tab.media': 'Mídia ({n})',
|
||||||
|
'device.assign.tab.widgets': 'Widgets ({n})',
|
||||||
|
'device.assign.tab.kiosk': 'Quiosque ({n})',
|
||||||
|
'device.assign.no_media': 'Nenhuma mídia enviada ainda',
|
||||||
|
'device.assign.no_widgets': 'Nenhum widget criado ainda.',
|
||||||
|
'device.assign.no_kiosk': 'Nenhuma página de quiosque ainda.',
|
||||||
|
'device.assign.create_one': 'Crie um',
|
||||||
|
'device.assign.add_selected': 'Adicionar selecionados',
|
||||||
|
'device.assign.select_first': 'Selecione algo primeiro',
|
||||||
|
'device.assign.kiosk_widget_name': 'Quiosque: {name}',
|
||||||
|
'device.toast.screenshot_requested': 'Captura solicitada',
|
||||||
|
'device.toast.renamed': 'Tela renomeada',
|
||||||
|
'device.toast.removing': 'Removendo...',
|
||||||
|
'device.toast.removed': 'Tela removida',
|
||||||
|
'device.toast.settings_saved': 'Configurações salvas',
|
||||||
|
'device.toast.published': 'Playlist publicada — dispositivos atualizados',
|
||||||
|
'device.toast.draft_discarded': 'Alterações do rascunho descartadas',
|
||||||
|
'device.toast.playlist_changed': 'Playlist alterada',
|
||||||
|
'device.toast.layout_applied': 'Layout aplicado',
|
||||||
|
'device.toast.switched_to_fullscreen': 'Alterado para tela cheia',
|
||||||
|
'device.toast.added_to_playlist': 'Adicionado à playlist',
|
||||||
|
'device.toast.unmuted': 'Áudio ativado',
|
||||||
|
'device.toast.muted': 'Silenciado',
|
||||||
|
'device.toast.zone_updated': 'Zona atualizada',
|
||||||
|
'device.toast.removed_from_playlist': 'Conteúdo removido da playlist',
|
||||||
|
'device.toast.playlist_reordered': 'Playlist reordenada',
|
||||||
|
'device.toast.reboot_sent': 'Comando de reinício enviado',
|
||||||
|
'device.toast.shutdown_sent': 'Comando de desligamento enviado',
|
||||||
|
'device.toast.screen_off_sent': 'Comando para desligar tela enviado',
|
||||||
|
'device.toast.screen_on_sent': 'Comando para ligar tela enviado',
|
||||||
|
'device.toast.launch_sent': 'Comando de início enviado',
|
||||||
|
'device.toast.update_triggered': 'Verificação de atualização disparada',
|
||||||
|
'device.toast.remote_started': 'Sessão de controle remoto iniciada',
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
'settings.title': 'Configurações',
|
||||||
|
'settings.subtitle': 'Configuração do servidor e informações de instalação',
|
||||||
|
'settings.account': 'Conta',
|
||||||
|
'settings.save_profile': 'Salvar perfil',
|
||||||
|
'settings.change_password': 'Alterar senha',
|
||||||
|
'settings.password_min_8': 'Deve ter no mínimo 8 caracteres.',
|
||||||
|
'settings.current_password': 'Senha atual',
|
||||||
|
'settings.new_password': 'Nova senha',
|
||||||
|
'settings.confirm_new_password': 'Confirmar nova senha',
|
||||||
|
'settings.sso_note': 'Você entra via {provider}. Gerencie sua senha lá.',
|
||||||
|
'settings.license': 'Licença',
|
||||||
|
'settings.license_mit': 'Licença MIT - todos os recursos incluídos.',
|
||||||
|
'settings.platform_admin_link': 'As ferramentas de admin da plataforma estão na página',
|
||||||
|
'settings.platform_admin_page_suffix': '.',
|
||||||
|
'settings.user_management': 'Gestão de usuários',
|
||||||
|
'settings.loading_users': 'Carregando usuários...',
|
||||||
|
'settings.white_label': 'Marca branca / Branding',
|
||||||
|
'settings.white_label_desc': 'Personalize a aparência do painel e do player para seus clientes.',
|
||||||
|
'settings.brand_name': 'Nome da marca',
|
||||||
|
'settings.logo_url': 'URL do logotipo',
|
||||||
|
'settings.primary_color': 'Cor primária',
|
||||||
|
'settings.bg_color': 'Cor de fundo',
|
||||||
|
'settings.custom_domain': 'Domínio personalizado',
|
||||||
|
'settings.favicon_url': 'URL do favicon',
|
||||||
|
'settings.custom_css': 'CSS personalizado (opcional)',
|
||||||
|
'settings.hide_branding': 'Ocultar marca "ScreenTinker"',
|
||||||
|
'settings.save_branding': 'Salvar branding',
|
||||||
|
'settings.preview': 'Pré-visualizar',
|
||||||
|
'settings.white_label_enterprise_only': 'Branding personalizado disponível no plano Enterprise',
|
||||||
|
'settings.view_plans': 'Ver planos',
|
||||||
|
'settings.server_info': 'Informações do servidor',
|
||||||
|
'settings.server_url': 'URL do servidor',
|
||||||
|
'settings.api_endpoint': 'Endpoint da API',
|
||||||
|
'settings.server_url_hint': 'Use esta URL ao configurar o app Android',
|
||||||
|
'settings.setup_guide': 'Guia de instalação',
|
||||||
|
'settings.setup_step_1': 'Instale o APK do ScreenTinker na sua TV via sideloading',
|
||||||
|
'settings.setup_step_2_prefix': 'Abra o app e digite esta URL do servidor:',
|
||||||
|
'settings.setup_step_3': 'O app exibirá um código de pareamento de 6 dígitos',
|
||||||
|
'settings.setup_step_4': 'Clique em "Adicionar tela" no painel e digite o código',
|
||||||
|
'settings.setup_step_5': 'Envie conteúdo na Biblioteca de conteúdo',
|
||||||
|
'settings.setup_step_6': 'Atribua conteúdo à playlist da tela',
|
||||||
|
'settings.your_data': 'Seus dados',
|
||||||
|
'settings.your_data_desc': 'Exporte ou importe seus dispositivos, conteúdo, layouts, agendas e todas as configurações. Use para migrar entre nuvem e auto-hospedado.',
|
||||||
|
'settings.export_my_data': 'Exportar meus dados',
|
||||||
|
'settings.include_media_zip': 'Incluir arquivos de mídia (ZIP)',
|
||||||
|
'settings.import_data': 'Importar dados',
|
||||||
|
'settings.language': 'Idioma',
|
||||||
|
'settings.about': 'Sobre',
|
||||||
|
'settings.about_tagline': 'Sistema de gerenciamento de sinalização digital.',
|
||||||
|
'settings.third_party_licenses': 'Licenças de terceiros',
|
||||||
|
'settings.import.reading_file': 'Lendo arquivo...',
|
||||||
|
'settings.import.zip_detected': 'Exportação ZIP detectada: <strong>{name}</strong> ({size} MB)<br>Contém dados + arquivos de mídia.',
|
||||||
|
'settings.import.confirm': 'Confirmar importação',
|
||||||
|
'settings.import.invalid_file': 'Arquivo inválido. Deve ser JSON ou ZIP de exportação do ScreenTinker.',
|
||||||
|
'settings.import.summary_devices': '{n} dispositivos',
|
||||||
|
'settings.import.summary_content': '{n} itens de conteúdo',
|
||||||
|
'settings.import.summary_widgets': '{n} widgets',
|
||||||
|
'settings.import.summary_layouts': '{n} layouts',
|
||||||
|
'settings.import.summary_schedules': '{n} agendas',
|
||||||
|
'settings.import.summary_walls': '{n} paredes de vídeo',
|
||||||
|
'settings.import.summary_kiosk': '{n} páginas de quiosque',
|
||||||
|
'settings.import.found_summary': 'Encontrado: {summary}.<br>De: {email} (exportado {date})',
|
||||||
|
'settings.import.empty_export': 'exportação vazia',
|
||||||
|
'settings.import.uploading_zip': 'Enviando e importando... Pode demorar para arquivos grandes.',
|
||||||
|
'settings.import.importing': 'Importando...',
|
||||||
|
'settings.import.complete': 'Importação concluída: {imported}.',
|
||||||
|
'settings.import.pairing_codes_title': 'Códigos de pareamento:',
|
||||||
|
'settings.import.pairing_codes_hint': 'Digite estes códigos em cada dispositivo para revinculá-los. Atribuições e agendas serão preservadas.',
|
||||||
|
'settings.import.failed': 'Falha na importação',
|
||||||
|
'settings.import.failed_with_error': 'Falha na importação: {error}',
|
||||||
|
'settings.import.read_failed': 'Falha ao ler o arquivo: {error}',
|
||||||
|
'settings.toast.support_token_generated': 'Token de suporte gerado (válido {hours}h)',
|
||||||
|
'settings.toast.import_success': 'Dados importados com sucesso',
|
||||||
|
'settings.toast.name_required': 'O nome não pode ficar em branco',
|
||||||
|
'settings.toast.profile_saved': 'Perfil salvo',
|
||||||
|
'settings.toast.current_password_required': 'Digite sua senha atual',
|
||||||
|
'settings.toast.new_password_min_8': 'A nova senha deve ter no mínimo 8 caracteres',
|
||||||
|
'settings.toast.passwords_dont_match': 'As novas senhas não conferem',
|
||||||
|
'settings.toast.password_changed': 'Senha alterada',
|
||||||
|
'settings.toast.branding_saved': 'Branding salvo',
|
||||||
|
'settings.toast.preview_applied': 'Pré-visualização aplicada (recarregue para redefinir)',
|
||||||
|
'settings.toast.plan_updated': 'Plano atualizado',
|
||||||
|
'settings.toast.user_removed': 'Usuário removido',
|
||||||
|
'settings.user.col_user': 'Usuário',
|
||||||
|
'settings.user.col_auth': 'Auth',
|
||||||
|
'settings.user.col_role': 'Função',
|
||||||
|
'settings.user.col_plan': 'Plano',
|
||||||
|
'settings.user.col_actions': 'Ações',
|
||||||
|
'settings.user.remove': 'Remover',
|
||||||
|
'settings.user.you': 'Você',
|
||||||
|
'settings.user.confirm': 'Confirmar?',
|
||||||
|
'settings.user.count_one': '1 usuário registrado',
|
||||||
|
'settings.user.count_other': '{n} usuários registrados',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { api } from '../api.js';
|
||||||
import { on, off, requestScreenshot, startRemote, stopRemote, sendTouch, sendKey, sendCommand } from '../socket.js';
|
import { on, off, requestScreenshot, startRemote, stopRemote, sendTouch, sendKey, sendCommand } from '../socket.js';
|
||||||
import { showToast } from '../components/toast.js';
|
import { showToast } from '../components/toast.js';
|
||||||
import { esc } from '../utils.js';
|
import { esc } from '../utils.js';
|
||||||
|
import { t, tn } from '../i18n.js';
|
||||||
|
|
||||||
let currentDevice = null;
|
let currentDevice = null;
|
||||||
let statusHandler = null;
|
let statusHandler = null;
|
||||||
|
|
@ -33,10 +34,10 @@ export function render(container, deviceId) {
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>
|
<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>
|
||||||
</svg>
|
</svg>
|
||||||
Back to Displays
|
${t('device.back')}
|
||||||
</a>
|
</a>
|
||||||
<div id="deviceContent">
|
<div id="deviceContent">
|
||||||
<div class="empty-state"><h3>Loading...</h3></div>
|
<div class="empty-state"><h3>${t('common.loading')}</h3></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -94,7 +95,7 @@ export function render(container, deviceId) {
|
||||||
if (data.device_id !== deviceId) return;
|
if (data.device_id !== deviceId) return;
|
||||||
const el = document.getElementById('nowPlayingInfo');
|
const el = document.getElementById('nowPlayingInfo');
|
||||||
if (el && data.current_content_id) {
|
if (el && data.current_content_id) {
|
||||||
el.textContent = `Playing: ${data.current_content_id}`;
|
el.textContent = t('device.now_playing_id', { id: data.current_content_id });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -115,26 +116,26 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
<div class="device-header-left">
|
<div class="device-header-left">
|
||||||
<h1 id="deviceName">${device.name}</h1>
|
<h1 id="deviceName">${device.name}</h1>
|
||||||
<span class="device-status-badge ${device.status}">${device.status}</span>
|
<span class="device-status-badge ${device.status}">${device.status}</span>
|
||||||
${device.owner_name || device.owner_email ? `<span style="font-size:12px;color:var(--text-muted)">Owner: ${device.owner_name || device.owner_email}</span>` : ''}
|
${device.owner_name || device.owner_email ? `<span style="font-size:12px;color:var(--text-muted)">${t('device.owner_label', { owner: device.owner_name || device.owner_email })}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:8px">
|
<div style="display:flex;gap:8px">
|
||||||
<button class="btn btn-secondary btn-sm" id="renameBtn">Rename</button>
|
<button class="btn btn-secondary btn-sm" id="renameBtn">${t('device.rename')}</button>
|
||||||
<button class="btn btn-secondary btn-sm" id="screenshotBtn">
|
<button class="btn btn-secondary btn-sm" id="screenshotBtn">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/>
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/>
|
||||||
<polyline points="21 15 16 10 5 21"/>
|
<polyline points="21 15 16 10 5 21"/>
|
||||||
</svg>
|
</svg>
|
||||||
Screenshot
|
${t('device.screenshot_btn')}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger btn-sm" id="deleteDeviceBtn">Remove</button>
|
<button class="btn btn-danger btn-sm" id="deleteDeviceBtn">${t('device.remove')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab active" data-tab="nowplaying">Now Playing <span class="help-tip" data-tip="Live screenshot of what's currently displaying on this device.">?</span></div>
|
<div class="tab active" data-tab="nowplaying">${t('device.tab.now_playing')} <span class="help-tip" data-tip="${t('device.tab.now_playing_tip')}">?</span></div>
|
||||||
<div class="tab" data-tab="playlist">Playlist <span class="help-tip" data-tip="Content assigned to this device. Drag items to reorder. Add media, widgets, or kiosk pages.">?</span></div>
|
<div class="tab" data-tab="playlist">${t('device.tab.playlist')} <span class="help-tip" data-tip="${t('device.tab.playlist_tip')}">?</span></div>
|
||||||
<div class="tab" data-tab="info">Device Info <span class="help-tip" data-tip="Hardware telemetry, orientation settings, notes, and device controls.">?</span></div>
|
<div class="tab" data-tab="info">${t('device.tab.info')} <span class="help-tip" data-tip="${t('device.tab.info_tip')}">?</span></div>
|
||||||
<div class="tab" data-tab="remote">Remote Control <span class="help-tip" data-tip="View the device screen in real-time and send key presses. Works on Android APK and web player.">?</span></div>
|
<div class="tab" data-tab="remote">${t('device.tab.remote')} <span class="help-tip" data-tip="${t('device.tab.remote_tip')}">?</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Now Playing Tab -->
|
<!-- Now Playing Tab -->
|
||||||
|
|
@ -148,12 +149,12 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
<line x1="8" y1="21" x2="16" y2="21"/>
|
<line x1="8" y1="21" x2="16" y2="21"/>
|
||||||
<line x1="12" y1="17" x2="12" y2="21"/>
|
<line x1="12" y1="17" x2="12" y2="21"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>No screenshot available. Click "Screenshot" to capture one.</span>
|
<span>${t('device.no_screenshot')}</span>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<p id="nowPlayingInfo" style="color:var(--text-secondary);font-size:13px;">
|
<p id="nowPlayingInfo" style="color:var(--text-secondary);font-size:13px;">
|
||||||
${device.assignments?.length ? `${device.assignments.length} item(s) in playlist` : 'No content assigned'}
|
${device.assignments?.length ? tn('device.playlist_count', device.assignments.length) : t('device.no_content_assigned')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -164,13 +165,13 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
<div style="display:flex;align-items:center;gap:10px;color:#fbbf24">
|
<div style="display:flex;align-items:center;gap:10px;color:#fbbf24">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight:600;font-size:14px">Unpublished changes</div>
|
<div style="font-weight:600;font-size:14px">${t('device.draft.banner_title')}</div>
|
||||||
<div style="font-size:12px;color:#fcd34d;opacity:0.85">${device.playlist_has_published ? 'Devices are still showing the last published version.' : 'This playlist has never been published. Devices will show nothing until you publish.'}</div>
|
<div style="font-size:12px;color:#fcd34d;opacity:0.85">${device.playlist_has_published ? t('device.draft.devices_showing_published') : t('device.draft.never_published')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:8px;flex-shrink:0">
|
<div style="display:flex;gap:8px;flex-shrink:0">
|
||||||
${device.playlist_has_published ? '<button class="btn btn-secondary btn-sm" id="deviceDiscardDraftBtn" style="color:#fbbf24;border-color:#92400e">Discard</button>' : ''}
|
${device.playlist_has_published ? `<button class="btn btn-secondary btn-sm" id="deviceDiscardDraftBtn" style="color:#fbbf24;border-color:#92400e">${t('device.draft.discard')}</button>` : ''}
|
||||||
<button class="btn btn-sm" id="devicePublishBtn" style="background:#f59e0b;color:#000;font-weight:600;border:none">Publish</button>
|
<button class="btn btn-sm" id="devicePublishBtn" style="background:#f59e0b;color:#000;font-weight:600;border:none">${t('device.draft.publish')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
@ -180,28 +181,28 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/>
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div style="flex:1">
|
<div style="flex:1">
|
||||||
<div style="font-size:12px;color:var(--text-muted);margin-bottom:4px">Screen Layout</div>
|
<div style="font-size:12px;color:var(--text-muted);margin-bottom:4px">${t('device.layout.label')}</div>
|
||||||
<select id="deviceLayoutSelect" class="input" style="background:var(--bg-input);padding:4px 8px;font-size:13px">
|
<select id="deviceLayoutSelect" class="input" style="background:var(--bg-input);padding:4px 8px;font-size:13px">
|
||||||
<option value="">Fullscreen (default)</option>
|
<option value="">${t('device.layout.fullscreen_default')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary btn-sm" id="applyLayoutBtn">Apply</button>
|
<button class="btn btn-secondary btn-sm" id="applyLayoutBtn">${t('device.layout.apply')}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||||||
<div style="display:flex;align-items:center;gap:12px">
|
<div style="display:flex;align-items:center;gap:12px">
|
||||||
<h3 style="font-size:16px">Playlist</h3>
|
<h3 style="font-size:16px">${t('device.playlist.label')}</h3>
|
||||||
<select class="input" id="playlistPicker" style="font-size:12px;padding:4px 8px;width:200px">
|
<select class="input" id="playlistPicker" style="font-size:12px;padding:4px 8px;width:200px">
|
||||||
<option value="">No playlist</option>
|
<option value="">${t('device.playlist.no_playlist')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:6px">
|
<div style="display:flex;gap:6px">
|
||||||
<button class="btn btn-secondary btn-sm" id="copyPlaylistBtn">Copy To...</button>
|
<button class="btn btn-secondary btn-sm" id="copyPlaylistBtn">${t('device.playlist.copy_to_btn')}</button>
|
||||||
<button class="btn btn-primary btn-sm" id="addContentBtn">
|
<button class="btn btn-primary btn-sm" id="addContentBtn">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||||
</svg>
|
</svg>
|
||||||
Add Content
|
${t('device.playlist.add_content_btn')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -214,16 +215,16 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
<div class="tab-content" id="tab-info">
|
<div class="tab-content" id="tab-info">
|
||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">Status</div>
|
<div class="info-card-label">${t('device.info.status')}</div>
|
||||||
<div class="info-card-value" style="color:var(--${device.status === 'online' ? 'success' : 'danger'})">${device.status}</div>
|
<div class="info-card-value" style="color:var(--${device.status === 'online' ? 'success' : 'danger'})">${device.status}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">IP Address</div>
|
<div class="info-card-label">${t('device.info.ip_address')}</div>
|
||||||
<div class="info-card-value small">${device.ip_address || '--'}</div>
|
<div class="info-card-value small">${device.ip_address || '--'}</div>
|
||||||
</div>
|
</div>
|
||||||
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">Battery</div>
|
<div class="info-card-label">${t('device.info.battery')}</div>
|
||||||
<div class="info-card-value" id="telBattery">${latestTelemetry.battery_level != null ? latestTelemetry.battery_level + '%' : '--'}</div>
|
<div class="info-card-value" id="telBattery">${latestTelemetry.battery_level != null ? latestTelemetry.battery_level + '%' : '--'}</div>
|
||||||
${latestTelemetry.battery_level != null ? `
|
${latestTelemetry.battery_level != null ? `
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
|
|
@ -232,8 +233,8 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">Storage</div>
|
<div class="info-card-label">${t('device.info.storage')}</div>
|
||||||
<div class="info-card-value small" id="telStorage">${latestTelemetry.storage_free_mb ? formatBytes(latestTelemetry.storage_free_mb) + ' free' : '--'}</div>
|
<div class="info-card-value small" id="telStorage">${latestTelemetry.storage_free_mb ? t('device.info.size_free', { size: formatBytes(latestTelemetry.storage_free_mb) }) : '--'}</div>
|
||||||
${latestTelemetry.storage_total_mb ? `
|
${latestTelemetry.storage_total_mb ? `
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-bar-fill ${((latestTelemetry.storage_total_mb - latestTelemetry.storage_free_mb) / latestTelemetry.storage_total_mb) < 0.8 ? 'success' : 'warning'}"
|
<div class="progress-bar-fill ${((latestTelemetry.storage_total_mb - latestTelemetry.storage_free_mb) / latestTelemetry.storage_total_mb) < 0.8 ? 'success' : 'warning'}"
|
||||||
|
|
@ -242,42 +243,42 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
</div>
|
</div>
|
||||||
` : `
|
` : `
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">Player Type</div>
|
<div class="info-card-label">${t('device.info.player_type')}</div>
|
||||||
<div class="info-card-value small">Web Player</div>
|
<div class="info-card-value small">${t('device.info.web_player')}</div>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">WiFi</div>
|
<div class="info-card-label">${t('device.info.wifi')}</div>
|
||||||
<div class="info-card-value small" id="telWifi">${latestTelemetry.wifi_ssid || '--'}</div>
|
<div class="info-card-value small" id="telWifi">${latestTelemetry.wifi_ssid || '--'}</div>
|
||||||
<div style="font-size:11px;color:var(--text-muted);margin-top:2px" id="telRssi">${latestTelemetry.wifi_rssi ? latestTelemetry.wifi_rssi + ' dBm' : ''}</div>
|
<div style="font-size:11px;color:var(--text-muted);margin-top:2px" id="telRssi">${latestTelemetry.wifi_rssi ? latestTelemetry.wifi_rssi + ' dBm' : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">Uptime</div>
|
<div class="info-card-label">${t('device.info.uptime')}</div>
|
||||||
<div class="info-card-value small" id="telUptime">${formatUptime(latestTelemetry.uptime_seconds)}</div>
|
<div class="info-card-value small" id="telUptime">${formatUptime(latestTelemetry.uptime_seconds)}</div>
|
||||||
</div>
|
</div>
|
||||||
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">Android Version</div>
|
<div class="info-card-label">${t('device.info.android_version')}</div>
|
||||||
<div class="info-card-value small">${device.android_version}</div>
|
<div class="info-card-value small">${device.android_version}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">App Version</div>
|
<div class="info-card-label">${t('device.info.app_version')}</div>
|
||||||
<div class="info-card-value small">${device.app_version || '--'}</div>
|
<div class="info-card-value small">${device.app_version || '--'}</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">Screen Resolution</div>
|
<div class="info-card-label">${t('device.info.screen_resolution')}</div>
|
||||||
<div class="info-card-value small">${device.screen_width && device.screen_height ? device.screen_width + 'x' + device.screen_height : '--'}</div>
|
<div class="info-card-value small">${device.screen_width && device.screen_height ? device.screen_width + 'x' + device.screen_height : '--'}</div>
|
||||||
</div>
|
</div>
|
||||||
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">RAM</div>
|
<div class="info-card-label">${t('device.info.ram')}</div>
|
||||||
<div class="info-card-value small" id="telRam">${latestTelemetry.ram_free_mb ? formatBytes(latestTelemetry.ram_free_mb) + ' free' : '--'}</div>
|
<div class="info-card-value small" id="telRam">${latestTelemetry.ram_free_mb ? t('device.info.size_free', { size: formatBytes(latestTelemetry.ram_free_mb) }) : '--'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">CPU Usage</div>
|
<div class="info-card-label">${t('device.info.cpu_usage')}</div>
|
||||||
<div class="info-card-value small" id="telCpu">${latestTelemetry.cpu_usage != null ? latestTelemetry.cpu_usage.toFixed(1) + '%' : '--'}</div>
|
<div class="info-card-value small" id="telCpu">${latestTelemetry.cpu_usage != null ? latestTelemetry.cpu_usage.toFixed(1) + '%' : '--'}</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
@ -285,16 +286,16 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
|
|
||||||
<!-- Uptime Timeline (24h) -->
|
<!-- Uptime Timeline (24h) -->
|
||||||
<div style="margin-top:20px">
|
<div style="margin-top:20px">
|
||||||
<h4 style="font-size:13px;margin-bottom:8px">Uptime Timeline (Last 24 Hours)</h4>
|
<h4 style="font-size:13px;margin-bottom:8px">${t('device.timeline.title')}</h4>
|
||||||
<div id="uptimeTimeline" style="display:flex;height:32px;border-radius:4px;overflow:hidden;border:1px solid var(--border);background:var(--bg-primary)"></div>
|
<div id="uptimeTimeline" style="display:flex;height:32px;border-radius:4px;overflow:hidden;border:1px solid var(--border);background:var(--bg-primary)"></div>
|
||||||
<div style="display:flex;justify-content:space-between;margin-top:4px">
|
<div style="display:flex;justify-content:space-between;margin-top:4px">
|
||||||
<span style="font-size:10px;color:var(--text-muted)">24h ago</span>
|
<span style="font-size:10px;color:var(--text-muted)">${t('device.timeline.h24_ago')}</span>
|
||||||
<span style="font-size:10px;color:var(--text-muted)">Now</span>
|
<span style="font-size:10px;color:var(--text-muted)">${t('device.timeline.now')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:12px;margin-top:8px;font-size:11px;color:var(--text-muted)">
|
<div style="display:flex;gap:12px;margin-top:8px;font-size:11px;color:var(--text-muted)">
|
||||||
<span><span style="display:inline-block;width:10px;height:10px;background:var(--success);border-radius:2px;vertical-align:-1px"></span> Online</span>
|
<span><span style="display:inline-block;width:10px;height:10px;background:var(--success);border-radius:2px;vertical-align:-1px"></span> ${t('device.timeline.online')}</span>
|
||||||
<span><span style="display:inline-block;width:10px;height:10px;background:var(--danger);border-radius:2px;vertical-align:-1px"></span> Offline</span>
|
<span><span style="display:inline-block;width:10px;height:10px;background:var(--danger);border-radius:2px;vertical-align:-1px"></span> ${t('device.timeline.offline')}</span>
|
||||||
<span><span style="display:inline-block;width:10px;height:10px;background:var(--bg-primary);border:1px solid var(--border);border-radius:2px;vertical-align:-1px"></span> No data</span>
|
<span><span style="display:inline-block;width:10px;height:10px;background:var(--bg-primary);border:1px solid var(--border);border-radius:2px;vertical-align:-1px"></span> ${t('device.timeline.no_data')}</span>
|
||||||
<span id="uptimePercent" style="margin-left:auto;font-weight:600"></span>
|
<span id="uptimePercent" style="margin-left:auto;font-weight:600"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -302,63 +303,63 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
<div style="margin-top:20px">
|
<div style="margin-top:20px">
|
||||||
<div style="display:flex;gap:12px;margin-bottom:12px">
|
<div style="display:flex;gap:12px;margin-bottom:12px">
|
||||||
<div class="form-group" style="flex:1;margin:0">
|
<div class="form-group" style="flex:1;margin:0">
|
||||||
<label>Orientation / Rotation</label>
|
<label>${t('device.form.orientation_label')}</label>
|
||||||
<select id="deviceOrientation" class="input" style="background:var(--bg-input)">
|
<select id="deviceOrientation" class="input" style="background:var(--bg-input)">
|
||||||
<option value="landscape" ${'landscape' === (device.orientation || 'landscape') ? 'selected' : ''}>Landscape (0°)</option>
|
<option value="landscape" ${'landscape' === (device.orientation || 'landscape') ? 'selected' : ''}>${t('device.form.orientation.landscape')}</option>
|
||||||
<option value="portrait" ${'portrait' === device.orientation ? 'selected' : ''}>Portrait (90° CW)</option>
|
<option value="portrait" ${'portrait' === device.orientation ? 'selected' : ''}>${t('device.form.orientation.portrait')}</option>
|
||||||
<option value="landscape-flipped" ${'landscape-flipped' === device.orientation ? 'selected' : ''}>Landscape Flipped (180°)</option>
|
<option value="landscape-flipped" ${'landscape-flipped' === device.orientation ? 'selected' : ''}>${t('device.form.orientation.landscape_flipped')}</option>
|
||||||
<option value="portrait-flipped" ${'portrait-flipped' === device.orientation ? 'selected' : ''}>Portrait Flipped (270° CW)</option>
|
<option value="portrait-flipped" ${'portrait-flipped' === device.orientation ? 'selected' : ''}>${t('device.form.orientation.portrait_flipped')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="flex:1;margin:0">
|
<div class="form-group" style="flex:1;margin:0">
|
||||||
<label>Default Content</label>
|
<label>${t('device.form.default_content_label')}</label>
|
||||||
<select id="deviceDefaultContent" class="input" style="background:var(--bg-input)">
|
<select id="deviceDefaultContent" class="input" style="background:var(--bg-input)">
|
||||||
<option value="">None (show "Waiting...")</option>
|
<option value="">${t('device.form.default_content_none')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Notes</label>
|
<label>${t('device.form.notes_label')}</label>
|
||||||
<textarea id="deviceNotes" class="input" rows="3" placeholder="Location, setup details, etc." style="resize:vertical">${esc(device.notes || '')}</textarea>
|
<textarea id="deviceNotes" class="input" rows="3" placeholder="${t('device.form.notes_placeholder')}" style="resize:vertical">${esc(device.notes || '')}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary btn-sm" id="saveNotesBtn">Save Settings</button>
|
<button class="btn btn-secondary btn-sm" id="saveNotesBtn">${t('device.form.save_settings')}</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:20px;display:flex;gap:8px;flex-wrap:wrap">
|
<div style="margin-top:20px;display:flex;gap:8px;flex-wrap:wrap">
|
||||||
<button class="btn btn-secondary btn-sm" id="rebootBtn">
|
<button class="btn btn-secondary btn-sm" id="rebootBtn">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
<polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
||||||
</svg>
|
</svg>
|
||||||
Reboot Device
|
${t('device.ctl.reboot_device')}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary btn-sm" id="screenOffBtn">
|
<button class="btn btn-secondary btn-sm" id="screenOffBtn">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>
|
||||||
</svg>
|
</svg>
|
||||||
Screen Off
|
${t('device.ctl.screen_off')}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary btn-sm" id="screenOnBtn">
|
<button class="btn btn-secondary btn-sm" id="screenOnBtn">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||||
</svg>
|
</svg>
|
||||||
Screen On
|
${t('device.ctl.screen_on')}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary btn-sm" id="launchAppBtn">
|
<button class="btn btn-secondary btn-sm" id="launchAppBtn">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<polygon points="5 3 19 12 5 21 5 3"/>
|
<polygon points="5 3 19 12 5 21 5 3"/>
|
||||||
</svg>
|
</svg>
|
||||||
Launch Player
|
${t('device.ctl.launch_player')}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary btn-sm" id="forceUpdateBtn">
|
<button class="btn btn-secondary btn-sm" id="forceUpdateBtn">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
|
||||||
</svg>
|
</svg>
|
||||||
Force Update
|
${t('device.ctl.force_update')}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger btn-sm" id="shutdownBtn">
|
<button class="btn btn-danger btn-sm" id="shutdownBtn">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/>
|
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/>
|
||||||
</svg>
|
</svg>
|
||||||
Shutdown
|
${t('device.ctl.shutdown')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -375,24 +376,24 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
<line x1="8" y1="21" x2="16" y2="21"/>
|
<line x1="8" y1="21" x2="16" y2="21"/>
|
||||||
<line x1="12" y1="17" x2="12" y2="21"/>
|
<line x1="12" y1="17" x2="12" y2="21"/>
|
||||||
</svg>
|
</svg>
|
||||||
<p style="color:var(--text-secondary)">Click "Start Remote" to begin</p>
|
<p style="color:var(--text-secondary)">${t('device.remote.start_prompt')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="remote-controls">
|
<div class="remote-controls">
|
||||||
<button class="btn btn-primary" id="startRemoteBtn">Start Remote</button>
|
<button class="btn btn-primary" id="startRemoteBtn">${t('device.remote.start')}</button>
|
||||||
<button class="btn btn-secondary" id="stopRemoteBtn" style="display:none">Stop Remote</button>
|
<button class="btn btn-secondary" id="stopRemoteBtn" style="display:none">${t('device.remote.stop')}</button>
|
||||||
<hr style="border-color:var(--border);margin:8px 0">
|
<hr style="border-color:var(--border);margin:8px 0">
|
||||||
<!-- Always available -->
|
<!-- Always available -->
|
||||||
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_VOLUME_UP')">Vol +</button>
|
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_VOLUME_UP')">${t('device.remote.vol_up')}</button>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_VOLUME_DOWN')">Vol -</button>
|
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_VOLUME_DOWN')">${t('device.remote.vol_down')}</button>
|
||||||
<hr style="border-color:var(--border);margin:8px 0">
|
<hr style="border-color:var(--border);margin:8px 0">
|
||||||
<!-- System View controls (disabled until enabled) -->
|
<!-- System View controls (disabled until enabled) -->
|
||||||
<div id="systemViewControls" style="opacity:0.4;pointer-events:none">
|
<div id="systemViewControls" style="opacity:0.4;pointer-events:none">
|
||||||
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_HOME')">Home</button>
|
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_HOME')">${t('device.remote.home')}</button>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_BACK')">Back</button>
|
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_BACK')">${t('device.remote.back')}</button>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_APP_SWITCH')">Recents</button>
|
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_APP_SWITCH')">${t('device.remote.recents')}</button>
|
||||||
<button class="btn btn-danger btn-sm" onclick="window._sendKey('KEYCODE_POWER')">Power</button>
|
<button class="btn btn-danger btn-sm" onclick="window._sendKey('KEYCODE_POWER')">${t('device.remote.power')}</button>
|
||||||
<hr style="border-color:var(--border);margin:8px 0">
|
<hr style="border-color:var(--border);margin:8px 0">
|
||||||
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_DPAD_UP')">▲</button>
|
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_DPAD_UP')">▲</button>
|
||||||
<div style="display:flex;gap:4px">
|
<div style="display:flex;gap:4px">
|
||||||
|
|
@ -400,19 +401,19 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
<button class="btn btn-secondary btn-sm" style="flex:1" onclick="window._sendKey('KEYCODE_DPAD_RIGHT')">▶</button>
|
<button class="btn btn-secondary btn-sm" style="flex:1" onclick="window._sendKey('KEYCODE_DPAD_RIGHT')">▶</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_DPAD_DOWN')">▼</button>
|
<button class="btn btn-secondary btn-sm" onclick="window._sendKey('KEYCODE_DPAD_DOWN')">▼</button>
|
||||||
<button class="btn btn-primary btn-sm" onclick="window._sendKey('KEYCODE_DPAD_CENTER')">OK</button>
|
<button class="btn btn-primary btn-sm" onclick="window._sendKey('KEYCODE_DPAD_CENTER')">${t('device.remote.ok')}</button>
|
||||||
<hr style="border-color:var(--border);margin:8px 0">
|
<hr style="border-color:var(--border);margin:8px 0">
|
||||||
<button class="btn btn-secondary btn-sm" onclick="window._sendCmd('settings')">Settings</button>
|
<button class="btn btn-secondary btn-sm" onclick="window._sendCmd('settings')">${t('device.remote.settings')}</button>
|
||||||
<hr style="border-color:var(--border);margin:8px 0">
|
<hr style="border-color:var(--border);margin:8px 0">
|
||||||
<div style="display:flex;gap:4px">
|
<div style="display:flex;gap:4px">
|
||||||
<button class="btn btn-secondary btn-sm" style="flex:1" onclick="window._sendCmd('screen_off')">Scrn Off</button>
|
<button class="btn btn-secondary btn-sm" style="flex:1" onclick="window._sendCmd('screen_off')">${t('device.remote.scrn_off')}</button>
|
||||||
<button class="btn btn-secondary btn-sm" style="flex:1" onclick="window._sendCmd('screen_on')">Scrn On</button>
|
<button class="btn btn-secondary btn-sm" style="flex:1" onclick="window._sendCmd('screen_on')">${t('device.remote.scrn_on')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary btn-sm" id="enableSystemCaptureBtn" onclick="window._enableSystemView()" title="Prompts the device user to allow full screen capture - enables remote view of home screen, settings, and other apps" style="margin-top:8px">
|
<button class="btn btn-primary btn-sm" id="enableSystemCaptureBtn" onclick="window._enableSystemView()" title="${t('device.remote.system_view_tooltip')}" style="margin-top:8px">
|
||||||
Enable System View
|
${t('device.remote.enable_system_view')}
|
||||||
</button>
|
</button>
|
||||||
<span id="systemViewHint" style="font-size:10px;color:var(--text-muted);line-height:1.2;display:block;margin-top:4px">Requires one-time approval on device</span>
|
<span id="systemViewHint" style="font-size:10px;color:var(--text-muted);line-height:1.2;display:block;margin-top:4px">${t('device.remote.system_view_hint')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -431,13 +432,13 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
// Unlock the system controls after a short delay (user needs to tap "Start now" on device)
|
// Unlock the system controls after a short delay (user needs to tap "Start now" on device)
|
||||||
const btn = document.getElementById('enableSystemCaptureBtn');
|
const btn = document.getElementById('enableSystemCaptureBtn');
|
||||||
const hint = document.getElementById('systemViewHint');
|
const hint = document.getElementById('systemViewHint');
|
||||||
if (btn) { btn.textContent = 'Waiting for device approval...'; btn.disabled = true; }
|
if (btn) { btn.textContent = t('device.remote.waiting_for_approval'); btn.disabled = true; }
|
||||||
// Check periodically if the device granted it (we'll know because screenshots keep coming even after Home)
|
// Check periodically if the device granted it (we'll know because screenshots keep coming even after Home)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const controls = document.getElementById('systemViewControls');
|
const controls = document.getElementById('systemViewControls');
|
||||||
if (controls) { controls.style.opacity = '1'; controls.style.pointerEvents = 'auto'; }
|
if (controls) { controls.style.opacity = '1'; controls.style.pointerEvents = 'auto'; }
|
||||||
if (btn) { btn.textContent = 'System View Enabled'; btn.style.background = 'var(--success)'; }
|
if (btn) { btn.textContent = t('device.remote.system_view_enabled'); btn.style.background = 'var(--success)'; }
|
||||||
if (hint) hint.textContent = 'Navigation and system controls unlocked';
|
if (hint) hint.textContent = t('device.remote.unlocked_hint');
|
||||||
}, 5000);
|
}, 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -465,13 +466,13 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
contentEl.innerHTML = `<div class="empty-state"><h3>Failed to load device</h3><p>${esc(err.message)}</p></div>`;
|
contentEl.innerHTML = `<div class="empty-state"><h3>${t('device.failed_load')}</h3><p>${esc(err.message)}</p></div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPlaylist(assignments) {
|
function renderPlaylist(assignments) {
|
||||||
if (!assignments.length) {
|
if (!assignments.length) {
|
||||||
return `<div class="empty-state"><h3>No content assigned</h3><p>Add content from your library to this display's playlist.</p></div>`;
|
return `<div class="empty-state"><h3>${t('device.playlist.empty_title')}</h3><p>${t('device.playlist.empty_desc')}</p></div>`;
|
||||||
}
|
}
|
||||||
return assignments.map((a, i) => `
|
return assignments.map((a, i) => `
|
||||||
<div class="playlist-item" data-assignment-id="${a.id}" draggable="true" data-sort="${i}">
|
<div class="playlist-item" data-assignment-id="${a.id}" draggable="true" data-sort="${i}">
|
||||||
|
|
@ -493,10 +494,10 @@ function renderPlaylist(assignments) {
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
<div class="playlist-item-info">
|
<div class="playlist-item-info">
|
||||||
<div class="playlist-item-name">${esc(a.filename || a.widget_name || 'Unknown')}</div>
|
<div class="playlist-item-name">${esc(a.filename || a.widget_name || t('common.unknown'))}</div>
|
||||||
<div class="playlist-item-meta">
|
<div class="playlist-item-meta">
|
||||||
${a.widget_id && !a.content_id ? `Widget (${a.widget_type || 'custom'})` : a.mime_type === 'video/youtube' ? 'YouTube' : a.mime_type?.startsWith('video/') ? 'Video' : 'Image'}
|
${a.widget_id && !a.content_id ? t('device.pl_item.widget_with_type', { type: a.widget_type || 'custom' }) : a.mime_type === 'video/youtube' ? t('device.pl_item.youtube') : a.mime_type?.startsWith('video/') ? t('device.pl_item.video') : t('device.pl_item.image')}
|
||||||
${a.zone_id ? ` · <span style="color:var(--accent)">Zone: ${a.zone_id.slice(0,8)}</span>` : ''}
|
${a.zone_id ? ` · <span style="color:var(--accent)">${t('device.pl_item.zone_label', { id: a.zone_id.slice(0,8) })}</span>` : ''}
|
||||||
${a.content_duration ? ` · ${Math.floor(a.content_duration / 60)}:${String(Math.floor(a.content_duration % 60)).padStart(2, '0')}` : ''}
|
${a.content_duration ? ` · ${Math.floor(a.content_duration / 60)}:${String(Math.floor(a.content_duration % 60)).padStart(2, '0')}` : ''}
|
||||||
${!a.content_duration && !a.mime_type?.startsWith('video/') && a.duration_sec ? ` · ${a.duration_sec}s` : ''}
|
${!a.content_duration && !a.mime_type?.startsWith('video/') && a.duration_sec ? ` · ${a.duration_sec}s` : ''}
|
||||||
${a.schedule_start ? ` · ${a.schedule_start}-${a.schedule_end}` : ''}
|
${a.schedule_start ? ` · ${a.schedule_start}-${a.schedule_end}` : ''}
|
||||||
|
|
@ -504,15 +505,15 @@ function renderPlaylist(assignments) {
|
||||||
</div>
|
</div>
|
||||||
<div class="playlist-item-actions" style="display:flex;align-items:center;gap:4px">
|
<div class="playlist-item-actions" style="display:flex;align-items:center;gap:4px">
|
||||||
<select class="input zone-select" data-assignment-id="${a.id}" style="width:100px;font-size:11px;padding:2px 4px;background:var(--bg-input);display:none">
|
<select class="input zone-select" data-assignment-id="${a.id}" style="width:100px;font-size:11px;padding:2px 4px;background:var(--bg-input);display:none">
|
||||||
<option value="">No zone</option>
|
<option value="">${t('device.pl_item.no_zone')}</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn-icon mute-toggle" data-mute-assignment="${a.id}" data-muted="${a.muted ? '1' : '0'}" title="${a.muted ? 'Unmute' : 'Mute'}" style="color:${a.muted ? 'var(--danger)' : 'var(--text-muted)'}">
|
<button class="btn-icon mute-toggle" data-mute-assignment="${a.id}" data-muted="${a.muted ? '1' : '0'}" title="${a.muted ? t('device.pl_item.unmute') : t('device.pl_item.mute')}" style="color:${a.muted ? 'var(--danger)' : 'var(--text-muted)'}">
|
||||||
${a.muted
|
${a.muted
|
||||||
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>'
|
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>'
|
||||||
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>'
|
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>'
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon" title="Remove" data-remove-assignment="${a.id}">
|
<button class="btn-icon" title="${t('device.pl_item.remove')}" data-remove-assignment="${a.id}">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -537,18 +538,18 @@ async function setupActions(device) {
|
||||||
// Screenshot button
|
// Screenshot button
|
||||||
document.getElementById('screenshotBtn')?.addEventListener('click', () => {
|
document.getElementById('screenshotBtn')?.addEventListener('click', () => {
|
||||||
requestScreenshot(device.id);
|
requestScreenshot(device.id);
|
||||||
showToast('Screenshot requested', 'info');
|
showToast(t('device.toast.screenshot_requested'), 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rename
|
// Rename
|
||||||
document.getElementById('renameBtn')?.addEventListener('click', async () => {
|
document.getElementById('renameBtn')?.addEventListener('click', async () => {
|
||||||
const name = prompt('Enter new name:', device.name);
|
const name = prompt(t('device.prompt_new_name'), device.name);
|
||||||
if (name && name !== device.name) {
|
if (name && name !== device.name) {
|
||||||
try {
|
try {
|
||||||
await api.updateDevice(device.id, { name });
|
await api.updateDevice(device.id, { name });
|
||||||
document.getElementById('deviceName').textContent = name;
|
document.getElementById('deviceName').textContent = name;
|
||||||
currentDevice.name = name;
|
currentDevice.name = name;
|
||||||
showToast('Display renamed', 'success');
|
showToast(t('device.toast.renamed'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
}
|
}
|
||||||
|
|
@ -577,7 +578,7 @@ async function setupActions(device) {
|
||||||
orientation: document.getElementById('deviceOrientation').value,
|
orientation: document.getElementById('deviceOrientation').value,
|
||||||
default_content_id: document.getElementById('deviceDefaultContent').value || null,
|
default_content_id: document.getElementById('deviceDefaultContent').value || null,
|
||||||
});
|
});
|
||||||
showToast('Settings saved', 'success');
|
showToast(t('device.toast.settings_saved'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
}
|
}
|
||||||
|
|
@ -589,13 +590,13 @@ async function setupActions(device) {
|
||||||
devicePublishBtn.addEventListener('click', async () => {
|
devicePublishBtn.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
devicePublishBtn.disabled = true;
|
devicePublishBtn.disabled = true;
|
||||||
devicePublishBtn.textContent = 'Publishing...';
|
devicePublishBtn.textContent = t('device.draft.publishing');
|
||||||
await api.publishPlaylist(device.playlist_id);
|
await api.publishPlaylist(device.playlist_id);
|
||||||
showToast('Playlist published — devices updated');
|
showToast(t('device.toast.published'));
|
||||||
loadDevice(device.id, 'playlist');
|
loadDevice(device.id, 'playlist');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
devicePublishBtn.disabled = false;
|
devicePublishBtn.disabled = false;
|
||||||
devicePublishBtn.textContent = 'Publish';
|
devicePublishBtn.textContent = t('device.draft.publish');
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -603,10 +604,10 @@ async function setupActions(device) {
|
||||||
const deviceDiscardBtn = document.getElementById('deviceDiscardDraftBtn');
|
const deviceDiscardBtn = document.getElementById('deviceDiscardDraftBtn');
|
||||||
if (deviceDiscardBtn && device.playlist_id) {
|
if (deviceDiscardBtn && device.playlist_id) {
|
||||||
deviceDiscardBtn.addEventListener('click', async () => {
|
deviceDiscardBtn.addEventListener('click', async () => {
|
||||||
if (!confirm('Discard all unpublished changes and revert to the last published version?')) return;
|
if (!confirm(t('device.confirm_discard_draft'))) return;
|
||||||
try {
|
try {
|
||||||
await api.discardPlaylistDraft(device.playlist_id);
|
await api.discardPlaylistDraft(device.playlist_id);
|
||||||
showToast('Draft changes discarded');
|
showToast(t('device.toast.draft_discarded'));
|
||||||
loadDevice(device.id, 'playlist');
|
loadDevice(device.id, 'playlist');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
|
|
@ -621,7 +622,9 @@ async function setupActions(device) {
|
||||||
playlists.forEach(p => {
|
playlists.forEach(p => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = p.id;
|
opt.value = p.id;
|
||||||
opt.textContent = `${p.name}${p.is_auto_generated ? ' (auto)' : ''} — ${p.item_count} items`;
|
opt.textContent = p.is_auto_generated
|
||||||
|
? t('device.playlist_picker.with_auto', { name: p.name, n: p.item_count })
|
||||||
|
: t('device.playlist_picker.with_count', { name: p.name, n: p.item_count });
|
||||||
if (p.id === device.playlist_id) opt.selected = true;
|
if (p.id === device.playlist_id) opt.selected = true;
|
||||||
playlistPicker.appendChild(opt);
|
playlistPicker.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
@ -638,7 +641,7 @@ async function setupActions(device) {
|
||||||
const assignments = await api.getAssignments(device.id);
|
const assignments = await api.getAssignments(device.id);
|
||||||
document.getElementById('playlistContainer').innerHTML = renderPlaylist(assignments);
|
document.getElementById('playlistContainer').innerHTML = renderPlaylist(assignments);
|
||||||
attachRemoveHandlers(device);
|
attachRemoveHandlers(device);
|
||||||
showToast('Playlist changed');
|
showToast(t('device.toast.playlist_changed'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
}
|
}
|
||||||
|
|
@ -650,12 +653,12 @@ async function setupActions(device) {
|
||||||
try {
|
try {
|
||||||
const devices = await api.getDevices();
|
const devices = await api.getDevices();
|
||||||
const others = devices.filter(d => d.id !== device.id);
|
const others = devices.filter(d => d.id !== device.id);
|
||||||
if (!others.length) { showToast('No other devices to copy to', 'info'); return; }
|
if (!others.length) { showToast(t('device.copy.no_other_devices'), 'info'); return; }
|
||||||
|
|
||||||
const targetId = prompt('Copy playlist to which device?\n\n' + others.map((d, i) => `${i + 1}. ${d.name}`).join('\n') + '\n\nEnter number:');
|
const targetId = prompt(t('device.copy.prompt', { list: others.map((d, i) => `${i + 1}. ${d.name}`).join('\n') }));
|
||||||
if (!targetId) return;
|
if (!targetId) return;
|
||||||
const target = others[parseInt(targetId) - 1];
|
const target = others[parseInt(targetId) - 1];
|
||||||
if (!target) { showToast('Invalid selection', 'error'); return; }
|
if (!target) { showToast(t('device.copy.invalid_selection'), 'error'); return; }
|
||||||
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
const res = await fetch(`/api/assignments/device/${device.id}/copy-to/${target.id}`, {
|
const res = await fetch(`/api/assignments/device/${device.id}/copy-to/${target.id}`, {
|
||||||
|
|
@ -664,7 +667,7 @@ async function setupActions(device) {
|
||||||
body: JSON.stringify({ replace: false })
|
body: JSON.stringify({ replace: false })
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok) showToast(`Copied ${data.copied} items to ${target.name}`, 'success');
|
if (res.ok) showToast(t('device.copy.toast', { n: data.copied, device: target.name }), 'success');
|
||||||
else showToast(data.error, 'error');
|
else showToast(data.error, 'error');
|
||||||
} catch (err) { showToast(err.message, 'error'); }
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
});
|
});
|
||||||
|
|
@ -676,27 +679,27 @@ async function setupActions(device) {
|
||||||
deleteBtn?.addEventListener('click', async () => {
|
deleteBtn?.addEventListener('click', async () => {
|
||||||
if (deleteConfirming) {
|
if (deleteConfirming) {
|
||||||
try {
|
try {
|
||||||
deleteBtn.textContent = 'Removing...';
|
deleteBtn.textContent = t('device.toast.removing');
|
||||||
deleteBtn.disabled = true;
|
deleteBtn.disabled = true;
|
||||||
await api.deleteDevice(device.id);
|
await api.deleteDevice(device.id);
|
||||||
showToast('Display removed', 'success');
|
showToast(t('device.toast.removed'), 'success');
|
||||||
window.location.hash = '/';
|
window.location.hash = '/';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
deleteBtn.textContent = 'Remove';
|
deleteBtn.textContent = t('device.remove');
|
||||||
deleteBtn.disabled = false;
|
deleteBtn.disabled = false;
|
||||||
deleteConfirming = false;
|
deleteConfirming = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
deleteConfirming = true;
|
deleteConfirming = true;
|
||||||
deleteBtn.textContent = 'Click again to confirm';
|
deleteBtn.textContent = t('device.click_to_confirm');
|
||||||
deleteBtn.style.background = 'var(--danger)';
|
deleteBtn.style.background = 'var(--danger)';
|
||||||
deleteBtn.style.color = 'white';
|
deleteBtn.style.color = 'white';
|
||||||
clearTimeout(deleteTimeout);
|
clearTimeout(deleteTimeout);
|
||||||
deleteTimeout = setTimeout(() => {
|
deleteTimeout = setTimeout(() => {
|
||||||
deleteConfirming = false;
|
deleteConfirming = false;
|
||||||
deleteBtn.textContent = 'Remove';
|
deleteBtn.textContent = t('device.remove');
|
||||||
deleteBtn.style.background = '';
|
deleteBtn.style.background = '';
|
||||||
deleteBtn.style.color = '';
|
deleteBtn.style.color = '';
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
@ -709,17 +712,17 @@ async function setupActions(device) {
|
||||||
rebootBtn?.addEventListener('click', () => {
|
rebootBtn?.addEventListener('click', () => {
|
||||||
if (rebootConfirming) {
|
if (rebootConfirming) {
|
||||||
sendCommand(device.id, 'reboot', {});
|
sendCommand(device.id, 'reboot', {});
|
||||||
showToast('Reboot command sent', 'info');
|
showToast(t('device.toast.reboot_sent'), 'info');
|
||||||
rebootConfirming = false;
|
rebootConfirming = false;
|
||||||
rebootBtn.textContent = 'Reboot Device';
|
rebootBtn.textContent = t('device.ctl.reboot_device');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rebootConfirming = true;
|
rebootConfirming = true;
|
||||||
rebootBtn.textContent = 'Click again to confirm';
|
rebootBtn.textContent = t('device.click_to_confirm');
|
||||||
clearTimeout(rebootTimeout);
|
clearTimeout(rebootTimeout);
|
||||||
rebootTimeout = setTimeout(() => {
|
rebootTimeout = setTimeout(() => {
|
||||||
rebootConfirming = false;
|
rebootConfirming = false;
|
||||||
rebootBtn.textContent = 'Reboot Device';
|
rebootBtn.textContent = t('device.ctl.reboot_device');
|
||||||
}, 3000);
|
}, 3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -730,19 +733,19 @@ async function setupActions(device) {
|
||||||
shutdownBtn?.addEventListener('click', () => {
|
shutdownBtn?.addEventListener('click', () => {
|
||||||
if (shutdownConfirming) {
|
if (shutdownConfirming) {
|
||||||
sendCommand(device.id, 'shutdown', {});
|
sendCommand(device.id, 'shutdown', {});
|
||||||
showToast('Shutdown command sent', 'info');
|
showToast(t('device.toast.shutdown_sent'), 'info');
|
||||||
shutdownConfirming = false;
|
shutdownConfirming = false;
|
||||||
shutdownBtn.textContent = 'Shutdown';
|
shutdownBtn.textContent = t('device.ctl.shutdown');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
shutdownConfirming = true;
|
shutdownConfirming = true;
|
||||||
shutdownBtn.textContent = 'Click again to confirm';
|
shutdownBtn.textContent = t('device.click_to_confirm');
|
||||||
shutdownBtn.style.background = 'var(--danger)';
|
shutdownBtn.style.background = 'var(--danger)';
|
||||||
shutdownBtn.style.color = 'white';
|
shutdownBtn.style.color = 'white';
|
||||||
clearTimeout(shutdownTimeout);
|
clearTimeout(shutdownTimeout);
|
||||||
shutdownTimeout = setTimeout(() => {
|
shutdownTimeout = setTimeout(() => {
|
||||||
shutdownConfirming = false;
|
shutdownConfirming = false;
|
||||||
shutdownBtn.textContent = 'Shutdown';
|
shutdownBtn.textContent = t('device.ctl.shutdown');
|
||||||
shutdownBtn.style.background = '';
|
shutdownBtn.style.background = '';
|
||||||
shutdownBtn.style.color = '';
|
shutdownBtn.style.color = '';
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
@ -751,25 +754,25 @@ async function setupActions(device) {
|
||||||
// Screen Off
|
// Screen Off
|
||||||
document.getElementById('screenOffBtn')?.addEventListener('click', () => {
|
document.getElementById('screenOffBtn')?.addEventListener('click', () => {
|
||||||
sendCommand(device.id, 'screen_off', {});
|
sendCommand(device.id, 'screen_off', {});
|
||||||
showToast('Screen off command sent', 'info');
|
showToast(t('device.toast.screen_off_sent'), 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Screen On
|
// Screen On
|
||||||
document.getElementById('screenOnBtn')?.addEventListener('click', () => {
|
document.getElementById('screenOnBtn')?.addEventListener('click', () => {
|
||||||
sendCommand(device.id, 'screen_on', {});
|
sendCommand(device.id, 'screen_on', {});
|
||||||
showToast('Screen on command sent', 'info');
|
showToast(t('device.toast.screen_on_sent'), 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Launch Player
|
// Launch Player
|
||||||
document.getElementById('launchAppBtn')?.addEventListener('click', () => {
|
document.getElementById('launchAppBtn')?.addEventListener('click', () => {
|
||||||
sendCommand(device.id, 'launch', {});
|
sendCommand(device.id, 'launch', {});
|
||||||
showToast('Launch command sent', 'info');
|
showToast(t('device.toast.launch_sent'), 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Force Update
|
// Force Update
|
||||||
document.getElementById('forceUpdateBtn')?.addEventListener('click', () => {
|
document.getElementById('forceUpdateBtn')?.addEventListener('click', () => {
|
||||||
sendCommand(device.id, 'update', {});
|
sendCommand(device.id, 'update', {});
|
||||||
showToast('Update check triggered', 'info');
|
showToast(t('device.toast.update_triggered'), 'info');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -787,7 +790,7 @@ function setupRemote(device) {
|
||||||
startBtn.style.display = 'none';
|
startBtn.style.display = 'none';
|
||||||
stopBtn.style.display = '';
|
stopBtn.style.display = '';
|
||||||
overlay.style.display = 'none';
|
overlay.style.display = 'none';
|
||||||
showToast('Remote session started', 'info');
|
showToast(t('device.toast.remote_started'), 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
stopBtn?.addEventListener('click', () => {
|
stopBtn?.addEventListener('click', () => {
|
||||||
|
|
@ -828,7 +831,7 @@ async function setupPlaylistActions(device) {
|
||||||
layouts.filter(l => !l.is_template).forEach(l => {
|
layouts.filter(l => !l.is_template).forEach(l => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = l.id;
|
opt.value = l.id;
|
||||||
opt.textContent = `${l.name} (${l.zones?.length || 0} zones)`;
|
opt.textContent = t('device.layout.zones_count', { name: l.name, n: l.zones?.length || 0 });
|
||||||
if (device.layout_id === l.id) opt.selected = true;
|
if (device.layout_id === l.id) opt.selected = true;
|
||||||
select.appendChild(opt);
|
select.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
@ -836,7 +839,7 @@ async function setupPlaylistActions(device) {
|
||||||
layouts.filter(l => l.is_template).forEach(l => {
|
layouts.filter(l => l.is_template).forEach(l => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = l.id;
|
opt.value = l.id;
|
||||||
opt.textContent = `[Template] ${l.name} (${l.zones?.length || 0} zones)`;
|
opt.textContent = t('device.layout.template_zones_count', { name: l.name, n: l.zones?.length || 0 });
|
||||||
if (device.layout_id === l.id) opt.selected = true;
|
if (device.layout_id === l.id) opt.selected = true;
|
||||||
select.appendChild(opt);
|
select.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
@ -854,7 +857,7 @@ async function setupPlaylistActions(device) {
|
||||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}` },
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}` },
|
||||||
body: JSON.stringify({ layout_id: layoutId || null })
|
body: JSON.stringify({ layout_id: layoutId || null })
|
||||||
});
|
});
|
||||||
showToast(layoutId ? 'Layout applied' : 'Switched to fullscreen', 'success');
|
showToast(layoutId ? t('device.toast.layout_applied') : t('device.toast.switched_to_fullscreen'), 'success');
|
||||||
// Reload the device page to show updated zone selectors, stay on playlist tab
|
// Reload the device page to show updated zone selectors, stay on playlist tab
|
||||||
loadDevice(device.id, 'playlist');
|
loadDevice(device.id, 'playlist');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -884,7 +887,7 @@ async function setupPlaylistActions(device) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content.length && !widgets.length && !kioskPages.length) {
|
if (!content.length && !widgets.length && !kioskPages.length) {
|
||||||
showToast('No content, widgets, or kiosk pages yet. Create something first!', 'error');
|
showToast(t('device.assign.empty_all'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -893,7 +896,7 @@ async function setupPlaylistActions(device) {
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="modal" style="max-width:650px;width:95vw">
|
<div class="modal" style="max-width:650px;width:95vw">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>Add to Playlist</h3>
|
<h3>${t('device.assign.modal_title')}</h3>
|
||||||
<button class="btn-icon" id="closeAssignModal">
|
<button class="btn-icon" id="closeAssignModal">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
|
|
@ -903,22 +906,22 @@ async function setupPlaylistActions(device) {
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
${zones.length > 0 ? `
|
${zones.length > 0 ? `
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Zone</label>
|
<label>${t('device.assign.zone_label')}</label>
|
||||||
<select id="assignZone" class="input" style="background:var(--bg-input)">
|
<select id="assignZone" class="input" style="background:var(--bg-input)">
|
||||||
<option value="">Default (fullscreen)</option>
|
<option value="">${t('device.assign.zone_default')}</option>
|
||||||
${zones.map(z => `<option value="${z.id}">${z.name} (${Math.round(z.width_percent)}% x ${Math.round(z.height_percent)}%)</option>`).join('')}
|
${zones.map(z => `<option value="${z.id}">${z.name} (${Math.round(z.width_percent)}% x ${Math.round(z.height_percent)}%)</option>`).join('')}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Display Duration (seconds, for images/widgets)</label>
|
<label>${t('device.assign.duration_label')}</label>
|
||||||
<input type="number" id="assignDuration" class="input" value="10" min="1" max="3600">
|
<input type="number" id="assignDuration" class="input" value="10" min="1" max="3600">
|
||||||
</div>
|
</div>
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div style="display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:12px">
|
<div style="display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:12px">
|
||||||
<div class="assign-tab active" data-tab="media" style="padding:8px 16px;font-size:13px;cursor:pointer;border-bottom:2px solid var(--accent);color:var(--accent)">Media (${content.length})</div>
|
<div class="assign-tab active" data-tab="media" style="padding:8px 16px;font-size:13px;cursor:pointer;border-bottom:2px solid var(--accent);color:var(--accent)">${t('device.assign.tab.media', { n: content.length })}</div>
|
||||||
<div class="assign-tab" data-tab="widgets" style="padding:8px 16px;font-size:13px;cursor:pointer;border-bottom:2px solid transparent;color:var(--text-secondary)">Widgets (${widgets.length})</div>
|
<div class="assign-tab" data-tab="widgets" style="padding:8px 16px;font-size:13px;cursor:pointer;border-bottom:2px solid transparent;color:var(--text-secondary)">${t('device.assign.tab.widgets', { n: widgets.length })}</div>
|
||||||
<div class="assign-tab" data-tab="kiosk" style="padding:8px 16px;font-size:13px;cursor:pointer;border-bottom:2px solid transparent;color:var(--text-secondary)">Kiosk (${kioskPages.length})</div>
|
<div class="assign-tab" data-tab="kiosk" style="padding:8px 16px;font-size:13px;cursor:pointer;border-bottom:2px solid transparent;color:var(--text-secondary)">${t('device.assign.tab.kiosk', { n: kioskPages.length })}</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Media grid -->
|
<!-- Media grid -->
|
||||||
<div class="assign-content-grid" id="assignMedia">
|
<div class="assign-content-grid" id="assignMedia">
|
||||||
|
|
@ -936,7 +939,7 @@ async function setupPlaylistActions(device) {
|
||||||
}
|
}
|
||||||
<div class="assign-content-item-name">${esc(c.filename)}</div>
|
<div class="assign-content-item-name">${esc(c.filename)}</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('') || '<p style="color:var(--text-muted);padding:16px;text-align:center">No media uploaded yet</p>'}
|
`).join('') || `<p style="color:var(--text-muted);padding:16px;text-align:center">${t('device.assign.no_media')}</p>`}
|
||||||
</div>
|
</div>
|
||||||
<!-- Widgets grid -->
|
<!-- Widgets grid -->
|
||||||
<div class="assign-content-grid" id="assignWidgets" style="display:none">
|
<div class="assign-content-grid" id="assignWidgets" style="display:none">
|
||||||
|
|
@ -949,7 +952,7 @@ async function setupPlaylistActions(device) {
|
||||||
</div>
|
</div>
|
||||||
<div class="assign-content-item-name">${w.name}</div>
|
<div class="assign-content-item-name">${w.name}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('') || '<p style="color:var(--text-muted);padding:16px;text-align:center">No widgets created yet. <a href="#/widgets" style="color:var(--accent)">Create one</a></p>'}
|
}).join('') || `<p style="color:var(--text-muted);padding:16px;text-align:center">${t('device.assign.no_widgets')} <a href="#/widgets" style="color:var(--accent)">${t('device.assign.create_one')}</a></p>`}
|
||||||
</div>
|
</div>
|
||||||
<!-- Kiosk grid -->
|
<!-- Kiosk grid -->
|
||||||
<div class="assign-content-grid" id="assignKiosk" style="display:none">
|
<div class="assign-content-grid" id="assignKiosk" style="display:none">
|
||||||
|
|
@ -958,12 +961,12 @@ async function setupPlaylistActions(device) {
|
||||||
<div style="aspect-ratio:16/9;display:flex;align-items:center;justify-content:center;background:var(--bg-primary);font-size:32px">🖱</div>
|
<div style="aspect-ratio:16/9;display:flex;align-items:center;justify-content:center;background:var(--bg-primary);font-size:32px">🖱</div>
|
||||||
<div class="assign-content-item-name">${k.name}</div>
|
<div class="assign-content-item-name">${k.name}</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('') || '<p style="color:var(--text-muted);padding:16px;text-align:center">No kiosk pages yet. <a href="#/kiosk" style="color:var(--accent)">Create one</a></p>'}
|
`).join('') || `<p style="color:var(--text-muted);padding:16px;text-align:center">${t('device.assign.no_kiosk')} <a href="#/kiosk" style="color:var(--accent)">${t('device.assign.create_one')}</a></p>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-secondary" id="cancelAssign">Cancel</button>
|
<button class="btn btn-secondary" id="cancelAssign">${t('common.cancel')}</button>
|
||||||
<button class="btn btn-primary" id="confirmAssign">Add Selected</button>
|
<button class="btn btn-primary" id="confirmAssign">${t('device.assign.add_selected')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -995,7 +998,7 @@ async function setupPlaylistActions(device) {
|
||||||
modal.querySelector('#cancelAssign').onclick = () => modal.remove();
|
modal.querySelector('#cancelAssign').onclick = () => modal.remove();
|
||||||
modal.querySelector('#confirmAssign').onclick = async () => {
|
modal.querySelector('#confirmAssign').onclick = async () => {
|
||||||
if (!selectedId) {
|
if (!selectedId) {
|
||||||
showToast('Select something first', 'error');
|
showToast(t('device.assign.select_first'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const duration = parseInt(modal.querySelector('#assignDuration').value) || 10;
|
const duration = parseInt(modal.querySelector('#assignDuration').value) || 10;
|
||||||
|
|
@ -1011,13 +1014,13 @@ async function setupPlaylistActions(device) {
|
||||||
const wRes = await fetch('/api/widgets', {
|
const wRes = await fetch('/api/widgets', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ widget_type: 'webpage', name: `Kiosk: ${kioskPages.find(k => k.id === selectedId)?.name || 'Page'}`, config: { url: `${serverUrl}/api/kiosk/${selectedId}/render` } })
|
body: JSON.stringify({ widget_type: 'webpage', name: t('device.assign.kiosk_widget_name', { name: kioskPages.find(k => k.id === selectedId)?.name || 'Page' }), config: { url: `${serverUrl}/api/kiosk/${selectedId}/render` } })
|
||||||
});
|
});
|
||||||
const widget = await wRes.json();
|
const widget = await wRes.json();
|
||||||
await api.addAssignment(device.id, { widget_id: widget.id, duration_sec: 0 });
|
await api.addAssignment(device.id, { widget_id: widget.id, duration_sec: 0 });
|
||||||
}
|
}
|
||||||
modal.remove();
|
modal.remove();
|
||||||
showToast('Added to playlist', 'success');
|
showToast(t('device.toast.added_to_playlist'), 'success');
|
||||||
loadDevice(device.id, 'playlist');
|
loadDevice(device.id, 'playlist');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
|
|
@ -1060,7 +1063,7 @@ function attachRemoveHandlers(device) {
|
||||||
select.onchange = async () => {
|
select.onchange = async () => {
|
||||||
try {
|
try {
|
||||||
await api.updateAssignment(assignmentId, { zone_id: select.value || null });
|
await api.updateAssignment(assignmentId, { zone_id: select.value || null });
|
||||||
showToast(`Zone updated`, 'success');
|
showToast(t('device.toast.zone_updated'), 'success');
|
||||||
loadDevice(device.id, 'playlist');
|
loadDevice(device.id, 'playlist');
|
||||||
} catch (err) { showToast(err.message, 'error'); }
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
};
|
};
|
||||||
|
|
@ -1076,7 +1079,7 @@ function attachRemoveHandlers(device) {
|
||||||
const currentlyMuted = btn.dataset.muted === '1';
|
const currentlyMuted = btn.dataset.muted === '1';
|
||||||
try {
|
try {
|
||||||
await api.updateAssignment(id, { muted: !currentlyMuted });
|
await api.updateAssignment(id, { muted: !currentlyMuted });
|
||||||
showToast(currentlyMuted ? 'Unmuted' : 'Muted', 'success');
|
showToast(currentlyMuted ? t('device.toast.unmuted') : t('device.toast.muted'), 'success');
|
||||||
loadDevice(device.id, 'playlist');
|
loadDevice(device.id, 'playlist');
|
||||||
} catch (err) { showToast(err.message, 'error'); }
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
});
|
});
|
||||||
|
|
@ -1089,7 +1092,7 @@ function attachRemoveHandlers(device) {
|
||||||
const id = btn.dataset.removeAssignment;
|
const id = btn.dataset.removeAssignment;
|
||||||
try {
|
try {
|
||||||
await api.deleteAssignment(id);
|
await api.deleteAssignment(id);
|
||||||
showToast('Content removed from playlist', 'success');
|
showToast(t('device.toast.removed_from_playlist'), 'success');
|
||||||
loadDevice(device.id, 'playlist');
|
loadDevice(device.id, 'playlist');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
|
|
@ -1140,7 +1143,7 @@ function attachRemoveHandlers(device) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.reorderAssignments(device.id, newOrder);
|
await api.reorderAssignments(device.id, newOrder);
|
||||||
showToast('Playlist reordered', 'success');
|
showToast(t('device.toast.playlist_reordered'), 'success');
|
||||||
loadDevice(device.id, 'playlist');
|
loadDevice(device.id, 'playlist');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
|
|
@ -1193,7 +1196,11 @@ function renderUptimeTimeline(uptimeData, statusLog = []) {
|
||||||
const knownSlots = slotStatus.filter(s => s !== 'unknown').length;
|
const knownSlots = slotStatus.filter(s => s !== 'unknown').length;
|
||||||
const onlineSlots = slotStatus.filter(s => s === 'online').length;
|
const onlineSlots = slotStatus.filter(s => s === 'online').length;
|
||||||
const uptimePct = knownSlots > 0 ? Math.round((onlineSlots / knownSlots) * 100) : 0;
|
const uptimePct = knownSlots > 0 ? Math.round((onlineSlots / knownSlots) * 100) : 0;
|
||||||
if (percentEl) percentEl.textContent = `${uptimePct}% uptime (${knownSlots > 0 ? knownSlots * 15 + 'min tracked' : 'no data'})`;
|
if (percentEl) {
|
||||||
|
percentEl.textContent = knownSlots > 0
|
||||||
|
? t('device.timeline.uptime_pct_tracked', { pct: uptimePct, n: knownSlots * 15 })
|
||||||
|
: t('device.timeline.uptime_pct_no_data', { pct: uptimePct });
|
||||||
|
}
|
||||||
|
|
||||||
// Color map
|
// Color map
|
||||||
const colors = {
|
const colors = {
|
||||||
|
|
@ -1207,7 +1214,7 @@ function renderUptimeTimeline(uptimeData, statusLog = []) {
|
||||||
timeline.innerHTML = slotStatus.map((status, i) => {
|
timeline.innerHTML = slotStatus.map((status, i) => {
|
||||||
const time = new Date((dayAgo + i * slotDuration) * 1000);
|
const time = new Date((dayAgo + i * slotDuration) * 1000);
|
||||||
const label = time.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
const label = time.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
||||||
const statusLabel = status === 'unknown' ? 'No data' : status.charAt(0).toUpperCase() + status.slice(1);
|
const statusLabel = status === 'unknown' ? t('device.timeline.no_data') : status === 'online' ? t('device.timeline.online') : t('device.timeline.offline');
|
||||||
return `<div style="flex:1;background:${colors[status]};opacity:${opacities[status]}" title="${label} - ${statusLabel}"></div>`;
|
return `<div style="flex:1;background:${colors[status]};opacity:${opacities[status]}" title="${label} - ${statusLabel}"></div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
@ -1218,11 +1225,11 @@ function updateTelemetryDisplay(telemetry) {
|
||||||
if (el) el.textContent = val;
|
if (el) el.textContent = val;
|
||||||
};
|
};
|
||||||
if (telemetry.battery_level != null) update('telBattery', telemetry.battery_level + '%');
|
if (telemetry.battery_level != null) update('telBattery', telemetry.battery_level + '%');
|
||||||
if (telemetry.storage_free_mb) update('telStorage', formatBytes(telemetry.storage_free_mb) + ' free');
|
if (telemetry.storage_free_mb) update('telStorage', t('device.info.size_free', { size: formatBytes(telemetry.storage_free_mb) }));
|
||||||
if (telemetry.wifi_ssid) update('telWifi', telemetry.wifi_ssid);
|
if (telemetry.wifi_ssid) update('telWifi', telemetry.wifi_ssid);
|
||||||
if (telemetry.wifi_rssi) update('telRssi', telemetry.wifi_rssi + ' dBm');
|
if (telemetry.wifi_rssi) update('telRssi', telemetry.wifi_rssi + ' dBm');
|
||||||
if (telemetry.uptime_seconds) update('telUptime', formatUptime(telemetry.uptime_seconds));
|
if (telemetry.uptime_seconds) update('telUptime', formatUptime(telemetry.uptime_seconds));
|
||||||
if (telemetry.ram_free_mb) update('telRam', formatBytes(telemetry.ram_free_mb) + ' free');
|
if (telemetry.ram_free_mb) update('telRam', t('device.info.size_free', { size: formatBytes(telemetry.ram_free_mb) }));
|
||||||
if (telemetry.cpu_usage != null) update('telCpu', telemetry.cpu_usage.toFixed(1) + '%');
|
if (telemetry.cpu_usage != null) update('telCpu', telemetry.cpu_usage.toFixed(1) + '%');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
import { showToast } from '../components/toast.js';
|
import { showToast } from '../components/toast.js';
|
||||||
import { getLanguage, setLanguage, getAvailableLanguages } from '../i18n.js';
|
import { getLanguage, setLanguage, getAvailableLanguages, t, tn } from '../i18n.js';
|
||||||
import { esc } from '../utils.js';
|
import { esc } from '../utils.js';
|
||||||
import { resetBranding } from '../branding.js';
|
import { resetBranding } from '../branding.js';
|
||||||
|
|
||||||
|
|
@ -17,118 +17,115 @@ export async function render(container) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h1>Settings</h1>
|
<h1>${t('settings.title')}</h1>
|
||||||
<div class="subtitle">Server configuration and setup information</div>
|
<div class="subtitle">${t('settings.subtitle')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Account</h3>
|
<h3>${t('settings.account')}</h3>
|
||||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px">
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px">
|
||||||
<div class="form-group"><label>Email</label><input type="email" class="input" value="${esc(user.email || '')}" disabled></div>
|
<div class="form-group"><label>${t('auth.email')}</label><input type="email" class="input" value="${esc(user.email || '')}" disabled></div>
|
||||||
<div class="form-group"><label>Name</label><input type="text" id="acctName" class="input" value="${esc(user.name || '')}"></div>
|
<div class="form-group"><label>${t('auth.name')}</label><input type="text" id="acctName" class="input" value="${esc(user.name || '')}"></div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary btn-sm" id="saveAcctBtn">Save Profile</button>
|
<button class="btn btn-secondary btn-sm" id="saveAcctBtn">${t('settings.save_profile')}</button>
|
||||||
|
|
||||||
${user.auth_provider === 'local' ? `
|
${user.auth_provider === 'local' ? `
|
||||||
<div style="border-top:1px solid var(--border);margin-top:20px;padding-top:16px">
|
<div style="border-top:1px solid var(--border);margin-top:20px;padding-top:16px">
|
||||||
<h4 style="font-size:14px;margin-bottom:8px">Change Password</h4>
|
<h4 style="font-size:14px;margin-bottom:8px">${t('settings.change_password')}</h4>
|
||||||
<p style="color:var(--text-muted);font-size:12px;margin-bottom:12px">Must be at least 8 characters.</p>
|
<p style="color:var(--text-muted);font-size:12px;margin-bottom:12px">${t('settings.password_min_8')}</p>
|
||||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
|
||||||
<div class="form-group"><label>Current Password</label><input type="password" id="acctCurrentPw" class="input" autocomplete="current-password"></div>
|
<div class="form-group"><label>${t('settings.current_password')}</label><input type="password" id="acctCurrentPw" class="input" autocomplete="current-password"></div>
|
||||||
<div class="form-group"><label>New Password</label><input type="password" id="acctNewPw" class="input" autocomplete="new-password"></div>
|
<div class="form-group"><label>${t('settings.new_password')}</label><input type="password" id="acctNewPw" class="input" autocomplete="new-password"></div>
|
||||||
<div class="form-group"><label>Confirm New Password</label><input type="password" id="acctConfirmPw" class="input" autocomplete="new-password"></div>
|
<div class="form-group"><label>${t('settings.confirm_new_password')}</label><input type="password" id="acctConfirmPw" class="input" autocomplete="new-password"></div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary btn-sm" id="changePwBtn">Change Password</button>
|
<button class="btn btn-primary btn-sm" id="changePwBtn">${t('settings.change_password')}</button>
|
||||||
</div>
|
</div>
|
||||||
` : `
|
` : `
|
||||||
<p style="color:var(--text-muted);font-size:12px;margin-top:16px">You sign in via <strong>${esc(user.auth_provider || 'SSO')}</strong>. Manage your password there.</p>
|
<p style="color:var(--text-muted);font-size:12px;margin-top:16px">${t('settings.sso_note', { provider: esc(user.auth_provider || 'SSO') })}</p>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${isAdmin ? `
|
${isAdmin ? `
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>License</h3>
|
<h3>${t('settings.license')}</h3>
|
||||||
<div id="licenseSection"><p style="color:var(--text-muted);font-size:13px">MIT License - all features included.</p></div>
|
<div id="licenseSection"><p style="color:var(--text-muted);font-size:13px">${t('settings.license_mit')}</p></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${isSuperAdmin ? '<p style="font-size:12px;color:var(--text-muted);margin-bottom:12px">Platform admin tools are in the <a href="#/admin" style="color:var(--accent)">Admin</a> page.</p>' : ''}
|
${isSuperAdmin ? `<p style="font-size:12px;color:var(--text-muted);margin-bottom:12px">${t('settings.platform_admin_link')} <a href="#/admin" style="color:var(--accent)">${t('nav.admin')}</a> ${t('settings.platform_admin_page_suffix')}</p>` : ''}
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>User Management</h3>
|
<h3>${t('settings.user_management')}</h3>
|
||||||
<div id="userManagement"><p style="color:var(--text-muted)">Loading users...</p></div>
|
<div id="userManagement"><p style="color:var(--text-muted)">${t('settings.loading_users')}</p></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section" id="whiteLabelSection">
|
<div class="settings-section" id="whiteLabelSection">
|
||||||
<h3>White Label / Branding</h3>
|
<h3>${t('settings.white_label')}</h3>
|
||||||
<div id="whiteLabelForm">
|
<div id="whiteLabelForm">
|
||||||
<p style="color:var(--text-muted);font-size:12px;margin-bottom:16px">Customize the look of your dashboard and player for your clients.</p>
|
<p style="color:var(--text-muted);font-size:12px;margin-bottom:16px">${t('settings.white_label_desc')}</p>
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||||
<div class="form-group"><label>Brand Name</label><input type="text" id="wlBrandName" class="input" placeholder="ScreenTinker"></div>
|
<div class="form-group"><label>${t('settings.brand_name')}</label><input type="text" id="wlBrandName" class="input" placeholder="ScreenTinker"></div>
|
||||||
<div class="form-group"><label>Logo URL</label><input type="text" id="wlLogoUrl" class="input" placeholder="https://..."></div>
|
<div class="form-group"><label>${t('settings.logo_url')}</label><input type="text" id="wlLogoUrl" class="input" placeholder="https://..."></div>
|
||||||
<div class="form-group"><label>Primary Color</label><input type="color" id="wlPrimaryColor" value="#3B82F6" style="width:100%;height:36px;border:none;cursor:pointer;border-radius:var(--radius)"></div>
|
<div class="form-group"><label>${t('settings.primary_color')}</label><input type="color" id="wlPrimaryColor" value="#3B82F6" style="width:100%;height:36px;border:none;cursor:pointer;border-radius:var(--radius)"></div>
|
||||||
<div class="form-group"><label>Background Color</label><input type="color" id="wlBgColor" value="#111827" style="width:100%;height:36px;border:none;cursor:pointer;border-radius:var(--radius)"></div>
|
<div class="form-group"><label>${t('settings.bg_color')}</label><input type="color" id="wlBgColor" value="#111827" style="width:100%;height:36px;border:none;cursor:pointer;border-radius:var(--radius)"></div>
|
||||||
<div class="form-group"><label>Custom Domain</label><input type="text" id="wlDomain" class="input" placeholder="signage.yourcompany.com"></div>
|
<div class="form-group"><label>${t('settings.custom_domain')}</label><input type="text" id="wlDomain" class="input" placeholder="signage.yourcompany.com"></div>
|
||||||
<div class="form-group"><label>Favicon URL</label><input type="text" id="wlFavicon" class="input" placeholder="https://..."></div>
|
<div class="form-group"><label>${t('settings.favicon_url')}</label><input type="text" id="wlFavicon" class="input" placeholder="https://..."></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group"><label>Custom CSS (optional)</label><textarea id="wlCustomCss" class="input" rows="3" style="font-family:monospace;font-size:12px" placeholder=":root { --accent: #ff6600; }"></textarea></div>
|
<div class="form-group"><label>${t('settings.custom_css')}</label><textarea id="wlCustomCss" class="input" rows="3" style="font-family:monospace;font-size:12px" placeholder=":root { --accent: #ff6600; }"></textarea></div>
|
||||||
<div class="form-group"><label style="display:flex;align-items:center;gap:8px"><input type="checkbox" id="wlHideBranding"> Hide "ScreenTinker" branding</label></div>
|
<div class="form-group"><label style="display:flex;align-items:center;gap:8px"><input type="checkbox" id="wlHideBranding"> ${t('settings.hide_branding')}</label></div>
|
||||||
<button class="btn btn-primary btn-sm" id="saveWhiteLabelBtn">Save Branding</button>
|
<button class="btn btn-primary btn-sm" id="saveWhiteLabelBtn">${t('settings.save_branding')}</button>
|
||||||
<button class="btn btn-secondary btn-sm" id="previewWhiteLabelBtn" style="margin-left:8px">Preview</button>
|
<button class="btn btn-secondary btn-sm" id="previewWhiteLabelBtn" style="margin-left:8px">${t('settings.preview')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Server Information</h3>
|
<h3>${t('settings.server_info')}</h3>
|
||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">Server URL</div>
|
<div class="info-card-label">${t('settings.server_url')}</div>
|
||||||
<div class="info-card-value small">${serverUrl}</div>
|
<div class="info-card-value small">${serverUrl}</div>
|
||||||
<p style="font-size:11px;color:var(--text-muted);margin-top:4px">Use this URL when setting up the Android app</p>
|
<p style="font-size:11px;color:var(--text-muted);margin-top:4px">${t('settings.server_url_hint')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">API Endpoint</div>
|
<div class="info-card-label">${t('settings.api_endpoint')}</div>
|
||||||
<div class="info-card-value small">${serverUrl}/api</div>
|
<div class="info-card-value small">${serverUrl}/api</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Setup Guide</h3>
|
<h3>${t('settings.setup_guide')}</h3>
|
||||||
<div style="color:var(--text-secondary);font-size:13px;line-height:1.8">
|
<div style="color:var(--text-secondary);font-size:13px;line-height:1.8">
|
||||||
<ol style="padding-left:20px;list-style:decimal">
|
<ol style="padding-left:20px;list-style:decimal">
|
||||||
<li>Install the <strong>ScreenTinker</strong> APK on your Apolosign portable TV via sideloading</li>
|
<li>${t('settings.setup_step_1')}</li>
|
||||||
<li>Open the app and enter this server URL: <code style="background:var(--bg-input);padding:2px 6px;border-radius:4px">${serverUrl}</code></li>
|
<li>${t('settings.setup_step_2_prefix')} <code style="background:var(--bg-input);padding:2px 6px;border-radius:4px">${serverUrl}</code></li>
|
||||||
<li>The app will display a <strong>6-digit pairing code</strong></li>
|
<li>${t('settings.setup_step_3')}</li>
|
||||||
<li>Click <strong>"Add Display"</strong> on the dashboard and enter the pairing code</li>
|
<li>${t('settings.setup_step_4')}</li>
|
||||||
<li>Upload content in the <strong>Content Library</strong></li>
|
<li>${t('settings.setup_step_5')}</li>
|
||||||
<li>Assign content to the display's <strong>Playlist</strong></li>
|
<li>${t('settings.setup_step_6')}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${isAdmin ? `
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Your Data</h3>
|
<h3>${t('settings.your_data')}</h3>
|
||||||
<p style="font-size:13px;color:var(--text-secondary);margin-bottom:12px">Export or import your devices, content, layouts, schedules, and all settings. Use this to migrate between cloud and self-hosted instances.</p>
|
<p style="font-size:13px;color:var(--text-secondary);margin-bottom:12px">${t('settings.your_data_desc')}</p>
|
||||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||||
<button class="btn btn-secondary btn-sm" id="exportDataBtn">
|
<button class="btn btn-secondary btn-sm" id="exportDataBtn">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
|
||||||
</svg>
|
</svg>
|
||||||
Export My Data
|
${t('settings.export_my_data')}
|
||||||
</button>
|
</button>
|
||||||
<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:var(--text-secondary);cursor:pointer">
|
<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:var(--text-secondary);cursor:pointer">
|
||||||
<input type="checkbox" id="exportIncludeFiles"> Include media files (ZIP)
|
<input type="checkbox" id="exportIncludeFiles"> ${t('settings.include_media_zip')}
|
||||||
</label>
|
</label>
|
||||||
<button class="btn btn-secondary btn-sm" id="importDataBtn">
|
<button class="btn btn-secondary btn-sm" id="importDataBtn">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
|
||||||
</svg>
|
</svg>
|
||||||
Import Data
|
${t('settings.import_data')}
|
||||||
</button>
|
</button>
|
||||||
<input type="file" id="importFileInput" accept=".json,.zip" style="display:none">
|
<input type="file" id="importFileInput" accept=".json,.zip" style="display:none">
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -136,23 +133,23 @@ export async function render(container) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Language</h3>
|
<h3>${t('settings.language')}</h3>
|
||||||
<select id="langSelect" class="input" style="width:200px;background:var(--bg-input)">
|
<select id="langSelect" class="input" style="width:200px;background:var(--bg-input)">
|
||||||
${getAvailableLanguages().map(l => `<option value="${l.code}" ${l.code === getLanguage() ? 'selected' : ''}>${l.name}</option>`).join('')}
|
${getAvailableLanguages().map(l => `<option value="${l.code}" ${l.code === getLanguage() ? 'selected' : ''}>${l.name}</option>`).join('')}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>About</h3>
|
<h3>${t('settings.about')}</h3>
|
||||||
<div style="color:var(--text-secondary);font-size:13px">
|
<div style="color:var(--text-secondary);font-size:13px">
|
||||||
<p><strong>ScreenTinker</strong> v1.4.1</p>
|
<p><strong>ScreenTinker</strong> v1.4.1</p>
|
||||||
<p style="margin-top:4px">Digital signage management system.</p>
|
<p style="margin-top:4px">${t('settings.about_tagline')}</p>
|
||||||
<p style="margin-top:12px">
|
<p style="margin-top:12px">
|
||||||
<a href="/legal/terms.html" target="_blank" style="color:var(--accent);font-size:12px">Terms of Service</a>
|
<a href="/legal/terms.html" target="_blank" style="color:var(--accent);font-size:12px">${t('auth.terms')}</a>
|
||||||
·
|
·
|
||||||
<a href="/legal/privacy.html" target="_blank" style="color:var(--accent);font-size:12px">Privacy Policy</a>
|
<a href="/legal/privacy.html" target="_blank" style="color:var(--accent);font-size:12px">${t('auth.privacy')}</a>
|
||||||
·
|
·
|
||||||
<a href="/legal/third-party.html" target="_blank" style="color:var(--accent);font-size:12px">Third-Party Licenses</a>
|
<a href="/legal/third-party.html" target="_blank" style="color:var(--accent);font-size:12px">${t('settings.third_party_licenses')}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -177,7 +174,7 @@ export async function render(container) {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
document.getElementById('supportTokenOutput').value = data.token;
|
document.getElementById('supportTokenOutput').value = data.token;
|
||||||
document.getElementById('supportTokenResult').style.display = 'block';
|
document.getElementById('supportTokenResult').style.display = 'block';
|
||||||
showToast(`Support token generated (valid ${hours}h)`, 'success');
|
showToast(t('settings.toast.support_token_generated', { hours }), 'success');
|
||||||
} else showToast(data.error, 'error');
|
} else showToast(data.error, 'error');
|
||||||
} catch (err) { showToast(err.message, 'error'); }
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
});
|
});
|
||||||
|
|
@ -204,35 +201,35 @@ export async function render(container) {
|
||||||
statusEl.style.background = 'var(--bg-secondary)';
|
statusEl.style.background = 'var(--bg-secondary)';
|
||||||
statusEl.style.border = '1px solid var(--border)';
|
statusEl.style.border = '1px solid var(--border)';
|
||||||
statusEl.style.color = 'var(--text-secondary)';
|
statusEl.style.color = 'var(--text-secondary)';
|
||||||
statusEl.textContent = 'Reading file...';
|
statusEl.textContent = t('settings.import.reading_file');
|
||||||
try {
|
try {
|
||||||
let data;
|
let data;
|
||||||
if (isZip) {
|
if (isZip) {
|
||||||
// For ZIP, show basic info and skip preview parsing
|
// For ZIP, show basic info and skip preview parsing
|
||||||
data = { format: 'screentinker-export-v1', _isZip: true };
|
data = { format: 'screentinker-export-v1', _isZip: true };
|
||||||
statusEl.innerHTML = `ZIP export detected: <strong>${esc(file.name)}</strong> (${(file.size / 1048576).toFixed(1)} MB)<br>Contains data + media files.<br><br><button class="btn btn-primary btn-sm" id="confirmImportBtn">Confirm Import</button> <button class="btn btn-secondary btn-sm" id="cancelImportBtn">Cancel</button>`;
|
statusEl.innerHTML = `${t('settings.import.zip_detected', { name: esc(file.name), size: (file.size / 1048576).toFixed(1) })}<br><br><button class="btn btn-primary btn-sm" id="confirmImportBtn">${t('settings.import.confirm')}</button> <button class="btn btn-secondary btn-sm" id="cancelImportBtn">${t('common.cancel')}</button>`;
|
||||||
} else {
|
} else {
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
data = JSON.parse(text);
|
data = JSON.parse(text);
|
||||||
if (!data.format || !data.format.startsWith('screentinker-export')) {
|
if (!data.format || !data.format.startsWith('screentinker-export')) {
|
||||||
statusEl.style.color = 'var(--danger)';
|
statusEl.style.color = 'var(--danger)';
|
||||||
statusEl.textContent = 'Invalid file. Must be a ScreenTinker export JSON or ZIP.';
|
statusEl.textContent = t('settings.import.invalid_file');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const summary = [
|
const summary = [
|
||||||
data.devices?.length ? `${data.devices.length} devices` : null,
|
data.devices?.length ? t('settings.import.summary_devices', { n: data.devices.length }) : null,
|
||||||
data.content?.length ? `${data.content.length} content items` : null,
|
data.content?.length ? t('settings.import.summary_content', { n: data.content.length }) : null,
|
||||||
data.widgets?.length ? `${data.widgets.length} widgets` : null,
|
data.widgets?.length ? t('settings.import.summary_widgets', { n: data.widgets.length }) : null,
|
||||||
data.layouts?.length ? `${data.layouts.length} layouts` : null,
|
data.layouts?.length ? t('settings.import.summary_layouts', { n: data.layouts.length }) : null,
|
||||||
data.schedules?.length ? `${data.schedules.length} schedules` : null,
|
data.schedules?.length ? t('settings.import.summary_schedules', { n: data.schedules.length }) : null,
|
||||||
data.video_walls?.length ? `${data.video_walls.length} video walls` : null,
|
data.video_walls?.length ? t('settings.import.summary_walls', { n: data.video_walls.length }) : null,
|
||||||
data.kiosk_pages?.length ? `${data.kiosk_pages.length} kiosk pages` : null,
|
data.kiosk_pages?.length ? t('settings.import.summary_kiosk', { n: data.kiosk_pages.length }) : null,
|
||||||
].filter(Boolean).join(', ');
|
].filter(Boolean).join(', ');
|
||||||
statusEl.innerHTML = `Found: ${esc(summary) || 'empty export'}.<br>From: ${esc(data.user?.email) || 'unknown'} (exported ${esc(data.exported_at?.split('T')[0]) || 'unknown'})<br><br><button class="btn btn-primary btn-sm" id="confirmImportBtn">Confirm Import</button> <button class="btn btn-secondary btn-sm" id="cancelImportBtn">Cancel</button>`;
|
statusEl.innerHTML = `${t('settings.import.found_summary', { summary: esc(summary) || t('settings.import.empty_export'), email: esc(data.user?.email) || t('common.unknown'), date: esc(data.exported_at?.split('T')[0]) || t('common.unknown') })}<br><br><button class="btn btn-primary btn-sm" id="confirmImportBtn">${t('settings.import.confirm')}</button> <button class="btn btn-secondary btn-sm" id="cancelImportBtn">${t('common.cancel')}</button>`;
|
||||||
}
|
}
|
||||||
document.getElementById('cancelImportBtn').onclick = () => { statusEl.style.display = 'none'; e.target.value = ''; };
|
document.getElementById('cancelImportBtn').onclick = () => { statusEl.style.display = 'none'; e.target.value = ''; };
|
||||||
document.getElementById('confirmImportBtn').onclick = async () => {
|
document.getElementById('confirmImportBtn').onclick = async () => {
|
||||||
statusEl.innerHTML = isZip ? 'Uploading and importing... This may take a moment for large files.' : 'Importing...';
|
statusEl.innerHTML = isZip ? t('settings.import.uploading_zip') : t('settings.import.importing');
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
let res;
|
let res;
|
||||||
|
|
@ -255,28 +252,28 @@ export async function render(container) {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const imported = Object.entries(result.stats).filter(([k,v]) => v > 0 && k !== 'files_restored').map(([k,v]) => `${v} ${k}`).join(', ');
|
const imported = Object.entries(result.stats).filter(([k,v]) => v > 0 && k !== 'files_restored').map(([k,v]) => `${v} ${k}`).join(', ');
|
||||||
statusEl.style.color = 'var(--success)';
|
statusEl.style.color = 'var(--success)';
|
||||||
let html = `Import complete: ${imported}.`;
|
let html = t('settings.import.complete', { imported });
|
||||||
if (result.device_pairings?.length) {
|
if (result.device_pairings?.length) {
|
||||||
html += `<br><br><strong>Device Pairing Codes:</strong><br><table style="margin-top:8px;font-size:12px;border-collapse:collapse">` +
|
html += `<br><br><strong>${t('settings.import.pairing_codes_title')}</strong><br><table style="margin-top:8px;font-size:12px;border-collapse:collapse">` +
|
||||||
result.device_pairings.map(d => `<tr><td style="padding:4px 12px 4px 0">${d.name}</td><td style="font-family:monospace;font-weight:700;font-size:14px;letter-spacing:2px">${d.pairing_code}</td></tr>`).join('') +
|
result.device_pairings.map(d => `<tr><td style="padding:4px 12px 4px 0">${d.name}</td><td style="font-family:monospace;font-weight:700;font-size:14px;letter-spacing:2px">${d.pairing_code}</td></tr>`).join('') +
|
||||||
`</table><br>Enter these codes on each device to re-link them. All assignments and schedules will be preserved.`;
|
`</table><br>${t('settings.import.pairing_codes_hint')}`;
|
||||||
}
|
}
|
||||||
html += `<br><br>${(result.notes || []).map(n => '• ' + n).join('<br>')}`;
|
html += `<br><br>${(result.notes || []).map(n => '• ' + n).join('<br>')}`;
|
||||||
statusEl.innerHTML = html;
|
statusEl.innerHTML = html;
|
||||||
showToast('Data imported successfully', 'success');
|
showToast(t('settings.toast.import_success'), 'success');
|
||||||
} else {
|
} else {
|
||||||
statusEl.style.color = 'var(--danger)';
|
statusEl.style.color = 'var(--danger)';
|
||||||
statusEl.textContent = result.error || 'Import failed';
|
statusEl.textContent = result.error || t('settings.import.failed');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusEl.style.color = 'var(--danger)';
|
statusEl.style.color = 'var(--danger)';
|
||||||
statusEl.textContent = 'Import failed: ' + err.message;
|
statusEl.textContent = t('settings.import.failed_with_error', { error: err.message });
|
||||||
}
|
}
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusEl.style.color = 'var(--danger)';
|
statusEl.style.color = 'var(--danger)';
|
||||||
statusEl.textContent = 'Failed to read file: ' + err.message;
|
statusEl.textContent = t('settings.import.read_failed', { error: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -288,14 +285,14 @@ export async function render(container) {
|
||||||
|
|
||||||
document.getElementById('saveAcctBtn')?.addEventListener('click', async () => {
|
document.getElementById('saveAcctBtn')?.addEventListener('click', async () => {
|
||||||
const name = document.getElementById('acctName').value.trim();
|
const name = document.getElementById('acctName').value.trim();
|
||||||
if (!name) return showToast('Name cannot be empty', 'error');
|
if (!name) return showToast(t('settings.toast.name_required'), 'error');
|
||||||
const btn = document.getElementById('saveAcctBtn');
|
const btn = document.getElementById('saveAcctBtn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
const updated = await api.updateMe({ name });
|
const updated = await api.updateMe({ name });
|
||||||
const stored = JSON.parse(localStorage.getItem('user') || '{}');
|
const stored = JSON.parse(localStorage.getItem('user') || '{}');
|
||||||
localStorage.setItem('user', JSON.stringify({ ...stored, ...updated }));
|
localStorage.setItem('user', JSON.stringify({ ...stored, ...updated }));
|
||||||
showToast('Profile saved', 'success');
|
showToast(t('settings.toast.profile_saved'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -307,9 +304,9 @@ export async function render(container) {
|
||||||
const current = document.getElementById('acctCurrentPw').value;
|
const current = document.getElementById('acctCurrentPw').value;
|
||||||
const next = document.getElementById('acctNewPw').value;
|
const next = document.getElementById('acctNewPw').value;
|
||||||
const confirm = document.getElementById('acctConfirmPw').value;
|
const confirm = document.getElementById('acctConfirmPw').value;
|
||||||
if (!current) return showToast('Enter your current password', 'error');
|
if (!current) return showToast(t('settings.toast.current_password_required'), 'error');
|
||||||
if (next.length < 8) return showToast('New password must be at least 8 characters', 'error');
|
if (next.length < 8) return showToast(t('settings.toast.new_password_min_8'), 'error');
|
||||||
if (next !== confirm) return showToast('New passwords do not match', 'error');
|
if (next !== confirm) return showToast(t('settings.toast.passwords_dont_match'), 'error');
|
||||||
const btn = document.getElementById('changePwBtn');
|
const btn = document.getElementById('changePwBtn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
|
|
@ -317,7 +314,7 @@ export async function render(container) {
|
||||||
document.getElementById('acctCurrentPw').value = '';
|
document.getElementById('acctCurrentPw').value = '';
|
||||||
document.getElementById('acctNewPw').value = '';
|
document.getElementById('acctNewPw').value = '';
|
||||||
document.getElementById('acctConfirmPw').value = '';
|
document.getElementById('acctConfirmPw').value = '';
|
||||||
showToast('Password changed', 'success');
|
showToast(t('settings.toast.password_changed'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -336,10 +333,10 @@ async function loadWhiteLabel() {
|
||||||
const section = document.getElementById('whiteLabelSection');
|
const section = document.getElementById('whiteLabelSection');
|
||||||
if (section && user.plan_id !== 'enterprise' && user.role !== 'superadmin') {
|
if (section && user.plan_id !== 'enterprise' && user.role !== 'superadmin') {
|
||||||
section.innerHTML = `
|
section.innerHTML = `
|
||||||
<h3>White Label / Branding</h3>
|
<h3>${t('settings.white_label')}</h3>
|
||||||
<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:16px;text-align:center">
|
<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:16px;text-align:center">
|
||||||
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:8px">Custom branding is available on the Enterprise plan</p>
|
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:8px">${t('settings.white_label_enterprise_only')}</p>
|
||||||
<a href="#/billing" class="btn btn-secondary btn-sm" style="text-decoration:none">View Plans</a>
|
<a href="#/billing" class="btn btn-secondary btn-sm" style="text-decoration:none">${t('settings.view_plans')}</a>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
|
|
@ -376,7 +373,7 @@ async function loadWhiteLabel() {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
await resetBranding();
|
await resetBranding();
|
||||||
showToast('Branding saved', 'success');
|
showToast(t('settings.toast.branding_saved'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
}
|
}
|
||||||
|
|
@ -387,7 +384,7 @@ async function loadWhiteLabel() {
|
||||||
const bg = document.getElementById('wlBgColor').value;
|
const bg = document.getElementById('wlBgColor').value;
|
||||||
document.documentElement.style.setProperty('--accent', primary);
|
document.documentElement.style.setProperty('--accent', primary);
|
||||||
document.documentElement.style.setProperty('--bg-primary', bg);
|
document.documentElement.style.setProperty('--bg-primary', bg);
|
||||||
showToast('Preview applied (refresh to reset)', 'info');
|
showToast(t('settings.toast.preview_applied'), 'info');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,11 +405,11 @@ async function loadUsers() {
|
||||||
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:520px">
|
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:520px">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style="border-bottom:1px solid var(--border);text-align:left">
|
<tr style="border-bottom:1px solid var(--border);text-align:left">
|
||||||
<th style="padding:8px 12px;color:var(--text-muted);font-weight:500">User</th>
|
<th style="padding:8px 12px;color:var(--text-muted);font-weight:500">${t('settings.user.col_user')}</th>
|
||||||
<th style="padding:8px 12px;color:var(--text-muted);font-weight:500">Auth</th>
|
<th style="padding:8px 12px;color:var(--text-muted);font-weight:500">${t('settings.user.col_auth')}</th>
|
||||||
<th style="padding:8px 12px;color:var(--text-muted);font-weight:500">Role</th>
|
<th style="padding:8px 12px;color:var(--text-muted);font-weight:500">${t('settings.user.col_role')}</th>
|
||||||
<th style="padding:8px 12px;color:var(--text-muted);font-weight:500">Plan</th>
|
<th style="padding:8px 12px;color:var(--text-muted);font-weight:500">${t('settings.user.col_plan')}</th>
|
||||||
<th style="padding:8px 12px;color:var(--text-muted);font-weight:500">Actions</th>
|
<th style="padding:8px 12px;color:var(--text-muted);font-weight:500">${t('settings.user.col_actions')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -434,14 +431,14 @@ async function loadUsers() {
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:10px 12px">
|
<td style="padding:10px 12px">
|
||||||
${u.id !== currentUser.id ? `<button class="btn btn-danger btn-sm delete-user-btn" data-user-id="${u.id}">Remove</button>` : '<span style="color:var(--text-muted);font-size:11px">You</span>'}
|
${u.id !== currentUser.id ? `<button class="btn btn-danger btn-sm delete-user-btn" data-user-id="${u.id}">${t('settings.user.remove')}</button>` : `<span style="color:var(--text-muted);font-size:11px">${t('settings.user.you')}</span>`}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p style="color:var(--text-muted);font-size:11px;margin-top:12px">${users.length} user(s) registered</p>
|
<p style="color:var(--text-muted);font-size:11px;margin-top:12px">${tn('settings.user.count', users.length)}</p>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Plan change handlers
|
// Plan change handlers
|
||||||
|
|
@ -451,7 +448,7 @@ async function loadUsers() {
|
||||||
const planId = select.value;
|
const planId = select.value;
|
||||||
try {
|
try {
|
||||||
await api.assignPlan(userId, planId);
|
await api.assignPlan(userId, planId);
|
||||||
showToast('Plan updated', 'success');
|
showToast(t('settings.toast.plan_updated'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
loadUsers(); // Revert
|
loadUsers(); // Revert
|
||||||
|
|
@ -466,7 +463,7 @@ async function loadUsers() {
|
||||||
if (confirming) {
|
if (confirming) {
|
||||||
try {
|
try {
|
||||||
await api.deleteUser(btn.dataset.userId);
|
await api.deleteUser(btn.dataset.userId);
|
||||||
showToast('User removed', 'success');
|
showToast(t('settings.toast.user_removed'), 'success');
|
||||||
loadUsers();
|
loadUsers();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
|
|
@ -474,12 +471,12 @@ async function loadUsers() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
confirming = true;
|
confirming = true;
|
||||||
btn.textContent = 'Confirm?';
|
btn.textContent = t('settings.user.confirm');
|
||||||
btn.style.background = 'var(--danger)';
|
btn.style.background = 'var(--danger)';
|
||||||
btn.style.color = 'white';
|
btn.style.color = 'white';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
confirming = false;
|
confirming = false;
|
||||||
btn.textContent = 'Remove';
|
btn.textContent = t('settings.user.remove');
|
||||||
btn.style.background = '';
|
btn.style.background = '';
|
||||||
btn.style.color = '';
|
btn.style.color = '';
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue