add tray and add settings
This commit is contained in:
@ -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';
|
||||
@ -68,14 +68,21 @@ const ensureTray = () => {
|
||||
if (tray) return;
|
||||
|
||||
const RESOURCES_PATH = app.isPackaged
|
||||
? path.join(process.resourcesPath, 'assets')
|
||||
: path.join(__dirname, '../../assets');
|
||||
? 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;
|
||||
});
|
||||
|
||||
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
BIN
src/renderer/assets/Icons/popa-popa.png
Normal file
BIN
src/renderer/assets/Icons/popa-popa.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
18
src/renderer/assets/Icons/popa-popa.svg
Normal file
18
src/renderer/assets/Icons/popa-popa.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 80 KiB |
@ -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}
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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={{
|
||||
|
||||
@ -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}`);
|
||||
|
||||
@ -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>
|
||||
|
||||
27
src/renderer/utils/TrayBridge.tsx
Normal file
27
src/renderer/utils/TrayBridge.tsx
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user