feat: Implement loading more search results
This commit is contained in:
@@ -9,13 +9,13 @@ import * as downloadService from "../services/downloadService";
|
||||
import { getVideoDuration } from "../services/metadataService";
|
||||
import * as storageService from "../services/storageService";
|
||||
import {
|
||||
extractBilibiliVideoId,
|
||||
extractSourceVideoId,
|
||||
extractUrlFromText,
|
||||
isBilibiliUrl,
|
||||
isValidUrl,
|
||||
resolveShortUrl,
|
||||
trimBilibiliUrl,
|
||||
extractBilibiliVideoId,
|
||||
extractSourceVideoId,
|
||||
extractUrlFromText,
|
||||
isBilibiliUrl,
|
||||
isValidUrl,
|
||||
resolveShortUrl,
|
||||
trimBilibiliUrl,
|
||||
} from "../utils/helpers";
|
||||
|
||||
// Configure Multer for file uploads
|
||||
@@ -44,7 +44,14 @@ export const searchVideos = async (
|
||||
return res.status(400).json({ error: "Search query is required" });
|
||||
}
|
||||
|
||||
const results = await downloadService.searchYouTube(query as string);
|
||||
const limit = req.query.limit ? parseInt(req.query.limit as string) : 8;
|
||||
const offset = req.query.offset ? parseInt(req.query.offset as string) : 1;
|
||||
|
||||
const results = await downloadService.searchYouTube(
|
||||
query as string,
|
||||
limit,
|
||||
offset
|
||||
);
|
||||
res.status(200).json({ results });
|
||||
} catch (error: any) {
|
||||
console.error("Error searching for videos:", error);
|
||||
|
||||
@@ -19,7 +19,7 @@ export type {
|
||||
BilibiliVideoInfo,
|
||||
BilibiliVideosResult,
|
||||
CollectionDownloadResult,
|
||||
DownloadResult,
|
||||
DownloadResult
|
||||
};
|
||||
|
||||
// Helper function to download Bilibili video
|
||||
@@ -121,8 +121,12 @@ export async function downloadRemainingBilibiliParts(
|
||||
}
|
||||
|
||||
// Search for videos on YouTube (using yt-dlp)
|
||||
export async function searchYouTube(query: string): Promise<any[]> {
|
||||
return YtDlpDownloader.search(query);
|
||||
export async function searchYouTube(
|
||||
query: string,
|
||||
limit?: number,
|
||||
offset?: number
|
||||
): Promise<any[]> {
|
||||
return YtDlpDownloader.search(query, limit, offset);
|
||||
}
|
||||
|
||||
// Download generic video (using yt-dlp)
|
||||
|
||||
@@ -66,19 +66,31 @@ async function extractXiaoHongShuAuthor(url: string): Promise<string | null> {
|
||||
|
||||
export class YtDlpDownloader {
|
||||
// Search for videos (primarily for YouTube, but could be adapted)
|
||||
static async search(query: string): Promise<any[]> {
|
||||
console.log("Processing search request for query:", query);
|
||||
static async search(
|
||||
query: string,
|
||||
limit: number = 8,
|
||||
offset: number = 1
|
||||
): Promise<any[]> {
|
||||
console.log(
|
||||
`Processing search request for query: "${query}", limit: ${limit}, offset: ${offset}`
|
||||
);
|
||||
|
||||
// Get user config for network options
|
||||
const userConfig = getUserYtDlpConfig();
|
||||
const networkConfig = getNetworkConfigFromUserConfig(userConfig);
|
||||
|
||||
// Calculate the total number of items to fetch from search
|
||||
// We need to request enough items to cover the offset + limit
|
||||
const searchLimit = offset + limit - 1;
|
||||
|
||||
// Use ytsearch for searching
|
||||
const searchResults = await executeYtDlpJson(`ytsearch5:${query}`, {
|
||||
const searchResults = await executeYtDlpJson(`ytsearch${searchLimit}:${query}`, {
|
||||
...networkConfig,
|
||||
noWarnings: true,
|
||||
skipDownload: true,
|
||||
playlistEnd: 5, // Limit to 5 results
|
||||
flatPlaylist: true, // Use flat playlist for faster search results
|
||||
playlistStart: offset,
|
||||
playlistEnd: searchLimit,
|
||||
extractorArgs: `youtubepot-bgutilscript:script_path=${PROVIDER_SCRIPT}`,
|
||||
});
|
||||
|
||||
@@ -91,7 +103,11 @@ export class YtDlpDownloader {
|
||||
id: entry.id,
|
||||
title: entry.title,
|
||||
author: entry.uploader,
|
||||
thumbnailUrl: entry.thumbnail,
|
||||
thumbnailUrl:
|
||||
entry.thumbnail ||
|
||||
(entry.thumbnails && entry.thumbnails.length > 0
|
||||
? entry.thumbnails[0].url
|
||||
: ""),
|
||||
duration: entry.duration,
|
||||
viewCount: entry.view_count,
|
||||
sourceUrl: `https://www.youtube.com/watch?v=${entry.id}`, // Default to YT for search results
|
||||
@@ -99,7 +115,7 @@ export class YtDlpDownloader {
|
||||
}));
|
||||
|
||||
console.log(
|
||||
`Found ${formattedResults.length} search results for "${query}"`
|
||||
`Found ${formattedResults.length} search results for "${query}" (requested ${limit})`
|
||||
);
|
||||
|
||||
return formattedResults;
|
||||
|
||||
@@ -29,6 +29,8 @@ interface VideoContextType {
|
||||
selectedTags: string[];
|
||||
handleTagToggle: (tag: string) => void;
|
||||
showYoutubeSearch: boolean;
|
||||
loadMoreSearchResults: () => Promise<void>;
|
||||
loadingMore: boolean;
|
||||
}
|
||||
|
||||
const VideoContext = createContext<VideoContextType | undefined>(undefined);
|
||||
@@ -86,6 +88,7 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
const [isSearchMode, setIsSearchMode] = useState<boolean>(false);
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
const [youtubeLoading, setYoutubeLoading] = useState<boolean>(false);
|
||||
const [loadingMore, setLoadingMore] = useState<boolean>(false);
|
||||
|
||||
// Reference to the current search request's abort controller
|
||||
const searchAbortController = useRef<AbortController | null>(null);
|
||||
@@ -149,7 +152,10 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
setSearchTerm('');
|
||||
setSearchResults([]);
|
||||
setLocalSearchResults([]);
|
||||
setSearchResults([]);
|
||||
setLocalSearchResults([]);
|
||||
setYoutubeLoading(false);
|
||||
setLoadingMore(false);
|
||||
};
|
||||
|
||||
const handleSearch = async (query: string): Promise<any> => {
|
||||
@@ -217,6 +223,34 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
}
|
||||
};
|
||||
|
||||
const loadMoreSearchResults = async (): Promise<void> => {
|
||||
if (!searchTerm || loadingMore || !showYoutubeSearch) return;
|
||||
|
||||
try {
|
||||
setLoadingMore(true);
|
||||
const currentCount = searchResults.length;
|
||||
const limit = 8;
|
||||
const offset = currentCount + 1;
|
||||
|
||||
const response = await axios.get(`${API_URL}/search`, {
|
||||
params: {
|
||||
query: searchTerm,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.results && response.data.results.length > 0) {
|
||||
setSearchResults(prev => [...prev, ...response.data.results]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading more results:', error);
|
||||
showSnackbar(t('failedToSearch'));
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTagToggle = (tag: string) => {
|
||||
setSelectedTags(prev =>
|
||||
prev.includes(tag)
|
||||
@@ -323,7 +357,9 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
availableTags,
|
||||
selectedTags,
|
||||
handleTagToggle,
|
||||
showYoutubeSearch
|
||||
showYoutubeSearch,
|
||||
loadMoreSearchResults,
|
||||
loadingMore
|
||||
}}>
|
||||
{children}
|
||||
</VideoContext.Provider>
|
||||
|
||||
@@ -30,7 +30,9 @@ const SearchPage: React.FC = () => {
|
||||
youtubeLoading,
|
||||
handleSearch,
|
||||
searchTerm: contextSearchTerm,
|
||||
showYoutubeSearch
|
||||
showYoutubeSearch,
|
||||
loadMoreSearchResults,
|
||||
loadingMore
|
||||
} = useVideo();
|
||||
const { collections } = useCollection();
|
||||
const { handleVideoSubmit } = useDownload();
|
||||
@@ -112,61 +114,73 @@ const SearchPage: React.FC = () => {
|
||||
<Typography sx={{ mt: 2 }}>{t('loadingYouTubeResults')}</Typography>
|
||||
</Box>
|
||||
) : hasYouTubeResults ? (
|
||||
<Grid container spacing={3}>
|
||||
{searchResults.map((result) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={result.id}>
|
||||
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ position: 'relative', paddingTop: '56.25%' }}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={result.thumbnailUrl || 'https://via.placeholder.com/480x360?text=No+Thumbnail'}
|
||||
alt={result.title}
|
||||
sx={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.onerror = null;
|
||||
target.src = 'https://via.placeholder.com/480x360?text=No+Thumbnail';
|
||||
}}
|
||||
/>
|
||||
{result.duration && (
|
||||
<Chip
|
||||
label={formatDuration(result.duration)}
|
||||
size="small"
|
||||
sx={{ position: 'absolute', bottom: 8, right: 8, bgcolor: 'rgba(0,0,0,0.8)', color: 'white' }}
|
||||
<>
|
||||
<Grid container spacing={3}>
|
||||
{searchResults.map((result) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={result.id}>
|
||||
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ position: 'relative', paddingTop: '56.25%' }}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={result.thumbnailUrl || 'https://via.placeholder.com/480x360?text=No+Thumbnail'}
|
||||
alt={result.title}
|
||||
sx={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.onerror = null;
|
||||
target.src = 'https://via.placeholder.com/480x360?text=No+Thumbnail';
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box sx={{ position: 'absolute', top: 8, right: 8, bgcolor: 'rgba(0,0,0,0.7)', borderRadius: '50%', p: 0.5, display: 'flex' }}>
|
||||
{result.source === 'bilibili' ? <OndemandVideo sx={{ color: '#23ade5' }} /> : <YouTube sx={{ color: '#ff0000' }} />}
|
||||
{result.duration && (
|
||||
<Chip
|
||||
label={formatDuration(result.duration)}
|
||||
size="small"
|
||||
sx={{ position: 'absolute', bottom: 8, right: 8, bgcolor: 'rgba(0,0,0,0.8)', color: 'white' }}
|
||||
/>
|
||||
)}
|
||||
<Box sx={{ position: 'absolute', top: 8, right: 8, bgcolor: 'rgba(0,0,0,0.7)', borderRadius: '50%', p: 0.5, display: 'flex' }}>
|
||||
{result.source === 'bilibili' ? <OndemandVideo sx={{ color: '#23ade5' }} /> : <YouTube sx={{ color: '#ff0000' }} />}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<CardContent sx={{ flexGrow: 1, p: 2 }}>
|
||||
<Typography gutterBottom variant="subtitle1" component="div" sx={{ fontWeight: 600, lineHeight: 1.2, mb: 1, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
||||
{result.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{result.author}
|
||||
</Typography>
|
||||
{result.viewCount && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{formatViewCount(result.viewCount)} {t('views')}
|
||||
<CardContent sx={{ flexGrow: 1, p: 2 }}>
|
||||
<Typography gutterBottom variant="subtitle1" component="div" sx={{ fontWeight: 600, lineHeight: 1.2, mb: 1, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
||||
{result.title}
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardActions sx={{ p: 2, pt: 0 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
startIcon={<Download />}
|
||||
onClick={() => handleDownload(result.id, result.sourceUrl)}
|
||||
disabled={downloadingId === result.id}
|
||||
>
|
||||
{t('download')}
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{result.author}
|
||||
</Typography>
|
||||
{result.viewCount && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{formatViewCount(result.viewCount)} {t('views')}
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardActions sx={{ p: 2, pt: 0 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
startIcon={<Download />}
|
||||
onClick={() => handleDownload(result.id, result.sourceUrl)}
|
||||
disabled={downloadingId === result.id}
|
||||
>
|
||||
{t('download')}
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={loadMoreSearchResults}
|
||||
disabled={loadingMore}
|
||||
startIcon={loadingMore ? <CircularProgress size={20} color="inherit" /> : null}
|
||||
>
|
||||
{loadingMore ? t('loading') : t('more')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Typography color="text.secondary">{t('noYouTubeResults')}</Typography>
|
||||
)}
|
||||
|
||||
@@ -17,10 +17,12 @@ import React, { useEffect, useState } from 'react';
|
||||
import VideoCard from '../components/VideoCard';
|
||||
import { useCollection } from '../contexts/CollectionContext';
|
||||
import { useDownload } from '../contexts/DownloadContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { useVideo } from '../contexts/VideoContext';
|
||||
import { formatDuration } from '../utils/formatUtils';
|
||||
|
||||
const SearchResults: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const {
|
||||
searchResults,
|
||||
localSearchResults,
|
||||
@@ -30,7 +32,10 @@ const SearchResults: React.FC = () => {
|
||||
deleteVideo,
|
||||
resetSearch,
|
||||
setIsSearchMode,
|
||||
showYoutubeSearch
|
||||
|
||||
showYoutubeSearch,
|
||||
loadMoreSearchResults,
|
||||
loadingMore
|
||||
} = useVideo();
|
||||
const { collections } = useCollection();
|
||||
const { handleVideoSubmit } = useDownload();
|
||||
@@ -140,60 +145,72 @@ const SearchResults: React.FC = () => {
|
||||
<Typography sx={{ mt: 2 }}>Loading YouTube results...</Typography>
|
||||
</Box>
|
||||
) : hasYouTubeResults ? (
|
||||
<Grid container spacing={3}>
|
||||
{searchResults.map((result) => <Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={result.id}>
|
||||
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ position: 'relative', paddingTop: '56.25%' }}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={result.thumbnailUrl || 'https://via.placeholder.com/480x360?text=No+Thumbnail'}
|
||||
alt={result.title}
|
||||
sx={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.onerror = null;
|
||||
target.src = 'https://via.placeholder.com/480x360?text=No+Thumbnail';
|
||||
}}
|
||||
/>
|
||||
{result.duration && (
|
||||
<Chip
|
||||
label={formatDuration(result.duration)}
|
||||
size="small"
|
||||
sx={{ position: 'absolute', bottom: 8, right: 8, bgcolor: 'rgba(0,0,0,0.8)', color: 'white' }}
|
||||
<>
|
||||
<Grid container spacing={3}>
|
||||
{searchResults.map((result) => <Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={result.id}>
|
||||
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ position: 'relative', paddingTop: '56.25%' }}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={result.thumbnailUrl || 'https://via.placeholder.com/480x360?text=No+Thumbnail'}
|
||||
alt={result.title}
|
||||
sx={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.onerror = null;
|
||||
target.src = 'https://via.placeholder.com/480x360?text=No+Thumbnail';
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box sx={{ position: 'absolute', top: 8, right: 8, bgcolor: 'rgba(0,0,0,0.7)', borderRadius: '50%', p: 0.5, display: 'flex' }}>
|
||||
{result.source === 'bilibili' ? <OndemandVideo sx={{ color: '#23ade5' }} /> : <YouTube sx={{ color: '#ff0000' }} />}
|
||||
{result.duration && (
|
||||
<Chip
|
||||
label={formatDuration(result.duration)}
|
||||
size="small"
|
||||
sx={{ position: 'absolute', bottom: 8, right: 8, bgcolor: 'rgba(0,0,0,0.8)', color: 'white' }}
|
||||
/>
|
||||
)}
|
||||
<Box sx={{ position: 'absolute', top: 8, right: 8, bgcolor: 'rgba(0,0,0,0.7)', borderRadius: '50%', p: 0.5, display: 'flex' }}>
|
||||
{result.source === 'bilibili' ? <OndemandVideo sx={{ color: '#23ade5' }} /> : <YouTube sx={{ color: '#ff0000' }} />}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<CardContent sx={{ flexGrow: 1, p: 2 }}>
|
||||
<Typography gutterBottom variant="subtitle1" component="div" sx={{ fontWeight: 600, lineHeight: 1.2, mb: 1, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
||||
{result.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{result.author}
|
||||
</Typography>
|
||||
{result.viewCount && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{formatViewCount(result.viewCount)} views
|
||||
<CardContent sx={{ flexGrow: 1, p: 2 }}>
|
||||
<Typography gutterBottom variant="subtitle1" component="div" sx={{ fontWeight: 600, lineHeight: 1.2, mb: 1, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
||||
{result.title}
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardActions sx={{ p: 2, pt: 0 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
startIcon={<Download />}
|
||||
onClick={() => handleDownload(result.id, result.sourceUrl)}
|
||||
disabled={downloadingId === result.id}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{result.author}
|
||||
</Typography>
|
||||
{result.viewCount && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{formatViewCount(result.viewCount)} views
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardActions sx={{ p: 2, pt: 0 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
startIcon={<Download />}
|
||||
onClick={() => handleDownload(result.id, result.sourceUrl)}
|
||||
disabled={downloadingId === result.id}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={loadMoreSearchResults}
|
||||
disabled={loadingMore}
|
||||
startIcon={loadingMore ? <CircularProgress size={20} color="inherit" /> : null}
|
||||
>
|
||||
{loadingMore ? t('loading') : t('more')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Typography color="text.secondary">No YouTube results found.</Typography>
|
||||
)}
|
||||
|
||||
@@ -444,4 +444,5 @@ export const ar = {
|
||||
customize: "تخصيص",
|
||||
hide: "إخفاء",
|
||||
reset: "إعادة تعيين",
|
||||
more: "المزيد",
|
||||
};
|
||||
|
||||
@@ -425,4 +425,5 @@ export const de = {
|
||||
customize: "Anpassen",
|
||||
hide: "Ausblenden",
|
||||
reset: "Zurücksetzen",
|
||||
more: "Mehr",
|
||||
};
|
||||
|
||||
@@ -453,4 +453,5 @@ export const en = {
|
||||
customize: "Customize",
|
||||
hide: "Hide",
|
||||
reset: "Reset",
|
||||
more: "More",
|
||||
};
|
||||
|
||||
@@ -431,4 +431,5 @@ export const es = {
|
||||
customize: "Personalizar",
|
||||
hide: "Ocultar",
|
||||
reset: "Restablecer",
|
||||
more: "Más",
|
||||
};
|
||||
|
||||
@@ -463,4 +463,5 @@ export const fr = {
|
||||
customize: "Personnaliser",
|
||||
hide: "Masquer",
|
||||
reset: "Réinitialiser",
|
||||
more: "Plus",
|
||||
};
|
||||
|
||||
@@ -452,4 +452,5 @@ export const ja = {
|
||||
customize: "カスタマイズ",
|
||||
hide: "隠す",
|
||||
reset: "リセット",
|
||||
more: "もっと見る",
|
||||
};
|
||||
|
||||
@@ -446,4 +446,5 @@ export const ko = {
|
||||
customize: "사용자 지정",
|
||||
hide: "숨기기",
|
||||
reset: "초기화",
|
||||
more: "더 보기",
|
||||
};
|
||||
|
||||
@@ -457,4 +457,5 @@ export const pt = {
|
||||
customize: "Personalizar",
|
||||
hide: "Ocultar",
|
||||
reset: "Redefinir",
|
||||
more: "Mais",
|
||||
};
|
||||
|
||||
@@ -451,4 +451,5 @@ export const ru = {
|
||||
customize: "Настроить",
|
||||
hide: "Скрыть",
|
||||
reset: "Сбросить",
|
||||
more: "Ещё",
|
||||
};
|
||||
|
||||
@@ -442,4 +442,5 @@ export const zh = {
|
||||
customize: "自定义",
|
||||
hide: "隐藏",
|
||||
reset: "重置",
|
||||
more: "更多",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user