add neoforge modpacks support
This commit is contained in:
@ -6,15 +6,13 @@ import extract from 'extract-zip';
|
|||||||
import { launch, Version, diagnose } from '@xmcl/core';
|
import { launch, Version, diagnose } from '@xmcl/core';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import {
|
import {
|
||||||
installDependencies,
|
|
||||||
installFabric,
|
installFabric,
|
||||||
getFabricLoaders,
|
|
||||||
getVersionList,
|
getVersionList,
|
||||||
install,
|
|
||||||
installTask,
|
installTask,
|
||||||
installDependenciesTask,
|
installDependenciesTask,
|
||||||
|
installNeoForged,
|
||||||
} from '@xmcl/installer';
|
} from '@xmcl/installer';
|
||||||
import { Dispatcher, Agent } from 'undici';
|
import { Agent } from 'undici';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { AuthService } from './auth-service';
|
import { AuthService } from './auth-service';
|
||||||
import { API_BASE_URL } from '../renderer/api';
|
import { API_BASE_URL } from '../renderer/api';
|
||||||
@ -546,12 +544,14 @@ export function initMinecraftHandlers() {
|
|||||||
username,
|
username,
|
||||||
memory = 4096,
|
memory = 4096,
|
||||||
baseVersion = '1.21.4',
|
baseVersion = '1.21.4',
|
||||||
fabricVersion = '0.16.14',
|
fabricVersion = null,
|
||||||
|
neoForgeVersion = null,
|
||||||
packName = 'Comfort', // имя сборки (папка с модами)
|
packName = 'Comfort', // имя сборки (папка с модами)
|
||||||
versionToLaunchOverride = '', // переопределение версии для запуска (например, 1.21.10 для ванили)
|
versionToLaunchOverride = '', // переопределение версии для запуска (например, 1.21.10 для ванили)
|
||||||
serverIp = 'popa-popa.ru',
|
serverIp = 'popa-popa.ru',
|
||||||
serverPort,
|
serverPort,
|
||||||
isVanillaVersion = false,
|
isVanillaVersion = false,
|
||||||
|
loaderType = 'fabric',
|
||||||
} = gameConfig || {};
|
} = gameConfig || {};
|
||||||
|
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
@ -590,23 +590,35 @@ export function initMinecraftHandlers() {
|
|||||||
// Ваниль — запускаем baseVersion (или override)
|
// Ваниль — запускаем baseVersion (или override)
|
||||||
versionToLaunch = effectiveBaseVersion;
|
versionToLaunch = effectiveBaseVersion;
|
||||||
} else {
|
} else {
|
||||||
// Модпак — запускаем именно fabric-версию
|
// Определяем ID версии в зависимости от типа загрузчика
|
||||||
|
if (loaderType === 'neoforge' && neoForgeVersion) {
|
||||||
|
const neoForgeId = `neoforge-${neoForgeVersion}`;
|
||||||
|
if (versionsContents.includes(neoForgeId)) {
|
||||||
|
versionToLaunch = neoForgeId;
|
||||||
|
} else {
|
||||||
|
versionToLaunch = neoForgeId;
|
||||||
|
}
|
||||||
|
} else if (fabricVersion) {
|
||||||
const fabricId = `${effectiveBaseVersion}-fabric${fabricVersion}`;
|
const fabricId = `${effectiveBaseVersion}-fabric${fabricVersion}`;
|
||||||
if (versionsContents.includes(fabricId)) {
|
if (versionsContents.includes(fabricId)) {
|
||||||
versionToLaunch = fabricId;
|
versionToLaunch = fabricId;
|
||||||
} else {
|
} else {
|
||||||
// даже если папки нет — installFabric её создаст
|
|
||||||
versionToLaunch = fabricId;
|
versionToLaunch = fabricId;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
versionToLaunch = effectiveBaseVersion;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('isVanillaVersion:', isVanillaVersion);
|
console.log('Конфигурация:', {
|
||||||
console.log('baseVersion:', baseVersion);
|
loaderType,
|
||||||
console.log('effectiveBaseVersion:', effectiveBaseVersion);
|
neoForgeVersion,
|
||||||
console.log('fabricVersion:', fabricVersion);
|
fabricVersion,
|
||||||
console.log('versionToLaunch:', versionToLaunch);
|
effectiveBaseVersion,
|
||||||
console.log('packDir:', packDir);
|
versionToLaunch,
|
||||||
|
versionsContents,
|
||||||
|
});
|
||||||
|
|
||||||
// --- Поиск Java ---
|
// --- Поиск Java ---
|
||||||
event.sender.send('installation-status', {
|
event.sender.send('installation-status', {
|
||||||
@ -705,7 +717,42 @@ export function initMinecraftHandlers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. Установка Fabric (только для модпаков) ---
|
// --- 2. Установка Fabric (только для модпаков) ---
|
||||||
if (!isVanillaVersion && fabricVersion) {
|
if (!isVanillaVersion) {
|
||||||
|
if (loaderType === 'neoforge' && neoForgeVersion) {
|
||||||
|
try {
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'neoforge-install',
|
||||||
|
message: `Установка NeoForge ${neoForgeVersion}...`,
|
||||||
|
});
|
||||||
|
|
||||||
|
event.sender.send(
|
||||||
|
'overall-progress',
|
||||||
|
getGlobalProgress('fabric-install', 0), // Используем фазу fabric-install
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('installNeoForged:', {
|
||||||
|
project: 'neoforge',
|
||||||
|
version: neoForgeVersion,
|
||||||
|
minecraftVersion: effectiveBaseVersion,
|
||||||
|
minecraftDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Установка NeoForge
|
||||||
|
await installNeoForged('neoforge', neoForgeVersion, minecraftDir, {
|
||||||
|
minecraft: effectiveBaseVersion,
|
||||||
|
java: javaPath,
|
||||||
|
side: 'client',
|
||||||
|
});
|
||||||
|
|
||||||
|
event.sender.send(
|
||||||
|
'overall-progress',
|
||||||
|
getGlobalProgress('fabric-install', 1),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Ошибка при установке NeoForge, продолжаем:', error);
|
||||||
|
}
|
||||||
|
} else if (fabricVersion) {
|
||||||
|
// Существующий код для Fabric
|
||||||
try {
|
try {
|
||||||
event.sender.send('installation-status', {
|
event.sender.send('installation-status', {
|
||||||
step: 'fabric-install',
|
step: 'fabric-install',
|
||||||
@ -736,6 +783,7 @@ export function initMinecraftHandlers() {
|
|||||||
console.log('Ошибка при установке Fabric, продолжаем:', error);
|
console.log('Ошибка при установке Fabric, продолжаем:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- 3. Установка зависимостей для versionToLaunch ---
|
// --- 3. Установка зависимостей для versionToLaunch ---
|
||||||
try {
|
try {
|
||||||
@ -991,12 +1039,17 @@ export function initMinecraftHandlers() {
|
|||||||
const versionPath = path.join(versionsDir, item);
|
const versionPath = path.join(versionsDir, item);
|
||||||
if (!fs.statSync(versionPath).isDirectory()) continue;
|
if (!fs.statSync(versionPath).isDirectory()) continue;
|
||||||
|
|
||||||
// ❗ Прячем технические fabric-версии типа 1.21.10-fabric0.18.1
|
// ❗ Прячем технические версии загрузчиков
|
||||||
if (item.includes('-fabric')) {
|
if (item.includes('-fabric') || item.includes('-neoforge')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionJsonPath = path.join(versionPath, `${item}.json`);
|
const files = fs.readdirSync(versionPath);
|
||||||
|
const jsonFile = files.find((f) => f.endsWith('.json'));
|
||||||
|
|
||||||
|
if (!jsonFile) continue;
|
||||||
|
|
||||||
|
const versionJsonPath = path.join(versionPath, jsonFile);
|
||||||
let versionInfo = {
|
let versionInfo = {
|
||||||
id: item,
|
id: item,
|
||||||
name: item,
|
name: item,
|
||||||
@ -1270,7 +1323,9 @@ ipcMain.handle('get-version-config', async (event, { versionId }) => {
|
|||||||
memory: 4096,
|
memory: 4096,
|
||||||
baseVersion: '1.21.10',
|
baseVersion: '1.21.10',
|
||||||
serverIp: 'popa-popa.ru',
|
serverIp: 'popa-popa.ru',
|
||||||
fabricVersion: '0.18.1',
|
fabricVersion: null,
|
||||||
|
neoForgeVersion: null,
|
||||||
|
loaderType: 'fabric', // 'fabric', 'neoforge', 'vanilla'
|
||||||
preserveFiles: ['popa-launcher-config.json'],
|
preserveFiles: ['popa-launcher-config.json'],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1282,6 +1337,20 @@ ipcMain.handle('get-version-config', async (event, { versionId }) => {
|
|||||||
'https://api.github.com/repos/DIKER0K/Comfort/releases/latest';
|
'https://api.github.com/repos/DIKER0K/Comfort/releases/latest';
|
||||||
config.baseVersion = '1.21.10';
|
config.baseVersion = '1.21.10';
|
||||||
config.fabricVersion = '0.18.1';
|
config.fabricVersion = '0.18.1';
|
||||||
|
config.loaderType = 'fabric';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если это NeoForge сборка, добавьте соответствующие настройки
|
||||||
|
if (versionId === 'MyNeoForgePack') {
|
||||||
|
config.downloadUrl =
|
||||||
|
'https://github.com/YOUR_USERNAME/YOUR_NEOFORGE_PACK/releases/latest/download/MyNeoForgePack.zip';
|
||||||
|
config.apiReleaseUrl =
|
||||||
|
'https://api.github.com/repos/YOUR_USERNAME/YOUR_NEOFORGE_PACK/releases/latest';
|
||||||
|
config.baseVersion = '1.21.1';
|
||||||
|
config.fabricVersion = null;
|
||||||
|
config.neoForgeVersion = '20.6.2';
|
||||||
|
config.loaderType = 'neoforge';
|
||||||
|
config.memory = 6144; // NeoForge может требовать больше памяти
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если есть конфигурационный файл, загружаем из него
|
// Если есть конфигурационный файл, загружаем из него
|
||||||
|
|||||||
@ -1,9 +1,4 @@
|
|||||||
import {
|
import { Box, Typography, Button, LinearProgress } from '@mui/material';
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
LinearProgress,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import ServerStatus from '../components/ServerStatus/ServerStatus';
|
import ServerStatus from '../components/ServerStatus/ServerStatus';
|
||||||
@ -13,7 +8,10 @@ import React from 'react';
|
|||||||
import SettingsModal from '../components/Settings/SettingsModal';
|
import SettingsModal from '../components/Settings/SettingsModal';
|
||||||
import CustomNotification from '../components/Notifications/CustomNotification';
|
import CustomNotification from '../components/Notifications/CustomNotification';
|
||||||
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
|
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
|
||||||
import { isNotificationsEnabled, getNotifPositionFromSettings } from '../utils/notifications';
|
import {
|
||||||
|
isNotificationsEnabled,
|
||||||
|
getNotifPositionFromSettings,
|
||||||
|
} from '../utils/notifications';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -31,7 +29,6 @@ declare global {
|
|||||||
interface LaunchPageProps {
|
interface LaunchPageProps {
|
||||||
onLaunchPage?: () => void;
|
onLaunchPage?: () => void;
|
||||||
launchOptions?: {
|
launchOptions?: {
|
||||||
// Делаем опциональным
|
|
||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
apiReleaseUrl: string;
|
apiReleaseUrl: string;
|
||||||
versionFileName: string;
|
versionFileName: string;
|
||||||
@ -40,6 +37,8 @@ interface LaunchPageProps {
|
|||||||
baseVersion: string;
|
baseVersion: string;
|
||||||
serverIp: string;
|
serverIp: string;
|
||||||
fabricVersion: string;
|
fabricVersion: string;
|
||||||
|
neoForgeVersion?: string;
|
||||||
|
loaderType?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +49,7 @@ const LaunchPage = ({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { versionId } = useParams();
|
const { versionId } = useParams();
|
||||||
const [versionConfig, setVersionConfig] = useState<any>(null);
|
const [versionConfig, setVersionConfig] = useState<any>(null);
|
||||||
|
const [fullVersionConfig, setFullVersionConfig] = useState<any>(null); // Полная конфигурация из Gist
|
||||||
|
|
||||||
// Начальное состояние должно быть пустым или с минимальными значениями
|
// Начальное состояние должно быть пустым или с минимальными значениями
|
||||||
const [config, setConfig] = useState<{
|
const [config, setConfig] = useState<{
|
||||||
@ -161,6 +161,29 @@ const LaunchPage = ({
|
|||||||
};
|
};
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
|
// Функция для загрузки полной конфигурации версии из Gist
|
||||||
|
const fetchFullVersionConfig = async (): Promise<any> => {
|
||||||
|
if (!versionId) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Загружаем весь список версий из Gist
|
||||||
|
const result = await window.electron.ipcRenderer.invoke(
|
||||||
|
'get-available-versions',
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success && result.versions) {
|
||||||
|
// Находим нужную версию по ID
|
||||||
|
const version = result.versions.find((v: any) => v.id === versionId);
|
||||||
|
return version || null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке полной конфигурации:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchVersionConfig = async () => {
|
const fetchVersionConfig = async () => {
|
||||||
if (!versionId) return;
|
if (!versionId) return;
|
||||||
@ -174,6 +197,7 @@ const LaunchPage = ({
|
|||||||
// Если конфиг пустой — считаем, что он невалидный и идём по IPC-ветке
|
// Если конфиг пустой — считаем, что он невалидный и идём по IPC-ветке
|
||||||
if (Object.keys(parsedConfig).length > 0) {
|
if (Object.keys(parsedConfig).length > 0) {
|
||||||
setVersionConfig(parsedConfig);
|
setVersionConfig(parsedConfig);
|
||||||
|
setFullVersionConfig(parsedConfig);
|
||||||
|
|
||||||
setConfig({
|
setConfig({
|
||||||
memory: parsedConfig.memory || 4096,
|
memory: parsedConfig.memory || 4096,
|
||||||
@ -187,7 +211,20 @@ const LaunchPage = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если нет в localStorage, запрашиваем с сервера
|
// Загружаем полную конфигурацию из Gist
|
||||||
|
const fullConfig = await fetchFullVersionConfig();
|
||||||
|
if (fullConfig) {
|
||||||
|
setFullVersionConfig(fullConfig);
|
||||||
|
|
||||||
|
// Сохраняем только config часть для совместимости
|
||||||
|
setVersionConfig(fullConfig.config || {});
|
||||||
|
|
||||||
|
setConfig({
|
||||||
|
memory: fullConfig.config?.memory || 4096,
|
||||||
|
preserveFiles: fullConfig.config?.preserveFiles || [],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Если не удалось получить конфигурацию из Gist, используем IPC
|
||||||
const result = await window.electron.ipcRenderer.invoke(
|
const result = await window.electron.ipcRenderer.invoke(
|
||||||
'get-version-config',
|
'get-version-config',
|
||||||
{ versionId },
|
{ versionId },
|
||||||
@ -218,6 +255,7 @@ const LaunchPage = ({
|
|||||||
preserveFiles: defaultConfig.preserveFiles || [],
|
preserveFiles: defaultConfig.preserveFiles || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при получении настроек версии:', error);
|
console.error('Ошибка при получении настроек версии:', error);
|
||||||
// Используем значения по умолчанию
|
// Используем значения по умолчанию
|
||||||
@ -266,16 +304,31 @@ const LaunchPage = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Используем настройки выбранной версии или дефолтные
|
// Загружаем полную конфигурацию, если еще не загружена
|
||||||
const currentConfig = versionConfig || {
|
if (!fullVersionConfig) {
|
||||||
|
const loadedConfig = await fetchFullVersionConfig();
|
||||||
|
if (loadedConfig) {
|
||||||
|
setFullVersionConfig(loadedConfig);
|
||||||
|
setVersionConfig(loadedConfig.config || {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Используем настройки из Gist или дефолтные
|
||||||
|
const currentConfig = fullVersionConfig?.config ||
|
||||||
|
versionConfig || {
|
||||||
packName: versionId || 'Comfort',
|
packName: versionId || 'Comfort',
|
||||||
memory: 4096,
|
memory: 4096,
|
||||||
baseVersion: '1.21.4',
|
baseVersion: '1.21.4',
|
||||||
serverIp: 'popa-popa.ru',
|
serverIp: 'popa-popa.ru',
|
||||||
fabricVersion: '0.16.14',
|
fabricVersion: '0.16.14',
|
||||||
|
neoForgeVersion: null,
|
||||||
|
loaderType: 'fabric',
|
||||||
preserveFiles: [],
|
preserveFiles: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Получаем версию для запуска из Gist (например, "1.21.1-neoforge21.1.215")
|
||||||
|
const versionFromGist = fullVersionConfig?.version || null;
|
||||||
|
|
||||||
// Проверяем, является ли это ванильной версией
|
// Проверяем, является ли это ванильной версией
|
||||||
const isVanillaVersion =
|
const isVanillaVersion =
|
||||||
!currentConfig.downloadUrl || currentConfig.downloadUrl === '';
|
!currentConfig.downloadUrl || currentConfig.downloadUrl === '';
|
||||||
@ -318,7 +371,8 @@ const LaunchPage = ({
|
|||||||
localStorage.getItem('launcher_config') || '{}',
|
localStorage.getItem('launcher_config') || '{}',
|
||||||
);
|
);
|
||||||
|
|
||||||
const options = {
|
// Формируем полные опции для запуска
|
||||||
|
const options: any = {
|
||||||
accessToken: savedConfig.accessToken,
|
accessToken: savedConfig.accessToken,
|
||||||
uuid: savedConfig.uuid,
|
uuid: savedConfig.uuid,
|
||||||
username: savedConfig.username,
|
username: savedConfig.username,
|
||||||
@ -327,22 +381,27 @@ const LaunchPage = ({
|
|||||||
packName: versionId || currentConfig.packName,
|
packName: versionId || currentConfig.packName,
|
||||||
serverIp: currentConfig.serverIp,
|
serverIp: currentConfig.serverIp,
|
||||||
fabricVersion: currentConfig.fabricVersion,
|
fabricVersion: currentConfig.fabricVersion,
|
||||||
// Для ванильной версии устанавливаем флаг
|
neoForgeVersion: currentConfig.neoForgeVersion,
|
||||||
|
loaderType: currentConfig.loaderType || 'fabric',
|
||||||
isVanillaVersion: isVanillaVersion,
|
isVanillaVersion: isVanillaVersion,
|
||||||
versionToLaunchOverride: isVanillaVersion ? versionId : undefined,
|
versionToLaunchOverride:
|
||||||
|
versionFromGist || (isVanillaVersion ? versionId : undefined),
|
||||||
|
// Передаем Gist URL для загрузки конфигурации в процессе запуска
|
||||||
|
gistUrl:
|
||||||
|
'https://gist.githubusercontent.com/DIKER0K/06cd12fb3a4d08b1f0f8c763a7d05e06/raw/versions.json',
|
||||||
};
|
};
|
||||||
|
|
||||||
const launchContext = {
|
const launchContext = {
|
||||||
versionId,
|
versionId,
|
||||||
packName: versionId || currentConfig.packName,
|
packName: versionId || currentConfig.packName,
|
||||||
|
|
||||||
baseVersion: currentConfig.baseVersion,
|
baseVersion: currentConfig.baseVersion,
|
||||||
fabricVersion: currentConfig.fabricVersion,
|
fabricVersion: currentConfig.fabricVersion,
|
||||||
|
neoForgeVersion: currentConfig.neoForgeVersion,
|
||||||
|
loaderType: currentConfig.loaderType,
|
||||||
serverIp: currentConfig.serverIp,
|
serverIp: currentConfig.serverIp,
|
||||||
|
|
||||||
isVanillaVersion,
|
isVanillaVersion,
|
||||||
versionToLaunchOverride: isVanillaVersion ? versionId : undefined,
|
versionToLaunchOverride:
|
||||||
|
versionFromGist || (isVanillaVersion ? versionId : undefined),
|
||||||
memory: config.memory,
|
memory: config.memory,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -358,8 +417,10 @@ const LaunchPage = ({
|
|||||||
|
|
||||||
if (launchResult?.success) {
|
if (launchResult?.success) {
|
||||||
showNotification('Minecraft успешно запущен!', 'success');
|
showNotification('Minecraft успешно запущен!', 'success');
|
||||||
|
} else if (launchResult?.error) {
|
||||||
|
showNotification(`Ошибка запуска: ${launchResult.error}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
showNotification(`Ошибка: ${error.message}`, 'error');
|
showNotification(`Ошибка: ${error.message}`, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
@ -408,9 +469,6 @@ const LaunchPage = ({
|
|||||||
config: configToSave,
|
config: configToSave,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обновляем launchOptions
|
|
||||||
launchOptions.memory = config.memory;
|
|
||||||
|
|
||||||
showNotification('Настройки сохранены', 'success');
|
showNotification('Настройки сохранены', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при сохранении настроек:', error);
|
console.error('Ошибка при сохранении настроек:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user