Merge branch 'feat/VersionsExplorer' of https://git.popa-popa.ru/DIKER/popa-launcher into feat/VersionsExplorer

This commit is contained in:
2025-12-28 14:12:09 +05:00
8 changed files with 305 additions and 30 deletions

154
package-lock.json generated
View File

@ -8,7 +8,6 @@
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/notarize": "^3.0.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.2.0",
@ -33,6 +32,7 @@
"react-router-dom": "^7.3.0",
"remark-gfm": "^4.0.1",
"skinview3d": "^3.4.1",
"socket.io-client": "^4.8.1",
"stream-browserify": "^3.0.0",
"three": "^0.178.0",
"util": "^0.12.5",
@ -2275,19 +2275,6 @@
"node": ">=12.13.0"
}
},
"node_modules/@electron/notarize": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-3.1.1.tgz",
"integrity": "sha512-uQQSlOiJnqRkTL1wlEBAxe90nVN/Fc/hEmk0bqpKk8nKjV1if/tXLHKUPePtv9Xsx90PtZU8aidx5lAiOpjkQQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"promise-retry": "^2.0.1"
},
"engines": {
"node": ">= 22.12.0"
}
},
"node_modules/@electron/osx-sign": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz",
@ -4469,6 +4456,12 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
@ -10619,6 +10612,66 @@
"once": "^1.4.0"
}
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-client/node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/enhanced-resolve": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz",
@ -10682,6 +10735,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
"dev": true,
"license": "MIT"
},
"node_modules/error-ex": {
@ -20088,6 +20142,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
"integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"err-code": "^2.0.2",
@ -20999,6 +21054,7 @@
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
@ -22107,6 +22163,68 @@
"tslib": "^2.0.3"
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/sockjs": {
"version": "0.3.24",
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
@ -25093,6 +25211,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -105,7 +105,6 @@
}
},
"dependencies": {
"@electron/notarize": "^3.0.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.2.0",
@ -130,6 +129,7 @@
"react-router-dom": "^7.3.0",
"remark-gfm": "^4.0.1",
"skinview3d": "^3.4.1",
"socket.io-client": "^4.8.1",
"stream-browserify": "^3.0.0",
"three": "^0.178.0",
"util": "^0.12.5",
@ -206,7 +206,6 @@
"productName": "popa-launcher",
"appId": "org.erb.ElectronReact",
"asar": true,
"afterSign": ".erb/scripts/notarize.js",
"asarUnpack": "**\\*.{node,dll}",
"files": [
"dist",
@ -263,7 +262,7 @@
],
"publish": {
"provider": "generic",
"url": "https://git.popa-popa.ru/DIKER/popa-launcher/releases/download/v${version}",
"url": "https://git.popa-popa.ru/DIKER/popa-launcher/releases/download/latest",
"channel": "latest",
"requestHeaders": {
"Authorization": "token ${env.GH_TOKEN}"

View File

@ -1,12 +1,12 @@
{
"name": "popa-launcher",
"version": "1.0.0",
"version": "1.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "popa-launcher",
"version": "1.0.0",
"version": "1.0.1",
"hasInstallScript": true,
"license": "MIT"
}

View File

@ -1,6 +1,6 @@
{
"name": "popa-launcher",
"version": "1.0.0",
"version": "1.0.1",
"description": "Popa Launcher",
"license": "MIT",
"author": {

View File

@ -96,7 +96,16 @@ const ensureTray = () => {
{ type: 'separator' },
{
label: 'Выход',
click: () => app.quit(),
click: () => {
isQuitting = true;
if (mainWindow) {
mainWindow.removeAllListeners('close');
mainWindow.destroy(); // ⬅ КЛЮЧЕВО
}
app.quit();
},
},
]);
@ -119,6 +128,7 @@ const applyLoginItemSettings = () => {
let tray: Tray | null = null;
let isAuthed = false;
let isQuitting = false;
let mainWindow: BrowserWindow | null = null;
@ -152,7 +162,19 @@ function buildTrayMenu() {
{ type: 'separator' },
{ label: 'Показать', click: () => { mainWindow?.show(); mainWindow?.focus(); } },
{ label: 'Выход', click: () => app.quit() },
{
label: 'Выход',
click: () => {
isQuitting = true;
if (mainWindow) {
mainWindow.removeAllListeners('close');
mainWindow.destroy(); // ⬅ КЛЮЧЕВО
}
app.quit();
},
},
];
tray?.setContextMenu(Menu.buildFromTemplate(template));
@ -259,9 +281,8 @@ const createWindow = async () => {
});
mainWindow.on('close', (e) => {
if (launcherSettings.closeToTray) {
if (!isQuitting && launcherSettings.closeToTray) {
e.preventDefault();
ensureTray();
mainWindow?.hide();
}
});
@ -317,6 +338,10 @@ app.on('window-all-closed', () => {
}
});
app.on('before-quit', () => {
isQuitting = true;
});
app
.whenReady()
.then(() => {

View File

@ -4,7 +4,7 @@ interface HeadAvatarProps {
skinUrl?: string;
size?: number;
style?: React.CSSProperties;
version?: number; // ✅ добавили
version?: number;
}
const DEFAULT_SKIN =
@ -14,15 +14,17 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
skinUrl,
size = 24,
style,
version = 0, // ✅ дефолт
version = 0,
...canvasProps
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const requestIdRef = useRef(0);
useEffect(() => {
const baseUrl = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN;
requestIdRef.current += 1;
const requestId = requestIdRef.current;
// ✅ cache-bust: чтобы браузер НЕ отдавал старую картинку
const baseUrl = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN;
const finalSkinUrl = `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}v=${version}`;
const canvas = canvasRef.current;
@ -33,6 +35,9 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
img.src = finalSkinUrl;
img.onload = () => {
// ✅ игнорим старые onload
if (requestIdRef.current !== requestId) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
@ -47,9 +52,16 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
};
img.onerror = (e) => {
if (requestIdRef.current !== requestId) return;
console.error('Не удалось загрузить скин для HeadAvatar:', e);
};
}, [skinUrl, size, version]); // ✅ version добавили
return () => {
// ✅ гарантированно “убиваем” обработчики старого запроса
img.onload = null;
img.onerror = null;
};
}, [skinUrl, size, version]);
return (
<canvas

View File

@ -49,6 +49,7 @@ import GradientTextField from '../components/GradientTextField';
import CloseIcon from '@mui/icons-material/Close';
import IconButton from '@mui/material/IconButton';
import type { SxProps, Theme } from '@mui/material/styles';
import { getWsBaseUrl } from '../realtime/wsBase';
interface TabPanelProps {
children?: React.ReactNode;
@ -80,6 +81,18 @@ const GLASS_BG =
const GLASS_BORDER = '1px solid rgba(255,255,255,0.08)';
const GLASS_SHADOW = '0 1.2vw 3.2vw rgba(0,0,0,0.55)';
function upsertIntoList<T extends { id: string }>(list: T[], item: T) {
const idx = list.findIndex((x) => x.id === item.id);
if (idx === -1) return [item, ...list];
const copy = list.slice();
copy[idx] = item;
return copy;
}
function removeFromList<T extends { id: string }>(list: T[], id: string) {
return list.filter((x) => x.id !== id);
}
export default function Marketplace() {
const [marketLoading, setMarketLoading] = useState<boolean>(false);
@ -154,6 +167,91 @@ export default function Marketplace() {
}
};
type MarketListedPayload = { serverIp: string; item: MarketplaceItemResponse };
type MarketSoldPayload = { serverIp: string; itemId: string };
type MarketPricePayload = { serverIp: string; item: MarketplaceItemResponse };
type MarketCancelledPayload = { serverIp: string; itemId: string };
useEffect(() => {
if (!selectedServer) return;
const serverIp = selectedServer.ip;
const base = 'wss://minecraft.api.popa-popa.ru'
const wsUrl = `${base}/ws/marketplace?server_ip=${encodeURIComponent(serverIp)}`;
const ws = new WebSocket(wsUrl);
const ping = window.setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.send('ping');
}, 25000);
ws.onmessage = (ev) => {
let msg: any;
try {
msg = JSON.parse(ev.data);
} catch {
return;
}
if (msg.server_ip !== serverIp && msg.serverIp !== serverIp) return;
switch (msg.event) {
case 'market:item_listed': {
const item: MarketplaceItemResponse = msg.item;
setMarketItems((prev) => {
if (!prev) return prev;
if (prev.page !== 1) return prev;
return { ...prev, items: upsertIntoList(prev.items, item) };
});
if (item?.seller_name === username) {
setMyItems((prev) => {
if (!prev) return prev;
if (prev.page !== 1) return prev;
return { ...prev, items: upsertIntoList(prev.items, item) };
});
}
break;
}
case 'market:item_sold': {
const itemId: string = msg.item_id ?? msg.itemId;
setMarketItems((prev) => (prev ? { ...prev, items: removeFromList(prev.items, itemId) } : prev));
setMyItems((prev) => (prev ? { ...prev, items: removeFromList(prev.items, itemId) } : prev));
break;
}
case 'market:item_cancelled': {
const itemId: string = msg.item_id ?? msg.itemId;
setMarketItems((prev) => (prev ? { ...prev, items: removeFromList(prev.items, itemId) } : prev));
setMyItems((prev) => (prev ? { ...prev, items: removeFromList(prev.items, itemId) } : prev));
break;
}
case 'market:item_price_updated': {
const item: MarketplaceItemResponse = msg.item;
setMarketItems((prev) => (prev ? { ...prev, items: upsertIntoList(prev.items, item) } : prev));
setMyItems((prev) => (prev ? { ...prev, items: upsertIntoList(prev.items, item) } : prev));
break;
}
}
};
ws.onerror = (e) => {
console.error('Marketplace WS error:', e);
};
ws.onclose = () => {
window.clearInterval(ping);
};
return () => {
window.clearInterval(ping);
ws.close();
};
}, [selectedServer?.ip, username]);
useEffect(() => {
if (tabValue !== 2) return;
if (!selectedServer) return;
@ -285,7 +383,7 @@ export default function Marketplace() {
const loadMarketItems = async (serverIp: string, pageNumber: number) => {
try {
setMarketLoading(true);
const marketData = await fetchMarketplace(serverIp, pageNumber, 10);
const marketData = await fetchMarketplace(serverIp, pageNumber, 12);
setMarketItems(marketData);
setPage(marketData.page);
setTotalPages(marketData.pages);
@ -988,6 +1086,7 @@ export default function Marketplace() {
width: '100%',
height: '100%',
display: 'flex',
ml: '25%',
flexDirection: 'column',
background: 'rgba(20,20,20,0.78)',
borderRadius: '2.0vw',

View File

@ -0,0 +1,14 @@
export function getWsBaseUrl(): string {
// 1) если ты пробрасываешь конфиг в window
const w = window as any;
if (w.__ENV__?.WS_BASE) return String(w.__ENV__.WS_BASE);
// 2) если открыто с https/http — строим ws/wss автоматически
if (typeof window !== 'undefined' && window.location?.origin) {
const origin = window.location.origin; // http(s)://host
return origin.replace(/^http/, 'ws');
}
// 3) дефолт
return 'wss://minecraft.api.popa-popa.ru';
}