mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
Merge branch 'feature/game-achievements' into chore/test-preview
# Conflicts: # yarn.lock
This commit is contained in:
commit
8b5ed96e9b
@ -44,6 +44,7 @@
|
|||||||
"@vanilla-extract/recipes": "^0.5.2",
|
"@vanilla-extract/recipes": "^0.5.2",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
|
"archiver-utils": "^5.0.2",
|
||||||
"auto-launch": "^5.0.6",
|
"auto-launch": "^5.0.6",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"better-sqlite3": "^11.3.0",
|
"better-sqlite3": "^11.3.0",
|
||||||
|
@ -6,7 +6,7 @@ export const checkUnlockedAchievements = (
|
|||||||
unlockedAchievements: any
|
unlockedAchievements: any
|
||||||
): UnlockedAchievement[] => {
|
): UnlockedAchievement[] => {
|
||||||
if (type === Cracker.onlineFix) return onlineFixMerge(unlockedAchievements);
|
if (type === Cracker.onlineFix) return onlineFixMerge(unlockedAchievements);
|
||||||
if (type === Cracker.goldberg)
|
if (type === Cracker.goldberg || type === Cracker.goldberg2)
|
||||||
return goldbergUnlockedAchievements(unlockedAchievements);
|
return goldbergUnlockedAchievements(unlockedAchievements);
|
||||||
if (type == Cracker.generic) return genericMerge(unlockedAchievements);
|
if (type == Cracker.generic) return genericMerge(unlockedAchievements);
|
||||||
return defaultMerge(unlockedAchievements);
|
return defaultMerge(unlockedAchievements);
|
||||||
|
@ -9,6 +9,15 @@ import { Game } from "@main/entity";
|
|||||||
const publicDir = path.join("C:", "Users", "Public", "Documents");
|
const publicDir = path.join("C:", "Users", "Public", "Documents");
|
||||||
const appData = app.getPath("appData");
|
const appData = app.getPath("appData");
|
||||||
|
|
||||||
|
const crackers = [
|
||||||
|
Cracker.codex,
|
||||||
|
Cracker.goldberg,
|
||||||
|
Cracker.goldberg2,
|
||||||
|
Cracker.rune,
|
||||||
|
Cracker.onlineFix,
|
||||||
|
Cracker.generic,
|
||||||
|
];
|
||||||
|
|
||||||
const addGame = (
|
const addGame = (
|
||||||
achievementFiles: Map<string, AchievementFile[]>,
|
achievementFiles: Map<string, AchievementFile[]>,
|
||||||
achievementPath: string,
|
achievementPath: string,
|
||||||
@ -39,14 +48,6 @@ const getObjectIdsInFolder = (path: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const findSteamGameAchievementFiles = (game: Game) => {
|
export const findSteamGameAchievementFiles = (game: Game) => {
|
||||||
const crackers = [
|
|
||||||
Cracker.codex,
|
|
||||||
Cracker.goldberg,
|
|
||||||
Cracker.rune,
|
|
||||||
Cracker.onlineFix,
|
|
||||||
Cracker.generic,
|
|
||||||
];
|
|
||||||
|
|
||||||
const achievementFiles: AchievementFile[] = [];
|
const achievementFiles: AchievementFile[] = [];
|
||||||
for (const cracker of crackers) {
|
for (const cracker of crackers) {
|
||||||
let achievementPath: string;
|
let achievementPath: string;
|
||||||
@ -58,9 +59,9 @@ export const findSteamGameAchievementFiles = (game: Game) => {
|
|||||||
} else if (cracker === Cracker.goldberg) {
|
} else if (cracker === Cracker.goldberg) {
|
||||||
achievementPath = path.join(appData, "Goldberg SteamEmu Saves");
|
achievementPath = path.join(appData, "Goldberg SteamEmu Saves");
|
||||||
fileLocation = ["achievements.json"];
|
fileLocation = ["achievements.json"];
|
||||||
} else if (cracker === Cracker.generic) {
|
} else if (cracker === Cracker.goldberg2) {
|
||||||
achievementPath = path.join(publicDir, Cracker.generic);
|
achievementPath = path.join(appData, "GSE Saves");
|
||||||
fileLocation = ["user_stats.ini"];
|
fileLocation = ["achievements.json"];
|
||||||
} else {
|
} else {
|
||||||
achievementPath = path.join(publicDir, "Steam", cracker);
|
achievementPath = path.join(publicDir, "Steam", cracker);
|
||||||
fileLocation = ["achievements.ini"];
|
fileLocation = ["achievements.ini"];
|
||||||
@ -102,13 +103,6 @@ export const findAchievementFileInExecutableDirectory = (
|
|||||||
export const findAllSteamGameAchievementFiles = () => {
|
export const findAllSteamGameAchievementFiles = () => {
|
||||||
const gameAchievementFiles = new Map<string, AchievementFile[]>();
|
const gameAchievementFiles = new Map<string, AchievementFile[]>();
|
||||||
|
|
||||||
const crackers = [
|
|
||||||
Cracker.codex,
|
|
||||||
Cracker.goldberg,
|
|
||||||
Cracker.rune,
|
|
||||||
Cracker.onlineFix,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const cracker of crackers) {
|
for (const cracker of crackers) {
|
||||||
let achievementPath: string;
|
let achievementPath: string;
|
||||||
let fileLocation: string[];
|
let fileLocation: string[];
|
||||||
@ -116,6 +110,9 @@ export const findAllSteamGameAchievementFiles = () => {
|
|||||||
if (cracker === Cracker.onlineFix) {
|
if (cracker === Cracker.onlineFix) {
|
||||||
achievementPath = path.join(publicDir, Cracker.onlineFix);
|
achievementPath = path.join(publicDir, Cracker.onlineFix);
|
||||||
fileLocation = ["Stats", "Achievements.ini"];
|
fileLocation = ["Stats", "Achievements.ini"];
|
||||||
|
} else if (cracker === Cracker.goldberg2) {
|
||||||
|
achievementPath = path.join(appData, "GSE Saves");
|
||||||
|
fileLocation = ["achievements.json"];
|
||||||
} else if (cracker === Cracker.goldberg) {
|
} else if (cracker === Cracker.goldberg) {
|
||||||
achievementPath = path.join(appData, "Goldberg SteamEmu Saves");
|
achievementPath = path.join(appData, "Goldberg SteamEmu Saves");
|
||||||
fileLocation = ["achievements.json"];
|
fileLocation = ["achievements.json"];
|
||||||
|
@ -52,7 +52,9 @@ export const mergeAchievements = async (
|
|||||||
const newAchievements = achievements
|
const newAchievements = achievements
|
||||||
.filter((achievement) => {
|
.filter((achievement) => {
|
||||||
return !unlockedAchievements.some((localAchievement) => {
|
return !unlockedAchievements.some((localAchievement) => {
|
||||||
return localAchievement.name === achievement.name.toUpperCase();
|
return (
|
||||||
|
localAchievement.name.toUpperCase() === achievement.name.toUpperCase()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.map((achievement) => {
|
.map((achievement) => {
|
||||||
@ -64,10 +66,16 @@ export const mergeAchievements = async (
|
|||||||
|
|
||||||
if (newAchievements.length && publishNotification) {
|
if (newAchievements.length && publishNotification) {
|
||||||
const achievementsInfo = newAchievements
|
const achievementsInfo = newAchievements
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.unlockTime - b.unlockTime;
|
||||||
|
})
|
||||||
.map((achievement) => {
|
.map((achievement) => {
|
||||||
return JSON.parse(localGameAchievement?.achievements || "[]").find(
|
return JSON.parse(localGameAchievement?.achievements || "[]").find(
|
||||||
(steamAchievement) => {
|
(steamAchievement) => {
|
||||||
return achievement.name === steamAchievement.name;
|
return (
|
||||||
|
achievement.name.toUpperCase() ===
|
||||||
|
steamAchievement.name.toUpperCase()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -85,12 +93,6 @@ export const mergeAchievements = async (
|
|||||||
shop,
|
shop,
|
||||||
achievementsInfo
|
achievementsInfo
|
||||||
);
|
);
|
||||||
|
|
||||||
WindowManager.notificationWindow?.setBounds({ y: 50 });
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
WindowManager.notificationWindow?.setBounds({ y: -9999 });
|
|
||||||
}, 4000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
||||||
|
@ -151,19 +151,21 @@ export class WindowManager {
|
|||||||
maximizable: false,
|
maximizable: false,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
minimizable: false,
|
minimizable: false,
|
||||||
focusable: true,
|
focusable: false,
|
||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
frame: false,
|
frame: false,
|
||||||
width: 240,
|
width: 240,
|
||||||
height: 60,
|
height: 60,
|
||||||
x: 25,
|
x: 25,
|
||||||
y: -9999,
|
y: 25,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||||
sandbox: false,
|
sandbox: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.notificationWindow.setIgnoreMouseEvents(true);
|
||||||
|
this.notificationWindow.webContents.openDevTools();
|
||||||
this.notificationWindow.setVisibleOnAllWorkspaces(true, {
|
this.notificationWindow.setVisibleOnAllWorkspaces(true, {
|
||||||
visibleOnFullScreen: true,
|
visibleOnFullScreen: true,
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *;"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *;"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body style="background-color: #1c1c1c">
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -35,7 +35,6 @@ globalStyle("body", {
|
|||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
fontFamily: "Noto Sans, sans-serif",
|
fontFamily: "Noto Sans, sans-serif",
|
||||||
fontSize: vars.size.body,
|
fontSize: vars.size.body,
|
||||||
background: vars.color.background,
|
|
||||||
color: vars.color.body,
|
color: vars.color.body,
|
||||||
margin: "0",
|
margin: "0",
|
||||||
});
|
});
|
||||||
|
@ -28,7 +28,7 @@ import {
|
|||||||
import { store } from "./store";
|
import { store } from "./store";
|
||||||
|
|
||||||
import resources from "@locales";
|
import resources from "@locales";
|
||||||
import { Achievemnt } from "./pages/achievement/achievement";
|
import { Achievement } from "./pages/achievement/achievement";
|
||||||
|
|
||||||
import "./workers";
|
import "./workers";
|
||||||
import { RepacksContextProvider } from "./context";
|
import { RepacksContextProvider } from "./context";
|
||||||
@ -70,7 +70,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<Route path="/settings" Component={Settings} />
|
<Route path="/settings" Component={Settings} />
|
||||||
<Route path="/profile/:userId" Component={Profile} />
|
<Route path="/profile/:userId" Component={Profile} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/achievement-notification" Component={Achievemnt} />
|
<Route path="/achievement-notification" Component={Achievement} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</RepacksContextProvider>
|
</RepacksContextProvider>
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import achievementSound from "@renderer/assets/audio/achievement.wav";
|
import achievementSound from "@renderer/assets/audio/achievement.wav";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { vars } from "@renderer/theme.css";
|
||||||
|
|
||||||
export function Achievemnt() {
|
interface AchievementInfo {
|
||||||
|
displayName: string;
|
||||||
|
iconUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Achievement() {
|
||||||
const { t } = useTranslation("achievement");
|
const { t } = useTranslation("achievement");
|
||||||
|
|
||||||
const [achievementInfo, setAchievementInfo] = useState<{
|
const [achievements, setAchievements] = useState<AchievementInfo[]>([]);
|
||||||
displayName: string;
|
const achievementAnimation = useRef(-1);
|
||||||
icon: string;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const audio = useMemo(() => {
|
const audio = useMemo(() => {
|
||||||
const audio = new Audio(achievementSound);
|
const audio = new Audio(achievementSound);
|
||||||
@ -23,11 +27,7 @@ export function Achievemnt() {
|
|||||||
if (!achievements) return;
|
if (!achievements) return;
|
||||||
|
|
||||||
if (achievements.length) {
|
if (achievements.length) {
|
||||||
const achievement = achievements[0];
|
setAchievements((ach) => ach.concat(achievements));
|
||||||
setAchievementInfo({
|
|
||||||
displayName: achievement.displayName,
|
|
||||||
icon: achievement.iconUrl,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
audio.play();
|
audio.play();
|
||||||
@ -39,7 +39,26 @@ export function Achievemnt() {
|
|||||||
};
|
};
|
||||||
}, [audio]);
|
}, [audio]);
|
||||||
|
|
||||||
if (!achievementInfo) return <p>Nada</p>;
|
const hasAchievementsPending = achievements.length > 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasAchievementsPending) {
|
||||||
|
let zero = performance.now();
|
||||||
|
achievementAnimation.current = requestAnimationFrame(
|
||||||
|
function animateLock(time) {
|
||||||
|
if (time - zero > 3000) {
|
||||||
|
zero = performance.now();
|
||||||
|
setAchievements((ach) => ach.slice(1));
|
||||||
|
}
|
||||||
|
achievementAnimation.current = requestAnimationFrame(animateLock);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
cancelAnimationFrame(achievementAnimation.current);
|
||||||
|
}
|
||||||
|
}, [hasAchievementsPending]);
|
||||||
|
|
||||||
|
if (!hasAchievementsPending) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -48,16 +67,17 @@ export function Achievemnt() {
|
|||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: "8px",
|
gap: "8px",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
background: vars.color.background,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={achievementInfo.icon}
|
src={achievements[0].iconUrl}
|
||||||
alt={achievementInfo.displayName}
|
alt={achievements[0].displayName}
|
||||||
style={{ width: 60, height: 60 }}
|
style={{ width: 60, height: 60 }}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p>{t("achievement_unlocked")}</p>
|
<p>{t("achievement_unlocked")}</p>
|
||||||
<p>{achievementInfo.displayName}</p>
|
<p>{achievements[0].displayName}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -29,5 +29,6 @@ export enum Cracker {
|
|||||||
rune = "RUNE",
|
rune = "RUNE",
|
||||||
onlineFix = "OnlineFix",
|
onlineFix = "OnlineFix",
|
||||||
goldberg = "Goldberg",
|
goldberg = "Goldberg",
|
||||||
|
goldberg2 = "Goldberg2",
|
||||||
generic = "Generic",
|
generic = "Generic",
|
||||||
}
|
}
|
||||||
|
15
yarn.lock
15
yarn.lock
@ -5902,6 +5902,15 @@ iterator.prototype@^1.1.2:
|
|||||||
reflect.getprototypeof "^1.0.4"
|
reflect.getprototypeof "^1.0.4"
|
||||||
set-function-name "^2.0.1"
|
set-function-name "^2.0.1"
|
||||||
|
|
||||||
|
jackspeak@^2.3.6:
|
||||||
|
version "2.3.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8"
|
||||||
|
integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==
|
||||||
|
dependencies:
|
||||||
|
"@isaacs/cliui" "^8.0.2"
|
||||||
|
optionalDependencies:
|
||||||
|
"@pkgjs/parseargs" "^0.11.0"
|
||||||
|
|
||||||
jackspeak@^3.1.2:
|
jackspeak@^3.1.2:
|
||||||
version "3.4.3"
|
version "3.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
|
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
|
||||||
@ -6484,7 +6493,7 @@ minimatch@^8.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion "^2.0.1"
|
brace-expansion "^2.0.1"
|
||||||
|
|
||||||
minimatch@^9.0.3, minimatch@^9.0.4:
|
minimatch@^9.0.1, minimatch@^9.0.3, minimatch@^9.0.4:
|
||||||
version "9.0.5"
|
version "9.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
|
||||||
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
|
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
|
||||||
@ -6552,7 +6561,7 @@ minipass@^5.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
|
resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
|
||||||
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
|
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
|
||||||
|
|
||||||
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2:
|
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4, minipass@^7.1.2:
|
||||||
version "7.1.2"
|
version "7.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
|
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
|
||||||
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
||||||
@ -7006,7 +7015,7 @@ path-parse@^1.0.7:
|
|||||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||||
|
|
||||||
path-scurry@^1.11.1, path-scurry@^1.6.1:
|
path-scurry@^1.11.0, path-scurry@^1.11.1, path-scurry@^1.6.1:
|
||||||
version "1.11.1"
|
version "1.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2"
|
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2"
|
||||||
integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==
|
integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==
|
||||||
|
Loading…
Reference in New Issue
Block a user