mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 22:06:17 +00:00
Compare commits
61 Commits
feat/addin
...
v3.7.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f28fc8ca1 | ||
|
|
d1eb174429 | ||
|
|
82a125237b | ||
|
|
19e312d31e | ||
|
|
79b1f05cde | ||
|
|
cc9ac9dc0f | ||
|
|
19406dd051 | ||
|
|
8aa6e113e7 | ||
|
|
91ad4a68f7 | ||
|
|
a69a6ec510 | ||
|
|
fada6507c3 | ||
|
|
0479f1347b | ||
|
|
817870cdbb | ||
|
|
f44d5c8b49 | ||
|
|
c36109c092 | ||
|
|
b59fb7dc36 | ||
|
|
214a7af408 | ||
|
|
14679fc31e | ||
|
|
e872b2ea8a | ||
|
|
dd7c84b433 | ||
|
|
1546da29cf | ||
|
|
a89b0bb2a8 | ||
|
|
9bdb216e0f | ||
|
|
9779aed8c1 | ||
|
|
058a148c7f | ||
|
|
16e3d52508 | ||
|
|
7e0002cf95 | ||
|
|
bf8b3ca836 | ||
|
|
77e376e742 | ||
|
|
bd28b202c4 | ||
|
|
153b954e78 | ||
|
|
a9e63730be | ||
|
|
316480930d | ||
|
|
0b5c9acaaa | ||
|
|
814a2da05c | ||
|
|
0ad1ebd6a2 | ||
|
|
e9de8264e2 | ||
|
|
b135087ffe | ||
|
|
b4a1af78a6 | ||
|
|
ede5bb0c23 | ||
|
|
9a27875cd8 | ||
|
|
cf20a942ae | ||
|
|
256d829a60 | ||
|
|
8cb18578e0 | ||
|
|
62950297e0 | ||
|
|
3eecc42430 | ||
|
|
f6edb45628 | ||
|
|
de8797bea6 | ||
|
|
828f82f647 | ||
|
|
bb22d9c4dd | ||
|
|
559bb45acc | ||
|
|
e176e624be | ||
|
|
4e92e794be | ||
|
|
de0dbcac35 | ||
|
|
07d5a5b3f3 | ||
|
|
a1117c8269 | ||
|
|
5bffaf17fa | ||
|
|
cc38be4383 | ||
|
|
0b70a28c08 | ||
|
|
3ff50a9932 | ||
|
|
83fbf20383 |
13
.cursorrules
13
.cursorrules
@@ -28,6 +28,19 @@
|
|||||||
- Use async/await instead of promises when possible
|
- Use async/await instead of promises when possible
|
||||||
- Prefer named exports over default exports for utilities and services
|
- Prefer named exports over default exports for utilities and services
|
||||||
|
|
||||||
|
## ESLint Issues
|
||||||
|
|
||||||
|
- **Always try to fix ESLint errors properly before disabling rules**
|
||||||
|
- When encountering ESLint errors, explore these solutions in order:
|
||||||
|
1. **Fix the code to comply with the rule** (e.g., add missing required elements, fix accessibility issues)
|
||||||
|
2. **Use minimal markup to satisfy the rule** (e.g., add empty `<track>` elements for videos without captions, add `role` attributes)
|
||||||
|
3. **Only disable the rule as a last resort** when no reasonable solution exists
|
||||||
|
- When disabling a rule, always include a comment explaining why it's necessary
|
||||||
|
- Examples of proper fixes:
|
||||||
|
- For `jsx-a11y/media-has-caption`: Add `<track kind="captions" />` even if no captions are available
|
||||||
|
- For `jsx-a11y/alt-text`: Add meaningful alt text or `alt=""` for decorative images
|
||||||
|
- For accessibility rules: Add appropriate ARIA attributes rather than disabling
|
||||||
|
|
||||||
## TypeScript Array Syntax
|
## TypeScript Array Syntax
|
||||||
|
|
||||||
- **Always use `T[]` syntax instead of `Array<T>`** for array types
|
- **Always use `T[]` syntax instead of `Array<T>`** for array types
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "3.7.4",
|
"version": "3.7.6",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
@@ -63,6 +63,7 @@
|
|||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"file-type": "^20.5.0",
|
"file-type": "^20.5.0",
|
||||||
"framer-motion": "^12.15.0",
|
"framer-motion": "^12.15.0",
|
||||||
|
"hls.js": "^1.5.12",
|
||||||
"i18next": "^23.11.2",
|
"i18next": "^23.11.2",
|
||||||
"i18next-browser-languagedetector": "^7.2.1",
|
"i18next-browser-languagedetector": "^7.2.1",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
@@ -84,7 +85,7 @@
|
|||||||
"sound-play": "^1.1.0",
|
"sound-play": "^1.1.0",
|
||||||
"steam-shortcut-editor": "https://github.com/hydralauncher/steam-shortcut-editor",
|
"steam-shortcut-editor": "https://github.com/hydralauncher/steam-shortcut-editor",
|
||||||
"sudo-prompt": "^9.2.1",
|
"sudo-prompt": "^9.2.1",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.5.2",
|
||||||
"tough-cookie": "^5.1.1",
|
"tough-cookie": "^5.1.1",
|
||||||
"user-agents": "^1.1.387",
|
"user-agents": "^1.1.387",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"search_library": "Search library",
|
"search_library": "Search library",
|
||||||
"recent_searches": "Recent Searches",
|
"recent_searches": "Recent Searches",
|
||||||
"suggestions": "Suggestions",
|
"suggestions": "Suggestions",
|
||||||
"clear_history": "Clear history",
|
"clear_history": "Clear",
|
||||||
"remove_from_history": "Remove from history",
|
"remove_from_history": "Remove from history",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"no_results": "No results",
|
"no_results": "No results",
|
||||||
|
|||||||
@@ -93,8 +93,16 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar juegos",
|
"search": "Buscar juegos",
|
||||||
|
"search_library": "Buscar en la librería",
|
||||||
|
"recent_searches": "Búsquedas Recientes",
|
||||||
|
"suggestions": "Sugerencias",
|
||||||
|
"clear_history": "Limpiar",
|
||||||
|
"remove_from_history": "Eliminar del historial",
|
||||||
|
"loading": "Cargando...",
|
||||||
|
"no_results": "Sin resultados",
|
||||||
"home": "Inicio",
|
"home": "Inicio",
|
||||||
"catalogue": "Catálogo",
|
"catalogue": "Catálogo",
|
||||||
|
"library": "Librería",
|
||||||
"downloads": "Descargas",
|
"downloads": "Descargas",
|
||||||
"search_results": "Resultados de búsqueda",
|
"search_results": "Resultados de búsqueda",
|
||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
@@ -450,6 +458,7 @@
|
|||||||
"description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas",
|
"description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas",
|
||||||
"button_delete_all_sources": "Eliminar todo",
|
"button_delete_all_sources": "Eliminar todo",
|
||||||
"added_download_source": "Añadir fuente de descarga",
|
"added_download_source": "Añadir fuente de descarga",
|
||||||
|
"adding": "Añadiendo…",
|
||||||
"download_sources_synced": "Todas las fuentes de descarga están sincronizadas",
|
"download_sources_synced": "Todas las fuentes de descarga están sincronizadas",
|
||||||
"insert_valid_json_url": "Introducí una URL de json válida",
|
"insert_valid_json_url": "Introducí una URL de json válida",
|
||||||
"found_download_option_zero": "Sin opciones de descargas encontrada",
|
"found_download_option_zero": "Sin opciones de descargas encontrada",
|
||||||
@@ -555,6 +564,19 @@
|
|||||||
"debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.",
|
"debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.",
|
||||||
"enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego",
|
"enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego",
|
||||||
"autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego",
|
"autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego",
|
||||||
|
"change_achievement_sound": "Cambiar sonido de logro",
|
||||||
|
"download_source_already_exists": "Esta fuente de descarga URL ya existe.",
|
||||||
|
"download_source_failed": "Error",
|
||||||
|
"download_source_matched": "Actualizado",
|
||||||
|
"download_source_matching": "Actualizando",
|
||||||
|
"download_source_no_information": "Sin información disponible",
|
||||||
|
"download_source_pending_matching": "Actualizando pronto",
|
||||||
|
"download_sources_synced_successfully": "Todas las fuentes de descarga están sincronizadas",
|
||||||
|
"failed_add_download_source": "Error al añadir la fuente de descarga. Por favor intentá de nuevo.",
|
||||||
|
"hydra_cloud": "Hydra Cloud",
|
||||||
|
"preview_sound": "Vista previa de sonido",
|
||||||
|
"remove_achievement_sound": "Eliminar sonido de logros",
|
||||||
|
"removed_all_download_sources": "Todas las fuentes de descarga eliminadas",
|
||||||
"hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego"
|
"hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|||||||
@@ -93,11 +93,19 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar jogos",
|
"search": "Buscar jogos",
|
||||||
|
"search_library": "Buscar na biblioteca",
|
||||||
|
"recent_searches": "Buscas Recentes",
|
||||||
|
"suggestions": "Sugestões",
|
||||||
|
"clear_history": "Limpar",
|
||||||
|
"remove_from_history": "Remover do histórico",
|
||||||
|
"loading": "Carregando...",
|
||||||
|
"no_results": "Sem resultados",
|
||||||
|
"home": "Início",
|
||||||
"catalogue": "Catálogo",
|
"catalogue": "Catálogo",
|
||||||
|
"library": "Biblioteca",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
"search_results": "Resultados da busca",
|
"search_results": "Resultados da busca",
|
||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
"home": "Início",
|
|
||||||
"version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.",
|
"version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.",
|
||||||
"version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download."
|
"version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,11 +30,19 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Procurar jogos",
|
"search": "Procurar jogos",
|
||||||
|
"search_library": "Procurar na biblioteca",
|
||||||
|
"recent_searches": "Pesquisas Recentes",
|
||||||
|
"suggestions": "Sugestões",
|
||||||
|
"clear_history": "Limpar",
|
||||||
|
"remove_from_history": "Remover do histórico",
|
||||||
|
"loading": "A carregar...",
|
||||||
|
"no_results": "Sem resultados",
|
||||||
|
"home": "Início",
|
||||||
"catalogue": "Catálogo",
|
"catalogue": "Catálogo",
|
||||||
|
"library": "Biblioteca",
|
||||||
"downloads": "Transferências",
|
"downloads": "Transferências",
|
||||||
"search_results": "Resultados da pesquisa",
|
"search_results": "Resultados da pesquisa",
|
||||||
"settings": "Definições",
|
"settings": "Definições",
|
||||||
"home": "Início",
|
|
||||||
"version_available_install": "Versão {{version}} disponível. Clica aqui para reiniciar e instalar.",
|
"version_available_install": "Versão {{version}} disponível. Clica aqui para reiniciar e instalar.",
|
||||||
"version_available_download": "Versão {{version}} disponível. Clica aqui para fazer o download."
|
"version_available_download": "Versão {{version}} disponível. Clica aqui para fazer o download."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -93,8 +93,16 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
|
"search_library": "Поиск в библиотеке",
|
||||||
|
"recent_searches": "Недавние поиски",
|
||||||
|
"suggestions": "Предложения",
|
||||||
|
"clear_history": "Очистить",
|
||||||
|
"remove_from_history": "Удалить из истории",
|
||||||
|
"loading": "Загрузка...",
|
||||||
|
"no_results": "Нет результатов",
|
||||||
"home": "Главная",
|
"home": "Главная",
|
||||||
"catalogue": "Каталог",
|
"catalogue": "Каталог",
|
||||||
|
"library": "Библиотека",
|
||||||
"downloads": "Загрузки",
|
"downloads": "Загрузки",
|
||||||
"search_results": "Результаты поиска",
|
"search_results": "Результаты поиска",
|
||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ const resumeGameDownload = async (
|
|||||||
|
|
||||||
const download = await downloadsSublevel.get(gameKey);
|
const download = await downloadsSublevel.get(gameKey);
|
||||||
|
|
||||||
if (download?.status === "paused") {
|
if (
|
||||||
|
download &&
|
||||||
|
(download.status === "paused" || download.status === "active") &&
|
||||||
|
download.progress !== 1
|
||||||
|
) {
|
||||||
await DownloadManager.pauseDownload();
|
await DownloadManager.pauseDownload();
|
||||||
|
|
||||||
for await (const [key, value] of downloadsSublevel.iterator()) {
|
for await (const [key, value] of downloadsSublevel.iterator()) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { downloadsSublevel } from "./level/sublevels/downloads";
|
import { downloadsSublevel } from "./level/sublevels/downloads";
|
||||||
import { sortBy } from "lodash-es";
|
import { orderBy } from "lodash-es";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
import { levelKeys, db } from "./level";
|
import { levelKeys, db } from "./level";
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
@@ -33,9 +33,7 @@ export const loadState = async () => {
|
|||||||
|
|
||||||
await import("./events");
|
await import("./events");
|
||||||
|
|
||||||
if (process.platform !== "darwin") {
|
Aria2.spawn();
|
||||||
Aria2.spawn();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
if (userPreferences?.realDebridApiToken) {
|
||||||
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||||
@@ -68,7 +66,7 @@ export const loadState = async () => {
|
|||||||
.values()
|
.values()
|
||||||
.all()
|
.all()
|
||||||
.then((games) => {
|
.then((games) => {
|
||||||
return sortBy(games, "timestamp", "DESC");
|
return orderBy(games, "timestamp", "desc");
|
||||||
});
|
});
|
||||||
|
|
||||||
downloads.forEach((download) => {
|
downloads.forEach((download) => {
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ export class Aria2 {
|
|||||||
private static process: cp.ChildProcess | null = null;
|
private static process: cp.ChildProcess | null = null;
|
||||||
|
|
||||||
public static spawn() {
|
public static spawn() {
|
||||||
const binaryPath = app.isPackaged
|
const binaryPath =
|
||||||
? path.join(process.resourcesPath, "aria2c")
|
process.platform === "darwin"
|
||||||
: path.join(__dirname, "..", "..", "binaries", "aria2c");
|
? "aria2c"
|
||||||
|
: app.isPackaged
|
||||||
|
? path.join(process.resourcesPath, "aria2c")
|
||||||
|
: path.join(__dirname, "..", "..", "binaries", "aria2c");
|
||||||
|
|
||||||
this.process = cp.spawn(
|
this.process = cp.spawn(
|
||||||
binaryPath,
|
binaryPath,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { RealDebridClient } from "./real-debrid";
|
|||||||
import path from "path";
|
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 { orderBy } from "lodash-es";
|
||||||
import { TorBoxClient } from "./torbox";
|
import { TorBoxClient } from "./torbox";
|
||||||
import { GameFilesManager } from "../game-files-manager";
|
import { GameFilesManager } from "../game-files-manager";
|
||||||
import { HydraDebridClient } from "./hydra-debrid";
|
import { HydraDebridClient } from "./hydra-debrid";
|
||||||
@@ -194,10 +194,10 @@ export class DownloadManager {
|
|||||||
.values()
|
.values()
|
||||||
.all()
|
.all()
|
||||||
.then((games) => {
|
.then((games) => {
|
||||||
return sortBy(
|
return orderBy(
|
||||||
games.filter((game) => game.status === "paused" && game.queued),
|
games.filter((game) => game.status === "paused" && game.queued),
|
||||||
"timestamp",
|
"timestamp",
|
||||||
"DESC"
|
"desc"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,16 +36,13 @@ export class GofileApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async getDownloadLink(id: string) {
|
public static async getDownloadLink(id: string) {
|
||||||
const searchParams = new URLSearchParams({
|
|
||||||
wt: WT,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await axios.get<{
|
const response = await axios.get<{
|
||||||
status: string;
|
status: string;
|
||||||
data: GofileContentsResponse;
|
data: GofileContentsResponse;
|
||||||
}>(`https://api.gofile.io/contents/${id}?${searchParams.toString()}`, {
|
}>(`https://api.gofile.io/contents/${id}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.token}`,
|
Authorization: `Bearer ${this.token}`,
|
||||||
|
"X-Website-Token": WT,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,13 @@ export class HydraApi {
|
|||||||
const decodedBase64 = atob(payload as string);
|
const decodedBase64 = atob(payload as string);
|
||||||
const jsonData = JSON.parse(decodedBase64);
|
const jsonData = JSON.parse(decodedBase64);
|
||||||
|
|
||||||
const { accessToken, expiresIn, refreshToken } = jsonData;
|
const {
|
||||||
|
accessToken,
|
||||||
|
expiresIn,
|
||||||
|
refreshToken,
|
||||||
|
featurebaseJwt,
|
||||||
|
workwondersJwt,
|
||||||
|
} = jsonData;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
@@ -85,6 +91,8 @@ export class HydraApi {
|
|||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
tokenExpirationTimestamp,
|
tokenExpirationTimestamp,
|
||||||
|
featurebaseJwt,
|
||||||
|
workwondersJwt,
|
||||||
},
|
},
|
||||||
{ valueEncoding: "json" }
|
{ valueEncoding: "json" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ export class WindowManager {
|
|||||||
(details, callback) => {
|
(details, callback) => {
|
||||||
if (
|
if (
|
||||||
details.webContentsId !== this.mainWindow?.webContents.id ||
|
details.webContentsId !== this.mainWindow?.webContents.id ||
|
||||||
details.url.includes("chatwoot")
|
details.url.includes("chatwoot") ||
|
||||||
|
details.url.includes("workwonders")
|
||||||
) {
|
) {
|
||||||
return callback(details);
|
return callback(details);
|
||||||
}
|
}
|
||||||
@@ -159,7 +160,8 @@ export class WindowManager {
|
|||||||
if (
|
if (
|
||||||
details.webContentsId !== this.mainWindow?.webContents.id ||
|
details.webContentsId !== this.mainWindow?.webContents.id ||
|
||||||
details.url.includes("featurebase") ||
|
details.url.includes("featurebase") ||
|
||||||
details.url.includes("chatwoot")
|
details.url.includes("chatwoot") ||
|
||||||
|
details.url.includes("workwonders")
|
||||||
) {
|
) {
|
||||||
return callback(details);
|
return callback(details);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>Hydra Launcher</title>
|
<title>Hydra Launcher</title>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self' 'unsafe-inline' * data: local:;"
|
content="default-src 'self' 'unsafe-inline' * data: local:; media-src 'self' 'unsafe-inline' * data: local: blob:;"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -122,10 +122,10 @@ export function BottomPanel() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-featurebase-changelog
|
data-open-workwonders-changelog-mini
|
||||||
className="bottom-panel__version-button"
|
className="bottom-panel__version-button"
|
||||||
>
|
>
|
||||||
<small data-featurebase-changelog>
|
<small>
|
||||||
{sessionHash ? `${sessionHash} -` : ""} v{version} "
|
{sessionHash ? `${sessionHash} -` : ""} v{version} "
|
||||||
{VERSION_CODENAME}"
|
{VERSION_CODENAME}"
|
||||||
</small>
|
</small>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface DropdownMenuProps {
|
|||||||
side?: "top" | "bottom" | "left" | "right";
|
side?: "top" | "bottom" | "left" | "right";
|
||||||
align?: "start" | "center" | "end";
|
align?: "start" | "center" | "end";
|
||||||
alignOffset?: number;
|
alignOffset?: number;
|
||||||
|
collisionPadding?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenu({
|
export function DropdownMenu({
|
||||||
@@ -29,6 +30,7 @@ export function DropdownMenu({
|
|||||||
loop = true,
|
loop = true,
|
||||||
align = "center",
|
align = "center",
|
||||||
alignOffset = 0,
|
alignOffset = 0,
|
||||||
|
collisionPadding = 16,
|
||||||
}: Readonly<DropdownMenuProps>) {
|
}: Readonly<DropdownMenuProps>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Root>
|
<DropdownMenuPrimitive.Root>
|
||||||
@@ -43,6 +45,7 @@ export function DropdownMenu({
|
|||||||
loop={loop}
|
loop={loop}
|
||||||
align={align}
|
align={align}
|
||||||
alignOffset={alignOffset}
|
alignOffset={alignOffset}
|
||||||
|
collisionPadding={collisionPadding}
|
||||||
className="dropdown-menu__content"
|
className="dropdown-menu__content"
|
||||||
>
|
>
|
||||||
{title && (
|
{title && (
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { AutoUpdateSubHeader } from "./auto-update-sub-header";
|
|||||||
import { setFilters, setLibrarySearchQuery } from "@renderer/features";
|
import { setFilters, setLibrarySearchQuery } from "@renderer/features";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { SearchDropdown } from "@renderer/components";
|
import { SearchDropdown } from "@renderer/components";
|
||||||
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const pathTitle: Record<string, string> = {
|
const pathTitle: Record<string, string> = {
|
||||||
"/": "home",
|
"/": "home",
|
||||||
@@ -161,11 +163,11 @@ export function Header() {
|
|||||||
const handleSelectSuggestion = (suggestion: {
|
const handleSelectSuggestion = (suggestion: {
|
||||||
title: string;
|
title: string;
|
||||||
objectId: string;
|
objectId: string;
|
||||||
shop: string;
|
shop: GameShop;
|
||||||
}) => {
|
}) => {
|
||||||
setIsDropdownVisible(false);
|
setIsDropdownVisible(false);
|
||||||
inputRef.current?.blur();
|
inputRef.current?.blur();
|
||||||
navigate(`/game/${suggestion.shop}/${suggestion.objectId}`);
|
navigate(buildGameDetailsPath(suggestion));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearSearch = () => {
|
const handleClearSearch = () => {
|
||||||
|
|||||||
@@ -19,24 +19,25 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
|
|||||||
return <>{text}</>;
|
return <>{text}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const textWords = text.split(/\b/);
|
const matches: { start: number; end: number }[] = [];
|
||||||
const matches: { start: number; end: number; text: string }[] = [];
|
const textLower = text.toLowerCase();
|
||||||
|
|
||||||
let currentIndex = 0;
|
queryWords.forEach((queryWord) => {
|
||||||
textWords.forEach((word) => {
|
const escapedQuery = queryWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const wordLower = word.toLowerCase();
|
const regex = new RegExp(
|
||||||
|
`(?:^|[\\s])${escapedQuery}(?=[\\s]|$)|^${escapedQuery}$`,
|
||||||
|
"gi"
|
||||||
|
);
|
||||||
|
|
||||||
queryWords.forEach((queryWord) => {
|
let match;
|
||||||
if (wordLower === queryWord) {
|
while ((match = regex.exec(textLower)) !== null) {
|
||||||
matches.push({
|
const matchedText = match[0];
|
||||||
start: currentIndex,
|
const leadingSpace = matchedText.startsWith(" ") ? 1 : 0;
|
||||||
end: currentIndex + word.length,
|
const start = match.index + leadingSpace;
|
||||||
text: word,
|
const end = start + queryWord.length;
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
currentIndex += word.length;
|
matches.push({ start, end });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (matches.length === 0) {
|
if (matches.length === 0) {
|
||||||
@@ -46,16 +47,14 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
|
|||||||
matches.sort((a, b) => a.start - b.start);
|
matches.sort((a, b) => a.start - b.start);
|
||||||
|
|
||||||
const mergedMatches: { start: number; end: number }[] = [];
|
const mergedMatches: { start: number; end: number }[] = [];
|
||||||
|
|
||||||
if (matches.length === 0) {
|
|
||||||
return <>{text}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let current = matches[0];
|
let current = matches[0];
|
||||||
|
|
||||||
for (let i = 1; i < matches.length; i++) {
|
for (let i = 1; i < matches.length; i++) {
|
||||||
if (matches[i].start <= current.end) {
|
if (matches[i].start <= current.end) {
|
||||||
current.end = Math.max(current.end, matches[i].end);
|
current = {
|
||||||
|
start: current.start,
|
||||||
|
end: Math.max(current.end, matches[i].end),
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
mergedMatches.push(current);
|
mergedMatches.push(current);
|
||||||
current = matches[i];
|
current = matches[i];
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 12px 4px;
|
padding: 8px 12px 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__section-title {
|
&__section-title {
|
||||||
@@ -35,19 +36,19 @@
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__clear-button {
|
&__clear-text-button {
|
||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px;
|
padding: 0;
|
||||||
border-radius: 4px;
|
font-size: 11px;
|
||||||
transition: all ease 0.2s;
|
font-weight: bold;
|
||||||
display: flex;
|
text-transform: uppercase;
|
||||||
align-items: center;
|
transition: color ease 0.2s;
|
||||||
justify-content: center;
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,9 +75,8 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 4px;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: all ease 0.15s;
|
transition: opacity ease 0.15s;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { useEffect, useRef, useCallback, useState } from "react";
|
import { useEffect, useRef, useCallback, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import {
|
import { ClockIcon, SearchIcon, XIcon } from "@primer/octicons-react";
|
||||||
ClockIcon,
|
|
||||||
SearchIcon,
|
|
||||||
TrashIcon,
|
|
||||||
XIcon,
|
|
||||||
} from "@primer/octicons-react";
|
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { SearchHistoryEntry } from "@renderer/hooks/use-search-history";
|
import type { SearchHistoryEntry } from "@renderer/hooks/use-search-history";
|
||||||
@@ -144,11 +139,10 @@ export function SearchDropdown({
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="search-dropdown__clear-button"
|
className="search-dropdown__clear-text-button"
|
||||||
onClick={onClearHistory}
|
onClick={onClearHistory}
|
||||||
title={t("clear_history")}
|
|
||||||
>
|
>
|
||||||
<TrashIcon size={14} />
|
{t("clear_history")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ul className="search-dropdown__list">
|
<ul className="search-dropdown__list">
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ export * from "./use-download-options-listener";
|
|||||||
export * from "./use-game-card";
|
export * from "./use-game-card";
|
||||||
export * from "./use-search-history";
|
export * from "./use-search-history";
|
||||||
export * from "./use-search-suggestions";
|
export * from "./use-search-suggestions";
|
||||||
|
export * from "./use-hls-video";
|
||||||
|
|||||||
102
src/renderer/src/hooks/use-hls-video.ts
Normal file
102
src/renderer/src/hooks/use-hls-video.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import Hls from "hls.js";
|
||||||
|
import { logger } from "@renderer/logger";
|
||||||
|
|
||||||
|
interface UseHlsVideoOptions {
|
||||||
|
videoSrc: string | undefined;
|
||||||
|
videoType: string | undefined;
|
||||||
|
autoplay?: boolean;
|
||||||
|
muted?: boolean;
|
||||||
|
loop?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHlsVideo(
|
||||||
|
videoRef: React.RefObject<HTMLVideoElement>,
|
||||||
|
{ videoSrc, videoType, autoplay, muted, loop }: UseHlsVideoOptions
|
||||||
|
) {
|
||||||
|
const hlsRef = useRef<Hls | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video || !videoSrc) return;
|
||||||
|
|
||||||
|
const isHls = videoType === "application/x-mpegURL";
|
||||||
|
|
||||||
|
if (!isHls) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
const hls = new Hls({
|
||||||
|
enableWorker: true,
|
||||||
|
lowLatencyMode: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
hlsRef.current = hls;
|
||||||
|
|
||||||
|
hls.loadSource(videoSrc);
|
||||||
|
hls.attachMedia(video);
|
||||||
|
|
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
|
if (autoplay) {
|
||||||
|
video.play().catch((err) => {
|
||||||
|
logger.warn("Failed to autoplay HLS video:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
hls.on(Hls.Events.ERROR, (_event, data) => {
|
||||||
|
if (data.fatal) {
|
||||||
|
switch (data.type) {
|
||||||
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||||
|
logger.error("HLS network error, trying to recover");
|
||||||
|
hls.startLoad();
|
||||||
|
break;
|
||||||
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||||
|
logger.error("HLS media error, trying to recover");
|
||||||
|
hls.recoverMediaError();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.error("HLS fatal error, destroying instance");
|
||||||
|
hls.destroy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
hls.destroy();
|
||||||
|
hlsRef.current = null;
|
||||||
|
};
|
||||||
|
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
||||||
|
video.src = videoSrc;
|
||||||
|
video.load();
|
||||||
|
if (autoplay) {
|
||||||
|
video.play().catch((err) => {
|
||||||
|
logger.warn("Failed to autoplay HLS video:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
video.src = "";
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
logger.warn("HLS playback is not supported in this browser");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, [videoRef, videoSrc, videoType, autoplay, muted, loop]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video) return;
|
||||||
|
|
||||||
|
if (muted !== undefined) {
|
||||||
|
video.muted = muted;
|
||||||
|
}
|
||||||
|
if (loop !== undefined) {
|
||||||
|
video.loop = loop;
|
||||||
|
}
|
||||||
|
}, [videoRef, muted, loop]);
|
||||||
|
|
||||||
|
return hlsRef.current;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useCallback, useEffect } from "react";
|
import { useState, useCallback, useEffect, useRef } from "react";
|
||||||
|
import { levelDBService } from "@renderer/services/leveldb.service";
|
||||||
|
|
||||||
export interface SearchHistoryEntry {
|
export interface SearchHistoryEntry {
|
||||||
query: string;
|
query: string;
|
||||||
@@ -6,22 +7,32 @@ export interface SearchHistoryEntry {
|
|||||||
context: "library" | "catalogue";
|
context: "library" | "catalogue";
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = "search-history";
|
const LEVELDB_KEY = "searchHistory";
|
||||||
const MAX_HISTORY_ENTRIES = 15;
|
const MAX_HISTORY_ENTRIES = 15;
|
||||||
|
|
||||||
export function useSearchHistory() {
|
export function useSearchHistory() {
|
||||||
const [history, setHistory] = useState<SearchHistoryEntry[]>([]);
|
const [history, setHistory] = useState<SearchHistoryEntry[]>([]);
|
||||||
|
const isInitialized = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
const loadHistory = async () => {
|
||||||
if (stored) {
|
if (isInitialized.current) return;
|
||||||
|
isInitialized.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(stored) as SearchHistoryEntry[];
|
const data = (await levelDBService.get(LEVELDB_KEY, null, "json")) as
|
||||||
setHistory(parsed);
|
| SearchHistoryEntry[]
|
||||||
|
| null;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
setHistory(data);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
setHistory([]);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
loadHistory();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addToHistory = useCallback(
|
const addToHistory = useCallback(
|
||||||
@@ -39,7 +50,7 @@ export function useSearchHistory() {
|
|||||||
(entry) => entry.query.toLowerCase() !== query.toLowerCase().trim()
|
(entry) => entry.query.toLowerCase() !== query.toLowerCase().trim()
|
||||||
);
|
);
|
||||||
const updated = [newEntry, ...filtered].slice(0, MAX_HISTORY_ENTRIES);
|
const updated = [newEntry, ...filtered].slice(0, MAX_HISTORY_ENTRIES);
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
levelDBService.put(LEVELDB_KEY, updated, null, "json");
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -49,14 +60,14 @@ export function useSearchHistory() {
|
|||||||
const removeFromHistory = useCallback((query: string) => {
|
const removeFromHistory = useCallback((query: string) => {
|
||||||
setHistory((prev) => {
|
setHistory((prev) => {
|
||||||
const updated = prev.filter((entry) => entry.query !== query);
|
const updated = prev.filter((entry) => entry.query !== query);
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
levelDBService.put(LEVELDB_KEY, updated, null, "json");
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const clearHistory = useCallback(() => {
|
const clearHistory = useCallback(() => {
|
||||||
setHistory([]);
|
setHistory([]);
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
levelDBService.del(LEVELDB_KEY, null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getRecentHistory = useCallback(
|
const getRecentHistory = useCallback(
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { useState, useEffect, useCallback, useRef } from "react";
|
|||||||
import { useAppSelector } from "./redux";
|
import { useAppSelector } from "./redux";
|
||||||
import { debounce } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
import { logger } from "@renderer/logger";
|
import { logger } from "@renderer/logger";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
export interface SearchSuggestion {
|
export interface SearchSuggestion {
|
||||||
title: string;
|
title: string;
|
||||||
objectId: string;
|
objectId: string;
|
||||||
shop: string;
|
shop: GameShop;
|
||||||
iconUrl: string | null;
|
iconUrl: string | null;
|
||||||
source: "library" | "catalogue";
|
source: "library" | "catalogue";
|
||||||
}
|
}
|
||||||
@@ -20,6 +21,7 @@ export function useSearchSuggestions(
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const library = useAppSelector((state) => state.library.value);
|
const library = useAppSelector((state) => state.library.value);
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
const cacheRef = useRef<Map<string, SearchSuggestion[]>>(new Map());
|
||||||
|
|
||||||
const getLibrarySuggestions = useCallback(
|
const getLibrarySuggestions = useCallback(
|
||||||
(searchQuery: string, limit: number = 3): SearchSuggestion[] => {
|
(searchQuery: string, limit: number = 3): SearchSuggestion[] => {
|
||||||
@@ -68,6 +70,15 @@ export function useSearchSuggestions(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cacheKey = `${searchQuery.toLowerCase()}_${limit}`;
|
||||||
|
const cachedResults = cacheRef.current.get(cacheKey);
|
||||||
|
|
||||||
|
if (cachedResults) {
|
||||||
|
setSuggestions(cachedResults);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
abortControllerRef.current?.abort();
|
abortControllerRef.current?.abort();
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
abortControllerRef.current = abortController;
|
abortControllerRef.current = abortController;
|
||||||
@@ -79,7 +90,7 @@ export function useSearchSuggestions(
|
|||||||
{
|
{
|
||||||
title: string;
|
title: string;
|
||||||
objectId: string;
|
objectId: string;
|
||||||
shop: string;
|
shop: GameShop;
|
||||||
iconUrl: string | null;
|
iconUrl: string | null;
|
||||||
}[]
|
}[]
|
||||||
>("/catalogue/search/suggestions", {
|
>("/catalogue/search/suggestions", {
|
||||||
@@ -99,6 +110,7 @@ export function useSearchSuggestions(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
cacheRef.current.set(cacheKey, catalogueSuggestions);
|
||||||
setSuggestions(catalogueSuggestions);
|
setSuggestions(catalogueSuggestions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!abortController.signal.aborted) {
|
if (!abortController.signal.aborted) {
|
||||||
|
|||||||
@@ -4,158 +4,537 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
margin-inline: calc(globals.$spacing-unit * 3);
|
||||||
|
padding-block: calc(globals.$spacing-unit * 3);
|
||||||
|
|
||||||
&__details-with-article {
|
&--queued {
|
||||||
display: flex;
|
padding-bottom: 0;
|
||||||
align-items: center;
|
}
|
||||||
gap: calc(globals.$spacing-unit / 2);
|
|
||||||
align-self: flex-start;
|
&--completed {
|
||||||
cursor: pointer;
|
padding-top: calc(globals.$spacing-unit * 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: calc(globals.$spacing-unit);
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
|
||||||
|
|
||||||
&-divider {
|
&-title-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: globals.$border-color;
|
|
||||||
height: 1px;
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-count {
|
&-count {
|
||||||
font-weight: 400;
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&--hero {
|
||||||
&__title-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: globals.$spacing-unit;
|
|
||||||
gap: globals.$spacing-unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
color: globals.$body-color;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 16px;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__downloads {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
position: relative;
|
||||||
display: flex;
|
overflow: hidden;
|
||||||
flex-direction: column;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-top: globals.$spacing-unit;
|
padding-bottom: globals.$spacing-unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&__hero-background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: globals.$background-color;
|
height: 120%;
|
||||||
display: flex;
|
z-index: 0;
|
||||||
border-radius: 8px;
|
|
||||||
border: solid 1px globals.$border-color;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0px 0px 5px 0px #000000;
|
|
||||||
transition: all ease 0.2s;
|
|
||||||
height: 140px;
|
|
||||||
min-height: 140px;
|
|
||||||
max-height: 140px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&--hydra {
|
img {
|
||||||
box-shadow: 0px 0px 16px 0px rgba(12, 241, 202, 0.15);
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: 50% 20%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&__cover {
|
// PLEASE FIX THE COLORS
|
||||||
width: 280px;
|
&__hero-overlay {
|
||||||
min-width: 280px;
|
position: absolute;
|
||||||
height: auto;
|
top: 0;
|
||||||
border-right: solid 1px globals.$border-color;
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(0, 0, 0, 0.3) 0%,
|
||||||
|
rgb(5, 5, 5) 70%,
|
||||||
|
rgb(26, 26, 26) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hero-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
padding: calc(globals.$spacing-unit * 4);
|
||||||
&-content {
|
padding-bottom: 0;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: globals.$spacing-unit;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-backdrop {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(
|
|
||||||
0deg,
|
|
||||||
rgba(0, 0, 0, 0.8) 5%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__right-content {
|
|
||||||
display: flex;
|
|
||||||
padding: calc(globals.$spacing-unit * 2);
|
|
||||||
flex: 1;
|
|
||||||
gap: globals.$spacing-unit;
|
|
||||||
background: linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__details {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
justify-content: center;
|
|
||||||
gap: calc(globals.$spacing-unit / 2);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__actions {
|
&__hero-logo {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: globals.$spacing-unit;
|
|
||||||
|
&-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: scale 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
scale: 1.05;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 180px;
|
||||||
|
max-height: 60px;
|
||||||
|
object-fit: contain;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid rgba(255, 255, 255, 0.5);
|
||||||
|
outline-offset: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 700px) {
|
||||||
|
max-width: 220px;
|
||||||
|
max-height: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 900px) {
|
||||||
|
max-width: 280px;
|
||||||
|
max-height: 95px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 1200px) {
|
||||||
|
max-width: 340px;
|
||||||
|
max-height: 115px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 1500px) {
|
||||||
|
max-width: 400px;
|
||||||
|
max-height: 130px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 2px 2px 12px rgba(0, 0, 0, 0.9);
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid rgba(255, 255, 255, 0.5);
|
||||||
|
outline-offset: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 700px) {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 900px) {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 1200px) {
|
||||||
|
font-size: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 1500px) {
|
||||||
|
font-size: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__menu-button {
|
&__hero-action-row {
|
||||||
position: absolute;
|
display: flex;
|
||||||
top: 12px;
|
justify-content: space-between;
|
||||||
right: 12px;
|
align-items: flex-start;
|
||||||
border-radius: 50%;
|
gap: calc(globals.$spacing-unit * 3);
|
||||||
|
margin-top: calc(globals.$spacing-unit * 4);
|
||||||
|
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hero-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__glass-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hero-progress {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-info-row {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
|
||||||
|
&--bar {
|
||||||
|
margin-top: calc(globals.$spacing-unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-status {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-percentage {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
align-self: flex-end;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.2;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-size {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-status {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-time {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
|
font-size: 13px;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hero-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(globals.$spacing-unit * 4);
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(26, 26, 26, 0.1);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
margin-top: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stats-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
min-width: 200px;
|
||||||
|
padding-right: calc(globals.$spacing-unit * 2);
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__speed-chart {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__speed-chart-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
opacity: 0.8;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-label {
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-value {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-list {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-thumbnail {
|
||||||
|
width: 200px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid globals.$border-color;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
opacity 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid rgba(255, 255, 255, 0.5);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit / 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-title-button {
|
||||||
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 8px;
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
font-size: 13px;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-size {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-extracting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
font-weight: 500;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-seeding {
|
||||||
|
color: #4ade80;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-progress {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
width: 200px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-progress-text {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-menu-btn {
|
||||||
|
padding: calc(globals.$spacing-unit);
|
||||||
min-height: unset;
|
min-height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__hydra-gradient {
|
&__progress-wrapper {
|
||||||
background: linear-gradient(90deg, #01483c 0%, #0cf1ca 50%, #01483c 100%);
|
flex: 1;
|
||||||
box-shadow: 0px 0px 8px 0px rgba(12, 241, 202, 0.15);
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: absolute;
|
height: 8px;
|
||||||
bottom: 0;
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
height: 2px;
|
border-radius: 4px;
|
||||||
z-index: 1;
|
overflow: hidden;
|
||||||
|
margin-top: calc(globals.$spacing-unit / 2);
|
||||||
|
|
||||||
|
&--small {
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
|||||||
.downloads {
|
.downloads {
|
||||||
&__container {
|
&__container {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: calc(globals.$spacing-unit * 3);
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import useEmblaCarousel from "embla-carousel-react";
|
import useEmblaCarousel from "embla-carousel-react";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import { useAppSelector } from "@renderer/hooks";
|
import { useAppSelector } from "@renderer/hooks";
|
||||||
|
import { VideoPlayer } from "./video-player";
|
||||||
import "./gallery-slider.scss";
|
import "./gallery-slider.scss";
|
||||||
|
|
||||||
export function GallerySlider() {
|
export function GallerySlider() {
|
||||||
@@ -106,8 +107,6 @@ export function GallerySlider() {
|
|||||||
|
|
||||||
if (shopDetails?.movies) {
|
if (shopDetails?.movies) {
|
||||||
shopDetails.movies.forEach((video, index) => {
|
shopDetails.movies.forEach((video, index) => {
|
||||||
// Prefer new formats: HLS (best browser support), then DASH H264, then DASH AV1
|
|
||||||
// Fallback to old format: mp4/webm if new formats are not available
|
|
||||||
let videoSrc: string | undefined;
|
let videoSrc: string | undefined;
|
||||||
let videoType: string | undefined;
|
let videoType: string | undefined;
|
||||||
|
|
||||||
@@ -121,11 +120,9 @@ export function GallerySlider() {
|
|||||||
videoSrc = video.dash_av1;
|
videoSrc = video.dash_av1;
|
||||||
videoType = "application/dash+xml";
|
videoType = "application/dash+xml";
|
||||||
} else if (video.mp4?.max) {
|
} else if (video.mp4?.max) {
|
||||||
// Fallback to old format
|
|
||||||
videoSrc = video.mp4.max;
|
videoSrc = video.mp4.max;
|
||||||
videoType = "video/mp4";
|
videoType = "video/mp4";
|
||||||
} else if (video.webm?.max) {
|
} else if (video.webm?.max) {
|
||||||
// Fallback to webm if mp4 is not available
|
|
||||||
videoSrc = video.webm.max;
|
videoSrc = video.webm.max;
|
||||||
videoType = "video/webm";
|
videoType = "video/webm";
|
||||||
}
|
}
|
||||||
@@ -191,19 +188,17 @@ export function GallerySlider() {
|
|||||||
{mediaItems.map((item) => (
|
{mediaItems.map((item) => (
|
||||||
<div key={item.id} className="gallery-slider__slide">
|
<div key={item.id} className="gallery-slider__slide">
|
||||||
{item.type === "video" ? (
|
{item.type === "video" ? (
|
||||||
<video
|
<VideoPlayer
|
||||||
controls
|
videoSrc={item.videoSrc}
|
||||||
className="gallery-slider__media"
|
videoType={item.videoType}
|
||||||
poster={item.poster}
|
poster={item.poster}
|
||||||
|
autoplay={autoplayEnabled}
|
||||||
loop
|
loop
|
||||||
muted
|
muted
|
||||||
autoPlay={autoplayEnabled}
|
controls
|
||||||
|
className="gallery-slider__media"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
/>
|
||||||
{item.videoSrc && (
|
|
||||||
<source src={item.videoSrc} type={item.videoType} />
|
|
||||||
)}
|
|
||||||
</video>
|
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
className="gallery-slider__media"
|
className="gallery-slider__media"
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { useHlsVideo } from "@renderer/hooks";
|
||||||
|
|
||||||
|
interface VideoPlayerProps {
|
||||||
|
videoSrc?: string;
|
||||||
|
videoType?: string;
|
||||||
|
poster?: string;
|
||||||
|
autoplay?: boolean;
|
||||||
|
muted?: boolean;
|
||||||
|
loop?: boolean;
|
||||||
|
controls?: boolean;
|
||||||
|
tabIndex?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VideoPlayer({
|
||||||
|
videoSrc,
|
||||||
|
videoType,
|
||||||
|
poster,
|
||||||
|
autoplay = false,
|
||||||
|
muted = true,
|
||||||
|
loop = false,
|
||||||
|
controls = true,
|
||||||
|
tabIndex = -1,
|
||||||
|
className,
|
||||||
|
}: VideoPlayerProps) {
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
const isHls = videoType === "application/x-mpegURL";
|
||||||
|
|
||||||
|
useHlsVideo(videoRef, {
|
||||||
|
videoSrc,
|
||||||
|
videoType,
|
||||||
|
autoplay,
|
||||||
|
muted,
|
||||||
|
loop,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isHls) {
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
controls={controls}
|
||||||
|
className={className}
|
||||||
|
poster={poster}
|
||||||
|
loop={loop}
|
||||||
|
muted={muted}
|
||||||
|
autoPlay={autoplay}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
>
|
||||||
|
<track kind="captions" />
|
||||||
|
</video>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
controls={controls}
|
||||||
|
className={className}
|
||||||
|
poster={poster}
|
||||||
|
loop={loop}
|
||||||
|
muted={muted}
|
||||||
|
autoPlay={autoplay}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
>
|
||||||
|
{videoSrc && <source src={videoSrc} type={videoType} />}
|
||||||
|
<track kind="captions" />
|
||||||
|
</video>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -231,9 +231,19 @@ export function RepacksModal({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastCheckUtc = new Date(lastCheckTimestamp).toISOString();
|
try {
|
||||||
|
const lastCheckDate = new Date(lastCheckTimestamp);
|
||||||
|
|
||||||
return repack.createdAt > lastCheckUtc;
|
if (isNaN(lastCheckDate.getTime())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastCheckUtc = lastCheckDate.toISOString();
|
||||||
|
|
||||||
|
return repack.createdAt > lastCheckUtc;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
||||||
|
|||||||
@@ -76,7 +76,13 @@ export default function Library() {
|
|||||||
|
|
||||||
switch (filterBy) {
|
switch (filterBy) {
|
||||||
case "recently_played":
|
case "recently_played":
|
||||||
filtered = library.filter((game) => game.lastTimePlayed !== null);
|
filtered = library
|
||||||
|
.filter((game) => game.lastTimePlayed !== null)
|
||||||
|
.sort(
|
||||||
|
(a: any, b: any) =>
|
||||||
|
new Date(b.lastTimePlayed).getTime() -
|
||||||
|
new Date(a.lastTimePlayed).getTime()
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case "favorites":
|
case "favorites":
|
||||||
filtered = library.filter((game) => game.favorite);
|
filtered = library.filter((game) => game.favorite);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export interface Auth {
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
tokenExpirationTimestamp: number;
|
tokenExpirationTimestamp: number;
|
||||||
|
featurebaseJwt: string;
|
||||||
|
workwondersJwt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface SteamVideoSource {
|
|||||||
"480": string;
|
"480": string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SteamMovies {
|
export interface SteamMovie {
|
||||||
id: number;
|
id: number;
|
||||||
dash_av1?: string;
|
dash_av1?: string;
|
||||||
dash_h264?: string;
|
dash_h264?: string;
|
||||||
@@ -34,7 +34,7 @@ export interface SteamAppDetails {
|
|||||||
short_description: string;
|
short_description: string;
|
||||||
publishers: string[];
|
publishers: string[];
|
||||||
genres: SteamGenre[];
|
genres: SteamGenre[];
|
||||||
movies?: SteamMovies[];
|
movies?: SteamMovie[];
|
||||||
supported_languages: string;
|
supported_languages: string;
|
||||||
screenshots?: SteamScreenshot[];
|
screenshots?: SteamScreenshot[];
|
||||||
pc_requirements: {
|
pc_requirements: {
|
||||||
|
|||||||
29
yarn.lock
29
yarn.lock
@@ -5690,6 +5690,11 @@ hasown@^2.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.2"
|
function-bind "^1.1.2"
|
||||||
|
|
||||||
|
hls.js@^1.5.12:
|
||||||
|
version "1.6.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.6.15.tgz#9ce13080d143a9bc9b903fb43f081e335b8321e5"
|
||||||
|
integrity sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==
|
||||||
|
|
||||||
hoist-non-react-statics@^3.3.2:
|
hoist-non-react-statics@^3.3.2:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
@@ -6205,9 +6210,9 @@ jiti@^2.6.1:
|
|||||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||||
|
|
||||||
js-yaml@^4.1.0:
|
js-yaml@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
|
||||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse "^2.0.1"
|
argparse "^2.0.1"
|
||||||
|
|
||||||
@@ -6325,7 +6330,7 @@ jsonwebtoken@^9.0.2:
|
|||||||
object.assign "^4.1.4"
|
object.assign "^4.1.4"
|
||||||
object.values "^1.1.6"
|
object.values "^1.1.6"
|
||||||
|
|
||||||
jwa@^1.4.1:
|
jwa@^1.4.2:
|
||||||
version "1.4.2"
|
version "1.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
|
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
|
||||||
integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
|
integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
|
||||||
@@ -6335,11 +6340,11 @@ jwa@^1.4.1:
|
|||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
jws@^3.2.2:
|
jws@^3.2.2:
|
||||||
version "3.2.2"
|
version "3.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
|
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.3.tgz#5ac0690b460900a27265de24520526853c0b8ca1"
|
||||||
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
|
integrity sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==
|
||||||
dependencies:
|
dependencies:
|
||||||
jwa "^1.4.1"
|
jwa "^1.4.2"
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
keyv@^4.0.0, keyv@^4.5.3:
|
keyv@^4.0.0, keyv@^4.5.3:
|
||||||
@@ -8518,10 +8523,10 @@ tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.2.1:
|
|||||||
mkdirp "^1.0.3"
|
mkdirp "^1.0.3"
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
tar@^7.4.3:
|
tar@^7.5.2:
|
||||||
version "7.5.1"
|
version "7.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.1.tgz#750a8bd63b7c44c1848e7bf982260a083cf747c9"
|
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.2.tgz#115c061495ec51ff3c6745ff8f6d0871c5b1dedc"
|
||||||
integrity sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==
|
integrity sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@isaacs/fs-minipass" "^4.0.0"
|
"@isaacs/fs-minipass" "^4.0.0"
|
||||||
chownr "^3.0.0"
|
chownr "^3.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user