mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-09 03:37:45 +03:00
fix: fixing css issues
This commit is contained in:
commit
d5a3e3fae5
@ -7,7 +7,7 @@ module.exports = {
|
|||||||
"plugin:jsx-a11y/recommended",
|
"plugin:jsx-a11y/recommended",
|
||||||
"@electron-toolkit/eslint-config-ts/recommended",
|
"@electron-toolkit/eslint-config-ts/recommended",
|
||||||
"plugin:prettier/recommended",
|
"plugin:prettier/recommended",
|
||||||
"plugin:storybook/recommended"
|
"plugin:storybook/recommended",
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
@ -94,7 +94,7 @@ def seed_status():
|
|||||||
|
|
||||||
@app.route("/healthcheck", methods=["GET"])
|
@app.route("/healthcheck", methods=["GET"])
|
||||||
def healthcheck():
|
def healthcheck():
|
||||||
return "", 200
|
return "ok", 200
|
||||||
|
|
||||||
@app.route("/process-list", methods=["GET"])
|
@app.route("/process-list", methods=["GET"])
|
||||||
def process_list():
|
def process_list():
|
||||||
|
@ -49,14 +49,14 @@ fs.readdir(dist, async (err, files) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (uploads.length > 0) {
|
for (const upload of uploads) {
|
||||||
await fetch(process.env.BUILD_WEBHOOK_URL, {
|
await fetch(process.env.BUILD_WEBHOOK_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
uploads,
|
upload,
|
||||||
branchName: process.env.BRANCH_NAME,
|
branchName: process.env.BRANCH_NAME,
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
githubActor: process.env.GITHUB_ACTOR,
|
githubActor: process.env.GITHUB_ACTOR,
|
||||||
|
@ -172,7 +172,8 @@
|
|||||||
"reset_achievements_description": "Isso irá resetar todas as conquistas de {{game}}",
|
"reset_achievements_description": "Isso irá resetar todas as conquistas de {{game}}",
|
||||||
"reset_achievements_title": "Tem certeza?",
|
"reset_achievements_title": "Tem certeza?",
|
||||||
"reset_achievements_success": "Conquistas resetadas com sucesso",
|
"reset_achievements_success": "Conquistas resetadas com sucesso",
|
||||||
"reset_achievements_error": "Falha ao resetar conquistas"
|
"reset_achievements_error": "Falha ao resetar conquistas",
|
||||||
|
"no_write_permission": "Não é possível baixar nesse diretório. Clique aqui para saber mais."
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
"featured": "Рекомендации",
|
"featured": "Рекомендации",
|
||||||
"surprise_me": "Удиви меня",
|
"surprise_me": "Удиви меня",
|
||||||
"no_results": "Ничего не найдено",
|
"no_results": "Ничего не найдено",
|
||||||
"hot": "Сейчас в топе",
|
"hot": "Сейчас популярно",
|
||||||
"start_typing": "Начинаю вводить текст для поиска...",
|
"start_typing": "Начинаю вводить текст...",
|
||||||
"weekly": "📅 Лучшие игры недели",
|
"weekly": "📅 Лучшие игры недели",
|
||||||
"achievements": "🏆 Игры, в которых нужно победить"
|
"achievements": "🏆 Игры, в которых нужно победить"
|
||||||
},
|
},
|
||||||
@ -424,7 +424,7 @@
|
|||||||
"subscribe_now": "Подпишитесь прямо сейчас",
|
"subscribe_now": "Подпишитесь прямо сейчас",
|
||||||
"cloud_saving": "Сохранение в облаке",
|
"cloud_saving": "Сохранение в облаке",
|
||||||
"cloud_achievements": "Сохраняйте свои достижения в облаке",
|
"cloud_achievements": "Сохраняйте свои достижения в облаке",
|
||||||
"animated_profile_picture": "Анимированные фотографии профиля",
|
"animated_profile_picture": "Анимированные аватарки",
|
||||||
"premium_support": "Премиальная поддержка",
|
"premium_support": "Премиальная поддержка",
|
||||||
"show_and_compare_achievements": "Показывайте и сравнивайте свои достижения с достижениями других пользователей",
|
"show_and_compare_achievements": "Показывайте и сравнивайте свои достижения с достижениями других пользователей",
|
||||||
"animated_profile_banner": "Анимированный баннер профиля",
|
"animated_profile_banner": "Анимированный баннер профиля",
|
||||||
|
@ -1,47 +1,8 @@
|
|||||||
import type { AppUpdaterEvent } from "@types";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import updater, { UpdateInfo } from "electron-updater";
|
import { UpdateManager } from "@main/services/update-manager";
|
||||||
import { WindowManager } from "@main/services";
|
|
||||||
import { app } from "electron";
|
|
||||||
import { publishNotificationUpdateReadyToInstall } from "@main/services/notifications";
|
|
||||||
|
|
||||||
const { autoUpdater } = updater;
|
|
||||||
|
|
||||||
const sendEvent = (event: AppUpdaterEvent) => {
|
|
||||||
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendEventsForDebug = false;
|
|
||||||
|
|
||||||
const isAutoInstallAvailable =
|
|
||||||
process.platform !== "darwin" && process.env.PORTABLE_EXECUTABLE_FILE == null;
|
|
||||||
|
|
||||||
const mockValuesForDebug = () => {
|
|
||||||
sendEvent({ type: "update-available", info: { version: "1.3.0" } });
|
|
||||||
sendEvent({ type: "update-downloaded" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const newVersionInfo = { version: "" };
|
|
||||||
|
|
||||||
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
|
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
autoUpdater
|
return UpdateManager.checkForUpdates();
|
||||||
.once("update-available", (info: UpdateInfo) => {
|
|
||||||
sendEvent({ type: "update-available", info });
|
|
||||||
newVersionInfo.version = info.version;
|
|
||||||
})
|
|
||||||
.once("update-downloaded", () => {
|
|
||||||
sendEvent({ type: "update-downloaded" });
|
|
||||||
publishNotificationUpdateReadyToInstall(newVersionInfo.version);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (app.isPackaged) {
|
|
||||||
autoUpdater.autoDownload = isAutoInstallAvailable;
|
|
||||||
autoUpdater.checkForUpdates();
|
|
||||||
} else if (sendEventsForDebug) {
|
|
||||||
mockValuesForDebug();
|
|
||||||
}
|
|
||||||
|
|
||||||
return isAutoInstallAvailable;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("checkForUpdates", checkForUpdates);
|
registerEvent("checkForUpdates", checkForUpdates);
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
const checkFolderWritePermission = async (
|
const checkFolderWritePermission = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
path: string
|
testPath: string
|
||||||
) =>
|
) => {
|
||||||
new Promise((resolve) => {
|
const testFilePath = path.join(testPath, ".hydra-write-test");
|
||||||
fs.access(path, fs.constants.W_OK, (err) => {
|
|
||||||
resolve(!err);
|
try {
|
||||||
});
|
fs.writeFileSync(testFilePath, "");
|
||||||
});
|
fs.rmSync(testFilePath);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
registerEvent("checkFolderWritePermission", checkFolderWritePermission);
|
registerEvent("checkFolderWritePermission", checkFolderWritePermission);
|
||||||
|
@ -3,15 +3,14 @@ import { db, levelKeys } from "@main/level";
|
|||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
|
|
||||||
export const getDownloadsPath = async () => {
|
export const getDownloadsPath = async () => {
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
valueEncoding: "json",
|
valueEncoding: "json",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userPreferences && userPreferences.downloadsPath)
|
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
|
||||||
return userPreferences.downloadsPath;
|
|
||||||
|
|
||||||
return defaultDownloadsPath;
|
return defaultDownloadsPath;
|
||||||
};
|
};
|
||||||
|
7
src/main/events/helpers/parse-launch-options.ts
Normal file
7
src/main/events/helpers/parse-launch-options.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const parseLaunchOptions = (params?: string | null): string[] => {
|
||||||
|
if (!params) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.split(" ");
|
||||||
|
};
|
@ -1,8 +1,10 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
|
import { spawn } from "child_process";
|
||||||
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
||||||
import { gamesSublevel, levelKeys } from "@main/level";
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { GameShop } from "@types";
|
import { GameShop } from "@types";
|
||||||
|
import { parseLaunchOptions } from "../helpers/parse-launch-options";
|
||||||
|
|
||||||
const openGame = async (
|
const openGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -11,8 +13,8 @@ const openGame = async (
|
|||||||
executablePath: string,
|
executablePath: string,
|
||||||
launchOptions?: string | null
|
launchOptions?: string | null
|
||||||
) => {
|
) => {
|
||||||
// TODO: revisit this for launchOptions
|
|
||||||
const parsedPath = parseExecutablePath(executablePath);
|
const parsedPath = parseExecutablePath(executablePath);
|
||||||
|
const parsedParams = parseLaunchOptions(launchOptions);
|
||||||
|
|
||||||
const gameKey = levelKeys.game(shop, objectId);
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
|
||||||
@ -26,7 +28,12 @@ const openGame = async (
|
|||||||
launchOptions,
|
launchOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
shell.openPath(parsedPath);
|
if (parsedParams.length === 0) {
|
||||||
|
shell.openPath(parsedPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn(parsedPath, parsedParams, { shell: false, detached: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("openGame", openGame);
|
registerEvent("openGame", openGame);
|
||||||
|
@ -10,7 +10,7 @@ const publishNewRepacksNotification = async (
|
|||||||
) => {
|
) => {
|
||||||
if (newRepacksCount < 1) return;
|
if (newRepacksCount < 1) return;
|
||||||
|
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
valueEncoding: "json",
|
valueEncoding: "json",
|
||||||
|
@ -7,7 +7,7 @@ import { omit } from "lodash-es";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { fileTypeFromFile } from "file-type";
|
import { fileTypeFromFile } from "file-type";
|
||||||
|
|
||||||
const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
|
export const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
|
||||||
return HydraApi.patch<UserProfile>("/profile", updateProfile);
|
return HydraApi.patch<UserProfile>("/profile", updateProfile);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,11 +5,11 @@ import type { UserPreferences } from "@types";
|
|||||||
|
|
||||||
const getUserPreferences = async () =>
|
const getUserPreferences = async () =>
|
||||||
db
|
db
|
||||||
.get<string, UserPreferences>(levelKeys.userPreferences, {
|
.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||||
valueEncoding: "json",
|
valueEncoding: "json",
|
||||||
})
|
})
|
||||||
.then((userPreferences) => {
|
.then((userPreferences) => {
|
||||||
if (userPreferences.realDebridApiToken) {
|
if (userPreferences?.realDebridApiToken) {
|
||||||
userPreferences.realDebridApiToken = Crypto.decrypt(
|
userPreferences.realDebridApiToken = Crypto.decrypt(
|
||||||
userPreferences.realDebridApiToken
|
userPreferences.realDebridApiToken
|
||||||
);
|
);
|
||||||
|
@ -4,12 +4,13 @@ import type { UserPreferences } from "@types";
|
|||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import { Crypto } from "@main/services";
|
import { Crypto } from "@main/services";
|
||||||
|
import { patchUserProfile } from "../profile/update-profile";
|
||||||
|
|
||||||
const updateUserPreferences = async (
|
const updateUserPreferences = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
preferences: Partial<UserPreferences>
|
preferences: Partial<UserPreferences>
|
||||||
) => {
|
) => {
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{ valueEncoding: "json" }
|
{ valueEncoding: "json" }
|
||||||
);
|
);
|
||||||
@ -20,6 +21,7 @@ const updateUserPreferences = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
i18next.changeLanguage(preferences.language);
|
i18next.changeLanguage(preferences.language);
|
||||||
|
patchUserProfile({ language: preferences.language }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferences.realDebridApiToken) {
|
if (preferences.realDebridApiToken) {
|
||||||
@ -28,6 +30,10 @@ const updateUserPreferences = async (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!preferences.downloadsPath) {
|
||||||
|
preferences.downloadsPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
await db.put<string, UserPreferences>(
|
await db.put<string, UserPreferences>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
|
@ -10,7 +10,7 @@ const getComparedUnlockedAchievements = async (
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
userId: string
|
userId: string
|
||||||
) => {
|
) => {
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
valueEncoding: "json",
|
valueEncoding: "json",
|
||||||
@ -25,7 +25,7 @@ const getComparedUnlockedAchievements = async (
|
|||||||
{
|
{
|
||||||
shop,
|
shop,
|
||||||
objectId,
|
objectId,
|
||||||
language: userPreferences?.language || "en",
|
language: userPreferences?.language ?? "en",
|
||||||
}
|
}
|
||||||
).then((achievements) => {
|
).then((achievements) => {
|
||||||
const sortedAchievements = achievements.achievements
|
const sortedAchievements = achievements.achievements
|
||||||
|
@ -12,7 +12,7 @@ export const getUnlockedAchievements = async (
|
|||||||
levelKeys.game(shop, objectId)
|
levelKeys.game(shop, objectId)
|
||||||
);
|
);
|
||||||
|
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
valueEncoding: "json",
|
valueEncoding: "json",
|
||||||
|
@ -9,6 +9,7 @@ import resources from "@locales";
|
|||||||
import { PythonRPC } from "./services/python-rpc";
|
import { PythonRPC } from "./services/python-rpc";
|
||||||
import { Aria2 } from "./services/aria2";
|
import { Aria2 } from "./services/aria2";
|
||||||
import { db, levelKeys } from "./level";
|
import { db, levelKeys } from "./level";
|
||||||
|
import { loadState } from "./main";
|
||||||
|
|
||||||
const { autoUpdater } = updater;
|
const { autoUpdater } = updater;
|
||||||
|
|
||||||
@ -57,7 +58,7 @@ app.whenReady().then(async () => {
|
|||||||
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
await import("./main");
|
await loadState();
|
||||||
|
|
||||||
const language = await db.get<string, string>(levelKeys.language, {
|
const language = await db.get<string, string>(levelKeys.language, {
|
||||||
valueEncoding: "utf-8",
|
valueEncoding: "utf-8",
|
||||||
|
@ -21,8 +21,18 @@ import {
|
|||||||
import { Auth, User, type UserPreferences } from "@types";
|
import { Auth, User, type UserPreferences } from "@types";
|
||||||
import { knexClient } from "./knex-client";
|
import { knexClient } from "./knex-client";
|
||||||
|
|
||||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
export const loadState = async () => {
|
||||||
import("./events");
|
const userPreferences = await migrateFromSqlite().then(async () => {
|
||||||
|
await db.put<string, boolean>(levelKeys.sqliteMigrationDone, true, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
|
||||||
|
return db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await import("./events");
|
||||||
|
|
||||||
Aria2.spawn();
|
Aria2.spawn();
|
||||||
|
|
||||||
@ -104,24 +114,29 @@ const migrateFromSqlite = async () => {
|
|||||||
if (userPreferences.length > 0) {
|
if (userPreferences.length > 0) {
|
||||||
const { realDebridApiToken, ...rest } = userPreferences[0];
|
const { realDebridApiToken, ...rest } = userPreferences[0];
|
||||||
|
|
||||||
await db.put(levelKeys.userPreferences, {
|
await db.put<string, UserPreferences>(
|
||||||
...rest,
|
levelKeys.userPreferences,
|
||||||
realDebridApiToken: realDebridApiToken
|
{
|
||||||
? Crypto.encrypt(realDebridApiToken)
|
...rest,
|
||||||
: null,
|
realDebridApiToken: realDebridApiToken
|
||||||
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
? Crypto.encrypt(realDebridApiToken)
|
||||||
runAtStartup: rest.runAtStartup === 1,
|
: null,
|
||||||
startMinimized: rest.startMinimized === 1,
|
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
||||||
disableNsfwAlert: rest.disableNsfwAlert === 1,
|
runAtStartup: rest.runAtStartup === 1,
|
||||||
seedAfterDownloadComplete: rest.seedAfterDownloadComplete === 1,
|
startMinimized: rest.startMinimized === 1,
|
||||||
showHiddenAchievementsDescription:
|
disableNsfwAlert: rest.disableNsfwAlert === 1,
|
||||||
rest.showHiddenAchievementsDescription === 1,
|
seedAfterDownloadComplete: rest.seedAfterDownloadComplete === 1,
|
||||||
downloadNotificationsEnabled: rest.downloadNotificationsEnabled === 1,
|
showHiddenAchievementsDescription:
|
||||||
repackUpdatesNotificationsEnabled:
|
rest.showHiddenAchievementsDescription === 1,
|
||||||
rest.repackUpdatesNotificationsEnabled === 1,
|
downloadNotificationsEnabled:
|
||||||
achievementNotificationsEnabled:
|
rest.downloadNotificationsEnabled === 1,
|
||||||
rest.achievementNotificationsEnabled === 1,
|
repackUpdatesNotificationsEnabled:
|
||||||
});
|
rest.repackUpdatesNotificationsEnabled === 1,
|
||||||
|
achievementNotificationsEnabled:
|
||||||
|
rest.achievementNotificationsEnabled === 1,
|
||||||
|
},
|
||||||
|
{ valueEncoding: "json" }
|
||||||
|
);
|
||||||
|
|
||||||
if (rest.language) {
|
if (rest.language) {
|
||||||
await db.put(levelKeys.language, rest.language);
|
await db.put(levelKeys.language, rest.language);
|
||||||
@ -192,15 +207,3 @@ const migrateFromSqlite = async () => {
|
|||||||
migrateUser,
|
migrateUser,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
migrateFromSqlite().then(async () => {
|
|
||||||
await db.put<string, boolean>(levelKeys.sqliteMigrationDone, true, {
|
|
||||||
valueEncoding: "json",
|
|
||||||
});
|
|
||||||
|
|
||||||
db.get<string, UserPreferences>(levelKeys.userPreferences, {
|
|
||||||
valueEncoding: "json",
|
|
||||||
}).then((userPreferences) => {
|
|
||||||
loadState(userPreferences);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
@ -141,7 +141,7 @@ const processAchievementFileDiff = async (
|
|||||||
export class AchievementWatcherManager {
|
export class AchievementWatcherManager {
|
||||||
private static hasFinishedMergingWithRemote = false;
|
private static hasFinishedMergingWithRemote = false;
|
||||||
|
|
||||||
public static watchAchievements = () => {
|
public static watchAchievements() {
|
||||||
if (!this.hasFinishedMergingWithRemote) return;
|
if (!this.hasFinishedMergingWithRemote) return;
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
@ -149,12 +149,12 @@ export class AchievementWatcherManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return watchAchievementsWithWine();
|
return watchAchievementsWithWine();
|
||||||
};
|
}
|
||||||
|
|
||||||
private static preProcessGameAchievementFiles = (
|
private static preProcessGameAchievementFiles(
|
||||||
game: Game,
|
game: Game,
|
||||||
gameAchievementFiles: AchievementFile[]
|
gameAchievementFiles: AchievementFile[]
|
||||||
) => {
|
) {
|
||||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||||
for (const achievementFile of gameAchievementFiles) {
|
for (const achievementFile of gameAchievementFiles) {
|
||||||
const parsedAchievements = parseAchievementFile(
|
const parsedAchievements = parseAchievementFile(
|
||||||
@ -182,7 +182,7 @@ export class AchievementWatcherManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return mergeAchievements(game, unlockedAchievements, false);
|
return mergeAchievements(game, unlockedAchievements, false);
|
||||||
};
|
}
|
||||||
|
|
||||||
private static preSearchAchievementsWindows = async () => {
|
private static preSearchAchievementsWindows = async () => {
|
||||||
const games = await gamesSublevel
|
const games = await gamesSublevel
|
||||||
@ -230,7 +230,7 @@ export class AchievementWatcherManager {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public static preSearchAchievements = async () => {
|
public static async preSearchAchievements() {
|
||||||
try {
|
try {
|
||||||
const newAchievementsCount =
|
const newAchievementsCount =
|
||||||
process.platform === "win32"
|
process.platform === "win32"
|
||||||
@ -256,5 +256,5 @@ export class AchievementWatcherManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.hasFinishedMergingWithRemote = true;
|
this.hasFinishedMergingWithRemote = true;
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ export const getGameAchievementData = async (
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error("Failed to get game achievements", err);
|
logger.error("Failed to get game achievements for", objectId, err);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
@ -59,7 +59,7 @@ export const mergeAchievements = async (
|
|||||||
const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
|
const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
|
||||||
|
|
||||||
const newAchievementsMap = new Map(
|
const newAchievementsMap = new Map(
|
||||||
achievements.reverse().map((achievement) => {
|
achievements.toReversed().map((achievement) => {
|
||||||
return [achievement.name.toUpperCase(), achievement];
|
return [achievement.name.toUpperCase(), achievement];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -87,7 +87,7 @@ export const mergeAchievements = async (
|
|||||||
userPreferences?.achievementNotificationsEnabled
|
userPreferences?.achievementNotificationsEnabled
|
||||||
) {
|
) {
|
||||||
const achievementsInfo = newAchievements
|
const achievementsInfo = newAchievements
|
||||||
.sort((a, b) => {
|
.toSorted((a, b) => {
|
||||||
return a.unlockTime - b.unlockTime;
|
return a.unlockTime - b.unlockTime;
|
||||||
})
|
})
|
||||||
.map((achievement) => {
|
.map((achievement) => {
|
||||||
|
@ -3,7 +3,7 @@ import { WindowManager } from "./window-manager";
|
|||||||
import url from "url";
|
import url from "url";
|
||||||
import { uploadGamesBatch } from "./library-sync";
|
import { uploadGamesBatch } from "./library-sync";
|
||||||
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
||||||
import { logger } from "./logger";
|
import { networkLogger as logger } from "./logger";
|
||||||
import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
|
import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
|
||||||
import { omit } from "lodash-es";
|
import { omit } from "lodash-es";
|
||||||
import { appVersion } from "@main/constants";
|
import { appVersion } from "@main/constants";
|
||||||
@ -32,7 +32,8 @@ export class HydraApi {
|
|||||||
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
||||||
private static readonly ADD_LOG_INTERCEPTOR = true;
|
private static readonly ADD_LOG_INTERCEPTOR = true;
|
||||||
|
|
||||||
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
|
private static readonly secondsToMilliseconds = (seconds: number) =>
|
||||||
|
seconds * 1000;
|
||||||
|
|
||||||
private static userAuth: HydraApiUserAuth = {
|
private static userAuth: HydraApiUserAuth = {
|
||||||
authToken: "",
|
authToken: "",
|
||||||
@ -153,7 +154,8 @@ export class HydraApi {
|
|||||||
(error) => {
|
(error) => {
|
||||||
logger.error(" ---- RESPONSE ERROR -----");
|
logger.error(" ---- RESPONSE ERROR -----");
|
||||||
const { config } = error;
|
const { config } = error;
|
||||||
const data = JSON.parse(config.data);
|
|
||||||
|
const data = JSON.parse(config.data ?? null);
|
||||||
|
|
||||||
logger.error(
|
logger.error(
|
||||||
config.method,
|
config.method,
|
||||||
@ -174,14 +176,22 @@ export class HydraApi {
|
|||||||
error.response.status,
|
error.response.status,
|
||||||
error.response.data
|
error.response.data
|
||||||
);
|
);
|
||||||
} else if (error.request) {
|
|
||||||
const errorData = error.toJSON();
|
return Promise.reject(error as Error);
|
||||||
logger.error("Request error:", errorData.message);
|
|
||||||
} else {
|
|
||||||
logger.error("Error", error.message);
|
|
||||||
}
|
}
|
||||||
logger.error(" ----- END RESPONSE ERROR -------");
|
|
||||||
return Promise.reject(error);
|
if (error.request) {
|
||||||
|
const errorData = error.toJSON();
|
||||||
|
logger.error("Request error:", errorData.code, errorData.message);
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
`Request failed with ${errorData.code} ${errorData.message}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("Error", error.message);
|
||||||
|
return Promise.reject(error as Error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,12 @@ log.transports.file.resolvePathFn = (
|
|||||||
_: log.PathVariables,
|
_: log.PathVariables,
|
||||||
message?: log.LogMessage | undefined
|
message?: log.LogMessage | undefined
|
||||||
) => {
|
) => {
|
||||||
if (message?.scope === "python-instance") {
|
if (message?.scope === "python-rpc") {
|
||||||
return path.join(logsPath, "pythoninstance.txt");
|
return path.join(logsPath, "pythonrpc.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message?.scope === "network") {
|
||||||
|
return path.join(logsPath, "network.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message?.scope == "achievements") {
|
if (message?.scope == "achievements") {
|
||||||
@ -34,3 +38,4 @@ log.initialize();
|
|||||||
export const pythonRpcLogger = log.scope("python-rpc");
|
export const pythonRpcLogger = log.scope("python-rpc");
|
||||||
export const logger = log.scope("main");
|
export const logger = log.scope("main");
|
||||||
export const achievementsLogger = log.scope("achievements");
|
export const achievementsLogger = log.scope("achievements");
|
||||||
|
export const networkLogger = log.scope("network");
|
||||||
|
@ -2,6 +2,7 @@ import { sleep } from "@main/helpers";
|
|||||||
import { DownloadManager } from "./download";
|
import { DownloadManager } from "./download";
|
||||||
import { watchProcesses } from "./process-watcher";
|
import { watchProcesses } from "./process-watcher";
|
||||||
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
|
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
|
||||||
|
import { UpdateManager } from "./update-manager";
|
||||||
|
|
||||||
export const startMainLoop = async () => {
|
export const startMainLoop = async () => {
|
||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition
|
||||||
@ -11,6 +12,7 @@ export const startMainLoop = async () => {
|
|||||||
DownloadManager.watchDownloads(),
|
DownloadManager.watchDownloads(),
|
||||||
AchievementWatcherManager.watchAchievements(),
|
AchievementWatcherManager.watchAchievements(),
|
||||||
DownloadManager.getSeedStatus(),
|
DownloadManager.getSeedStatus(),
|
||||||
|
UpdateManager.checkForUpdatePeriodically(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await sleep(1500);
|
await sleep(1500);
|
||||||
|
@ -9,6 +9,7 @@ import { achievementSoundPath } from "@main/constants";
|
|||||||
import icon from "@resources/icon.png?asset";
|
import icon from "@resources/icon.png?asset";
|
||||||
import { NotificationOptions, toXmlString } from "./xml";
|
import { NotificationOptions, toXmlString } from "./xml";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
import { WindowManager } from "../window-manager";
|
||||||
import type { Game, UserPreferences } from "@types";
|
import type { Game, UserPreferences } from "@types";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
|
|
||||||
@ -96,7 +97,9 @@ export const publishCombinedNewAchievementNotification = async (
|
|||||||
toastXml: toXmlString(options),
|
toastXml: toXmlString(options),
|
||||||
}).show();
|
}).show();
|
||||||
|
|
||||||
if (process.platform !== "linux") {
|
if (WindowManager.mainWindow) {
|
||||||
|
WindowManager.mainWindow.webContents.send("on-achievement-unlocked");
|
||||||
|
} else if (process.platform !== "linux") {
|
||||||
sound.play(achievementSoundPath);
|
sound.play(achievementSoundPath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -143,7 +146,9 @@ export const publishNewAchievementNotification = async (info: {
|
|||||||
toastXml: toXmlString(options),
|
toastXml: toXmlString(options),
|
||||||
}).show();
|
}).show();
|
||||||
|
|
||||||
if (process.platform !== "linux") {
|
if (WindowManager.mainWindow) {
|
||||||
|
WindowManager.mainWindow.webContents.send("on-achievement-unlocked");
|
||||||
|
} else if (process.platform !== "linux") {
|
||||||
sound.play(achievementSoundPath);
|
sound.play(achievementSoundPath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
60
src/main/services/update-manager.ts
Normal file
60
src/main/services/update-manager.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import updater, { UpdateInfo } from "electron-updater";
|
||||||
|
import { logger, WindowManager } from "@main/services";
|
||||||
|
import { AppUpdaterEvent } from "@types";
|
||||||
|
import { app } from "electron";
|
||||||
|
import { publishNotificationUpdateReadyToInstall } from "@main/services/notifications";
|
||||||
|
|
||||||
|
const isAutoInstallAvailable =
|
||||||
|
process.platform !== "darwin" && process.env.PORTABLE_EXECUTABLE_FILE == null;
|
||||||
|
|
||||||
|
const { autoUpdater } = updater;
|
||||||
|
const sendEventsForDebug = false;
|
||||||
|
|
||||||
|
export class UpdateManager {
|
||||||
|
private static hasNotified = false;
|
||||||
|
private static newVersion = "";
|
||||||
|
private static checkTick = 0;
|
||||||
|
|
||||||
|
private static mockValuesForDebug() {
|
||||||
|
this.sendEvent({ type: "update-available", info: { version: "1.3.0" } });
|
||||||
|
this.sendEvent({ type: "update-downloaded" });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static sendEvent(event: AppUpdaterEvent) {
|
||||||
|
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static checkForUpdates() {
|
||||||
|
autoUpdater
|
||||||
|
.once("update-available", (info: UpdateInfo) => {
|
||||||
|
this.sendEvent({ type: "update-available", info });
|
||||||
|
this.newVersion = info.version;
|
||||||
|
})
|
||||||
|
.once("update-downloaded", () => {
|
||||||
|
this.sendEvent({ type: "update-downloaded" });
|
||||||
|
|
||||||
|
if (!this.hasNotified) {
|
||||||
|
this.hasNotified = true;
|
||||||
|
publishNotificationUpdateReadyToInstall(this.newVersion);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (app.isPackaged) {
|
||||||
|
autoUpdater.autoDownload = isAutoInstallAvailable;
|
||||||
|
autoUpdater.checkForUpdates().then((result) => {
|
||||||
|
logger.log(`Check for updates result: ${result}`);
|
||||||
|
});
|
||||||
|
} else if (sendEventsForDebug) {
|
||||||
|
this.mockValuesForDebug();
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAutoInstallAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static checkForUpdatePeriodically() {
|
||||||
|
if (this.checkTick % 2000 == 0) {
|
||||||
|
this.checkForUpdates();
|
||||||
|
}
|
||||||
|
this.checkTick++;
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,6 @@ export const getUserData = async () => {
|
|||||||
})
|
})
|
||||||
.catch(async (err) => {
|
.catch(async (err) => {
|
||||||
if (err instanceof UserNotLoggedInError) {
|
if (err instanceof UserNotLoggedInError) {
|
||||||
logger.info("User is not logged in", err);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
logger.error("Failed to get logged user");
|
logger.error("Failed to get logged user");
|
||||||
@ -59,6 +58,7 @@ export const getUserData = async () => {
|
|||||||
expiresAt: loggedUser.subscription.expiresAt,
|
expiresAt: loggedUser.subscription.expiresAt,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
featurebaseJwt: "",
|
||||||
} as UserDetails;
|
} as UserDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ export class WindowManager {
|
|||||||
minHeight: 540,
|
minHeight: 540,
|
||||||
backgroundColor: "#1c1c1c",
|
backgroundColor: "#1c1c1c",
|
||||||
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
||||||
...(process.platform === "linux" ? { icon } : {}),
|
icon,
|
||||||
trafficLightPosition: { x: 16, y: 16 },
|
trafficLightPosition: { x: 16, y: 16 },
|
||||||
titleBarOverlay: {
|
titleBarOverlay: {
|
||||||
symbolColor: "#DADBE1",
|
symbolColor: "#DADBE1",
|
||||||
@ -145,6 +145,11 @@ export class WindowManager {
|
|||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
WindowManager.mainWindow = null;
|
WindowManager.mainWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.mainWindow.webContents.setWindowOpenHandler((handler) => {
|
||||||
|
shell.openExternal(handler.url);
|
||||||
|
return { action: "deny" };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static openAuthWindow(page: AuthPage, searchParams: URLSearchParams) {
|
public static openAuthWindow(page: AuthPage, searchParams: URLSearchParams) {
|
||||||
|
@ -169,6 +169,12 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
return () =>
|
return () =>
|
||||||
ipcRenderer.removeListener("on-library-batch-complete", listener);
|
ipcRenderer.removeListener("on-library-batch-complete", listener);
|
||||||
},
|
},
|
||||||
|
onAchievementUnlocked: (cb: () => void) => {
|
||||||
|
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||||
|
ipcRenderer.on("on-achievement-unlocked", listener);
|
||||||
|
return () =>
|
||||||
|
ipcRenderer.removeListener("on-achievement-unlocked", listener);
|
||||||
|
},
|
||||||
|
|
||||||
/* Hardware */
|
/* Hardware */
|
||||||
getDiskFreeSpace: (path: string) =>
|
getDiskFreeSpace: (path: string) =>
|
||||||
|
@ -123,7 +123,7 @@ export const titleBar = style({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: `0 ${SPACING_UNIT * 2}px`,
|
padding: `0 ${SPACING_UNIT * 2}px`,
|
||||||
WebkitAppRegion: "drag",
|
WebkitAppRegion: "drag",
|
||||||
zIndex: "4",
|
zIndex: vars.zIndex.titleBar,
|
||||||
borderBottom: `1px solid ${vars.color.border}`,
|
borderBottom: `1px solid ${vars.color.border}`,
|
||||||
} as ComplexStyleRule);
|
} as ComplexStyleRule);
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import achievementSound from "@renderer/assets/audio/achievement.wav";
|
||||||
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -234,6 +234,22 @@ export function App() {
|
|||||||
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
||||||
}, [updateRepacks]);
|
}, [updateRepacks]);
|
||||||
|
|
||||||
|
const playAudio = useCallback(() => {
|
||||||
|
const audio = new Audio(achievementSound);
|
||||||
|
audio.volume = 0.2;
|
||||||
|
audio.play();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = window.electron.onAchievementUnlocked(() => {
|
||||||
|
playAudio();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [playAudio]);
|
||||||
|
|
||||||
const handleToastClose = useCallback(() => {
|
const handleToastClose = useCallback(() => {
|
||||||
dispatch(closeToast());
|
dispatch(closeToast());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
@ -262,6 +278,7 @@ export function App() {
|
|||||||
>
|
>
|
||||||
<Toast
|
<Toast
|
||||||
visible={toast.visible}
|
visible={toast.visible}
|
||||||
|
title={toast.title}
|
||||||
message={toast.message}
|
message={toast.message}
|
||||||
type={toast.type}
|
type={toast.type}
|
||||||
onClose={handleToastClose}
|
onClose={handleToastClose}
|
||||||
|
BIN
src/renderer/src/assets/audio/achievement.wav
Normal file
BIN
src/renderer/src/assets/audio/achievement.wav
Normal file
Binary file not shown.
@ -21,4 +21,14 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__version-button {
|
||||||
|
color: globals.$body-color;
|
||||||
|
border-bottom: solid 1px transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-bottom: solid 1px globals.$body-color;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,10 +76,15 @@ export function BottomPanel() {
|
|||||||
<small>{status}</small>
|
<small>{status}</small>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<small>
|
<button
|
||||||
{sessionHash ? `${sessionHash} -` : ""} v{version} "
|
data-featurebase-changelog
|
||||||
{VERSION_CODENAME}"
|
className="bottom-panel__version-button"
|
||||||
</small>
|
>
|
||||||
|
<small data-featurebase-changelog>
|
||||||
|
{sessionHash ? `${sessionHash} -` : ""} v{version} "
|
||||||
|
{VERSION_CODENAME}"
|
||||||
|
</small>
|
||||||
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,152 +0,0 @@
|
|||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
|
|
||||||
export const sidebar = recipe({
|
|
||||||
base: {
|
|
||||||
backgroundColor: vars.color.darkBackground,
|
|
||||||
color: vars.color.muted,
|
|
||||||
flexDirection: "column",
|
|
||||||
display: "flex",
|
|
||||||
transition: "opacity ease 0.2s",
|
|
||||||
borderRight: `solid 1px ${vars.color.border}`,
|
|
||||||
position: "relative",
|
|
||||||
overflow: "hidden",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
resizing: {
|
|
||||||
true: {
|
|
||||||
opacity: vars.opacity.active,
|
|
||||||
pointerEvents: "none",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
darwin: {
|
|
||||||
true: {
|
|
||||||
paddingTop: `${SPACING_UNIT * 6}px`,
|
|
||||||
},
|
|
||||||
false: {
|
|
||||||
paddingTop: `${SPACING_UNIT}px`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const content = style({
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
padding: `${SPACING_UNIT * 2}px`,
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
width: "100%",
|
|
||||||
overflow: "auto",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const handle = style({
|
|
||||||
width: "5px",
|
|
||||||
height: "100%",
|
|
||||||
cursor: "col-resize",
|
|
||||||
position: "absolute",
|
|
||||||
right: "0",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const menu = style({
|
|
||||||
listStyle: "none",
|
|
||||||
padding: "0",
|
|
||||||
margin: "0",
|
|
||||||
gap: `${SPACING_UNIT / 2}px`,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
overflow: "hidden",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const menuItem = recipe({
|
|
||||||
base: {
|
|
||||||
transition: "all ease 0.1s",
|
|
||||||
cursor: "pointer",
|
|
||||||
textWrap: "nowrap",
|
|
||||||
display: "flex",
|
|
||||||
color: vars.color.muted,
|
|
||||||
borderRadius: "4px",
|
|
||||||
":hover": {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
active: {
|
|
||||||
true: {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
muted: {
|
|
||||||
true: {
|
|
||||||
opacity: vars.opacity.disabled,
|
|
||||||
":hover": {
|
|
||||||
opacity: "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const menuItemButton = style({
|
|
||||||
color: "inherit",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
cursor: "pointer",
|
|
||||||
overflow: "hidden",
|
|
||||||
width: "100%",
|
|
||||||
padding: `9px ${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const menuItemButtonLabel = style({
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
overflow: "hidden",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const gameIcon = style({
|
|
||||||
width: "20px",
|
|
||||||
height: "20px",
|
|
||||||
minWidth: "20px",
|
|
||||||
minHeight: "20px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
backgroundSize: "cover",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const sectionTitle = style({
|
|
||||||
textTransform: "uppercase",
|
|
||||||
fontWeight: "bold",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const section = style({
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
paddingBottom: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const helpButton = style({
|
|
||||||
color: vars.color.muted,
|
|
||||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
|
|
||||||
gap: "9px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
cursor: "pointer",
|
|
||||||
borderTop: `solid 1px ${vars.color.border}`,
|
|
||||||
transition: "background-color ease 0.1s",
|
|
||||||
":hover": {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const helpButtonIcon = style({
|
|
||||||
background: "linear-gradient(0deg, #16B195 50%, #3E62C0 100%)",
|
|
||||||
width: "24px",
|
|
||||||
height: "24px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
color: "#fff",
|
|
||||||
borderRadius: "50%",
|
|
||||||
});
|
|
136
src/renderer/src/components/sidebar/sidebar.scss
Normal file
136
src/renderer/src/components/sidebar/sidebar.scss
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
transition: opacity ease 0.2s;
|
||||||
|
border-right: solid 1px globals.$border-color;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-top: globals.$spacing-unit;
|
||||||
|
|
||||||
|
&--resizing {
|
||||||
|
opacity: globals.$active-opacity;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--darwin {
|
||||||
|
padding-top: calc(globals.$spacing-unit * 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__handle {
|
||||||
|
width: 5px;
|
||||||
|
height: 100%;
|
||||||
|
cursor: col-resize;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu-item {
|
||||||
|
transition: all ease 0.1s;
|
||||||
|
cursor: pointer;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
display: flex;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--muted {
|
||||||
|
opacity: globals.$disabled-opacity;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu-item-button {
|
||||||
|
color: inherit;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
padding: 9px globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu-item-button-label {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
min-height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section-title {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section {
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__help-button {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
|
||||||
|
gap: 9px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-top: solid 1px globals.$border-color;
|
||||||
|
transition: background-color ease 0.1s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__help-button-icon {
|
||||||
|
background: linear-gradient(0deg, #16b195 50%, #3e62c0 100%);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
@ -181,7 +181,12 @@ export function Sidebar() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SidebarProfile />
|
<SidebarProfile />
|
||||||
|
|
||||||
|
85
src/renderer/src/components/toast/toast.scss
Normal file
85
src/renderer/src/components/toast/toast.scss
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
animation-duration: 0.2s;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
position: absolute;
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
z-index: globals.$toast-z-index;
|
||||||
|
max-width: 420px;
|
||||||
|
animation-name: enter;
|
||||||
|
transform: translateY(0);
|
||||||
|
|
||||||
|
&--closing {
|
||||||
|
animation-name: exit;
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 2);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 5px;
|
||||||
|
|
||||||
|
&::-webkit-progress-bar {
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
}
|
||||||
|
&::-webkit-progress-value {
|
||||||
|
background-color: globals.$muted-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close-button {
|
||||||
|
color: globals.$body-color;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
transition: color 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
&--success {
|
||||||
|
color: globals.$success-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
color: globals.$danger-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--warning {
|
||||||
|
color: globals.$warning-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes enter {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes exit {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
1
src/renderer/src/declaration.d.ts
vendored
1
src/renderer/src/declaration.d.ts
vendored
@ -142,6 +142,7 @@ declare global {
|
|||||||
minimized: boolean;
|
minimized: boolean;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||||
|
onAchievementUnlocked: (cb: () => void) => () => Electron.IpcRenderer;
|
||||||
|
|
||||||
/* Download sources */
|
/* Download sources */
|
||||||
putDownloadSource: (
|
putDownloadSource: (
|
||||||
|
@ -3,12 +3,14 @@ import type { PayloadAction } from "@reduxjs/toolkit";
|
|||||||
import { ToastProps } from "@renderer/components/toast/toast";
|
import { ToastProps } from "@renderer/components/toast/toast";
|
||||||
|
|
||||||
export interface ToastState {
|
export interface ToastState {
|
||||||
message: string;
|
title: string;
|
||||||
|
message?: string;
|
||||||
type: ToastProps["type"];
|
type: ToastProps["type"];
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ToastState = {
|
const initialState: ToastState = {
|
||||||
|
title: "",
|
||||||
message: "",
|
message: "",
|
||||||
type: "success",
|
type: "success",
|
||||||
visible: false,
|
visible: false,
|
||||||
@ -19,6 +21,7 @@ export const toastSlice = createSlice({
|
|||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
showToast: (state, action: PayloadAction<Omit<ToastState, "visible">>) => {
|
showToast: (state, action: PayloadAction<Omit<ToastState, "visible">>) => {
|
||||||
|
state.title = action.payload.title;
|
||||||
state.message = action.payload.message;
|
state.message = action.payload.message;
|
||||||
state.type = action.payload.type;
|
state.type = action.payload.type;
|
||||||
state.visible = true;
|
state.visible = true;
|
||||||
|
@ -6,9 +6,10 @@ export function useToast() {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const showSuccessToast = useCallback(
|
const showSuccessToast = useCallback(
|
||||||
(message: string) => {
|
(title: string, message?: string) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
showToast({
|
showToast({
|
||||||
|
title,
|
||||||
message,
|
message,
|
||||||
type: "success",
|
type: "success",
|
||||||
})
|
})
|
||||||
@ -18,9 +19,10 @@ export function useToast() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const showErrorToast = useCallback(
|
const showErrorToast = useCallback(
|
||||||
(message: string) => {
|
(title: string, message?: string) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
showToast({
|
showToast({
|
||||||
|
title,
|
||||||
message,
|
message,
|
||||||
type: "error",
|
type: "error",
|
||||||
})
|
})
|
||||||
@ -30,9 +32,10 @@ export function useToast() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const showWarningToast = useCallback(
|
const showWarningToast = useCallback(
|
||||||
(message: string) => {
|
(title: string, message?: string) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
showToast({
|
showToast({
|
||||||
|
title,
|
||||||
message,
|
message,
|
||||||
type: "warning",
|
type: "warning",
|
||||||
})
|
})
|
||||||
|
@ -78,9 +78,15 @@ export function useUserDetails() {
|
|||||||
...response,
|
...response,
|
||||||
username: userDetails?.username || "",
|
username: userDetails?.username || "",
|
||||||
subscription: userDetails?.subscription || null,
|
subscription: userDetails?.subscription || null,
|
||||||
|
featurebaseJwt: userDetails?.featurebaseJwt || "",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[updateUserDetails, userDetails?.username, userDetails?.subscription]
|
[
|
||||||
|
updateUserDetails,
|
||||||
|
userDetails?.username,
|
||||||
|
userDetails?.subscription,
|
||||||
|
userDetails?.featurebaseJwt,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const syncFriendRequests = useCallback(async () => {
|
const syncFriendRequests = useCallback(async () => {
|
||||||
|
@ -45,6 +45,7 @@ Sentry.init({
|
|||||||
tracesSampleRate: 1.0,
|
tracesSampleRate: 1.0,
|
||||||
replaysSessionSampleRate: 0.1,
|
replaysSessionSampleRate: 0.1,
|
||||||
replaysOnErrorSampleRate: 1.0,
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
release: await window.electron.getVersion(),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log = logger.log;
|
console.log = logger.log;
|
||||||
|
262
src/renderer/src/pages/achievements/achievements.scss
Normal file
262
src/renderer/src/pages/achievements/achievements.scss
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
@use "../../scss/globals.scss";
|
||||||
|
@use "sass:math";
|
||||||
|
|
||||||
|
$hero-height: 150px;
|
||||||
|
$logo-height: 100px;
|
||||||
|
$logo-max-width: 200px;
|
||||||
|
|
||||||
|
.achievements {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
|
||||||
|
&__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;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-logo-backdrop {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-image-skeleton {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-logo {
|
||||||
|
width: $logo-max-width;
|
||||||
|
height: $logo-height;
|
||||||
|
object-fit: contain;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: auto;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__table-header {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--color-dark-background);
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
border-bottom: solid 1px var(--color-border);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&--stuck {
|
||||||
|
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: globals.$spacing-unit * 2;
|
||||||
|
padding: globals.$spacing-unit * 2;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
display: flex;
|
||||||
|
transition: all ease 0.1s;
|
||||||
|
color: var(--color-muted);
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: globals.$spacing-unit globals.$spacing-unit;
|
||||||
|
gap: globals.$spacing-unit * 2;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-image {
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
|
||||||
|
&--locked {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-hidden-icon {
|
||||||
|
display: flex;
|
||||||
|
color: var(--color-warning);
|
||||||
|
opacity: 0.8;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-eye-closed {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
color: globals.$warning-color;
|
||||||
|
scale: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-points {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&--locked {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-value {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-unlock-time {
|
||||||
|
white-space: nowrap;
|
||||||
|
gap: 4px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-compared {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3fr 1fr 1fr;
|
||||||
|
|
||||||
|
&--no-owner {
|
||||||
|
grid-template-columns: 3fr 2fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-status {
|
||||||
|
display: flex;
|
||||||
|
padding: globals.$spacing-unit;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&--unlocked {
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__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: var(--color-muted);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__profile-avatar {
|
||||||
|
height: 54px;
|
||||||
|
width: 54px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
position: relative;
|
||||||
|
object-fit: cover;
|
||||||
|
|
||||||
|
&--small {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subscription-button {
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: math.div(globals.$spacing-unit, 2);
|
||||||
|
color: var(--color-body);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,109 +0,0 @@
|
|||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
|
|
||||||
export const downloadTitleWrapper = style({
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: `${SPACING_UNIT}px`,
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadTitle = style({
|
|
||||||
fontWeight: "bold",
|
|
||||||
cursor: "pointer",
|
|
||||||
color: vars.color.body,
|
|
||||||
textAlign: "left",
|
|
||||||
fontSize: "16px",
|
|
||||||
display: "block",
|
|
||||||
":hover": {
|
|
||||||
textDecoration: "underline",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloads = style({
|
|
||||||
width: "100%",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
margin: "0",
|
|
||||||
padding: "0",
|
|
||||||
marginTop: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadCover = style({
|
|
||||||
width: "280px",
|
|
||||||
minWidth: "280px",
|
|
||||||
height: "auto",
|
|
||||||
borderRight: `solid 1px ${vars.color.border}`,
|
|
||||||
position: "relative",
|
|
||||||
zIndex: "1",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadCoverContent = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
padding: `${SPACING_UNIT}px`,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "flex-end",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadCoverBackdrop = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 5%, transparent 100%)",
|
|
||||||
display: "flex",
|
|
||||||
overflow: "hidden",
|
|
||||||
zIndex: "1",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadCoverImage = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
position: "absolute",
|
|
||||||
zIndex: "-1",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const download = style({
|
|
||||||
width: "100%",
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
display: "flex",
|
|
||||||
borderRadius: "8px",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
overflow: "hidden",
|
|
||||||
boxShadow: "0px 0px 5px 0px #000000",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
height: "140px",
|
|
||||||
minHeight: "140px",
|
|
||||||
maxHeight: "140px",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadDetails = style({
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
flex: "1",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: `${SPACING_UNIT / 2}px`,
|
|
||||||
fontSize: "14px",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadRightContent = style({
|
|
||||||
display: "flex",
|
|
||||||
padding: `${SPACING_UNIT * 2}px`,
|
|
||||||
flex: "1",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
background: "linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%)",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadActions = style({
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadGroup = style({
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
});
|
|
140
src/renderer/src/pages/downloads/download-group.scss
Normal file
140
src/renderer/src/pages/downloads/download-group.scss
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.download-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
|
||||||
|
&-divider {
|
||||||
|
flex: 1;
|
||||||
|
background-color: globals.$border-color;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-count {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__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%;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
width: 100%;
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
display: flex;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cover {
|
||||||
|
width: 280px;
|
||||||
|
min-width: 280px;
|
||||||
|
height: auto;
|
||||||
|
border-right: solid 1px globals.$border-color;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
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;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
padding: 8px;
|
||||||
|
min-height: unset;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.hero-panel-playtime {
|
||||||
|
&__download-details {
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
display: flex;
|
||||||
|
color: globals.$body-color;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__downloads-link {
|
||||||
|
color: globals.$body-color;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
@ -1,77 +0,0 @@
|
|||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
|
||||||
|
|
||||||
export const panel = recipe({
|
|
||||||
base: {
|
|
||||||
width: "100%",
|
|
||||||
height: "72px",
|
|
||||||
minHeight: "72px",
|
|
||||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
|
||||||
backgroundColor: vars.color.darkBackground,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
borderBottom: `solid 1px ${vars.color.border}`,
|
|
||||||
position: "sticky",
|
|
||||||
overflow: "hidden",
|
|
||||||
top: "0",
|
|
||||||
zIndex: "2",
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
stuck: {
|
|
||||||
true: {
|
|
||||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.8)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const content = style({
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
66
src/renderer/src/pages/game-details/hero/hero-panel.scss
Normal file
66
src/renderer/src/pages/game-details/hero/hero-panel.scss
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.hero-panel {
|
||||||
|
width: 100%;
|
||||||
|
height: 72px;
|
||||||
|
min-height: 72px;
|
||||||
|
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 3);
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
border-bottom: solid 1px globals.$border-color;
|
||||||
|
position: sticky;
|
||||||
|
overflow: hidden;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
&--stuck {
|
||||||
|
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__download-details {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -98,9 +98,7 @@ export function DownloadSettingsModal({
|
|||||||
? Downloader.RealDebrid
|
? Downloader.RealDebrid
|
||||||
: filteredDownloaders[0];
|
: filteredDownloaders[0];
|
||||||
|
|
||||||
setSelectedDownloader(
|
setSelectedDownloader(selectedDownloader ?? null);
|
||||||
selectedDownloader === undefined ? null : selectedDownloader
|
|
||||||
);
|
|
||||||
}, [
|
}, [
|
||||||
userPreferences?.downloadsPath,
|
userPreferences?.downloadsPath,
|
||||||
downloaders,
|
downloaders,
|
||||||
@ -127,8 +125,8 @@ export function DownloadSettingsModal({
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
onClose();
|
onClose();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((error) => {
|
||||||
showErrorToast(t("download_error"));
|
showErrorToast(t("download_error"), error.message);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setDownloadStarting(false);
|
setDownloadStarting(false);
|
||||||
|
@ -2,6 +2,7 @@ import { useContext, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Modal, TextField } from "@renderer/components";
|
import { Button, Modal, TextField } from "@renderer/components";
|
||||||
import type { LibraryGame } from "@types";
|
import type { LibraryGame } from "@types";
|
||||||
|
import * as styles from "./game-options-modal.css";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
||||||
import { useDownload, useToast, useUserDetails } from "@renderer/hooks";
|
import { useDownload, useToast, useUserDetails } from "@renderer/hooks";
|
||||||
@ -9,7 +10,6 @@ import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
|||||||
import { ResetAchievementsModal } from "./reset-achievements-modal";
|
import { ResetAchievementsModal } from "./reset-achievements-modal";
|
||||||
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
||||||
import { debounce } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
import "./game-options-modal.scss";
|
|
||||||
|
|
||||||
export interface GameOptionsModalProps {
|
export interface GameOptionsModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -183,8 +183,6 @@ export function GameOptionsModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldShowLaunchOptionsConfiguration = false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteGameModal
|
<DeleteGameModal
|
||||||
@ -192,12 +190,14 @@ export function GameOptionsModal({
|
|||||||
onClose={() => setShowDeleteModal(false)}
|
onClose={() => setShowDeleteModal(false)}
|
||||||
deleteGame={handleDeleteGame}
|
deleteGame={handleDeleteGame}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RemoveGameFromLibraryModal
|
<RemoveGameFromLibraryModal
|
||||||
visible={showRemoveGameModal}
|
visible={showRemoveGameModal}
|
||||||
onClose={() => setShowRemoveGameModal(false)}
|
onClose={() => setShowRemoveGameModal(false)}
|
||||||
removeGameFromLibrary={handleRemoveGameFromLibrary}
|
removeGameFromLibrary={handleRemoveGameFromLibrary}
|
||||||
game={game}
|
game={game}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ResetAchievementsModal
|
<ResetAchievementsModal
|
||||||
visible={showResetAchievementsModal}
|
visible={showResetAchievementsModal}
|
||||||
onClose={() => setShowResetAchievementsModal(false)}
|
onClose={() => setShowResetAchievementsModal(false)}
|
||||||
@ -211,66 +211,59 @@ export function GameOptionsModal({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
large={true}
|
large={true}
|
||||||
>
|
>
|
||||||
<div className="game-options-modal__container">
|
<div className={styles.optionsContainer}>
|
||||||
<div className="game-options-modal__section">
|
<div className={styles.gameOptionHeader}>
|
||||||
<div className="game-options-modal__header">
|
<h2>{t("executable_section_title")}</h2>
|
||||||
<h2>{t("executable_section_title")}</h2>
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
<h4 className="game-options-modal__header-description">
|
{t("executable_section_description")}
|
||||||
{t("executable_section_description")}
|
</h4>
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="game-options-modal__executable-field">
|
|
||||||
<TextField
|
|
||||||
value={game.executablePath || ""}
|
|
||||||
readOnly
|
|
||||||
theme="dark"
|
|
||||||
disabled
|
|
||||||
placeholder={t("no_executable_selected")}
|
|
||||||
rightContent={
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
theme="outline"
|
|
||||||
onClick={handleChangeExecutableLocation}
|
|
||||||
>
|
|
||||||
<FileIcon />
|
|
||||||
{t("select_executable")}
|
|
||||||
</Button>
|
|
||||||
{game.executablePath && (
|
|
||||||
<Button
|
|
||||||
onClick={handleClearExecutablePath}
|
|
||||||
theme="outline"
|
|
||||||
>
|
|
||||||
{t("clear")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{game.executablePath && (
|
|
||||||
<div className="game-options-modal__executable-field-buttons">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
theme="outline"
|
|
||||||
onClick={handleOpenGameExecutablePath}
|
|
||||||
>
|
|
||||||
{t("open_folder")}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleCreateShortcut} theme="outline">
|
|
||||||
{t("create_shortcut")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
value={game.executablePath || ""}
|
||||||
|
readOnly
|
||||||
|
theme="dark"
|
||||||
|
disabled
|
||||||
|
placeholder={t("no_executable_selected")}
|
||||||
|
rightContent={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={handleChangeExecutableLocation}
|
||||||
|
>
|
||||||
|
<FileIcon />
|
||||||
|
{t("select_executable")}
|
||||||
|
</Button>
|
||||||
|
{game.executablePath && (
|
||||||
|
<Button onClick={handleClearExecutablePath} theme="outline">
|
||||||
|
{t("clear")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{game.executablePath && (
|
||||||
|
<div className={styles.gameOptionRow}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={handleOpenGameExecutablePath}
|
||||||
|
>
|
||||||
|
{t("open_folder")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCreateShortcut} theme="outline">
|
||||||
|
{t("create_shortcut")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{shouldShowWinePrefixConfiguration && (
|
{shouldShowWinePrefixConfiguration && (
|
||||||
<div className="game-options-modal__wine-prefix">
|
<div className={styles.optionsContainer}>
|
||||||
<div className="game-options-modal__header">
|
<div className={styles.gameOptionHeader}>
|
||||||
<h2>{t("wine_prefix")}</h2>
|
<h2>{t("wine_prefix")}</h2>
|
||||||
<h4 className="game-options-modal__header-description">
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
{t("wine_prefix_description")}
|
{t("wine_prefix_description")}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@ -304,100 +297,95 @@ export function GameOptionsModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowLaunchOptionsConfiguration && (
|
<div className={styles.optionsContainer}>
|
||||||
<div className="game-options-modal__launch-options">
|
<div className={styles.gameOptionHeader}>
|
||||||
<div className="game-options-modal__header">
|
<h2>{t("launch_options")}</h2>
|
||||||
<h2>{t("launch_options")}</h2>
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
<h4 className="game-options-modal__header-description">
|
{t("launch_options_description")}
|
||||||
{t("launch_options_description")}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<TextField
|
|
||||||
value={launchOptions}
|
|
||||||
theme="dark"
|
|
||||||
placeholder={t("launch_options_placeholder")}
|
|
||||||
onChange={handleChangeLaunchOptions}
|
|
||||||
rightContent={
|
|
||||||
game.launchOptions && (
|
|
||||||
<Button onClick={handleClearLaunchOptions} theme="outline">
|
|
||||||
{t("clear")}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="game-options-modal__downloads">
|
|
||||||
<div className="game-options-modal__header">
|
|
||||||
<h2>{t("downloads_secion_title")}</h2>
|
|
||||||
<h4 className="game-options-modal__header-description">
|
|
||||||
{t("downloads_section_description")}
|
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="game-options-modal__row">
|
<TextField
|
||||||
<Button
|
value={launchOptions}
|
||||||
onClick={() => setShowRepacksModal(true)}
|
theme="dark"
|
||||||
theme="outline"
|
placeholder={t("launch_options_placeholder")}
|
||||||
disabled={deleting || isGameDownloading || !repacks.length}
|
onChange={handleChangeLaunchOptions}
|
||||||
>
|
rightContent={
|
||||||
{t("open_download_options")}
|
game.launchOptions && (
|
||||||
</Button>
|
<Button onClick={handleClearLaunchOptions} theme="outline">
|
||||||
{game.download?.downloadPath && (
|
{t("clear")}
|
||||||
<Button
|
</Button>
|
||||||
onClick={handleOpenDownloadFolder}
|
)
|
||||||
theme="outline"
|
}
|
||||||
disabled={deleting}
|
/>
|
||||||
>
|
|
||||||
{t("open_download_location")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="game-options-modal__danger-zone">
|
<div className={styles.gameOptionHeader}>
|
||||||
<div className="game-options-modal__header">
|
<h2>{t("downloads_secion_title")}</h2>
|
||||||
<h2>{t("danger_zone_section_title")}</h2>
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
<h4 className="game-options-modal__danger-zone-description">
|
{t("downloads_section_description")}
|
||||||
{t("danger_zone_section_description")}
|
</h4>
|
||||||
</h4>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="game-options-modal__danger-zone-buttons">
|
<div className={styles.gameOptionRow}>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowRepacksModal(true)}
|
||||||
|
theme="outline"
|
||||||
|
disabled={deleting || isGameDownloading || !repacks.length}
|
||||||
|
>
|
||||||
|
{t("open_download_options")}
|
||||||
|
</Button>
|
||||||
|
{game.download?.downloadPath && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowRemoveGameModal(true)}
|
onClick={handleOpenDownloadFolder}
|
||||||
theme="danger"
|
theme="outline"
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
>
|
>
|
||||||
{t("remove_from_library")}
|
{t("open_download_location")}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<div className={styles.gameOptionHeader}>
|
||||||
onClick={() => setShowResetAchievementsModal(true)}
|
<h2>{t("danger_zone_section_title")}</h2>
|
||||||
theme="danger"
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
disabled={
|
{t("danger_zone_section_description")}
|
||||||
deleting ||
|
</h4>
|
||||||
isDeletingAchievements ||
|
</div>
|
||||||
!hasAchievements ||
|
|
||||||
!userDetails
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("reset_achievements")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<div className={styles.gameOptionRow}>
|
||||||
onClick={() => {
|
<Button
|
||||||
setShowDeleteModal(true);
|
onClick={() => setShowRemoveGameModal(true)}
|
||||||
}}
|
theme="danger"
|
||||||
theme="danger"
|
disabled={deleting}
|
||||||
disabled={
|
>
|
||||||
isGameDownloading || deleting || !game.download?.downloadPath
|
{t("remove_from_library")}
|
||||||
}
|
</Button>
|
||||||
>
|
|
||||||
{t("remove_files")}
|
<Button
|
||||||
</Button>
|
onClick={() => setShowResetAchievementsModal(true)}
|
||||||
</div>
|
theme="danger"
|
||||||
|
disabled={
|
||||||
|
deleting ||
|
||||||
|
isDeletingAchievements ||
|
||||||
|
!hasAchievements ||
|
||||||
|
!userDetails
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("reset_achievements")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShowDeleteModal(true);
|
||||||
|
}}
|
||||||
|
theme="danger"
|
||||||
|
disabled={
|
||||||
|
isGameDownloading || deleting || !game.download?.downloadPath
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("remove_files")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
174
src/renderer/src/pages/game-details/sidebar/sidebar.scss
Normal file
174
src/renderer/src/pages/game-details/sidebar/sidebar.scss
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.content-sidebar {
|
||||||
|
border-left: solid 1px globals.$border-color;
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
max-width: 300px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.requirement {
|
||||||
|
&__button-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
border-radius: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__details {
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
line-height: 22px;
|
||||||
|
font-size: globals.$body-font-size;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
color: globals.$body-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__details-skeleton {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
font-size: globals.$body-font-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.how-long-to-beat {
|
||||||
|
&__categories-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__category {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 20%,
|
||||||
|
rgb(255 255 255 / 2%) 100%
|
||||||
|
);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__category-label {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__category-skeleton {
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 76px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
&__section {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
justify-content: space-between;
|
||||||
|
transition: max-height ease 0.5s;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__category-title {
|
||||||
|
font-size: globals.$small-font-size;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__category {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all ease 0.1s;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: globals.$spacing-unit;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item-image {
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
|
||||||
|
&--locked {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-required-button {
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
color: globals.$warning-color;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
@ -65,7 +65,7 @@ export function SettingsAccount() {
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, [fetchUserDetails, updateUserDetails]);
|
}, [fetchUserDetails, updateUserDetails, showSuccessToast]);
|
||||||
|
|
||||||
const visibilityOptions = [
|
const visibilityOptions = [
|
||||||
{ value: "PUBLIC", label: t("public") },
|
{ value: "PUBLIC", label: t("public") },
|
||||||
|
@ -19,3 +19,8 @@ $bottom-panel-z-index: 3;
|
|||||||
$title-bar-z-index: 4;
|
$title-bar-z-index: 4;
|
||||||
$backdrop-z-index: 4;
|
$backdrop-z-index: 4;
|
||||||
$modal-z-index: 5;
|
$modal-z-index: 5;
|
||||||
|
|
||||||
|
$body-font-size: 14px;
|
||||||
|
$small-font-size: 12px;
|
||||||
|
|
||||||
|
$app-container: app-container;
|
||||||
|
@ -24,7 +24,7 @@ export const vars = createGlobalTheme(":root", {
|
|||||||
zIndex: {
|
zIndex: {
|
||||||
toast: "5",
|
toast: "5",
|
||||||
bottomPanel: "3",
|
bottomPanel: "3",
|
||||||
titleBar: "4",
|
titleBar: "1900000001",
|
||||||
backdrop: "4",
|
backdrop: "4",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -39,7 +39,7 @@ export const pipe =
|
|||||||
fns.reduce((prev, fn) => fn(prev), arg);
|
fns.reduce((prev, fn) => fn(prev), arg);
|
||||||
|
|
||||||
export const removeReleaseYearFromName = (name: string) =>
|
export const removeReleaseYearFromName = (name: string) =>
|
||||||
name.replace(/\([0-9]{4}\)/g, "");
|
name.replace(/\(\d{4}\)/g, "");
|
||||||
|
|
||||||
export const removeSymbolsFromName = (name: string) =>
|
export const removeSymbolsFromName = (name: string) =>
|
||||||
name.replace(/[^A-Za-z 0-9]/g, "");
|
name.replace(/[^A-Za-z 0-9]/g, "");
|
||||||
|
@ -139,6 +139,7 @@ export interface UserDetails {
|
|||||||
backgroundImageUrl: string | null;
|
backgroundImageUrl: string | null;
|
||||||
profileVisibility: ProfileVisibility;
|
profileVisibility: ProfileVisibility;
|
||||||
bio: string;
|
bio: string;
|
||||||
|
featurebaseJwt: string;
|
||||||
subscription: Subscription | null;
|
subscription: Subscription | null;
|
||||||
quirks?: {
|
quirks?: {
|
||||||
backupsPerGameLimit: number;
|
backupsPerGameLimit: number;
|
||||||
@ -171,6 +172,7 @@ export interface UpdateProfileRequest {
|
|||||||
profileImageUrl?: string | null;
|
profileImageUrl?: string | null;
|
||||||
backgroundImageUrl?: string | null;
|
backgroundImageUrl?: string | null;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
|
language?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadSourceDownload {
|
export interface DownloadSourceDownload {
|
||||||
|
@ -66,16 +66,16 @@ export interface GameAchievement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
downloadsPath: string | null;
|
downloadsPath?: string | null;
|
||||||
language: string;
|
language?: string;
|
||||||
realDebridApiToken: string | null;
|
realDebridApiToken?: string | null;
|
||||||
preferQuitInsteadOfHiding: boolean;
|
preferQuitInsteadOfHiding?: boolean;
|
||||||
runAtStartup: boolean;
|
runAtStartup?: boolean;
|
||||||
startMinimized: boolean;
|
startMinimized?: boolean;
|
||||||
disableNsfwAlert: boolean;
|
disableNsfwAlert?: boolean;
|
||||||
seedAfterDownloadComplete: boolean;
|
seedAfterDownloadComplete?: boolean;
|
||||||
showHiddenAchievementsDescription: boolean;
|
showHiddenAchievementsDescription?: boolean;
|
||||||
downloadNotificationsEnabled: boolean;
|
downloadNotificationsEnabled?: boolean;
|
||||||
repackUpdatesNotificationsEnabled: boolean;
|
repackUpdatesNotificationsEnabled?: boolean;
|
||||||
achievementNotificationsEnabled: boolean;
|
achievementNotificationsEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user