personal web client for Bluesky
typescript
solidjs
bluesky
atcute
1import { For, Show, createMemo, createSignal, untrack } from 'solid-js';
2
3import { mapDefined } from '@mary/array-fns';
4
5import { LANGUAGE_CODES, getEnglishLanguageName, getNativeLanguageName } from '~/lib/intl/languages';
6import { useTitle } from '~/lib/navigation/router';
7import { useSession } from '~/lib/states/session';
8
9import * as Boxed from '~/components/boxed';
10import EndOfListView from '~/components/end-of-list-view';
11import CheckOutlinedIcon from '~/components/icons-central/check-outline';
12import * as Page from '~/components/page';
13import SearchInput from '~/components/search-input';
14
15const ContentTranslationExclusionSettingsPage = () => {
16 const { currentAccount } = useSession();
17
18 const preferences = currentAccount!.preferences;
19 const translationPrefs = preferences.translation;
20
21 const [search, setSearch] = createSignal('');
22
23 const availableLanguages = mapDefined(LANGUAGE_CODES, (code) => {
24 const englishName = getEnglishLanguageName(code);
25 const nativeName = getNativeLanguageName(code);
26
27 if (!englishName || !nativeName) {
28 return;
29 }
30
31 return {
32 query: `${code}${englishName}${nativeName}`.toLowerCase(),
33 code: code,
34 english: englishName,
35 native: nativeName,
36 };
37 });
38
39 const normalizedSearch = createMemo(() => search().trim().toLowerCase());
40 const filteredLanguages = createMemo(() => {
41 const $search = normalizedSearch();
42
43 let filtered: typeof availableLanguages;
44 if ($search === '') {
45 filtered = availableLanguages.slice();
46 } else {
47 filtered = availableLanguages.filter((entry) => entry.query.includes($search));
48 }
49
50 const boundary = filtered.length;
51
52 untrack(() => {
53 const $languages = translationPrefs.exclusions;
54
55 filtered.sort((a, b) => {
56 const aidx = $languages.indexOf(a.code);
57 const bidx = $languages.indexOf(b.code);
58
59 return (aidx !== -1 ? aidx : boundary) - (bidx !== -1 ? bidx : boundary);
60 });
61 });
62
63 return filtered;
64 });
65
66 useTitle(() => `Content translation exclusions settings — ${import.meta.env.VITE_APP_NAME}`);
67
68 return (
69 <>
70 <Page.Header>
71 <Page.HeaderAccessory>
72 <Page.Back to="/settings/content/translation" />
73 </Page.HeaderAccessory>
74
75 <Page.Heading title="Exclude languages from translation" />
76 </Page.Header>
77
78 <Boxed.Container>
79 <Boxed.Group>
80 <div class="mx-4 mb-2">
81 <SearchInput value={search()} onChange={setSearch} />
82 </div>
83
84 <Show when={filteredLanguages().length === 0}>
85 <EndOfListView />
86 </Show>
87
88 <Boxed.List>
89 <For each={filteredLanguages()}>
90 {({ code, english, native }) => {
91 const index = createMemo(() => translationPrefs.exclusions.indexOf(code));
92
93 return (
94 <button
95 onClick={() => {
96 const $index = index();
97
98 if ($index === -1) {
99 translationPrefs.exclusions.push(code);
100 } else {
101 translationPrefs.exclusions.splice($index, 1);
102 }
103 }}
104 class="flex items-center justify-between gap-3 px-4 py-3 text-left hover:bg-contrast/sm active:bg-contrast/sm-pressed"
105 >
106 <div class="text-sm">
107 <p>{english}</p>
108 <p class="text-contrast-muted">{native}</p>
109 </div>
110
111 {index() !== -1 && <CheckOutlinedIcon class="text-2xl text-accent" />}
112 </button>
113 );
114 }}
115 </For>
116 </Boxed.List>
117 </Boxed.Group>
118 </Boxed.Container>
119 </>
120 );
121};
122
123export default ContentTranslationExclusionSettingsPage;