diff --git a/package.json b/package.json index bb74198f..43aa7b78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydralauncher", - "version": "3.7.6", + "version": "3.8.0", "description": "Hydra", "main": "./out/main/index.js", "author": "Los Broxas", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 7180fab2..8ddeb83d 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -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", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 12dae377..f4cf078c 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -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" } } diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 719f72f7..5f7d672a 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -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" } } diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 1cf7ae2f..d43a0976 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -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": "Запрос в друзья отклонен" } } diff --git a/src/main/services/aria2.ts b/src/main/services/aria2.ts index 438008b2..f3f49018 100644 --- a/src/main/services/aria2.ts +++ b/src/main/services/aria2.ts @@ -21,7 +21,6 @@ export class Aria2 { "--rpc-listen-all", "--file-allocation=none", "--allow-overwrite=true", - "--disable-ipv6", ], { stdio: "inherit", windowsHide: true } ); diff --git a/src/main/services/hosters/vikingfile.ts b/src/main/services/hosters/vikingfile.ts index ad89b876..97b4236c 100644 --- a/src/main/services/hosters/vikingfile.ts +++ b/src/main/services/hosters/vikingfile.ts @@ -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 { const unlockResponse = await axios.post( `${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 ( diff --git a/src/renderer/src/constants.ts b/src/renderer/src/constants.ts index 153fd644..d227969b 100644 --- a/src/renderer/src/constants.ts +++ b/src/renderer/src/constants.ts @@ -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", diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.scss b/src/renderer/src/pages/game-details/modals/download-settings-modal.scss index 4c33ebb4..79c8252d 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.scss +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.scss @@ -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 { diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index 0b8aff7d..597f6ae5 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -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( 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({
{t("downloader")} -
- {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)); +
+
+ {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 ( - - ); - })} + return ( +
+ +
+ ); + })} +
@@ -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({ )}
+ + setShowRealDebridModal(false)} + /> ); } diff --git a/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.scss b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.scss new file mode 100644 index 00000000..222ce4ec --- /dev/null +++ b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.scss @@ -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; + } + } +} + diff --git a/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.tsx b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.tsx new file mode 100644 index 00000000..be82bd76 --- /dev/null +++ b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.tsx @@ -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) { + const { t } = useTranslation("game_details"); + const { t: tSettings } = useTranslation("settings"); + const navigate = useNavigate(); + + return ( + +
+
+

+ {tSettings("real_debrid_description")} +

+ + + {tSettings("create_real_debrid_account")} + +
+ + +
+
+ ); +} +