Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 35 additions & 21 deletions src/app/(app)/library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,23 @@ import { VideoTile } from '@/components/video-tile';
import type { DownloadMetadata } from '@/lib/download';
import { cancelDownload, retryDownload } from '@/lib/download/download-video';
import { useColumns } from '@/lib/hooks/use-columns';
import { useTranslate } from '@/lib/i18n';
import { type ActiveDownload, useActiveDownloadsStore } from '@/lib/stores/active-downloads-store';
import { baseIdOf, useDownloadedStore } from '@/lib/stores/downloaded-store';
import type { FavoriteItem } from '@/lib/stores/favorites-store';
import { useFavoritesStore } from '@/lib/stores/favorites-store';
import { useHistoryStore } from '@/lib/stores/history-store';

function HistoryTab(): React.ReactElement {
const t = useTranslate();
const { history, clearHistory } = useHistoryStore();
const numColumns = useColumns();
const [refreshing, setRefreshing] = React.useState(false);

if (history.length === 0) {
return (
<View className="flex-1 items-center justify-center py-20">
<Text className="text-neutral-500 dark:text-neutral-400">No watch history</Text>
<Text className="text-neutral-500 dark:text-neutral-400">{t('history.empty')}</Text>
</View>
);
}
Expand All @@ -34,7 +36,7 @@ function HistoryTab(): React.ReactElement {
<>
<View className="flex-row justify-end px-4 py-2">
<TouchableOpacity onPress={clearHistory}>
<Text className="text-primary-500 text-xs">Clear All</Text>
<Text className="text-primary-500 text-xs">{t('history.clear_all')}</Text>
</TouchableOpacity>
</View>
<FlashList
Expand Down Expand Up @@ -71,15 +73,16 @@ function DownloadRow({
item: DownloadMetadata;
onRemove: (videoId: string) => void;
}): React.ReactElement {
const t = useTranslate();
const baseId = baseIdOf(item.videoId);
const isFav = useFavoritesStore((s) => s.favorites.some((f) => f.id === baseId));
const swipeableRef = React.useRef<Swipeable>(null);

const confirmRemove = () => {
swipeableRef.current?.close();
Alert.alert('Delete Download', `Remove "${item.title}" from downloads?`, [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Delete', style: 'destructive', onPress: () => onRemove(item.videoId) },
Alert.alert(t('library.delete_title'), t('library.delete_msg', { title: item.title }), [
{ text: t('common.cancel'), style: 'cancel' },
{ text: t('common.delete'), style: 'destructive', onPress: () => onRemove(item.videoId) },
]);
};

Expand Down Expand Up @@ -120,7 +123,7 @@ function DownloadRow({
</Text>
{item.uploader ? (
<Text className="text-neutral-500 dark:text-neutral-400 text-xs">
By {item.uploader}
{t('library.by_uploader', { uploader: item.uploader })}
</Text>
) : null}
<Text className="text-neutral-500 dark:text-neutral-400 text-xs">
Expand All @@ -134,6 +137,7 @@ function DownloadRow({
}

function ActiveDownloadRow({ task }: { task: ActiveDownload }): React.ReactElement {
const t = useTranslate();
const [cancelling, setCancelling] = React.useState(false);
const [retrying, setRetrying] = React.useState(false);
const indeterminate = task.progress < 0;
Expand Down Expand Up @@ -173,12 +177,12 @@ function ActiveDownloadRow({ task }: { task: ActiveDownload }): React.ReactEleme
</Text>
<Text className="text-neutral-500 dark:text-neutral-400 text-xs">
{task.status === 'error'
? `Failed${task.error ? `: ${task.error}` : ''} — tap Retry to try again`
? `${t('library.status_failed', { error: task.error ? `: ${task.error}` : '' })}${t('library.tap_retry_hint')}`
: task.status === 'cancelled'
? 'Cancelled'
? t('library.status_cancelled')
: indeterminate
? 'Downloading…'
: `Downloading · ${pct}%`}
? t('library.status_downloading_indeterminate')
: t('library.status_downloading', { progress: pct })}
</Text>
<View className="mt-1 h-1.5 overflow-hidden rounded-full bg-neutral-200 dark:bg-neutral-700">
<View
Expand All @@ -194,7 +198,9 @@ function ActiveDownloadRow({ task }: { task: ActiveDownload }): React.ReactEleme
className="items-center justify-center rounded-full bg-primary-500 px-3 py-1.5"
testID="active-download-retry"
>
<Text className="text-xs font-medium text-white">{retrying ? '…' : 'Retry'}</Text>
<Text className="text-xs font-medium text-white">
{retrying ? '…' : t('library.retry')}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={onCancel}
Expand All @@ -203,7 +209,7 @@ function ActiveDownloadRow({ task }: { task: ActiveDownload }): React.ReactEleme
testID="active-download-cancel"
>
<Text className="text-xs font-medium text-neutral-600 dark:text-neutral-300">
{cancelling ? '…' : 'Cancel'}
{cancelling ? '…' : t('common.cancel')}
</Text>
</TouchableOpacity>
</View>
Expand All @@ -215,7 +221,7 @@ function ActiveDownloadRow({ task }: { task: ActiveDownload }): React.ReactEleme
testID="active-download-cancel"
>
<Text className="text-xs font-medium text-neutral-600 dark:text-neutral-300">
{cancelling ? '…' : 'Cancel'}
{cancelling ? '…' : t('common.cancel')}
</Text>
</TouchableOpacity>
)}
Expand All @@ -228,6 +234,7 @@ type DownloadListItem =
| { kind: 'done'; entry: DownloadMetadata };

function DownloadsTab(): React.ReactElement {
const t = useTranslate();
const entries = useDownloadedStore((s) => s.entries);
const loaded = useDownloadedStore((s) => s.loaded);
const hydrate = useDownloadedStore((s) => s.hydrate);
Expand Down Expand Up @@ -265,7 +272,7 @@ function DownloadsTab(): React.ReactElement {
if (loaded && data.length === 0) {
return (
<View className="flex-1 items-center justify-center py-20">
<Text className="text-neutral-500 dark:text-neutral-400">No downloads yet</Text>
<Text className="text-neutral-500 dark:text-neutral-400">{t('library.no_downloads')}</Text>
</View>
);
}
Expand All @@ -290,16 +297,17 @@ function DownloadsTab(): React.ReactElement {
}

function FavoritesTab(): React.ReactElement {
const t = useTranslate();
const { favorites, removeFavorite } = useFavoritesStore();
const numColumns = useColumns();
const [refreshing, setRefreshing] = React.useState(false);

if (favorites.length === 0) {
return (
<View className="flex-1 items-center justify-center py-20">
<Text className="text-neutral-500 dark:text-neutral-400">No favorites yet</Text>
<Text className="text-neutral-500 dark:text-neutral-400">{t('favorites.empty')}</Text>
<Text className="mt-1 text-xs text-neutral-400 dark:text-neutral-500">
Tap the heart on a video to save it here.
{t('favorites.empty_hint')}
</Text>
</View>
);
Expand Down Expand Up @@ -331,13 +339,19 @@ function FavoritesTab(): React.ReactElement {
}

type Tab = 'history' | 'downloads' | 'favorites';
const TABS: { key: Tab; label: string }[] = [
{ key: 'history', label: '🕐 History' },
{ key: 'downloads', label: '⬇️ Downloads' },
{ key: 'favorites', label: '❤️ Favorites' },
// Emojis stay in code (language-neutral); labels come from the translations.
const TABS: {
key: Tab;
emoji: string;
tx: 'history.title' | 'library.downloads' | 'favorites.title';
}[] = [
{ key: 'history', emoji: '🕐', tx: 'history.title' },
{ key: 'downloads', emoji: '⬇️', tx: 'library.downloads' },
{ key: 'favorites', emoji: '❤️', tx: 'favorites.title' },
];

export default function Library(): React.ReactElement {
const t = useTranslate();
const [activeTab, setActiveTab] = React.useState<Tab>('history');
const pagerRef = React.useRef<ScrollView>(null);
const { width } = useWindowDimensions();
Expand Down Expand Up @@ -388,7 +402,7 @@ export default function Library(): React.ReactElement {
<Text
className={`text-sm font-medium ${activeTab === tab.key ? 'text-primary-600 dark:text-primary-400' : 'text-neutral-500 dark:text-neutral-400'}`}
>
{tab.label}
{`${tab.emoji} ${t(tab.tx)}`}
</Text>
</TouchableOpacity>
))}
Expand Down
17 changes: 15 additions & 2 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"common": {
"cancel": "Cancel",
"clear": "Clear",
"delete": "Delete",
"done": "Done",
"error": "Error"
},
Expand Down Expand Up @@ -89,11 +90,23 @@
"searching": "Searching..."
},
"library": {
"by_uploader": "By {{uploader}}",
"delete_msg": "Remove \"{{title}}\" from downloads?",
"delete_title": "Delete Download",
"downloads": "Downloads",
"no_downloads": "No downloads yet",
"retry": "Retry",
"status_cancelled": "Cancelled",
"status_downloading": "Downloading · {{progress}}%",
"status_downloading_indeterminate": "Downloading…",
"status_failed": "Failed{{error}}",
"tap_retry_hint": " — tap Retry to try again",
"title": "Library"
},
"favorites": {
"title": "Favorites",
"empty": "No favorites yet"
"empty": "No favorites yet",
"empty_hint": "Tap the heart on a video to save it here.",
"title": "Favorites"
},
"history": {
"title": "History",
Expand Down
17 changes: 15 additions & 2 deletions src/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"common": {
"cancel": "Cancelar",
"clear": "Borrar",
"delete": "Eliminar",
"done": "Listo",
"error": "Error"
},
Expand Down Expand Up @@ -89,11 +90,23 @@
"searching": "Buscando..."
},
"library": {
"by_uploader": "De {{uploader}}",
"delete_msg": "¿Eliminar \"{{title}}\" de las descargas?",
"delete_title": "Eliminar descarga",
"downloads": "Descargas",
"no_downloads": "Aún no hay descargas",
"retry": "Reintentar",
"status_cancelled": "Cancelada",
"status_downloading": "Descargando · {{progress}}%",
"status_downloading_indeterminate": "Descargando…",
"status_failed": "Error{{error}}",
"tap_retry_hint": " — toca Reintentar para volver a intentarlo",
"title": "Biblioteca"
},
"favorites": {
"title": "Favoritos",
"empty": "Aún no hay favoritos"
"empty": "Aún no hay favoritos",
"empty_hint": "Toca el corazón de un vídeo para guardarlo aquí.",
"title": "Favoritos"
},
"history": {
"title": "Historial",
Expand Down
17 changes: 15 additions & 2 deletions src/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"common": {
"cancel": "キャンセル",
"clear": "クリア",
"delete": "削除",
"done": "完了",
"error": "エラー"
},
Expand Down Expand Up @@ -89,11 +90,23 @@
"searching": "検索中..."
},
"library": {
"by_uploader": "アップローダー:{{uploader}}",
"delete_msg": "「{{title}}」をダウンロードから削除しますか?",
"delete_title": "ダウンロードを削除",
"downloads": "ダウンロード",
"no_downloads": "ダウンロードはまだありません",
"retry": "再試行",
"status_cancelled": "キャンセル済み",
"status_downloading": "ダウンロード中 · {{progress}}%",
"status_downloading_indeterminate": "ダウンロード中…",
"status_failed": "失敗{{error}}",
"tap_retry_hint": " — 「再試行」をタップしてもう一度",
"title": "ライブラリ"
},
"favorites": {
"title": "お気に入り",
"empty": "お気に入りはまだありません"
"empty": "お気に入りはまだありません",
"empty_hint": "動画のハートをタップするとここに保存されます。",
"title": "お気に入り"
},
"history": {
"title": "履歴",
Expand Down
17 changes: 15 additions & 2 deletions src/translations/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"common": {
"cancel": "취소",
"clear": "삭제",
"delete": "삭제",
"done": "완료",
"error": "오류"
},
Expand Down Expand Up @@ -89,11 +90,23 @@
"searching": "검색 중..."
},
"library": {
"by_uploader": "업로더: {{uploader}}",
"delete_msg": "\"{{title}}\"을(를) 다운로드에서 제거할까요?",
"delete_title": "다운로드 삭제",
"downloads": "다운로드",
"no_downloads": "다운로드가 없습니다",
"retry": "다시 시도",
"status_cancelled": "취소됨",
"status_downloading": "다운로드 중 · {{progress}}%",
"status_downloading_indeterminate": "다운로드 중…",
"status_failed": "실패{{error}}",
"tap_retry_hint": " — 다시 시도를 눌러 재시도하세요",
"title": "보관함"
},
"favorites": {
"title": "즐겨찾기",
"empty": "즐겨찾기가 없습니다"
"empty": "즐겨찾기가 없습니다",
"empty_hint": "동영상의 하트를 누르면 여기에 저장됩니다.",
"title": "즐겨찾기"
},
"history": {
"title": "시청 기록",
Expand Down
17 changes: 15 additions & 2 deletions src/translations/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"common": {
"cancel": "Cancelar",
"clear": "Limpar",
"delete": "Excluir",
"done": "Concluído",
"error": "Erro"
},
Expand Down Expand Up @@ -89,11 +90,23 @@
"searching": "Pesquisando..."
},
"library": {
"by_uploader": "Por {{uploader}}",
"delete_msg": "Remover \"{{title}}\" dos downloads?",
"delete_title": "Excluir download",
"downloads": "Downloads",
"no_downloads": "Nenhum download ainda",
"retry": "Repetir",
"status_cancelled": "Cancelado",
"status_downloading": "Baixando · {{progress}}%",
"status_downloading_indeterminate": "Baixando…",
"status_failed": "Falhou{{error}}",
"tap_retry_hint": " — toque em Repetir para tentar de novo",
"title": "Biblioteca"
},
"favorites": {
"title": "Favoritos",
"empty": "Nenhum favorito ainda"
"empty": "Nenhum favorito ainda",
"empty_hint": "Toque no coração de um vídeo para salvá-lo aqui.",
"title": "Favoritos"
},
"history": {
"title": "Histórico",
Expand Down
17 changes: 15 additions & 2 deletions src/translations/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"common": {
"cancel": "取消",
"clear": "清除",
"delete": "刪除",
"done": "完成",
"error": "錯誤"
},
Expand Down Expand Up @@ -89,11 +90,23 @@
"searching": "搜尋中..."
},
"library": {
"by_uploader": "上傳者:{{uploader}}",
"delete_msg": "從下載中移除「{{title}}」?",
"delete_title": "刪除下載",
"downloads": "下載",
"no_downloads": "暫無下載",
"retry": "重試",
"status_cancelled": "已取消",
"status_downloading": "下載中 · {{progress}}%",
"status_downloading_indeterminate": "下載中…",
"status_failed": "失敗{{error}}",
"tap_retry_hint": " — 點擊重試再試一次",
"title": "媒體庫"
},
"favorites": {
"title": "收藏",
"empty": "尚無收藏"
"empty": "尚無收藏",
"empty_hint": "點擊影片上的愛心即可收藏到這裡。",
"title": "收藏"
},
"history": {
"title": "歷史",
Expand Down
Loading