diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
16 files changed, 95 insertions, 17 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/chat-app-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/chat-app-settings.js index 68569b9..5a15074 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/chat-app-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/chat-app-settings.js @@ -45,11 +45,14 @@ function ChatSettings({ roots, dirs, settings, saveDirectories, transport, signF value=${directory} disabled=${busy || noWritable} onChange=${setDirectory} /> - <button class="btn btn-small btn-secondary" style="margin-top:4px" - disabled=${busy || !dirty} + <div class="app-save-row"> + <button class="app-save" disabled=${busy || !dirty} onClick=${() => run(() => transport.setChatDirectory(directory, signFn))}> ${busy ? t('settings_app.saving') : t('settings_app.save')} </button> + ${!busy && !dirty && html` + <span class="app-save-why">${t('settings_app.no_changes')}</span>`} + </div> <div class="settings-row" style="margin-top:12px"> <${ToggleSwitch} checked=${linkPreview} disabled=${busy} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index d05d3a3..d47762e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -840,6 +840,7 @@ export default { 'folder_tree.read_only_blocked': 'Nur lesen — kein Schreiben möglich', 'settings_app.save': 'Speichern', 'settings_app.saving': 'Wird gespeichert…', + 'settings_app.no_changes': 'Keine Änderungen zu speichern', 'settings_app.disabled_hint': 'Schalten Sie diese App ein, um sie zu konfigurieren.', 'settings_app.video_directories_label': 'Video-Ordner', 'settings_app.video_directories_hint': 'Wo die Filme und Serien dieser Gruppe liegen. Nichts außerhalb erscheint im Videos-Tab.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 6dbb70f..932aad3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -628,6 +628,7 @@ export default { 'folder_tree.read_only_blocked': 'Read-only — cannot be written to', 'settings_app.save': 'Save', 'settings_app.saving': 'Saving…', + 'settings_app.no_changes': 'No changes to save', 'settings_app.disabled_hint': 'Turn this app on to configure it.', 'settings_app.video_directories_label': 'Video folders', 'settings_app.video_directories_hint': 'Where this group\'s films and shows live. Nothing outside them appears in the Videos tab.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index aa950cc..ca912a4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -836,6 +836,7 @@ export default { 'folder_tree.read_only_blocked': 'Solo lectura: no se puede escribir', 'settings_app.save': 'Guardar', 'settings_app.saving': 'Guardando…', + 'settings_app.no_changes': 'Sin cambios que guardar', 'settings_app.disabled_hint': 'Activa esta aplicación para configurarla.', 'settings_app.video_directories_label': 'Carpetas de vídeo', 'settings_app.video_directories_hint': 'Dónde están las películas y series de este grupo. Nada fuera de ellas aparece en la pestaña Vídeos.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index daa9f94..ccd9e25 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -854,6 +854,7 @@ export default { 'folder_tree.read_only_blocked': 'Lecture seule — écriture impossible', 'settings_app.save': 'Enregistrer', 'settings_app.saving': 'Enregistrement…', + 'settings_app.no_changes': 'Aucune modification à enregistrer', 'settings_app.disabled_hint': 'Activez cette application pour la configurer.', 'settings_app.video_directories_label': 'Dossiers vidéo', 'settings_app.video_directories_hint': 'Où vivent les films et séries de ce groupe. Rien en dehors n\'apparaît dans l\'onglet Vidéos.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 364c26c..c1c3d52 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -850,6 +850,7 @@ export default { 'folder_tree.read_only_blocked': 'Sola lettura: non vi si può scrivere', 'settings_app.save': 'Salva', 'settings_app.saving': 'Salvataggio…', + 'settings_app.no_changes': 'Nessuna modifica da salvare', 'settings_app.disabled_hint': 'Attiva questa applicazione per configurarla.', 'settings_app.video_directories_label': 'Cartelle video', 'settings_app.video_directories_hint': 'Dove si trovano film e serie di questo gruppo. Nulla al di fuori compare nella scheda Video.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index f778fc4..e5392a3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -834,6 +834,7 @@ export default { 'folder_tree.read_only_blocked': '読み取り専用 — 書き込みできません', 'settings_app.save': '保存', 'settings_app.saving': '保存中…', + 'settings_app.no_changes': '保存する変更はありません', 'settings_app.disabled_hint': 'このアプリを有効にすると設定できます。', 'settings_app.video_directories_label': '動画フォルダー', 'settings_app.video_directories_hint': 'このグループの映画や番組がある場所です。それ以外は動画タブに表示されません。', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 6e6842e..09e3e8d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -852,6 +852,7 @@ export default { 'folder_tree.read_only_blocked': 'Alleen-lezen — kan niet worden beschreven', 'settings_app.save': 'Opslaan', 'settings_app.saving': 'Opslaan…', + 'settings_app.no_changes': 'Geen wijzigingen om op te slaan', 'settings_app.disabled_hint': 'Zet deze app aan om hem in te stellen.', 'settings_app.video_directories_label': 'Videomappen', 'settings_app.video_directories_hint': 'Waar de films en series van deze groep staan. Niets daarbuiten verschijnt op het tabblad Video\'s.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index c8cbd16..34847e1 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -878,6 +878,7 @@ export default { 'folder_tree.read_only_blocked': 'Tylko do odczytu — nie można zapisywać', 'settings_app.save': 'Zapisz', 'settings_app.saving': 'Zapisywanie…', + 'settings_app.no_changes': 'Brak zmian do zapisania', 'settings_app.disabled_hint': 'Włącz tę aplikację, aby ją skonfigurować.', 'settings_app.video_directories_label': 'Foldery wideo', 'settings_app.video_directories_hint': 'Gdzie znajdują się filmy i seriale tej grupy. Nic poza nimi nie pojawi się w zakładce Wideo.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 6eb2242..fc211c9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -837,6 +837,7 @@ export default { 'folder_tree.read_only_blocked': 'Somente leitura — não é possível gravar', 'settings_app.save': 'Salvar', 'settings_app.saving': 'Salvando…', + 'settings_app.no_changes': 'Nenhuma alteração para salvar', 'settings_app.disabled_hint': 'Ative este aplicativo para configurá-lo.', 'settings_app.video_directories_label': 'Pastas de vídeo', 'settings_app.video_directories_hint': 'Onde ficam os filmes e séries deste grupo. Nada fora delas aparece na aba Vídeos.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 044611e..9db7cc7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -821,6 +821,7 @@ export default { 'folder_tree.read_only_blocked': '只读 — 无法写入', 'settings_app.save': '保存', 'settings_app.saving': '正在保存…', + 'settings_app.no_changes': '没有需要保存的更改', 'settings_app.disabled_hint': '启用该应用后即可配置。', 'settings_app.video_directories_label': '视频文件夹', 'settings_app.video_directories_hint': '该群组的影片和剧集所在位置。其外的内容不会出现在「视频」标签页。', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-app-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/music-app-settings.js index 01174b1..a3b02d9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-app-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app-settings.js @@ -38,11 +38,14 @@ function MusicSettings({ roots, dirs, settings, saveDirectories, transport, sign value=${directories} disabled=${busy} onChange=${setDirectories} /> - <button class="btn btn-small btn-secondary" style="margin-top:4px" - disabled=${busy || !dirsDirty} + <div class="app-save-row"> + <button class="app-save" disabled=${busy || !dirsDirty} onClick=${() => run(() => saveDirectories(directories))}> ${busy ? t('settings_app.saving') : t('settings_app.save')} </button> + ${!busy && !dirsDirty && html` + <span class="app-save-why">${t('settings_app.no_changes')}</span>`} + </div> <h4 class="app-settings-sub">${t('settings_node.musicbrainz_title')}</h4> <p class="settings-hint">${t('settings_node.musicbrainz_hint')}</p> diff --git a/packages/meshbay-hub/src/meshbay_hub/static/photos-app-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/photos-app-settings.js index 81e6cc1..a241eea 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/photos-app-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/photos-app-settings.js @@ -32,11 +32,14 @@ function PhotoSettings({ roots, dirs, settings, saveDirectories }) { value=${directories} disabled=${busy} onChange=${setDirectories} /> - <button class="btn btn-small btn-secondary" style="margin-top:4px" - disabled=${busy || !dirty} + <div class="app-save-row"> + <button class="app-save" disabled=${busy || !dirty} onClick=${() => run(() => saveDirectories(directories))}> ${busy ? t('settings_app.saving') : t('settings_app.save')} </button> + ${!busy && !dirty && html` + <span class="app-save-why">${t('settings_app.no_changes')}</span>`} + </div> ${msg && html`<p class="settings-hint">${msg}</p>`} </div> `; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index 9c0d869..bf4e8e1 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -1271,6 +1271,36 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } /* One app's settings pane, inside its section. */ .app-settings > .settings-row:first-child { margin-top: 0; } +/* The Save button of an app's settings pane. + * + * It was `btn btn-small btn-secondary`, and there is no `.btn` rule at all — + * so it inherited `.btn-secondary`: no background, a transparent border, dim + * grey text. Enabled, it already looked like a disabled control; disabled, it + * was the same thing at 40% opacity. "You cannot always click Save, you do not + * notice, and it does not work" was one sentence describing all of that. + * + * It looks like a button now, and a disabled one looks disabled. */ +.app-save { + display: inline-flex; align-items: center; gap: 6px; + height: 30px; padding: 0 14px; margin-top: 4px; + border: 1px solid var(--accent); border-radius: 6px; + background: var(--accent); color: var(--accent-text); + font-family: inherit; font-size: 0.83em; cursor: pointer; +} +.app-save:hover:not(:disabled) { background: var(--accent-hover); } +/* No transition on the enabled/disabled swap. It animates a colour change that + coincides with a text-colour change, which reads as a flicker — and it made + the state genuinely hard to observe: a measurement taken during it reports a + transparent background on a button that is not transparent. */ +.app-save:disabled { + background: none; color: var(--text-dim); + border-color: var(--border); opacity: 1; cursor: default; +} +/* Why it is inert, beside it — a disabled control that explains itself is the + difference between "nothing changed" and "this is broken". */ +.app-save-row { display: flex; align-items: center; gap: 10px; margin-top: 8px; } +.app-save-why { font-size: 0.82em; color: var(--text-dim); } + .app-settings-sub { margin: 18px 0 4px; font-size: 0.9em; font-weight: 600; padding-top: 12px; border-top: 1px solid var(--border); diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 3acfd20..179292e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -72,13 +72,35 @@ function _aborted() { // change anything — `op` is already on every admin_challenge, and this // list is what lets a response two steps later be tied back to the right // one. -// The acks whose payload is state no caller could have predicted: they carry -// the node's whole roots table back. See the note where they are dispatched. -const ROOT_ACK_TYPES = new Set([ +// Acks the node *broadcasts* to everyone in the group, which the requester +// therefore also has to be handed. +// +// `_dispatch` resolves an admin ack against the pending request and returns, +// which is right for an op whose caller already knows the value it chose. It is +// wrong for these: every *other* connected client learns the change from the +// broadcast, and the one that asked for it is the only one that does not, +// because its own request swallowed its copy. Found twice — first on the root +// table, then on Chat's directory, where it meant the pane went on showing an +// unsaved-looking draft after a save that had worked. +const BROADCAST_ACK_TYPES = new Set([ 'root_update_ack', 'root_eject_ack', 'root_plug_ack', 'root_add_ack', 'root_remove_ack', + 'app_directories_ack', 'chat_directory_ack', 'chat_link_preview_ack', ]); +/** Hand a broadcast ack to the callback that would have had it from a peer. */ +function _replayBroadcast(transport, msg) { + if (msg.type === 'app_directories_ack' && transport._onAppDirectories) { + transport._onAppDirectories(msg.app, msg.directories || []); + } else if (msg.type === 'chat_directory_ack' && transport._onChatDirectory) { + transport._onChatDirectory(msg.path || ''); + } else if (msg.type === 'chat_link_preview_ack' && transport._onChatLinkPreview) { + transport._onChatLinkPreview(Boolean(msg.enabled)); + } else if (transport._onRootsChanged) { + transport._onRootsChanged(msg); + } +} + const ADMIN_OP_TYPES = new Set([ 'tmdb_override', 'tmdb_rematch', 'tmdb_config', 'tmdb_enabled', 'video_root', 'audio_root', 'photo_roots', @@ -2349,9 +2371,7 @@ class MeshBayTransport { // name it settled on. Returning here left the operator who clicked // Eject as the one client that never saw it happen, while every // other peer got the broadcast. So this one type is handed on. - if (ROOT_ACK_TYPES.has(msg.type) && this._onRootsChanged) { - this._onRootsChanged(msg); - } + if (BROADCAST_ACK_TYPES.has(msg.type)) _replayBroadcast(this, msg); return; } } @@ -2486,8 +2506,10 @@ class MeshBayTransport { // A root's flags changed, or one was ejected, plugged, added or removed. // Broadcast by the node to every peer, so everyone's table updates without // waiting for the next index_sync. - if (ROOT_ACK_TYPES.has(msg.type) && this._onRootsChanged) { - this._onRootsChanged(msg); + if (msg.type === 'root_update_ack' || msg.type === 'root_eject_ack' + || msg.type === 'root_plug_ack' || msg.type === 'root_add_ack' + || msg.type === 'root_remove_ack') { + if (this._onRootsChanged) this._onRootsChanged(msg); } // The operator's node is scanning — never the entries themselves, just diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-app-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/video-app-settings.js index b2d5015..86e47ec 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/video-app-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app-settings.js @@ -56,11 +56,14 @@ function VideoSettings({ roots, dirs, settings, saveDirectories, transport, sign value=${directories} disabled=${busy} onChange=${setDirectories} /> - <button class="btn btn-small btn-secondary" style="margin-top:4px" - disabled=${busy || !dirsDirty} + <div class="app-save-row"> + <button class="app-save" disabled=${busy || !dirsDirty} onClick=${() => run(() => saveDirectories(directories))}> ${busy ? t('settings_app.saving') : t('settings_app.save')} </button> + ${!busy && !dirsDirty && html` + <span class="app-save-why">${t('settings_app.no_changes')}</span>`} + </div> <h4 class="app-settings-sub">${t('settings_node.tmdb_title')}</h4> <p class="settings-hint">${t('settings_node.tmdb_hint')}</p> @@ -108,7 +111,11 @@ function VideoSettings({ roots, dirs, settings, saveDirectories, transport, sign <p class="settings-hint">${t('settings_node.tmdb_language_hint')}</p> </div> - <button class="btn btn-small btn-secondary" disabled=${busy} + ${/* The same control as the one above it. Two Save buttons in one pane + that do not look alike is worse than either looking wrong. This one + is never inert: the language always has a value to send, and an + empty token means "leave the stored one alone", not "no change". */''} + <button class="app-save" disabled=${busy} onClick=${() => run(async () => { // `undefined` for the token means "leave the stored one alone", // which is not the same as `''` — that clears it. The input starts |