mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
* basic i18n poc * translate home, filters, tabs support dot notation in backend for namespaces * i18n context menu, artist filters, list controls also nothings here fix backend not caching fallback * i18n queue, artist view, search/results * i18n settings and server view * Added translation using Weblate (Norwegian Bokmål) * Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (6 of 6 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/nb_NO/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/ * fix url escaping * added some mostly naive text overflow fixes rewrote filter context menu as a slide in because the old one apparently can't handle dynamic width * Added translation using Weblate (French) * Translated using Weblate (French) Currently translated at 17.4% (11 of 63 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * Translated using Weblate (French) Currently translated at 19.0% (12 of 63 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * Translated using Weblate (French) Currently translated at 40.0% (26 of 65 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * add weblate and some pretty badges to readme * fix link * Translated using Weblate (French) Currently translated at 50.7% (33 of 65 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * Translated using Weblate (English) Currently translated at 100.0% (65 of 65 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/en/ * Translated using Weblate (French) Currently translated at 90.7% (59 of 65 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * i18n now playing context type fix overscroll on new filter menu fix getting default namespace from the i18n backend * Translated using Weblate (French) Currently translated at 96.9% (63 of 65 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * Translated using Weblate (French) Currently translated at 100.0% (66 of 66 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * Translated using Weblate (Japanese) (#98) Currently translated at 7.5% (5 of 66 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/ja/ Co-authored-by: Austin Riedhammer <austinried@functionkey.xyz> * little note to remind me why that's there * update licenses Co-authored-by: Allan Nordhøy <epost@anotheragency.no> Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Clyhtsuriva <aimeric@adjutor.xyz>
210 lines
5.6 KiB
TypeScript
210 lines
5.6 KiB
TypeScript
import Button from '@app/components/Button'
|
|
import GradientScrollView from '@app/components/GradientScrollView'
|
|
import Header from '@app/components/Header'
|
|
import ListItem from '@app/components/ListItem'
|
|
import NothingHere from '@app/components/NothingHere'
|
|
import TextInput from '@app/components/TextInput'
|
|
import { withSuspense, withSuspenseMemo } from '@app/components/withSuspense'
|
|
import { useQuerySearchResults } from '@app/hooks/query'
|
|
import { useSetQueue } from '@app/hooks/trackplayer'
|
|
import { Album, Artist, SearchResults, Song } from '@app/models/library'
|
|
import colors from '@app/styles/colors'
|
|
import font from '@app/styles/font'
|
|
import { useFocusEffect, useNavigation } from '@react-navigation/native'
|
|
import equal from 'fast-deep-equal/es6/react'
|
|
import _ from 'lodash'
|
|
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import {
|
|
ActivityIndicator,
|
|
InteractionManager,
|
|
ScrollView,
|
|
StyleSheet,
|
|
TextInput as ReactTextInput,
|
|
View,
|
|
} from 'react-native'
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
|
|
const SongItem = React.memo<{ item: Song }>(({ item }) => {
|
|
const { setQueue, isReady, contextId } = useSetQueue('song', [item])
|
|
|
|
return (
|
|
<ListItem
|
|
item={item}
|
|
contextId={contextId}
|
|
queueId={0}
|
|
showArt={true}
|
|
showStar={false}
|
|
onPress={() => setQueue({ title: item.title, playTrack: 0 })}
|
|
disabled={!isReady}
|
|
/>
|
|
)
|
|
}, equal)
|
|
|
|
const ResultsCategory = withSuspenseMemo<{
|
|
name: string
|
|
query: string
|
|
items: (Artist | Album | Song)[]
|
|
type: 'artist' | 'album' | 'song'
|
|
}>(
|
|
({ name, query, type, items }) => {
|
|
const navigation = useNavigation()
|
|
const { t } = useTranslation('search')
|
|
|
|
if (items.length === 0) {
|
|
return <></>
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Header>{name}</Header>
|
|
{items.map(a =>
|
|
type === 'song' ? (
|
|
<SongItem key={a.id} item={a as Song} />
|
|
) : (
|
|
<ListItem key={a.id} item={a} showArt={true} showStar={false} />
|
|
),
|
|
)}
|
|
{items.length === 5 && (
|
|
<Button
|
|
title={t('moreResults')}
|
|
buttonStyle="hollow"
|
|
style={styles.more}
|
|
onPress={() => navigation.navigate('results', { query, type: items[0].itemType })}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
},
|
|
null,
|
|
equal,
|
|
)
|
|
|
|
const Results = withSuspenseMemo<{
|
|
results: SearchResults
|
|
query: string
|
|
}>(
|
|
({ results, query }) => {
|
|
const { t } = useTranslation('resources')
|
|
|
|
return (
|
|
<>
|
|
<ResultsCategory name={t('artist.name', { count: 2 })} query={query} type={'artist'} items={results.artists} />
|
|
<ResultsCategory name={t('album.name', { count: 2 })} query={query} type={'album'} items={results.albums} />
|
|
<ResultsCategory name={t('song.name', { count: 2 })} query={query} type={'song'} items={results.songs} />
|
|
</>
|
|
)
|
|
},
|
|
null,
|
|
equal,
|
|
)
|
|
|
|
const Search = withSuspense(() => {
|
|
const [query, setQuery] = useState('')
|
|
const { data, isLoading } = useQuerySearchResults({ query, albumCount: 5, artistCount: 5, songCount: 5 })
|
|
const { t } = useTranslation('search')
|
|
|
|
const [text, setText] = useState('')
|
|
const searchBarRef = useRef<ReactTextInput>(null)
|
|
const scrollRef = useRef<ScrollView>(null)
|
|
const paddingTop = useSafeAreaInsets().top
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
const task = InteractionManager.runAfterInteractions(() => {
|
|
setTimeout(() => {
|
|
if (text) {
|
|
return
|
|
}
|
|
setText('')
|
|
setQuery('')
|
|
searchBarRef.current?.focus()
|
|
scrollRef.current?.scrollTo({ y: 0, animated: true })
|
|
}, 50)
|
|
})
|
|
return () => task.cancel()
|
|
}, [text]),
|
|
)
|
|
|
|
const debouncedSetQuery = useMemo(
|
|
() =>
|
|
_.debounce((value: string) => {
|
|
setQuery(value)
|
|
}, 400),
|
|
[],
|
|
)
|
|
|
|
const onChangeText = useCallback(
|
|
(value: string) => {
|
|
setText(value)
|
|
debouncedSetQuery(value)
|
|
},
|
|
[setText, debouncedSetQuery],
|
|
)
|
|
|
|
const resultsCount =
|
|
(data ? data.pages.reduce((acc, val) => (acc += val.artists.length), 0) : 0) +
|
|
(data ? data.pages.reduce((acc, val) => (acc += val.albums.length), 0) : 0) +
|
|
(data ? data.pages.reduce((acc, val) => (acc += val.songs.length), 0) : 0)
|
|
|
|
return (
|
|
<GradientScrollView ref={scrollRef} style={styles.scroll} contentContainerStyle={{ paddingTop }}>
|
|
<View style={styles.content}>
|
|
<View style={styles.inputBar}>
|
|
<TextInput
|
|
ref={searchBarRef}
|
|
style={styles.textInput}
|
|
placeholder={t('inputPlaceholder')}
|
|
value={text}
|
|
onChangeText={onChangeText}
|
|
/>
|
|
<ActivityIndicator animating={isLoading} size="small" color={colors.text.secondary} style={styles.activity} />
|
|
</View>
|
|
{data !== undefined && resultsCount > 0 ? (
|
|
<Results results={data.pages[0]} query={text} />
|
|
) : (
|
|
<NothingHere style={styles.noResults} />
|
|
)}
|
|
</View>
|
|
</GradientScrollView>
|
|
)
|
|
})
|
|
|
|
const styles = StyleSheet.create({
|
|
scroll: {
|
|
flex: 1,
|
|
},
|
|
content: {
|
|
paddingHorizontal: 20,
|
|
paddingBottom: 20,
|
|
alignItems: 'stretch',
|
|
},
|
|
inputBar: {
|
|
justifyContent: 'center',
|
|
},
|
|
activity: {
|
|
position: 'absolute',
|
|
right: 16,
|
|
bottom: 15,
|
|
},
|
|
noResults: {
|
|
width: '100%',
|
|
},
|
|
itemText: {
|
|
color: colors.text.primary,
|
|
fontFamily: font.regular,
|
|
fontSize: 14,
|
|
},
|
|
more: {
|
|
marginTop: 5,
|
|
marginBottom: 10,
|
|
},
|
|
textInput: {
|
|
marginTop: 20,
|
|
paddingHorizontal: 12,
|
|
paddingRight: 46,
|
|
},
|
|
})
|
|
|
|
export default Search
|