add tray and add settings

This commit is contained in:
aurinex
2025-12-16 00:30:00 +05:00
parent cd7ad5039e
commit 6db213d602
12 changed files with 224 additions and 22 deletions

View File

@ -9,7 +9,7 @@
* `./src/main.js` using webpack. This gives us some performance wins.
*/
import path from 'path';
import { app, BrowserWindow, shell, ipcMain, Tray, Menu, nativeImage } from 'electron';
import { app, BrowserWindow, shell, ipcMain, Tray, Menu, nativeImage, type MenuItemConstructorOptions } from 'electron';
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';
import MenuBuilder from './menu';
@ -71,11 +71,18 @@ const ensureTray = () => {
? path.join(process.resourcesPath, 'assets')
: path.join(__dirname, '../../assets');
const iconPath = path.join(RESOURCES_PATH, 'icon.png');
const image = nativeImage.createFromPath(iconPath);
const getAssetPath = (...paths: string[]) => path.join(RESOURCES_PATH, ...paths);
tray = new Tray(image);
tray.setToolTip('Popa Launcher');
const trayIconPath = getAssetPath('pop-popa.png'); // или 'Icons/popa-popa.png'
const trayImage = nativeImage.createFromPath(trayIconPath);
tray = new Tray(trayImage);
tray.setToolTip('popa-launcher');
tray.on('click', () => {
if (!mainWindow) return;
mainWindow.show();
mainWindow.focus();
});
const menu = Menu.buildFromTemplate([
{
@ -111,9 +118,46 @@ const applyLoginItemSettings = () => {
};
let tray: Tray | null = null;
let isAuthed = false;
let mainWindow: BrowserWindow | null = null;
function buildTrayMenu() {
const icon = nativeImage.createFromPath(
app.isPackaged
? path.join(__dirname, '../../assets/popa-popa.png')
: path.join(process.resourcesPath, 'assets', 'popa-popa.png'),
);
const template: MenuItemConstructorOptions[] = [
{ label: 'popa-popa', enabled: false, icon },
{ type: 'separator' },
...(isAuthed
? ([
{ label: 'Новости', click: () => mainWindow?.webContents.send('tray-navigate', '/news') },
{ label: 'Версии', click: () => mainWindow?.webContents.send('tray-navigate', '/') },
{ label: 'Магазин', click: () => mainWindow?.webContents.send('tray-navigate', '/shop') },
{ label: 'Рынок', click: () => mainWindow?.webContents.send('tray-navigate', '/marketplace') },
{ label: 'Профиль', click: () => mainWindow?.webContents.send('tray-navigate', '/profile') },
{ label: 'Настройки', click: () => mainWindow?.webContents.send('tray-navigate', '/settings') },
{ label: 'Ежедневная награда', click: () => mainWindow?.webContents.send('tray-navigate', '/daily') },
{ label: 'Ежедневные квесты', click: () => mainWindow?.webContents.send('tray-navigate', '/dailyquests') },
{ type: 'separator' },
{ label: 'Выйти', click: () => mainWindow?.webContents.send('tray-logout') },
] as MenuItemConstructorOptions[])
: ([
{ label: 'Войти', click: () => mainWindow?.webContents.send('tray-navigate', '/login') },
] as MenuItemConstructorOptions[])),
{ type: 'separator' },
{ label: 'Показать', click: () => { mainWindow?.show(); mainWindow?.focus(); } },
{ label: 'Выход', click: () => app.quit() },
];
tray?.setContextMenu(Menu.buildFromTemplate(template));
}
ipcMain.on('ipc-example', async (event, arg) => {
const msgTemplate = (pingPong: string) => `IPC test: ${pingPong}`;
console.log(msgTemplate(arg));
@ -192,6 +236,8 @@ const createWindow = async () => {
? path.join(process.resourcesPath, 'assets')
: path.join(__dirname, '../../assets');
ensureTray();
const getAssetPath = (...paths: string[]): string => {
return path.join(RESOURCES_PATH, ...paths);
};
@ -203,7 +249,7 @@ const createWindow = async () => {
autoHideMenuBar: true,
resizable: true,
frame: false,
icon: getAssetPath('icon.png'),
icon: getAssetPath('popa-popa.png'),
webPreferences: {
webSecurity: false,
preload: app.isPackaged
@ -286,3 +332,9 @@ app
ipcMain.handle('install-update', () => {
autoUpdater.quitAndInstall();
});
ipcMain.handle('auth-changed', (_e, payload: { isAuthed: boolean }) => {
isAuthed = Boolean(payload?.isAuthed);
buildTrayMenu();
return true;
});

View File

@ -20,6 +20,9 @@ export type Channels =
| 'stop-minecraft'
| 'minecraft-started'
| 'apply-launcher-settings'
| 'tray-navigate'
| 'tray-logout'
| 'auth-changed'
| 'minecraft-stopped';
const electronHandler = {
@ -42,6 +45,9 @@ const electronHandler = {
invoke(channel: Channels, ...args: unknown[]): Promise<any> {
return ipcRenderer.invoke(channel, ...args);
},
removeAllListeners(channel: Channels) {
ipcRenderer.removeAllListeners(channel);
},
},
};

View File

@ -25,6 +25,7 @@ import { useLocation } from 'react-router-dom';
import DailyReward from './pages/DailyReward';
import DailyQuests from './pages/DailyQuests';
import Settings from './pages/Settings';
import { TrayBridge } from './utils/TrayBridge';
const AuthCheck = ({ children }: { children: ReactNode }) => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
@ -205,6 +206,13 @@ const AppLayout = () => {
}
}, []);
useEffect(() => {
const raw = localStorage.getItem('launcher_config');
const isAuthed = !!raw && !!JSON.parse(raw).accessToken;
window.electron.ipcRenderer.invoke('auth-changed', { isAuthed });
}, []);
return (
<Box sx={{ width: '100vw', height: '100vh', position: 'relative', overflow: 'hidden' }}>
{/* ФОН — НЕ масштабируется */}
@ -247,6 +255,7 @@ const AppLayout = () => {
<TopBar onRegister={handleRegister} username={username || ''} />
<PageHeader />
<Notifier />
<TrayBridge />
<Routes>
<Route

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -42,6 +42,8 @@ export default function CoinsDisplay({
sx,
}: CoinsDisplayProps) {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [settingsVersion, setSettingsVersion] = useState(0);
const storageKey = useMemo(() => {
// ключ под конкретного пользователя
return username ? `coins:${username}` : 'coins:anonymous';
@ -71,7 +73,24 @@ export default function CoinsDisplay({
return 0;
});
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(() => {
const handler = () => setSettingsVersion((v) => v + 1);
window.addEventListener('settings-updated', handler as EventListener);
return () => window.removeEventListener('settings-updated', handler as EventListener);
}, []);
const isTooltipDisabledBySettings = useMemo(() => {
try {
const raw = localStorage.getItem('launcher_settings');
if (!raw) return false;
const s = JSON.parse(raw);
return Boolean(s?.disableToolTip);
} catch {
return false;
}
}, [settingsVersion]);
const tooltipEnabled = showTooltip && !isTooltipDisabledBySettings;
const getSizes = () => {
switch (size) {
@ -172,7 +191,7 @@ export default function CoinsDisplay({
borderRadius: sizes.borderRadius,
padding: sizes.containerPadding,
border: '1px solid rgba(255, 255, 255, 0.1)',
cursor: showTooltip ? 'help' : 'default',
cursor: tooltipEnabled ? 'help' : 'default',
// можно оставить лёгкий намёк на загрузку, но без "пульса" текста
opacity: isLoading ? 0.85 : 1,
@ -222,7 +241,7 @@ export default function CoinsDisplay({
</Box>
);
if (showTooltip) {
if (tooltipEnabled) {
return (
<CustomTooltip
title={tooltipText}

View File

@ -1,12 +1,39 @@
/* eslint-disable react/jsx-props-no-spreading */
import React, { useEffect, useState, useMemo } from 'react';
import { styled } from '@mui/material/styles';
import Tooltip, { tooltipClasses, TooltipProps } from '@mui/material/Tooltip';
// Создаем кастомный стилизованный Tooltip с правильной типизацией
const CustomTooltip = styled(({ className, ...props }: TooltipProps) => (
const STORAGE_KEY = 'launcher_settings';
function readDisableTooltip(): boolean {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return false;
const s = JSON.parse(raw);
return Boolean(s?.disableToolTip);
} catch {
return false;
}
}
const readTooltipPolicy = () => {
try {
const raw = localStorage.getItem('launcher_settings');
if (!raw) return { disableToolTip: false, allowEssentialTooltips: true };
const s = JSON.parse(raw);
return {
disableToolTip: Boolean(s?.disableToolTip),
allowEssentialTooltips: s?.allowEssentialTooltips !== false, // default true
};
} catch {
return { disableToolTip: false, allowEssentialTooltips: true };
}
};
// ВАЖНО: styled-компонент отдельно, чтобы не пересоздавался на каждый рендер
const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
))(() => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: 'rgba(0, 0, 0, 0.9)',
color: '#fff',
@ -72,4 +99,29 @@ const CustomTooltip = styled(({ className, ...props }: TooltipProps) => (
},
}));
export default CustomTooltip;
export type CustomTooltipProps = TooltipProps & {
/**
* Можно принудительно отключить тултип снаружи,
* плюс учитывается настройка disableToolTip из launcher_settings
*/
essential?: boolean;
disabled?: boolean;
};
export default function CustomTooltip(props: CustomTooltipProps) {
const { essential = false, children, ...rest } = props;
const { disableToolTip, allowEssentialTooltips } = useMemo(
() => readTooltipPolicy(),
// важно: чтобы при "Save" пересчитывалось — ты уже диспатчишь settings-updated
// поэтому ниже мы просто прочитаем ещё раз через key в местах использования (или можно слушать event тут)
[],
);
const disabledBySettings = disableToolTip && !(essential && allowEssentialTooltips);
// Если отключено — просто возвращаем children без обёртки Tooltip
if (disabledBySettings) return <>{children}</>;
return <StyledTooltip {...props}>{children}</StyledTooltip>;
}

View File

@ -206,6 +206,7 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
const logout = () => {
localStorage.removeItem('launcher_config');
navigate('/login');
window.electron.ipcRenderer.invoke('auth-changed', { isAuthed: false });
};
const loadSkin = useCallback(async () => {

View File

@ -435,7 +435,7 @@ export default function DailyReward({ onClaimed }: Props) {
</Box>
<Stack direction="row" alignItems="center" spacing={1}>
<CustomTooltip title="К текущему месяцу">
<CustomTooltip essential title="К текущему месяцу">
<IconButton
onClick={goToday}
sx={{
@ -661,7 +661,7 @@ export default function DailyReward({ onClaimed }: Props) {
</Box>
<Box sx={{ display: 'flex', gap: '1.2vw', alignItems: 'center' }}>
<CustomTooltip title={subtitle} disableInteractive>
<CustomTooltip essential title={subtitle} disableInteractive>
<Box
sx={{
display: 'inline-block',
@ -701,7 +701,7 @@ export default function DailyReward({ onClaimed }: Props) {
</Box>
</CustomTooltip>
<CustomTooltip title="Сбросить выбор на сегодня">
<CustomTooltip essential title="Сбросить выбор на сегодня">
<IconButton
onClick={() => setSelected(today)}
sx={{

View File

@ -96,7 +96,7 @@ const Login = ({ onLoginSuccess }: LoginProps) => {
if (onLoginSuccess) {
onLoginSuccess(config.username);
}
await window.electron.ipcRenderer.invoke('auth-changed', { isAuthed: true });
navigate('/');
} catch (error: any) {
console.log(`ОШИБКА при авторизации: ${error.message}`);

View File

@ -29,6 +29,8 @@ type SettingsState = {
autoLaunch: boolean;
startInTray: boolean;
closeToTray: boolean;
disableToolTip: boolean;
allowEssentialTooltips: boolean;
// Game
autoRotateSkinViewer: boolean;
@ -73,7 +75,7 @@ const GRADIENT =
backgroundColor: 'rgba(10,10,20,0.92)',
border: '2px solid rgba(255,255,255,0.18)',
boxShadow: '0 0 1.6vw rgba(233,64,205,0.35)',
transition: 'transform 0.15s ease, box-shadow 0.15s ease',
transition: 'transform 0.15s ease, box-shadow 0.15s ease, height 0.3s ease, width 0.3s ease',
'&:before': { display: 'none' },
'&:hover, &.Mui-focusVisible': {
width: '1.95vw',
@ -100,6 +102,8 @@ const defaultSettings: SettingsState = {
startInTray: false,
autoLaunch: false,
closeToTray: true,
disableToolTip: false,
allowEssentialTooltips: true,
autoRotateSkinViewer: true,
walkingSpeed: 0.5,
@ -351,7 +355,7 @@ const Settings = () => {
sx={{
px: '2vw',
pb: '2vw',
width: '100%',
width: '95%',
boxSizing: 'border-box',
}}
>
@ -567,6 +571,20 @@ const Settings = () => {
checked={settings.rememberLastRoute}
onChange={setFlag('rememberLastRoute')}
/>
<SettingCheckboxRow
title="Отключить подсказки"
description="Отключить подсказки при наведении на элементы"
checked={settings.disableToolTip}
onChange={setFlag('disableToolTip')}
/>
<SettingCheckboxRow
title="Показывать важные подсказки"
description="Некоторые подсказки нельзя отключить (важные)"
checked={settings.allowEssentialTooltips}
onChange={setFlag('allowEssentialTooltips')}
/>
</Box>
</Glass>
</Box>

View File

@ -0,0 +1,27 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
export function TrayBridge() {
const navigate = useNavigate();
useEffect(() => {
const onNavigate = (to: unknown) => {
navigate(String(to));
};
const onLogout = () => {
localStorage.removeItem('launcher_config');
navigate('/login');
};
window.electron.ipcRenderer.on('tray-navigate', onNavigate);
window.electron.ipcRenderer.on('tray-logout', onLogout);
return () => {
window.electron.ipcRenderer.removeAllListeners('tray-navigate');
window.electron.ipcRenderer.removeAllListeners('tray-logout');
};
}, [navigate]);
return null;
}