mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-10 21:36:17 +00:00
chore: bump version to 3.8.0 and update translations for downloader status and notifications
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "3.7.6",
|
||||
"version": "3.8.0",
|
||||
"description": "Hydra",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Los Broxas",
|
||||
|
||||
@@ -185,6 +185,12 @@
|
||||
"open_screenshot": "Open screenshot {{number}}",
|
||||
"download_settings": "Download settings",
|
||||
"downloader": "Downloader",
|
||||
"downloader_online": "Online",
|
||||
"downloader_not_configured": "Available but not configured",
|
||||
"downloader_offline": "Link is offline",
|
||||
"downloader_not_available": "Not available",
|
||||
"recommended": "Recommended",
|
||||
"go_to_settings": "Go to Settings",
|
||||
"select_executable": "Select",
|
||||
"no_executable_selected": "No executable selected",
|
||||
"open_folder": "Open folder",
|
||||
|
||||
@@ -182,6 +182,12 @@
|
||||
"open_screenshot": "Abrir captura número {{number}}",
|
||||
"download_settings": "Descargar ajustes",
|
||||
"downloader": "Descargador",
|
||||
"downloader_online": "En línea",
|
||||
"downloader_not_configured": "Disponible pero no configurado",
|
||||
"downloader_offline": "El enlace está fuera de línea",
|
||||
"downloader_not_available": "No disponible",
|
||||
"recommended": "Recomendado",
|
||||
"go_to_settings": "Ir a Ajustes",
|
||||
"select_executable": "Seleccionar",
|
||||
"no_executable_selected": "Sin ejecutable seleccionado",
|
||||
"open_folder": "Abrir carpeta",
|
||||
@@ -651,6 +657,7 @@
|
||||
"sending": "Enviando",
|
||||
"friend_request_sent": "Solicitud de amistad enviada",
|
||||
"friends": "Amistades",
|
||||
"badges": "Insignias",
|
||||
"friends_list": "Lista de amistades",
|
||||
"user_not_found": "Usuario no encontrado",
|
||||
"block_user": "Bloquear usuario",
|
||||
@@ -661,12 +668,16 @@
|
||||
"ignore_request": "Ignorar solicitud",
|
||||
"cancel_request": "Cancelar solicitud",
|
||||
"undo_friendship": "Deshacer amistad",
|
||||
"friendship_removed": "Amigo eliminado",
|
||||
"request_accepted": "Solicitud aceptada",
|
||||
"user_blocked_successfully": "Usuario bloqueado exitosamente",
|
||||
"user_block_modal_text": "Esto va a bloquear a {{displayName}}",
|
||||
"blocked_users": "Usuarios bloqueados",
|
||||
"unblock": "Desbloquear",
|
||||
"no_friends_added": "No tenés amistades añadidas",
|
||||
"view_all": "Ver todo",
|
||||
"load_more": "Cargar más",
|
||||
"loading": "Cargando",
|
||||
"pending": "Pendiente",
|
||||
"no_pending_invites": "No tenés invitaciones pendientes",
|
||||
"no_blocked_users": "No has bloqueado a nadie",
|
||||
@@ -690,6 +701,7 @@
|
||||
"report_reason_other": "Otros",
|
||||
"profile_reported": "Perfil reportado",
|
||||
"your_friend_code": "Tu código de amistad:",
|
||||
"copy_friend_code": "Copiar código de amistad",
|
||||
"upload_banner": "Subir banner",
|
||||
"uploading_banner": "Subiendo banner…",
|
||||
"background_image_updated": "Imagen de fondo actualizada",
|
||||
@@ -715,6 +727,9 @@
|
||||
"game_added_to_pinned": "Juego añadido a fijados",
|
||||
"user_reviews": "Reseñas",
|
||||
"loading_reviews": "Cargando reseñas...",
|
||||
"wrapped_2025": "Wrapped 2025",
|
||||
"view_my_wrapped_button": "Ver Mi Wrapped 2025",
|
||||
"view_wrapped_button": "Ver Wrapped 2025 de {{displayName}}",
|
||||
"no_reviews": "Sin reseñas aún",
|
||||
"delete_review": "Eliminar reseña"
|
||||
},
|
||||
@@ -767,5 +782,41 @@
|
||||
"all_games": "Todos los Juegos",
|
||||
"recently_played": "Jugados Recientemente",
|
||||
"favorites": "Favoritos"
|
||||
},
|
||||
"notifications_page": {
|
||||
"title": "Notificaciones",
|
||||
"mark_all_as_read": "Marcar todo como leído",
|
||||
"clear_all": "Limpiar todo",
|
||||
"loading": "Cargando...",
|
||||
"empty_title": "Sin notificaciones",
|
||||
"empty_description": "¡Estás al día! Volvé más tarde para ver nuevas actualizaciones.",
|
||||
"empty_filter_description": "No hay notificaciones que coincidan con este filtro.",
|
||||
"filter_all": "Todas",
|
||||
"filter_unread": "No leídas",
|
||||
"filter_friends": "Amigos",
|
||||
"filter_badges": "Insignias",
|
||||
"filter_upvotes": "Votos",
|
||||
"filter_local": "Locales",
|
||||
"load_more": "Cargar más",
|
||||
"dismiss": "Descartar",
|
||||
"accept": "Aceptar",
|
||||
"refuse": "Rechazar",
|
||||
"notification": "Notificación",
|
||||
"friend_request_received_title": "¡Nueva solicitud de amistad!",
|
||||
"friend_request_received_description": "{{displayName}} quiere ser tu amigo",
|
||||
"friend_request_accepted_title": "¡Solicitud de amistad aceptada!",
|
||||
"friend_request_accepted_description": "{{displayName}} aceptó tu solicitud de amistad",
|
||||
"badge_received_title": "¡Obtuviste una nueva insignia!",
|
||||
"badge_received_description": "{{badgeName}}",
|
||||
"review_upvote_title": "¡Tu reseña de {{gameTitle}} recibió votos!",
|
||||
"review_upvote_description": "Tu reseña recibió {{count}} nuevos votos",
|
||||
"marked_all_as_read": "Todas las notificaciones marcadas como leídas",
|
||||
"failed_to_mark_as_read": "Error al marcar las notificaciones como leídas",
|
||||
"cleared_all": "Todas las notificaciones eliminadas",
|
||||
"failed_to_clear": "Error al eliminar las notificaciones",
|
||||
"failed_to_load": "Error al cargar las notificaciones",
|
||||
"failed_to_dismiss": "Error al descartar la notificación",
|
||||
"friend_request_accepted": "Solicitud de amistad aceptada",
|
||||
"friend_request_refused": "Solicitud de amistad rechazada"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +172,12 @@
|
||||
"open_screenshot": "Ver captura de tela {{number}}",
|
||||
"download_settings": "Ajustes do download",
|
||||
"downloader": "Downloader",
|
||||
"downloader_online": "Online",
|
||||
"downloader_not_configured": "Disponível mas não configurado",
|
||||
"downloader_offline": "Link está offline",
|
||||
"downloader_not_available": "Não disponível",
|
||||
"recommended": "Recomendado",
|
||||
"go_to_settings": "Ir para Configurações",
|
||||
"select_executable": "Explorar",
|
||||
"no_executable_selected": "Nenhum executável selecionado",
|
||||
"open_folder": "Abrir pasta",
|
||||
@@ -654,6 +660,7 @@
|
||||
"see_profile": "Ver perfil",
|
||||
"friend_request_sent": "Pedido de amizade enviado",
|
||||
"friends": "Amigos",
|
||||
"badges": "Insígnias",
|
||||
"add": "Adicionar",
|
||||
"sending": "Enviando",
|
||||
"friends_list": "Lista de amigos",
|
||||
@@ -666,12 +673,16 @@
|
||||
"ignore_request": "Ignorar pedido",
|
||||
"cancel_request": "Cancelar pedido",
|
||||
"undo_friendship": "Desfazer amizade",
|
||||
"friendship_removed": "Amigo removido",
|
||||
"request_accepted": "Pedido de amizade aceito",
|
||||
"user_blocked_successfully": "Usuário bloqueado com sucesso",
|
||||
"user_block_modal_text": "Bloquear {{displayName}}",
|
||||
"blocked_users": "Usuários bloqueados",
|
||||
"unblock": "Desbloquear",
|
||||
"no_friends_added": "Você ainda não possui amigos adicionados",
|
||||
"view_all": "Ver todos",
|
||||
"load_more": "Carregar mais",
|
||||
"loading": "Carregando",
|
||||
"pending": "Pendentes",
|
||||
"no_pending_invites": "Você não possui convites de amizade pendentes",
|
||||
"no_blocked_users": "Você não tem nenhum usuário bloqueado",
|
||||
@@ -695,6 +706,7 @@
|
||||
"report_reason_other": "Outro",
|
||||
"profile_reported": "Perfil reportado",
|
||||
"your_friend_code": "Seu código de amigo:",
|
||||
"copy_friend_code": "Copiar código de amigo",
|
||||
"upload_banner": "Carregar banner",
|
||||
"uploading_banner": "Carregando banner…",
|
||||
"background_image_updated": "Imagem de fundo salva",
|
||||
@@ -724,6 +736,9 @@
|
||||
"manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente",
|
||||
"user_reviews": "Avaliações",
|
||||
"loading_reviews": "Carregando avaliações...",
|
||||
"wrapped_2025": "Wrapped 2025",
|
||||
"view_my_wrapped_button": "Ver Meu Wrapped 2025",
|
||||
"view_wrapped_button": "Ver Wrapped 2025 de {{displayName}}",
|
||||
"no_reviews": "Ainda não há avaliações",
|
||||
"delete_review": "Excluir avaliação"
|
||||
},
|
||||
@@ -776,5 +791,41 @@
|
||||
"all_games": "Todos os Jogos",
|
||||
"recently_played": "Jogados Recentemente",
|
||||
"favorites": "Favoritos"
|
||||
},
|
||||
"notifications_page": {
|
||||
"title": "Notificações",
|
||||
"mark_all_as_read": "Marcar todas como lidas",
|
||||
"clear_all": "Limpar todas",
|
||||
"loading": "Carregando...",
|
||||
"empty_title": "Sem notificações",
|
||||
"empty_description": "Você está em dia! Volte mais tarde para ver novas atualizações.",
|
||||
"empty_filter_description": "Nenhuma notificação corresponde a este filtro.",
|
||||
"filter_all": "Todas",
|
||||
"filter_unread": "Não lidas",
|
||||
"filter_friends": "Amigos",
|
||||
"filter_badges": "Insígnias",
|
||||
"filter_upvotes": "Votos",
|
||||
"filter_local": "Locais",
|
||||
"load_more": "Carregar mais",
|
||||
"dismiss": "Descartar",
|
||||
"accept": "Aceitar",
|
||||
"refuse": "Recusar",
|
||||
"notification": "Notificação",
|
||||
"friend_request_received_title": "Nova solicitação de amizade!",
|
||||
"friend_request_received_description": "{{displayName}} quer ser seu amigo",
|
||||
"friend_request_accepted_title": "Solicitação de amizade aceita!",
|
||||
"friend_request_accepted_description": "{{displayName}} aceitou sua solicitação de amizade",
|
||||
"badge_received_title": "Você recebeu uma nova insígnia!",
|
||||
"badge_received_description": "{{badgeName}}",
|
||||
"review_upvote_title": "Sua avaliação de {{gameTitle}} recebeu votos!",
|
||||
"review_upvote_description": "Sua avaliação recebeu {{count}} novos votos",
|
||||
"marked_all_as_read": "Todas as notificações marcadas como lidas",
|
||||
"failed_to_mark_as_read": "Falha ao marcar notificações como lidas",
|
||||
"cleared_all": "Todas as notificações limpas",
|
||||
"failed_to_clear": "Falha ao limpar notificações",
|
||||
"failed_to_load": "Falha ao carregar notificações",
|
||||
"failed_to_dismiss": "Falha ao descartar notificação",
|
||||
"friend_request_accepted": "Solicitação de amizade aceita",
|
||||
"friend_request_refused": "Solicitação de amizade recusada"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,12 @@
|
||||
"open_screenshot": "Открыть скриншот {{number}}",
|
||||
"download_settings": "Параметры загрузки",
|
||||
"downloader": "Загрузчик",
|
||||
"downloader_online": "Онлайн",
|
||||
"downloader_not_configured": "Доступен, но не настроен",
|
||||
"downloader_offline": "Ссылка недоступна",
|
||||
"downloader_not_available": "Недоступно",
|
||||
"recommended": "Рекомендуется",
|
||||
"go_to_settings": "Перейти в настройки",
|
||||
"select_executable": "Выбрать",
|
||||
"no_executable_selected": "Файл не выбран",
|
||||
"open_folder": "Открыть папку",
|
||||
@@ -651,6 +657,7 @@
|
||||
"sending": "Отправка",
|
||||
"friend_request_sent": "Запрос в друзья отправлен",
|
||||
"friends": "Друзья",
|
||||
"badges": "Значки",
|
||||
"friends_list": "Список друзей",
|
||||
"user_not_found": "Пользователь не найден",
|
||||
"block_user": "Заблокировать пользователя",
|
||||
@@ -661,12 +668,16 @@
|
||||
"ignore_request": "Игнорировать запрос",
|
||||
"cancel_request": "Отменить запрос",
|
||||
"undo_friendship": "Удалить друга",
|
||||
"friendship_removed": "Друг удален",
|
||||
"request_accepted": "Запрос принят",
|
||||
"user_blocked_successfully": "Пользователь успешно заблокирован",
|
||||
"user_block_modal_text": "{{displayName}} будет заблокирован",
|
||||
"blocked_users": "Заблокированные пользователи",
|
||||
"unblock": "Разблокировать",
|
||||
"no_friends_added": "Вы ещё не добавили ни одного друга",
|
||||
"view_all": "Показать все",
|
||||
"load_more": "Загрузить еще",
|
||||
"loading": "Загрузка",
|
||||
"pending": "Ожидание",
|
||||
"no_pending_invites": "У вас нет запросов ожидающих ответа",
|
||||
"no_blocked_users": "Вы не заблокировали ни одного пользователя",
|
||||
@@ -690,6 +701,7 @@
|
||||
"report_reason_other": "Другое",
|
||||
"profile_reported": "Жалоба на профиль отправлена",
|
||||
"your_friend_code": "Код вашего друга:",
|
||||
"copy_friend_code": "Копировать код друга",
|
||||
"upload_banner": "Загрузить баннер",
|
||||
"uploading_banner": "Загрузка баннера...",
|
||||
"background_image_updated": "Фоновое изображение обновлено",
|
||||
@@ -712,6 +724,9 @@
|
||||
"karma_description": "Заработана положительными оценками отзывов",
|
||||
"user_reviews": "Отзывы",
|
||||
"loading_reviews": "Загрузка отзывов...",
|
||||
"wrapped_2025": "Wrapped 2025",
|
||||
"view_my_wrapped_button": "Просмотреть мой Wrapped 2025",
|
||||
"view_wrapped_button": "Просмотреть Wrapped 2025 {{displayName}}",
|
||||
"no_reviews": "Пока нет отзывов",
|
||||
"delete_review": "Удалить отзыв"
|
||||
},
|
||||
@@ -764,5 +779,41 @@
|
||||
"all_games": "Все игры",
|
||||
"recently_played": "Недавно сыгранные",
|
||||
"favorites": "Избранное"
|
||||
},
|
||||
"notifications_page": {
|
||||
"title": "Уведомления",
|
||||
"mark_all_as_read": "Отметить все как прочитанные",
|
||||
"clear_all": "Очистить все",
|
||||
"loading": "Загрузка...",
|
||||
"empty_title": "Нет уведомлений",
|
||||
"empty_description": "Вы в курсе всех событий! Загляните позже за новыми обновлениями.",
|
||||
"empty_filter_description": "Нет уведомлений, соответствующих этому фильтру.",
|
||||
"filter_all": "Все",
|
||||
"filter_unread": "Непрочитанные",
|
||||
"filter_friends": "Друзья",
|
||||
"filter_badges": "Значки",
|
||||
"filter_upvotes": "Голоса",
|
||||
"filter_local": "Локальные",
|
||||
"load_more": "Загрузить еще",
|
||||
"dismiss": "Отклонить",
|
||||
"accept": "Принять",
|
||||
"refuse": "Отклонить",
|
||||
"notification": "Уведомление",
|
||||
"friend_request_received_title": "Новый запрос в друзья!",
|
||||
"friend_request_received_description": "{{displayName}} хочет добавить вас в друзья",
|
||||
"friend_request_accepted_title": "Запрос в друзья принят!",
|
||||
"friend_request_accepted_description": "{{displayName}} принял ваш запрос в друзья",
|
||||
"badge_received_title": "Вы получили новый значок!",
|
||||
"badge_received_description": "{{badgeName}}",
|
||||
"review_upvote_title": "Ваш отзыв на {{gameTitle}} получил голоса!",
|
||||
"review_upvote_description": "Ваш отзыв получил {{count}} новых голосов",
|
||||
"marked_all_as_read": "Все уведомления отмечены как прочитанные",
|
||||
"failed_to_mark_as_read": "Не удалось отметить уведомления как прочитанные",
|
||||
"cleared_all": "Все уведомления очищены",
|
||||
"failed_to_clear": "Не удалось очистить уведомления",
|
||||
"failed_to_load": "Не удалось загрузить уведомления",
|
||||
"failed_to_dismiss": "Не удалось отклонить уведомление",
|
||||
"friend_request_accepted": "Запрос в друзья принят",
|
||||
"friend_request_refused": "Запрос в друзья отклонен"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ export class Aria2 {
|
||||
"--rpc-listen-all",
|
||||
"--file-allocation=none",
|
||||
"--allow-overwrite=true",
|
||||
"--disable-ipv6",
|
||||
],
|
||||
{ stdio: "inherit", windowsHide: true }
|
||||
);
|
||||
|
||||
@@ -8,13 +8,6 @@ interface UnlockResponse {
|
||||
}
|
||||
|
||||
export class VikingFileApi {
|
||||
private static readonly browserHeaders = {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
Referer: "https://vikingfile.com/",
|
||||
};
|
||||
|
||||
public static async getDownloadUrl(uri: string): Promise<string> {
|
||||
const unlockResponse = await axios.post<UnlockResponse>(
|
||||
`${import.meta.env.MAIN_VITE_NIMBUS_API_URL}/hosters/unlock`,
|
||||
@@ -27,16 +20,11 @@ export class VikingFileApi {
|
||||
|
||||
const redirectUrl = unlockResponse.data.link;
|
||||
|
||||
// Follow the redirect to get the final Cloudflare storage URL
|
||||
try {
|
||||
const redirectResponse = await axios.head(redirectUrl, {
|
||||
headers: this.browserHeaders,
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) =>
|
||||
status === 301 || status === 302 || status === 200,
|
||||
httpsAgent: new https.Agent({
|
||||
family: 4, // Force IPv4
|
||||
}),
|
||||
});
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Downloader } from "@shared";
|
||||
|
||||
export const VERSION_CODENAME = "Supernova";
|
||||
export const VERSION_CODENAME = "Harbinger";
|
||||
|
||||
export const DOWNLOADER_NAME = {
|
||||
[Downloader.RealDebrid]: "Real-Debrid",
|
||||
|
||||
@@ -19,25 +19,52 @@
|
||||
color: globals.$body-color;
|
||||
}
|
||||
|
||||
&__downloaders-list-wrapper {
|
||||
border: 1px solid globals.$border-color;
|
||||
overflow: hidden;
|
||||
background-color: globals.$dark-background-color;
|
||||
}
|
||||
|
||||
&__downloaders-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
gap: 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid globals.$border-color;
|
||||
border-radius: 4px;
|
||||
padding: calc(globals.$spacing-unit / 2);
|
||||
background-color: globals.$dark-background-color;
|
||||
overflow-x: hidden;
|
||||
padding: 0;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&__recommendation-badge {
|
||||
margin-left: calc(globals.$spacing-unit);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&__downloader-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit * 1.5);
|
||||
gap: 8px;
|
||||
padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
border-bottom: 1px solid globals.$border-color;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
@@ -46,8 +73,10 @@
|
||||
color: globals.$body-color;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
height: 48px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover:not(&--disabled) {
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
@@ -55,19 +84,63 @@
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
&--last {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__downloader-name {
|
||||
flex: 1;
|
||||
&__downloader-item-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__recommendation-badge {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__check-icon {
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__check-icon-wrapper {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__recommendation-badge {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.badge {
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__availability-indicator-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__availability-indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -80,6 +153,32 @@
|
||||
background-color: #ef4444;
|
||||
box-shadow: 0 0 6px rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
&--not-present {
|
||||
background-color: #6b7280;
|
||||
box-shadow: 0 0 6px rgba(107, 114, 128, 0.5);
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background-color: #eab308;
|
||||
box-shadow: 0 0 6px rgba(234, 179, 8, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&__availability-indicator--pulsating {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&__path-error {
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
CheckboxField,
|
||||
Link,
|
||||
Modal,
|
||||
TextField,
|
||||
} from "@renderer/components";
|
||||
import { DownloadIcon, SyncIcon } from "@primer/octicons-react";
|
||||
import {
|
||||
Downloader,
|
||||
formatBytes,
|
||||
getDownloadersForUri,
|
||||
getDownloadersForUris,
|
||||
} from "@shared";
|
||||
DownloadIcon,
|
||||
SyncIcon,
|
||||
CheckCircleFillIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { Downloader, formatBytes, getDownloadersForUri } from "@shared";
|
||||
import type { GameRepack } from "@types";
|
||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||
import { useAppSelector, useFeature, useToast } from "@renderer/hooks";
|
||||
import { motion } from "framer-motion";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import { RealDebridInfoModal } from "./real-debrid-info-modal";
|
||||
import "./download-settings-modal.scss";
|
||||
|
||||
export interface DownloadSettingsModalProps {
|
||||
@@ -56,6 +59,7 @@ export function DownloadSettingsModal({
|
||||
const [hasWritePermission, setHasWritePermission] = useState<boolean | null>(
|
||||
null
|
||||
);
|
||||
const [showRealDebridModal, setShowRealDebridModal] = useState(false);
|
||||
|
||||
const { isFeatureEnabled, Feature } = useFeature();
|
||||
|
||||
@@ -83,51 +87,115 @@ export function DownloadSettingsModal({
|
||||
}
|
||||
}, [visible, checkFolderWritePermission, selectedPath]);
|
||||
|
||||
const downloaders = useMemo(() => {
|
||||
return getDownloadersForUris(repack?.uris ?? []);
|
||||
}, [repack?.uris]);
|
||||
|
||||
const downloadOptions = useMemo(() => {
|
||||
if (!repack) return [];
|
||||
|
||||
const unavailableUrisSet = new Set(repack.unavailableUris ?? []);
|
||||
const unavailableUrisSet = new Set(repack?.unavailableUris ?? []);
|
||||
|
||||
const downloaderMap = new Map<
|
||||
Downloader,
|
||||
{ hasAvailable: boolean; hasUnavailable: boolean }
|
||||
>();
|
||||
|
||||
for (const uri of repack.uris) {
|
||||
const uriDownloaders = getDownloadersForUri(uri);
|
||||
const isAvailable = !unavailableUrisSet.has(uri);
|
||||
if (repack) {
|
||||
for (const uri of repack.uris) {
|
||||
const uriDownloaders = getDownloadersForUri(uri);
|
||||
const isAvailable = !unavailableUrisSet.has(uri);
|
||||
|
||||
for (const downloader of uriDownloaders) {
|
||||
const existing = downloaderMap.get(downloader);
|
||||
if (existing) {
|
||||
existing.hasAvailable = existing.hasAvailable || isAvailable;
|
||||
existing.hasUnavailable = existing.hasUnavailable || !isAvailable;
|
||||
} else {
|
||||
downloaderMap.set(downloader, {
|
||||
hasAvailable: isAvailable,
|
||||
hasUnavailable: !isAvailable,
|
||||
});
|
||||
for (const downloader of uriDownloaders) {
|
||||
const existing = downloaderMap.get(downloader);
|
||||
if (existing) {
|
||||
existing.hasAvailable = existing.hasAvailable || isAvailable;
|
||||
existing.hasUnavailable = existing.hasUnavailable || !isAvailable;
|
||||
} else {
|
||||
downloaderMap.set(downloader, {
|
||||
hasAvailable: isAvailable,
|
||||
hasUnavailable: !isAvailable,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(downloaderMap.entries()).map(([downloader, status]) => ({
|
||||
downloader,
|
||||
isAvailable: status.hasAvailable,
|
||||
}));
|
||||
}, [repack]);
|
||||
const allDownloaders = Object.values(Downloader).filter(
|
||||
(value) => typeof value === "number"
|
||||
) as Downloader[];
|
||||
|
||||
return allDownloaders
|
||||
.filter((downloader) => downloader !== Downloader.Hydra) // Temporarily comment out Nimbus
|
||||
.map((downloader) => {
|
||||
const status = downloaderMap.get(downloader);
|
||||
const canHandle = status !== undefined;
|
||||
const isAvailable = status?.hasAvailable ?? false;
|
||||
|
||||
let isConfigured = true;
|
||||
if (downloader === Downloader.RealDebrid) {
|
||||
isConfigured = !!userPreferences?.realDebridApiToken;
|
||||
} else if (downloader === Downloader.TorBox) {
|
||||
isConfigured = !!userPreferences?.torBoxApiToken;
|
||||
}
|
||||
// } else if (downloader === Downloader.Hydra) {
|
||||
// isConfigured = isFeatureEnabled(Feature.Nimbus);
|
||||
// }
|
||||
|
||||
const isAvailableButNotConfigured =
|
||||
isAvailable && !isConfigured && canHandle;
|
||||
|
||||
return {
|
||||
downloader,
|
||||
isAvailable: isAvailable && isConfigured,
|
||||
canHandle,
|
||||
isAvailableButNotConfigured,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.isAvailable && !b.isAvailable) return -1;
|
||||
if (!a.isAvailable && b.isAvailable) return 1;
|
||||
if (
|
||||
a.canHandle &&
|
||||
!a.isAvailable &&
|
||||
!a.isAvailableButNotConfigured &&
|
||||
!b.canHandle
|
||||
)
|
||||
return -1;
|
||||
if (
|
||||
!a.canHandle &&
|
||||
b.canHandle &&
|
||||
!b.isAvailable &&
|
||||
!b.isAvailableButNotConfigured
|
||||
)
|
||||
return 1;
|
||||
if (a.isAvailableButNotConfigured && !b.canHandle) return -1;
|
||||
if (!a.canHandle && b.isAvailableButNotConfigured) return 1;
|
||||
if (
|
||||
a.isAvailableButNotConfigured &&
|
||||
b.canHandle &&
|
||||
!b.isAvailable &&
|
||||
!b.isAvailableButNotConfigured
|
||||
)
|
||||
return 1;
|
||||
if (
|
||||
!a.isAvailableButNotConfigured &&
|
||||
a.canHandle &&
|
||||
!a.isAvailable &&
|
||||
b.isAvailableButNotConfigured
|
||||
)
|
||||
return -1;
|
||||
return 0;
|
||||
});
|
||||
}, [
|
||||
repack,
|
||||
userPreferences?.realDebridApiToken,
|
||||
userPreferences?.torBoxApiToken,
|
||||
isFeatureEnabled,
|
||||
Feature,
|
||||
]);
|
||||
|
||||
const getDefaultDownloader = useCallback(
|
||||
(availableDownloaders: Downloader[]) => {
|
||||
if (availableDownloaders.length === 0) return null;
|
||||
|
||||
if (availableDownloaders.includes(Downloader.Hydra)) {
|
||||
return Downloader.Hydra;
|
||||
}
|
||||
// if (availableDownloaders.includes(Downloader.Hydra)) {
|
||||
// return Downloader.Hydra;
|
||||
// }
|
||||
|
||||
if (availableDownloaders.includes(Downloader.RealDebrid)) {
|
||||
return Downloader.RealDebrid;
|
||||
@@ -151,26 +219,12 @@ export function DownloadSettingsModal({
|
||||
.then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath));
|
||||
}
|
||||
|
||||
const filteredDownloaders = downloaders.filter((downloader) => {
|
||||
if (downloader === Downloader.RealDebrid)
|
||||
return userPreferences?.realDebridApiToken;
|
||||
if (downloader === Downloader.TorBox)
|
||||
return userPreferences?.torBoxApiToken;
|
||||
if (downloader === Downloader.Hydra)
|
||||
return isFeatureEnabled(Feature.Nimbus);
|
||||
return true;
|
||||
});
|
||||
const availableDownloaders = downloadOptions
|
||||
.filter((option) => option.isAvailable)
|
||||
.map((option) => option.downloader);
|
||||
|
||||
setSelectedDownloader(getDefaultDownloader(filteredDownloaders));
|
||||
}, [
|
||||
Feature,
|
||||
isFeatureEnabled,
|
||||
getDefaultDownloader,
|
||||
userPreferences?.downloadsPath,
|
||||
downloaders,
|
||||
userPreferences?.realDebridApiToken,
|
||||
userPreferences?.torBoxApiToken,
|
||||
]);
|
||||
setSelectedDownloader(getDefaultDownloader(availableDownloaders));
|
||||
}, [getDefaultDownloader, userPreferences?.downloadsPath, downloadOptions]);
|
||||
|
||||
const handleChooseDownloadsPath = async () => {
|
||||
const { filePaths } = await window.electron.showOpenDialog({
|
||||
@@ -225,49 +279,111 @@ export function DownloadSettingsModal({
|
||||
<div className="download-settings-modal__downloads-path-field">
|
||||
<span>{t("downloader")}</span>
|
||||
|
||||
<div className="download-settings-modal__downloaders-list">
|
||||
{downloadOptions.map((option) => {
|
||||
const isUnavailable = !option.isAvailable;
|
||||
const shouldDisableOption =
|
||||
isUnavailable ||
|
||||
(option.downloader === Downloader.RealDebrid &&
|
||||
!userPreferences?.realDebridApiToken) ||
|
||||
(option.downloader === Downloader.TorBox &&
|
||||
!userPreferences?.torBoxApiToken) ||
|
||||
(option.downloader === Downloader.Hydra &&
|
||||
!isFeatureEnabled(Feature.Nimbus));
|
||||
<div className="download-settings-modal__downloaders-list-wrapper">
|
||||
<div className="download-settings-modal__downloaders-list">
|
||||
{downloadOptions.map((option, index) => {
|
||||
const isSelected = selectedDownloader === option.downloader;
|
||||
const tooltipId = `availability-indicator-${option.downloader}`;
|
||||
const isLastItem = index === downloadOptions.length - 1;
|
||||
|
||||
const isSelected = selectedDownloader === option.downloader;
|
||||
const Indicator = option.isAvailable ? motion.span : "span";
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={option.downloader}
|
||||
className={`download-settings-modal__downloader-item ${
|
||||
isSelected
|
||||
? "download-settings-modal__downloader-item--selected"
|
||||
: ""
|
||||
} ${
|
||||
shouldDisableOption
|
||||
? "download-settings-modal__downloader-item--disabled"
|
||||
: ""
|
||||
}`}
|
||||
disabled={shouldDisableOption}
|
||||
onClick={() => setSelectedDownloader(option.downloader)}
|
||||
>
|
||||
<span className="download-settings-modal__downloader-name">
|
||||
{DOWNLOADER_NAME[option.downloader]}
|
||||
</span>
|
||||
<span
|
||||
className={`download-settings-modal__availability-indicator ${
|
||||
option.isAvailable
|
||||
? "download-settings-modal__availability-indicator--available"
|
||||
: "download-settings-modal__availability-indicator--unavailable"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<div
|
||||
key={option.downloader}
|
||||
className="download-settings-modal__downloader-item-wrapper"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`download-settings-modal__downloader-item ${
|
||||
isSelected
|
||||
? "download-settings-modal__downloader-item--selected"
|
||||
: ""
|
||||
} ${
|
||||
isLastItem
|
||||
? "download-settings-modal__downloader-item--last"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (
|
||||
option.downloader === Downloader.RealDebrid &&
|
||||
option.isAvailableButNotConfigured
|
||||
) {
|
||||
setShowRealDebridModal(true);
|
||||
} else {
|
||||
setSelectedDownloader(option.downloader);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="download-settings-modal__downloader-name">
|
||||
{DOWNLOADER_NAME[option.downloader]}
|
||||
</span>
|
||||
<div className="download-settings-modal__availability-indicator-wrapper">
|
||||
{option.isAvailable ? (
|
||||
<Indicator
|
||||
className={`download-settings-modal__availability-indicator download-settings-modal__availability-indicator--available download-settings-modal__availability-indicator--pulsating`}
|
||||
animate={{
|
||||
scale: [1, 1.1, 1],
|
||||
opacity: [1, 0.7, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
data-tooltip-id={tooltipId}
|
||||
data-tooltip-content={t("downloader_online")}
|
||||
/>
|
||||
) : option.isAvailableButNotConfigured ? (
|
||||
<span
|
||||
className={`download-settings-modal__availability-indicator download-settings-modal__availability-indicator--warning`}
|
||||
data-tooltip-id={tooltipId}
|
||||
data-tooltip-content={t(
|
||||
"downloader_not_configured"
|
||||
)}
|
||||
/>
|
||||
) : option.canHandle ? (
|
||||
<span
|
||||
className={`download-settings-modal__availability-indicator download-settings-modal__availability-indicator--unavailable`}
|
||||
data-tooltip-id={tooltipId}
|
||||
data-tooltip-content={t("downloader_offline")}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={`download-settings-modal__availability-indicator download-settings-modal__availability-indicator--not-present`}
|
||||
data-tooltip-id={tooltipId}
|
||||
data-tooltip-content={t("downloader_not_available")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip id={tooltipId} />
|
||||
{isSelected ? (
|
||||
<motion.div
|
||||
className="download-settings-modal__check-icon-wrapper"
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 20,
|
||||
}}
|
||||
>
|
||||
<CheckCircleFillIcon
|
||||
size={16}
|
||||
className="download-settings-modal__check-icon"
|
||||
/>
|
||||
</motion.div>
|
||||
) : option.downloader === Downloader.RealDebrid &&
|
||||
option.canHandle ? (
|
||||
<div className="download-settings-modal__recommendation-badge">
|
||||
<Badge>{t("recommended")}</Badge>
|
||||
</div>
|
||||
) : null}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -319,7 +435,14 @@ export function DownloadSettingsModal({
|
||||
disabled={
|
||||
downloadStarting ||
|
||||
selectedDownloader === null ||
|
||||
!hasWritePermission
|
||||
!hasWritePermission ||
|
||||
downloadOptions.some(
|
||||
(option) =>
|
||||
option.downloader === selectedDownloader &&
|
||||
(option.isAvailableButNotConfigured ||
|
||||
(!option.isAvailable && option.canHandle) ||
|
||||
!option.canHandle)
|
||||
)
|
||||
}
|
||||
>
|
||||
{downloadStarting ? (
|
||||
@@ -335,6 +458,11 @@ export function DownloadSettingsModal({
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<RealDebridInfoModal
|
||||
visible={showRealDebridModal}
|
||||
onClose={() => setShowRealDebridModal(false)}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
@use "../../../scss/globals.scss";
|
||||
|
||||
.real-debrid-info-modal {
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 2.5);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
&__description-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 1.5);
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin: 0;
|
||||
color: globals.$body-color;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
&__create-account {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
color: #c0c1c7;
|
||||
text-decoration: underline;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Link, Modal } from "@renderer/components";
|
||||
import { LinkExternalIcon } from "@primer/octicons-react";
|
||||
import "./real-debrid-info-modal.scss";
|
||||
|
||||
const realDebridReferralId = import.meta.env
|
||||
.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID;
|
||||
|
||||
const REAL_DEBRID_URL = realDebridReferralId
|
||||
? `https://real-debrid.com/?id=${realDebridReferralId}`
|
||||
: "https://real-debrid.com";
|
||||
|
||||
export interface RealDebridInfoModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function RealDebridInfoModal({
|
||||
visible,
|
||||
onClose,
|
||||
}: Readonly<RealDebridInfoModalProps>) {
|
||||
const { t } = useTranslation("game_details");
|
||||
const { t: tSettings } = useTranslation("settings");
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={tSettings("enable_real_debrid")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="real-debrid-info-modal__content">
|
||||
<div className="real-debrid-info-modal__description-container">
|
||||
<p className="real-debrid-info-modal__description">
|
||||
{tSettings("real_debrid_description")}
|
||||
</p>
|
||||
<Link
|
||||
to={REAL_DEBRID_URL}
|
||||
className="real-debrid-info-modal__create-account"
|
||||
>
|
||||
<LinkExternalIcon />
|
||||
{tSettings("create_real_debrid_account")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClose();
|
||||
navigate("/settings?tab=4");
|
||||
}}
|
||||
>
|
||||
{t("go_to_settings")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user