v1.0.3(add quick launch, redesign shop, add new directory game)

This commit is contained in:
aurinex
2025-12-29 16:49:27 +05:00
parent 287116103d
commit b1f378c5a8
9 changed files with 375 additions and 146 deletions

View File

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

View File

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

View File

@ -19,6 +19,8 @@ import { spawn } from 'child_process';
import { AuthService } from './auth-service';
import { API_BASE_URL } from '../renderer/api';
app.setName('.popa-popa');
// const CDN = 'https://cdn.minecraft.popa-popa.ru';
// const DOWNLOAD_OPTIONS = {
@ -413,8 +415,8 @@ export function initMinecraftHandlers() {
preserveFiles = [], // Новый параметр: список файлов/папок для сохранения
} = options || {};
const appPath = path.dirname(app.getPath('exe'));
const minecraftDir = path.join(appPath, '.minecraft');
const userDataPath = app.getPath('userData');
const minecraftDir = path.join(app.getPath('userData'), 'minecraft');
const versionsDir = path.join(minecraftDir, 'versions');
const versionFilePath = path.join(minecraftDir, versionFileName);
@ -437,7 +439,7 @@ export function initMinecraftHandlers() {
};
}
const tempDir = path.join(appPath, 'temp');
const tempDir = path.join(userDataPath, 'temp');
const packDir = path.join(versionsDir, packName); // Директория пакета
// Создаем/очищаем временную директорию
@ -552,14 +554,14 @@ export function initMinecraftHandlers() {
isVanillaVersion = false,
} = gameConfig || {};
const appPath = path.dirname(app.getPath('exe'));
const minecraftDir = path.join(appPath, '.minecraft');
const userDataPath = app.getPath('userData');
const minecraftDir = path.join(app.getPath('userData'), 'minecraft');
const versionsDir = path.join(minecraftDir, 'versions');
fs.mkdirSync(versionsDir, { recursive: true });
// gamePath:
// - ваниль → .minecraft
// - модпак → .minecraft/versions/Comfort (или другое packName)
// - ваниль → .popa-popa
// - модпак → .popa-popa/versions/Comfort (или другое packName)
const packDir = isVanillaVersion
? minecraftDir
: path.join(versionsDir, packName);
@ -802,7 +804,7 @@ export function initMinecraftHandlers() {
}
// --- authlib-injector ---
const authlibPath = await ensureAuthlibInjectorExists(appPath);
const authlibPath = await ensureAuthlibInjectorExists(userDataPath);
console.log('authlibPath:', authlibPath);
event.sender.send('installation-status', {
@ -930,8 +932,8 @@ export function initMinecraftHandlers() {
// Добавьте в функцию initMinecraftHandlers или создайте новую
ipcMain.handle('get-pack-files', async (event, packName) => {
try {
const appPath = path.dirname(app.getPath('exe'));
const minecraftDir = path.join(appPath, '.minecraft');
const userDataPath = app.getPath('userData');
const minecraftDir = path.join(app.getPath('userData'), 'minecraft');
const packDir = path.join(minecraftDir, 'versions', packName);
if (!fs.existsSync(packDir)) {
@ -974,8 +976,8 @@ export function initMinecraftHandlers() {
// Сначала создаем общую функцию для получения установленных версий
function getInstalledVersions() {
try {
const appPath = path.dirname(app.getPath('exe'));
const minecraftDir = path.join(appPath, '.minecraft');
const userDataPath = app.getPath('userData');
const minecraftDir = path.join(app.getPath('userData'), 'minecraft');
const versionsDir = path.join(minecraftDir, 'versions');
if (!fs.existsSync(versionsDir)) {
@ -1170,8 +1172,8 @@ export function initPackConfigHandlers() {
// Обработчик для сохранения настроек сборки
ipcMain.handle('save-pack-config', async (event, { packName, config }) => {
try {
const appPath = path.dirname(app.getPath('exe'));
const minecraftDir = path.join(appPath, '.minecraft');
const userDataPath = app.getPath('userData');
const minecraftDir = path.join(app.getPath('userData'), 'minecraft');
const packDir = path.join(minecraftDir, 'versions', packName);
// Создаем папку для сборки, если она не существует
@ -1201,8 +1203,8 @@ export function initPackConfigHandlers() {
// Обработчик для загрузки настроек сборки
ipcMain.handle('load-pack-config', async (event, { packName }) => {
try {
const appPath = path.dirname(app.getPath('exe'));
const minecraftDir = path.join(appPath, '.minecraft');
const userDataPath = app.getPath('userData');
const minecraftDir = path.join(app.getPath('userData'), 'minecraft');
const packDir = path.join(minecraftDir, 'versions', packName);
const configPath = path.join(packDir, CONFIG_FILENAME);
@ -1246,8 +1248,8 @@ export function initPackConfigHandlers() {
// Добавляем после обработчика get-available-versions
ipcMain.handle('get-version-config', async (event, { versionId }) => {
try {
const appPath = path.dirname(app.getPath('exe'));
const minecraftDir = path.join(appPath, '.minecraft');
const userDataPath = app.getPath('userData');
const minecraftDir = path.join(app.getPath('userData'), 'minecraft');
const versionsDir = path.join(minecraftDir, 'versions');
const versionPath = path.join(versionsDir, versionId);

View File

@ -96,8 +96,9 @@ export const BonusShopItem: React.FC<BonusShopItemProps> = ({
transition: 'transform 0.35s ease, box-shadow 0.35s ease',
'&:hover': {
transform: 'scale(1.01)',
boxShadow: '0 10px 20px rgba(242,113,33,0.1)',
// transform: 'scale(1.01)',
borderColor: 'rgba(200, 33, 242, 0.35)',
boxShadow: '0 1.2vw 3.2vw rgba(53, 3, 66, 0.75)',
},
}}
>
@ -107,8 +108,10 @@ export const BonusShopItem: React.FC<BonusShopItemProps> = ({
position: 'absolute',
inset: 0,
pointerEvents: 'none',
// background:
// 'radial-gradient(circle at top, rgba(242,113,33,0.25), transparent 60%)',
background:
'radial-gradient(circle at top, rgba(242,113,33,0.25), transparent 60%)',
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.10), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.16), transparent 55%)',
}}
/>

View File

@ -67,9 +67,9 @@ export default function CapeCard({
transition:
'transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease',
'&:hover': {
transform: 'scale(1.01)',
borderColor: 'rgba(242,113,33,0.35)',
boxShadow: '0 1.4vw 3.8vw rgba(0,0,0,0.60)',
// transform: 'scale(1.01)',
borderColor: 'rgba(200, 33, 242, 0.35)',
boxShadow: '0 1.2vw 3.2vw rgba(53, 3, 66, 0.75)',
},
}}
>
@ -135,8 +135,8 @@ export default function CapeCard({
{/* Здесь показываем ЛЕВУЮ половину текстуры (лицевую часть) */}
<Box
sx={{
width: '44vw',
height: '36vw',
width: '46.2vw',
height: '39.2vw',
minWidth: '462px',
minHeight: '380px',
imageRendering: 'pixelated',

View File

@ -70,8 +70,8 @@ export default function ShopItem({
transition: 'transform 0.35s ease, box-shadow 0.35s ease',
'&:hover': {
transform: 'scale(1.01)',
boxShadow: '0 20px 20px rgba(242,113,33,0.1)',
borderColor: 'rgba(200, 33, 242, 0.35)',
boxShadow: '0 1.2vw 3.2vw rgba(53, 3, 66, 0.75)',
},
}}
>
@ -82,7 +82,7 @@ export default function ShopItem({
inset: 0,
pointerEvents: 'none',
background:
'radial-gradient(circle at top, rgba(242,113,33,0.25), transparent 60%)',
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.10), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.16), transparent 55%)',
}}
/>
@ -243,6 +243,7 @@ export default function ShopItem({
fontFamily: 'Benzin-Bold',
borderRadius: '2.5vw',
fontSize: '0.85rem',
color: 'white',
'&:hover': {
transform: 'scale(1.02)',
},

View File

@ -24,6 +24,9 @@ import { useTheme } from '@mui/material/styles';
import InventoryIcon from '@mui/icons-material/Inventory';
import { RiCoupon3Fill } from 'react-icons/ri';
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
import { isNotificationsEnabled, getNotifPositionFromSettings } from '../utils/notifications';
declare global {
interface Window {
electron: {
@ -75,10 +78,48 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
null,
);
// ===== QUICK LAUNCH ===== \\
const [lastVersion, setLastVersion] = useState<null | any>(null);
useEffect(() => {
try {
const raw = localStorage.getItem('last_launched_version');
if (!raw) return;
setLastVersion(JSON.parse(raw));
} catch {
setLastVersion(null);
}
}, []);
// ===== QUICK LAUNCH ===== \\
const path = location.pathname || '';
const isAuthPage =
path.startsWith('/login') || path.startsWith('/registration');
const [notifOpen, setNotifOpen] = useState(false);
const [notifMsg, setNotifMsg] = useState<React.ReactNode>('');
const [notifSeverity, setNotifSeverity] = useState<
'success' | 'info' | 'warning' | 'error'
>('info');
const [notifPos, setNotifPos] = useState<NotificationPosition>({
vertical: 'bottom',
horizontal: 'center',
});
const showNotification = (
message: React.ReactNode,
severity: 'success' | 'info' | 'warning' | 'error' = 'info',
position: NotificationPosition = getNotifPositionFromSettings(),
) => {
if (!isNotificationsEnabled()) return;
setNotifMsg(message);
setNotifSeverity(severity);
setNotifPos(position);
setNotifOpen(true);
};
const TAB_ROUTES: Array<{
value: number;
match: (p: string) => boolean;
@ -267,6 +308,66 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
window.removeEventListener('settings-updated', handler as EventListener);
}, [updateGradientVars]);
const handleQuickLaunch = async () => {
const raw = localStorage.getItem('last_launched_version');
if (!raw) {
showNotification('Вы не запускали ни одну из сборок!', 'warning');
return;
}
const ctx = JSON.parse(raw);
const savedConfig = JSON.parse(
localStorage.getItem('launcher_config') || '{}',
);
if (!savedConfig.accessToken) {
showNotification('Вы не авторизованы', 'error');
return;
}
await window.electron.ipcRenderer.invoke('launch-minecraft', {
accessToken: savedConfig.accessToken,
uuid: savedConfig.uuid,
username: savedConfig.username,
memory: ctx.memory,
baseVersion: ctx.baseVersion,
fabricVersion: ctx.fabricVersion,
packName: ctx.packName,
serverIp: ctx.serverIp,
isVanillaVersion: ctx.isVanillaVersion,
versionToLaunchOverride: ctx.versionToLaunchOverride,
});
};
const getLastLaunchLabel = (v: any) => {
if (!v) return '';
const title = v.isVanillaVersion
? `Minecraft ${v.versionId}`
: `Сборка ${v.packName}`;
const details = [
v.baseVersion ? `MC ${v.baseVersion}` : null,
v.memory ? `${v.memory} MB RAM` : null,
]
.filter(Boolean)
.join(' · ');
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '0.2vw' }}>
<Typography sx={{ fontSize: '0.9vw', fontWeight: 600 }}>
{title}
</Typography>
<Typography sx={{ fontSize: '0.75vw', opacity: 0.7 }}>
{details}
</Typography>
</Box>
);
};
return (
<Box
className={isAuthPage ? undefined : 'glass-ui'}
@ -435,6 +536,78 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
marginRight: '1vw',
}}
>
{lastVersion &&
<CustomTooltip
title={getLastLaunchLabel(lastVersion)}
arrow
placement="bottom"
TransitionProps={{ timeout: 120 }}
>
<Button
onClick={handleQuickLaunch}
disableRipple
disableFocusRipple
sx={{
minWidth: 'unset',
width: '3vw',
height: '3vw',
borderRadius: '3vw',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
overflow: 'hidden',
px: '0.8vw',
background:
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.20), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.16), transparent 55%), rgba(10,10,20,0.92)',
border: '1px solid rgba(255,255,255,0.10)',
boxShadow: '0 1.4vw 3.8vw rgba(0,0,0,0.55)',
color: 'white',
backdropFilter: 'blur(14px)',
transition: 'all 0.3s ease',
'& .quick-text': {
opacity: 0,
whiteSpace: 'nowrap',
marginRight: '0.6vw',
fontSize: '0.9vw',
fontFamily: 'Benzin-Bold',
transform: 'translateX(10px)',
transition: 'all 0.25s ease',
},
'&:hover': {
width: '16.5vw',
transform: 'scale(1.05)',
'& .quick-text': {
opacity: 1,
transform: 'translateX(0)',
},
},
'&:after': {
content: '""',
position: 'absolute',
left: '0%',
right: '0%',
bottom: 0,
height: '0.15vw',
borderRadius: '999px',
background:
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
opacity: 0.9,
},
}}
>
<span className="quick-text">Быстрый запуск</span>
<span style={{ fontSize: '1vw' }}></span>
</Button>
</CustomTooltip>
}
{!isLoginPage && !isRegistrationPage && username && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: '1vw' }}>
<HeadAvatar

View File

@ -115,6 +115,21 @@ const LaunchPage = ({
const minecraftStartedListener = () => {
setIsGameRunning(true);
const raw = localStorage.getItem('pending_launch_context');
if (!raw) return;
const context = JSON.parse(raw);
localStorage.setItem(
'last_launched_version',
JSON.stringify({
...context,
launchedAt: Date.now(),
}),
);
localStorage.removeItem('pending_launch_context');
};
const minecraftStoppedListener = () => {
@ -246,6 +261,11 @@ const LaunchPage = ({
setIsDownloading(true);
setBuffer(10);
if (isGameRunning) {
showNotification('Minecraft уже запущен', 'info');
return;
}
// Используем настройки выбранной версии или дефолтные
const currentConfig = versionConfig || {
packName: versionId || 'Comfort',
@ -312,6 +332,25 @@ const LaunchPage = ({
versionToLaunchOverride: isVanillaVersion ? versionId : undefined,
};
const launchContext = {
versionId,
packName: versionId || currentConfig.packName,
baseVersion: currentConfig.baseVersion,
fabricVersion: currentConfig.fabricVersion,
serverIp: currentConfig.serverIp,
isVanillaVersion,
versionToLaunchOverride: isVanillaVersion ? versionId : undefined,
memory: config.memory,
};
localStorage.setItem(
'pending_launch_context',
JSON.stringify(launchContext),
);
const launchResult = await window.electron.ipcRenderer.invoke(
'launch-minecraft',
options,
@ -328,6 +367,12 @@ const LaunchPage = ({
}
};
useEffect(() => {
window.addEventListener('beforeunload', () => {
localStorage.removeItem('pending_launch_context');
});
}, []);
const handleStopMinecraft = async () => {
try {
const result = await window.electron.ipcRenderer.invoke('stop-minecraft');

View File

@ -59,6 +59,8 @@ import {
} from '../utils/notifications';
import { translateServer } from '../utils/serverTranslator';
import { Server } from 'http';
import CapeCard from '../components/CapeCard'
import CoinsDisplay from '../components/CoinsDisplay';
interface TabPanelProps {
children?: React.ReactNode;
@ -189,6 +191,8 @@ export default function Shop() {
const [prankServerId, setPrankServerId] = useState<string>('');
const [prankProcessing, setPrankProcessing] = useState(false);
const [hoveredPrankId, setHoveredPrankId] = useState<string | null>(null);
// Уведомления
const [notification, setNotification] = useState<{
open: boolean;
@ -948,34 +952,22 @@ export default function Shop() {
<TabPanel value={tabValue} index={2}>
{/* Блок плащей */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Typography
variant="h6"
sx={{
fontFamily: 'Benzin-Bold',
backgroundImage:
'linear-gradient(136deg, rgb(242,113,33), rgb(233,64,87), rgb(138,35,135))',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
>
Плащи
</Typography>
{availableCapes.length > 0 ? (
<Grid container spacing={2}>
{availableCapes.map((cape) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={cape.id}>
<ShopItem
type="cape"
id={cape.id}
name={cape.name}
description={cape.description}
imageUrl={cape.image_url}
price={cape.price}
disabled={false}
playerSkinUrl={playerSkinUrl}
onClick={() => handlePurchaseCape(cape.id)}
<CapeCard
key={cape.id}
cape={{
id: cape.id,
name: cape.name,
description: cape.description,
image_url: cape.image_url,
price: cape.price,
}}
mode="shop"
onAction={() => handlePurchaseCape(cape.id)}
actionDisabled={false}
/>
</Grid>
))}
@ -983,12 +975,14 @@ export default function Shop() {
) : (
<Typography>У вас уже есть все доступные плащи!</Typography>
)}
</Box>
</TabPanel>
<TabPanel value={tabValue} index={3}>
<Grid container spacing={2}>
{prankCommands.map((cmd) => (
{prankCommands.map((cmd) => {
const isHovered = hoveredPrankId === cmd.id; // 👈 ВОТ СЮДА
return (
<Grid item xs={12} sm={6} md={4} lg={3} key={cmd.id}>
<Box
sx={{
@ -996,27 +990,30 @@ export default function Shop() {
height: '100%',
borderRadius: '1.4vw',
background:
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.14), transparent 55%), rgba(10,10,20,0.88)',
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.10), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.16), transparent 55%)',
border: '1px solid rgba(255,255,255,0.10)',
boxShadow: '0 1.2vw 3.2vw rgba(0,0,0,0.55)',
backdropFilter: 'blur(14px)',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
transition: 'transform 0.18s ease',
transition: 'all 0.18s ease',
'&:hover': {
transform: 'scale(1.03)',
// transform: 'scale(1.03)',
borderColor: 'rgba(200, 33, 242, 0.35)',
boxShadow: '0 1.2vw 3.2vw rgba(53, 3, 66, 0.75)',
},
}}
>
{/* Картинка */}
<Box
sx={{
height: '9vw',
height: '15vw',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(0,0,0,0.25)',
borderBottom: '1px solid rgba(255,255,255,0.05)',
}}
>
<Box
@ -1024,8 +1021,8 @@ export default function Shop() {
src={`https://cdn.minecraft.popa-popa.ru/textures/${cmd.material?.toLowerCase()}.png`}
alt={cmd.material}
sx={{
width: '6vw',
height: '6vw',
width: '12vw',
height: '12vw',
imageRendering: 'pixelated',
}}
/>
@ -1063,16 +1060,19 @@ export default function Shop() {
{cmd.description}
</Typography>
<Typography
{/* <Box
sx={{
fontWeight: 900,
fontSize: '1vw',
display: 'flex',
gap: '1vw',
alignItems: 'center'
}}
>
Цена: {cmd.price} монет
</Typography>
<Typography>Цена:</Typography> <CoinsDisplay value={cmd.price} size="small" />
</Box> */}
<Button
onMouseEnter={() => setHoveredPrankId(cmd.id)}
onMouseLeave={() => setHoveredPrankId(null)}
disableRipple
onClick={() => {
setSelectedPrank(cmd);
@ -1089,12 +1089,17 @@ export default function Shop() {
'&:hover': { filter: 'brightness(1.05)' },
}}
>
Купить
{isHovered ? (
'Купить'
) : (
<CoinsDisplay showTooltip={false} value={cmd.price} size="small" />
)}
</Button>
</Box>
</Box>
</Grid>
))}
);
})}
</Grid>
</TabPanel>
</Box>