diff --git a/android/app/src/main/java/com/remotedisplay/player/util/WebViewSupport.kt b/android/app/src/main/java/com/remotedisplay/player/util/WebViewSupport.kt index cb7baac..392604e 100644 --- a/android/app/src/main/java/com/remotedisplay/player/util/WebViewSupport.kt +++ b/android/app/src/main/java/com/remotedisplay/player/util/WebViewSupport.kt @@ -38,6 +38,14 @@ object WebViewSupport { mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW } webView.setBackgroundColor(android.graphics.Color.TRANSPARENT) + // Interactive widgets (e.g. directory-search) need the WebView to take + // touch focus so the search field accepts a tap/cursor inside the kiosk + // lock-task WebView. Harmless for passive widgets (board/YouTube): they + // have no focusable inputs, so nothing steals focus or pops the IME. The + // widget's own on-screen keyboard drives the filter even when the system + // IME is suppressed (it mutates the input value directly, no focus needed). + webView.isFocusable = true + webView.isFocusableInTouchMode = true webView.webViewClient = object : WebViewClient() { override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) { if (request?.isForMainFrame == true) { diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 23062e6..43d5045 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -222,7 +222,7 @@ components: type: string widget_type: type: string - description: clock | weather | rss | text | webpage | social | directory-board + description: clock | weather | rss | text | webpage | social | directory-board | directory-search name: type: string config: diff --git a/frontend/js/i18n/de.js b/frontend/js/i18n/de.js index 919d570..3d13074 100644 --- a/frontend/js/i18n/de.js +++ b/frontend/js/i18n/de.js @@ -533,6 +533,21 @@ export default { 'widget.type.social.desc': 'Social-Media-Feed', 'widget.type.directory_board.name': 'Verzeichnistafel', 'widget.type.directory_board.desc': 'Scrollendes Mieter-/Raumverzeichnis für Lobbys', + 'widget.type.directory_search.name': 'Verzeichnissuche', + 'widget.type.directory_search.desc': 'Interaktive Suche in einer Verzeichnistafel', + // directory-search widget + 'widget.dirsearch.source_label': 'Verzeichnistafel', + 'widget.dirsearch.source_hint': 'Liest die Einträge live aus der ausgewählten Tafel; nichts wird kopiert.', + 'widget.dirsearch.source_empty': 'Erstellen Sie zuerst eine Verzeichnistafel: Dieses Widget durchsucht eine vorhandene Tafel.', + 'widget.dirsearch.title_label': 'Titel', + 'widget.dirsearch.title_placeholder': 'Mieter finden', + 'widget.dirsearch.logo_label': 'Logo (optional)', + 'widget.dirsearch.placeholder_label': 'Platzhalter des Suchfelds', + 'widget.dirsearch.placeholder_hint': 'Name, Büro oder Abteilung eingeben…', + 'widget.dirsearch.theme': 'Design', + 'widget.dirsearch.keyboard_label': 'Bildschirmtastatur anzeigen', + 'widget.dirsearch.no_matches': 'Keine Treffer', + 'widget.dirsearch.hint': 'Tippen Sie, um das Verzeichnis zu durchsuchen…', 'widget.field.name': 'Widget-Name', 'widget.field.format': 'Format', 'widget.field.format_12h': '12 Stunden', diff --git a/frontend/js/i18n/en.js b/frontend/js/i18n/en.js index 4babdca..492e540 100644 --- a/frontend/js/i18n/en.js +++ b/frontend/js/i18n/en.js @@ -702,6 +702,21 @@ export default { 'widget.type.social.desc': 'Social media feed', 'widget.type.directory_board.name': 'Directory Board', 'widget.type.directory_board.desc': 'Scrolling tenant/room directory for lobbies', + 'widget.type.directory_search.name': 'Directory Search', + 'widget.type.directory_search.desc': 'Walk-up search of a directory board', + // directory-search widget + 'widget.dirsearch.source_label': 'Directory board', + 'widget.dirsearch.source_hint': 'Reads entries live from the selected board — nothing is copied.', + 'widget.dirsearch.source_empty': 'Create a directory board first — this widget searches an existing board.', + 'widget.dirsearch.title_label': 'Title', + 'widget.dirsearch.title_placeholder': 'Find a Tenant', + 'widget.dirsearch.logo_label': 'Logo (optional)', + 'widget.dirsearch.placeholder_label': 'Search box placeholder', + 'widget.dirsearch.placeholder_hint': 'Type a name, suite, or department…', + 'widget.dirsearch.theme': 'Theme', + 'widget.dirsearch.keyboard_label': 'Show on-screen keyboard', + 'widget.dirsearch.no_matches': 'No matches', + 'widget.dirsearch.hint': 'Start typing to search the directory…', // Widget config form fields 'widget.field.name': 'Widget Name', 'widget.field.format': 'Format', diff --git a/frontend/js/i18n/es.js b/frontend/js/i18n/es.js index fd64d95..7e5e3f6 100644 --- a/frontend/js/i18n/es.js +++ b/frontend/js/i18n/es.js @@ -533,6 +533,21 @@ export default { 'widget.type.social.desc': 'Feed de redes sociales', 'widget.type.directory_board.name': 'Directorio', 'widget.type.directory_board.desc': 'Directorio de inquilinos/salas con desplazamiento para vestíbulos', + 'widget.type.directory_search.name': 'Búsqueda de directorio', + 'widget.type.directory_search.desc': 'Búsqueda interactiva de un panel de directorio', + // directory-search widget + 'widget.dirsearch.source_label': 'Panel de directorio', + 'widget.dirsearch.source_hint': 'Lee las entradas en vivo del panel seleccionado; no se copia nada.', + 'widget.dirsearch.source_empty': 'Cree primero un panel de directorio: este widget busca en un panel existente.', + 'widget.dirsearch.title_label': 'Título', + 'widget.dirsearch.title_placeholder': 'Buscar un inquilino', + 'widget.dirsearch.logo_label': 'Logotipo (opcional)', + 'widget.dirsearch.placeholder_label': 'Texto del cuadro de búsqueda', + 'widget.dirsearch.placeholder_hint': 'Escriba un nombre, oficina o departamento…', + 'widget.dirsearch.theme': 'Tema', + 'widget.dirsearch.keyboard_label': 'Mostrar teclado en pantalla', + 'widget.dirsearch.no_matches': 'Sin coincidencias', + 'widget.dirsearch.hint': 'Empiece a escribir para buscar en el directorio…', 'widget.field.name': 'Nombre del widget', 'widget.field.format': 'Formato', 'widget.field.format_12h': '12 horas', diff --git a/frontend/js/i18n/fr.js b/frontend/js/i18n/fr.js index 1525925..4664fc2 100644 --- a/frontend/js/i18n/fr.js +++ b/frontend/js/i18n/fr.js @@ -533,6 +533,21 @@ export default { 'widget.type.social.desc': 'Flux de réseaux sociaux', 'widget.type.directory_board.name': 'Annuaire', 'widget.type.directory_board.desc': 'Annuaire défilant des locataires/salles pour halls', + 'widget.type.directory_search.name': 'Recherche d’annuaire', + 'widget.type.directory_search.desc': 'Recherche interactive d’un panneau d’annuaire', + // directory-search widget + 'widget.dirsearch.source_label': 'Panneau d’annuaire', + 'widget.dirsearch.source_hint': 'Lit les entrées en direct depuis le panneau sélectionné ; rien n’est copié.', + 'widget.dirsearch.source_empty': 'Créez d’abord un panneau d’annuaire : ce widget recherche dans un panneau existant.', + 'widget.dirsearch.title_label': 'Titre', + 'widget.dirsearch.title_placeholder': 'Trouver un locataire', + 'widget.dirsearch.logo_label': 'Logo (facultatif)', + 'widget.dirsearch.placeholder_label': 'Texte du champ de recherche', + 'widget.dirsearch.placeholder_hint': 'Saisissez un nom, un bureau ou un service…', + 'widget.dirsearch.theme': 'Thème', + 'widget.dirsearch.keyboard_label': 'Afficher le clavier à l’écran', + 'widget.dirsearch.no_matches': 'Aucun résultat', + 'widget.dirsearch.hint': 'Commencez à taper pour rechercher dans l’annuaire…', 'widget.field.name': 'Nom du widget', 'widget.field.format': 'Format', 'widget.field.format_12h': '12 heures', diff --git a/frontend/js/i18n/it.js b/frontend/js/i18n/it.js index bbaee53..c35a90a 100644 --- a/frontend/js/i18n/it.js +++ b/frontend/js/i18n/it.js @@ -513,6 +513,21 @@ export default { 'widget.type.social.desc': 'Feed dei social media', 'widget.type.directory_board.name': 'Albo Fornitori/Directory', 'widget.type.directory_board.desc': 'Elenco scorrevole di inquilini/uffici per hall', + 'widget.type.directory_search.name': 'Ricerca directory', + 'widget.type.directory_search.desc': 'Ricerca interattiva di un pannello directory', + // directory-search widget + 'widget.dirsearch.source_label': 'Pannello directory', + 'widget.dirsearch.source_hint': 'Legge le voci in tempo reale dal pannello selezionato; nulla viene copiato.', + 'widget.dirsearch.source_empty': 'Crea prima un pannello directory: questo widget cerca in un pannello esistente.', + 'widget.dirsearch.title_label': 'Titolo', + 'widget.dirsearch.title_placeholder': 'Trova un inquilino', + 'widget.dirsearch.logo_label': 'Logo (opzionale)', + 'widget.dirsearch.placeholder_label': 'Segnaposto casella di ricerca', + 'widget.dirsearch.placeholder_hint': 'Digita un nome, ufficio o reparto…', + 'widget.dirsearch.theme': 'Tema', + 'widget.dirsearch.keyboard_label': 'Mostra tastiera a schermo', + 'widget.dirsearch.no_matches': 'Nessuna corrispondenza', + 'widget.dirsearch.hint': 'Inizia a digitare per cercare nella directory…', // Widget config form fields 'widget.field.name': 'Nome Widget', 'widget.field.format': 'Formato', diff --git a/frontend/js/i18n/pt.js b/frontend/js/i18n/pt.js index 3747a32..6e78036 100644 --- a/frontend/js/i18n/pt.js +++ b/frontend/js/i18n/pt.js @@ -533,6 +533,21 @@ export default { 'widget.type.social.desc': 'Feed de redes sociais', 'widget.type.directory_board.name': 'Diretório', 'widget.type.directory_board.desc': 'Diretório rolante de inquilinos/salas para lobbies', + 'widget.type.directory_search.name': 'Pesquisa de diretório', + 'widget.type.directory_search.desc': 'Pesquisa interativa de um painel de diretório', + // directory-search widget + 'widget.dirsearch.source_label': 'Painel de diretório', + 'widget.dirsearch.source_hint': 'Lê as entradas ao vivo do painel selecionado; nada é copiado.', + 'widget.dirsearch.source_empty': 'Crie primeiro um painel de diretório: este widget pesquisa em um painel existente.', + 'widget.dirsearch.title_label': 'Título', + 'widget.dirsearch.title_placeholder': 'Encontrar um inquilino', + 'widget.dirsearch.logo_label': 'Logotipo (opcional)', + 'widget.dirsearch.placeholder_label': 'Texto do campo de pesquisa', + 'widget.dirsearch.placeholder_hint': 'Digite um nome, sala ou departamento…', + 'widget.dirsearch.theme': 'Tema', + 'widget.dirsearch.keyboard_label': 'Mostrar teclado na tela', + 'widget.dirsearch.no_matches': 'Sem correspondências', + 'widget.dirsearch.hint': 'Comece a digitar para pesquisar no diretório…', 'widget.field.name': 'Nome do widget', 'widget.field.format': 'Formato', 'widget.field.format_12h': '12 horas', diff --git a/frontend/js/views/widgets.js b/frontend/js/views/widgets.js index 413b7ca..c283bfb 100644 --- a/frontend/js/views/widgets.js +++ b/frontend/js/views/widgets.js @@ -6,7 +6,7 @@ const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': // Widget type ids only — name + desc are looked up via t() so they switch // language with the rest of the UI. -const WIDGET_TYPES = ['clock', 'weather', 'rss', 'text', 'webpage', 'social', 'directory-board']; +const WIDGET_TYPES = ['clock', 'weather', 'rss', 'text', 'webpage', 'social', 'directory-board', 'directory-search']; const WIDGET_ICONS = { clock: '🕓', weather: '⛅', @@ -15,6 +15,7 @@ const WIDGET_ICONS = { webpage: '🌐', social: '💬', 'directory-board': '🏢', + 'directory-search': '🔍', }; const widgetTypeName = (id) => t(`widget.type.${id.replace(/-/g, '_')}.name`); const widgetTypeDesc = (id) => t(`widget.type.${id.replace(/-/g, '_')}.desc`); @@ -179,6 +180,9 @@ export async function render(container) { let editingWidget = null; let creatingType = null; let dirState = { categories: [], logo_url: '', background_images: [] }; + // Cached widget list from the last load — used to populate the directory-search + // source-board dropdown without a second fetch. + let loadedWidgets = []; document.getElementById('newWidgetBtn').onclick = () => { const grid = document.getElementById('widgetTypeGrid'); @@ -279,6 +283,26 @@ export async function render(container) { `; break; + case 'directory-search': { + const boards = (loadedWidgets || []).filter(w => w.widget_type === 'directory-board'); + const sourceField = boards.length + ? ` +
${t('widget.empty_desc')}