diff --git a/.env.example b/.env.example index 051d8aa3..e13fc1bd 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ MAIN_VITE_API_URL= MAIN_VITE_AUTH_URL= MAIN_VITE_WS_URL= +MAIN_VITE_NIMBUS_API_URL= RENDERER_VITE_REAL_DEBRID_REFERRAL_ID= RENDERER_VITE_TORBOX_REFERRAL_CODE= MAIN_VITE_LAUNCHER_SUBDOMAIN= diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5086d8e5..31354bc3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,6 +57,7 @@ jobs: MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }} MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }} + MAIN_VITE_NIMBUS_API_URL: ${{ vars.MAIN_VITE_NIMBUS_API_URL }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -73,6 +74,7 @@ jobs: MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }} MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }} + MAIN_VITE_NIMBUS_API_URL: ${{ vars.MAIN_VITE_NIMBUS_API_URL }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df01b358..45163c4b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,9 +54,10 @@ jobs: MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }} + MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }} + MAIN_VITE_NIMBUS_API_URL: ${{ vars.MAIN_VITE_NIMBUS_API_URL }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} - MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }} @@ -71,9 +72,10 @@ jobs: MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }} + MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }} + MAIN_VITE_NIMBUS_API_URL: ${{ vars.MAIN_VITE_NIMBUS_API_URL }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} - MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }} diff --git a/package.json b/package.json index bb74198f..09e7ceda 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", @@ -91,6 +91,7 @@ "user-agents": "^1.1.387", "uuid": "^13.0.0", "winreg": "^1.2.5", + "workwonders-sdk": "0.0.10", "ws": "^8.18.1", "yaml": "^2.6.1", "yup": "^1.5.0" diff --git a/python_rpc/http_downloader.py b/python_rpc/http_downloader.py index 71e4b57e..40428bd7 100644 --- a/python_rpc/http_downloader.py +++ b/python_rpc/http_downloader.py @@ -1,4 +1,5 @@ import aria2p +from aria2p.client import ClientException as DownloadNotFound class HttpDownloader: def __init__(self): @@ -11,12 +12,16 @@ class HttpDownloader: ) ) - def start_download(self, url: str, save_path: str, header: str, out: str = None): + def start_download(self, url: str, save_path: str, header, out: str = None): if self.download: self.aria2.resume([self.download]) else: - downloads = self.aria2.add(url, options={"header": header, "dir": save_path, "out": out}) - + options = {"dir": save_path} + if header: + options["header"] = header + if out: + options["out"] = out + downloads = self.aria2.add(url, options=options) self.download = downloads[0] def pause_download(self): @@ -32,7 +37,11 @@ class HttpDownloader: if self.download == None: return None - download = self.aria2.get_download(self.download.gid) + try: + download = self.aria2.get_download(self.download.gid) + except DownloadNotFound: + self.download = None + return None response = { 'folderName': download.name, diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index dce99b28..d8358e34 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -175,6 +175,7 @@ "repacks_modal_description": "Choose the repack you want to download", "select_folder_hint": "To change the default folder, go to the <0>Settings", "download_now": "Download now", + "loading": "Loading...", "no_shop_details": "Could not retrieve shop details.", "download_options": "Download options", "download_path": "Download path", @@ -184,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", @@ -732,7 +739,6 @@ "game_added_to_pinned": "Game added to pinned", "karma": "Karma", "karma_count": "karma", - "karma_description": "Earned from positive likes on reviews", "user_reviews": "Reviews", "delete_review": "Delete Review", "loading_reviews": "Loading reviews...", @@ -805,6 +811,7 @@ "empty_description": "You're all caught up! Check back later for new updates.", "empty_filter_description": "No notifications match this filter.", "filter_all": "All", + "filter_unread": "Unread", "filter_friends": "Friends", "filter_badges": "Badges", "filter_upvotes": "Upvotes", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 12dae377..666dd065 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", @@ -710,11 +722,13 @@ "amount_minutes_short": "{{amount}}m", "karma": "Karma", "karma_count": "karma", - "karma_description": "Conseguido por me gustas positivos en reseñas", "sort_by": "Filtrar por:", "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 +781,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/fi/translation.json b/src/locales/fi/translation.json index fee3ff22..05268771 100644 --- a/src/locales/fi/translation.json +++ b/src/locales/fi/translation.json @@ -673,8 +673,7 @@ "game_removed_from_pinned": "Peli poistettu kiinnitetyistä", "game_added_to_pinned": "Peli lisätty kiinnitettyihin", "karma": "Karma", - "karma_count": "karmaa", - "karma_description": "Ansittu positiivisilla arvosteluäänillä" + "karma_count": "karmaa" }, "achievement": { "achievement_unlocked": "Saavutus avattu", diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index b83fec51..532c77e7 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -718,7 +718,6 @@ "game_added_to_pinned": "Játék hozzáadva a kitűzöttekhez", "karma": "Karma", "karma_count": "karma", - "karma_description": "Pozitív értékelésekkel szerzett pontok", "user_reviews": "Vélemények", "delete_review": "Vélemény Törlése", "loading_reviews": "Vélemények betöltése..." diff --git a/src/locales/lv/translation.json b/src/locales/lv/translation.json index 26aacb74..4b87dade 100644 --- a/src/locales/lv/translation.json +++ b/src/locales/lv/translation.json @@ -673,8 +673,7 @@ "game_removed_from_pinned": "Spēle dzēsta no piespraustajiem", "game_added_to_pinned": "Spēle pievienota piespraustajiem", "karma": "Karma", - "karma_count": "karma", - "karma_description": "Nopelnīta ar pozitīviem atsauksmju vērtējumiem" + "karma_count": "karma" }, "achievement": { "achievement_unlocked": "Sasniegums atbloķēts", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 719f72f7..ee5ef5dd 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", @@ -720,10 +732,12 @@ "achievements_earned": "Conquistas recebidas", "karma": "Karma", "karma_count": "karma", - "karma_description": "Ganho a partir de curtidas positivas em avaliações", "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 +790,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/pt-PT/translation.json b/src/locales/pt-PT/translation.json index e48e1458..313a4fd9 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -508,7 +508,7 @@ "show_and_compare_achievements": "Mostra e compara as tuas conquistas com as de outros utilizadores", "animated_profile_banner": "Banner animado no perfil", "cloud_saving": "Progresso dos jogos na nuvem", - "hydra_cloud_feature_found": "Descubriste uma funcionalidade Hydra Cloud!", + "hydra_cloud_feature_found": "Descobriste uma funcionalidade Hydra Cloud!", "learn_more": "Saber mais" } } diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 1cf7ae2f..ff863617 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": "Фоновое изображение обновлено", @@ -709,9 +721,11 @@ "game_added_to_pinned": "Игра добавлена в закрепленные", "karma": "Карма", "karma_count": "карма", - "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 +778,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/locales/tr/translation.json b/src/locales/tr/translation.json index 52e1f10f..94d12ee3 100644 --- a/src/locales/tr/translation.json +++ b/src/locales/tr/translation.json @@ -706,7 +706,6 @@ "game_added_to_pinned": "Oyun sabitlenmişlere eklendi", "karma": "Karma", "karma_count": "karma", - "karma_description": "İncelemelerdeki olumlu beğenilerden kazanılır", "user_reviews": "İncelemeler", "delete_review": "İncelemeyi Sil", "loading_reviews": "İncelemeler yükleniyor..." diff --git a/src/locales/uk/translation.json b/src/locales/uk/translation.json index 323d8ad5..32c79f9c 100644 --- a/src/locales/uk/translation.json +++ b/src/locales/uk/translation.json @@ -668,8 +668,7 @@ "game_removed_from_pinned": "Гру видалено із закріплених", "game_added_to_pinned": "Гру додано до закріплених", "karma": "Карма", - "karma_count": "карма", - "karma_description": "Зароблена позитивними оцінками на відгуках" + "karma_count": "карма" }, "achievement": { "achievement_unlocked": "Досягнення розблоковано", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index bfc353d9..094a49ca 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -689,7 +689,6 @@ "game_removed_from_pinned": "游戏已从置顶移除", "karma": "业力", "karma_count": "业力值", - "karma_description": "通过评论获得的点赞", "loading_reviews": "正在加载评价...", "manual_playtime_tooltip": "该游戏时长已手动更新", "pinned": "已置顶", diff --git a/src/main/services/7zip.ts b/src/main/services/7zip.ts index 0fa333dc..72c952c7 100644 --- a/src/main/services/7zip.ts +++ b/src/main/services/7zip.ts @@ -46,7 +46,7 @@ export class SevenZip { onProgress?: (progress: ExtractionProgress) => void ): Promise { return new Promise((resolve, reject) => { - const tryPassword = (index = -1) => { + const tryPassword = (index = 0) => { const password = passwords[index] ?? ""; logger.info( `Trying password "${password || "(empty)"}" on ${filePath}` @@ -115,7 +115,7 @@ export class SevenZip { }); }; - tryPassword(); + tryPassword(0); }); } diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index c36bf8ce..bc6746e2 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -8,6 +8,7 @@ import { DatanodesApi, MediafireApi, PixelDrainApi, + VikingFileApi, } from "../hosters"; import { PythonRPC } from "../python-rpc"; import { @@ -150,14 +151,28 @@ export class DownloadManager { if (!isDownloadingMetadata && !isCheckingFiles) { if (!download) return null; - await downloadsSublevel.put(downloadId, { + const updatedDownload = { ...download, bytesDownloaded, fileSize, progress, folderName, - status: "active", - }); + status: "active" as const, + }; + + await downloadsSublevel.put(downloadId, updatedDownload); + + return { + numPeers, + numSeeds, + downloadSpeed, + timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed), + isDownloadingMetadata, + isCheckingFiles, + progress, + gameId: downloadId, + download: updatedDownload, + } as DownloadProgress; } return { @@ -499,6 +514,29 @@ export class DownloadManager { allow_multiple_connections: true, }; } + case Downloader.VikingFile: { + logger.log( + `[DownloadManager] Processing VikingFile download for URI: ${download.uri}` + ); + try { + const downloadUrl = await VikingFileApi.getDownloadUrl(download.uri); + logger.log(`[DownloadManager] VikingFile direct URL obtained`); + return { + action: "start", + game_id: downloadId, + url: downloadUrl, + save_path: download.downloadPath, + header: + "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", + }; + } catch (error) { + logger.error( + `[DownloadManager] Error processing VikingFile download:`, + error + ); + throw error; + } + } } } diff --git a/src/main/services/hosters/buzzheavier.ts b/src/main/services/hosters/buzzheavier.ts index 9ef2d830..581f5a87 100644 --- a/src/main/services/hosters/buzzheavier.ts +++ b/src/main/services/hosters/buzzheavier.ts @@ -1,4 +1,6 @@ import axios from "axios"; +import http from "node:http"; +import https from "node:https"; import { HOSTER_USER_AGENT, extractHosterFilename, @@ -28,6 +30,12 @@ export class BuzzheavierApi { await axios.get(baseUrl, { headers: { "User-Agent": HOSTER_USER_AGENT }, timeout: 30000, + httpAgent: new http.Agent({ + family: 4, // Force IPv4 + }), + httpsAgent: new https.Agent({ + family: 4, // Force IPv4 + }), }); const downloadUrl = `${baseUrl}/download`; @@ -43,6 +51,12 @@ export class BuzzheavierApi { validateStatus: (status) => status === 200 || status === 204 || status === 301 || status === 302, timeout: 30000, + httpAgent: new http.Agent({ + family: 4, // Force IPv4 + }), + httpsAgent: new https.Agent({ + family: 4, // Force IPv4 + }), }); const hxRedirect = headResponse.headers["hx-redirect"]; diff --git a/src/main/services/hosters/index.ts b/src/main/services/hosters/index.ts index 5f918811..e22fb680 100644 --- a/src/main/services/hosters/index.ts +++ b/src/main/services/hosters/index.ts @@ -5,3 +5,4 @@ export * from "./mediafire"; export * from "./pixeldrain"; export * from "./buzzheavier"; export * from "./fuckingfast"; +export * from "./vikingfile"; diff --git a/src/main/services/hosters/vikingfile.ts b/src/main/services/hosters/vikingfile.ts new file mode 100644 index 00000000..0c6d30dc --- /dev/null +++ b/src/main/services/hosters/vikingfile.ts @@ -0,0 +1,46 @@ +import axios from "axios"; +import { logger } from "../logger"; + +interface UnlockResponse { + link: string; + hoster: string; +} + +export class VikingFileApi { + public static async getDownloadUrl(uri: string): Promise { + const unlockResponse = await axios.post( + `${import.meta.env.MAIN_VITE_NIMBUS_API_URL}/hosters/unlock`, + { url: uri } + ); + + if (!unlockResponse.data.link) { + throw new Error("Failed to unlock VikingFile URL"); + } + + const redirectUrl = unlockResponse.data.link; + + try { + const redirectResponse = await axios.head(redirectUrl, { + maxRedirects: 0, + validateStatus: (status) => + status === 301 || status === 302 || status === 200, + }); + + if ( + redirectResponse.headers.location || + redirectResponse.status === 301 || + redirectResponse.status === 302 + ) { + return redirectResponse.headers.location || redirectUrl; + } + + return redirectUrl; + } catch (error) { + logger.error( + `[VikingFile] Error following redirect, using redirect URL:`, + error + ); + return redirectUrl; + } + } +} diff --git a/src/main/services/python-rpc.ts b/src/main/services/python-rpc.ts index 2a1dce79..d04b00ab 100644 --- a/src/main/services/python-rpc.ts +++ b/src/main/services/python-rpc.ts @@ -1,4 +1,5 @@ import axios from "axios"; +import http from "node:http"; import cp from "node:child_process"; import fs from "node:fs"; @@ -31,6 +32,9 @@ export class PythonRPC { public static readonly RPC_PORT = "8084"; public static readonly rpc = axios.create({ baseURL: `http://localhost:${this.RPC_PORT}`, + httpAgent: new http.Agent({ + family: 4, // Force IPv4 + }), }); private static pythonProcess: cp.ChildProcess | null = null; diff --git a/src/main/vite-env.d.ts b/src/main/vite-env.d.ts index 7b0ed536..888d8329 100644 --- a/src/main/vite-env.d.ts +++ b/src/main/vite-env.d.ts @@ -7,6 +7,7 @@ interface ImportMetaEnv { readonly MAIN_VITE_CHECKOUT_URL: string; readonly MAIN_VITE_EXTERNAL_RESOURCES_URL: string; readonly MAIN_VITE_WS_URL: string; + readonly MAIN_VITE_NIMBUS_API_URL: string; readonly MAIN_VITE_LAUNCHER_SUBDOMAIN: string; readonly ELECTRON_RENDERER_URL: string; } diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 9334b5b9..bc23f844 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components"; - +import { WorkWondersSdk } from "workwonders-sdk"; import { useAppDispatch, useAppSelector, @@ -52,6 +52,8 @@ export function App() { const { clearDownload, setLastPacket } = useDownload(); + const workwondersRef = useRef(null); + const { hasActiveSubscription, fetchUserDetails, @@ -114,7 +116,30 @@ export function App() { return () => unsubscribe(); }, [updateLibrary]); - useEffect(() => { + const setupWorkWonders = useCallback( + async (token?: string, locale?: string) => { + if (workwondersRef.current) return; + + const possibleLocales = ["en", "pt", "ru"]; + + const parsedLocale = + possibleLocales.find((l) => l === locale?.slice(0, 2)) ?? "en"; + + workwondersRef.current = new WorkWondersSdk(); + await workwondersRef.current.init({ + organization: "hydra", + token, + locale: parsedLocale, + }); + + await workwondersRef.current.initChangelogWidget(); + workwondersRef.current.initChangelogWidgetMini(); + workwondersRef.current.initFeedbackWidget(); + }, + [workwondersRef] + ); + + const setupExternalResources = useCallback(async () => { const cachedUserDetails = window.localStorage.getItem("userDetails"); if (cachedUserDetails) { @@ -125,21 +150,26 @@ export function App() { dispatch(setProfileBackground(profileBackground)); } - fetchUserDetails() - .then((response) => { - if (response) { - updateUserDetails(response); - } - }) - .finally(() => { - if (document.getElementById("external-resources")) return; + const userPreferences = await window.electron.getUserPreferences(); + const userDetails = await fetchUserDetails().catch(() => null); - const $script = document.createElement("script"); - $script.id = "external-resources"; - $script.src = `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}/bundle.js?t=${Date.now()}`; - document.head.appendChild($script); - }); - }, [fetchUserDetails, updateUserDetails, dispatch]); + if (userDetails) { + updateUserDetails(userDetails); + } + + setupWorkWonders(userDetails?.workwondersJwt, userPreferences?.language); + + if (!document.getElementById("external-resources")) { + const $script = document.createElement("script"); + $script.id = "external-resources"; + $script.src = `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}/bundle.js?t=${Date.now()}`; + document.head.appendChild($script); + } + }, [fetchUserDetails, updateUserDetails, dispatch, setupWorkWonders]); + + useEffect(() => { + setupExternalResources(); + }, [setupExternalResources]); const onSignIn = useCallback(() => { fetchUserDetails().then((response) => { @@ -203,6 +233,7 @@ export function App() { useEffect(() => { if (contentRef.current) contentRef.current.scrollTop = 0; + workwondersRef.current?.notifyUrlChange(); }, [location.pathname, location.search]); useEffect(() => { diff --git a/src/renderer/src/constants.ts b/src/renderer/src/constants.ts index 89de5503..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", @@ -14,6 +14,7 @@ export const DOWNLOADER_NAME = { [Downloader.FuckingFast]: "FuckingFast", [Downloader.TorBox]: "TorBox", [Downloader.Hydra]: "Nimbus", + [Downloader.VikingFile]: "VikingFile", }; export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120; diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index d8b9bbd2..8b9a7a17 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -59,6 +59,7 @@ export function useUserDetails() { username: userDetails?.username || "", subscription: userDetails?.subscription || null, featurebaseJwt: userDetails?.featurebaseJwt || "", + workwondersJwt: userDetails?.workwondersJwt || "", karma: userDetails?.karma || 0, }); }, @@ -111,7 +112,7 @@ export function useUserDetails() { ); const undoFriendship = (userId: string) => - window.electron.hydraApi.delete(`/profile/friends/${userId}`); + window.electron.hydraApi.delete(`/profile/friend-requests/${userId}`); const blockUser = (userId: string) => window.electron.hydraApi.post(`/users/${userId}/block`); 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 1b7c51e8..75add6d3 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,23 +19,173 @@ color: globals.$body-color; } - &__downloaders { - display: grid; - gap: globals.$spacing-unit; - grid-template-columns: repeat(2, 1fr); + &__downloaders-list-wrapper { + border: 1px solid globals.$border-color; + overflow: hidden; + background-color: globals.$dark-background-color; } - &__downloader-option { - position: relative; + &__downloaders-list { + display: flex; + flex-direction: column; + gap: 0; + max-height: 200px; + overflow-y: auto; + overflow-x: hidden; + padding: 0; - &:only-child { - grid-column: 1 / -1; + &::-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); } } - &__downloader-icon { - position: absolute; - left: calc(globals.$spacing-unit * 2); + &__downloader-item { + display: flex; + align-items: center; + gap: 8px; + padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2); + border: 1px solid transparent; + border-bottom: 1px solid globals.$border-color; + border-radius: 0; + background-color: transparent; + cursor: pointer; + transition: + background-color 0.15s ease, + border-color 0.15s ease; + color: globals.$body-color; + font-size: 14px; + text-align: left; + height: 48px; + box-sizing: border-box; + + &:hover { + background-color: rgba(255, 255, 255, 0.05); + } + + &--selected { + background-color: rgba(255, 255, 255, 0.08); + } + + &--last { + border-bottom: none; + } + + &:disabled { + cursor: default; + + &:hover { + background-color: transparent; + } + + .download-settings-modal__downloader-name { + opacity: 0.5; + } + + .download-settings-modal__availability-indicator-wrapper { + opacity: 0.5; + } + } + } + + &__downloader-item-wrapper { + display: flex; + flex-direction: column; + } + + &__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: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + + &--available { + background-color: #22c55e; + box-shadow: 0 0 6px rgba(34, 197, 94, 0.5); + } + + &--unavailable { + 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 { @@ -49,4 +199,17 @@ &__change-path-button { align-self: flex-end; } + + &__loading-spinner { + animation: spin 1s linear infinite; + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } 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 a6c32b6e..0a2c6721 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,17 +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 { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react"; -import { Downloader, formatBytes, getDownloadersForUris } from "@shared"; +import { + 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 { @@ -51,6 +59,7 @@ export function DownloadSettingsModal({ const [hasWritePermission, setHasWritePermission] = useState( null ); + const [showRealDebridModal, setShowRealDebridModal] = useState(false); const { isFeatureEnabled, Feature } = useFeature(); @@ -78,18 +87,89 @@ export function DownloadSettingsModal({ } }, [visible, checkFolderWritePermission, selectedPath]); - const downloaders = useMemo(() => { - return getDownloadersForUris(repack?.uris ?? []); - }, [repack?.uris]); + const downloadOptions = useMemo(() => { + const unavailableUrisSet = new Set(repack?.unavailableUris ?? []); + + const downloaderMap = new Map< + Downloader, + { hasAvailable: boolean; hasUnavailable: boolean } + >(); + + 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, + }); + } + } + } + } + + const allDownloaders = Object.values(Downloader).filter( + (value) => typeof value === "number" + ) as Downloader[]; + + const getDownloaderPriority = (option: { + isAvailable: boolean; + canHandle: boolean; + isAvailableButNotConfigured: boolean; + }) => { + if (option.isAvailable) return 0; + if (option.canHandle && !option.isAvailableButNotConfigured) return 1; + if (option.isAvailableButNotConfigured) return 2; + return 3; + }; + + 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) => getDownloaderPriority(a) - getDownloaderPriority(b)); + }, [ + 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.RealDebrid)) { return Downloader.RealDebrid; } @@ -112,26 +192,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({ @@ -186,33 +252,144 @@ export function DownloadSettingsModal({
{t("downloader")} -
- {downloaders.map((downloader) => { - const shouldDisableButton = - (downloader === Downloader.RealDebrid && - !userPreferences?.realDebridApiToken) || - (downloader === Downloader.TorBox && - !userPreferences?.torBoxApiToken) || - (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; - return ( - - ); - })} + + if (option.isAvailableButNotConfigured) { + return ( + + ); + } + + if (option.canHandle) { + return ( + + ); + } + + return ( + + ); + }; + + const getRightContent = () => { + if (isSelected) { + return ( + + + + ); + } + + if ( + option.downloader === Downloader.RealDebrid && + option.canHandle + ) { + return ( +
+ {t("recommended")} +
+ ); + } + + return null; + }; + + return ( +
+ +
+ ); + })} +
@@ -264,13 +441,34 @@ export function DownloadSettingsModal({ disabled={ downloadStarting || selectedDownloader === null || - !hasWritePermission + !hasWritePermission || + downloadOptions.some( + (option) => + option.downloader === selectedDownloader && + (option.isAvailableButNotConfigured || + (!option.isAvailable && option.canHandle) || + !option.canHandle) + ) } > - - {t("download_now")} + {downloadStarting ? ( + <> + + {t("loading")} + + ) : ( + <> + + {t("download_now")} + + )}
+ + 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..5a97ae92 --- /dev/null +++ b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.scss @@ -0,0 +1,36 @@ +@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..be539db7 --- /dev/null +++ b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.tsx @@ -0,0 +1,58 @@ +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")} + +
+ + +
+
+ ); +} diff --git a/src/renderer/src/pages/library/library-game-card.scss b/src/renderer/src/pages/library/library-game-card.scss index ab9a9f2a..a0ef7e29 100644 --- a/src/renderer/src/pages/library/library-game-card.scss +++ b/src/renderer/src/pages/library/library-game-card.scss @@ -221,6 +221,26 @@ left: 0; z-index: 0; } + + &__cover-placeholder { + position: relative; + width: 100%; + height: 100%; + min-width: 100%; + min-height: 100%; + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0.08) 0%, + rgba(255, 255, 255, 0.04) 50%, + rgba(255, 255, 255, 0.08) 100% + ); + border-radius: 4px; + color: rgba(255, 255, 255, 0.3); + display: flex; + align-items: center; + justify-content: center; + z-index: 0; + } } @keyframes pulse { diff --git a/src/renderer/src/pages/library/library-game-card.tsx b/src/renderer/src/pages/library/library-game-card.tsx index a91176cb..101236ae 100644 --- a/src/renderer/src/pages/library/library-game-card.tsx +++ b/src/renderer/src/pages/library/library-game-card.tsx @@ -1,7 +1,12 @@ import { LibraryGame } from "@types"; import { useGameCard } from "@renderer/hooks"; -import { memo } from "react"; -import { ClockIcon, AlertFillIcon, TrophyIcon } from "@primer/octicons-react"; +import { memo, useState } from "react"; +import { + ClockIcon, + AlertFillIcon, + TrophyIcon, + ImageIcon, +} from "@primer/octicons-react"; import "./library-game-card.scss"; interface LibraryGameCardProps { @@ -25,14 +30,9 @@ export const LibraryGameCard = memo(function LibraryGameCard({ const { formatPlayTime, handleCardClick, handleContextMenuClick } = useGameCard(game, onContextMenu); - const coverImage = ( - game.customIconUrl ?? - game.coverImageUrl ?? - game.libraryImageUrl ?? - game.libraryHeroImageUrl ?? - game.iconUrl ?? - "" - ).replaceAll("\\", "/"); + const coverImage = game.coverImageUrl?.replaceAll("\\", "/") ?? ""; + + const [imageError, setImageError] = useState(false); return ( ); }); diff --git a/src/renderer/src/pages/notifications/notifications.scss b/src/renderer/src/pages/notifications/notifications.scss index c8fa7c3f..20fbc343 100644 --- a/src/renderer/src/pages/notifications/notifications.scss +++ b/src/renderer/src/pages/notifications/notifications.scss @@ -8,6 +8,72 @@ width: 100%; max-width: 800px; margin: 0 auto; + min-height: calc(100vh - 200px); + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + gap: calc(globals.$spacing-unit * 2); + } + + &__filter-tabs { + display: flex; + gap: globals.$spacing-unit; + position: relative; + flex: 1; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + + &__tab-wrapper { + position: relative; + } + + &__tab { + background: none; + border: none; + color: rgba(255, 255, 255, 0.6); + padding: calc(globals.$spacing-unit) calc(globals.$spacing-unit * 2); + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: color ease 0.2s; + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit * 0.5); + + &:hover { + color: rgba(255, 255, 255, 0.8); + } + + &--active { + color: white; + } + } + + &__tab-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 6px; + background-color: rgba(255, 255, 255, 0.15); + border-radius: 6px; + font-size: 11px; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); + line-height: 20px; + } + + &__tab-underline { + position: absolute; + bottom: -1px; + left: 0; + right: 0; + height: 2px; + background: white; + } &__actions { display: flex; @@ -15,6 +81,12 @@ justify-content: flex-end; } + &__content-wrapper { + display: flex; + flex-direction: column; + flex: 1; + } + &__list { display: flex; flex-direction: column; @@ -23,14 +95,22 @@ &__empty { display: flex; + flex: 1; width: 100%; - height: 100%; justify-content: center; align-items: center; flex-direction: column; gap: globals.$spacing-unit; } + &__empty-filter { + display: flex; + justify-content: center; + align-items: center; + padding: calc(globals.$spacing-unit * 6); + color: globals.$body-color; + } + &__icon-container { width: 60px; height: 60px; diff --git a/src/renderer/src/pages/notifications/notifications.tsx b/src/renderer/src/pages/notifications/notifications.tsx index f9bd0b46..f1c2d4de 100644 --- a/src/renderer/src/pages/notifications/notifications.tsx +++ b/src/renderer/src/pages/notifications/notifications.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { BellIcon } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; import { AnimatePresence, motion } from "framer-motion"; @@ -18,6 +18,11 @@ import type { } from "@types"; import "./notifications.scss"; +type NotificationFilter = "all" | "unread"; + +const STAGGER_DELAY_MS = 70; +const EXIT_DURATION_MS = 250; + export default function Notifications() { const { t, i18n } = useTranslation("notifications_page"); const { showSuccessToast, showErrorToast } = useToast(); @@ -34,12 +39,14 @@ export default function Notifications() { >([]); const [badges, setBadges] = useState([]); const [isLoading, setIsLoading] = useState(true); - const [clearingIds, setClearingIds] = useState>(new Set()); + const [isClearing, setIsClearing] = useState(false); + const [filter, setFilter] = useState("all"); const [pagination, setPagination] = useState({ total: 0, hasMore: false, skip: 0, }); + const clearingTimeoutsRef = useRef([]); const fetchLocalNotifications = useCallback(async () => { try { @@ -65,7 +72,11 @@ export default function Notifications() { }, [i18n.language]); const fetchApiNotifications = useCallback( - async (skip = 0, append = false) => { + async ( + skip = 0, + append = false, + filterParam: NotificationFilter = "all" + ) => { if (!userDetails) return; try { @@ -74,7 +85,7 @@ export default function Notifications() { await window.electron.hydraApi.get( "/profile/notifications", { - params: { filter: "all", take: 20, skip }, + params: { filter: filterParam, take: 20, skip }, needsAuth: true, } ); @@ -101,24 +112,24 @@ export default function Notifications() { [userDetails] ); - const fetchAllNotifications = useCallback(async () => { - setIsLoading(true); - await Promise.all([ - fetchLocalNotifications(), - fetchBadges(), - userDetails ? fetchApiNotifications(0, false) : Promise.resolve(), - ]); - setIsLoading(false); - }, [ - fetchLocalNotifications, - fetchBadges, - fetchApiNotifications, - userDetails, - ]); + const fetchAllNotifications = useCallback( + async (filterParam: NotificationFilter = "all") => { + setIsLoading(true); + await Promise.all([ + fetchLocalNotifications(), + fetchBadges(), + userDetails + ? fetchApiNotifications(0, false, filterParam) + : Promise.resolve(), + ]); + setIsLoading(false); + }, + [fetchLocalNotifications, fetchBadges, fetchApiNotifications, userDetails] + ); useEffect(() => { - fetchAllNotifications(); - }, [fetchAllNotifications]); + fetchAllNotifications(filter); + }, [fetchAllNotifications, filter]); useEffect(() => { const unsubscribe = window.electron.onLocalNotificationCreated( @@ -130,6 +141,13 @@ export default function Notifications() { return () => unsubscribe(); }, []); + // Cleanup timeouts on unmount + useEffect(() => { + return () => { + clearingTimeoutsRef.current.forEach(clearTimeout); + }; + }, []); + const mergedNotifications = useMemo(() => { const sortByDate = (a: MergedNotification, b: MergedNotification) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); @@ -144,23 +162,28 @@ export default function Notifications() { .filter((n) => n.priority !== 1) .map((n) => ({ ...n, source: "api" as const })); - const localWithSource: MergedNotification[] = localNotifications.map( - (n) => ({ + // Filter local notifications based on current filter + const filteredLocalNotifications = + filter === "unread" + ? localNotifications.filter((n) => !n.isRead) + : localNotifications; + + const localWithSource: MergedNotification[] = + filteredLocalNotifications.map((n) => ({ ...n, source: "local" as const, - }) - ); + })); const lowPriority = [...lowPriorityApi, ...localWithSource].sort( sortByDate ); return [...highPriority, ...lowPriority]; - }, [apiNotifications, localNotifications]); + }, [apiNotifications, localNotifications, filter]); const displayedNotifications = useMemo(() => { - return mergedNotifications.filter((n) => !clearingIds.has(n.id)); - }, [mergedNotifications, clearingIds]); + return mergedNotifications; + }, [mergedNotifications]); const notifyCountChange = useCallback(() => { window.dispatchEvent(new CustomEvent("notificationsChanged")); @@ -251,42 +274,86 @@ export default function Notifications() { [showErrorToast, t, notifyCountChange] ); + const removeNotificationFromState = useCallback( + (notification: MergedNotification) => { + if (notification.source === "api") { + setApiNotifications((prev) => + prev.filter((n) => n.id !== notification.id) + ); + } else { + setLocalNotifications((prev) => + prev.filter((n) => n.id !== notification.id) + ); + } + }, + [] + ); + + const removeNotificationWithDelay = useCallback( + (notification: MergedNotification, delayMs: number): Promise => { + return new Promise((resolve) => { + const timeout = setTimeout(() => { + removeNotificationFromState(notification); + resolve(); + }, delayMs); + + clearingTimeoutsRef.current.push(timeout); + }); + }, + [removeNotificationFromState] + ); + const handleClearAll = useCallback(async () => { + if (isClearing) return; + try { - // Mark all as clearing for animation - const allIds = new Set([ - ...apiNotifications.map((n) => n.id), - ...localNotifications.map((n) => n.id), - ]); - setClearingIds(allIds); + setIsClearing(true); - // Wait for exit animation - await new Promise((resolve) => setTimeout(resolve, 300)); + // Clear any existing timeouts + clearingTimeoutsRef.current.forEach(clearTimeout); + clearingTimeoutsRef.current = []; - // Clear all API notifications - if (userDetails && apiNotifications.length > 0) { + // Snapshot current notifications for staggered removal + const notificationsToRemove = [...displayedNotifications]; + const totalNotifications = notificationsToRemove.length; + + if (totalNotifications === 0) { + setIsClearing(false); + return; + } + + // Remove items one by one with staggered delays for visual effect + const removalPromises = notificationsToRemove.map((notification, index) => + removeNotificationWithDelay(notification, index * STAGGER_DELAY_MS) + ); + + // Wait for all items to be removed from state + await Promise.all(removalPromises); + + // Wait for the last exit animation to complete + await new Promise((resolve) => setTimeout(resolve, EXIT_DURATION_MS)); + + // Perform actual backend deletions (state is already cleared by staggered removal) + if (userDetails) { await window.electron.hydraApi.delete(`/profile/notifications/all`, { needsAuth: true, }); - setApiNotifications([]); } - - // Clear all local notifications await window.electron.clearAllLocalNotifications(); - setLocalNotifications([]); - - setClearingIds(new Set()); setPagination({ total: 0, hasMore: false, skip: 0 }); notifyCountChange(); showSuccessToast(t("cleared_all")); } catch (error) { logger.error("Failed to clear all notifications", error); - setClearingIds(new Set()); showErrorToast(t("failed_to_clear")); + } finally { + setIsClearing(false); + clearingTimeoutsRef.current = []; } }, [ - apiNotifications, - localNotifications, + displayedNotifications, + isClearing, + removeNotificationWithDelay, userDetails, showSuccessToast, showErrorToast, @@ -296,9 +363,19 @@ export default function Notifications() { const handleLoadMore = useCallback(() => { if (pagination.hasMore && !isLoading) { - fetchApiNotifications(pagination.skip, true); + fetchApiNotifications(pagination.skip, true, filter); } - }, [pagination, isLoading, fetchApiNotifications]); + }, [pagination, isLoading, fetchApiNotifications, filter]); + + const handleFilterChange = useCallback( + (newFilter: NotificationFilter) => { + if (newFilter !== filter) { + setFilter(newFilter); + setPagination({ total: 0, hasMore: false, skip: 0 }); + } + }, + [filter] + ); const handleAcceptFriendRequest = useCallback(() => { showSuccessToast(t("friend_request_accepted")); @@ -317,10 +394,13 @@ export default function Notifications() { return ( {notification.source === "local" ? ( @@ -343,8 +423,57 @@ export default function Notifications() { ); }; + const unreadCount = useMemo(() => { + const apiUnread = apiNotifications.filter((n) => !n.isRead).length; + const localUnread = localNotifications.filter((n) => !n.isRead).length; + return apiUnread + localUnread; + }, [apiNotifications, localNotifications]); + + const renderFilterTabs = () => ( +
+
+ + {filter === "all" && ( + + )} +
+
+ + {filter === "unread" && ( + + )} +
+
+ ); + + const hasNoNotifications = mergedNotifications.length === 0; + const shouldDisableActions = isClearing || hasNoNotifications; + const renderContent = () => { - if (isLoading && mergedNotifications.length === 0) { + if (isLoading && hasNoNotifications) { return (
{t("loading")} @@ -352,36 +481,61 @@ export default function Notifications() { ); } - if (mergedNotifications.length === 0) { - return ( -
-
- -
-

{t("empty_title")}

-

{t("empty_description")}

-
- ); - } - return (
-
- - +
+ {renderFilterTabs()} +
+ + +
-
- - {displayedNotifications.map(renderNotification)} - -
+ {/* Keep AnimatePresence mounted during clearing to preserve exit animations */} + + + {hasNoNotifications && !isClearing ? ( +
+
+ +
+

{t("empty_title")}

+

+ {filter === "unread" + ? t("empty_filter_description") + : t("empty_description")} +

+
+ ) : ( +
+ + {displayedNotifications.map(renderNotification)} + +
+ )} +
+
- {pagination.hasMore && ( + {pagination.hasMore && !isClearing && (
-
- - {t("karma_description")} - -
)} diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 6e900175..5c28a27e 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -10,6 +10,7 @@ export enum Downloader { Hydra, Buzzheavier, FuckingFast, + VikingFile, } export enum DownloadSourceStatus { diff --git a/src/shared/index.ts b/src/shared/index.ts index d54ef387..36996e1d 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -124,6 +124,9 @@ export const getDownloadersForUri = (uri: string) => { if (uri.startsWith("https://fuckingfast.co")) { return [Downloader.FuckingFast]; } + if (uri.startsWith("https://vikingfile.com")) { + return [Downloader.VikingFile]; + } if (realDebridHosts.some((host) => uri.startsWith(host))) return [Downloader.RealDebrid]; diff --git a/src/types/index.ts b/src/types/index.ts index cc931be5..855d88d5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -20,6 +20,7 @@ export interface GameRepack { title: string; fileSize: string | null; uris: string[]; + unavailableUris: string[]; uploadDate: string | null; downloadSourceId: string; downloadSourceName: string; @@ -187,6 +188,7 @@ export interface UserDetails { profileVisibility: ProfileVisibility; bio: string; featurebaseJwt: string; + workwondersJwt: string; subscription: Subscription | null; karma: number; achievements: ProfileAchievement[] | null; diff --git a/yarn.lock b/yarn.lock index 9d354966..27d1e902 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2203,14 +2203,15 @@ tslib "^2.6.2" "@smithy/config-resolver@^4.3.0", "@smithy/config-resolver@^4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.3.1.tgz#f1a0ed6faa52377909440002e1632be9fc901840" - integrity sha512-tWDwrWy37CDVGeaP8AIGZPFL2RoFtmd5Y+nTzLw5qroXNedT2S66EY2d+XzB1zxulCd6nfDXnAQu4auq90aj5Q== + version "4.4.5" + resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.4.5.tgz#35e792b6db00887bdd029df9b41780ca005d064b" + integrity sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg== dependencies: - "@smithy/node-config-provider" "^4.3.1" - "@smithy/types" "^4.7.0" + "@smithy/node-config-provider" "^4.3.7" + "@smithy/types" "^4.11.0" "@smithy/util-config-provider" "^4.2.0" - "@smithy/util-middleware" "^4.2.1" + "@smithy/util-endpoints" "^3.2.7" + "@smithy/util-middleware" "^4.2.7" tslib "^2.6.2" "@smithy/core@^3.15.0", "@smithy/core@^3.16.0": @@ -2421,6 +2422,16 @@ "@smithy/types" "^4.7.0" tslib "^2.6.2" +"@smithy/node-config-provider@^4.3.7": + version "4.3.7" + resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-4.3.7.tgz#c023fa857b008c314f621fb5b124724c157b2fd3" + integrity sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw== + dependencies: + "@smithy/property-provider" "^4.2.7" + "@smithy/shared-ini-file-loader" "^4.4.2" + "@smithy/types" "^4.11.0" + tslib "^2.6.2" + "@smithy/node-http-handler@^4.3.0", "@smithy/node-http-handler@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.4.0.tgz#e1f6ae4a90cd7257699263bf8e06e653ff0e5f83" @@ -2440,6 +2451,14 @@ "@smithy/types" "^4.7.0" tslib "^2.6.2" +"@smithy/property-provider@^4.2.7": + version "4.2.7" + resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-4.2.7.tgz#cd0044e13495cf4064b3a6ed3299e5f549ba7513" + integrity sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA== + dependencies: + "@smithy/types" "^4.11.0" + tslib "^2.6.2" + "@smithy/protocol-http@^5.3.0", "@smithy/protocol-http@^5.3.1": version "5.3.1" resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-5.3.1.tgz#add01f73290f1e8fd49d7102b63e3fe53a5e6e18" @@ -2480,6 +2499,14 @@ "@smithy/types" "^4.7.0" tslib "^2.6.2" +"@smithy/shared-ini-file-loader@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.2.tgz#8fa1b459de485b11185fe8c64182e3205a280ba9" + integrity sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg== + dependencies: + "@smithy/types" "^4.11.0" + tslib "^2.6.2" + "@smithy/signature-v4@^5.3.0": version "5.3.1" resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.3.1.tgz#c3d711c29d37f3db4daf51750eea75204c4f51d4" @@ -2507,6 +2534,13 @@ "@smithy/util-stream" "^4.5.1" tslib "^2.6.2" +"@smithy/types@^4.11.0": + version "4.11.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.11.0.tgz#c02f6184dcb47c4f0b387a32a7eca47956cc09f1" + integrity sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA== + dependencies: + tslib "^2.6.2" + "@smithy/types@^4.6.0", "@smithy/types@^4.7.0": version "4.7.0" resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.7.0.tgz#42d707276d9184aef705f04e04615cd1979d044f" @@ -2601,6 +2635,15 @@ "@smithy/types" "^4.7.0" tslib "^2.6.2" +"@smithy/util-endpoints@^3.2.7": + version "3.2.7" + resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-3.2.7.tgz#78cd5dd4aac8d9977f49d256d1e3418a09cade72" + integrity sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg== + dependencies: + "@smithy/node-config-provider" "^4.3.7" + "@smithy/types" "^4.11.0" + tslib "^2.6.2" + "@smithy/util-hex-encoding@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz#1c22ea3d1e2c3a81ff81c0a4f9c056a175068a7b" @@ -2616,6 +2659,14 @@ "@smithy/types" "^4.7.0" tslib "^2.6.2" +"@smithy/util-middleware@^4.2.7": + version "4.2.7" + resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-4.2.7.tgz#1cae2c4fd0389ac858d29f7170c33b4443e83524" + integrity sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w== + dependencies: + "@smithy/types" "^4.11.0" + tslib "^2.6.2" + "@smithy/util-retry@^4.2.0", "@smithy/util-retry@^4.2.1": version "4.2.1" resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-4.2.1.tgz#8336368586a458cdce86fc92d6fb11fd1db41521" @@ -6354,6 +6405,11 @@ keyv@^4.0.0, keyv@^4.5.3: dependencies: json-buffer "3.0.1" +ky@^1.11.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/ky/-/ky-1.14.1.tgz#16f20b3bf3939abcc04e2a9613f47360fe5f64c9" + integrity sha512-hYje4L9JCmpEQBtudo+v52X5X8tgWXUYyPcxKSuxQNboqufecl9VMWjGiucAFH060AwPXHZuH+WB2rrqfkmafw== + language-subtag-registry@^0.3.20: version "0.3.23" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" @@ -9123,6 +9179,13 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== +workwonders-sdk@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/workwonders-sdk/-/workwonders-sdk-0.0.10.tgz#377167370a39c905c5228f8972c37c19004b7b21" + integrity sha512-bnswhlLRz1TCiqGV8l+VEOBej7u1SAkzLMEv6A60Sp0+S4j4pnmSve92KeOts/GYtUeNDuNM7fLPwZwMKY3sAg== + dependencies: + ky "^1.11.0" + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"