forked from OctoWoW/OctoLauncher
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fbad749f0c | |||
| 16e442ea0f |
@@ -2,3 +2,9 @@
|
|||||||
hooks/* text eol=lf
|
hooks/* text eol=lf
|
||||||
*.sh text eol=lf
|
*.sh text eol=lf
|
||||||
*.py text eol=lf
|
*.py text eol=lf
|
||||||
|
|
||||||
|
.gitea/** export-ignore
|
||||||
|
.env.ptr export-ignore
|
||||||
|
electron-builder.ptr.yml export-ignore
|
||||||
|
server/Dockerfile export-ignore
|
||||||
|
scripts/publish-oss.sh export-ignore
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
dist/
|
dist*/
|
||||||
out/
|
out/
|
||||||
release/
|
release/
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
productName: OctoLauncher
|
productName: OctoLauncher
|
||||||
|
appId: st.octowow.launcher
|
||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
output: dist
|
output: distprod
|
||||||
files:
|
files:
|
||||||
- '!**/.vscode/*'
|
- '!**/.vscode/*'
|
||||||
- '!src/*'
|
- '!src/*'
|
||||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||||
# Strip every project-root .md file from the asar bundle
|
|
||||||
- '!*.md'
|
- '!*.md'
|
||||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,.prettierrc.cjs,dev-app-update.yml}'
|
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,.prettierrc.cjs,dev-app-update.yml}'
|
||||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
- '!{tailwind.config.ts,postcss.config.cjs}'
|
- '!{tailwind.config.ts,postcss.config.cjs}'
|
||||||
- '!{dist,dist-new,dist-test,out/main/chunks}/**'
|
- '!dist*/**'
|
||||||
|
- '!out/main/chunks/**'
|
||||||
|
- '!{.gitea,.github}/**'
|
||||||
|
- '!Tools/**'
|
||||||
|
- '!**/builder-debug.yml'
|
||||||
|
- '!**/electron-builder.*'
|
||||||
- '!.launcher/**'
|
- '!.launcher/**'
|
||||||
- '!WTF/**'
|
- '!WTF/**'
|
||||||
- '!server/**'
|
- '!server/**'
|
||||||
@@ -20,10 +25,8 @@ files:
|
|||||||
- '!**/node_modules/**/{__tests__,test,tests,docs,example,examples,demo,demos,benchmark,benchmarks}/**'
|
- '!**/node_modules/**/{__tests__,test,tests,docs,example,examples,demo,demos,benchmark,benchmarks}/**'
|
||||||
- '!**/node_modules/**/*.{tsx,map,markdown}'
|
- '!**/node_modules/**/*.{tsx,map,markdown}'
|
||||||
- '!**/node_modules/**/build/Release/obj/**'
|
- '!**/node_modules/**/build/Release/obj/**'
|
||||||
- '!**/node_modules/**/build/Release/{*.iobj,*.ipdb,*.recipe}'
|
- '!**/node_modules/**/build/Release/{*.iobj,*.ipdb,*.recipe,*.exp,*.lib,*.pdb,*.obj}'
|
||||||
- '!**/node_modules/**/*.{vcxproj,vcxproj.filters}'
|
- '!**/node_modules/**/*.{vcxproj,vcxproj.filters}'
|
||||||
asarUnpack:
|
|
||||||
- resources/**
|
|
||||||
npmRebuild: false
|
npmRebuild: false
|
||||||
electronLanguages: en
|
electronLanguages: en
|
||||||
win:
|
win:
|
||||||
|
|||||||
Generated
+19
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "octo-launcher",
|
"name": "octo-launcher",
|
||||||
"version": "1.0.18",
|
"version": "1.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "octo-launcher",
|
"name": "octo-launcher",
|
||||||
"version": "1.0.18",
|
"version": "1.2.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^1.0.3",
|
"@electron-toolkit/preload": "^1.0.3",
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"adm-zip": "^0.5.17",
|
"adm-zip": "^0.5.17",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"dll-inject": "^0.0.3",
|
"dll-inject": "^0.0.3",
|
||||||
|
"dompurify": "^3.4.11",
|
||||||
"electron-log": "^5.1.5",
|
"electron-log": "^5.1.5",
|
||||||
"electron-trpc": "^0.5.2",
|
"electron-trpc": "^0.5.2",
|
||||||
"electron-updater": "^5.3.0",
|
"electron-updater": "^5.3.0",
|
||||||
@@ -1785,6 +1786,13 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz",
|
||||||
"integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg=="
|
"integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/verror": {
|
"node_modules/@types/verror": {
|
||||||
"version": "1.10.9",
|
"version": "1.10.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz",
|
||||||
@@ -3425,6 +3433,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.4.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
|
||||||
|
"integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "9.0.2",
|
"version": "9.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz",
|
||||||
|
|||||||
+7
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "octo-launcher",
|
"name": "octo-launcher",
|
||||||
"version": "1.1.1",
|
"version": "1.2.0",
|
||||||
"description": "An Electron application for launching and updating the OctoWoW client",
|
"description": "An Electron application for launching and updating the OctoWoW client",
|
||||||
"author": "OctoWoW",
|
"author": "OctoWoW",
|
||||||
"copyright": "Copyright © 2026 OctoWoW",
|
"copyright": "Copyright © 2026 OctoWoW",
|
||||||
@@ -9,11 +9,14 @@
|
|||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "electron-vite dev",
|
"dev": "electron-vite dev",
|
||||||
"server": "cd server && npm run dev",
|
"server": "cd server && npm run dev",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps && node scripts/scrub-native-paths.cjs",
|
||||||
"build": "electron-vite build",
|
"build": "electron-vite build",
|
||||||
"build:test": "electron-vite build --mode test",
|
"build:test": "electron-vite build --mode test",
|
||||||
|
"build:ptr": "electron-vite build --mode ptr",
|
||||||
"pack": "electron-builder --config",
|
"pack": "electron-builder --config",
|
||||||
"dist": "tsc && npm run build && npm run pack"
|
"pack:ptr": "electron-builder --config electron-builder.ptr.yml",
|
||||||
|
"dist": "tsc && npm run build && npm run pack",
|
||||||
|
"dist:ptr": "tsc && npm run build:ptr && npm run pack:ptr"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^1.0.3",
|
"@electron-toolkit/preload": "^1.0.3",
|
||||||
@@ -27,6 +30,7 @@
|
|||||||
"adm-zip": "^0.5.17",
|
"adm-zip": "^0.5.17",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"dll-inject": "^0.0.3",
|
"dll-inject": "^0.0.3",
|
||||||
|
"dompurify": "^3.4.11",
|
||||||
"electron-log": "^5.1.5",
|
"electron-log": "^5.1.5",
|
||||||
"electron-trpc": "^0.5.2",
|
"electron-trpc": "^0.5.2",
|
||||||
"electron-updater": "^5.3.0",
|
"electron-updater": "^5.3.0",
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ const giteaProvider: Provider = {
|
|||||||
)}`
|
)}`
|
||||||
};
|
};
|
||||||
|
|
||||||
// only allow known git hosts over https
|
|
||||||
const parseGitUrl = (
|
const parseGitUrl = (
|
||||||
git: string
|
git: string
|
||||||
): { owner: string; repo: string; provider: Provider } => {
|
): { owner: string; repo: string; provider: Provider } => {
|
||||||
|
|||||||
@@ -231,8 +231,9 @@ export const defaultSources: AddonSource[] = [
|
|||||||
description: 'Smart consolidated buff frames with extensive customization'
|
description: 'Smart consolidated buff frames with extensive customization'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
git: 'https://github.com/acolis/VitalWatch.git',
|
git: 'https://octowow.st/git/shaga/LifeSafer_LowHealthWarning.git',
|
||||||
description: 'Customizable health, mana, and aggro alert system for solo, group, and raid play'
|
branch: 'main',
|
||||||
|
description: 'Low health and mana fullscreen flash warnings with heartbeat sound; re-enables the hidden Blizzard alert effect'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
git: 'https://github.com/Fiurs-Hearth/WIIIUI.git',
|
git: 'https://github.com/Fiurs-Hearth/WIIIUI.git',
|
||||||
|
|||||||
+6
-1
@@ -24,7 +24,12 @@ const skipFiles = new Set([
|
|||||||
'.manifest-overrides.json'
|
'.manifest-overrides.json'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const skipPatterns: RegExp[] = [/\.bak(\.|$)/, /\.crashing(\.|$)/];
|
const skipPatterns: RegExp[] = [
|
||||||
|
/\.bak([.\-]|$)/,
|
||||||
|
/\.crashing(\.|$)/,
|
||||||
|
/\.torrent$/,
|
||||||
|
/^manifest\.json\./
|
||||||
|
];
|
||||||
const isSkipPattern = (file: string) => skipPatterns.some(p => p.test(file));
|
const isSkipPattern = (file: string) => skipPatterns.some(p => p.test(file));
|
||||||
|
|
||||||
const skipDirsPosix = new Set([
|
const skipDirsPosix = new Set([
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ app.get(
|
|||||||
const filePath = req.params[0];
|
const filePath = req.params[0];
|
||||||
console.log(`Fetching file: ${filePath}`);
|
console.log(`Fetching file: ${filePath}`);
|
||||||
|
|
||||||
// keep the resolved path inside SourceDir
|
|
||||||
const root = path.resolve(SourceDir);
|
const root = path.resolve(SourceDir);
|
||||||
const target = path.resolve(SourceDir, filePath);
|
const target = path.resolve(SourceDir, filePath);
|
||||||
if (target !== root && !target.startsWith(root + path.sep)) {
|
if (target !== root && !target.startsWith(root + path.sep)) {
|
||||||
|
|||||||
+1
-2
@@ -93,8 +93,7 @@ export const MODS: ModEntry[] = [
|
|||||||
pinnedTag: '0.2',
|
pinnedTag: '0.2',
|
||||||
format: 'zip',
|
format: 'zip',
|
||||||
extractMap: {
|
extractMap: {
|
||||||
'VanillaMultiMonitorFix.dll': 'VanillaMultiMonitorFix.dll',
|
'VanillaMultiMonitorFix.dll': 'VanillaMultiMonitorFix.dll'
|
||||||
'VMMFix_preferred_monitor.txt': 'VMMFix_preferred_monitor.txt'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
registerInDllsTxt: 'VanillaMultiMonitorFix.dll'
|
registerInDllsTxt: 'VanillaMultiMonitorFix.dll'
|
||||||
|
|||||||
+26
-2
@@ -36,6 +36,18 @@ export const ModStateSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type ModState = z.infer<typeof ModStateSchema>;
|
export type ModState = z.infer<typeof ModStateSchema>;
|
||||||
|
|
||||||
|
export const HardwareInfoSchema = z.object({
|
||||||
|
totalRamMb: z.number(),
|
||||||
|
cpuCores: z.number(),
|
||||||
|
cpuModel: z.string(),
|
||||||
|
gpuModel: z.string(),
|
||||||
|
vramMb: z.number().nullable(),
|
||||||
|
vramSource: z.enum(['registry', 'wmi', 'none']),
|
||||||
|
detectedAt: z.string(),
|
||||||
|
schemaVersion: z.number()
|
||||||
|
});
|
||||||
|
export type HardwareInfo = z.infer<typeof HardwareInfoSchema>;
|
||||||
|
|
||||||
export const PreferencesSchema = z.object({
|
export const PreferencesSchema = z.object({
|
||||||
isPortable: z.boolean().optional(),
|
isPortable: z.boolean().optional(),
|
||||||
server: z.enum(['live', 'ptr']).default('live'),
|
server: z.enum(['live', 'ptr']).default('live'),
|
||||||
@@ -60,7 +72,9 @@ export const PreferencesSchema = z.object({
|
|||||||
})
|
})
|
||||||
.nullish(),
|
.nullish(),
|
||||||
config: ConfigWtfSchema.default({}),
|
config: ConfigWtfSchema.default({}),
|
||||||
mods: z.record(ModStateSchema).default({})
|
mods: z.record(ModStateSchema).default({}),
|
||||||
|
hardware: HardwareInfoSchema.optional(),
|
||||||
|
farClipUserSet: z.boolean().optional()
|
||||||
});
|
});
|
||||||
export type PreferencesSchema = z.infer<typeof PreferencesSchema>;
|
export type PreferencesSchema = z.infer<typeof PreferencesSchema>;
|
||||||
|
|
||||||
@@ -105,7 +119,7 @@ export const NewsItemSchema = z.object({
|
|||||||
date: z.string(),
|
date: z.string(),
|
||||||
body: z.string(),
|
body: z.string(),
|
||||||
url: z.string().url().optional(),
|
url: z.string().url().optional(),
|
||||||
author: z.string().optional()
|
author: z.string().nullish()
|
||||||
});
|
});
|
||||||
export type NewsItem = z.infer<typeof NewsItemSchema>;
|
export type NewsItem = z.infer<typeof NewsItemSchema>;
|
||||||
|
|
||||||
@@ -113,3 +127,13 @@ export const NewsFeedSchema = z.object({
|
|||||||
items: z.array(NewsItemSchema)
|
items: z.array(NewsItemSchema)
|
||||||
});
|
});
|
||||||
export type NewsFeed = z.infer<typeof NewsFeedSchema>;
|
export type NewsFeed = z.infer<typeof NewsFeedSchema>;
|
||||||
|
|
||||||
|
export const ForumAnnouncementSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
author: z.string().nullish(),
|
||||||
|
date: z.string(),
|
||||||
|
url: z.string().url(),
|
||||||
|
html: z.string()
|
||||||
|
});
|
||||||
|
export type ForumAnnouncement = z.infer<typeof ForumAnnouncementSchema>;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
type Path = readonly (string | number)[];
|
type Path = readonly (string | number)[];
|
||||||
|
|
||||||
// reject prototype-polluting keys
|
|
||||||
const isUnsafeKey = (key: string | number) =>
|
const isUnsafeKey = (key: string | number) =>
|
||||||
key === '__proto__' || key === 'constructor' || key === 'prototype';
|
key === '__proto__' || key === 'constructor' || key === 'prototype';
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { patcherRouter } from './routers/patcher';
|
|||||||
import { generalRouter } from './routers/general';
|
import { generalRouter } from './routers/general';
|
||||||
import { preferencesRouter } from './routers/preferences';
|
import { preferencesRouter } from './routers/preferences';
|
||||||
import { newsRouter } from './routers/news';
|
import { newsRouter } from './routers/news';
|
||||||
|
import { forumRouter } from './routers/forum';
|
||||||
import { modsRouter } from './routers/mods';
|
import { modsRouter } from './routers/mods';
|
||||||
import { selfUpdaterRouter } from './routers/selfUpdater';
|
import { selfUpdaterRouter } from './routers/selfUpdater';
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
patcher: patcherRouter,
|
patcher: patcherRouter,
|
||||||
updater: updaterRouter,
|
updater: updaterRouter,
|
||||||
news: newsRouter,
|
news: newsRouter,
|
||||||
|
forum: forumRouter,
|
||||||
mods: modsRouter,
|
mods: modsRouter,
|
||||||
selfUpdater: selfUpdaterRouter
|
selfUpdater: selfUpdaterRouter
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import fetch from 'node-fetch';
|
||||||
|
import Logger from 'electron-log/main';
|
||||||
|
|
||||||
|
import { ForumAnnouncementSchema, type ForumAnnouncement } from '~common/schemas';
|
||||||
|
|
||||||
|
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||||
|
|
||||||
|
const FETCH_TIMEOUT_MS = 8_000;
|
||||||
|
|
||||||
|
const fetchLatestAnnouncement = async (): Promise<ForumAnnouncement | null> => {
|
||||||
|
const url = `${
|
||||||
|
import.meta.env.MAIN_VITE_SERVER_URL || 'https://octowow.st'
|
||||||
|
}/forum/octonews.php?forum=35&mode=full`;
|
||||||
|
const controller = new AbortController();
|
||||||
|
const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { signal: controller.signal });
|
||||||
|
if (!res.ok) throw Error(`HTTP ${res.status}`);
|
||||||
|
const json = (await res.json()) as unknown;
|
||||||
|
if (!json || typeof json !== 'object' || !('id' in json)) return null;
|
||||||
|
const parsed = ForumAnnouncementSchema.safeParse(json);
|
||||||
|
if (!parsed.success) {
|
||||||
|
Logger.error(
|
||||||
|
'Forum announcement failed schema validation',
|
||||||
|
parsed.error.flatten()
|
||||||
|
);
|
||||||
|
throw Error('Malformed forum announcement');
|
||||||
|
}
|
||||||
|
return parsed.data;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(t);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forumRouter = createTRPCRouter({
|
||||||
|
latestAnnouncement: publicProcedure.query(async () => {
|
||||||
|
try {
|
||||||
|
return await fetchLatestAnnouncement();
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error('Failed to fetch forum announcement', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -5,11 +5,21 @@ import { z } from 'zod';
|
|||||||
import { mainWindow } from '~main/index';
|
import { mainWindow } from '~main/index';
|
||||||
import Preferences from '~main/modules/preferences';
|
import Preferences from '~main/modules/preferences';
|
||||||
import { addDefenderExclusions } from '~main/modules/defender';
|
import { addDefenderExclusions } from '~main/modules/defender';
|
||||||
|
import { detectHardware, recommendFarClip } from '~main/modules/hardware';
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||||
|
|
||||||
export const generalRouter = createTRPCRouter({
|
export const generalRouter = createTRPCRouter({
|
||||||
appVersion: publicProcedure.query(() => app.getVersion()),
|
appVersion: publicProcedure.query(() => app.getVersion()),
|
||||||
|
hardware: publicProcedure.query(() => {
|
||||||
|
const hardware = Preferences.data.hardware ?? null;
|
||||||
|
return { hardware, recommendedFarClip: recommendFarClip(hardware) };
|
||||||
|
}),
|
||||||
|
redetectHardware: publicProcedure.mutation(async () => {
|
||||||
|
const hardware = await detectHardware();
|
||||||
|
Preferences.data = { hardware };
|
||||||
|
return { hardware, recommendedFarClip: recommendFarClip(hardware) };
|
||||||
|
}),
|
||||||
quit: publicProcedure.mutation(() => app.quit()),
|
quit: publicProcedure.mutation(() => app.quit()),
|
||||||
minimize: publicProcedure.mutation(() => mainWindow?.minimize()),
|
minimize: publicProcedure.mutation(() => mainWindow?.minimize()),
|
||||||
openLink: publicProcedure
|
openLink: publicProcedure
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import Mods from '~main/modules/mods';
|
import Mods from '~main/modules/mods';
|
||||||
|
import Preferences from '~main/modules/preferences';
|
||||||
|
import { isGameRunning } from '~main/modules/updater';
|
||||||
import { ModIdSchema } from '~common/mods';
|
import { ModIdSchema } from '~common/mods';
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||||
@@ -15,5 +19,14 @@ export const modsRouter = createTRPCRouter({
|
|||||||
.input(z.object({ id: ModIdSchema, ignore: z.boolean() }))
|
.input(z.object({ id: ModIdSchema, ignore: z.boolean() }))
|
||||||
.mutation(({ input }) => Mods.setIgnoreUpdates(input.id, input.ignore)),
|
.mutation(({ input }) => Mods.setIgnoreUpdates(input.id, input.ignore)),
|
||||||
applyAll: publicProcedure.mutation(() => Mods.applyAll()),
|
applyAll: publicProcedure.mutation(() => Mods.applyAll()),
|
||||||
|
repair: publicProcedure.mutation(async () => {
|
||||||
|
const clientDir = Preferences.data?.clientDir;
|
||||||
|
if (clientDir) {
|
||||||
|
const exePath = path.join(clientDir, 'WoW.exe');
|
||||||
|
if (await isGameRunning(exePath))
|
||||||
|
throw new Error('Please close WoW first before verifying files.');
|
||||||
|
}
|
||||||
|
return Mods.applyAll({ repairOnly: true });
|
||||||
|
}),
|
||||||
observe: publicProcedure.subscription(() => Mods.observe())
|
observe: publicProcedure.subscription(() => Mods.observe())
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import { createTRPCRouter, publicProcedure } from '../trpc';
|
|||||||
const FETCH_TIMEOUT_MS = 8_000;
|
const FETCH_TIMEOUT_MS = 8_000;
|
||||||
|
|
||||||
const fetchNews = async (): Promise<NewsItem[]> => {
|
const fetchNews = async (): Promise<NewsItem[]> => {
|
||||||
const url = `${import.meta.env.MAIN_VITE_SERVER_URL || 'https://octowow.st'}/news.json`;
|
const url = `${
|
||||||
|
import.meta.env.MAIN_VITE_SERVER_URL || 'https://octowow.st'
|
||||||
|
}/forum/octonews.php?mode=list&forum=2&limit=3`;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||||
try {
|
try {
|
||||||
|
|||||||
+49
-1
@@ -1,6 +1,6 @@
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import { app, shell, BrowserWindow, screen } from 'electron';
|
import { app, shell, session, BrowserWindow, screen } from 'electron';
|
||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils';
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils';
|
||||||
import { createIPCHandler } from 'electron-trpc/main';
|
import { createIPCHandler } from 'electron-trpc/main';
|
||||||
import Logger from 'electron-log/main';
|
import Logger from 'electron-log/main';
|
||||||
@@ -13,6 +13,11 @@ import Updater from './modules/updater';
|
|||||||
import Addons from './modules/addons';
|
import Addons from './modules/addons';
|
||||||
import Mods from './modules/mods';
|
import Mods from './modules/mods';
|
||||||
import { initSelfUpdater } from './modules/selfUpdater';
|
import { initSelfUpdater } from './modules/selfUpdater';
|
||||||
|
import {
|
||||||
|
detectHardware,
|
||||||
|
recommendFarClip,
|
||||||
|
HARDWARE_SCHEMA_VERSION
|
||||||
|
} from './modules/hardware';
|
||||||
|
|
||||||
Logger.initialize();
|
Logger.initialize();
|
||||||
Logger.errorHandler.startCatching();
|
Logger.errorHandler.startCatching();
|
||||||
@@ -134,8 +139,51 @@ if (!gotSingleInstanceLock) {
|
|||||||
Mods.verify();
|
Mods.verify();
|
||||||
initSelfUpdater();
|
initSelfUpdater();
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
let hardware = Preferences.data.hardware;
|
||||||
|
if (!hardware || hardware.schemaVersion < HARDWARE_SCHEMA_VERSION) {
|
||||||
|
hardware = await detectHardware();
|
||||||
|
Preferences.data = { hardware };
|
||||||
|
}
|
||||||
|
const rec = recommendFarClip(hardware ?? null);
|
||||||
|
if (
|
||||||
|
Preferences.data.farClipUserSet !== true &&
|
||||||
|
Preferences.data.config.farClip !== rec
|
||||||
|
)
|
||||||
|
Preferences.data = {
|
||||||
|
config: { ...Preferences.data.config, farClip: rec }
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error('Hardware detection / farClip recommendation failed', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
electronApp.setAppUserModelId('st.octowow.launcher');
|
electronApp.setAppUserModelId('st.octowow.launcher');
|
||||||
|
|
||||||
|
if (app.isPackaged) {
|
||||||
|
const serverOrigin = new URL(
|
||||||
|
import.meta.env.MAIN_VITE_SERVER_URL || 'https://octowow.st'
|
||||||
|
).origin;
|
||||||
|
session.defaultSession.webRequest.onHeadersReceived((details, cb) => {
|
||||||
|
cb({
|
||||||
|
responseHeaders: {
|
||||||
|
...details.responseHeaders,
|
||||||
|
'Content-Security-Policy': [
|
||||||
|
[
|
||||||
|
"default-src 'self'",
|
||||||
|
"script-src 'self'",
|
||||||
|
"style-src 'self' 'unsafe-inline'",
|
||||||
|
`img-src 'self' data: https://octowow.st https://forum.octowow.st ${serverOrigin}`,
|
||||||
|
"font-src 'self' data:",
|
||||||
|
"connect-src 'self'"
|
||||||
|
].join('; ')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
app.on('browser-window-created', (_, window) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
optimizer.watchWindowShortcuts(window);
|
optimizer.watchWindowShortcuts(window);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ const readTocData = (content: string) =>
|
|||||||
const isUnsafeFolder = (name?: string) =>
|
const isUnsafeFolder = (name?: string) =>
|
||||||
!name || name === '.' || name === '..' || /[/\\]/.test(name);
|
!name || name === '.' || name === '..' || /[/\\]/.test(name);
|
||||||
|
|
||||||
// only allow known git hosts over https
|
|
||||||
const ALLOWED_GIT_HOSTS = [
|
const ALLOWED_GIT_HOSTS = [
|
||||||
'github.com',
|
'github.com',
|
||||||
'gitlab.com',
|
'gitlab.com',
|
||||||
@@ -150,7 +149,6 @@ class AddonsClass extends Observable<AddonsStatus> {
|
|||||||
)?.[1];
|
)?.[1];
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const folder = gitUrl.slice(0, -4).split('/').at(-1);
|
const folder = gitUrl.slice(0, -4).split('/').at(-1);
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import os from 'node:os';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
|
||||||
|
import { app } from 'electron';
|
||||||
|
import Logger from 'electron-log/main';
|
||||||
|
|
||||||
|
import type { HardwareInfo } from '~common/schemas';
|
||||||
|
|
||||||
|
export const HARDWARE_SCHEMA_VERSION = 1;
|
||||||
|
|
||||||
|
export const FARCLIP_FLOOR = 777;
|
||||||
|
export const FARCLIP_CEILING = 3000;
|
||||||
|
|
||||||
|
const VIDEO_CLASS_GUID = '{4d36e968-e325-11ce-bfc1-08002be10318}';
|
||||||
|
|
||||||
|
const getVramMb = (): Promise<{
|
||||||
|
mb: number | null;
|
||||||
|
source: HardwareInfo['vramSource'];
|
||||||
|
}> => {
|
||||||
|
if (os.platform() !== 'win32')
|
||||||
|
return Promise.resolve({ mb: null, source: 'none' });
|
||||||
|
|
||||||
|
const script = [
|
||||||
|
'$ErrorActionPreference = "Stop"',
|
||||||
|
`$base = 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\${VIDEO_CLASS_GUID}'`,
|
||||||
|
'$max = [int64]0',
|
||||||
|
'Get-ChildItem $base -ErrorAction SilentlyContinue | ForEach-Object {',
|
||||||
|
' $p = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue',
|
||||||
|
' $v = [int64]0',
|
||||||
|
" if ($p.'HardwareInformation.qwMemorySize') { $v = [int64]$p.'HardwareInformation.qwMemorySize' }",
|
||||||
|
" elseif ($p.'HardwareInformation.MemorySize') {",
|
||||||
|
" $m = $p.'HardwareInformation.MemorySize'",
|
||||||
|
' if ($m -is [byte[]]) { $v = [int64][System.BitConverter]::ToUInt32($m, 0) } else { $v = [int64]$m }',
|
||||||
|
' }',
|
||||||
|
' if ($v -gt $max) { $max = $v }',
|
||||||
|
'}',
|
||||||
|
'Write-Output $max'
|
||||||
|
].join('\n');
|
||||||
|
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let settled = false;
|
||||||
|
const finish = (mb: number | null, source: HardwareInfo['vramSource']) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ mb, source });
|
||||||
|
};
|
||||||
|
|
||||||
|
const child = spawn(
|
||||||
|
'powershell.exe',
|
||||||
|
['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded],
|
||||||
|
{ windowsHide: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
child.kill();
|
||||||
|
Logger.warn('VRAM detection timed out');
|
||||||
|
finish(null, 'none');
|
||||||
|
}, 8000);
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
child.stdout.on('data', d => (stdout += String(d)));
|
||||||
|
child.on('error', e => {
|
||||||
|
Logger.warn('VRAM detection failed to launch PowerShell', e);
|
||||||
|
finish(null, 'none');
|
||||||
|
});
|
||||||
|
child.on('exit', code => {
|
||||||
|
const bytes = Number(stdout.trim());
|
||||||
|
if (code === 0 && Number.isFinite(bytes) && bytes > 0)
|
||||||
|
finish(Math.round(bytes / 1024 / 1024), 'registry');
|
||||||
|
else finish(null, 'none');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGpuModel = async (): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const info = (await app.getGPUInfo('complete')) as {
|
||||||
|
auxAttributes?: { glRenderer?: string };
|
||||||
|
gpuDevice?: { active?: boolean; vendorId?: number; deviceId?: number }[];
|
||||||
|
};
|
||||||
|
const renderer = info?.auxAttributes?.glRenderer?.trim();
|
||||||
|
if (renderer) return renderer;
|
||||||
|
const active = info?.gpuDevice?.find(d => d.active) ?? info?.gpuDevice?.[0];
|
||||||
|
if (active) return `vendor ${active.vendorId} device ${active.deviceId}`;
|
||||||
|
} catch (e) {
|
||||||
|
Logger.warn('GPU info detection failed', e);
|
||||||
|
}
|
||||||
|
return 'unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const detectHardware = async (): Promise<HardwareInfo> => {
|
||||||
|
const cpus = os.cpus();
|
||||||
|
const [vram, gpuModel] = await Promise.all([getVramMb(), getGpuModel()]);
|
||||||
|
|
||||||
|
const info: HardwareInfo = {
|
||||||
|
totalRamMb: Math.round(os.totalmem() / 1024 / 1024),
|
||||||
|
cpuCores: cpus.length,
|
||||||
|
cpuModel: cpus[0]?.model?.trim() || 'unknown',
|
||||||
|
gpuModel,
|
||||||
|
vramMb: vram.mb,
|
||||||
|
vramSource: vram.source,
|
||||||
|
detectedAt: new Date().toISOString(),
|
||||||
|
schemaVersion: HARDWARE_SCHEMA_VERSION
|
||||||
|
};
|
||||||
|
Logger.info('Detected hardware', info);
|
||||||
|
return info;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clampFarClip = (n: number) =>
|
||||||
|
Math.min(FARCLIP_CEILING, Math.max(FARCLIP_FLOOR, Math.round(n)));
|
||||||
|
|
||||||
|
export const recommendFarClip = (hw: HardwareInfo | null): number => {
|
||||||
|
if (!hw) return clampFarClip(1000);
|
||||||
|
|
||||||
|
const ramGb = hw.totalRamMb / 1024;
|
||||||
|
const cores = hw.cpuCores;
|
||||||
|
const vramTrusted = hw.vramSource !== 'none' && hw.vramMb != null;
|
||||||
|
const vramGb = vramTrusted ? (hw.vramMb as number) / 1024 : null;
|
||||||
|
|
||||||
|
if (ramGb < 6 || cores <= 2) return clampFarClip(FARCLIP_FLOOR);
|
||||||
|
|
||||||
|
if (vramGb === null)
|
||||||
|
return clampFarClip(ramGb >= 8 && cores >= 4 ? 1500 : 1000);
|
||||||
|
|
||||||
|
if (ramGb < 8 || vramGb < 2) return clampFarClip(1000);
|
||||||
|
if (ramGb >= 32 && vramGb >= 8 && cores >= 8) return clampFarClip(3000);
|
||||||
|
if (ramGb >= 16 && vramGb >= 4 && cores >= 6) return clampFarClip(2200);
|
||||||
|
if (ramGb >= 8 && vramGb >= 2 && cores >= 4) return clampFarClip(1500);
|
||||||
|
return clampFarClip(1000);
|
||||||
|
};
|
||||||
@@ -97,7 +97,6 @@ export const applyLocalePatch = async (
|
|||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class ModsClass extends Observable<ModsStatus> {
|
|||||||
const vmmfDll = path.join(clientDir, 'VanillaMultiMonitorFix.dll');
|
const vmmfDll = path.join(clientDir, 'VanillaMultiMonitorFix.dll');
|
||||||
const vmmfCfg = path.join(clientDir, 'VMMFix_preferred_monitor.txt');
|
const vmmfCfg = path.join(clientDir, 'VMMFix_preferred_monitor.txt');
|
||||||
if ((await fs.pathExists(vmmfDll)) && !(await fs.pathExists(vmmfCfg)))
|
if ((await fs.pathExists(vmmfDll)) && !(await fs.pathExists(vmmfCfg)))
|
||||||
await fs.writeFile(vmmfCfg, '1\n', 'utf8').catch(() => {});
|
await fs.writeFile(vmmfCfg, '0\n', 'utf8').catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const m of MODS) {
|
for (const m of MODS) {
|
||||||
@@ -182,7 +182,7 @@ class ModsClass extends Observable<ModsStatus> {
|
|||||||
this.#patchRow(id, { ignoreUpdates: ignore });
|
this.#patchRow(id, { ignoreUpdates: ignore });
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyAll() {
|
async applyAll(opts: { repairOnly?: boolean } = {}) {
|
||||||
const clientDir = Preferences.data?.clientDir;
|
const clientDir = Preferences.data?.clientDir;
|
||||||
if (!clientDir) {
|
if (!clientDir) {
|
||||||
Logger.warn('No clientDir set; cannot apply mods.');
|
Logger.warn('No clientDir set; cannot apply mods.');
|
||||||
@@ -192,6 +192,7 @@ class ModsClass extends Observable<ModsStatus> {
|
|||||||
Logger.warn('applyAll already running; ignoring re-entrant call.');
|
Logger.warn('applyAll already running; ignoring re-entrant call.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await this.verify();
|
||||||
this._value = { ...this._value, state: 'busy' };
|
this._value = { ...this._value, state: 'busy' };
|
||||||
this._notifyObservers();
|
this._notifyObservers();
|
||||||
|
|
||||||
@@ -219,7 +220,7 @@ class ModsClass extends Observable<ModsStatus> {
|
|||||||
await this.#install(m);
|
await this.#install(m);
|
||||||
} else if (!wantInstalled && isInstalled) {
|
} else if (!wantInstalled && isInstalled) {
|
||||||
await this.#uninstall(m);
|
await this.#uninstall(m);
|
||||||
} else if (wantInstalled && updateAvailable) {
|
} else if (wantInstalled && updateAvailable && !opts.repairOnly) {
|
||||||
await this.#uninstall(m);
|
await this.#uninstall(m);
|
||||||
await this.#install(m);
|
await this.#install(m);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ export const patchExecutable = async () => {
|
|||||||
[0x006e62a8, [0x006e62a9]]
|
[0x006e62a8, [0x006e62a9]]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// version-pinned in-place patch of the WoW.exe routine at this offset
|
|
||||||
{
|
{
|
||||||
synthetic: true,
|
synthetic: true,
|
||||||
key: 'skillUiGateHijack',
|
key: 'skillUiGateHijack',
|
||||||
@@ -127,6 +126,22 @@ export const patchExecutable = async () => {
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
synthetic: true,
|
||||||
|
key: 'octowowUrlAllowlist',
|
||||||
|
type: 'bytes',
|
||||||
|
default: true,
|
||||||
|
forced: true,
|
||||||
|
tweaks: [
|
||||||
|
[
|
||||||
|
0x45ccd8,
|
||||||
|
[
|
||||||
|
0x6f, 0x63, 0x74, 0x6f, 0x77, 0x6f, 0x77, 0x2e, 0x73, 0x74,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
] satisfies Tweak[];
|
] satisfies Tweak[];
|
||||||
|
|
||||||
@@ -180,7 +195,9 @@ export const patchConfig = async (forceTweaks = false) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const primaryDisplay = screen.getPrimaryDisplay();
|
const primaryDisplay = screen.getPrimaryDisplay();
|
||||||
const { width, height } = primaryDisplay.bounds;
|
const scale = primaryDisplay.scaleFactor || 1;
|
||||||
|
const width = Math.round(primaryDisplay.bounds.width * scale);
|
||||||
|
const height = Math.round(primaryDisplay.bounds.height * scale);
|
||||||
|
|
||||||
const parsed = {
|
const parsed = {
|
||||||
scriptMemory: 512000,
|
scriptMemory: 512000,
|
||||||
|
|||||||
@@ -423,9 +423,15 @@ class UpdaterClass extends Observable<UpdaterStatus> {
|
|||||||
#clientTotalBytes = 0;
|
#clientTotalBytes = 0;
|
||||||
#bytesAlreadyOnDisk = 0;
|
#bytesAlreadyOnDisk = 0;
|
||||||
#cachePath = path.join(Preferences.userDataDir, 'cache.json');
|
#cachePath = path.join(Preferences.userDataDir, 'cache.json');
|
||||||
#cache: CacheTree = fs.existsSync(this.#cachePath)
|
#cache: CacheTree = this.#readCache();
|
||||||
? fs.readJSONSync(this.#cachePath)
|
|
||||||
: {};
|
#readCache(): CacheTree {
|
||||||
|
try {
|
||||||
|
return fs.existsSync(this.#cachePath) ? fs.readJSONSync(this.#cachePath) : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async #saveCache() {
|
async #saveCache() {
|
||||||
await fs.writeJSON(this.#cachePath, this.#cache);
|
await fs.writeJSON(this.#cachePath, this.#cache);
|
||||||
@@ -618,7 +624,11 @@ class UpdaterClass extends Observable<UpdaterStatus> {
|
|||||||
const item = getManifestItem(hashTree, filePath);
|
const item = getManifestItem(hashTree, filePath);
|
||||||
if (!item) return undefined;
|
if (!item) return undefined;
|
||||||
|
|
||||||
if (item.type === 'del') return item;
|
if (item.type === 'del') {
|
||||||
|
if (await fs.exists(path.join(clientPath, ...filePath)))
|
||||||
|
return item;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (item.type === 'dir') {
|
if (item.type === 'dir') {
|
||||||
const files = (
|
const files = (
|
||||||
@@ -832,7 +842,6 @@ class UpdaterClass extends Observable<UpdaterStatus> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (clean) {
|
if (clean) {
|
||||||
// never wipe a drive root or a non-client dir
|
|
||||||
const resolvedClientPath = path.resolve(clientPath);
|
const resolvedClientPath = path.resolve(clientPath);
|
||||||
const isFilesystemRoot =
|
const isFilesystemRoot =
|
||||||
path.parse(resolvedClientPath).root === resolvedClientPath;
|
path.parse(resolvedClientPath).root === resolvedClientPath;
|
||||||
@@ -854,9 +863,10 @@ class UpdaterClass extends Observable<UpdaterStatus> {
|
|||||||
message: 'Cleaning up old files...'
|
message: 'Cleaning up old files...'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const preserve = new Set(['octolauncher.exe', 'wtf', 'interface', 'screenshots']);
|
||||||
const files = await fs.readdir(clientPath);
|
const files = await fs.readdir(clientPath);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file === 'OctoLauncher.exe') continue;
|
if (preserve.has(file.toLowerCase())) continue;
|
||||||
await fs.rm(path.join(clientPath, file), {
|
await fs.rm(path.join(clientPath, file), {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
force: true
|
force: true
|
||||||
|
|||||||
Vendored
+5
-1
@@ -5,4 +5,8 @@ export {
|
|||||||
type ModsStatus,
|
type ModsStatus,
|
||||||
type ModRowStatus
|
type ModRowStatus
|
||||||
} from './modules/mods';
|
} from './modules/mods';
|
||||||
export { type NewsItem, type NewsFeed } from '../common/schemas';
|
export {
|
||||||
|
type NewsItem,
|
||||||
|
type NewsFeed,
|
||||||
|
type ForumAnnouncement
|
||||||
|
} from '../common/schemas';
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export const getClientVersion = async () => {
|
|||||||
const file = await fs.readFile(exePath);
|
const file = await fs.readFile(exePath);
|
||||||
const buffer = Buffer.from(file);
|
const buffer = Buffer.from(file);
|
||||||
|
|
||||||
// Fixed addresses in the 1.12.1 client binary.
|
|
||||||
const VERSION_OFFSET = 0x00437c04;
|
const VERSION_OFFSET = 0x00437c04;
|
||||||
const VERSION_LEN = 6;
|
const VERSION_LEN = 6;
|
||||||
const BUILD_OFFSET = 0x00437bfc;
|
const BUILD_OFFSET = 0x00437bfc;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,218 @@
|
|||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
import { AlertTriangle, ExternalLink, RefreshCw } from 'lucide-react';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { api } from '~renderer/utils/api';
|
||||||
|
import { useT } from '~renderer/i18n';
|
||||||
|
import useScrollHint from '~renderer/utils/useScrollHint';
|
||||||
|
import Parchment from '~renderer/assets/parchment.jpg';
|
||||||
|
|
||||||
|
import IconSpinner from './styled/IconSpinner';
|
||||||
|
import TextButton from './styled/TextButton';
|
||||||
|
|
||||||
|
const SANITIZE_CONFIG = {
|
||||||
|
ALLOWED_TAGS: [
|
||||||
|
'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'a', 'ul', 'ol', 'li',
|
||||||
|
'blockquote', 'cite', 'span', 'div', 'img', 'br', 'p', 'code', 'pre',
|
||||||
|
'dl', 'dt', 'dd', 'hr', 'sub', 'sup', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||||
|
'table', 'thead', 'tbody', 'tr', 'td', 'th'
|
||||||
|
],
|
||||||
|
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'style'],
|
||||||
|
ALLOWED_URI_REGEXP: /^(?:https?:|mailto:)/i,
|
||||||
|
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'style', 'link', 'meta']
|
||||||
|
};
|
||||||
|
|
||||||
|
const LIGHT_NAMED = new Set([
|
||||||
|
'white', 'snow', 'ivory', 'floralwhite', 'ghostwhite', 'seashell', 'beige',
|
||||||
|
'linen', 'cornsilk', 'lightyellow', 'lightgoldenrodyellow', 'lemonchiffon',
|
||||||
|
'yellow', 'aqua', 'cyan', 'lime', 'aquamarine', 'azure', 'mintcream',
|
||||||
|
'honeydew', 'lavender', 'lavenderblush', 'aliceblue', 'whitesmoke',
|
||||||
|
'gainsboro', 'silver', 'lightgray', 'lightgrey', 'antiquewhite', 'papayawhip',
|
||||||
|
'blanchedalmond', 'bisque', 'moccasin', 'navajowhite', 'peachpuff', 'khaki',
|
||||||
|
'wheat', 'greenyellow', 'chartreuse', 'springgreen', 'palegoldenrod', 'gold'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const LIGHT_THRESHOLD = 165;
|
||||||
|
|
||||||
|
const SAFE_STYLE_PROPS = new Set([
|
||||||
|
'color',
|
||||||
|
'background-color',
|
||||||
|
'font-weight',
|
||||||
|
'font-style',
|
||||||
|
'text-decoration',
|
||||||
|
'text-align',
|
||||||
|
'font-size'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const colorIsTooLight = (raw: string): boolean => {
|
||||||
|
const v = raw.trim().toLowerCase();
|
||||||
|
const lum = (r: number, g: number, b: number) => 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||||
|
|
||||||
|
const hex = v.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);
|
||||||
|
if (hex) {
|
||||||
|
const h =
|
||||||
|
hex[1].length === 3
|
||||||
|
? hex[1]
|
||||||
|
.split('')
|
||||||
|
.map(c => c + c)
|
||||||
|
.join('')
|
||||||
|
: hex[1];
|
||||||
|
return (
|
||||||
|
lum(
|
||||||
|
parseInt(h.slice(0, 2), 16),
|
||||||
|
parseInt(h.slice(2, 4), 16),
|
||||||
|
parseInt(h.slice(4, 6), 16)
|
||||||
|
) > LIGHT_THRESHOLD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const rgb = v.match(/^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
|
||||||
|
if (rgb) return lum(+rgb[1], +rgb[2], +rgb[3]) > LIGHT_THRESHOLD;
|
||||||
|
|
||||||
|
return LIGHT_NAMED.has(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clampFontSize = (val: string): string => {
|
||||||
|
const m = val.match(/^(\d+(?:\.\d+)?)(px|pt|%|em|rem)$/i);
|
||||||
|
if (!m) return val;
|
||||||
|
const n = parseFloat(m[1]);
|
||||||
|
const unit = m[2].toLowerCase();
|
||||||
|
const max = unit === 'px' ? 28 : unit === 'pt' ? 21 : unit === '%' ? 200 : 2;
|
||||||
|
return `${Math.min(n, max)}${unit}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
DOMPurify.addHook('afterSanitizeAttributes', node => {
|
||||||
|
if (node.tagName === 'IMG') node.setAttribute('loading', 'lazy');
|
||||||
|
|
||||||
|
const style = node.getAttribute('style');
|
||||||
|
if (!style) return;
|
||||||
|
|
||||||
|
const kept: string[] = [];
|
||||||
|
for (const decl of style.split(';')) {
|
||||||
|
const idx = decl.indexOf(':');
|
||||||
|
if (idx < 0) continue;
|
||||||
|
const prop = decl.slice(0, idx).trim().toLowerCase();
|
||||||
|
let val = decl.slice(idx + 1).trim();
|
||||||
|
if (!val || !SAFE_STYLE_PROPS.has(prop)) continue;
|
||||||
|
|
||||||
|
if (prop === 'color' && colorIsTooLight(val)) continue;
|
||||||
|
if (prop === 'font-size') val = clampFontSize(val);
|
||||||
|
kept.push(`${prop}:${val}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kept.length) node.setAttribute('style', kept.join(';'));
|
||||||
|
else node.removeAttribute('style');
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatDate = (raw: string) => {
|
||||||
|
const d = new Date(raw);
|
||||||
|
if (Number.isNaN(d.getTime())) return raw;
|
||||||
|
return d.toLocaleDateString(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const ForumAnnouncementPanel = () => {
|
||||||
|
const t = useT();
|
||||||
|
const openLink = api.general.openLink.useMutation();
|
||||||
|
const query = api.forum.latestAnnouncement.useQuery(undefined, {
|
||||||
|
staleTime: 10 * 60 * 1000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: 1
|
||||||
|
});
|
||||||
|
const scrollRef = useScrollHint<HTMLDivElement>();
|
||||||
|
|
||||||
|
const data = query.data;
|
||||||
|
const safeHtml = useMemo(
|
||||||
|
() => (data ? DOMPurify.sanitize(data.html, SANITIZE_CONFIG) : ''),
|
||||||
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onBodyClick = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const anchor = (e.target as HTMLElement).closest('a[href]');
|
||||||
|
if (!anchor) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const href = anchor.getAttribute('href');
|
||||||
|
if (href) openLink.mutateAsync(href);
|
||||||
|
},
|
||||||
|
[openLink]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
className="parchment-post flex min-h-0 flex-grow flex-col gap-3"
|
||||||
|
style={{ backgroundImage: `url(${Parchment})` }}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<h4 className="parchment-post-title min-w-0 flex-1 break-words">
|
||||||
|
{data?.title ?? t('forum.title')}
|
||||||
|
</h4>
|
||||||
|
<TextButton
|
||||||
|
icon={RefreshCw}
|
||||||
|
size={18}
|
||||||
|
className="-mr-2 -mt-1 shrink-0"
|
||||||
|
loading={query.isFetching}
|
||||||
|
onClick={() => query.refetch()}
|
||||||
|
title={t('misc.refresh')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data?.author && (
|
||||||
|
<span className="parchment-post-meta">
|
||||||
|
{t('misc.newsByAuthor', { author: data.author })} · {formatDate(data.date)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="relative -mx-4 flex min-h-0 flex-grow flex-col overflow-y-auto overflow-x-hidden px-4"
|
||||||
|
>
|
||||||
|
{query.isLoading ? (
|
||||||
|
<div className="flex flex-grow flex-col items-center justify-center gap-2">
|
||||||
|
<IconSpinner className="parchment-post-muted" />
|
||||||
|
<p className="parchment-post-muted italic">{t('forum.loading')}</p>
|
||||||
|
</div>
|
||||||
|
) : query.isError ? (
|
||||||
|
<div className="flex flex-grow flex-col items-center justify-center gap-3">
|
||||||
|
<AlertTriangle size={32} className="text-red" />
|
||||||
|
<p className="parchment-post-muted italic">{t('forum.error')}</p>
|
||||||
|
<TextButton
|
||||||
|
icon={RefreshCw}
|
||||||
|
size={18}
|
||||||
|
onClick={() => query.refetch()}
|
||||||
|
>
|
||||||
|
{t('misc.tryAgain')}
|
||||||
|
</TextButton>
|
||||||
|
</div>
|
||||||
|
) : !data ? (
|
||||||
|
<div className="flex flex-grow flex-col items-center justify-center">
|
||||||
|
<p className="parchment-post-muted italic">{t('forum.empty')}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="parchment-post-body"
|
||||||
|
onClick={onBodyClick}
|
||||||
|
dangerouslySetInnerHTML={{ __html: safeHtml }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data && (
|
||||||
|
<TextButton
|
||||||
|
icon={ExternalLink}
|
||||||
|
size={14}
|
||||||
|
className="-ml-2 self-start"
|
||||||
|
onClick={() => openLink.mutateAsync(data.url)}
|
||||||
|
>
|
||||||
|
{t('forum.readFullPost')}
|
||||||
|
</TextButton>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ForumAnnouncementPanel;
|
||||||
@@ -13,8 +13,7 @@ const LOCALES: { value: Lang; code: string; label: string }[] = [
|
|||||||
{ value: 'zhCN', code: 'Zh', label: '中文' },
|
{ value: 'zhCN', code: 'Zh', label: '中文' },
|
||||||
{ value: 'esES', code: 'Es', label: 'Español' },
|
{ value: 'esES', code: 'Es', label: 'Español' },
|
||||||
{ value: 'ptBR', code: 'Pt', label: 'Português' },
|
{ value: 'ptBR', code: 'Pt', label: 'Português' },
|
||||||
{ value: 'ruRU', code: 'Ru', label: 'Русский' },
|
{ value: 'ruRU', code: 'Ru', label: 'Русский' }
|
||||||
{ value: 'trTR', code: 'Tr', label: 'Türkçe' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const LanguageDropdown = () => {
|
const LanguageDropdown = () => {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const PreferencesDialog = ({ close }: Props) => {
|
|||||||
const setPref = api.preferences.set.useMutation();
|
const setPref = api.preferences.set.useMutation();
|
||||||
|
|
||||||
const verify = api.updater.verify.useMutation();
|
const verify = api.updater.verify.useMutation();
|
||||||
|
const repair = api.mods.repair.useMutation();
|
||||||
const openInstallFolder = api.general.openInstallFolder.useMutation();
|
const openInstallFolder = api.general.openInstallFolder.useMutation();
|
||||||
const openLogFile = api.general.openLogFile.useMutation();
|
const openLogFile = api.general.openLogFile.useMutation();
|
||||||
const addExclusion = api.general.addDefenderExclusion.useMutation();
|
const addExclusion = api.general.addDefenderExclusion.useMutation();
|
||||||
@@ -147,7 +148,7 @@ const PreferencesDialog = ({ close }: Props) => {
|
|||||||
<h4 className="tw-color">{t('prefs.troubleshooting')}</h4>
|
<h4 className="tw-color">{t('prefs.troubleshooting')}</h4>
|
||||||
<TextButton
|
<TextButton
|
||||||
icon={ShieldCheck}
|
icon={ShieldCheck}
|
||||||
onClick={() => verify.mutateAsync().then(close)}
|
onClick={() => repair.mutateAsync().then(close)}
|
||||||
className="!items-start text-left text-warmGreen"
|
className="!items-start text-left text-warmGreen"
|
||||||
>
|
>
|
||||||
{t('prefs.verifyGameFiles')}
|
{t('prefs.verifyGameFiles')}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ const DialogButton = ({
|
|||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Click away
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!clickAway) return;
|
if (!clickAway) return;
|
||||||
const callback = (e: MouseEvent) => e.target === ref.current && close();
|
const callback = (e: MouseEvent) => e.target === ref.current && close();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { api } from '~renderer/utils/api';
|
|||||||
import { useT } from '~renderer/i18n';
|
import { useT } from '~renderer/i18n';
|
||||||
import useScrollHint from '~renderer/utils/useScrollHint';
|
import useScrollHint from '~renderer/utils/useScrollHint';
|
||||||
|
|
||||||
|
import ForumAnnouncementPanel from '../ForumAnnouncementPanel';
|
||||||
import IconSpinner from '../styled/IconSpinner';
|
import IconSpinner from '../styled/IconSpinner';
|
||||||
import TextButton from '../styled/TextButton';
|
import TextButton from '../styled/TextButton';
|
||||||
|
|
||||||
@@ -49,7 +50,8 @@ const NewsEntry = ({ item }: { item: NewsItem }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NewsTab = () => {
|
// The "Announcements" list — most-recent forum topics as short previews + links.
|
||||||
|
const AnnouncementsBox = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const query = api.news.list.useQuery(undefined, {
|
const query = api.news.list.useQuery(undefined, {
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
@@ -59,9 +61,9 @@ const NewsTab = () => {
|
|||||||
const scrollRef = useScrollHint<HTMLDivElement>();
|
const scrollRef = useScrollHint<HTMLDivElement>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tw-surface flex min-h-0 flex-grow flex-col gap-3">
|
<div className="tw-surface flex min-h-0 w-[360px] shrink-0 flex-col gap-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="tw-color">{t('misc.newsTitle')}</h4>
|
<h4 className="tw-color">{t('misc.announcementsTitle')}</h4>
|
||||||
<TextButton
|
<TextButton
|
||||||
icon={RefreshCw}
|
icon={RefreshCw}
|
||||||
size={18}
|
size={18}
|
||||||
@@ -106,4 +108,14 @@ const NewsTab = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The News tab holds both boxes side by side: the parchment "newsletter" (the
|
||||||
|
// featured Nautilus News Network post, biggest) and the "Announcements" list.
|
||||||
|
// Living inside the tab means they only show on News — not on Tweaks/Addons/Mods.
|
||||||
|
const NewsTab = () => (
|
||||||
|
<div className="flex min-h-0 flex-grow gap-3">
|
||||||
|
<ForumAnnouncementPanel />
|
||||||
|
<AnnouncementsBox />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default NewsTab;
|
export default NewsTab;
|
||||||
|
|||||||
@@ -78,6 +78,15 @@ const TweaksTab = () => {
|
|||||||
});
|
});
|
||||||
const { handleSubmit, reset, formState } = form;
|
const { handleSubmit, reset, formState } = form;
|
||||||
|
|
||||||
|
const { data: hw } = api.general.hardware.useQuery();
|
||||||
|
const recommendedFarClip = hw?.recommendedFarClip;
|
||||||
|
const farClipValue = form.watch('farClip');
|
||||||
|
const farClipText =
|
||||||
|
t('tweaks.farClip.text') +
|
||||||
|
(recommendedFarClip != null
|
||||||
|
? ' ' + t('tweaks.farClip.recommendedHint', { value: recommendedFarClip })
|
||||||
|
: '');
|
||||||
|
|
||||||
const isApplying =
|
const isApplying =
|
||||||
setPref.isLoading || applyPatch.isLoading || verify.isLoading;
|
setPref.isLoading || applyPatch.isLoading || verify.isLoading;
|
||||||
|
|
||||||
@@ -90,7 +99,7 @@ const TweaksTab = () => {
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(async config => {
|
onSubmit={handleSubmit(async config => {
|
||||||
await setPref.mutateAsync({ config });
|
await setPref.mutateAsync({ config, farClipUserSet: true });
|
||||||
await applyPatch.mutateAsync();
|
await applyPatch.mutateAsync();
|
||||||
await verify.mutateAsync();
|
await verify.mutateAsync();
|
||||||
|
|
||||||
@@ -143,7 +152,11 @@ const TweaksTab = () => {
|
|||||||
id="farClip"
|
id="farClip"
|
||||||
label={t('tweaks.farClip.label')}
|
label={t('tweaks.farClip.label')}
|
||||||
type="number"
|
type="number"
|
||||||
text={t('tweaks.farClip.text')}
|
text={farClipText}
|
||||||
|
recommended={
|
||||||
|
recommendedFarClip != null &&
|
||||||
|
Number(farClipValue) === recommendedFarClip
|
||||||
|
}
|
||||||
min={100}
|
min={100}
|
||||||
max={10000}
|
max={10000}
|
||||||
sensitivity={3}
|
sensitivity={3}
|
||||||
@@ -199,8 +212,11 @@ const TweaksTab = () => {
|
|||||||
</p>
|
</p>
|
||||||
<TextButton
|
<TextButton
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const config = ConfigWtfSchema.parse({});
|
const config =
|
||||||
await setPref.mutateAsync({ config });
|
recommendedFarClip != null
|
||||||
|
? { ...ConfigWtfSchema.parse({}), farClip: recommendedFarClip }
|
||||||
|
: ConfigWtfSchema.parse({});
|
||||||
|
await setPref.mutateAsync({ config, farClipUserSet: false });
|
||||||
reset(config);
|
reset(config);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type Lang = 'enUS' | 'deDE' | 'zhCN' | 'esES' | 'ptBR' | 'ruRU' | 'trTR';
|
export type Lang = 'enUS' | 'deDE' | 'zhCN' | 'esES' | 'ptBR' | 'ruRU';
|
||||||
|
|
||||||
type Dict = Record<string, string>;
|
type Dict = Record<string, string>;
|
||||||
|
|
||||||
@@ -48,7 +48,8 @@ const enUS: Dict = {
|
|||||||
'Recommended for widescreen window resolutions. [Vanilla: 90] [Tweaks: 110]',
|
'Recommended for widescreen window resolutions. [Vanilla: 90] [Tweaks: 110]',
|
||||||
'tweaks.farClip.label': 'Render distance',
|
'tweaks.farClip.label': 'Render distance',
|
||||||
'tweaks.farClip.text':
|
'tweaks.farClip.text':
|
||||||
'Increases maximum render distance. [Vanilla: 777] [Tweaks: 10000]',
|
'Increases maximum render distance. Very high values can crash the game on world entry. [Vanilla: 777]',
|
||||||
|
'tweaks.farClip.recommendedHint': 'Recommended for your PC: {value}.',
|
||||||
'tweaks.frillDistance.label': 'Ground clutter distance',
|
'tweaks.frillDistance.label': 'Ground clutter distance',
|
||||||
'tweaks.frillDistance.text':
|
'tweaks.frillDistance.text':
|
||||||
'Changes ground clutter render distance. [Vanilla: 70] [Tweaks: 300]',
|
'Changes ground clutter render distance. [Vanilla: 70] [Tweaks: 300]',
|
||||||
@@ -65,6 +66,7 @@ const enUS: Dict = {
|
|||||||
'tweaks.reset': 'Reset',
|
'tweaks.reset': 'Reset',
|
||||||
'tweaks.apply': 'Apply',
|
'tweaks.apply': 'Apply',
|
||||||
'misc.newsTitle': 'News',
|
'misc.newsTitle': 'News',
|
||||||
|
'misc.announcementsTitle': 'Announcements',
|
||||||
'misc.newsByAuthor': 'by {author}',
|
'misc.newsByAuthor': 'by {author}',
|
||||||
'misc.newsReadMore': 'Read more',
|
'misc.newsReadMore': 'Read more',
|
||||||
'misc.refresh': 'Refresh',
|
'misc.refresh': 'Refresh',
|
||||||
@@ -72,6 +74,11 @@ const enUS: Dict = {
|
|||||||
'misc.newsError': "Couldn't reach the news feed.",
|
'misc.newsError': "Couldn't reach the news feed.",
|
||||||
'misc.newsEmpty': 'No news yet. Check back later.',
|
'misc.newsEmpty': 'No news yet. Check back later.',
|
||||||
'misc.tryAgain': 'Try again',
|
'misc.tryAgain': 'Try again',
|
||||||
|
'forum.title': 'Nautilus News Network',
|
||||||
|
'forum.readFullPost': 'Read full post on the forum',
|
||||||
|
'forum.loading': 'Loading latest dispatch...',
|
||||||
|
'forum.error': "Couldn't reach the forum.",
|
||||||
|
'forum.empty': 'No dispatches yet.',
|
||||||
'misc.comingSoon': 'Coming soon...',
|
'misc.comingSoon': 'Coming soon...',
|
||||||
'misc.selfUpdateCheckFailed': 'Update check failed: {message}',
|
'misc.selfUpdateCheckFailed': 'Update check failed: {message}',
|
||||||
'misc.selfUpdateAvailable':
|
'misc.selfUpdateAvailable':
|
||||||
@@ -268,7 +275,8 @@ const deDE: Dict = {
|
|||||||
'Empfohlen für Breitbild-Fensterauflösungen. [Vanilla: 90] [Tweaks: 110]',
|
'Empfohlen für Breitbild-Fensterauflösungen. [Vanilla: 90] [Tweaks: 110]',
|
||||||
'tweaks.farClip.label': 'Sichtweite',
|
'tweaks.farClip.label': 'Sichtweite',
|
||||||
'tweaks.farClip.text':
|
'tweaks.farClip.text':
|
||||||
'Erhöht die maximale Sichtweite. [Vanilla: 777] [Tweaks: 10000]',
|
'Erhöht die maximale Sichtweite. Sehr hohe Werte können beim Betreten der Welt zum Absturz führen. [Vanilla: 777]',
|
||||||
|
'tweaks.farClip.recommendedHint': 'Empfohlen für deinen PC: {value}.',
|
||||||
'tweaks.frillDistance.label': 'Bodendetail-Distanz',
|
'tweaks.frillDistance.label': 'Bodendetail-Distanz',
|
||||||
'tweaks.frillDistance.text':
|
'tweaks.frillDistance.text':
|
||||||
'Ändert die Renderdistanz für Bodendetails. [Vanilla: 70] [Tweaks: 300]',
|
'Ändert die Renderdistanz für Bodendetails. [Vanilla: 70] [Tweaks: 300]',
|
||||||
@@ -490,7 +498,9 @@ const zhCN: Dict = {
|
|||||||
'tweaks.fieldOfView.text':
|
'tweaks.fieldOfView.text':
|
||||||
'推荐用于宽屏窗口分辨率。[Vanilla: 90] [Tweaks: 110]',
|
'推荐用于宽屏窗口分辨率。[Vanilla: 90] [Tweaks: 110]',
|
||||||
'tweaks.farClip.label': '渲染距离',
|
'tweaks.farClip.label': '渲染距离',
|
||||||
'tweaks.farClip.text': '增加最大渲染距离。[Vanilla: 777] [Tweaks: 10000]',
|
'tweaks.farClip.text':
|
||||||
|
'增加最大渲染距离。数值过高可能导致进入游戏世界时崩溃。[Vanilla: 777]',
|
||||||
|
'tweaks.farClip.recommendedHint': '为您的电脑推荐:{value}。',
|
||||||
'tweaks.frillDistance.label': '地表杂物距离',
|
'tweaks.frillDistance.label': '地表杂物距离',
|
||||||
'tweaks.frillDistance.text':
|
'tweaks.frillDistance.text':
|
||||||
'更改地表杂物的渲染距离。[Vanilla: 70] [Tweaks: 300]',
|
'更改地表杂物的渲染距离。[Vanilla: 70] [Tweaks: 300]',
|
||||||
@@ -698,7 +708,8 @@ const esES: Dict = {
|
|||||||
'Recomendado para resoluciones de ventana panorámicas. [Vanilla: 90] [Tweaks: 110]',
|
'Recomendado para resoluciones de ventana panorámicas. [Vanilla: 90] [Tweaks: 110]',
|
||||||
'tweaks.farClip.label': 'Distancia de renderizado',
|
'tweaks.farClip.label': 'Distancia de renderizado',
|
||||||
'tweaks.farClip.text':
|
'tweaks.farClip.text':
|
||||||
'Aumenta la distancia máxima de renderizado. [Vanilla: 777] [Tweaks: 10000]',
|
'Aumenta la distancia máxima de renderizado. Los valores muy altos pueden bloquear el juego al entrar al mundo. [Vanilla: 777]',
|
||||||
|
'tweaks.farClip.recommendedHint': 'Recomendado para tu PC: {value}.',
|
||||||
'tweaks.frillDistance.label': 'Distancia de la maleza',
|
'tweaks.frillDistance.label': 'Distancia de la maleza',
|
||||||
'tweaks.frillDistance.text':
|
'tweaks.frillDistance.text':
|
||||||
'Cambia la distancia de renderizado de la maleza del suelo. [Vanilla: 70] [Tweaks: 300]',
|
'Cambia la distancia de renderizado de la maleza del suelo. [Vanilla: 70] [Tweaks: 300]',
|
||||||
@@ -922,7 +933,8 @@ const ptBR: Dict = {
|
|||||||
'Recomendado para resoluções de janela widescreen. [Vanilla: 90] [Tweaks: 110]',
|
'Recomendado para resoluções de janela widescreen. [Vanilla: 90] [Tweaks: 110]',
|
||||||
'tweaks.farClip.label': 'Distância de renderização',
|
'tweaks.farClip.label': 'Distância de renderização',
|
||||||
'tweaks.farClip.text':
|
'tweaks.farClip.text':
|
||||||
'Aumenta a distância máxima de renderização. [Vanilla: 777] [Tweaks: 10000]',
|
'Aumenta a distância máxima de renderização. Valores muito altos podem travar o jogo ao entrar no mundo. [Vanilla: 777]',
|
||||||
|
'tweaks.farClip.recommendedHint': 'Recomendado para o seu PC: {value}.',
|
||||||
'tweaks.frillDistance.label': 'Distância da vegetação do solo',
|
'tweaks.frillDistance.label': 'Distância da vegetação do solo',
|
||||||
'tweaks.frillDistance.text':
|
'tweaks.frillDistance.text':
|
||||||
'Altera a distância de renderização da vegetação do solo. [Vanilla: 70] [Tweaks: 300]',
|
'Altera a distância de renderização da vegetação do solo. [Vanilla: 70] [Tweaks: 300]',
|
||||||
@@ -1144,7 +1156,8 @@ const ruRU: Dict = {
|
|||||||
'Рекомендуется для широкоэкранных разрешений. [Vanilla: 90] [Tweaks: 110]',
|
'Рекомендуется для широкоэкранных разрешений. [Vanilla: 90] [Tweaks: 110]',
|
||||||
'tweaks.farClip.label': 'Дальность прорисовки',
|
'tweaks.farClip.label': 'Дальность прорисовки',
|
||||||
'tweaks.farClip.text':
|
'tweaks.farClip.text':
|
||||||
'Увеличивает максимальную дальность прорисовки. [Vanilla: 777] [Tweaks: 10000]',
|
'Увеличивает максимальную дальность прорисовки. Слишком высокие значения могут привести к вылету при входе в мир. [Vanilla: 777]',
|
||||||
|
'tweaks.farClip.recommendedHint': 'Рекомендовано для вашего ПК: {value}.',
|
||||||
'tweaks.frillDistance.label': 'Дальность травы и деталей',
|
'tweaks.frillDistance.label': 'Дальность травы и деталей',
|
||||||
'tweaks.frillDistance.text':
|
'tweaks.frillDistance.text':
|
||||||
'Изменяет дальность прорисовки травы и мелких деталей земли. [Vanilla: 70] [Tweaks: 300]',
|
'Изменяет дальность прорисовки травы и мелких деталей земли. [Vanilla: 70] [Tweaks: 300]',
|
||||||
@@ -1316,230 +1329,11 @@ const ruRU: Dict = {
|
|||||||
'misc.reload': 'Перезагрузить'
|
'misc.reload': 'Перезагрузить'
|
||||||
};
|
};
|
||||||
|
|
||||||
const trTR: Dict = {
|
|
||||||
'tab.news': 'Haberler',
|
|
||||||
'tab.tweaks': 'İnce Ayarlar',
|
|
||||||
'tab.addons': 'Eklentiler',
|
|
||||||
'tab.mods': 'Modlar',
|
|
||||||
'topbar.settings': 'Ayarlar',
|
|
||||||
'topbar.minimize': 'Simge Durumuna Küçült',
|
|
||||||
'topbar.quit': 'Çıkış',
|
|
||||||
'topbar.language': 'Dil',
|
|
||||||
'quit.title': 'Çıkılsın mı?',
|
|
||||||
'quit.warn':
|
|
||||||
'Oyununuz şu anda güncelleniyor. Şimdi çıkmak sorunlara neden olabilir.',
|
|
||||||
'quit.return': 'Geri Dön',
|
|
||||||
'launch.verifying': 'Doğrulanıyor',
|
|
||||||
'launch.play': 'Oyna',
|
|
||||||
'launch.retry': 'Yeniden Dene',
|
|
||||||
'launch.install': 'Yükle',
|
|
||||||
'launch.update': 'Güncelle',
|
|
||||||
'launch.updating': 'Güncelleniyor',
|
|
||||||
'launch.applying': 'Uygulanıyor',
|
|
||||||
'launch.updateAvailable': 'Güncelleme mevcut!',
|
|
||||||
'launch.modsChanged': 'Modlar değişti, oynamadan önce uygulayın',
|
|
||||||
'launch.upToDate': 'Her şey güncel!',
|
|
||||||
'launch.version': 'Sürüm: {version}',
|
|
||||||
'launch.errorLabel': 'Hata:',
|
|
||||||
'launch.serverFail': 'Güncelleme sunucusuna ulaşılamadı',
|
|
||||||
'launch.localVersion': 'Yerel sürümü başlatabilirsiniz: {version}',
|
|
||||||
'launch.tryLater': 'Lütfen daha sonra tekrar deneyin',
|
|
||||||
'launch.verifyHint': 'Yeniden Dene butonuna tıklayarak oyun verilerinizi doğrulayın.',
|
|
||||||
'launch.remaining': 'kaldı',
|
|
||||||
'launch.calculating': 'hesaplanıyor…',
|
|
||||||
'launch.onDisk': 'diskte',
|
|
||||||
'tweaks.alwaysAutoLoot.label': 'Her zaman otomatik yağmala',
|
|
||||||
'tweaks.alwaysAutoLoot.text':
|
|
||||||
'Otomatik yağmalama (auto-loot) davranışını tersine çevirerek her zaman otomatik yağmalar ve atanan tuşla otomatik yağmalamayı devre dışı bırakır.',
|
|
||||||
'tweaks.largeAddress.label': 'Büyük Adres Farkındalığı (LAA)',
|
|
||||||
'tweaks.largeAddress.text': 'Oyunun 2GB\'tan fazla bellek (RAM) kullanmasına izin verir.',
|
|
||||||
'tweaks.nameplateRange.label': 'İsim plakası mesafesi',
|
|
||||||
'tweaks.nameplateRange.text':
|
|
||||||
'İsim plakalarının görünür olduğu mesafeyi artırır. [Orijinal: 20] [Klasik: 41]',
|
|
||||||
'tweaks.cameraHeading': 'Kamera',
|
|
||||||
'tweaks.fieldOfView.label': 'Görüş Alanı (FoV)',
|
|
||||||
'tweaks.fieldOfView.text':
|
|
||||||
'Geniş ekran pencere çözünürlükleri için önerilir. [Orijinal: 90] [Ayarlı: 110]',
|
|
||||||
'tweaks.farClip.label': 'Görüş mesafesi',
|
|
||||||
'tweaks.farClip.text':
|
|
||||||
'Maksimum grafik oluşturma mesafesini artırır. [Orijinal: 777] [Ayarlı: 10000]',
|
|
||||||
'tweaks.frillDistance.label': 'Zemin detay mesafesi',
|
|
||||||
'tweaks.frillDistance.text':
|
|
||||||
'Çim ve zemin detaylarının oluşturma mesafesini değiştirir. [Orijinal: 70] [Ayarlı: 300]',
|
|
||||||
'tweaks.cameraDistance.label': 'Kamera mesafesi',
|
|
||||||
'tweaks.cameraDistance.text':
|
|
||||||
'Maksimum kamera (uzaklaşma) mesafesini artırır. [Orijinal: 50] [Maks: 100]',
|
|
||||||
'tweaks.soundsHeading': 'Sesler',
|
|
||||||
'tweaks.soundInBackground.label': 'Arka plan sesleri',
|
|
||||||
'tweaks.soundInBackground.text':
|
|
||||||
'Oyun simge durumuna küçültüldüğünde oyun seslerinin çalmaya devam etmesini sağlar.',
|
|
||||||
'tweaks.applyFailed': 'Ayarlar uygulanamadı: {message}',
|
|
||||||
'tweaks.highlighted': 'Öne Çıkan',
|
|
||||||
'tweaks.recommendedNote': 'seçenekler önerilir ve varsayılan olarak etkindir',
|
|
||||||
'tweaks.reset': 'Sıfırla',
|
|
||||||
'tweaks.apply': 'Uygula',
|
|
||||||
'misc.newsTitle': 'Haberler',
|
|
||||||
'misc.newsByAuthor': 'Yazar: {author}',
|
|
||||||
'misc.newsReadMore': 'Devamını oku',
|
|
||||||
'misc.refresh': 'Yenile',
|
|
||||||
'misc.newsLoading': 'Haberler yükleniyor...',
|
|
||||||
'misc.newsError': 'Haber akışına ulaşılamadı.',
|
|
||||||
'misc.newsEmpty': 'Henüz haber yok. Daha sonra tekrar kontrol edin.',
|
|
||||||
'misc.tryAgain': 'Tekrar dene',
|
|
||||||
'misc.comingSoon': 'Çok yakında...',
|
|
||||||
'misc.selfUpdateCheckFailed': 'Güncelleme kontrolü başarısız oldu: {message}',
|
|
||||||
'misc.selfUpdateAvailable':
|
|
||||||
'Başlatıcı güncellemesi {version} mevcut, indirme hazırlanıyor…',
|
|
||||||
'misc.selfUpdateDownloading': 'Güncelleme indiriliyor {version} · %{percent}',
|
|
||||||
'misc.selfUpdateReady': 'Başlatıcı güncellemesi {version} kuruluma hazır',
|
|
||||||
'misc.selfUpdateInstallNow': 'Şimdi kur',
|
|
||||||
'misc.tabCrashed': '{tab} sekmesi çöktü',
|
|
||||||
'misc.somethingWentWrong': 'Bir şeyler ters gitti!',
|
|
||||||
'misc.uncaughtError': 'Yakalanamayan {name}: {message}',
|
|
||||||
'misc.unknownError': 'Bilinmeyen hata',
|
|
||||||
'misc.copyError': 'Hatayı kopyala',
|
|
||||||
'misc.reload': 'Yeniden Yükle',
|
|
||||||
'mods.update': 'güncelle',
|
|
||||||
'mods.ignoreUpdates': 'Güncellemeleri yoksay',
|
|
||||||
'mods.title': 'ÖZEL MODLAR',
|
|
||||||
'mods.unsavedChanges': 'kaydedilmemiş değişiklikler',
|
|
||||||
'mods.warning':
|
|
||||||
'Özel modları etkinleştirmek herhangi bir performans artışı sağlamayabilir veya sisteminize bağlı olarak oyunun çökmesine neden olabilir. Sorun yaşarsanız lütfen bunları devre dışı bırakmayı deneyin.',
|
|
||||||
'mods.enableRequired': 'Seçtiğiniz modlar için gerekli olan {mods} modunu etkinleştirin.',
|
|
||||||
'mods.depRequired': '{mod} etkinleştirilmelidir. {requiredBy} için gereklidir.',
|
|
||||||
'mods.highlighted': 'Öne Çıkan',
|
|
||||||
'mods.highlightedRecommended': 'modlar önerilir.',
|
|
||||||
'mods.apply': 'Uygula',
|
|
||||||
'mods.cantApplyYet': 'HENÜZ UYGULANAMAZ',
|
|
||||||
'mods.close': 'Kapat',
|
|
||||||
'av.blockedTitle': 'ANTİVİRÜS TARAFINDAN ENGELLENDİ',
|
|
||||||
'av.blockedOne': 'Windows Defender bu modu engelledi:',
|
|
||||||
'av.blockedMany': 'Windows Defender bu modları engelledi:',
|
|
||||||
'av.blockedExplain':
|
|
||||||
'Bunlar, Defender\'ın hatalı bir şekilde tehdit olarak işaretlediği (yanlış pozitif) güvenli topluluk modlarıdır. Bir kez izin verip ardından tekrar deneyin.',
|
|
||||||
'av.whyHappening': 'Bu neden oluyor?',
|
|
||||||
'av.allowThrough': 'Antivirüse izin ver',
|
|
||||||
'av.addedRetry': 'Eklendi. Bunu kapatıp tekrar deneyin.',
|
|
||||||
'av.close': 'Kapat',
|
|
||||||
'av.whyTitle': 'ANTİVİRÜSLER NEDEN MODLARI İŞARETLER?',
|
|
||||||
'av.whyIntro':
|
|
||||||
'Bu modlardan bazıları Windows Defender (veya diğer antivirüsler) tarafından "{detection}" gibi bir tehdit olarak işaretlenir. Bu durum bir',
|
|
||||||
'av.falsePositive': 'yanlış pozitiftir (hatalı alarm)',
|
|
||||||
'av.whatSetsItOff': 'Tetikleyen şey nedir?',
|
|
||||||
'av.whatSetsItOffIntro':
|
|
||||||
'Bu modlar, antivirüs sezgisel (heuristic) tarayıcılarına şüpheli görünen iki teknik kullanarak 20 yıllık 1.12 oyun istemcisini modernize eder:',
|
|
||||||
'av.dllInjection': 'DLL enjeksiyonu (DLL injection)',
|
|
||||||
'av.dllInjectionText':
|
|
||||||
'modlar, özellik eklemek (gecikme dengelemesi, daha iyi isim plakaları, performans) için çalışan oyuna ekstra kod yükler. Başka bir programa kod yüklemek zararlı yazılımların da yaptığı bir şeydir, bu nedenle bu davranış tek başına tarayıcıyı tetikler.',
|
|
||||||
'av.exePatching': 'Yürütülebilir dosya yamalama (Executable patching)',
|
|
||||||
'av.exePatchingText':
|
|
||||||
'bazı modlar, eski motor sınırlarını kaldırmak için WoW.exe dosyasını düzenler (örneğin 32-bit istemcinin 2GB yerine 4GB RAM kullanmasına izin vermek gibi). Bir yürütülebilir dosyayı değiştirmek de başka bir zararlı yazılım benzeri modeldir.',
|
|
||||||
'av.heuristicNote':
|
|
||||||
'Bunlar gerçek bir zararlı yazılım eşleşmesi değil de davranışa dayalı (sezgisel) tespitler olduğundan, bu yöntemleri kullanan meşru modlar genel "trojan" veya "hacktool" gibi yanlış pozitiflere yakalanır.',
|
|
||||||
'av.whatModsDo': 'Modlar gerçekte ne yapıyor?',
|
|
||||||
'av.modVanillaFixes':
|
|
||||||
'takılma ve zamanlayıcı hatalarını düzelten ve diğer modları başlatan yükleyici.',
|
|
||||||
'av.modNampower':
|
|
||||||
'yüksek gecikme sürelerinde (ping) yeteneklerin akıcı hissedilmesi için gecikme dengelemesi.',
|
|
||||||
'av.modSuperWowUnitXp':
|
|
||||||
'gelişmiş Lua, gerçek isim plakaları, kamera ve hedefleme yükseltmeleri.',
|
|
||||||
'av.modDxvk':
|
|
||||||
'büyük FPS kazançları için eski DirectX 9\'u modern Vulkan\'a dönüştürür.',
|
|
||||||
'av.howToVerify': 'Bu iddialar nasıl doğrulanır?',
|
|
||||||
'av.howToVerifyText':
|
|
||||||
'Bunlar açık kaynaklı topluluk projeleridir. Modlar listesinde yanlarında bulunan kaynak kod linkinden her bir modun kodunun her satırını okuyabilir ve bunun belgelenmiş bir yanlış pozitif olduğunu görmek için tespit adını (örneğin "{detection}") aratabilirsiniz.',
|
|
||||||
'av.whatAllowDoesTitle': '"Antivirüse izin ver" ne işe yarar?',
|
|
||||||
'av.whatAllowDoesIntro':
|
|
||||||
'Windows\'tan yönetici izni ister (bir kez gelen uyarıda Evet\'e tıklayın), ardından Windows\'un kendi yerleşik',
|
|
||||||
'av.whatAllowDoesIntroAfter':
|
|
||||||
'komutunu çalıştırarak Windows Defender dışlama listesine birkaç girdi ekler:',
|
|
||||||
'av.exclusionFoldersBefore': 'Oyun',
|
|
||||||
'av.gameFolder': 'klasörünüz',
|
|
||||||
'av.exclusionFoldersAnd': 've',
|
|
||||||
'av.launcherFolder': 'başlatıcı klasörünüz',
|
|
||||||
'av.exclusionFoldersAfter':
|
|
||||||
'dışlandığı için içlerindeki mod dosyaları artık taranmaz veya karantinaya alınmaz.',
|
|
||||||
'av.exclusionExesAnd': 've',
|
|
||||||
'av.exclusionExesAfter':
|
|
||||||
'izin verilen işlemler olarak eklenir, böylece çalışırken engellenmezler.',
|
|
||||||
'av.whatAllowDoesOutro':
|
|
||||||
'Bu, Windows Güvenliği > Virüs ve tehdit koruması > Dışlamalar altından kendi elinizle yapacağınız şeyin aynısıdır. Hiçbir şey yüklemez, üçüncü taraf yazılım çalıştırmaz ve bu belirli yolların dışındaki hiçbir şeyi değiştirmez.',
|
|
||||||
'av.back': 'Geri',
|
|
||||||
'addons.sectionInstalled': 'Yüklü',
|
|
||||||
'addons.sectionRecommended': 'Önerilen',
|
|
||||||
'addons.sectionAvailable': 'Mevcut',
|
|
||||||
'addons.checkForUpdates': 'Güncellemeleri kontrol et',
|
|
||||||
'addons.addCustomGitAddon': 'Özel git eklentisi ekle',
|
|
||||||
'addons.updateAll': 'Tümünü güncelle',
|
|
||||||
'addons.everythingUpToDate': 'Her şey güncel.',
|
|
||||||
'addons.installAddon': 'Eklentiyi yükle',
|
|
||||||
'addons.previewAlt': 'Önizleme',
|
|
||||||
'addons.readyToInstall': 'Yüklemeye hazır',
|
|
||||||
'addons.invalidGitUrl': 'Geçerli bir git deposu URL\'si değil',
|
|
||||||
'addons.install': 'Yükle',
|
|
||||||
'addons.warnIncorrectVersionFull':
|
|
||||||
'Bu eklenti farklı bir oyun sürümü ({version}) için yapılmış gibi görünüyor ve düzgün çalışmayabilir',
|
|
||||||
'addons.warnIncorrectVersionShort': 'Uyumsuz sürüm',
|
|
||||||
'addons.warnMissingDependenciesFull':
|
|
||||||
'Bu eklentinin eksik bağımlılıkları var: {deps}',
|
|
||||||
'addons.warnMissingDependenciesShort': 'Eksik bağımlılıklar',
|
|
||||||
'addons.details': 'Detaylar',
|
|
||||||
'addons.openOnGithub': '{url} adresini GitHub\'da aç',
|
|
||||||
'addons.upToDate': 'Güncel',
|
|
||||||
'addons.notVersioned': 'Sürümlendirilmemiş',
|
|
||||||
'addons.update': 'Güncelle',
|
|
||||||
'addons.download': 'İndir',
|
|
||||||
'addons.deleteConfirmTitle': 'Emin misiniz?',
|
|
||||||
'addons.deleteConfirmBody': '{folder} eklentisini silmek istediğinizden emin misiniz?',
|
|
||||||
'addons.deleteConfirmFiles':
|
|
||||||
'Bu işlem eklenti klasöründeki tüm dosyaları silecektir.',
|
|
||||||
'addons.delete': 'Sil',
|
|
||||||
'addons.remove': 'Kaldır',
|
|
||||||
'addons.detailSource': 'Kaynak',
|
|
||||||
'addons.openOnGithubShort': 'GitHub\'da Aç',
|
|
||||||
'addons.detailContributions': 'Katkıda Bulunanlar',
|
|
||||||
'addons.detailAddonVersion': 'Eklenti sürümü',
|
|
||||||
'addons.detailDependencies': 'Bağımlılıklar',
|
|
||||||
'addons.optional': '(isteğe bağlı)',
|
|
||||||
'prefs.mirrorOffline': 'çevrimdışı',
|
|
||||||
'prefs.mirrorChecking': 'kontrol ediliyor…',
|
|
||||||
'prefs.mirrorOnline': 'çevrimiçi',
|
|
||||||
'prefs.title': 'AYARLAR',
|
|
||||||
'prefs.installLocation': 'YÜKLEME KONUMU:',
|
|
||||||
'prefs.openFolder': 'Klasörü aç',
|
|
||||||
'prefs.notSelected': 'Seçilmedi',
|
|
||||||
'prefs.change': 'Değiştir',
|
|
||||||
'prefs.downloadMirror': 'İNDİRME YANSISI (MIRROR):',
|
|
||||||
'prefs.recheck': 'Yeniden kontrol et',
|
|
||||||
'prefs.troubleshooting': 'SORUN GİDERME:',
|
|
||||||
'prefs.verifyGameFiles': 'Oyun dosyalarını doğrula',
|
|
||||||
'prefs.openLogFile': 'Günlük (log) dosyasını aç',
|
|
||||||
'prefs.allowThroughAntivirus': 'Antivirüse izin ver',
|
|
||||||
'prefs.exclusionAdded': 'Eklendi. Silinen dosya varsa oyun dosyalarını yeniden doğrulayın.',
|
|
||||||
'prefs.generalSettings': 'GENEL AYARLAR:',
|
|
||||||
'prefs.cleanWdb': 'Her açılışta WDB klasörünü temizle',
|
|
||||||
'prefs.minimizeToTray': 'Oynarken sistem tepsisine küçült',
|
|
||||||
'prefs.save': 'Kaydet',
|
|
||||||
'prefs.installLocationTitle': 'Yükleme konumu',
|
|
||||||
'prefs.portableInfo':
|
|
||||||
'Başlatıcının taşınabilir (portable) sürümünü kullanıyorsunuz. Yükleme konumu, başlatıcı yürütülebilir dosyasının bulunduğu yere göre belirlenir.',
|
|
||||||
'prefs.errorLabel': 'Hata: ',
|
|
||||||
'prefs.wowExeNotFound':
|
|
||||||
'Mevcut klasörde {exe} bulunamadı. Lütfen başlatıcıyı kapatın ve WoW 1.12 istemci dizininize taşıyın.',
|
|
||||||
'prefs.selectDirectory':
|
|
||||||
'Oyun istemcisi kurulumu için bir dizin seçin.',
|
|
||||||
'prefs.upgradeExisting':
|
|
||||||
'Mevcut bir Turtle WoW veya Vanilla WoW kurulumunun olduğu bir dizini de seçebilirsiniz; bu durumda otomatik olarak yükseltilecektir.',
|
|
||||||
'prefs.installDirectory': 'Kurulum dizini:',
|
|
||||||
'prefs.confirm': 'Onayla'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const translations: Record<Lang, Dict> = {
|
export const translations: Record<Lang, Dict> = {
|
||||||
enUS,
|
enUS,
|
||||||
deDE,
|
deDE,
|
||||||
zhCN,
|
zhCN,
|
||||||
esES,
|
esES,
|
||||||
ptBR,
|
ptBR,
|
||||||
ruRU,
|
ruRU
|
||||||
trTR
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -202,6 +202,119 @@ input[type='number'] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.parchment-post {
|
||||||
|
position: relative;
|
||||||
|
color: #2b1a0c;
|
||||||
|
background-color: #e9dab5;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
@apply border p-4;
|
||||||
|
border-color: rgba(60, 40, 15, 0.4);
|
||||||
|
box-shadow: rgb(0 0 0 / 45%) 0px 25px 20px -20px;
|
||||||
|
|
||||||
|
& .parchment-post-title {
|
||||||
|
@apply font-fontin uppercase;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
color: #3a230f;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .parchment-post-meta {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-style: italic;
|
||||||
|
color: #6b5836;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .parchment-post-muted {
|
||||||
|
color: #7a6a47;
|
||||||
|
}
|
||||||
|
|
||||||
|
& hr {
|
||||||
|
@apply -mx-4;
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid rgba(60, 40, 15, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
& button {
|
||||||
|
color: #6b3e15;
|
||||||
|
}
|
||||||
|
& button:hover,
|
||||||
|
& button:focus {
|
||||||
|
color: #9c4a12;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .parchment-post-body,
|
||||||
|
& .parchment-post-body * {
|
||||||
|
color: #2b1a0c;
|
||||||
|
}
|
||||||
|
& .parchment-post-body {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 24px;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
& .parchment-post-body a {
|
||||||
|
color: #6b3e15;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
& .parchment-post-body a:hover {
|
||||||
|
color: #9c4a12;
|
||||||
|
}
|
||||||
|
& .parchment-post-body p {
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
& .parchment-post-body strong,
|
||||||
|
& .parchment-post-body b {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
& .parchment-post-body em,
|
||||||
|
& .parchment-post-body i {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
& .parchment-post-body ul {
|
||||||
|
list-style: disc;
|
||||||
|
padding-left: 20px;
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
& .parchment-post-body ol {
|
||||||
|
list-style: decimal;
|
||||||
|
padding-left: 20px;
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
& .parchment-post-body blockquote {
|
||||||
|
border-left: 3px solid #b08a57;
|
||||||
|
background: rgba(120, 80, 30, 0.08);
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 8px 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
& .parchment-post-body code,
|
||||||
|
& .parchment-post-body pre,
|
||||||
|
& .parchment-post-body .codebox {
|
||||||
|
font-family: monospace;
|
||||||
|
background: rgba(60, 40, 15, 0.1);
|
||||||
|
border: 1px solid rgba(60, 40, 15, 0.2);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 8px;
|
||||||
|
display: block;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
& .parchment-post-body img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
& .parchment-post-body h1,
|
||||||
|
& .parchment-post-body h2,
|
||||||
|
& .parchment-post-body h3,
|
||||||
|
& .parchment-post-body h4 {
|
||||||
|
color: #3a230f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tw-hocus {
|
.tw-hocus {
|
||||||
@apply hocus:text-orange hocus:drop-shadow-[0px_0px_15px_white];
|
@apply hocus:text-orange hocus:drop-shadow-[0px_0px_15px_white];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user