personal web client for Bluesky
typescript solidjs bluesky atcute
at trunk 3.5 kB view raw
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;