diff --git a/package-lock.json b/package-lock.json index d47d39d..0cdb650 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index bd339ec..70330e5 100644 --- a/package.json +++ b/package.json @@ -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}" diff --git a/release/app/package-lock.json b/release/app/package-lock.json index 417fda3..ad5ec55 100644 --- a/release/app/package-lock.json +++ b/release/app/package-lock.json @@ -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" } diff --git a/release/app/package.json b/release/app/package.json index 11892fe..3a67c99 100644 --- a/release/app/package.json +++ b/release/app/package.json @@ -1,6 +1,6 @@ { "name": "popa-launcher", - "version": "1.0.0", + "version": "1.0.1", "description": "Popa Launcher", "license": "MIT", "author": { diff --git a/src/main/main.ts b/src/main/main.ts index f9be41d..9d8e6fd 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -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(() => { diff --git a/src/renderer/components/HeadAvatar.tsx b/src/renderer/components/HeadAvatar.tsx index 6be1d5c..4f19864 100644 --- a/src/renderer/components/HeadAvatar.tsx +++ b/src/renderer/components/HeadAvatar.tsx @@ -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 = ({ skinUrl, size = 24, style, - version = 0, // ✅ дефолт + version = 0, ...canvasProps }) => { const canvasRef = useRef(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 = ({ 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 = ({ }; 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 ( (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(list: T[], id: string) { + return list.filter((x) => x.id !== id); +} + export default function Marketplace() { const [marketLoading, setMarketLoading] = useState(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', diff --git a/src/renderer/realtime/wsBase.ts b/src/renderer/realtime/wsBase.ts new file mode 100644 index 0000000..54e2909 --- /dev/null +++ b/src/renderer/realtime/wsBase.ts @@ -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'; +} \ No newline at end of file