mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
fix: fixing seeding on level
This commit is contained in:
commit
5574f6cb20
231 changed files with 5236 additions and 4761 deletions
|
@ -6,7 +6,6 @@ import {
|
||||||
externalizeDepsPlugin,
|
externalizeDepsPlugin,
|
||||||
} from "electron-vite";
|
} from "electron-vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
|
|
||||||
import svgr from "vite-plugin-svgr";
|
import svgr from "vite-plugin-svgr";
|
||||||
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
||||||
|
|
||||||
|
@ -55,7 +54,6 @@ export default defineConfig(({ mode }) => {
|
||||||
plugins: [
|
plugins: [
|
||||||
svgr(),
|
svgr(),
|
||||||
react(),
|
react(),
|
||||||
vanillaExtractPlugin(),
|
|
||||||
sentryVitePlugin({
|
sentryVitePlugin({
|
||||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||||
org: "hydra-launcher",
|
org: "hydra-launcher",
|
||||||
|
|
13
package.json
13
package.json
|
@ -41,9 +41,6 @@
|
||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
"@sentry/react": "^8.47.0",
|
"@sentry/react": "^8.47.0",
|
||||||
"@sentry/vite-plugin": "^2.22.7",
|
"@sentry/vite-plugin": "^2.22.7",
|
||||||
"@vanilla-extract/css": "^1.14.2",
|
|
||||||
"@vanilla-extract/dynamic": "^2.1.2",
|
|
||||||
"@vanilla-extract/recipes": "^0.5.2",
|
|
||||||
"auto-launch": "^5.0.6",
|
"auto-launch": "^5.0.6",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"better-sqlite3": "^11.7.0",
|
"better-sqlite3": "^11.7.0",
|
||||||
|
@ -90,9 +87,8 @@
|
||||||
"@swc/core": "^1.4.16",
|
"@swc/core": "^1.4.16",
|
||||||
"@types/auto-launch": "^5.0.5",
|
"@types/auto-launch": "^5.0.5",
|
||||||
"@types/color": "^3.0.6",
|
"@types/color": "^3.0.6",
|
||||||
"@types/folder-hash": "^4.0.4",
|
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/jsonwebtoken": "^9.0.7",
|
"@types/jsonwebtoken": "^9.0.8",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
"@types/parse-torrent": "^5.8.7",
|
"@types/parse-torrent": "^5.8.7",
|
||||||
|
@ -100,14 +96,13 @@
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
"@types/sound-play": "^1.1.3",
|
"@types/sound-play": "^1.1.3",
|
||||||
"@types/user-agents": "^1.0.4",
|
"@types/user-agents": "^1.0.4",
|
||||||
"@vanilla-extract/vite-plugin": "^4.0.7",
|
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"electron": "^31.7.6",
|
"electron": "^31.7.7",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"electron-vite": "^2.0.0",
|
"electron-vite": "^2.3.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
|
|
|
@ -11,11 +11,12 @@ class HttpDownloader:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def start_download(self, url: str, save_path: str, header: str):
|
def start_download(self, url: str, save_path: str, header: str, out: str = None):
|
||||||
if self.download:
|
if self.download:
|
||||||
self.aria2.resume([self.download])
|
self.aria2.resume([self.download])
|
||||||
else:
|
else:
|
||||||
downloads = self.aria2.add(url, options={"header": header, "dir": save_path})
|
downloads = self.aria2.add(url, options={"header": header, "dir": save_path, "out": out})
|
||||||
|
|
||||||
self.download = downloads[0]
|
self.download = downloads[0]
|
||||||
|
|
||||||
def pause_download(self):
|
def pause_download(self):
|
||||||
|
|
|
@ -28,14 +28,14 @@ if start_download_payload:
|
||||||
torrent_downloader = TorrentDownloader(torrent_session)
|
torrent_downloader = TorrentDownloader(torrent_session)
|
||||||
downloads[initial_download['game_id']] = torrent_downloader
|
downloads[initial_download['game_id']] = torrent_downloader
|
||||||
try:
|
try:
|
||||||
torrent_downloader.start_download(initial_download['url'], initial_download['save_path'], "")
|
torrent_downloader.start_download(initial_download['url'], initial_download['save_path'])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error starting torrent download", e)
|
print("Error starting torrent download", e)
|
||||||
else:
|
else:
|
||||||
http_downloader = HttpDownloader()
|
http_downloader = HttpDownloader()
|
||||||
downloads[initial_download['game_id']] = http_downloader
|
downloads[initial_download['game_id']] = http_downloader
|
||||||
try:
|
try:
|
||||||
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'))
|
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error starting http download", e)
|
print("Error starting http download", e)
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ if start_seeding_payload:
|
||||||
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
||||||
downloads[seed['game_id']] = torrent_downloader
|
downloads[seed['game_id']] = torrent_downloader
|
||||||
try:
|
try:
|
||||||
torrent_downloader.start_download(seed['url'], seed['save_path'], "")
|
torrent_downloader.start_download(seed['url'], seed['save_path'])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error starting seeding", e)
|
print("Error starting seeding", e)
|
||||||
|
|
||||||
|
@ -140,18 +140,18 @@ def action():
|
||||||
|
|
||||||
if url.startswith('magnet'):
|
if url.startswith('magnet'):
|
||||||
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
||||||
existing_downloader.start_download(url, data['save_path'], "")
|
existing_downloader.start_download(url, data['save_path'])
|
||||||
else:
|
else:
|
||||||
torrent_downloader = TorrentDownloader(torrent_session)
|
torrent_downloader = TorrentDownloader(torrent_session)
|
||||||
downloads[game_id] = torrent_downloader
|
downloads[game_id] = torrent_downloader
|
||||||
torrent_downloader.start_download(url, data['save_path'], "")
|
torrent_downloader.start_download(url, data['save_path'])
|
||||||
else:
|
else:
|
||||||
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
|
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
|
||||||
existing_downloader.start_download(url, data['save_path'], data.get('header'))
|
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||||
else:
|
else:
|
||||||
http_downloader = HttpDownloader()
|
http_downloader = HttpDownloader()
|
||||||
downloads[game_id] = http_downloader
|
downloads[game_id] = http_downloader
|
||||||
http_downloader.start_download(url, data['save_path'], data.get('header'))
|
http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||||
|
|
||||||
downloading_game_id = game_id
|
downloading_game_id = game_id
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ def action():
|
||||||
elif action == 'resume_seeding':
|
elif action == 'resume_seeding':
|
||||||
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
||||||
downloads[game_id] = torrent_downloader
|
downloads[game_id] = torrent_downloader
|
||||||
torrent_downloader.start_download(data['url'], data['save_path'], "")
|
torrent_downloader.start_download(data['url'], data['save_path'])
|
||||||
elif action == 'pause_seeding':
|
elif action == 'pause_seeding':
|
||||||
downloader = downloads.get(game_id)
|
downloader = downloads.get(game_id)
|
||||||
if downloader:
|
if downloader:
|
||||||
|
|
|
@ -102,7 +102,7 @@ class TorrentDownloader:
|
||||||
"http://bvarf.tracker.sh:2086/announce",
|
"http://bvarf.tracker.sh:2086/announce",
|
||||||
]
|
]
|
||||||
|
|
||||||
def start_download(self, magnet: str, save_path: str, header: str):
|
def start_download(self, magnet: str, save_path: str):
|
||||||
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers, 'flags': self.flags}
|
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers, 'flags': self.flags}
|
||||||
self.torrent_handle = self.session.add_torrent(params)
|
self.torrent_handle = self.session.add_torrent(params)
|
||||||
self.torrent_handle.resume()
|
self.torrent_handle.resume()
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -236,13 +236,13 @@
|
||||||
"behavior": "السلوك",
|
"behavior": "السلوك",
|
||||||
"download_sources": "مصادر التنزيل",
|
"download_sources": "مصادر التنزيل",
|
||||||
"language": "اللغة",
|
"language": "اللغة",
|
||||||
"real_debrid_api_token": "رمز API",
|
"api_token": "رمز API",
|
||||||
"enable_real_debrid": "تفعيل Real-Debrid",
|
"enable_real_debrid": "تفعيل Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، مقيدة فقط بسرعة الإنترنت لديك.",
|
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، مقيدة فقط بسرعة الإنترنت لديك.",
|
||||||
"real_debrid_invalid_token": "رمز API غير صالح",
|
"debrid_invalid_token": "رمز API غير صالح",
|
||||||
"real_debrid_api_token_hint": "يمكنك الحصول على رمز API الخاص بك <0>هنا</0>",
|
"debrid_api_token_hint": "يمكنك الحصول على رمز API الخاص بك <0>هنا</0>",
|
||||||
"real_debrid_free_account_error": "الحساب \"{{username}}\" هو حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
"real_debrid_free_account_error": "الحساب \"{{username}}\" هو حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
||||||
"real_debrid_linked_message": "تم ربط الحساب \"{{username}}\"",
|
"debrid_linked_message": "تم ربط الحساب \"{{username}}\"",
|
||||||
"save_changes": "حفظ التغييرات",
|
"save_changes": "حفظ التغييرات",
|
||||||
"changes_saved": "تم حفظ التغييرات بنجاح",
|
"changes_saved": "تم حفظ التغييرات بنجاح",
|
||||||
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL المصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL المصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
||||||
|
|
|
@ -230,13 +230,13 @@
|
||||||
"behavior": "Поведение",
|
"behavior": "Поведение",
|
||||||
"download_sources": "Източници за изтегляне",
|
"download_sources": "Източници за изтегляне",
|
||||||
"language": "Език",
|
"language": "Език",
|
||||||
"real_debrid_api_token": "API Токен",
|
"api_token": "API Токен",
|
||||||
"enable_real_debrid": "Включи Real-Debrid",
|
"enable_real_debrid": "Включи Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid е неограничен даунлоудър, който ви позволява бързо да изтегляте файлове, ограничени само от скоростта на интернет..",
|
"real_debrid_description": "Real-Debrid е неограничен даунлоудър, който ви позволява бързо да изтегляте файлове, ограничени само от скоростта на интернет..",
|
||||||
"real_debrid_invalid_token": "Невалиден API токен",
|
"debrid_invalid_token": "Невалиден API токен",
|
||||||
"real_debrid_api_token_hint": "Вземете своя API токен <0>тук</0>",
|
"debrid_api_token_hint": "Вземете своя API токен <0>тук</0>",
|
||||||
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен акаунт. Моля абонирай се за Real-Debrid",
|
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен акаунт. Моля абонирай се за Real-Debrid",
|
||||||
"real_debrid_linked_message": "Акаунтът \"{{username}}\" е свързан",
|
"debrid_linked_message": "Акаунтът \"{{username}}\" е свързан",
|
||||||
"save_changes": "Запази промените",
|
"save_changes": "Запази промените",
|
||||||
"changes_saved": "Промените са успешно запазни",
|
"changes_saved": "Промените са успешно запазни",
|
||||||
"download_sources_description": "Hydra ще извлича връзките за изтегляне от тези източници. URL адресът на източника трябва да е директна връзка към .json файл, съдържащ връзките за изтегляне.",
|
"download_sources_description": "Hydra ще извлича връзките за изтегляне от тези източници. URL адресът на източника трябва да е директна връзка към .json файл, съдържащ връзките за изтегляне.",
|
||||||
|
|
|
@ -161,13 +161,13 @@
|
||||||
"behavior": "Comportament",
|
"behavior": "Comportament",
|
||||||
"download_sources": "Fonts de descàrrega",
|
"download_sources": "Fonts de descàrrega",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"real_debrid_api_token": "Testimoni API",
|
"api_token": "Testimoni API",
|
||||||
"enable_real_debrid": "Activa el Real Debrid",
|
"enable_real_debrid": "Activa el Real Debrid",
|
||||||
"real_debrid_description": "Real-Debrid és un programa de descàrrega sense restriccions que us permet descarregar fitxers a l'instant i al màxim de la vostra velocitat d'Internet.",
|
"real_debrid_description": "Real-Debrid és un programa de descàrrega sense restriccions que us permet descarregar fitxers a l'instant i al màxim de la vostra velocitat d'Internet.",
|
||||||
"real_debrid_invalid_token": "Invalida el testimoni de l'API",
|
"debrid_invalid_token": "Invalida el testimoni de l'API",
|
||||||
"real_debrid_api_token_hint": "Pots obtenir la teva clau de l'API <0>aquí</0>.",
|
"debrid_api_token_hint": "Pots obtenir la teva clau de l'API <0>aquí</0>.",
|
||||||
"real_debrid_free_account_error": "L'usuari \"{{username}}\" és un compte gratuït. Si us plau subscriu-te a Real-Debrid",
|
"real_debrid_free_account_error": "L'usuari \"{{username}}\" és un compte gratuït. Si us plau subscriu-te a Real-Debrid",
|
||||||
"real_debrid_linked_message": "Compte \"{{username}}\" vinculat",
|
"debrid_linked_message": "Compte \"{{username}}\" vinculat",
|
||||||
"save_changes": "Desa els canvis",
|
"save_changes": "Desa els canvis",
|
||||||
"changes_saved": "Els canvis s'han desat correctament",
|
"changes_saved": "Els canvis s'han desat correctament",
|
||||||
"download_sources_description": "Hydra buscarà els enllaços de descàrrega d'aquestes fonts. L'URL d'origen ha de ser un enllaç directe a un fitxer .json que contingui els enllaços de descàrrega.",
|
"download_sources_description": "Hydra buscarà els enllaços de descàrrega d'aquestes fonts. L'URL d'origen ha de ser un enllaç directe a un fitxer .json que contingui els enllaços de descàrrega.",
|
||||||
|
|
|
@ -214,13 +214,13 @@
|
||||||
"behavior": "Chování",
|
"behavior": "Chování",
|
||||||
"download_sources": "Zdroje stahování",
|
"download_sources": "Zdroje stahování",
|
||||||
"language": "Jazyk",
|
"language": "Jazyk",
|
||||||
"real_debrid_api_token": "API Token",
|
"api_token": "API Token",
|
||||||
"enable_real_debrid": "Povolit Real-Debrid",
|
"enable_real_debrid": "Povolit Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid je neomezený správce stahování, který umožňuje stahovat soubory v nejvyšší rychlosti vašeho internetu.",
|
"real_debrid_description": "Real-Debrid je neomezený správce stahování, který umožňuje stahovat soubory v nejvyšší rychlosti vašeho internetu.",
|
||||||
"real_debrid_invalid_token": "Neplatný API token",
|
"debrid_invalid_token": "Neplatný API token",
|
||||||
"real_debrid_api_token_hint": "API token můžeš sehnat <0>zde</0>",
|
"debrid_api_token_hint": "API token můžeš sehnat <0>zde</0>",
|
||||||
"real_debrid_free_account_error": "Účet \"{{username}}\" má základní úroveň. Prosím předplaťte si Real-Debrid",
|
"real_debrid_free_account_error": "Účet \"{{username}}\" má základní úroveň. Prosím předplaťte si Real-Debrid",
|
||||||
"real_debrid_linked_message": "Účet \"{{username}}\" je propojen",
|
"debrid_linked_message": "Účet \"{{username}}\" je propojen",
|
||||||
"save_changes": "Uložit změny",
|
"save_changes": "Uložit změny",
|
||||||
"changes_saved": "Změny úspěšně uloženy",
|
"changes_saved": "Změny úspěšně uloženy",
|
||||||
"download_sources_description": "Hydra bude odsud sbírat soubory. Zdrojový odkaz musí být .json soubor obsahující odkazy na soubory.",
|
"download_sources_description": "Hydra bude odsud sbírat soubory. Zdrojový odkaz musí být .json soubor obsahující odkazy na soubory.",
|
||||||
|
|
|
@ -177,13 +177,13 @@
|
||||||
"behavior": "Opførsel",
|
"behavior": "Opførsel",
|
||||||
"download_sources": "Download kilder",
|
"download_sources": "Download kilder",
|
||||||
"language": "Sprog",
|
"language": "Sprog",
|
||||||
"real_debrid_api_token": "API nøgle",
|
"api_token": "API nøgle",
|
||||||
"enable_real_debrid": "Slå Real-Debrid til",
|
"enable_real_debrid": "Slå Real-Debrid til",
|
||||||
"real_debrid_description": "Real-Debrid er en ubegrænset downloader der gør det muligt for dig at downloade filer med det samme og med den bedste udnyttelse af din internet hastighed.",
|
"real_debrid_description": "Real-Debrid er en ubegrænset downloader der gør det muligt for dig at downloade filer med det samme og med den bedste udnyttelse af din internet hastighed.",
|
||||||
"real_debrid_invalid_token": "Ugyldig API nøgle",
|
"debrid_invalid_token": "Ugyldig API nøgle",
|
||||||
"real_debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>",
|
"debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>",
|
||||||
"real_debrid_free_account_error": "Brugeren \"{{username}}\" er en gratis bruger. Venligst abbonér på Real-Debrid",
|
"real_debrid_free_account_error": "Brugeren \"{{username}}\" er en gratis bruger. Venligst abbonér på Real-Debrid",
|
||||||
"real_debrid_linked_message": "Brugeren \"{{username}}\" er forbundet",
|
"debrid_linked_message": "Brugeren \"{{username}}\" er forbundet",
|
||||||
"save_changes": "Gem ændringer",
|
"save_changes": "Gem ændringer",
|
||||||
"changes_saved": "Ændringer gemt successfuldt",
|
"changes_saved": "Ændringer gemt successfuldt",
|
||||||
"download_sources_description": "Hydra vil hente download links fra disse kilder. Kilde URLen skal være et direkte link til en .json fil der indeholder download linkene.",
|
"download_sources_description": "Hydra vil hente download links fra disse kilder. Kilde URLen skal være et direkte link til en .json fil der indeholder download linkene.",
|
||||||
|
|
|
@ -161,13 +161,13 @@
|
||||||
"behavior": "Verhalten",
|
"behavior": "Verhalten",
|
||||||
"download_sources": "Download-Quellen",
|
"download_sources": "Download-Quellen",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"real_debrid_api_token": "API Token",
|
"api_token": "API Token",
|
||||||
"enable_real_debrid": "Real-Debrid aktivieren",
|
"enable_real_debrid": "Real-Debrid aktivieren",
|
||||||
"real_debrid_description": "Real-Debrid ist ein unrestriktiver Downloader, der es dir ermöglicht Dateien sofort und mit deiner maximalen Internetgeschwindigkeit herunterzuladen.",
|
"real_debrid_description": "Real-Debrid ist ein unrestriktiver Downloader, der es dir ermöglicht Dateien sofort und mit deiner maximalen Internetgeschwindigkeit herunterzuladen.",
|
||||||
"real_debrid_invalid_token": "API token nicht gültig",
|
"debrid_invalid_token": "API token nicht gültig",
|
||||||
"real_debrid_api_token_hint": "<0>Hier</0> kannst du dir deinen API Token holen",
|
"debrid_api_token_hint": "<0>Hier</0> kannst du dir deinen API Token holen",
|
||||||
"real_debrid_free_account_error": "Das Konto \"{{username}}\" ist ein gratis account. Bitte abonniere Real-Debrid",
|
"real_debrid_free_account_error": "Das Konto \"{{username}}\" ist ein gratis account. Bitte abonniere Real-Debrid",
|
||||||
"real_debrid_linked_message": "Konto \"{{username}}\" verknüpft",
|
"debrid_linked_message": "Konto \"{{username}}\" verknüpft",
|
||||||
"save_changes": "Änderungen speichern",
|
"save_changes": "Änderungen speichern",
|
||||||
"changes_saved": "Änderungen erfolgreich gespeichert",
|
"changes_saved": "Änderungen erfolgreich gespeichert",
|
||||||
"download_sources_description": "Hydra wird die Download-Links von diesen Quellen abrufen. Die Quell-URL muss ein direkter Link zu einer .json Datei, welche die Download-Links enthält, sein.",
|
"download_sources_description": "Hydra wird die Download-Links von diesen Quellen abrufen. Die Quell-URL muss ein direkter Link zu einer .json Datei, welche die Download-Links enthält, sein.",
|
||||||
|
|
|
@ -240,13 +240,13 @@
|
||||||
"behavior": "Behavior",
|
"behavior": "Behavior",
|
||||||
"download_sources": "Download sources",
|
"download_sources": "Download sources",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"real_debrid_api_token": "API Token",
|
"api_token": "API Token",
|
||||||
"enable_real_debrid": "Enable Real-Debrid",
|
"enable_real_debrid": "Enable Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to quickly download files, only limited by your internet speed.",
|
"real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to quickly download files, only limited by your internet speed.",
|
||||||
"real_debrid_invalid_token": "Invalid API token",
|
"debrid_invalid_token": "Invalid API token",
|
||||||
"real_debrid_api_token_hint": "You can get your API token <0>here</0>",
|
"debrid_api_token_hint": "You can get your API token <0>here</0>",
|
||||||
"real_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to Real-Debrid",
|
"real_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to Real-Debrid",
|
||||||
"real_debrid_linked_message": "Account \"{{username}}\" linked",
|
"debrid_linked_message": "Account \"{{username}}\" linked",
|
||||||
"save_changes": "Save changes",
|
"save_changes": "Save changes",
|
||||||
"changes_saved": "Changes successfully saved",
|
"changes_saved": "Changes successfully saved",
|
||||||
"download_sources_description": "Hydra will fetch the download links from these sources. The source URL must be a direct link to a .json file containing the download links.",
|
"download_sources_description": "Hydra will fetch the download links from these sources. The source URL must be a direct link to a .json file containing the download links.",
|
||||||
|
@ -300,7 +300,11 @@
|
||||||
"become_subscriber": "Be Hydra Cloud",
|
"become_subscriber": "Be Hydra Cloud",
|
||||||
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
||||||
"subscription_renews_on": "Your subscription renews on {{date}}",
|
"subscription_renews_on": "Your subscription renews on {{date}}",
|
||||||
"bill_sent_until": "Your next bill will be sent until this day"
|
"bill_sent_until": "Your next bill will be sent until this day",
|
||||||
|
"enable_torbox": "Enable Torbox",
|
||||||
|
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
||||||
|
"torbox_account_linked": "TorBox account linked",
|
||||||
|
"real_debrid_account_linked": "Real-Debrid account linked"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
|
|
|
@ -236,13 +236,13 @@
|
||||||
"behavior": "Otros",
|
"behavior": "Otros",
|
||||||
"download_sources": "Fuentes de descarga",
|
"download_sources": "Fuentes de descarga",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"real_debrid_api_token": "Token API",
|
"api_token": "Token API",
|
||||||
"enable_real_debrid": "Activar Real-Debrid",
|
"enable_real_debrid": "Activar Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid es una forma de descargar sin restricciones archivos instantáneamente con la máxima velocidad de tu internet.",
|
"real_debrid_description": "Real-Debrid es una forma de descargar sin restricciones archivos instantáneamente con la máxima velocidad de tu internet.",
|
||||||
"real_debrid_invalid_token": "Token de API inválido",
|
"debrid_invalid_token": "Token de API inválido",
|
||||||
"real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>",
|
"debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>",
|
||||||
"real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratuita. Por favor, suscríbete a Real-Debrid",
|
"real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratuita. Por favor, suscríbete a Real-Debrid",
|
||||||
"real_debrid_linked_message": "Cuenta \"{{username}}\" vinculada",
|
"debrid_linked_message": "Cuenta \"{{username}}\" vinculada",
|
||||||
"save_changes": "Guardar cambios",
|
"save_changes": "Guardar cambios",
|
||||||
"changes_saved": "Ajustes guardados exitosamente",
|
"changes_saved": "Ajustes guardados exitosamente",
|
||||||
"download_sources_description": "Hydra buscará los enlaces de descarga de estas fuentes. La URL de origen debe ser un enlace directo a un archivo .json que contenga los enlaces de descarga",
|
"download_sources_description": "Hydra buscará los enlaces de descarga de estas fuentes. La URL de origen debe ser un enlace directo a un archivo .json que contenga los enlaces de descarga",
|
||||||
|
|
|
@ -213,13 +213,13 @@
|
||||||
"behavior": "Käitumine",
|
"behavior": "Käitumine",
|
||||||
"download_sources": "Allalaadimise allikad",
|
"download_sources": "Allalaadimise allikad",
|
||||||
"language": "Keel",
|
"language": "Keel",
|
||||||
"real_debrid_api_token": "API Võti",
|
"api_token": "API Võti",
|
||||||
"enable_real_debrid": "Luba Real-Debrid",
|
"enable_real_debrid": "Luba Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid on piiranguteta allalaadija, mis võimaldab sul faile alla laadida koheselt ja sinu internetiühenduse parima kiirusega.",
|
"real_debrid_description": "Real-Debrid on piiranguteta allalaadija, mis võimaldab sul faile alla laadida koheselt ja sinu internetiühenduse parima kiirusega.",
|
||||||
"real_debrid_invalid_token": "Vigane API võti",
|
"debrid_invalid_token": "Vigane API võti",
|
||||||
"real_debrid_api_token_hint": "Sa saad oma API võtme <0>siit</0>",
|
"debrid_api_token_hint": "Sa saad oma API võtme <0>siit</0>",
|
||||||
"real_debrid_free_account_error": "Konto \"{{username}}\" on tasuta konto. Palun telli Real-Debrid",
|
"real_debrid_free_account_error": "Konto \"{{username}}\" on tasuta konto. Palun telli Real-Debrid",
|
||||||
"real_debrid_linked_message": "Konto \"{{username}}\" ühendatud",
|
"debrid_linked_message": "Konto \"{{username}}\" ühendatud",
|
||||||
"save_changes": "Salvesta muudatused",
|
"save_changes": "Salvesta muudatused",
|
||||||
"changes_saved": "Muudatused edukalt salvestatud",
|
"changes_saved": "Muudatused edukalt salvestatud",
|
||||||
"download_sources_description": "Hydra laeb allalaadimise lingid nendest allikatest. Allika URL peab olema otsene link .json failile, mis sisaldab allalaadimise linke.",
|
"download_sources_description": "Hydra laeb allalaadimise lingid nendest allikatest. Allika URL peab olema otsene link .json failile, mis sisaldab allalaadimise linke.",
|
||||||
|
|
|
@ -110,7 +110,7 @@
|
||||||
"general": "کلی",
|
"general": "کلی",
|
||||||
"behavior": "رفتار",
|
"behavior": "رفتار",
|
||||||
"enable_real_debrid": "فعالسازی Real-Debrid",
|
"enable_real_debrid": "فعالسازی Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.",
|
"debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.",
|
||||||
"save_changes": "ذخیره تغییرات"
|
"save_changes": "ذخیره تغییرات"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -161,13 +161,13 @@
|
||||||
"behavior": "Perilaku",
|
"behavior": "Perilaku",
|
||||||
"download_sources": "Sumber unduhan",
|
"download_sources": "Sumber unduhan",
|
||||||
"language": "Bahasa",
|
"language": "Bahasa",
|
||||||
"real_debrid_api_token": "Token API",
|
"api_token": "Token API",
|
||||||
"enable_real_debrid": "Aktifkan Real-Debrid",
|
"enable_real_debrid": "Aktifkan Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid adalah downloader tanpa batas yang memungkinkan kamu untuk mengunduh file dengan cepat dan pada kecepatan terbaik dari Internet kamu.",
|
"real_debrid_description": "Real-Debrid adalah downloader tanpa batas yang memungkinkan kamu untuk mengunduh file dengan cepat dan pada kecepatan terbaik dari Internet kamu.",
|
||||||
"real_debrid_invalid_token": "Token API tidak valid",
|
"debrid_invalid_token": "Token API tidak valid",
|
||||||
"real_debrid_api_token_hint": "Kamu bisa dapatkan token API di <0>sini</0>",
|
"debrid_api_token_hint": "Kamu bisa dapatkan token API di <0>sini</0>",
|
||||||
"real_debrid_free_account_error": "Akun \"{{username}}\" adalah akun gratis. Silakan berlangganan Real-Debrid",
|
"real_debrid_free_account_error": "Akun \"{{username}}\" adalah akun gratis. Silakan berlangganan Real-Debrid",
|
||||||
"real_debrid_linked_message": "Akun \"{{username}}\" terhubung",
|
"debrid_linked_message": "Akun \"{{username}}\" terhubung",
|
||||||
"save_changes": "Simpan perubahan",
|
"save_changes": "Simpan perubahan",
|
||||||
"changes_saved": "Perubahan disimpan berhasil",
|
"changes_saved": "Perubahan disimpan berhasil",
|
||||||
"download_sources_description": "Hydra akan mencari link unduhan dari sini. URL harus menuju file .json dengan link unduhan.",
|
"download_sources_description": "Hydra akan mencari link unduhan dari sini. URL harus menuju file .json dengan link unduhan.",
|
||||||
|
|
|
@ -118,7 +118,7 @@
|
||||||
"general": "Generale",
|
"general": "Generale",
|
||||||
"behavior": "Comportamento",
|
"behavior": "Comportamento",
|
||||||
"enable_real_debrid": "Abilita Real Debrid",
|
"enable_real_debrid": "Abilita Real Debrid",
|
||||||
"real_debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here</0>",
|
"debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here</0>",
|
||||||
"save_changes": "Salva modifiche"
|
"save_changes": "Salva modifiche"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -159,13 +159,13 @@
|
||||||
"behavior": "Мінез-құлық",
|
"behavior": "Мінез-құлық",
|
||||||
"download_sources": "Жүктеу көздері",
|
"download_sources": "Жүктеу көздері",
|
||||||
"language": "Тіл",
|
"language": "Тіл",
|
||||||
"real_debrid_api_token": "API Кілті",
|
"api_token": "API Кілті",
|
||||||
"enable_real_debrid": "Real-Debrid-ті қосу",
|
"enable_real_debrid": "Real-Debrid-ті қосу",
|
||||||
"real_debrid_description": "Real-Debrid - бұл шектеусіз жүктеуші, ол интернетте орналастырылған файлдарды тез жүктеуге немесе жеке желі арқылы кез келген блоктарды айналып өтіп, оларды бірден плеерге беруге мүмкіндік береді.",
|
"real_debrid_description": "Real-Debrid - бұл шектеусіз жүктеуші, ол интернетте орналастырылған файлдарды тез жүктеуге немесе жеке желі арқылы кез келген блоктарды айналып өтіп, оларды бірден плеерге беруге мүмкіндік береді.",
|
||||||
"real_debrid_invalid_token": "Қате API кілті",
|
"debrid_invalid_token": "Қате API кілті",
|
||||||
"real_debrid_api_token_hint": "API кілтін <0>осы жерден</0> алуға болады",
|
"debrid_api_token_hint": "API кілтін <0>осы жерден</0> алуға болады",
|
||||||
"real_debrid_free_account_error": "\"{{username}}\" аккаунты жазылымға ие емес. Real-Debrid жазылымын алыңыз",
|
"real_debrid_free_account_error": "\"{{username}}\" аккаунты жазылымға ие емес. Real-Debrid жазылымын алыңыз",
|
||||||
"real_debrid_linked_message": "\"{{username}}\" аккаунты байланған",
|
"debrid_linked_message": "\"{{username}}\" аккаунты байланған",
|
||||||
"save_changes": "Өзгерістерді сақтау",
|
"save_changes": "Өзгерістерді сақтау",
|
||||||
"changes_saved": "Өзгерістер сәтті сақталды",
|
"changes_saved": "Өзгерістер сәтті сақталды",
|
||||||
"download_sources_description": "Hydra осы көздерден жүктеу сілтемелерін алады. URL-да жүктеу сілтемелері бар .json файлына тікелей сілтеме болуы керек.",
|
"download_sources_description": "Hydra осы көздерден жүктеу сілтемелерін алады. URL-да жүктеу сілтемелері бар .json файлына тікелей сілтеме болуы керек.",
|
||||||
|
|
|
@ -110,7 +110,7 @@
|
||||||
"general": "일반",
|
"general": "일반",
|
||||||
"behavior": "행동",
|
"behavior": "행동",
|
||||||
"enable_real_debrid": "Real-Debrid 활성화",
|
"enable_real_debrid": "Real-Debrid 활성화",
|
||||||
"real_debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.",
|
"debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.",
|
||||||
"save_changes": "변경 사항 저장"
|
"save_changes": "변경 사항 저장"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -177,13 +177,13 @@
|
||||||
"behavior": "Oppførsel",
|
"behavior": "Oppførsel",
|
||||||
"download_sources": "Nedlastingskilder",
|
"download_sources": "Nedlastingskilder",
|
||||||
"language": "Språk",
|
"language": "Språk",
|
||||||
"real_debrid_api_token": "API nøkkel",
|
"api_token": "API nøkkel",
|
||||||
"enable_real_debrid": "Slå på Real-Debrid",
|
"enable_real_debrid": "Slå på Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid er en ubegrenset nedlaster som gør det mulig for deg å laste ned filer med en gang og med den beste utnyttelsen av internethastigheten din.",
|
"real_debrid_description": "Real-Debrid er en ubegrenset nedlaster som gør det mulig for deg å laste ned filer med en gang og med den beste utnyttelsen av internethastigheten din.",
|
||||||
"real_debrid_invalid_token": "Ugyldig API nøkkel",
|
"debrid_invalid_token": "Ugyldig API nøkkel",
|
||||||
"real_debrid_api_token_hint": "Du kan få API nøkkelen din <0>her</0>",
|
"debrid_api_token_hint": "Du kan få API nøkkelen din <0>her</0>",
|
||||||
"real_debrid_free_account_error": "Brukeren \"{{username}}\" er en gratis bruker. Vennligst abboner på Real-Debrid",
|
"real_debrid_free_account_error": "Brukeren \"{{username}}\" er en gratis bruker. Vennligst abboner på Real-Debrid",
|
||||||
"real_debrid_linked_message": "Brukeren \"{{username}}\" er forbunnet",
|
"debrid_linked_message": "Brukeren \"{{username}}\" er forbunnet",
|
||||||
"save_changes": "Lagre endringer",
|
"save_changes": "Lagre endringer",
|
||||||
"changes_saved": "Lagring av endringer vellykket",
|
"changes_saved": "Lagring av endringer vellykket",
|
||||||
"download_sources_description": "Hydra vil hente nedlastingslenker fra disse kildene. Kilde URLen skal være en direkte lenke til en .json fil som inneholder nedlastingslenkene.",
|
"download_sources_description": "Hydra vil hente nedlastingslenker fra disse kildene. Kilde URLen skal være en direkte lenke til en .json fil som inneholder nedlastingslenkene.",
|
||||||
|
|
|
@ -111,7 +111,7 @@
|
||||||
"general": "Algemeen",
|
"general": "Algemeen",
|
||||||
"behavior": "Gedrag",
|
"behavior": "Gedrag",
|
||||||
"enable_real_debrid": "Enable Real-Debrid",
|
"enable_real_debrid": "Enable Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
|
"debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
|
||||||
"save_changes": "Wijzigingen opslaan"
|
"save_changes": "Wijzigingen opslaan"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -119,7 +119,7 @@
|
||||||
"behavior": "Zachowania",
|
"behavior": "Zachowania",
|
||||||
"language": "Język",
|
"language": "Język",
|
||||||
"enable_real_debrid": "Włącz Real-Debrid",
|
"enable_real_debrid": "Włącz Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>",
|
"debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>",
|
||||||
"save_changes": "Zapisz zmiany"
|
"save_changes": "Zapisz zmiany"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -229,13 +229,13 @@
|
||||||
"behavior": "Comportamento",
|
"behavior": "Comportamento",
|
||||||
"download_sources": "Fontes de download",
|
"download_sources": "Fontes de download",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"real_debrid_api_token": "Token de API",
|
"api_token": "Token de API",
|
||||||
"enable_real_debrid": "Habilitar Real-Debrid",
|
"enable_real_debrid": "Habilitar Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "Você pode obter seu token de API <0>aqui</0>",
|
"debrid_api_token_hint": "Você pode obter seu token de API <0>aqui</0>",
|
||||||
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite baixar arquivos instantaneamente e com a melhor velocidade da sua Internet.",
|
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite baixar arquivos instantaneamente e com a melhor velocidade da sua Internet.",
|
||||||
"real_debrid_invalid_token": "Token de API inválido",
|
"debrid_invalid_token": "Token de API inválido",
|
||||||
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, assine a Real-Debrid",
|
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, assine a Real-Debrid",
|
||||||
"real_debrid_linked_message": "Conta \"{{username}}\" vinculada",
|
"debrid_linked_message": "Conta \"{{username}}\" vinculada",
|
||||||
"save_changes": "Salvar mudanças",
|
"save_changes": "Salvar mudanças",
|
||||||
"changes_saved": "Ajustes salvos com sucesso",
|
"changes_saved": "Ajustes salvos com sucesso",
|
||||||
"download_sources_description": "Hydra vai buscar links de download em todas as fontes habilitadas. A URL da fonte deve ser um link direto para um arquivo .json contendo uma lista de links.",
|
"download_sources_description": "Hydra vai buscar links de download em todas as fontes habilitadas. A URL da fonte deve ser um link direto para um arquivo .json contendo uma lista de links.",
|
||||||
|
@ -289,7 +289,11 @@
|
||||||
"become_subscriber": "Seja Hydra Cloud",
|
"become_subscriber": "Seja Hydra Cloud",
|
||||||
"subscription_renew_cancelled": "A renovação automática está desativada",
|
"subscription_renew_cancelled": "A renovação automática está desativada",
|
||||||
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
||||||
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia"
|
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia",
|
||||||
|
"enable_torbox": "Habilitar Torbox",
|
||||||
|
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
|
||||||
|
"torbox_account_linked": "Conta do TorBox vinculada",
|
||||||
|
"real_debrid_account_linked": "Conta Real-Debrid associada"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
|
|
|
@ -205,13 +205,13 @@
|
||||||
"behavior": "Comportamento",
|
"behavior": "Comportamento",
|
||||||
"download_sources": "Fontes de transferência",
|
"download_sources": "Fontes de transferência",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"real_debrid_api_token": "Token de API",
|
"api_token": "Token de API",
|
||||||
"enable_real_debrid": "Ativar Real-Debrid",
|
"enable_real_debrid": "Ativar Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "Podes obter o teu token de API <0>aqui</0>",
|
"debrid_api_token_hint": "Podes obter o teu token de API <0>aqui</0>",
|
||||||
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite descarregar ficheiros instantaneamente e com a melhor velocidade da tua Internet.",
|
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite descarregar ficheiros instantaneamente e com a melhor velocidade da tua Internet.",
|
||||||
"real_debrid_invalid_token": "Token de API inválido",
|
"debrid_invalid_token": "Token de API inválido",
|
||||||
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, subscreve o Real-Debrid",
|
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, subscreve o Real-Debrid",
|
||||||
"real_debrid_linked_message": "Conta \"{{username}}\" associada",
|
"debrid_linked_message": "Conta \"{{username}}\" associada",
|
||||||
"save_changes": "Guardar alterações",
|
"save_changes": "Guardar alterações",
|
||||||
"changes_saved": "Alterações guardadas com sucesso",
|
"changes_saved": "Alterações guardadas com sucesso",
|
||||||
"download_sources_description": "O Hydra vai procurar links de download em todas as fontes ativadas. O URL da fonte deve ser um link direto para um ficheiro .json que contenha uma lista de links.",
|
"download_sources_description": "O Hydra vai procurar links de download em todas as fontes ativadas. O URL da fonte deve ser um link direto para um ficheiro .json que contenha uma lista de links.",
|
||||||
|
|
|
@ -124,13 +124,13 @@
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"behavior": "Comportament",
|
"behavior": "Comportament",
|
||||||
"language": "Limbă",
|
"language": "Limbă",
|
||||||
"real_debrid_api_token": "Token API",
|
"api_token": "Token API",
|
||||||
"enable_real_debrid": "Activează Real-Debrid",
|
"enable_real_debrid": "Activează Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid este un descărcător fără restricții care îți permite să descarci fișiere instantaneu și la cea mai bună viteză a internetului tău.",
|
"real_debrid_description": "Real-Debrid este un descărcător fără restricții care îți permite să descarci fișiere instantaneu și la cea mai bună viteză a internetului tău.",
|
||||||
"real_debrid_invalid_token": "Token API invalid",
|
"debrid_invalid_token": "Token API invalid",
|
||||||
"real_debrid_api_token_hint": "Poți obține token-ul tău API <0>aici</0>",
|
"debrid_api_token_hint": "Poți obține token-ul tău API <0>aici</0>",
|
||||||
"real_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la Real-Debrid",
|
"real_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la Real-Debrid",
|
||||||
"real_debrid_linked_message": "Contul \"{{username}}\" a fost legat",
|
"debrid_linked_message": "Contul \"{{username}}\" a fost legat",
|
||||||
"save_changes": "Salvează modificările",
|
"save_changes": "Salvează modificările",
|
||||||
"changes_saved": "Modificările au fost salvate cu succes"
|
"changes_saved": "Modificările au fost salvate cu succes"
|
||||||
},
|
},
|
||||||
|
|
|
@ -237,13 +237,13 @@
|
||||||
"behavior": "Поведение",
|
"behavior": "Поведение",
|
||||||
"download_sources": "Источники загрузки",
|
"download_sources": "Источники загрузки",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"real_debrid_api_token": "API Ключ",
|
"api_token": "API Ключ",
|
||||||
"enable_real_debrid": "Включить Real-Debrid",
|
"enable_real_debrid": "Включить Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid - это неограниченный загрузчик, который позволяет быстро скачивать файлы, размещенные в Интернете, или мгновенно передавать их в плеер через частную сеть, позволяющую обходить любые блокировки.",
|
"real_debrid_description": "Real-Debrid - это неограниченный загрузчик, который позволяет быстро скачивать файлы, размещенные в Интернете, или мгновенно передавать их в плеер через частную сеть, позволяющую обходить любые блокировки.",
|
||||||
"real_debrid_invalid_token": "Неверный API ключ",
|
"debrid_invalid_token": "Неверный API ключ",
|
||||||
"real_debrid_api_token_hint": "API ключ можно получить <0>здесь</0>",
|
"debrid_api_token_hint": "API ключ можно получить <0>здесь</0>",
|
||||||
"real_debrid_free_account_error": "Аккаунт \"{{username}}\" - не имеет подписки. Пожалуйста, оформите подписку на Real-Debrid",
|
"real_debrid_free_account_error": "Аккаунт \"{{username}}\" - не имеет подписки. Пожалуйста, оформите подписку на Real-Debrid",
|
||||||
"real_debrid_linked_message": "Привязан аккаунт \"{{username}}\"",
|
"debrid_linked_message": "Привязан аккаунт \"{{username}}\"",
|
||||||
"save_changes": "Сохранить изменения",
|
"save_changes": "Сохранить изменения",
|
||||||
"changes_saved": "Изменения успешно сохранены",
|
"changes_saved": "Изменения успешно сохранены",
|
||||||
"download_sources_description": "Hydra будет получать ссылки на загрузки из этих источников. URL должна содержать прямую ссылку на .json-файл с ссылками для загрузок.",
|
"download_sources_description": "Hydra будет получать ссылки на загрузки из этих источников. URL должна содержать прямую ссылку на .json-файл с ссылками для загрузок.",
|
||||||
|
|
|
@ -236,13 +236,13 @@
|
||||||
"behavior": "Davranış",
|
"behavior": "Davranış",
|
||||||
"download_sources": "İndirme kaynakları",
|
"download_sources": "İndirme kaynakları",
|
||||||
"language": "Dil",
|
"language": "Dil",
|
||||||
"real_debrid_api_token": "API Anahtarı",
|
"api_token": "API Anahtarı",
|
||||||
"enable_real_debrid": "Real-Debrid'i Etkinleştir",
|
"enable_real_debrid": "Real-Debrid'i Etkinleştir",
|
||||||
"real_debrid_description": "Real-Debrid, yalnızca internet hızınızla sınırlı olarak hızlı dosya indirmenizi sağlayan sınırsız bir indirici.",
|
"real_debrid_description": "Real-Debrid, yalnızca internet hızınızla sınırlı olarak hızlı dosya indirmenizi sağlayan sınırsız bir indirici.",
|
||||||
"real_debrid_invalid_token": "Geçersiz API anahtarı",
|
"debrid_invalid_token": "Geçersiz API anahtarı",
|
||||||
"real_debrid_api_token_hint": "API anahtarınızı <0>buradan</0> alabilirsiniz",
|
"debrid_api_token_hint": "API anahtarınızı <0>buradan</0> alabilirsiniz",
|
||||||
"real_debrid_free_account_error": "\"{{username}}\" hesabı ücretsiz bir hesaptır. Lütfen Real-Debrid abonesi olun",
|
"real_debrid_free_account_error": "\"{{username}}\" hesabı ücretsiz bir hesaptır. Lütfen Real-Debrid abonesi olun",
|
||||||
"real_debrid_linked_message": "\"{{username}}\" hesabı bağlandı",
|
"debrid_linked_message": "\"{{username}}\" hesabı bağlandı",
|
||||||
"save_changes": "Değişiklikleri Kaydet",
|
"save_changes": "Değişiklikleri Kaydet",
|
||||||
"changes_saved": "Değişiklikler başarıyla kaydedildi",
|
"changes_saved": "Değişiklikler başarıyla kaydedildi",
|
||||||
"download_sources_description": "Hydra, indirme bağlantılarını bu kaynaklardan alacak. Kaynak URL, indirme bağlantılarını içeren bir .json dosyasına doğrudan bir bağlantı olmalıdır.",
|
"download_sources_description": "Hydra, indirme bağlantılarını bu kaynaklardan alacak. Kaynak URL, indirme bağlantılarını içeren bir .json dosyasına doğrudan bir bağlantı olmalıdır.",
|
||||||
|
|
|
@ -174,13 +174,13 @@
|
||||||
"import": "Імпортувати",
|
"import": "Імпортувати",
|
||||||
"insert_valid_json_url": "Вставте дійсний URL JSON-файлу",
|
"insert_valid_json_url": "Вставте дійсний URL JSON-файлу",
|
||||||
"language": "Мова",
|
"language": "Мова",
|
||||||
"real_debrid_api_token": "API-токен",
|
"api_token": "API-токен",
|
||||||
"real_debrid_api_token_hint": "API токен можливо отримати <0>тут</0>",
|
"debrid_api_token_hint": "API токен можливо отримати <0>тут</0>",
|
||||||
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
||||||
"real_debrid_description": "Real-Debrid — це необмежений завантажувач, який дозволяє швидко завантажувати файли, розміщені в Інтернеті, або миттєво передавати їх у плеєр через приватну мережу, що дозволяє обходити будь-які блокування.",
|
"real_debrid_description": "Real-Debrid — це необмежений завантажувач, який дозволяє швидко завантажувати файли, розміщені в Інтернеті, або миттєво передавати їх у плеєр через приватну мережу, що дозволяє обходити будь-які блокування.",
|
||||||
"real_debrid_free_account_error": "Акаунт \"{{username}}\" - не має наявної підписки. Будь ласка, оформіть підписку на Real-Debrid",
|
"real_debrid_free_account_error": "Акаунт \"{{username}}\" - не має наявної підписки. Будь ласка, оформіть підписку на Real-Debrid",
|
||||||
"real_debrid_invalid_token": "Невірний API-токен",
|
"debrid_invalid_token": "Невірний API-токен",
|
||||||
"real_debrid_linked_message": "Акаунт \"{{username}}\" привязаний",
|
"debrid_linked_message": "Акаунт \"{{username}}\" привязаний",
|
||||||
"remove_download_source": "Видалити",
|
"remove_download_source": "Видалити",
|
||||||
"removed_download_source": "Джерело завантажень було видалено",
|
"removed_download_source": "Джерело завантажень було видалено",
|
||||||
"save_changes": "Зберегти зміни",
|
"save_changes": "Зберегти зміни",
|
||||||
|
|
|
@ -213,13 +213,13 @@
|
||||||
"behavior": "行为",
|
"behavior": "行为",
|
||||||
"download_sources": "下载源",
|
"download_sources": "下载源",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"real_debrid_api_token": "API 令牌",
|
"api_token": "API 令牌",
|
||||||
"enable_real_debrid": "启用 Real-Debrid",
|
"enable_real_debrid": "启用 Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid 是一个无限制的下载器,允许您以最快的互联网速度即时下载文件。",
|
"real_debrid_description": "Real-Debrid 是一个无限制的下载器,允许您以最快的互联网速度即时下载文件。",
|
||||||
"real_debrid_invalid_token": "无效的 API 令牌",
|
"debrid_invalid_token": "无效的 API 令牌",
|
||||||
"real_debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.",
|
"debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.",
|
||||||
"real_debrid_free_account_error": "账户 \"{{username}}\" 是免费账户。请订阅 Real-Debrid",
|
"real_debrid_free_account_error": "账户 \"{{username}}\" 是免费账户。请订阅 Real-Debrid",
|
||||||
"real_debrid_linked_message": "账户 \"{{username}}\" 已链接",
|
"debrid_linked_message": "账户 \"{{username}}\" 已链接",
|
||||||
"save_changes": "保存更改",
|
"save_changes": "保存更改",
|
||||||
"changes_saved": "更改已成功保存",
|
"changes_saved": "更改已成功保存",
|
||||||
"download_sources_description": "Hydra 将从这些源获取下载链接。源 URL 必须是直接链接到包含下载链接的 .json 文件。",
|
"download_sources_description": "Hydra 将从这些源获取下载链接。源 URL 必须是直接链接到包含下载链接的 .json 文件。",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
|
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
|
||||||
import { PythonRPC } from "@main/services/python-rpc";
|
|
||||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
@ -25,9 +24,6 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
/* Cancels any ongoing downloads */
|
/* Cancels any ongoing downloads */
|
||||||
DownloadManager.cancelDownload();
|
DownloadManager.cancelDownload();
|
||||||
|
|
||||||
/* Disconnects libtorrent */
|
|
||||||
PythonRPC.kill();
|
|
||||||
|
|
||||||
HydraApi.handleSignOut();
|
HydraApi.handleSignOut();
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|
|
@ -46,6 +46,7 @@ import "./user-preferences/auto-launch";
|
||||||
import "./autoupdater/check-for-updates";
|
import "./autoupdater/check-for-updates";
|
||||||
import "./autoupdater/restart-and-install-update";
|
import "./autoupdater/restart-and-install-update";
|
||||||
import "./user-preferences/authenticate-real-debrid";
|
import "./user-preferences/authenticate-real-debrid";
|
||||||
|
import "./user-preferences/authenticate-torbox";
|
||||||
import "./download-sources/put-download-source";
|
import "./download-sources/put-download-source";
|
||||||
import "./auth/sign-out";
|
import "./auth/sign-out";
|
||||||
import "./auth/open-auth-window";
|
import "./auth/open-auth-window";
|
||||||
|
|
|
@ -46,9 +46,9 @@ const addGameToLibrary = async (
|
||||||
|
|
||||||
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
|
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
|
||||||
|
|
||||||
updateLocalUnlockedAchivements(game!);
|
updateLocalUnlockedAchivements(game);
|
||||||
|
|
||||||
createGame(game!).catch(() => {});
|
createGame(game).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -76,10 +76,10 @@ const startGameDownload = async (
|
||||||
queued: true,
|
queued: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
await downloadsSublevel.put(gameKey, download);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await DownloadManager.startDownload(download);
|
await DownloadManager.startDownload(download).then(() => {
|
||||||
|
return downloadsSublevel.put(gameKey, download);
|
||||||
|
});
|
||||||
|
|
||||||
const updatedGame = await gamesSublevel.get(gameKey);
|
const updatedGame = await gamesSublevel.get(gameKey);
|
||||||
|
|
||||||
|
@ -113,6 +113,10 @@ const startGameDownload = async (
|
||||||
error: DownloadError.RealDebridAccountNotAuthorized,
|
error: DownloadError.RealDebridAccountNotAuthorized,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (downloader === Downloader.TorBox) {
|
||||||
|
return { ok: false, error: err.response?.data?.detail };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
|
|
14
src/main/events/user-preferences/authenticate-torbox.ts
Normal file
14
src/main/events/user-preferences/authenticate-torbox.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { TorBoxClient } from "@main/services/download/torbox";
|
||||||
|
|
||||||
|
const authenticateTorBox = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
apiToken: string
|
||||||
|
) => {
|
||||||
|
TorBoxClient.authorize(apiToken);
|
||||||
|
|
||||||
|
const user = await TorBoxClient.getUser();
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("authenticateTorBox", authenticateTorBox);
|
|
@ -15,6 +15,12 @@ const getUserPreferences = async () =>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userPreferences?.torBoxApiToken) {
|
||||||
|
userPreferences.torBoxApiToken = Crypto.decrypt(
|
||||||
|
userPreferences.torBoxApiToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return userPreferences;
|
return userPreferences;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,10 @@ const updateUserPreferences = async (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preferences.torBoxApiToken) {
|
||||||
|
preferences.torBoxApiToken = Crypto.encrypt(preferences.torBoxApiToken);
|
||||||
|
}
|
||||||
|
|
||||||
if (!preferences.downloadsPath) {
|
if (!preferences.downloadsPath) {
|
||||||
preferences.downloadsPath = null;
|
preferences.downloadsPath = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export { db } from "./level";
|
export { db } from "./level";
|
||||||
|
|
||||||
export * from "./sublevels";
|
export * from "./sublevels";
|
||||||
|
|
|
@ -2,5 +2,4 @@ export * from "./downloads";
|
||||||
export * from "./games";
|
export * from "./games";
|
||||||
export * from "./game-shop-cache";
|
export * from "./game-shop-cache";
|
||||||
export * from "./game-achievements";
|
export * from "./game-achievements";
|
||||||
|
|
||||||
export * from "./keys";
|
export * from "./keys";
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
} from "./level";
|
} from "./level";
|
||||||
import { Auth, User, type UserPreferences } from "@types";
|
import { Auth, User, type UserPreferences } from "@types";
|
||||||
import { knexClient } from "./knex-client";
|
import { knexClient } from "./knex-client";
|
||||||
|
import { TorBoxClient } from "./services/download/torbox";
|
||||||
|
|
||||||
export const loadState = async () => {
|
export const loadState = async () => {
|
||||||
const userPreferences = await migrateFromSqlite().then(async () => {
|
const userPreferences = await migrateFromSqlite().then(async () => {
|
||||||
|
@ -42,6 +43,10 @@ export const loadState = async () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userPreferences?.torBoxApiToken) {
|
||||||
|
TorBoxClient.authorize(Crypto.decrypt(userPreferences.torBoxApiToken));
|
||||||
|
}
|
||||||
|
|
||||||
Ludusavi.addManifestToLudusaviConfig();
|
Ludusavi.addManifestToLudusaviConfig();
|
||||||
|
|
||||||
HydraApi.setupApi().then(() => {
|
HydraApi.setupApi().then(() => {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import path from "path";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
|
import { TorBoxClient } from "./torbox";
|
||||||
|
|
||||||
export class DownloadManager {
|
export class DownloadManager {
|
||||||
private static downloadingGameId: string | null = null;
|
private static downloadingGameId: string | null = null;
|
||||||
|
@ -233,7 +234,9 @@ export class DownloadManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
|
|
||||||
if (downloadKey === this.downloadingGameId) {
|
if (downloadKey === this.downloadingGameId) {
|
||||||
|
WindowManager.mainWindow?.webContents.send("on-download-progress", null);
|
||||||
this.downloadingGameId = null;
|
this.downloadingGameId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,6 +278,7 @@ export class DownloadManager {
|
||||||
}
|
}
|
||||||
case Downloader.PixelDrain: {
|
case Downloader.PixelDrain: {
|
||||||
const id = download.uri.split("/").pop();
|
const id = download.uri.split("/").pop();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
game_id: downloadId,
|
game_id: downloadId,
|
||||||
|
@ -329,6 +333,18 @@ export class DownloadManager {
|
||||||
save_path: download.downloadPath,
|
save_path: download.downloadPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case Downloader.TorBox: {
|
||||||
|
const { name, url } = await TorBoxClient.getDownloadInfo(download.uri);
|
||||||
|
|
||||||
|
if (!url) return;
|
||||||
|
return {
|
||||||
|
action: "start",
|
||||||
|
game_id: downloadId,
|
||||||
|
url,
|
||||||
|
save_path: download.downloadPath,
|
||||||
|
out: name,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,24 +6,23 @@ import type {
|
||||||
TorBoxAddTorrentRequest,
|
TorBoxAddTorrentRequest,
|
||||||
TorBoxRequestLinkRequest,
|
TorBoxRequestLinkRequest,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import { logger } from "../logger";
|
|
||||||
|
|
||||||
export class TorBoxClient {
|
export class TorBoxClient {
|
||||||
private static instance: AxiosInstance;
|
private static instance: AxiosInstance;
|
||||||
private static readonly baseURL = "https://api.torbox.app/v1/api";
|
private static readonly baseURL = "https://api.torbox.app/v1/api";
|
||||||
public static apiToken: string;
|
private static apiToken: string;
|
||||||
|
|
||||||
static authorize(apiToken: string) {
|
static authorize(apiToken: string) {
|
||||||
|
this.apiToken = apiToken;
|
||||||
this.instance = axios.create({
|
this.instance = axios.create({
|
||||||
baseURL: this.baseURL,
|
baseURL: this.baseURL,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${apiToken}`,
|
Authorization: `Bearer ${apiToken}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.apiToken = apiToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async addMagnet(magnet: string) {
|
private static async addMagnet(magnet: string) {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append("magnet", magnet);
|
form.append("magnet", magnet);
|
||||||
|
|
||||||
|
@ -32,6 +31,10 @@ export class TorBoxClient {
|
||||||
form
|
form
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!response.data.success) {
|
||||||
|
throw new Error(response.data.detail);
|
||||||
|
}
|
||||||
|
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,22 +58,16 @@ export class TorBoxClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async requestLink(id: number) {
|
static async requestLink(id: number) {
|
||||||
const searchParams = new URLSearchParams({});
|
const searchParams = new URLSearchParams({
|
||||||
|
token: this.apiToken,
|
||||||
searchParams.set("token", this.apiToken);
|
torrent_id: id.toString(),
|
||||||
searchParams.set("torrent_id", id.toString());
|
zip_link: "true",
|
||||||
searchParams.set("zip_link", "true");
|
});
|
||||||
|
|
||||||
const response = await this.instance.get<TorBoxRequestLinkRequest>(
|
const response = await this.instance.get<TorBoxRequestLinkRequest>(
|
||||||
"/torrents/requestdl?" + searchParams.toString()
|
"/torrents/requestdl?" + searchParams.toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
logger.error(response.data.error);
|
|
||||||
logger.error(response.data.detail);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +78,7 @@ export class TorBoxClient {
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getTorrentId(magnetUri: string) {
|
private static async getTorrentIdAndName(magnetUri: string) {
|
||||||
const userTorrents = await this.getAllTorrentsFromUser();
|
const userTorrents = await this.getAllTorrentsFromUser();
|
||||||
|
|
||||||
const { infoHash } = await parseTorrent(magnetUri);
|
const { infoHash } = await parseTorrent(magnetUri);
|
||||||
|
@ -89,9 +86,18 @@ export class TorBoxClient {
|
||||||
(userTorrent) => userTorrent.hash === infoHash
|
(userTorrent) => userTorrent.hash === infoHash
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userTorrent) return userTorrent.id;
|
if (userTorrent) return { id: userTorrent.id, name: userTorrent.name };
|
||||||
|
|
||||||
const torrent = await this.addMagnet(magnetUri);
|
const torrent = await this.addMagnet(magnetUri);
|
||||||
return torrent.torrent_id;
|
return { id: torrent.torrent_id, name: torrent.name };
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getDownloadInfo(uri: string) {
|
||||||
|
const torrentData = await this.getTorrentIdAndName(uri);
|
||||||
|
const url = await this.requestLink(torrentData.id);
|
||||||
|
|
||||||
|
const name = torrentData.name ? `${torrentData.name}.zip` : undefined;
|
||||||
|
|
||||||
|
return { url, name };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,22 +35,20 @@ export const mergeWithRemoteGames = async () => {
|
||||||
name: "getById",
|
name: "getById",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (steamGame) {
|
const iconUrl = steamGame?.clientIcon
|
||||||
const iconUrl = steamGame?.clientIcon
|
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
||||||
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
: null;
|
||||||
: null;
|
|
||||||
|
|
||||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||||
objectId: game.objectId,
|
objectId: game.objectId,
|
||||||
title: steamGame?.name,
|
title: steamGame?.name,
|
||||||
remoteId: game.id,
|
remoteId: game.id,
|
||||||
shop: game.shop,
|
shop: game.shop,
|
||||||
iconUrl,
|
iconUrl,
|
||||||
lastTimePlayed: game.lastTimePlayed,
|
lastTimePlayed: game.lastTimePlayed,
|
||||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,7 +15,7 @@ export const uploadGamesBatch = async () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const gamesChunks = chunk(games, 200);
|
const gamesChunks = chunk(games, 50);
|
||||||
|
|
||||||
for (const chunk of gamesChunks) {
|
for (const chunk of gamesChunks) {
|
||||||
await HydraApi.post(
|
await HydraApi.post(
|
||||||
|
|
|
@ -21,11 +21,18 @@ export const getSteamAppDetails = async (
|
||||||
});
|
});
|
||||||
|
|
||||||
return axios
|
return axios
|
||||||
.get(
|
.get<SteamAppDetailsResponse>(
|
||||||
`http://store.steampowered.com/api/appdetails?${searchParams.toString()}`
|
`http://store.steampowered.com/api/appdetails?${searchParams.toString()}`
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.data[objectId].success) return response.data[objectId].data;
|
if (response.data[objectId].success) {
|
||||||
|
const data = response.data[objectId].data;
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
objectId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|
|
@ -32,10 +32,10 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
ipcRenderer.invoke("pauseGameSeed", shop, objectId),
|
ipcRenderer.invoke("pauseGameSeed", shop, objectId),
|
||||||
resumeGameSeed: (shop: GameShop, objectId: string) =>
|
resumeGameSeed: (shop: GameShop, objectId: string) =>
|
||||||
ipcRenderer.invoke("resumeGameSeed", shop, objectId),
|
ipcRenderer.invoke("resumeGameSeed", shop, objectId),
|
||||||
onDownloadProgress: (cb: (value: DownloadProgress) => void) => {
|
onDownloadProgress: (cb: (value: DownloadProgress | null) => void) => {
|
||||||
const listener = (
|
const listener = (
|
||||||
_event: Electron.IpcRendererEvent,
|
_event: Electron.IpcRendererEvent,
|
||||||
value: DownloadProgress
|
value: DownloadProgress | null
|
||||||
) => cb(value);
|
) => cb(value);
|
||||||
ipcRenderer.on("on-download-progress", listener);
|
ipcRenderer.on("on-download-progress", listener);
|
||||||
return () => ipcRenderer.removeListener("on-download-progress", listener);
|
return () => ipcRenderer.removeListener("on-download-progress", listener);
|
||||||
|
@ -92,6 +92,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
ipcRenderer.invoke("autoLaunch", autoLaunchProps),
|
ipcRenderer.invoke("autoLaunch", autoLaunchProps),
|
||||||
authenticateRealDebrid: (apiToken: string) =>
|
authenticateRealDebrid: (apiToken: string) =>
|
||||||
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
|
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
|
||||||
|
authenticateTorBox: (apiToken: string) =>
|
||||||
|
ipcRenderer.invoke("authenticateTorBox", apiToken),
|
||||||
|
|
||||||
/* Download sources */
|
/* Download sources */
|
||||||
putDownloadSource: (objectIds: string[]) =>
|
putDownloadSource: (objectIds: string[]) =>
|
||||||
|
|
|
@ -1,134 +0,0 @@
|
||||||
import {
|
|
||||||
ComplexStyleRule,
|
|
||||||
createContainer,
|
|
||||||
globalStyle,
|
|
||||||
style,
|
|
||||||
} from "@vanilla-extract/css";
|
|
||||||
import { SPACING_UNIT, vars } from "./theme.css";
|
|
||||||
|
|
||||||
export const appContainer = createContainer();
|
|
||||||
|
|
||||||
globalStyle("*", {
|
|
||||||
boxSizing: "border-box",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("::-webkit-scrollbar", {
|
|
||||||
width: "9px",
|
|
||||||
backgroundColor: vars.color.darkBackground,
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("::-webkit-scrollbar-track", {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.03)",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("::-webkit-scrollbar-thumb", {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
|
||||||
borderRadius: "24px",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("::-webkit-scrollbar-thumb:hover", {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.16)",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("html, body, #root, main", {
|
|
||||||
height: "100%",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("body", {
|
|
||||||
overflow: "hidden",
|
|
||||||
userSelect: "none",
|
|
||||||
fontFamily: "Noto Sans, sans-serif",
|
|
||||||
fontSize: vars.size.body,
|
|
||||||
color: vars.color.body,
|
|
||||||
margin: "0",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("button", {
|
|
||||||
padding: "0",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
border: "none",
|
|
||||||
fontFamily: "inherit",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("h1, h2, h3, h4, h5, h6, p", {
|
|
||||||
margin: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("p", {
|
|
||||||
lineHeight: "20px",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("#root, main", {
|
|
||||||
display: "flex",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("#root", {
|
|
||||||
flexDirection: "column",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("main", {
|
|
||||||
overflow: "hidden",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle(
|
|
||||||
"input::-webkit-outer-spin-button, input::-webkit-inner-spin-button",
|
|
||||||
{
|
|
||||||
WebkitAppearance: "none",
|
|
||||||
margin: "0",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
globalStyle("label", {
|
|
||||||
fontSize: vars.size.body,
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("input[type=number]", {
|
|
||||||
MozAppearance: "textfield",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("img", {
|
|
||||||
WebkitUserDrag: "none",
|
|
||||||
} as Record<string, string>);
|
|
||||||
|
|
||||||
globalStyle("progress[value]", {
|
|
||||||
WebkitAppearance: "none",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const container = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
overflow: "hidden",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
containerName: appContainer,
|
|
||||||
containerType: "inline-size",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const content = style({
|
|
||||||
overflowY: "auto",
|
|
||||||
alignItems: "center",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
position: "relative",
|
|
||||||
height: "100%",
|
|
||||||
background: `linear-gradient(0deg, ${vars.color.darkBackground} 50%, ${vars.color.background} 100%)`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const titleBar = style({
|
|
||||||
display: "flex",
|
|
||||||
width: "100%",
|
|
||||||
height: "35px",
|
|
||||||
minHeight: "35px",
|
|
||||||
backgroundColor: vars.color.darkBackground,
|
|
||||||
alignItems: "center",
|
|
||||||
padding: `0 ${SPACING_UNIT * 2}px`,
|
|
||||||
WebkitAppRegion: "drag",
|
|
||||||
zIndex: vars.zIndex.titleBar,
|
|
||||||
borderBottom: `1px solid ${vars.color.border}`,
|
|
||||||
} as ComplexStyleRule);
|
|
||||||
|
|
||||||
export const cloudText = style({
|
|
||||||
background: "linear-gradient(270deg, #16B195 50%, #3E62C0 100%)",
|
|
||||||
backgroundClip: "text",
|
|
||||||
color: "transparent",
|
|
||||||
});
|
|
136
src/renderer/src/app.scss
Normal file
136
src/renderer/src/app.scss
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
@use "./scss/globals.scss";
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 9px;
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root,
|
||||||
|
main {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
font-family:
|
||||||
|
Noto Sans,
|
||||||
|
sans-serif;
|
||||||
|
font-size: globals.$body-font-size;
|
||||||
|
color: globals.$body-color;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root,
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: globals.$body-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress[value] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
container-name: globals.$app-container;
|
||||||
|
container-type: inline-size;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
overflow-y: auto;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
globals.$dark-background-color 50%,
|
||||||
|
globals.$background-color 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 35px;
|
||||||
|
min-height: 35px;
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 calc(globals.$spacing-unit * 2);
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
z-index: 4;
|
||||||
|
border-bottom: 1px solid globals.$border-color;
|
||||||
|
|
||||||
|
&__cloud-text {
|
||||||
|
background: linear-gradient(270deg, #16b195 50%, #3e62c0 100%);
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,8 +12,6 @@ import {
|
||||||
useUserDetails,
|
useUserDetails,
|
||||||
} from "@renderer/hooks";
|
} from "@renderer/hooks";
|
||||||
|
|
||||||
import * as styles from "./app.css";
|
|
||||||
|
|
||||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
setUserPreferences,
|
setUserPreferences,
|
||||||
|
@ -29,7 +27,8 @@ import { downloadSourcesWorker } from "./workers";
|
||||||
import { downloadSourcesTable } from "./dexie";
|
import { downloadSourcesTable } from "./dexie";
|
||||||
import { useSubscription } from "./hooks/use-subscription";
|
import { useSubscription } from "./hooks/use-subscription";
|
||||||
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
||||||
import { SPACING_UNIT } from "./theme.css";
|
|
||||||
|
import "./app.scss";
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -85,7 +84,7 @@ export function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onDownloadProgress(
|
const unsubscribe = window.electron.onDownloadProgress(
|
||||||
(downloadProgress) => {
|
(downloadProgress) => {
|
||||||
if (downloadProgress.progress === 1) {
|
if (downloadProgress?.progress === 1) {
|
||||||
clearDownload();
|
clearDownload();
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
return;
|
return;
|
||||||
|
@ -257,34 +256,24 @@ export function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{window.electron.platform === "win32" && (
|
{window.electron.platform === "win32" && (
|
||||||
<div className={styles.titleBar}>
|
<div className="title-bar">
|
||||||
<h4>
|
<h4>
|
||||||
Hydra
|
Hydra
|
||||||
{hasActiveSubscription && (
|
{hasActiveSubscription && (
|
||||||
<span className={styles.cloudText}> Cloud</span>
|
<span className="title-bar__cloud-text"> Cloud</span>
|
||||||
)}
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<Toast
|
||||||
style={{
|
visible={toast.visible}
|
||||||
position: "absolute",
|
title={toast.title}
|
||||||
bottom: `${26 + SPACING_UNIT * 2}px`,
|
message={toast.message}
|
||||||
right: "16px",
|
type={toast.type}
|
||||||
maxWidth: "420px",
|
onClose={handleToastClose}
|
||||||
width: "420px",
|
duration={toast.duration}
|
||||||
}}
|
/>
|
||||||
>
|
|
||||||
<Toast
|
|
||||||
visible={toast.visible}
|
|
||||||
title={toast.title}
|
|
||||||
message={toast.message}
|
|
||||||
type={toast.type}
|
|
||||||
onClose={handleToastClose}
|
|
||||||
duration={toast.duration}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<HydraCloudModal
|
<HydraCloudModal
|
||||||
visible={isHydraCloudModalVisible}
|
visible={isHydraCloudModalVisible}
|
||||||
|
@ -304,10 +293,10 @@ export function App() {
|
||||||
<main>
|
<main>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
<article className={styles.container}>
|
<article className="container">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<section ref={contentRef} className={styles.content}>
|
<section ref={contentRef} className="container__content">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
BIN
src/renderer/src/assets/icons/torbox.webp
Normal file
BIN
src/renderer/src/assets/icons/torbox.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -1,54 +0,0 @@
|
||||||
import { keyframes } from "@vanilla-extract/css";
|
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
|
|
||||||
export const backdropFadeIn = keyframes({
|
|
||||||
"0%": { backdropFilter: "blur(0px)", backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
|
||||||
"100%": {
|
|
||||||
backdropFilter: "blur(2px)",
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const backdropFadeOut = keyframes({
|
|
||||||
"0%": { backdropFilter: "blur(2px)", backgroundColor: "rgba(0, 0, 0, 0.7)" },
|
|
||||||
"100%": {
|
|
||||||
backdropFilter: "blur(0px)",
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const backdrop = recipe({
|
|
||||||
base: {
|
|
||||||
animationName: backdropFadeIn,
|
|
||||||
animationDuration: "0.4s",
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
|
||||||
position: "absolute",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
zIndex: vars.zIndex.backdrop,
|
|
||||||
top: "0",
|
|
||||||
padding: `${SPACING_UNIT * 3}px`,
|
|
||||||
backdropFilter: "blur(2px)",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
closing: {
|
|
||||||
true: {
|
|
||||||
animationName: backdropFadeOut,
|
|
||||||
backdropFilter: "blur(0px)",
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
windows: {
|
|
||||||
true: {
|
|
||||||
// SPACING_UNIT * 3 + title bar spacing
|
|
||||||
paddingTop: `${SPACING_UNIT * 3 + 35}px`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
50
src/renderer/src/components/backdrop/backdrop.scss
Normal file
50
src/renderer/src/components/backdrop/backdrop.scss
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
animation-name: backdrop-fade-in;
|
||||||
|
animation-duration: 0.4s;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: globals.$backdrop-z-index;
|
||||||
|
top: 0;
|
||||||
|
padding: calc(globals.$spacing-unit * 3);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
|
&--closing {
|
||||||
|
animation-name: backdrop-fade-out;
|
||||||
|
backdrop-filter: blur(0px);
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--windows {
|
||||||
|
padding-top: calc(#{globals.$spacing-unit * 3} + 35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes backdrop-fade-in {
|
||||||
|
0% {
|
||||||
|
backdrop-filter: blur(0px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes backdrop-fade-out {
|
||||||
|
0% {
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
backdrop-filter: blur(0px);
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import * as styles from "./backdrop.css";
|
import "./backdrop.scss";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
export interface BackdropProps {
|
export interface BackdropProps {
|
||||||
isClosing?: boolean;
|
isClosing?: boolean;
|
||||||
|
@ -8,9 +9,9 @@ export interface BackdropProps {
|
||||||
export function Backdrop({ isClosing = false, children }: BackdropProps) {
|
export function Backdrop({ isClosing = false, children }: BackdropProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.backdrop({
|
className={cn("backdrop", {
|
||||||
closing: isClosing,
|
"backdrop--closing": isClosing,
|
||||||
windows: window.electron.platform === "win32",
|
"backdrop--windows": window.electron.platform === "win32",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -7,5 +7,6 @@
|
||||||
border: solid 1px globals.$muted-color;
|
border: solid 1px globals.$muted-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,6 @@ export function BottomPanel() {
|
||||||
|
|
||||||
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
|
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
|
||||||
|
|
||||||
const isGameDownloading = !!lastPacket;
|
|
||||||
|
|
||||||
const [version, setVersion] = useState("");
|
const [version, setVersion] = useState("");
|
||||||
const [sessionHash, setSessionHash] = useState<null | string>("");
|
const [sessionHash, setSessionHash] = useState<null | string>("");
|
||||||
|
|
||||||
|
@ -33,9 +31,11 @@ export function BottomPanel() {
|
||||||
}, [userDetails?.id]);
|
}, [userDetails?.id]);
|
||||||
|
|
||||||
const status = useMemo(() => {
|
const status = useMemo(() => {
|
||||||
if (isGameDownloading) {
|
const game = lastPacket
|
||||||
const game = library.find((game) => game.id === lastPacket?.gameId)!;
|
? library.find((game) => game.id === lastPacket?.gameId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (game) {
|
||||||
if (lastPacket?.isCheckingFiles)
|
if (lastPacket?.isCheckingFiles)
|
||||||
return t("checking_files", {
|
return t("checking_files", {
|
||||||
title: game.title,
|
title: game.title,
|
||||||
|
@ -64,7 +64,7 @@ export function BottomPanel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return t("no_downloads_in_progress");
|
return t("no_downloads_in_progress");
|
||||||
}, [t, isGameDownloading, library, lastPacket, progress, eta, downloadSpeed]);
|
}, [t, library, lastPacket, progress, eta, downloadSpeed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="bottom-panel">
|
<footer className="bottom-panel">
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
|
|
||||||
export const checkboxField = style({
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
cursor: "pointer",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const checkbox = recipe({
|
|
||||||
base: {
|
|
||||||
width: "20px",
|
|
||||||
height: "20px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
backgroundColor: vars.color.darkBackground,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
position: "relative",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
minWidth: "20px",
|
|
||||||
minHeight: "20px",
|
|
||||||
color: vars.color.darkBackground,
|
|
||||||
":hover": {
|
|
||||||
borderColor: "rgba(255, 255, 255, 0.5)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
checked: {
|
|
||||||
true: {
|
|
||||||
backgroundColor: vars.color.muted,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const checkboxInput = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
position: "absolute",
|
|
||||||
margin: "0",
|
|
||||||
padding: "0",
|
|
||||||
opacity: "0",
|
|
||||||
cursor: "pointer",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const checkboxLabel = style({
|
|
||||||
cursor: "pointer",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
overflow: "hidden",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
});
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.checkbox-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:has(input:disabled) {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: globals.$disabled-opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__checkbox {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
min-height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
|
||||||
|
&:hover:not(:has(input:disabled)) {
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
cursor: pointer;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:has(+ input:disabled) {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { useId } from "react";
|
import { useId } from "react";
|
||||||
import * as styles from "./checkbox-field.css";
|
|
||||||
import { CheckIcon } from "@primer/octicons-react";
|
import { CheckIcon } from "@primer/octicons-react";
|
||||||
|
import "./checkbox-field.scss";
|
||||||
|
|
||||||
export interface CheckboxFieldProps
|
export interface CheckboxFieldProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
|
@ -14,17 +14,19 @@ export function CheckboxField({ label, ...props }: CheckboxFieldProps) {
|
||||||
const id = useId();
|
const id = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.checkboxField}>
|
<div className="checkbox-field">
|
||||||
<div className={styles.checkbox({ checked: props.checked })}>
|
<div
|
||||||
|
className={`checkbox-field__checkbox ${props.checked ? "checked" : ""}`}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className={styles.checkboxInput}
|
className="checkbox-field__input"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{props.checked && <CheckIcon />}
|
{props.checked && <CheckIcon />}
|
||||||
</div>
|
</div>
|
||||||
<label htmlFor={id} className={styles.checkboxLabel}>
|
<label htmlFor={id} className="checkbox-field__label">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { SPACING_UNIT } from "../../theme.css";
|
|
||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
|
|
||||||
export const actions = style({
|
|
||||||
display: "flex",
|
|
||||||
alignSelf: "flex-end",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const descriptionText = style({
|
|
||||||
fontSize: "16px",
|
|
||||||
lineHeight: "24px",
|
|
||||||
});
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.confirmation-modal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-self: flex-end;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
&__description {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { Button } from "../button/button";
|
import { Button } from "../button/button";
|
||||||
import { Modal, type ModalProps } from "../modal/modal";
|
import { Modal, type ModalProps } from "../modal/modal";
|
||||||
|
|
||||||
import * as styles from "./confirmation-modal.css";
|
import "./confirmation-modal.scss";
|
||||||
|
|
||||||
export interface ConfirmationModalProps extends Omit<ModalProps, "children"> {
|
export interface ConfirmationModalProps extends Omit<ModalProps, "children"> {
|
||||||
confirmButtonLabel: string;
|
confirmButtonLabel: string;
|
||||||
|
@ -31,10 +31,10 @@ export function ConfirmationModal({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal {...props}>
|
<Modal {...props}>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
<div className="confirmation-modal">
|
||||||
<p className={styles.descriptionText}>{descriptionText}</p>
|
<p className="confirmation-modal__description">{descriptionText}</p>
|
||||||
|
|
||||||
<div className={styles.actions}>
|
<div className="confirmation-modal__actions">
|
||||||
<Button theme="outline" onClick={handleCancelClick}>
|
<Button theme="outline" onClick={handleCancelClick}>
|
||||||
{cancelButtonLabel}
|
{cancelButtonLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,106 +0,0 @@
|
||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
|
|
||||||
export const card = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "180px",
|
|
||||||
boxShadow: "0px 0px 15px 0px #000000",
|
|
||||||
overflow: "hidden",
|
|
||||||
borderRadius: "4px",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
cursor: "pointer",
|
|
||||||
zIndex: "1",
|
|
||||||
":active": {
|
|
||||||
opacity: vars.opacity.active,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const backdrop = style({
|
|
||||||
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.7) 50%, transparent 100%)",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
flexDirection: "column",
|
|
||||||
position: "relative",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const cover = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
objectFit: "cover",
|
|
||||||
objectPosition: "center",
|
|
||||||
position: "absolute",
|
|
||||||
zIndex: "-1",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
selectors: {
|
|
||||||
[`${card}:hover &`]: {
|
|
||||||
transform: "scale(1.05)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const content = style({
|
|
||||||
color: "#DADBE1",
|
|
||||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
flexDirection: "column",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
transform: "translateY(24px)",
|
|
||||||
selectors: {
|
|
||||||
[`${card}:hover &`]: {
|
|
||||||
transform: "translateY(0px)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const title = style({
|
|
||||||
fontSize: "16px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
textAlign: "left",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadOptions = style({
|
|
||||||
display: "flex",
|
|
||||||
margin: "0",
|
|
||||||
padding: "0",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
listStyle: "none",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const specifics = style({
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
justifyContent: "center",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const specificsItem = style({
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
display: "flex",
|
|
||||||
color: vars.color.muted,
|
|
||||||
fontSize: "12px",
|
|
||||||
alignItems: "flex-end",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const titleContainer = style({
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
color: vars.color.muted,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const shopIcon = style({
|
|
||||||
width: "20px",
|
|
||||||
height: "20px",
|
|
||||||
minWidth: "20px",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const noDownloadsLabel = style({
|
|
||||||
color: vars.color.body,
|
|
||||||
fontWeight: "bold",
|
|
||||||
});
|
|
102
src/renderer/src/components/game-card/game-card.scss
Normal file
102
src/renderer/src/components/game-card/game-card.scss
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.game-card {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
box-shadow: 0px 0px 15px 0px #000000;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: globals.$active-opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__backdrop {
|
||||||
|
background: linear-gradient(0deg, rgba(0, 0, 0, 0.7) 50%, transparent 100%);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
color: #dadbe1;
|
||||||
|
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
transform: translateY(24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__download-options {
|
||||||
|
display: flex;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__specifics {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__specifics-item {
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
display: flex;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
font-size: 12px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__shop-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-download-label {
|
||||||
|
color: globals.$body-color;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover &__cover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
&:hover &__content {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,8 @@ import type { GameStats } from "@types";
|
||||||
|
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
|
|
||||||
import * as styles from "./game-card.css";
|
import "./game-card.scss";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Badge } from "../badge/badge";
|
import { Badge } from "../badge/badge";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
@ -19,7 +20,7 @@ export interface GameCardProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const shopIcon = {
|
const shopIcon = {
|
||||||
steam: <SteamLogo className={styles.shopIcon} />,
|
steam: <SteamLogo className="game-card__shop-icon" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function GameCard({ game, ...props }: GameCardProps) {
|
export function GameCard({ game, ...props }: GameCardProps) {
|
||||||
|
@ -48,25 +49,25 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.card}
|
className="game-card"
|
||||||
onMouseEnter={handleHover}
|
onMouseEnter={handleHover}
|
||||||
>
|
>
|
||||||
<div className={styles.backdrop}>
|
<div className="game-card__backdrop">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.library(game.objectId)}
|
src={steamUrlBuilder.library(game.objectId)}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
className={styles.cover}
|
className="game-card__cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className="game-card__content">
|
||||||
<div className={styles.titleContainer}>
|
<div className="game-card__title-container">
|
||||||
{shopIcon[game.shop]}
|
{shopIcon[game.shop]}
|
||||||
<p className={styles.title}>{game.title}</p>
|
<p className="game-card__title">{game.title}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{uniqueRepackers.length > 0 ? (
|
{uniqueRepackers.length > 0 ? (
|
||||||
<ul className={styles.downloadOptions}>
|
<ul className="game-card__download-options">
|
||||||
{uniqueRepackers.map((repacker) => (
|
{uniqueRepackers.map((repacker) => (
|
||||||
<li key={repacker}>
|
<li key={repacker}>
|
||||||
<Badge>{repacker}</Badge>
|
<Badge>{repacker}</Badge>
|
||||||
|
@ -74,17 +75,17 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<p className={styles.noDownloadsLabel}>{t("no_downloads")}</p>
|
<p className="game-card__no-download-label">{t("no_downloads")}</p>
|
||||||
)}
|
)}
|
||||||
<div className={styles.specifics}>
|
<div className="game-card__specifics">
|
||||||
<div className={styles.specificsItem}>
|
<div className="game-card__specifics-item">
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
<span>
|
<span>
|
||||||
{stats ? numberFormatter.format(stats.downloadCount) : "…"}
|
{stats ? numberFormatter.format(stats.downloadCount) : "…"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.specificsItem}>
|
<div className="game-card__specifics-item">
|
||||||
<PeopleIcon />
|
<PeopleIcon />
|
||||||
<span>
|
<span>
|
||||||
{stats ? numberFormatter.format(stats?.playerCount) : "…"}
|
{stats ? numberFormatter.format(stats?.playerCount) : "…"}
|
||||||
|
|
32
src/renderer/src/components/header/auto-update-header.scss
Normal file
32
src/renderer/src/components/header/auto-update-header.scss
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.auto-update-sub-header {
|
||||||
|
border-bottom: solid 1px globals.$body-color;
|
||||||
|
padding: calc(globals.$spacing-unit / 2) calc(globals.$spacing-unit * 3);
|
||||||
|
|
||||||
|
&__new-version-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
color: #8e919b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__new-version-icon {
|
||||||
|
color: globals.$success-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__new-version-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
color: globals.$body-color;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SyncIcon } from "@primer/octicons-react";
|
import { SyncIcon } from "@primer/octicons-react";
|
||||||
import { Link } from "../link/link";
|
import { Link } from "../link/link";
|
||||||
import * as styles from "./header.css";
|
import "./auto-update-header.scss";
|
||||||
import type { AppUpdaterEvent } from "@types";
|
import type { AppUpdaterEvent } from "@types";
|
||||||
|
|
||||||
export const releasesPageUrl =
|
export const releasesPageUrl =
|
||||||
|
@ -45,9 +45,15 @@ export function AutoUpdateSubHeader() {
|
||||||
|
|
||||||
if (!isAutoInstallAvailable) {
|
if (!isAutoInstallAvailable) {
|
||||||
return (
|
return (
|
||||||
<header className={styles.subheader}>
|
<header className="auto-update-sub-header">
|
||||||
<Link to={releasesPageUrl} className={styles.newVersionLink}>
|
<Link
|
||||||
<SyncIcon className={styles.newVersionIcon} size={12} />
|
to={releasesPageUrl}
|
||||||
|
className="auto-update-sub-header__new-version-link"
|
||||||
|
>
|
||||||
|
<SyncIcon
|
||||||
|
className="auto-update-sub-header__new-version-icon"
|
||||||
|
size={12}
|
||||||
|
/>
|
||||||
{t("version_available_download", { version: newVersion })}
|
{t("version_available_download", { version: newVersion })}
|
||||||
</Link>
|
</Link>
|
||||||
</header>
|
</header>
|
||||||
|
@ -56,13 +62,16 @@ export function AutoUpdateSubHeader() {
|
||||||
|
|
||||||
if (isReadyToInstall) {
|
if (isReadyToInstall) {
|
||||||
return (
|
return (
|
||||||
<header className={styles.subheader}>
|
<header className="auto-update-sub-header">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.newVersionButton}
|
className="auto-update-sub-header__new-version-button"
|
||||||
onClick={handleClickInstallUpdate}
|
onClick={handleClickInstallUpdate}
|
||||||
>
|
>
|
||||||
<SyncIcon className={styles.newVersionIcon} size={12} />
|
<SyncIcon
|
||||||
|
className="auto-update-sub-header__new-version-icon"
|
||||||
|
size={12}
|
||||||
|
/>
|
||||||
{t("version_available_install", { version: newVersion })}
|
{t("version_available_install", { version: newVersion })}
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -1,182 +0,0 @@
|
||||||
import type { ComplexStyleRule } from "@vanilla-extract/css";
|
|
||||||
import { keyframes, style } from "@vanilla-extract/css";
|
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
|
|
||||||
export const slideIn = keyframes({
|
|
||||||
"0%": { transform: "translateX(20px)", opacity: "0" },
|
|
||||||
"100%": {
|
|
||||||
transform: "translateX(0)",
|
|
||||||
opacity: "1",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const slideOut = keyframes({
|
|
||||||
"0%": { transform: "translateX(0px)", opacity: "1" },
|
|
||||||
"100%": {
|
|
||||||
transform: "translateX(20px)",
|
|
||||||
opacity: "0",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const header = recipe({
|
|
||||||
base: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
WebkitAppRegion: "drag",
|
|
||||||
width: "100%",
|
|
||||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
|
||||||
color: vars.color.muted,
|
|
||||||
borderBottom: `solid 1px ${vars.color.border}`,
|
|
||||||
backgroundColor: vars.color.darkBackground,
|
|
||||||
} as ComplexStyleRule,
|
|
||||||
variants: {
|
|
||||||
draggingDisabled: {
|
|
||||||
true: {
|
|
||||||
WebkitAppRegion: "no-drag",
|
|
||||||
} as ComplexStyleRule,
|
|
||||||
},
|
|
||||||
isWindows: {
|
|
||||||
true: {
|
|
||||||
WebkitAppRegion: "no-drag",
|
|
||||||
} as ComplexStyleRule,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const search = recipe({
|
|
||||||
base: {
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
display: "inline-flex",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
width: "200px",
|
|
||||||
alignItems: "center",
|
|
||||||
borderRadius: "8px",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
height: "40px",
|
|
||||||
WebkitAppRegion: "no-drag",
|
|
||||||
} as ComplexStyleRule,
|
|
||||||
variants: {
|
|
||||||
focused: {
|
|
||||||
true: {
|
|
||||||
width: "250px",
|
|
||||||
borderColor: "#DADBE1",
|
|
||||||
},
|
|
||||||
false: {
|
|
||||||
":hover": {
|
|
||||||
borderColor: "rgba(255, 255, 255, 0.5)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const searchInput = style({
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
border: "none",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
outline: "none",
|
|
||||||
color: "#DADBE1",
|
|
||||||
cursor: "default",
|
|
||||||
fontFamily: "inherit",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
":focus": {
|
|
||||||
cursor: "text",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const actionButton = style({
|
|
||||||
color: "inherit",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
padding: `${SPACING_UNIT}px`,
|
|
||||||
":hover": {
|
|
||||||
color: "#DADBE1",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const section = style({
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
height: "100%",
|
|
||||||
overflow: "hidden",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const backButton = recipe({
|
|
||||||
base: {
|
|
||||||
color: vars.color.body,
|
|
||||||
cursor: "pointer",
|
|
||||||
WebkitAppRegion: "no-drag",
|
|
||||||
position: "absolute",
|
|
||||||
transition: "transform ease 0.2s",
|
|
||||||
animationDuration: "0.2s",
|
|
||||||
width: "16px",
|
|
||||||
height: "16px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
} as ComplexStyleRule,
|
|
||||||
variants: {
|
|
||||||
enabled: {
|
|
||||||
true: {
|
|
||||||
animationName: slideIn,
|
|
||||||
},
|
|
||||||
false: {
|
|
||||||
opacity: "0",
|
|
||||||
pointerEvents: "none",
|
|
||||||
animationName: slideOut,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const title = recipe({
|
|
||||||
base: {
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
hasBackButton: {
|
|
||||||
true: {
|
|
||||||
transform: "translateX(28px)",
|
|
||||||
width: "calc(100% - 28px)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const subheader = style({
|
|
||||||
borderBottom: `solid 1px ${vars.color.border}`,
|
|
||||||
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 3}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const newVersionButton = style({
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
color: vars.color.body,
|
|
||||||
fontSize: "12px",
|
|
||||||
":hover": {
|
|
||||||
textDecoration: "underline",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const newVersionLink = style({
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
color: "#8e919b",
|
|
||||||
fontSize: "12px",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const newVersionIcon = style({
|
|
||||||
color: vars.color.success,
|
|
||||||
});
|
|
137
src/renderer/src/components/header/header.scss
Normal file
137
src/renderer/src/components/header/header.scss
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
width: 100%;
|
||||||
|
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 3);
|
||||||
|
color: globals.$muted-color;
|
||||||
|
border-bottom: solid 1px globals.$border-color;
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
|
||||||
|
&--dragging-disabled {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--is-windows {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__search {
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
display: inline-flex;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
width: 200px;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
height: 40px;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--focused {
|
||||||
|
width: 250px;
|
||||||
|
border-color: #dadbe1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__search-input {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
outline: none;
|
||||||
|
color: #dadbe1;
|
||||||
|
cursor: default;
|
||||||
|
font-family: inherit;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action-button {
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
padding: globals.$spacing-unit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #dadbe1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&--left {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__back-button {
|
||||||
|
color: globals.$body-color;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
position: absolute;
|
||||||
|
transition: transform ease 0.2s;
|
||||||
|
animation-duration: 0.2s;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
animation-name: slide-out;
|
||||||
|
|
||||||
|
&--enabled {
|
||||||
|
animation: slide-in;
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&--has-back-button {
|
||||||
|
transform: translateX(28px);
|
||||||
|
width: calc(100% - 28px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in {
|
||||||
|
0% {
|
||||||
|
transform: translateX(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-out {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0px);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,9 +5,10 @@ import { ArrowLeftIcon, SearchIcon, XIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from "@renderer/hooks";
|
import { useAppDispatch, useAppSelector } from "@renderer/hooks";
|
||||||
|
|
||||||
import * as styles from "./header.css";
|
import "./header.scss";
|
||||||
import { AutoUpdateSubHeader } from "./auto-update-sub-header";
|
import { AutoUpdateSubHeader } from "./auto-update-sub-header";
|
||||||
import { setFilters } from "@renderer/features";
|
import { setFilters } from "@renderer/features";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
const pathTitle: Record<string, string> = {
|
const pathTitle: Record<string, string> = {
|
||||||
"/": "home",
|
"/": "home",
|
||||||
|
@ -75,16 +76,16 @@ export function Header() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header
|
<header
|
||||||
className={styles.header({
|
className={cn("header", {
|
||||||
draggingDisabled,
|
"header--dragging-disabled": draggingDisabled,
|
||||||
isWindows: window.electron.platform === "win32",
|
"header--is-windows": window.electron.platform === "win32",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<section className={styles.section} style={{ flex: 1 }}>
|
<section className="header__section header__section--left">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.backButton({
|
className={cn("header__back-button", {
|
||||||
enabled: location.key !== "default",
|
"header__back-button--enabled": location.key !== "default",
|
||||||
})}
|
})}
|
||||||
onClick={handleBackButtonClick}
|
onClick={handleBackButtonClick}
|
||||||
disabled={location.key === "default"}
|
disabled={location.key === "default"}
|
||||||
|
@ -93,19 +94,23 @@ export function Header() {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h3
|
<h3
|
||||||
className={styles.title({
|
className={cn("header__title", {
|
||||||
hasBackButton: location.key !== "default",
|
"header__title--has-back-button": location.key !== "default",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className={styles.section}>
|
<section className="header__section">
|
||||||
<div className={styles.search({ focused: isFocused })}>
|
<div
|
||||||
|
className={cn("header__search", {
|
||||||
|
"header__search--focused": isFocused,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.actionButton}
|
className="header__action-button"
|
||||||
onClick={focusInput}
|
onClick={focusInput}
|
||||||
>
|
>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
|
@ -117,7 +122,7 @@ export function Header() {
|
||||||
name="search"
|
name="search"
|
||||||
placeholder={t("search")}
|
placeholder={t("search")}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
className={styles.searchInput}
|
className="header__search-input"
|
||||||
onChange={(event) => handleSearch(event.target.value)}
|
onChange={(event) => handleSearch(event.target.value)}
|
||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
|
@ -127,7 +132,7 @@ export function Header() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => dispatch(setFilters({ title: "" }))}
|
onClick={() => dispatch(setFilters({ title: "" }))}
|
||||||
className={styles.actionButton}
|
className="header__action-button"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
|
|
||||||
export const hero = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "280px",
|
|
||||||
minHeight: "280px",
|
|
||||||
maxHeight: "280px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
color: "#DADBE1",
|
|
||||||
overflow: "hidden",
|
|
||||||
boxShadow: "0px 0px 15px 0px #000000",
|
|
||||||
cursor: "pointer",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
zIndex: "1",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const heroMedia = style({
|
|
||||||
objectFit: "cover",
|
|
||||||
objectPosition: "center",
|
|
||||||
position: "absolute",
|
|
||||||
zIndex: "-1",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
imageRendering: "revert",
|
|
||||||
selectors: {
|
|
||||||
[`${hero}:hover &`]: {
|
|
||||||
transform: "scale(1.02)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const backdrop = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 25%, transparent 100%)",
|
|
||||||
position: "relative",
|
|
||||||
display: "flex",
|
|
||||||
overflow: "hidden",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const description = style({
|
|
||||||
maxWidth: "700px",
|
|
||||||
color: vars.color.muted,
|
|
||||||
textAlign: "left",
|
|
||||||
lineHeight: "20px",
|
|
||||||
marginTop: `${SPACING_UNIT * 2}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const content = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 3}px`,
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
});
|
|
56
src/renderer/src/components/hero/hero.scss
Normal file
56
src/renderer/src/components/hero/hero.scss
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
width: 100%;
|
||||||
|
height: 280px;
|
||||||
|
min-height: 280px;
|
||||||
|
max-height: 280px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #dadbe1;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0px 0px 15px 0px #000000;
|
||||||
|
cursor: pointer;
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&__media {
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
image-rendering: revert;
|
||||||
|
}
|
||||||
|
&:hover &__media {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__backdrop {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 25%, transparent 100%);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
max-width: 700px;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-top: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 3);
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import * as styles from "./hero.css";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import type { TrendingGame } from "@types";
|
import type { TrendingGame } from "@types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
|
import "./hero.scss";
|
||||||
|
|
||||||
export function Hero() {
|
export function Hero() {
|
||||||
const [featuredGameDetails, setFeaturedGameDetails] = useState<
|
const [featuredGameDetails, setFeaturedGameDetails] = useState<
|
||||||
|
@ -29,7 +29,7 @@ export function Hero() {
|
||||||
}, [i18n.language]);
|
}, [i18n.language]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Skeleton className={styles.hero} />;
|
return <Skeleton className="hero" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (featuredGameDetails?.length) {
|
if (featuredGameDetails?.length) {
|
||||||
|
@ -37,17 +37,17 @@ export function Hero() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate(game.uri)}
|
onClick={() => navigate(game.uri)}
|
||||||
className={styles.hero}
|
className="hero"
|
||||||
key={index}
|
key={index}
|
||||||
>
|
>
|
||||||
<div className={styles.backdrop}>
|
<div className="hero__backdrop">
|
||||||
<img
|
<img
|
||||||
src={game.background}
|
src={game.background}
|
||||||
alt={game.description}
|
alt={game.description}
|
||||||
className={styles.heroMedia}
|
className="hero__media"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className="hero__content">
|
||||||
{game.logo && (
|
{game.logo && (
|
||||||
<img
|
<img
|
||||||
src={game.logo}
|
src={game.logo}
|
||||||
|
@ -56,7 +56,7 @@ export function Hero() {
|
||||||
loading="eager"
|
loading="eager"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className={styles.description}>{game.description}</p>
|
<p className="hero__description">{game.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
|
|
||||||
export const link = style({
|
|
||||||
textDecoration: "none",
|
|
||||||
color: "#C0C1C7",
|
|
||||||
":hover": {
|
|
||||||
textDecoration: "underline",
|
|
||||||
},
|
|
||||||
});
|
|
7
src/renderer/src/components/link/link.scss
Normal file
7
src/renderer/src/components/link/link.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #c0c1c7;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Link as ReactRouterDomLink, LinkProps } from "react-router-dom";
|
import { Link as ReactRouterDomLink, LinkProps } from "react-router-dom";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import * as styles from "./link.css";
|
import "./link.scss";
|
||||||
|
|
||||||
export function Link({ children, to, className, ...props }: LinkProps) {
|
export function Link({ children, to, className, ...props }: LinkProps) {
|
||||||
const openExternal = (event: React.MouseEvent) => {
|
const openExternal = (event: React.MouseEvent) => {
|
||||||
|
@ -12,7 +12,7 @@ export function Link({ children, to, className, ...props }: LinkProps) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={to}
|
href={to}
|
||||||
className={cn(styles.link, className)}
|
className={cn("link", className)}
|
||||||
onClick={openExternal}
|
onClick={openExternal}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
@ -22,11 +22,7 @@ export function Link({ children, to, className, ...props }: LinkProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactRouterDomLink
|
<ReactRouterDomLink className={cn("link", className)} to={to} {...props}>
|
||||||
className={cn(styles.link, className)}
|
|
||||||
to={to}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</ReactRouterDomLink>
|
</ReactRouterDomLink>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
import { keyframes, style } from "@vanilla-extract/css";
|
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
|
|
||||||
export const scaleFadeIn = keyframes({
|
|
||||||
"0%": { opacity: "0", scale: "0.5" },
|
|
||||||
"100%": {
|
|
||||||
opacity: "1",
|
|
||||||
scale: "1",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const scaleFadeOut = keyframes({
|
|
||||||
"0%": { opacity: "1", scale: "1" },
|
|
||||||
"100%": {
|
|
||||||
opacity: "0",
|
|
||||||
scale: "0.5",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const modal = recipe({
|
|
||||||
base: {
|
|
||||||
animation: `${scaleFadeIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`,
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
borderRadius: "4px",
|
|
||||||
minWidth: "400px",
|
|
||||||
maxWidth: "600px",
|
|
||||||
color: vars.color.body,
|
|
||||||
maxHeight: "100%",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
overflow: "hidden",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
closing: {
|
|
||||||
true: {
|
|
||||||
animationName: scaleFadeOut,
|
|
||||||
opacity: "0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
large: {
|
|
||||||
true: {
|
|
||||||
width: "800px",
|
|
||||||
maxWidth: "800px",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const modalContent = style({
|
|
||||||
height: "100%",
|
|
||||||
overflow: "auto",
|
|
||||||
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const modalHeader = style({
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
padding: `${SPACING_UNIT * 2}px`,
|
|
||||||
borderBottom: `solid 1px ${vars.color.border}`,
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const closeModalButton = style({
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
alignSelf: "flex-start",
|
|
||||||
":hover": {
|
|
||||||
opacity: "0.75",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const closeModalButtonIcon = style({
|
|
||||||
color: vars.color.body,
|
|
||||||
});
|
|
83
src/renderer/src/components/modal/modal.scss
Normal file
83
src/renderer/src/components/modal/modal.scss
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
animation: scale-fade-in 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none
|
||||||
|
running;
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 600px;
|
||||||
|
color: globals.$body-color;
|
||||||
|
max-height: 100%;
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&--closing {
|
||||||
|
animation-name: scale-fade-out;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--large {
|
||||||
|
width: 800px;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
border-bottom: solid 1px globals.$border-color;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close-button {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
align-self: flex-start;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close-button-icon {
|
||||||
|
color: globals.$body-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scale-fade-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
scale: 0.5;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scale-fade-out {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
scale: 0.5;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,10 +2,11 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { XIcon } from "@primer/octicons-react";
|
import { XIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
import * as styles from "./modal.css";
|
import "./modal.scss";
|
||||||
|
|
||||||
import { Backdrop } from "../backdrop/backdrop";
|
import { Backdrop } from "../backdrop/backdrop";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
export interface ModalProps {
|
export interface ModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -109,15 +110,18 @@ export function Modal({
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<Backdrop isClosing={isClosing}>
|
<Backdrop isClosing={isClosing}>
|
||||||
<div
|
<div
|
||||||
className={styles.modal({ closing: isClosing, large })}
|
className={cn("modal", {
|
||||||
|
"modal--closing": isClosing,
|
||||||
|
"modal--large": large,
|
||||||
|
})}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-labelledby={title}
|
aria-labelledby={title}
|
||||||
aria-describedby={description}
|
aria-describedby={description}
|
||||||
ref={modalContentRef}
|
ref={modalContentRef}
|
||||||
data-hydra-dialog
|
data-hydra-dialog
|
||||||
>
|
>
|
||||||
<div className={styles.modalHeader}>
|
<div className="modal__header">
|
||||||
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
|
<div className="modal__header-title">
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
{description && <p>{description}</p>}
|
{description && <p>{description}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -125,13 +129,13 @@ export function Modal({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCloseClick}
|
onClick={handleCloseClick}
|
||||||
className={styles.closeModalButton}
|
className="modal__close-button"
|
||||||
aria-label={t("close")}
|
aria-label={t("close")}
|
||||||
>
|
>
|
||||||
<XIcon className={styles.closeModalButtonIcon} size={24} />
|
<XIcon className="modal__close-button-icon" size={24} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.modalContent}>{children}</div>
|
<div className="modal__content">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</Backdrop>,
|
</Backdrop>,
|
||||||
document.body
|
document.body
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
|
|
||||||
export const select = recipe({
|
|
||||||
base: {
|
|
||||||
display: "inline-flex",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
width: "fit-content",
|
|
||||||
alignItems: "center",
|
|
||||||
borderRadius: "8px",
|
|
||||||
border: `1px solid ${vars.color.border}`,
|
|
||||||
height: "40px",
|
|
||||||
minHeight: "40px",
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
focused: {
|
|
||||||
true: {
|
|
||||||
borderColor: "#DADBE1",
|
|
||||||
},
|
|
||||||
false: {
|
|
||||||
":hover": {
|
|
||||||
borderColor: "rgba(255, 255, 255, 0.5)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
theme: {
|
|
||||||
primary: {
|
|
||||||
backgroundColor: vars.color.darkBackground,
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const option = style({
|
|
||||||
backgroundColor: vars.color.darkBackground,
|
|
||||||
borderRight: "4px solid",
|
|
||||||
borderColor: "transparent",
|
|
||||||
borderRadius: "8px",
|
|
||||||
width: "fit-content",
|
|
||||||
height: "100%",
|
|
||||||
outline: "none",
|
|
||||||
color: "#DADBE1",
|
|
||||||
cursor: "default",
|
|
||||||
fontFamily: "inherit",
|
|
||||||
fontSize: vars.size.body,
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
padding: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const label = style({
|
|
||||||
marginBottom: `${SPACING_UNIT}px`,
|
|
||||||
display: "block",
|
|
||||||
color: vars.color.body,
|
|
||||||
});
|
|
54
src/renderer/src/components/select-field/select-field.scss
Normal file
54
src/renderer/src/components/select-field/select-field.scss
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.select-field {
|
||||||
|
display: inline-flex;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
width: fit-content;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid globals.$border-color;
|
||||||
|
height: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--focused {
|
||||||
|
border-color: #dadbe1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--primary {
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--dark {
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__option {
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
border-right: 4px solid;
|
||||||
|
border-color: transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: fit-content;
|
||||||
|
height: 100%;
|
||||||
|
outline: none;
|
||||||
|
color: #dadbe1;
|
||||||
|
cursor: default;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: globals.$body-font-size;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
margin-bottom: globals.$spacing-unit;
|
||||||
|
display: block;
|
||||||
|
color: globals.$body-color;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
import { useId, useState } from "react";
|
import { useId, useState } from "react";
|
||||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
import "./select-field.scss";
|
||||||
import * as styles from "./select-field.css";
|
import cn from "classnames";
|
||||||
|
|
||||||
export interface SelectProps
|
export interface SelectProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
React.SelectHTMLAttributes<HTMLSelectElement>,
|
React.SelectHTMLAttributes<HTMLSelectElement>,
|
||||||
HTMLSelectElement
|
HTMLSelectElement
|
||||||
> {
|
> {
|
||||||
theme?: NonNullable<RecipeVariants<typeof styles.select>>["theme"];
|
theme?: "primary" | "dark";
|
||||||
label?: string;
|
label?: string;
|
||||||
options?: { key: string; value: string; label: string }[];
|
options?: { key: string; value: string; label: string }[];
|
||||||
}
|
}
|
||||||
|
@ -23,18 +23,22 @@ export function SelectField({
|
||||||
const id = useId();
|
const id = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ flex: 1 }}>
|
<div className="select-field__container">
|
||||||
{label && (
|
{label && (
|
||||||
<label htmlFor={id} className={styles.label}>
|
<label htmlFor={id} className="select-field__label">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.select({ focused: isFocused, theme })}>
|
<div
|
||||||
|
className={cn("select-field", `select-field--${theme}`, {
|
||||||
|
"select-field--focused": isFocused,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<select
|
<select
|
||||||
id={id}
|
id={id}
|
||||||
value={value}
|
value={value}
|
||||||
className={styles.option}
|
className="select-field__option"
|
||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
onBlur={() => setIsFocused(false)}
|
onBlur={() => setIsFocused(false)}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
|
|
||||||
export const profileContainer = style({
|
|
||||||
position: "relative",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const profileButton = style({
|
|
||||||
display: "flex",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all ease 0.1s",
|
|
||||||
color: vars.color.muted,
|
|
||||||
width: "100%",
|
|
||||||
overflow: "hidden",
|
|
||||||
borderRadius: "4px",
|
|
||||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
|
|
||||||
":hover": {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const profileButtonContent = style({
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
|
||||||
width: "100%",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const profileButtonInformation = style({
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
flex: "1",
|
|
||||||
minWidth: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const profileButtonTitle = style({
|
|
||||||
fontWeight: "bold",
|
|
||||||
fontSize: vars.size.body,
|
|
||||||
width: "100%",
|
|
||||||
textAlign: "left",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const friendsButton = style({
|
|
||||||
color: vars.color.muted,
|
|
||||||
cursor: "pointer",
|
|
||||||
borderRadius: "50%",
|
|
||||||
width: "40px",
|
|
||||||
minWidth: "40px",
|
|
||||||
minHeight: "40px",
|
|
||||||
height: "40px",
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
position: "relative",
|
|
||||||
transition: "all ease 0.3s",
|
|
||||||
":hover": {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const friendsButtonBadge = style({
|
|
||||||
backgroundColor: vars.color.success,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "20px",
|
|
||||||
height: "20px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
position: "absolute",
|
|
||||||
top: "-5px",
|
|
||||||
right: "-5px",
|
|
||||||
});
|
|
89
src/renderer/src/components/sidebar/sidebar-profile.scss
Normal file
89
src/renderer/src/components/sidebar/sidebar-profile.scss
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.sidebar-profile {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all ease 0.1s;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: globals.$spacing-unit globals.$spacing-unit;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit + globals.$spacing-unit / 2);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button-information {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button-title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: globals.$body-font-size;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__friends-button {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
position: relative;
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__friends-button-badge {
|
||||||
|
background-color: globals.$success-color;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
right: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-running-icon {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button-game-running-title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { PeopleIcon } from "@primer/octicons-react";
|
import { PeopleIcon } from "@primer/octicons-react";
|
||||||
import * as styles from "./sidebar-profile.css";
|
|
||||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
@ -8,6 +7,7 @@ import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-mo
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { Avatar } from "../avatar/avatar";
|
import { Avatar } from "../avatar/avatar";
|
||||||
import { AuthPage } from "@shared";
|
import { AuthPage } from "@shared";
|
||||||
|
import "./sidebar-profile.scss";
|
||||||
|
|
||||||
const LONG_POLLING_INTERVAL = 120_000;
|
const LONG_POLLING_INTERVAL = 120_000;
|
||||||
|
|
||||||
|
@ -50,14 +50,14 @@ export function SidebarProfile() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.friendsButton}
|
className="sidebar-profile__friends-button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
||||||
}
|
}
|
||||||
title={t("friends")}
|
title={t("friends")}
|
||||||
>
|
>
|
||||||
{friendRequestCount > 0 && (
|
{friendRequestCount > 0 && (
|
||||||
<small className={styles.friendsButtonBadge}>
|
<small className="sidebar-profile__friends-button-badge">
|
||||||
{friendRequestCount > 99 ? "99+" : friendRequestCount}
|
{friendRequestCount > 99 ? "99+" : friendRequestCount}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
|
@ -73,9 +73,9 @@ export function SidebarProfile() {
|
||||||
if (gameRunning.iconUrl) {
|
if (gameRunning.iconUrl) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
|
className="sidebar-profile__game-running-icon"
|
||||||
alt={gameRunning.title}
|
alt={gameRunning.title}
|
||||||
width={24}
|
width={24}
|
||||||
style={{ borderRadius: 4 }}
|
|
||||||
src={gameRunning.iconUrl}
|
src={gameRunning.iconUrl}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -85,34 +85,26 @@ export function SidebarProfile() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.profileContainer}>
|
<div className="sidebar-profile">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.profileButton}
|
className="sidebar-profile__button"
|
||||||
onClick={handleProfileClick}
|
onClick={handleProfileClick}
|
||||||
>
|
>
|
||||||
<div className={styles.profileButtonContent}>
|
<div className="sidebar-profile__button-content">
|
||||||
<Avatar
|
<Avatar
|
||||||
size={35}
|
size={35}
|
||||||
src={userDetails?.profileImageUrl}
|
src={userDetails?.profileImageUrl}
|
||||||
alt={userDetails?.displayName}
|
alt={userDetails?.displayName}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.profileButtonInformation}>
|
<div className="sidebar-profile__button-information">
|
||||||
<p className={styles.profileButtonTitle}>
|
<p className="sidebar-profile__button-title">
|
||||||
{userDetails ? userDetails.displayName : t("sign_in")}
|
{userDetails ? userDetails.displayName : t("sign_in")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{userDetails && gameRunning && (
|
{userDetails && gameRunning && (
|
||||||
<div
|
<div className="sidebar-profile__button-game-running-title">
|
||||||
style={{
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
width: "100%",
|
|
||||||
textAlign: "left",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<small>{gameRunning.title}</small>
|
<small>{gameRunning.title}</small>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -101,6 +101,12 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
&__section {
|
&__section {
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -180,14 +180,7 @@ export function Sidebar() {
|
||||||
maxWidth: sidebarWidth,
|
maxWidth: sidebarWidth,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div className="sidebar__container">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
overflow: "hidden",
|
|
||||||
flex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SidebarProfile />
|
<SidebarProfile />
|
||||||
|
|
||||||
<div className="sidebar__content">
|
<div className="sidebar__content">
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
|
|
||||||
export const textFieldContainer = style({
|
|
||||||
flex: "1",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const textField = recipe({
|
|
||||||
base: {
|
|
||||||
display: "inline-flex",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
width: "100%",
|
|
||||||
alignItems: "center",
|
|
||||||
borderRadius: "8px",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
height: "40px",
|
|
||||||
minHeight: "40px",
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
theme: {
|
|
||||||
primary: {
|
|
||||||
backgroundColor: vars.color.darkBackground,
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hasError: {
|
|
||||||
true: {
|
|
||||||
borderColor: vars.color.danger,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
focused: {
|
|
||||||
true: {
|
|
||||||
borderColor: "#DADBE1",
|
|
||||||
},
|
|
||||||
false: {
|
|
||||||
":hover": {
|
|
||||||
borderColor: "rgba(255, 255, 255, 0.5)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const textFieldInput = recipe({
|
|
||||||
base: {
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
border: "none",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
outline: "none",
|
|
||||||
color: "#DADBE1",
|
|
||||||
cursor: "default",
|
|
||||||
fontFamily: "inherit",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
padding: `${SPACING_UNIT}px`,
|
|
||||||
":focus": {
|
|
||||||
cursor: "text",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
readOnly: {
|
|
||||||
true: {
|
|
||||||
textOverflow: "inherit",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const togglePasswordButton = style({
|
|
||||||
cursor: "pointer",
|
|
||||||
color: vars.color.muted,
|
|
||||||
padding: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const textFieldWrapper = style({
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const errorLabel = style({
|
|
||||||
color: vars.color.danger,
|
|
||||||
});
|
|
79
src/renderer/src/components/text-field/text-field.scss
Normal file
79
src/renderer/src/components/text-field/text-field.scss
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.text-field-container {
|
||||||
|
flex: 1;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&__text-field {
|
||||||
|
display: inline-flex;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
height: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--primary {
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--dark {
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--has-error {
|
||||||
|
border-color: globals.$danger-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--focused {
|
||||||
|
border-color: #dadbe1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text-field-input {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
outline: none;
|
||||||
|
color: #dadbe1;
|
||||||
|
cursor: default;
|
||||||
|
font-family: inherit;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: globals.$spacing-unit;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--read-only {
|
||||||
|
text-overflow: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toggle-password-button {
|
||||||
|
cursor: pointer;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
padding: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text-field-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error-label {
|
||||||
|
color: globals.$danger-color;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,17 @@
|
||||||
import React, { useId, useMemo, useState } from "react";
|
import React, { useId, useMemo, useState } from "react";
|
||||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
|
||||||
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
|
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import * as styles from "./text-field.css";
|
import cn from "classnames";
|
||||||
|
|
||||||
|
import "./text-field.scss";
|
||||||
|
|
||||||
export interface TextFieldProps
|
export interface TextFieldProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
> {
|
> {
|
||||||
theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"];
|
theme?: "primary" | "dark";
|
||||||
label?: string | React.ReactNode;
|
label?: string | React.ReactNode;
|
||||||
hint?: string | React.ReactNode;
|
hint?: string | React.ReactNode;
|
||||||
textFieldProps?: React.DetailedHTMLProps<
|
textFieldProps?: React.DetailedHTMLProps<
|
||||||
|
@ -54,7 +55,10 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||||
}, [props.type, isPasswordVisible]);
|
}, [props.type, isPasswordVisible]);
|
||||||
|
|
||||||
const hintContent = useMemo(() => {
|
const hintContent = useMemo(() => {
|
||||||
if (error) return <small className={styles.errorLabel}>{error}</small>;
|
if (error)
|
||||||
|
return (
|
||||||
|
<small className="text-field-container__error-label">{error}</small>
|
||||||
|
);
|
||||||
|
|
||||||
if (hint) return <small>{hint}</small>;
|
if (hint) return <small>{hint}</small>;
|
||||||
return null;
|
return null;
|
||||||
|
@ -73,22 +77,28 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||||
const hasError = !!error;
|
const hasError = !!error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.textFieldContainer} {...containerProps}>
|
<div className="text-field-container" {...containerProps}>
|
||||||
{label && <label htmlFor={id}>{label}</label>}
|
{label && <label htmlFor={id}>{label}</label>}
|
||||||
|
|
||||||
<div className={styles.textFieldWrapper}>
|
<div className="text-field-container__text-field-wrapper">
|
||||||
<div
|
<div
|
||||||
className={styles.textField({
|
className={cn(
|
||||||
theme,
|
"text-field-container__text-field",
|
||||||
hasError,
|
`text-field-container__text-field--${theme}`,
|
||||||
focused: isFocused,
|
{
|
||||||
})}
|
"text-field-container__text-field--has-error": hasError,
|
||||||
|
"text-field-container__text-field--focused": isFocused,
|
||||||
|
}
|
||||||
|
)}
|
||||||
{...textFieldProps}
|
{...textFieldProps}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={id}
|
id={id}
|
||||||
className={styles.textFieldInput({ readOnly: props.readOnly })}
|
className={cn("text-field-container__text-field-input", {
|
||||||
|
"text-field-container__text-field-input--read-only":
|
||||||
|
props.readOnly,
|
||||||
|
})}
|
||||||
{...props}
|
{...props}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
|
@ -98,7 +108,7 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||||
{showPasswordToggleButton && (
|
{showPasswordToggleButton && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.togglePasswordButton}
|
className="text-field-container__toggle-password-button"
|
||||||
onClick={() => setIsPasswordVisible(!isPasswordVisible)}
|
onClick={() => setIsPasswordVisible(!isPasswordVisible)}
|
||||||
aria-label={t("toggle_password_visibility")}
|
aria-label={t("toggle_password_visibility")}
|
||||||
>
|
>
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
background-color: globals.$dark-background-color;
|
background-color: globals.$dark-background-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: solid 1px globals.$border-color;
|
border: solid 1px globals.$border-color;
|
||||||
right: 0;
|
right: 16px;
|
||||||
bottom: 0;
|
bottom: 26px + globals.$spacing-unit;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -31,6 +31,16 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__message-container {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
&__progress {
|
&__progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
|
@ -38,6 +48,7 @@
|
||||||
&::-webkit-progress-bar {
|
&::-webkit-progress-bar {
|
||||||
background-color: globals.$dark-background-color;
|
background-color: globals.$dark-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-progress-value {
|
&::-webkit-progress-value {
|
||||||
background-color: globals.$muted-color;
|
background-color: globals.$muted-color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,13 +87,7 @@ export function Toast({
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="toast__content">
|
<div className="toast__content">
|
||||||
<div
|
<div className="toast__message-container">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: `8px`,
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
|
@ -10,6 +10,7 @@ export const DOWNLOADER_NAME = {
|
||||||
[Downloader.Qiwi]: "Qiwi",
|
[Downloader.Qiwi]: "Qiwi",
|
||||||
[Downloader.Datanodes]: "Datanodes",
|
[Downloader.Datanodes]: "Datanodes",
|
||||||
[Downloader.Mediafire]: "Mediafire",
|
[Downloader.Mediafire]: "Mediafire",
|
||||||
|
[Downloader.TorBox]: "TorBox",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||||
|
|
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
|
@ -28,6 +28,7 @@ import type {
|
||||||
CatalogueSearchPayload,
|
CatalogueSearchPayload,
|
||||||
LibraryGame,
|
LibraryGame,
|
||||||
GameRunning,
|
GameRunning,
|
||||||
|
TorBoxUser,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import type disk from "diskusage";
|
import type disk from "diskusage";
|
||||||
|
@ -49,7 +50,7 @@ declare global {
|
||||||
pauseGameSeed: (shop: GameShop, objectId: string) => Promise<void>;
|
pauseGameSeed: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
resumeGameSeed: (shop: GameShop, objectId: string) => Promise<void>;
|
resumeGameSeed: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
onDownloadProgress: (
|
onDownloadProgress: (
|
||||||
cb: (value: DownloadProgress) => void
|
cb: (value: DownloadProgress | null) => void
|
||||||
) => () => Electron.IpcRenderer;
|
) => () => Electron.IpcRenderer;
|
||||||
onSeedingStatus: (
|
onSeedingStatus: (
|
||||||
cb: (value: SeedingStatus[]) => void
|
cb: (value: SeedingStatus[]) => void
|
||||||
|
@ -144,6 +145,7 @@ declare global {
|
||||||
minimized: boolean;
|
minimized: boolean;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||||
|
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
||||||
onAchievementUnlocked: (cb: () => void) => () => Electron.IpcRenderer;
|
onAchievementUnlocked: (cb: () => void) => () => Electron.IpcRenderer;
|
||||||
|
|
||||||
/* Download sources */
|
/* Download sources */
|
||||||
|
|
|
@ -18,9 +18,9 @@ export const downloadSlice = createSlice({
|
||||||
name: "download",
|
name: "download",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setLastPacket: (state, action: PayloadAction<DownloadProgress>) => {
|
setLastPacket: (state, action: PayloadAction<DownloadProgress | null>) => {
|
||||||
state.lastPacket = action.payload;
|
state.lastPacket = action.payload;
|
||||||
if (!state.gameId) state.gameId = action.payload.gameId;
|
if (!state.gameId && action.payload) state.gameId = action.payload.gameId;
|
||||||
},
|
},
|
||||||
clearDownload: (state) => {
|
clearDownload: (state) => {
|
||||||
state.lastPacket = null;
|
state.lastPacket = null;
|
||||||
|
|
|
@ -114,7 +114,7 @@ export function useDownload() {
|
||||||
pauseSeeding,
|
pauseSeeding,
|
||||||
resumeSeeding,
|
resumeSeeding,
|
||||||
clearDownload: () => dispatch(clearDownload()),
|
clearDownload: () => dispatch(clearDownload()),
|
||||||
setLastPacket: (packet: DownloadProgress) =>
|
setLastPacket: (packet: DownloadProgress | null) =>
|
||||||
dispatch(setLastPacket(packet)),
|
dispatch(setLastPacket(packet)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
|
|
||||||
export const panel = style({
|
|
||||||
width: "100%",
|
|
||||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "start",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
borderBottom: `solid 1px ${vars.color.border}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const content = style({
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
justifyContent: "center",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const actions = style({
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadDetailsRow = style({
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
display: "flex",
|
|
||||||
color: vars.color.body,
|
|
||||||
alignItems: "center",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadsLink = style({
|
|
||||||
color: vars.color.body,
|
|
||||||
textDecoration: "underline",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const progressBar = recipe({
|
|
||||||
base: {
|
|
||||||
position: "absolute",
|
|
||||||
bottom: "0",
|
|
||||||
left: "0",
|
|
||||||
width: "100%",
|
|
||||||
height: "3px",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
"::-webkit-progress-bar": {
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
},
|
|
||||||
"::-webkit-progress-value": {
|
|
||||||
backgroundColor: vars.color.muted,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
disabled: {
|
|
||||||
true: {
|
|
||||||
opacity: vars.opacity.disabled,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const link = style({
|
|
||||||
textAlign: "start",
|
|
||||||
color: vars.color.body,
|
|
||||||
":hover": {
|
|
||||||
textDecoration: "underline",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
});
|
|
97
src/renderer/src/pages/achievements/achievement-panel.scss
Normal file
97
src/renderer/src/pages/achievements/achievement-panel.scss
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.achievement-panel {
|
||||||
|
width: 100%;
|
||||||
|
padding: globals.$spacing-unit * 2 globals.$spacing-unit * 3;
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: solid 1px globals.$border-color;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__download-details-row {
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
display: flex;
|
||||||
|
color: globals.$body-color;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__downloads-link {
|
||||||
|
color: globals.$body-color;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-bar {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
|
&::-webkit-progress-bar {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-progress-value {
|
||||||
|
background-color: globals.$muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
opacity: globals.$disabled-opacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__link {
|
||||||
|
text-align: start;
|
||||||
|
color: globals.$body-color;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--warning {
|
||||||
|
color: globals.$warning-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__grid {
|
||||||
|
display: grid;
|
||||||
|
gap: globals.$spacing-unit * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__grid--with-subscription {
|
||||||
|
grid-template-columns: 3fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__grid--without-subscription {
|
||||||
|
grid-template-columns: 3fr 2fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__points-container {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,8 +3,7 @@ import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||||
import { UserAchievement } from "@types";
|
import { UserAchievement } from "@types";
|
||||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
import { vars } from "@renderer/theme.css";
|
import "./achievement-panel.scss";
|
||||||
import * as styles from "./achievement-panel.css";
|
|
||||||
|
|
||||||
export interface AchievementPanelProps {
|
export interface AchievementPanelProps {
|
||||||
achievements: UserAchievement[];
|
achievements: UserAchievement[];
|
||||||
|
@ -28,17 +27,18 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
|
||||||
|
|
||||||
if (!hasActiveSubscription) {
|
if (!hasActiveSubscription) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.panel}>
|
<div className="achievement-panel">
|
||||||
<div className={styles.content}>
|
<div className="achievement-panel__content">
|
||||||
{t("earned_points")} <HydraIcon width={20} height={20} />
|
{t("earned_points")}{" "}
|
||||||
|
<HydraIcon className="achievement-panel__content-icon" />
|
||||||
??? / ???
|
??? / ???
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showHydraCloudModal("achievements-points")}
|
onClick={() => showHydraCloudModal("achievements-points")}
|
||||||
className={styles.link}
|
className="achievement-panel__link"
|
||||||
>
|
>
|
||||||
<small style={{ color: vars.color.warning }}>
|
<small className="achievement-panel__link--warning">
|
||||||
{t("how_to_earn_achievements_points")}
|
{t("how_to_earn_achievements_points")}
|
||||||
</small>
|
</small>
|
||||||
</button>
|
</button>
|
||||||
|
@ -47,9 +47,10 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.panel}>
|
<div className="achievement-panel">
|
||||||
<div className={styles.content}>
|
<div className="achievement-panel__content">
|
||||||
{t("earned_points")} <HydraIcon width={20} height={20} />
|
{t("earned_points")}{" "}
|
||||||
|
<HydraIcon className="achievement-panel__content-icon" />
|
||||||
{achievementsPointsEarnedSum} / {achievementsPointsTotal}
|
{achievementsPointsEarnedSum} / {achievementsPointsTotal}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
248
src/renderer/src/pages/achievements/achievements-content.scss
Normal file
248
src/renderer/src/pages/achievements/achievements-content.scss
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
$hero-height: 150px;
|
||||||
|
$logo-height: 100px;
|
||||||
|
$logo-max-width: 200px;
|
||||||
|
|
||||||
|
.achievements-content {
|
||||||
|
&__comparison {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit * 2;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
padding: globals.$spacing-unit;
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
border-radius: 4px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&__subscription-required-button {
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: globals.$spacing-unit / 2;
|
||||||
|
color: globals.$body-color;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__blured-avatar {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit * 2;
|
||||||
|
align-items: center;
|
||||||
|
height: 62px;
|
||||||
|
position: relative;
|
||||||
|
filter: blur(4px);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__small-avatar {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subscription-required-button {
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
color: globals.$body-color;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__user-summary {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit * 2;
|
||||||
|
align-items: center;
|
||||||
|
padding: globals.$spacing-unit globals.$spacing-unit * 2;
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
|
||||||
|
&__trophy-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
|
&::-webkit-progress-bar {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-progress-value {
|
||||||
|
background-color: globals.$muted-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__achievements-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
|
||||||
|
&__image {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: auto;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
globals.$background-color 0%,
|
||||||
|
globals.$background-color 100%
|
||||||
|
);
|
||||||
|
|
||||||
|
&__hero {
|
||||||
|
width: 100%;
|
||||||
|
height: $hero-height;
|
||||||
|
min-height: $hero-height;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
padding: globals.$spacing-unit * 2;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__game-logo {
|
||||||
|
width: $logo-max-width;
|
||||||
|
height: $logo-height;
|
||||||
|
object-fit: contain;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__achievements-summary-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
padding: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__table-header {
|
||||||
|
width: 100%;
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
border-bottom: 1px solid globals.$border-color;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&--stuck {
|
||||||
|
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
display: grid;
|
||||||
|
gap: globals.$spacing-unit * 2;
|
||||||
|
padding: globals.$spacing-unit * 3;
|
||||||
|
|
||||||
|
&--has-no-active-subscription {
|
||||||
|
grid-template-columns: 3fr 2fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--has-active-subscription {
|
||||||
|
grid-template-columns: 3fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__user-avatar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__other-user-avatar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__profile-avatar {
|
||||||
|
height: 54px;
|
||||||
|
width: 54px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
position: relative;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,18 +8,17 @@ import {
|
||||||
formatDownloadProgress,
|
formatDownloadProgress,
|
||||||
} from "@renderer/helpers";
|
} from "@renderer/helpers";
|
||||||
import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react";
|
import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import type { ComparedAchievements } from "@types";
|
import type { ComparedAchievements } from "@types";
|
||||||
import { average } from "color.js";
|
import { average } from "color.js";
|
||||||
import Color from "color";
|
import Color from "color";
|
||||||
import { Link } from "@renderer/components";
|
import { Link } from "@renderer/components";
|
||||||
import { ComparedAchievementList } from "./compared-achievement-list";
|
import { ComparedAchievementList } from "./compared-achievement-list";
|
||||||
import * as styles from "./achievements.css";
|
|
||||||
import { AchievementList } from "./achievement-list";
|
import { AchievementList } from "./achievement-list";
|
||||||
import { AchievementPanel } from "./achievement-panel";
|
import { AchievementPanel } from "./achievement-panel";
|
||||||
import { ComparedAchievementPanel } from "./compared-achievement-panel";
|
import { ComparedAchievementPanel } from "./compared-achievement-panel";
|
||||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||||
|
import "./achievements-content.scss";
|
||||||
|
|
||||||
interface UserInfo {
|
interface UserInfo {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -48,10 +47,10 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||||
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.profileAvatar}>
|
<div className="achievements-content__profile-avatar">
|
||||||
{user.profileImageUrl ? (
|
{user.profileImageUrl ? (
|
||||||
<img
|
<img
|
||||||
className={styles.profileAvatar}
|
className="achievements-content__profile-avatar"
|
||||||
src={user.profileImageUrl}
|
src={user.profileImageUrl}
|
||||||
alt={user.displayName}
|
alt={user.displayName}
|
||||||
/>
|
/>
|
||||||
|
@ -64,91 +63,33 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||||
|
|
||||||
if (isComparison && userDetails?.id == user.id && !hasActiveSubscription) {
|
if (isComparison && userDetails?.id == user.id && !hasActiveSubscription) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="achievements-content__comparison">
|
||||||
style={{
|
<div className="achievements-content__comparison__container">
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
alignItems: "center",
|
|
||||||
position: "relative",
|
|
||||||
padding: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
zIndex: 2,
|
|
||||||
inset: 0,
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
background: "rgba(0, 0, 0, 0.7)",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
flexDirection: "row",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
borderRadius: "4px",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LockIcon size={24} />
|
<LockIcon size={24} />
|
||||||
<h3>
|
<h3>
|
||||||
<button
|
<button
|
||||||
className={styles.subscriptionRequiredButton}
|
className="achievements-content__comparison__container__subscription-required-button"
|
||||||
onClick={() => showHydraCloudModal("achievements")}
|
onClick={() => showHydraCloudModal("achievements")}
|
||||||
>
|
>
|
||||||
{t("subscription_needed")}
|
{t("subscription_needed")}
|
||||||
</button>
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="achievements-content__comparison__blured-avatar">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
alignItems: "center",
|
|
||||||
height: "62px",
|
|
||||||
position: "relative",
|
|
||||||
filter: "blur(4px)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getProfileImage(user)}
|
{getProfileImage(user)}
|
||||||
<h1 style={{ marginBottom: "8px" }}>{user.displayName}</h1>
|
<h1>{user.displayName}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="achievements-content__user-summary">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
alignItems: "center",
|
|
||||||
padding: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getProfileImage(user)}
|
{getProfileImage(user)}
|
||||||
<div
|
<div className="achievements-content__user-summary__container">
|
||||||
style={{
|
<h1>{user.displayName}</h1>
|
||||||
display: "flex",
|
<div className="achievements-content__user-summary__container__stats">
|
||||||
flexDirection: "column",
|
<div className="achievements-content__user-summary__container__stats__trophy-count">
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h1 style={{ marginBottom: "8px" }}>{user.displayName}</h1>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
marginBottom: 8,
|
|
||||||
color: vars.color.muted,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrophyIcon size={13} />
|
<TrophyIcon size={13} />
|
||||||
<span>
|
<span>
|
||||||
{user.unlockedAchievementCount} / {user.totalAchievementCount}
|
{user.unlockedAchievementCount} / {user.totalAchievementCount}
|
||||||
|
@ -164,7 +105,7 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||||
<progress
|
<progress
|
||||||
max={1}
|
max={1}
|
||||||
value={user.unlockedAchievementCount / user.totalAchievementCount}
|
value={user.unlockedAchievementCount / user.totalAchievementCount}
|
||||||
className={styles.achievementsProgressBar}
|
className="achievements-content__user-summary__container__stats__progress-bar"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -203,7 +144,7 @@ export function AchievementsContent({
|
||||||
};
|
};
|
||||||
|
|
||||||
const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
|
const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
|
||||||
const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT;
|
const heroHeight = heroRef.current?.clientHeight ?? 150;
|
||||||
|
|
||||||
const scrollY = (event.target as HTMLDivElement).scrollTop;
|
const scrollY = (event.target as HTMLDivElement).scrollTop;
|
||||||
if (scrollY >= heroHeight && !isHeaderStuck) {
|
if (scrollY >= heroHeight && !isHeaderStuck) {
|
||||||
|
@ -219,10 +160,10 @@ export function AchievementsContent({
|
||||||
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.profileAvatarSmall}>
|
<div className="achievements-content__comparison__small-avatar">
|
||||||
{user.profileImageUrl ? (
|
{user.profileImageUrl ? (
|
||||||
<img
|
<img
|
||||||
className={styles.profileAvatarSmall}
|
className="achievements-content__comparison__small-avatar"
|
||||||
src={user.profileImageUrl}
|
src={user.profileImageUrl}
|
||||||
alt={user.displayName}
|
alt={user.displayName}
|
||||||
/>
|
/>
|
||||||
|
@ -236,10 +177,10 @@ export function AchievementsContent({
|
||||||
if (!objectId || !shop || !gameTitle || !userDetails) return null;
|
if (!objectId || !shop || !gameTitle || !userDetails) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className="achievements-content__achievements-list">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.libraryHero(objectId)}
|
src={steamUrlBuilder.libraryHero(objectId)}
|
||||||
style={{ display: "none" }}
|
className="achievements-content__achievements-list__image"
|
||||||
alt={gameTitle}
|
alt={gameTitle}
|
||||||
onLoad={handleHeroLoad}
|
onLoad={handleHeroLoad}
|
||||||
/>
|
/>
|
||||||
|
@ -247,38 +188,32 @@ export function AchievementsContent({
|
||||||
<section
|
<section
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
className={styles.container}
|
className="achievements-content__achievements-list__section"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
className="achievements-content__achievements-list__section__container"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
background: `linear-gradient(0deg, #151515 0%, ${gameColor} 100%)`,
|
||||||
flexDirection: "column",
|
|
||||||
background: `linear-gradient(0deg, ${vars.color.darkBackground} 0%, ${gameColor} 100%)`,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div ref={heroRef} className={styles.hero}>
|
<div
|
||||||
<div className={styles.heroContent}>
|
ref={heroRef}
|
||||||
|
className="achievements-content__achievements-list__section__container__hero"
|
||||||
|
>
|
||||||
|
<div className="achievements-content__achievements-list__section__container__hero__content">
|
||||||
<Link
|
<Link
|
||||||
to={buildGameDetailsPath({ shop, objectId, title: gameTitle })}
|
to={buildGameDetailsPath({ shop, objectId, title: gameTitle })}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.logo(objectId)}
|
src={steamUrlBuilder.logo(objectId)}
|
||||||
className={styles.gameLogo}
|
className="achievements-content__achievements-list__section__container__hero__content__game-logo"
|
||||||
alt={gameTitle}
|
alt={gameTitle}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="achievements-content__achievements-list__section__container__achievements-summary-wrapper">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
width: "100%",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
padding: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AchievementSummary
|
<AchievementSummary
|
||||||
user={{
|
user={{
|
||||||
...userDetails,
|
...userDetails,
|
||||||
|
@ -298,24 +233,19 @@ export function AchievementsContent({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{otherUser && (
|
{otherUser && (
|
||||||
<div className={styles.tableHeader({ stuck: isHeaderStuck })}>
|
<div
|
||||||
|
className={`achievements-content__achievements-list__section__table-header ${isHeaderStuck ? "achievements-content__achievements-list__section__table-header--stuck" : ""}`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
className={`achievements-content__achievements-list__section__table-header__container ${hasActiveSubscription ? "achievements-content__achievements-list__section__table-header__container--has-active-subscription" : "achievements-content__achievements-list__section__table-header__container--has-no-active-subscription"}`}
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: hasActiveSubscription
|
|
||||||
? "3fr 1fr 1fr"
|
|
||||||
: "3fr 2fr",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 3}px`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div></div>
|
<div></div>
|
||||||
{hasActiveSubscription && (
|
{hasActiveSubscription && (
|
||||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
<div className="achievements-content__achievements-list__section__table-header__container__user-avatar">
|
||||||
{getProfileImage({ ...userDetails })}
|
{getProfileImage({ ...userDetails })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
<div className="achievements-content__achievements-list__section__table-header__container__other-user-avatar">
|
||||||
{getProfileImage(otherUser)}
|
{getProfileImage(otherUser)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue