1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
import { html, useState, useEffect } from './vendor/htm-preact.js';
import { t, getLocale, LOCALES } from './i18n.js';
import { ToggleSwitch, useSaver } from './settings-ui.js';
import { FolderPickerField } from './folder-tree.js';
// MeshBay's own locale codes (i18n.js LOCALES) to the language tag TMDB
// expects. Duplicated from nothing: this is the only place it lives now that
// the TMDB fields moved out of the monolithic settings page.
const TMDB_LANGUAGE_BY_LOCALE = {
en: 'en-US', fr: 'fr-FR', es: 'es-ES', 'pt-BR': 'pt-BR', 'zh-CN': 'zh-CN',
ja: 'ja-JP', de: 'de-DE', it: 'it-IT', nl: 'nl-NL', pl: 'pl-PL',
};
/**
* The Videos app's operator settings: which folders it works over, and the
* TMDB lookups it makes.
*
* Several folders now, not one. A film library is as likely to be two drives
* as one, and the single-root model made the second one invisible — the
* operator's only recourse was to point Videos at a parent containing both,
* which pulls in everything else under it too.
*
* The TMDB parts are two independent settings that happen to sit together:
* the on/off switch is per group, while the API key and the query language
* are node-wide, because they are one operator's credential and one shared
* cache (docs/mediacenter.md §5.5). They save separately for that reason.
*/
function VideoSettings({ roots, dirs, settings, saveDirectories, transport, signFn }) {
const { busy, msg, run } = useSaver();
const [directories, setDirectories] = useState(settings.videoDirectories || []);
const [tmdbEnabled, setTmdbEnabled] = useState(settings.tmdbEnabled !== false);
const [token, setToken] = useState('');
const [language, setLanguage] = useState(
settings.tmdbLanguage || TMDB_LANGUAGE_BY_LOCALE[getLocale()] || 'en-US');
useEffect(() => {
setDirectories(settings.videoDirectories || []);
}, [settings.videoDirectories]);
useEffect(() => {
setTmdbEnabled(settings.tmdbEnabled !== false);
}, [settings.tmdbEnabled]);
useEffect(() => {
if (settings.tmdbLanguage) setLanguage(settings.tmdbLanguage);
}, [settings.tmdbLanguage]);
const current = settings.videoDirectories || [];
const dirsDirty = directories.length !== current.length
|| directories.some((d, i) => d !== current[i]);
return html`
<div class="app-settings">
<${FolderPickerField}
label=${t('settings_app.video_directories_label')}
hint=${t('settings_app.video_directories_hint')}
roots=${roots} dirs=${dirs} mode="multi"
value=${directories} disabled=${busy}
onChange=${setDirectories} />
<button class="btn btn-small btn-secondary" style="margin-top:4px"
disabled=${busy || !dirsDirty}
onClick=${() => run(() => saveDirectories(directories))}>
${busy ? t('settings_app.saving') : t('settings_app.save')}
</button>
<h4 class="app-settings-sub">${t('settings_node.tmdb_title')}</h4>
<p class="settings-hint">${t('settings_node.tmdb_hint')}</p>
<div class="settings-row">
<${ToggleSwitch} checked=${tmdbEnabled} disabled=${busy}
onChange=${(v) => { setTmdbEnabled(v);
run(() => transport.setTmdbEnabled(v, signFn)); }}
label=${tmdbEnabled ? t('settings_node.tmdb_enabled')
: t('settings_node.tmdb_disabled')} />
</div>
<div class="settings-row">
<label class="settings-label">
${t('settings_node.tmdb_token_label')}
<input type="password" value=${token} disabled=${busy}
placeholder=${t('settings_node.tmdb_token_placeholder')}
onInput=${(e) => setToken(e.target.value)} />
</label>
${/* No "optional", and no mention of a shipped default. A key that
works without one is a key somebody else is paying the rate
limit for, and the operator should know they are meant to have
their own. */''}
<p class="settings-hint">
${settings.tmdbTokenCustomized
? t('settings_node.tmdb_token_customized')
: t('settings_app.tmdb_token_prompt')}
${' '}
<a href="https://www.themoviedb.org/settings/api" target="_blank"
rel="noopener noreferrer">${t('settings_app.tmdb_token_link')}</a>
</p>
</div>
<div class="settings-row">
<label class="settings-label">
${t('settings_node.tmdb_language_label')}
<select value=${language} disabled=${busy}
onChange=${(e) => setLanguage(e.target.value)}>
${LOCALES.map((l) => html`
<option key=${l.code} value=${TMDB_LANGUAGE_BY_LOCALE[l.code]}>
${l.name}
</option>`)}
</select>
</label>
<p class="settings-hint">${t('settings_node.tmdb_language_hint')}</p>
</div>
<button class="btn btn-small btn-secondary" 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
// empty on every render because a secret is not read back, so
// sending it as a value would wipe the key every time the language
// was changed.
await transport.setTmdbConfig(token || undefined, language, signFn);
setToken('');
})}>
${busy ? t('settings_app.saving') : t('settings_node.tmdb_save')}
</button>
${msg && html`<p class="settings-hint">${msg}</p>`}
</div>
`;
}
export { VideoSettings, TMDB_LANGUAGE_BY_LOCALE };
|