mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-19 01:03:57 +00:00
Compare commits
7 Commits
feat/custo
...
feat/rever
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c87964e16 | ||
|
|
776859c58e | ||
|
|
cb0fc82644 | ||
|
|
a625541125 | ||
|
|
300cff2be6 | ||
|
|
4f5c345c42 | ||
|
|
d513377f1c |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
os: [windows-2022, ubuntu-latest]
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: yarn build:win
|
||||
env:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
os: [windows-2022, ubuntu-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: yarn build:win
|
||||
env:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||
|
||||
@@ -70,7 +70,13 @@
|
||||
"edit_game_modal_icon_resolution": "Recommended resolution: 256x256px",
|
||||
"edit_game_modal_logo_resolution": "Recommended resolution: 640x360px",
|
||||
"edit_game_modal_hero_resolution": "Recommended resolution: 1920x620px",
|
||||
"edit_game_modal_assets": "Assets"
|
||||
"edit_game_modal_assets": "Assets",
|
||||
"edit_game_modal_drop_icon_image_here": "Drop icon image here",
|
||||
"edit_game_modal_drop_logo_image_here": "Drop logo image here",
|
||||
"edit_game_modal_drop_hero_image_here": "Drop hero image here",
|
||||
"edit_game_modal_drop_to_replace_icon": "Drop to replace icon",
|
||||
"edit_game_modal_drop_to_replace_logo": "Drop to replace logo",
|
||||
"edit_game_modal_drop_to_replace_hero": "Drop to replace hero"
|
||||
},
|
||||
"header": {
|
||||
"search": "Search games",
|
||||
|
||||
@@ -555,7 +555,7 @@
|
||||
"playtime": "Tempo de jogo",
|
||||
"played_recently": "Jogado recentemente",
|
||||
"pinned": "Fixado",
|
||||
"amount_minutes_short": "{{amount}}h",
|
||||
"amount_minutes_short": "{{amount}}m",
|
||||
"amount_hours_short": "{{amount}}h",
|
||||
"game_added_to_pinned": "Jogo adicionado aos fixados",
|
||||
"achievements_earned": "Conquistas recebidas"
|
||||
|
||||
@@ -67,7 +67,14 @@
|
||||
"edit_game_modal_image_filter": "Изображение",
|
||||
"edit_game_modal_icon_resolution": "Рекомендуемое разрешение: 256x256px",
|
||||
"edit_game_modal_logo_resolution": "Рекомендуемое разрешение: 640x360px",
|
||||
"edit_game_modal_hero_resolution": "Рекомендуемое разрешение: 1920x620px"
|
||||
"edit_game_modal_hero_resolution": "Рекомендуемое разрешение: 1920x620px",
|
||||
"edit_game_modal_assets": "Ресурсы",
|
||||
"edit_game_modal_drop_icon_image_here": "Перетащите изображение иконки сюда",
|
||||
"edit_game_modal_drop_logo_image_here": "Перетащите изображение логотипа сюда",
|
||||
"edit_game_modal_drop_hero_image_here": "Перетащите изображение обложки сюда",
|
||||
"edit_game_modal_drop_to_replace_icon": "Перетащите для замены иконки",
|
||||
"edit_game_modal_drop_to_replace_logo": "Перетащите для замены логотипа",
|
||||
"edit_game_modal_drop_to_replace_hero": "Перетащите для замены обложки"
|
||||
},
|
||||
"header": {
|
||||
"search": "Поиск",
|
||||
|
||||
@@ -10,7 +10,7 @@ export const requestSteam250 = async (path: string) => {
|
||||
const { window } = new JSDOM(response.data);
|
||||
const { document } = window;
|
||||
|
||||
return Array.from(document.querySelectorAll(".appline .title a"))
|
||||
return Array.from(document.querySelectorAll("a[data-title]"))
|
||||
.map(($title) => {
|
||||
const steamGameUrl = ($title as HTMLAnchorElement).href;
|
||||
if (!steamGameUrl) return null;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
@use "../../../scss/globals.scss";
|
||||
|
||||
.description-header {
|
||||
width: calc(100% - calc(globals.$spacing-unit * 2));
|
||||
margin: calc(globals.$spacing-unit * 1) calc(globals.$spacing-unit * 1);
|
||||
width: 100%;
|
||||
padding: calc(globals.$spacing-unit * 1.5);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -12,6 +11,7 @@
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: calc(globals.$spacing-unit * 1);
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
|
||||
@@ -191,14 +191,16 @@ export function GameDetailsContent() {
|
||||
{renderGameLogo()}
|
||||
|
||||
<div className="game-details__hero-buttons game-details__hero-buttons--right">
|
||||
<button
|
||||
type="button"
|
||||
className="game-details__edit-custom-game-button"
|
||||
onClick={handleEditGameClick}
|
||||
title={t("edit_game_modal_button")}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
</button>
|
||||
{game && (
|
||||
<button
|
||||
type="button"
|
||||
className="game-details__edit-custom-game-button"
|
||||
onClick={handleEditGameClick}
|
||||
title={t("edit_game_modal_button")}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{game?.shop !== "custom" && (
|
||||
<button
|
||||
@@ -240,13 +242,15 @@ export function GameDetailsContent() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<EditGameModal
|
||||
visible={showEditGameModal}
|
||||
onClose={() => setShowEditGameModal(false)}
|
||||
game={game}
|
||||
shopDetails={shopDetails}
|
||||
onGameUpdated={handleGameUpdated}
|
||||
/>
|
||||
{game && (
|
||||
<EditGameModal
|
||||
visible={showEditGameModal}
|
||||
onClose={() => setShowEditGameModal(false)}
|
||||
game={game}
|
||||
shopDetails={shopDetails}
|
||||
onGameUpdated={handleGameUpdated}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,6 +182,8 @@ $hero-height: 300px;
|
||||
globals.$background-color 50%,
|
||||
globals.$dark-background-color 100%
|
||||
);
|
||||
padding: calc(globals.$spacing-unit * 1.5);
|
||||
gap: calc(globals.$spacing-unit * 1.5);
|
||||
}
|
||||
|
||||
&__description-content {
|
||||
@@ -196,22 +198,12 @@ $hero-height: 300px;
|
||||
user-select: text;
|
||||
line-height: 22px;
|
||||
font-size: globals.$body-font-size;
|
||||
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 1.5);
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
overflow-x: auto;
|
||||
min-height: auto;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ImageIcon, XIcon } from "@primer/octicons-react";
|
||||
|
||||
import { Modal, TextField, Button } from "@renderer/components";
|
||||
import { useToast } from "@renderer/hooks";
|
||||
import { generateRandomGradient } from "@renderer/helpers";
|
||||
import type { LibraryGame, Game, ShopDetailsWithAssets } from "@types";
|
||||
|
||||
import "./edit-game-modal.scss";
|
||||
@@ -44,6 +45,11 @@ export function EditGameModal({
|
||||
logo: "",
|
||||
hero: "",
|
||||
});
|
||||
const [removedAssets, setRemovedAssets] = useState({
|
||||
icon: false,
|
||||
logo: false,
|
||||
hero: false,
|
||||
});
|
||||
const [defaultUrls, setDefaultUrls] = useState({
|
||||
icon: null as string | null,
|
||||
logo: null as string | null,
|
||||
@@ -158,6 +164,21 @@ export function EditGameModal({
|
||||
return defaultUrls[assetType];
|
||||
};
|
||||
|
||||
const getOriginalAssetUrl = (assetType: AssetType): string | null => {
|
||||
if (!game || !isCustomGame(game)) return null;
|
||||
|
||||
switch (assetType) {
|
||||
case "icon":
|
||||
return game.iconUrl;
|
||||
case "logo":
|
||||
return game.logoImageUrl;
|
||||
case "hero":
|
||||
return game.libraryHeroImageUrl;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAsset = async (assetType: AssetType) => {
|
||||
const { filePaths } = await window.electron.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
@@ -183,6 +204,8 @@ export function EditGameModal({
|
||||
...prev,
|
||||
[assetType]: originalPath,
|
||||
}));
|
||||
// Clear the removed flag when a new asset is selected
|
||||
setRemovedAssets((prev) => ({ ...prev, [assetType]: false }));
|
||||
} catch (error) {
|
||||
console.error(`Failed to copy ${assetType} asset:`, error);
|
||||
setAssetPath(assetType, originalPath);
|
||||
@@ -191,14 +214,25 @@ export function EditGameModal({
|
||||
...prev,
|
||||
[assetType]: originalPath,
|
||||
}));
|
||||
// Clear the removed flag when a new asset is selected
|
||||
setRemovedAssets((prev) => ({ ...prev, [assetType]: false }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestoreDefault = (assetType: AssetType) => {
|
||||
setAssetPath(assetType, "");
|
||||
setAssetDisplayPath(assetType, "");
|
||||
setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: "" }));
|
||||
if (game && isCustomGame(game)) {
|
||||
// For custom games, mark asset as removed and clear paths
|
||||
setRemovedAssets((prev) => ({ ...prev, [assetType]: true }));
|
||||
setAssetPath(assetType, "");
|
||||
setAssetDisplayPath(assetType, "");
|
||||
setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: "" }));
|
||||
} else {
|
||||
// For non-custom games, clear custom assets (restore to shop defaults)
|
||||
setAssetPath(assetType, "");
|
||||
setAssetDisplayPath(assetType, "");
|
||||
setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
const getOriginalTitle = (): string => {
|
||||
@@ -330,13 +364,38 @@ export function EditGameModal({
|
||||
|
||||
// Helper function to prepare custom game assets
|
||||
const prepareCustomGameAssets = (game: LibraryGame | Game) => {
|
||||
const iconUrl = assetPaths.icon ? `local:${assetPaths.icon}` : game.iconUrl;
|
||||
const logoImageUrl = assetPaths.logo
|
||||
? `local:${assetPaths.logo}`
|
||||
: game.logoImageUrl;
|
||||
const libraryHeroImageUrl = assetPaths.hero
|
||||
? `local:${assetPaths.hero}`
|
||||
: game.libraryHeroImageUrl;
|
||||
// For custom games, check if asset was explicitly removed
|
||||
let iconUrl;
|
||||
if (removedAssets.icon) {
|
||||
iconUrl = null;
|
||||
} else if (assetPaths.icon) {
|
||||
iconUrl = `local:${assetPaths.icon}`;
|
||||
} else {
|
||||
iconUrl = game.iconUrl;
|
||||
}
|
||||
|
||||
let logoImageUrl;
|
||||
if (removedAssets.logo) {
|
||||
logoImageUrl = null;
|
||||
} else if (assetPaths.logo) {
|
||||
logoImageUrl = `local:${assetPaths.logo}`;
|
||||
} else {
|
||||
logoImageUrl = game.logoImageUrl;
|
||||
}
|
||||
|
||||
// For hero image, if removed, restore to the original gradient or keep the original
|
||||
let libraryHeroImageUrl;
|
||||
if (removedAssets.hero) {
|
||||
// If the original hero was a gradient (data URL), keep it, otherwise generate a new one
|
||||
const originalHero = game.libraryHeroImageUrl;
|
||||
libraryHeroImageUrl = originalHero?.startsWith("data:image/svg+xml")
|
||||
? originalHero
|
||||
: generateRandomGradient();
|
||||
} else {
|
||||
libraryHeroImageUrl = assetPaths.hero
|
||||
? `local:${assetPaths.hero}`
|
||||
: game.libraryHeroImageUrl;
|
||||
}
|
||||
|
||||
return { iconUrl, logoImageUrl, libraryHeroImageUrl };
|
||||
};
|
||||
@@ -418,6 +477,13 @@ export function EditGameModal({
|
||||
(game: LibraryGame | Game) => {
|
||||
setGameName(game.title || "");
|
||||
|
||||
// Reset removed assets state
|
||||
setRemovedAssets({
|
||||
icon: false,
|
||||
logo: false,
|
||||
hero: false,
|
||||
});
|
||||
|
||||
if (isCustomGame(game)) {
|
||||
setCustomGameAssets(game);
|
||||
// Clear default URLs for custom games
|
||||
@@ -481,17 +547,19 @@ export function EditGameModal({
|
||||
<ImageIcon />
|
||||
{t("edit_game_modal_browse")}
|
||||
</Button>
|
||||
{game && !isCustomGame(game) && assetPath && (
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={() => handleRestoreDefault(assetType)}
|
||||
disabled={isUpdating}
|
||||
title={`Remove ${assetType}`}
|
||||
>
|
||||
<XIcon />
|
||||
</Button>
|
||||
)}
|
||||
{game &&
|
||||
(assetPath ||
|
||||
(isCustomGame(game) && getOriginalAssetUrl(assetType))) && (
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={() => handleRestoreDefault(assetType)}
|
||||
disabled={isUpdating}
|
||||
title={`Remove ${assetType}`}
|
||||
>
|
||||
<XIcon />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
@@ -519,7 +587,7 @@ export function EditGameModal({
|
||||
/>
|
||||
{isDragOver && (
|
||||
<div className="edit-game-modal__drop-overlay">
|
||||
<span>Drop to replace {assetType}</span>
|
||||
<span>{t(`edit_game_modal_drop_to_replace_${assetType}`)}</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
@@ -542,7 +610,7 @@ export function EditGameModal({
|
||||
>
|
||||
<div className="edit-game-modal__drop-zone-content">
|
||||
<ImageIcon />
|
||||
<span>Drop {assetType} image here</span>
|
||||
<span>{t(`edit_game_modal_drop_${assetType}_image_here`)}</span>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@use "../../../scss/globals.scss";
|
||||
|
||||
.sidebar-section {
|
||||
background-color: globals.$dark-background-color;
|
||||
background-color: globals.$background-color;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
@@ -21,7 +21,7 @@
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
@@ -40,6 +40,7 @@
|
||||
&__content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.4s cubic-bezier(0, 1, 0, 1);
|
||||
background-color: globals.$dark-background-color;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
@use "../../../scss/globals.scss";
|
||||
|
||||
.content-sidebar {
|
||||
border-left: solid 1px globals.$border-color;
|
||||
background-color: globals.$dark-background-color;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
width: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 1.5);
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
width: 320px;
|
||||
|
||||
@@ -177,6 +177,7 @@ export function ProfileContent() {
|
||||
statIndex={statsIndex}
|
||||
onMouseEnter={handleOnMouseEnterGameCard}
|
||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||
sortBy={sortBy}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
@@ -208,6 +209,7 @@ export function ProfileContent() {
|
||||
statIndex={statsIndex}
|
||||
onMouseEnter={handleOnMouseEnterGameCard}
|
||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||
sortBy={sortBy}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -26,6 +26,7 @@ interface UserLibraryGameCardProps {
|
||||
statIndex: number;
|
||||
onMouseEnter: () => void;
|
||||
onMouseLeave: () => void;
|
||||
sortBy?: string;
|
||||
}
|
||||
|
||||
export function UserLibraryGameCard({
|
||||
@@ -33,6 +34,7 @@ export function UserLibraryGameCard({
|
||||
statIndex,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
sortBy,
|
||||
}: UserLibraryGameCardProps) {
|
||||
const { userProfile, isMe, getUserLibraryGames } =
|
||||
useContext(userProfileContext);
|
||||
@@ -108,7 +110,7 @@ export function UserLibraryGameCard({
|
||||
!game.isPinned
|
||||
);
|
||||
|
||||
await getUserLibraryGames();
|
||||
await getUserLibraryGames(sortBy);
|
||||
|
||||
if (game.isPinned) {
|
||||
showSuccessToast(t("game_removed_from_pinned"));
|
||||
|
||||
Reference in New Issue
Block a user