Repository: bepass-org/oblivion-desktop Branch: main Commit: 9fdf21769396 Files: 184 Total size: 826.2 KB Directory structure: gitextract_gl2ervna/ ├── .erb/ │ ├── configs/ │ │ ├── .eslintrc │ │ ├── webpack.config.base.ts │ │ ├── webpack.config.eslint.ts │ │ ├── webpack.config.main.dev.ts │ │ ├── webpack.config.main.prod.ts │ │ ├── webpack.config.preload.dev.ts │ │ ├── webpack.config.renderer.dev.dll.ts │ │ ├── webpack.config.renderer.dev.ts │ │ ├── webpack.config.renderer.prod.ts │ │ └── webpack.paths.ts │ ├── mocks/ │ │ └── fileMock.js │ └── scripts/ │ ├── .eslintrc │ ├── check-build-exists.ts │ ├── check-native-dep.js │ ├── check-node-env.js │ ├── check-port-in-use.js │ ├── clean.js │ ├── delete-source-maps.js │ ├── electron-rebuild.js │ ├── link-modules.ts │ └── notarize.js ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yaml │ │ ├── config.yml │ │ └── feature_request.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── config.yml │ ├── dependabot.yml │ ├── release_message.md │ ├── stale.yml │ └── workflows/ │ ├── deploy-ppa.yml │ └── publish.yml ├── .gitignore ├── .husky/ │ ├── pre-commit │ └── pre-push ├── .prettierignore ├── .prettierrc ├── .vscode/ │ ├── launch.json │ └── settings.json ├── @types/ │ └── node-aplay.d.ts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DOCS.md ├── FAQ.md ├── LICENSE.md ├── README-fa.md ├── README.md ├── SECURITY.md ├── assets/ │ ├── assets.d.ts │ ├── css/ │ │ ├── materialIcons.css │ │ ├── noto.css │ │ ├── shabnam.css │ │ └── style.css │ ├── entitlements.mac.plist │ ├── icon.icns │ ├── json/ │ │ └── 1713988096625.json │ └── proto/ │ └── oblivion.proto ├── package.json ├── release/ │ └── app/ │ └── package.json ├── script/ │ ├── beforePackHook.js │ ├── changeVersion.ts │ ├── dlBins.ts │ ├── makeRegeditVBSAvailable.ts │ ├── playground.ts │ └── postinstall.ts ├── src/ │ ├── __tests__/ │ │ └── App.test.tsx │ ├── constants.ts │ ├── defaultSettings.ts │ ├── localization/ │ │ ├── am.ts │ │ ├── ar.ts │ │ ├── cn.ts │ │ ├── electron.ts │ │ ├── en.ts │ │ ├── es.ts │ │ ├── fa.ts │ │ ├── id.ts │ │ ├── index.ts │ │ ├── my.ts │ │ ├── pt.ts │ │ ├── ru.ts │ │ ├── tr.ts │ │ ├── type.ts │ │ ├── ur.ts │ │ ├── useTranslate.ts │ │ └── vi.ts │ ├── main/ │ │ ├── config.ts │ │ ├── dxConfig.ts │ │ ├── ipc.ts │ │ ├── ipcListeners/ │ │ │ ├── log.ts │ │ │ └── settings.ts │ │ ├── lib/ │ │ │ ├── customEvent.ts │ │ │ ├── netStatsManager.ts │ │ │ ├── pacScript.ts │ │ │ ├── proxy.ts │ │ │ ├── sbConfig.ts │ │ │ ├── sbHelper.ts │ │ │ ├── sbManager.ts │ │ │ ├── speedTestManager.ts │ │ │ ├── utils.ts │ │ │ ├── wpHelper.ts │ │ │ └── wpManager.ts │ │ ├── main.ts │ │ ├── menu.ts │ │ ├── playground.ts │ │ └── preload.ts │ ├── renderer/ │ │ ├── App.tsx │ │ ├── components/ │ │ │ ├── BackButton.tsx │ │ │ ├── Card/ │ │ │ │ └── index.tsx │ │ │ ├── ConfigHandler.tsx │ │ │ ├── Dropdown/ │ │ │ │ └── index.tsx │ │ │ ├── Input/ │ │ │ │ ├── index.tsx │ │ │ │ └── useInput.ts │ │ │ ├── Modal/ │ │ │ │ ├── DNS/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── useDnsModal.ts │ │ │ │ ├── Endpoint/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── useEndpointModal.ts │ │ │ │ ├── License/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── useLicenseModal.ts │ │ │ │ ├── MTU/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── useMTUModal.ts │ │ │ │ ├── Port/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── usePortModal.ts │ │ │ │ ├── Profile/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── useProfileModal.ts │ │ │ │ ├── Restore/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── useRestoreModal.ts │ │ │ │ ├── RoutingRules/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── useRoutingRulesModal.ts │ │ │ │ └── TestUrl/ │ │ │ │ ├── index.tsx │ │ │ │ └── useTestUrlModal.ts │ │ │ ├── Nav/ │ │ │ │ ├── index.tsx │ │ │ │ └── useNav.ts │ │ │ ├── Tabs.tsx │ │ │ └── Textarea/ │ │ │ ├── index.tsx │ │ │ └── useTextarea.ts │ │ ├── context/ │ │ │ └── GlobalContext.tsx │ │ ├── hooks/ │ │ │ ├── useButtonKeyDown.ts │ │ │ └── useGoBackOnEscape.tsx │ │ ├── index.ejs │ │ ├── index.tsx │ │ ├── lib/ │ │ │ ├── cfFlag.ts │ │ │ ├── dx.ts │ │ │ ├── getIspName.ts │ │ │ ├── globalEvents.ts │ │ │ ├── inputSanitizer.ts │ │ │ ├── isAnyUndefined.ts │ │ │ ├── loaders.ts │ │ │ ├── settings.ts │ │ │ ├── systemDateValidator.ts │ │ │ ├── toPersianNumber.ts │ │ │ ├── toasts.tsx │ │ │ ├── utils.ts │ │ │ └── withDefault.ts │ │ ├── pages/ │ │ │ ├── About/ │ │ │ │ └── index.tsx │ │ │ ├── Debug/ │ │ │ │ ├── index.tsx │ │ │ │ └── useDebug.ts │ │ │ ├── Landing/ │ │ │ │ ├── DownloadProgressBar.tsx │ │ │ │ ├── LandingBody.tsx │ │ │ │ ├── LandingDrawer.tsx │ │ │ │ ├── LandingHeader.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── useLanding.ts │ │ │ ├── Network/ │ │ │ │ ├── index.tsx │ │ │ │ └── useOptions.ts │ │ │ ├── Options/ │ │ │ │ ├── index.tsx │ │ │ │ └── useOptions.ts │ │ │ ├── Scanner/ │ │ │ │ ├── index.tsx │ │ │ │ └── useScanner.ts │ │ │ ├── Settings/ │ │ │ │ ├── index.tsx │ │ │ │ └── useSettings.ts │ │ │ ├── SingBox/ │ │ │ │ ├── index.tsx │ │ │ │ └── useSingBox.ts │ │ │ ├── SpeedTest/ │ │ │ │ ├── index.tsx │ │ │ │ └── useSpeedTest.ts │ │ │ └── SplashScreen/ │ │ │ ├── index.tsx │ │ │ └── useSplashScreen.ts │ │ ├── preload.d.ts │ │ ├── routes/ │ │ │ └── index.tsx │ │ └── store.ts │ └── types/ │ └── global.d.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .erb/configs/.eslintrc ================================================ { "rules": { "no-console": "off", "global-require": "off", "import/no-dynamic-require": "off" } } ================================================ FILE: .erb/configs/webpack.config.base.ts ================================================ /** * Base webpack config used across other specific configs */ import webpack from 'webpack'; import TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin'; import webpackPaths from './webpack.paths'; import { dependencies as externals } from '../../release/app/package.json'; const configuration: webpack.Configuration = { externals: [...Object.keys(externals || {})], stats: 'errors-only', module: { rules: [ { test: /\.[jt]sx?$/, exclude: /node_modules/, use: { loader: 'ts-loader', options: { // Remove this line to enable type checking in webpack builds transpileOnly: true, compilerOptions: { module: 'esnext' } } } } ] }, output: { path: webpackPaths.srcPath, // https://github.com/webpack/webpack/issues/1114 library: { type: 'commonjs2' } }, /** * Determine the array of extensions that should be used to resolve modules. */ resolve: { extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], modules: [webpackPaths.srcPath, 'node_modules'], // There is no need to add aliases here, the paths in tsconfig get mirrored plugins: [new TsconfigPathsPlugins()] }, plugins: [ new webpack.EnvironmentPlugin({ NODE_ENV: 'production' }) ] }; export default configuration; ================================================ FILE: .erb/configs/webpack.config.eslint.ts ================================================ /* eslint import/no-unresolved: off, import/no-self-import: off */ module.exports = require('./webpack.config.renderer.dev').default; ================================================ FILE: .erb/configs/webpack.config.main.dev.ts ================================================ /** * Webpack config for development electron main process */ import path from 'path'; import webpack from 'webpack'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import { merge } from 'webpack-merge'; import checkNodeEnv from '../scripts/check-node-env'; import baseConfig from './webpack.config.base'; import webpackPaths from './webpack.paths'; // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's // at the dev webpack config is not accidentally run in a production environment if (process.env.NODE_ENV === 'production') { checkNodeEnv('development'); } const configuration: webpack.Configuration = { devtool: 'inline-source-map', mode: 'development', target: 'electron-main', entry: { main: path.join(webpackPaths.srcMainPath, 'main.ts'), preload: path.join(webpackPaths.srcMainPath, 'preload.ts') }, output: { path: webpackPaths.dllPath, filename: '[name].bundle.dev.js', library: { type: 'umd' } }, plugins: [ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore new BundleAnalyzerPlugin({ analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', analyzerPort: 8888 }), new webpack.DefinePlugin({ 'process.type': '"browser"' }) ], /** * Disables webpack processing of __dirname and __filename. * If you run the bundle in node.js it falls back to these values of node.js. * https://github.com/webpack/webpack/issues/2010 */ node: { __dirname: false, __filename: false } }; export default merge(baseConfig, configuration); ================================================ FILE: .erb/configs/webpack.config.main.prod.ts ================================================ /** * Webpack config for production electron main process */ import path from 'path'; import webpack from 'webpack'; import { merge } from 'webpack-merge'; import TerserPlugin from 'terser-webpack-plugin'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import baseConfig from './webpack.config.base'; import webpackPaths from './webpack.paths'; import checkNodeEnv from '../scripts/check-node-env'; import deleteSourceMaps from '../scripts/delete-source-maps'; checkNodeEnv('production'); deleteSourceMaps(); const configuration: webpack.Configuration = { devtool: 'source-map', mode: 'production', target: 'electron-main', entry: { main: path.join(webpackPaths.srcMainPath, 'main.ts'), preload: path.join(webpackPaths.srcMainPath, 'preload.ts') }, output: { path: webpackPaths.distMainPath, filename: '[name].js', library: { type: 'umd' } }, optimization: { minimizer: [ new TerserPlugin({ parallel: true }) ] }, plugins: [ new BundleAnalyzerPlugin({ analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', analyzerPort: 8888 }), /** * Create global constants which can be configured at compile time. * * Useful for allowing different behaviour between development builds and * release builds * * NODE_ENV should be production so that modules do not perform certain * development checks */ new webpack.EnvironmentPlugin({ NODE_ENV: 'production', DEBUG_PROD: false, START_MINIMIZED: false }), new webpack.DefinePlugin({ 'process.type': '"browser"' }) ], /** * Disables webpack processing of __dirname and __filename. * If you run the bundle in node.js it falls back to these values of node.js. * https://github.com/webpack/webpack/issues/2010 */ node: { __dirname: false, __filename: false } }; export default merge(baseConfig, configuration); ================================================ FILE: .erb/configs/webpack.config.preload.dev.ts ================================================ import path from 'path'; import webpack from 'webpack'; import { merge } from 'webpack-merge'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import baseConfig from './webpack.config.base'; import webpackPaths from './webpack.paths'; import checkNodeEnv from '../scripts/check-node-env'; // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's // at the dev webpack config is not accidentally run in a production environment if (process.env.NODE_ENV === 'production') { checkNodeEnv('development'); } const configuration: webpack.Configuration = { devtool: 'inline-source-map', mode: 'development', target: 'electron-preload', entry: path.join(webpackPaths.srcMainPath, 'preload.ts'), output: { path: webpackPaths.dllPath, filename: 'preload.js', library: { type: 'umd' } }, plugins: [ new BundleAnalyzerPlugin({ analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled' }), /** * Create global constants which can be configured at compile time. * * Useful for allowing different behaviour between development builds and * release builds * * NODE_ENV should be production so that modules do not perform certain * development checks * * By default, use 'development' as NODE_ENV. This can be overriden with * 'staging', for example, by changing the ENV variables in the npm scripts */ new webpack.EnvironmentPlugin({ NODE_ENV: 'development' }), new webpack.LoaderOptionsPlugin({ debug: true }) ], /** * Disables webpack processing of __dirname and __filename. * If you run the bundle in node.js it falls back to these values of node.js. * https://github.com/webpack/webpack/issues/2010 */ node: { __dirname: false, __filename: false }, watch: true }; export default merge(baseConfig, configuration); ================================================ FILE: .erb/configs/webpack.config.renderer.dev.dll.ts ================================================ /** * Builds the DLL for development electron renderer process */ import webpack from 'webpack'; import path from 'path'; import { merge } from 'webpack-merge'; import baseConfig from './webpack.config.base'; import webpackPaths from './webpack.paths'; import { dependencies } from '../../package.json'; import checkNodeEnv from '../scripts/check-node-env'; checkNodeEnv('development'); const dist = webpackPaths.dllPath; const configuration: webpack.Configuration = { context: webpackPaths.rootPath, devtool: 'eval', mode: 'development', target: 'electron-renderer', externals: ['fsevents', 'crypto-browserify'], /** * Use `module` from `webpack.config.renderer.dev.js` */ module: require('./webpack.config.renderer.dev').default.module, entry: { renderer: Object.keys(dependencies || {}) }, output: { path: dist, filename: '[name].dev.dll.js', library: { name: 'renderer', type: 'var' } }, plugins: [ new webpack.DllPlugin({ path: path.join(dist, '[name].json'), name: '[name]' }), /** * Create global constants which can be configured at compile time. * * Useful for allowing different behaviour between development builds and * release builds * * NODE_ENV should be production so that modules do not perform certain * development checks */ new webpack.EnvironmentPlugin({ NODE_ENV: 'development' }), new webpack.LoaderOptionsPlugin({ debug: true, options: { context: webpackPaths.srcPath, output: { path: webpackPaths.dllPath } } }) ] }; export default merge(baseConfig, configuration); ================================================ FILE: .erb/configs/webpack.config.renderer.dev.ts ================================================ import 'webpack-dev-server'; import path from 'path'; import fs from 'fs'; import webpack from 'webpack'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import chalk from 'chalk'; import { merge } from 'webpack-merge'; import { execSync, spawn } from 'child_process'; import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import baseConfig from './webpack.config.base'; import webpackPaths from './webpack.paths'; import checkNodeEnv from '../scripts/check-node-env'; // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's // at the dev webpack config is not accidentally run in a production environment if (process.env.NODE_ENV === 'production') { checkNodeEnv('development'); } const port = process.env.PORT || 1212; const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json'); const skipDLLs = module.parent?.filename.includes('webpack.config.renderer.dev.dll') || module.parent?.filename.includes('webpack.config.eslint'); /** * Warn if the DLL is not built */ if (!skipDLLs && !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest))) { console.log( chalk.black.bgYellow.bold( 'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"' ) ); execSync('npm run postinstall'); } const configuration: webpack.Configuration = { devtool: 'inline-source-map', mode: 'development', target: ['web', 'electron-renderer'], entry: [ `webpack-dev-server/client?http://localhost:${port}/dist`, 'webpack/hot/only-dev-server', path.join(webpackPaths.srcRendererPath, 'index.tsx') ], output: { path: webpackPaths.distRendererPath, publicPath: '/', filename: 'renderer.dev.js', library: { type: 'umd' } }, module: { rules: [ { test: /\.s?(c|a)ss$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, sourceMap: true, importLoaders: 1 } }, 'sass-loader' ], include: /\.module\.s?(c|a)ss$/ }, { test: /\.s?css$/, use: ['style-loader', 'css-loader', 'sass-loader'], exclude: /\.module\.s?(c|a)ss$/ }, // Fonts { test: /\.(woff|woff2|eot|ttf|otf)$/i, type: 'asset/resource' }, // Images { test: /\.(png|jpg|jpeg|gif)$/i, type: 'asset/resource' }, // SVG { test: /\.svg$/, use: [ { loader: '@svgr/webpack', options: { prettier: false, svgo: false, svgoConfig: { plugins: [{ removeViewBox: false }] }, titleProp: true, ref: true } }, 'file-loader' ] } ] }, plugins: [ ...(skipDLLs ? [] : [ new webpack.DllReferencePlugin({ context: webpackPaths.dllPath, manifest: require(manifest), sourceType: 'var' }) ]), new webpack.NoEmitOnErrorsPlugin(), /** * Create global constants which can be configured at compile time. * * Useful for allowing different behaviour between development builds and * release builds * * NODE_ENV should be production so that modules do not perform certain * development checks * * By default, use 'development' as NODE_ENV. This can be overriden with * 'staging', for example, by changing the ENV variables in the npm scripts */ new webpack.EnvironmentPlugin({ NODE_ENV: 'development' }), new webpack.LoaderOptionsPlugin({ debug: true }), new ReactRefreshWebpackPlugin(), new HtmlWebpackPlugin({ filename: path.join('index.html'), template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), minify: { collapseWhitespace: true, removeAttributeQuotes: true, removeComments: true }, isBrowser: false, env: process.env.NODE_ENV, isDevelopment: process.env.NODE_ENV !== 'production', nodeModules: webpackPaths.appNodeModulesPath }) ], node: { __dirname: false, __filename: false }, devServer: { port, compress: true, hot: true, headers: { 'Access-Control-Allow-Origin': '*' }, static: { publicPath: '/' }, historyApiFallback: { verbose: true }, setupMiddlewares(middlewares) { console.log('Starting preload.js builder...'); const preloadProcess = spawn('npm', ['run', 'start:preload'], { shell: true, stdio: 'inherit' }) .on('close', (code: number) => process.exit(code!)) .on('error', (spawnError) => console.error(spawnError)); console.log('Starting Main Process...'); let args = ['run', 'start:main']; if (process.env.MAIN_ARGS) { args = args.concat( ['--', ...process.env.MAIN_ARGS.matchAll(/"[^"]+"|[^\s"]+/g)].flat() ); } spawn('npm', args, { shell: true, stdio: 'inherit' }) .on('close', (code: number) => { preloadProcess.kill(); process.exit(code!); }) .on('error', (spawnError) => console.error(spawnError)); return middlewares; } } }; export default merge(baseConfig, configuration); ================================================ FILE: .erb/configs/webpack.config.renderer.prod.ts ================================================ /** * Build config for electron renderer process */ import path from 'path'; import webpack from 'webpack'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; import { merge } from 'webpack-merge'; import TerserPlugin from 'terser-webpack-plugin'; import baseConfig from './webpack.config.base'; import webpackPaths from './webpack.paths'; import checkNodeEnv from '../scripts/check-node-env'; import deleteSourceMaps from '../scripts/delete-source-maps'; checkNodeEnv('production'); deleteSourceMaps(); const configuration: webpack.Configuration = { devtool: 'source-map', mode: 'production', target: ['web', 'electron-renderer'], entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')], output: { path: webpackPaths.distRendererPath, publicPath: './', filename: 'renderer.js', library: { type: 'umd' } }, module: { rules: [ { test: /\.s?(a|c)ss$/, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { modules: true, sourceMap: true, importLoaders: 1 } }, 'sass-loader' ], include: /\.module\.s?(c|a)ss$/ }, { test: /\.s?(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], exclude: /\.module\.s?(c|a)ss$/ }, // Fonts { test: /\.(woff|woff2|eot|ttf|otf)$/i, type: 'asset/resource' }, // Images { test: /\.(png|jpg|jpeg|gif)$/i, type: 'asset/resource' }, // SVG { test: /\.svg$/, use: [ { loader: '@svgr/webpack', options: { prettier: false, svgo: false, svgoConfig: { plugins: [{ removeViewBox: false }] }, titleProp: true, ref: true } }, 'file-loader' ] } ] }, optimization: { minimize: true, minimizer: [new TerserPlugin(), new CssMinimizerPlugin()] }, plugins: [ /** * Create global constants which can be configured at compile time. * * Useful for allowing different behaviour between development builds and * release builds * * NODE_ENV should be production so that modules do not perform certain * development checks */ new webpack.EnvironmentPlugin({ NODE_ENV: 'production', DEBUG_PROD: false }), new MiniCssExtractPlugin({ filename: 'style.css' }), new BundleAnalyzerPlugin({ analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', analyzerPort: 8889 }), new HtmlWebpackPlugin({ filename: 'index.html', template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), minify: { collapseWhitespace: true, removeAttributeQuotes: true, removeComments: true }, isBrowser: false, isDevelopment: false }), new webpack.DefinePlugin({ 'process.type': '"renderer"' }) ] }; export default merge(baseConfig, configuration); ================================================ FILE: .erb/configs/webpack.paths.ts ================================================ const path = require('path'); const rootPath = path.join(__dirname, '../..'); const erbPath = path.join(__dirname, '..'); const erbNodeModulesPath = path.join(erbPath, 'node_modules'); const dllPath = path.join(__dirname, '../dll'); const srcPath = path.join(rootPath, 'src'); const srcMainPath = path.join(srcPath, 'main'); const srcRendererPath = path.join(srcPath, 'renderer'); const releasePath = path.join(rootPath, 'release'); const appPath = path.join(releasePath, 'app'); const appPackagePath = path.join(appPath, 'package.json'); const appNodeModulesPath = path.join(appPath, 'node_modules'); const srcNodeModulesPath = path.join(srcPath, 'node_modules'); const distPath = path.join(appPath, 'dist'); const distMainPath = path.join(distPath, 'main'); const distRendererPath = path.join(distPath, 'renderer'); const buildPath = path.join(releasePath, 'build'); export default { rootPath, erbNodeModulesPath, dllPath, srcPath, srcMainPath, srcRendererPath, releasePath, appPath, appPackagePath, appNodeModulesPath, srcNodeModulesPath, distPath, distMainPath, distRendererPath, buildPath }; ================================================ FILE: .erb/mocks/fileMock.js ================================================ export default 'test-file-stub'; ================================================ FILE: .erb/scripts/.eslintrc ================================================ { "rules": { "no-console": "off", "global-require": "off", "import/no-dynamic-require": "off", "import/no-extraneous-dependencies": "off" } } ================================================ FILE: .erb/scripts/check-build-exists.ts ================================================ // Check if the renderer and main bundles are built import path from 'path'; import chalk from 'chalk'; import fs from 'fs'; import webpackPaths from '../configs/webpack.paths'; const mainPath = path.join(webpackPaths.distMainPath, 'main.js'); const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js'); if (!fs.existsSync(mainPath)) { throw new Error( chalk.whiteBright.bgRed.bold( 'The main process is not built yet. Build it by running "npm run build:main"' ) ); } if (!fs.existsSync(rendererPath)) { throw new Error( chalk.whiteBright.bgRed.bold( 'The renderer process is not built yet. Build it by running "npm run build:renderer"' ) ); } ================================================ FILE: .erb/scripts/check-native-dep.js ================================================ import fs from 'fs'; import chalk from 'chalk'; import { execSync } from 'child_process'; import { dependencies } from '../../package.json'; if (dependencies) { const dependenciesKeys = Object.keys(dependencies); const nativeDeps = fs .readdirSync('node_modules') .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); if (nativeDeps.length === 0) { process.exit(0); } try { // Find the reason for why the dependency is installed. If it is installed // because of a devDependency then that is okay. Warn when it is installed // because of a dependency const { dependencies: dependenciesObject } = JSON.parse( execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString() ); const rootDependencies = Object.keys(dependenciesObject); const filteredRootDependencies = rootDependencies.filter((rootDependency) => dependenciesKeys.includes(rootDependency) ); if (filteredRootDependencies.length > 0) { const plural = filteredRootDependencies.length > 1; console.log(` ${chalk.whiteBright.bgYellow.bold('Webpack does not work with native dependencies.')} ${chalk.bold(filteredRootDependencies.join(', '))} ${ plural ? 'are native dependencies' : 'is a native dependency' } and should be installed inside of the "./release/app" folder. First, uninstall the packages from "./package.json": ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} ${chalk.bold('Then, instead of installing the package to the root "./package.json":')} ${chalk.whiteBright.bgRed.bold('npm install your-package')} ${chalk.bold('Install the package to "./release/app/package.json"')} ${chalk.whiteBright.bgGreen.bold('cd ./release/app && npm install your-package')} Read more about native dependencies at: ${chalk.bold( 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure' )} `); process.exit(1); } } catch (e) { console.log('Native dependencies could not be checked'); } } ================================================ FILE: .erb/scripts/check-node-env.js ================================================ import chalk from 'chalk'; export default function checkNodeEnv(expectedEnv) { if (!expectedEnv) { throw new Error('"expectedEnv" not set'); } if (process.env.NODE_ENV !== expectedEnv) { console.log( chalk.whiteBright.bgRed.bold( `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` ) ); process.exit(2); } } ================================================ FILE: .erb/scripts/check-port-in-use.js ================================================ import chalk from 'chalk'; import detectPort from 'detect-port'; const port = process.env.PORT || '1212'; detectPort(port, (_err, availablePort) => { if (port !== String(availablePort)) { throw new Error( chalk.whiteBright.bgRed.bold( `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start` ) ); } else { process.exit(0); } }); ================================================ FILE: .erb/scripts/clean.js ================================================ import { rimrafSync } from 'rimraf'; import fs from 'fs'; import webpackPaths from '../configs/webpack.paths'; const foldersToRemove = [webpackPaths.distPath, webpackPaths.buildPath, webpackPaths.dllPath]; foldersToRemove.forEach((folder) => { if (fs.existsSync(folder)) rimrafSync(folder); }); ================================================ FILE: .erb/scripts/delete-source-maps.js ================================================ import fs from 'fs'; import path from 'path'; import { rimrafSync } from 'rimraf'; import webpackPaths from '../configs/webpack.paths'; export default function deleteSourceMaps() { if (fs.existsSync(webpackPaths.distMainPath)) rimrafSync(path.join(webpackPaths.distMainPath, '*.js.map'), { glob: true }); if (fs.existsSync(webpackPaths.distRendererPath)) rimrafSync(path.join(webpackPaths.distRendererPath, '*.js.map'), { glob: true }); } ================================================ FILE: .erb/scripts/electron-rebuild.js ================================================ import { execSync } from 'child_process'; import fs from 'fs'; import { dependencies } from '../../release/app/package.json'; import webpackPaths from '../configs/webpack.paths'; if (Object.keys(dependencies || {}).length > 0 && fs.existsSync(webpackPaths.appNodeModulesPath)) { const electronRebuildCmd = '../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .'; const cmd = process.platform === 'win32' ? electronRebuildCmd.replace(/\//g, '\\') : electronRebuildCmd; execSync(cmd, { cwd: webpackPaths.appPath, stdio: 'inherit' }); } ================================================ FILE: .erb/scripts/link-modules.ts ================================================ import fs from 'fs'; import webpackPaths from '../configs/webpack.paths'; const { srcNodeModulesPath, appNodeModulesPath, erbNodeModulesPath } = webpackPaths; if (fs.existsSync(appNodeModulesPath)) { if (!fs.existsSync(srcNodeModulesPath)) { fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction'); } if (!fs.existsSync(erbNodeModulesPath)) { fs.symlinkSync(appNodeModulesPath, erbNodeModulesPath, 'junction'); } } ================================================ FILE: .erb/scripts/notarize.js ================================================ const { notarize } = require('@electron/notarize'); const { build } = require('../../package.json'); exports.default = async function notarizeMacos(context) { const { electronPlatformName, appOutDir } = context; if (electronPlatformName !== 'darwin') { return; } if (process.env.CI !== 'true') { console.warn('Skipping notarizing step. Packaging is not running in CI'); return; } if (!('APPLE_ID' in process.env && 'APPLE_APP_SPECIFIC_PASSWORD' in process.env)) { console.warn( 'Skipping notarizing step. APPLE_ID and APPLE_APP_SPECIFIC_PASSWORD env variables must be set' ); return; } const appName = context.packager.appInfo.productFilename; await notarize({ appBundleId: build.appId, appPath: `${appOutDir}/${appName}.app`, appleId: process.env.APPLE_ID, appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD }); }; ================================================ FILE: .eslintignore ================================================ # Logs logs *.log # Runtime data pids *.pid *.seed # Coverage directory used by tools like istanbul coverage .eslintcache # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules # OSX .DS_Store release/app/dist release/build .erb/dll .idea npm-debug.log.* *.css.d.ts *.sass.d.ts *.scss.d.ts # eslint ignores hidden directories by default: # https://github.com/eslint/eslint/issues/8429 !.erb ================================================ FILE: .eslintrc.js ================================================ module.exports = { extends: 'erb', plugins: ['@typescript-eslint'], rules: { // A temporary hack related to IDE not resolving correct package.json 'import/no-extraneous-dependencies': 'off', 'react/react-in-jsx-scope': 'off', 'react/jsx-filename-extension': 'off', 'import/extensions': 'off', 'import/no-unresolved': 'off', 'import/no-import-module-exports': 'off', 'no-shadow': 'off', '@typescript-eslint/no-shadow': 'error', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'warn', 'no-nested-ternary': 'off', 'jsx-a11y/label-has-associated-control': 'warn', 'no-console': 'off', 'import/prefer-default-export': 'off', 'prefer-destructuring': 'off', 'prettier/prettier': 'off', 'react/jsx-no-useless-fragment': 'off', 'react/jsx-curly-brace-presence': 'off', 'jsx-a11y/click-events-have-key-events': 'warn', 'jsx-a11y/no-static-element-interactions': 'warn', 'react/button-has-type': 'off', 'prefer-template': 'off', 'no-param-reassign': 'off', 'no-lonely-if': 'off', 'no-plusplus': 'off', 'promise/always-return': 'off', 'class-methods-use-this': 'off', 'react-hooks/exhaustive-deps': 'warn', 'object-shorthand': 'off', 'import/no-cycle': 'off', 'promise/catch-or-return': 'off', 'import/order': 'warn', 'spaced-comment': 'off', 'react/jsx-boolean-value': 'off', 'react/require-default-props': 'off', 'no-unneeded-ternary': 'off', 'consistent-return': 'off', 'no-async-promise-executor': 'off', 'no-else-return': 'off', 'func-names': 'off', 'prefer-promise-reject-errors': 'off', 'react/function-component-definition': 'off' }, parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, settings: { 'import/resolver': { // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below node: {}, webpack: { config: require.resolve('./.erb/configs/webpack.config.eslint.ts') }, typescript: {} }, 'import/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'] } } }; ================================================ FILE: .gitattributes ================================================ * text eol=lf *.exe binary *.png binary *.jpg binary *.jpeg binary *.ico binary *.icns binary *.eot binary *.otf binary *.ttf binary *.woff binary *.woff2 binary ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ name: Bug Report description: Report a bug encountered while using Oblivion Desktop body: - type: markdown attributes: value: | Please make sure to provide a descriptive report. It saves time for both the developers and users who are looking for solutions. Providing as much information as possible, including screenshots and logs, is highly appreciated. This will help us to better understand the issue and respond more effectively. لطفا مطمئن شوید که گزارشی تشریحی ارائه دهید. این باعث می‌شود که توسعه‌دهندگان و کاربرانی که در جستجوی راهکارها هستند، زمان کمتری صرف کنند. ارائه اطلاعات بیشتر از جمله اسکرین‌شات و لاگ توصیه می‌شود. این باعث می‌شود که ما بهتر بتوانیم مشکل را درک کرده و به‌صورت بهتر پاسخ دهیم. - type: checkboxes id: confirm-search attributes: label: Attention | توجه description: Please search [existing issues](https://github.com/bepass-org/oblivion-desktop/issues) before reporting | لطفا قبل از ارسال گزارش [ایشوهای موجود](https://github.com/bepass-org/oblivion-desktop/issues) را بررسی کنید. options: - label: I searched and no similar issues were found | جستجو کردم و هیچ گزارش مشابهی پیدا نشد required: true - type: textarea id: problem attributes: label: What Happened? | چه اتفاقی افتاده؟ description: | Please provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. لطفاً اطلاعات کاملی را ارائه دهید. اگر این کار را انجام ندهید، ممکن است مشکل شما به صورت تمام شده تلقی شده یا در زمان مناسبی بررسی نشود. validations: required: true - type: textarea id: reproduce attributes: label: Minimal Reproducible Example | چه پروسه‌ای برای مشاهده این مشکل طی کرده‌اید؟ placeholder: | 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error - type: textarea id: logs attributes: label: Relevant log output | لاگ برنامه description: Refer to the program menu and copy the log and send it to us. render: shell validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Wiki url: https://github.com/bepass-org/oblivion-desktop/wiki about: A set of guidelines for using the application and troubleshooting issues. - name: FAQ url: https://github.com/bepass-org/oblivion-desktop/blob/main/FAQ.md about: Before asking a question or presenting a problem, read through the frequently asked questions. - name: In Progress url: https://github.com/orgs/bepass-org/projects/4/views/1 about: a List of work in progress & items planned for development ... ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yaml ================================================ name: Feature Request description: Request a new feature labels: ['enhancement'] body: - type: markdown attributes: value: | Here is a template you can use to share your idea. اینجا می‌توانید ایده خود به اشتراک بگذارید. - type: textarea attributes: label: Feature description | توضیحات ویژگی description: Please provide a clear and concise description of what you want to happen and what problem will this solve | لطفاً توضیحات روشن و کوتاهی از آنچه که می‌خواهید رخ دهد و مشکلی که پیش می‌آید را حل کنید، ارائه دهید. validations: required: true ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Change Description What does this change add to or fix in the project? ### Checklist - [ ] Code has been tested. - [ ] Windows 10 - [ ] Windows 11 - [ ] macOS - [ ] Linux - [ ] Relevant documentation has been updated. ### Related Links Link to any related issues. Example: Closes #123 ================================================ FILE: .github/config.yml ================================================ requiredHeaders: - Prerequisites - Expected Behavior - Current Behavior - Possible Solution - Your Environment ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: 'npm' directory: '/' schedule: interval: 'monthly' ignore: - dependency-name: '*' update-types: - 'version-update:semver-major' ================================================ FILE: .github/release_message.md ================================================ ## Pre-release RELEASE_TAG has been released for Windows, Linux & macOS. ### Some software changes: - [x] Fixed some minor bugs #### To report issues or provide suggestions: https://github.com/bepass-org/oblivion-desktop/issues
## نسخه پیش‌ازانتشار RELEASE_TAG برای ویندوز، لینوکس و مک منتشر شد. ### برخی‌از تغییرات برنامه: - [x] رفع برخی‌از اشکالات جزئی #### گزارش مشکل یا ارائه پیشنهاد: https://github.com/bepass-org/oblivion-desktop/issues

## Download
OS / Arch Compatibility


10+

10.15+

Gnome
KDE
6+
================================================ FILE: .github/stale.yml ================================================ # Number of days of inactivity before an issue becomes stale daysUntilStale: 60 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - discussion - security # Label to use when marking an issue as stale staleLabel: wontfix # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false ================================================ FILE: .github/workflows/deploy-ppa.yml ================================================ name: Build and Upload to Launchpad PPA on: workflow_dispatch: {} jobs: build-and-upload: runs-on: ubuntu-latest env: GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} LAUNCHPAD_USERNAME: ${{ secrets.LAUNCHPAD_USERNAME }} LAUNCHPAD_PASSWORD: ${{ secrets.LAUNCHPAD_PASSWORD }} steps: - name: Checkout repository uses: actions/checkout@v3 - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y jq devscripts debhelper dh-make dput gnupg lintian build-essential - name: Extract package name and version from package.json id: extract run: | echo "PACKAGE_NAME=$(jq -r '.name' package.json)" >> $GITHUB_ENV echo "VERSION=$(jq -r '.version' package.json)" >> $GITHUB_ENV - name: Prepare Debian packaging directory run: | rm -rf ${PACKAGE_NAME}-${VERSION} mkdir ${PACKAGE_NAME}-${VERSION} cp -r . ${PACKAGE_NAME}-${VERSION}/ cd ${PACKAGE_NAME}-${VERSION} dh_make -s -y -p ${PACKAGE_NAME}_${VERSION} rm -f debian/*.ex debian/*.EX debian/README.* printf "%s\n" "${PACKAGE_NAME} (${VERSION}) unstable; urgency=medium" "" " * Initial release." "" " -- Oblivion Desktop <$LAUNCHPAD_USERNAME> $(date -R)" > debian/changelog - name: Setup GPG for signing commits and packages run: | echo "${{ secrets.GPG_PRIVATE_KEY }}" > private.key gpg --batch --import private.key git config --global user.signingkey "$GPG_KEY_ID" git config --global commit.gpgsign true git config --global user.name "Oblivion Desktop" git config --global user.email "$LAUNCHPAD_USERNAME" - name: Build source package run: | cd ${PACKAGE_NAME}-${VERSION} debuild -S -sa -k"$GPG_KEY_ID" - name: Sign changes and dsc files run: | cd .. gpg --batch --yes -u "$GPG_KEY_ID" --armor --detach-sign ${PACKAGE_NAME}_${VERSION}_source.changes gpg --batch --yes -u "$GPG_KEY_ID" --armor --detach-sign ${PACKAGE_NAME}_${VERSION}.dsc - name: Upload source package to Launchpad PPA run: | dput ppa ${PACKAGE_NAME}_${VERSION}_source.changes ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish permissions: write-all on: push: tags: - '*' jobs: create-release: runs-on: ubuntu-22.04 steps: - name: Checkout git repo uses: actions/checkout@v4 - name: Prepare Release Message run: | sed 's|RELEASE_TAG|${{ github.ref_name }}|g' ./.github/release_message.md >> release.md - name: Set Release Message uses: softprops/action-gh-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref_name }} body_path: './release.md' draft: true publish: needs: create-release runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-22.04, ubuntu-24.04, windows-latest, macos-13, macos-14] steps: - name: Checkout git repo uses: actions/checkout@v4 - name: Install Node and NPM uses: actions/setup-node@v4 with: node-version: 20 cache: npm - name: Install and build run: | npm install if [ "$RUNNER_OS" == "macOS" ]; then npm install dmg-license fi npm run build shell: bash - name: Publish releases mac x64 if: matrix.os == 'macos-13' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm exec electron-builder -- --mac dmg zip --x64 --publish onTagOrDraft - name: Publish releases mac arm64 if: matrix.os == 'macos-14' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm exec electron-builder -- --mac dmg zip --arm64 --publish onTagOrDraft - name: Publish releases windows if: matrix.os == 'windows-latest' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm exec electron-builder -- --publish onTagOrDraft - name: Publish releases GNU Linux x64 if: matrix.os == 'ubuntu-22.04' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm exec electron-builder -- --linux deb rpm tar.xz AppImage --x64 --publish onTagOrDraft - name: Publish releases GNU Linux arm64 if: matrix.os == 'ubuntu-24.04' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm exec electron-builder -- --linux deb rpm tar.xz AppImage --arm64 --publish onTagOrDraft - name: Delete unwanted files uses: mknejp/delete-release-assets@v1 with: token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ github.ref_name }} fail-if-no-assets: false assets: | oblivion-desktop-win.exe oblivion-desktop-mac-arm64.zip.blockmap oblivion-desktop-mac-x64.zip.blockmap latest-linux-arm64.yml latest-linux.yml latest-mac.yml latest.yml oblivion-desktop-mac-arm64.dmg.blockmap oblivion-desktop-mac-x64.dmg.blockmap ================================================ FILE: .gitignore ================================================ # Logs logs *.log # Runtime data pids *.pid *.seed # Coverage directory used by tools like istanbul coverage .eslintcache # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules # OSX .DS_Store release/app/dist release/build .erb/dll .idea npm-debug.log.* *.css.d.ts *.sass.d.ts *.scss.d.ts *.tsbuildinfo log.txt warp-plus*.zip masque-plus*.zip assets/bin stuff ca.psiphon.PsiphonTunnel.tunnel-core oblivion-helper*.zip zag-netStats*.zip proxy-reset*.zip ================================================ FILE: .husky/pre-commit ================================================ ================================================ FILE: .husky/pre-push ================================================ ================================================ FILE: .prettierignore ================================================ .erb .vscode node_modules .git # Logs logs *.log # Runtime data pids *.pid *.seed # Coverage directory used by tools like istanbul coverage .eslintcache # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules # OSX .DS_Store release/app/dist release/build .github/release_message.md .erb/dll .idea npm-debug.log.* *.css.d.ts *.sass.d.ts *.scss.d.ts # eslint ignores hidden directories by default: # https://github.com/eslint/eslint/issues/8429 !.erb bin README.md stuff assets/* !assets/css ================================================ FILE: .prettierrc ================================================ { "tabWidth": 4, "useTabs": false, "singleQuote": true, "jsxSingleQuote": true, "printWidth": 100, "trailingComma": "none" } ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "name": "Electron: Main", "type": "node", "request": "launch", "protocol": "inspector", "runtimeExecutable": "npm", "runtimeArgs": ["run", "start"], "env": { "MAIN_ARGS": "--inspect=5858 --remote-debugging-port=9223" } }, { "name": "Electron: Renderer", "type": "chrome", "request": "attach", "port": 9223, "webRoot": "${workspaceFolder}", "timeout": 15000 } ], "compounds": [ { "name": "Electron: All", "configurations": ["Electron: Main", "Electron: Renderer"] } ] } ================================================ FILE: .vscode/settings.json ================================================ { "files.associations": { ".eslintrc": "jsonc", ".prettierrc": "jsonc", ".eslintignore": "ignore" }, "eslint.validate": [ "javascript", "javascriptreact", "html", "typescriptreact" ], "javascript.validate.enable": false, "javascript.format.enable": false, "typescript.format.enable": false, "search.exclude": { ".git": true, ".eslintcache": true, ".erb/dll": true, "release/{build,app/dist}": true, "node_modules": true, "npm-debug.log.*": true, "test/**/__snapshots__": true, "package-lock.json": true, "*.{css,sass,scss}.d.ts": true }, "cSpell.words": [ "asar", "Authmode", "bepass", "cfon", "electronmon", "fullscreenable", "Ghobadi", "gool", "gsettings", "hiddify", "HKCU", "ircf", "ircfspace", "kiomarzsss", "kioslaverc", "kwriteconfig", "networksetup", "nsis", "Pashmfouroush", "plusplus", "psiphon", "regedit", "shabnam", "webm", "webp", "wintun", "worktree", "Yousef", "zustand", "اتصال", "اجرا", "اعمال", "اندپوینت", "برنامه", "پروکسی", "پشتیبانی", "پورت", "پیکربندی", "تلاش", "توسط", "حاضر", "حالت", "دارد", "دسکتاپ", "دیگری", "روبرو", "سنجی", "صورت", "فایل", "کلودفلر", "لازم", "لایسنس", "متصل", "متوجه", "مجدد", "مجدداً", "محیط", "مواجه", "نیاز", "نیازمند", "نیست", "هویت", "وارد", "یافت" ], "editor.formatOnSave": false, "yaml.validate": false } ================================================ FILE: @types/node-aplay.d.ts ================================================ declare module 'node-aplay' { export default class Aplay { constructor(file: string); play(): void; pause(): void; resume(): void; stop(): void; on(event: 'complete' | 'error', listener: (...args: any[]) => void): this; } } ================================================ FILE: CHANGELOG.md ================================================ ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at ircfspace@gmail.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # CONTRIBUTION GUIDE Thank you for your interest in contributing to Oblivion Desktop! We appreciate your help and support. - We have tried our best to provide some documentation on how to contribute to this project; here are some guidelines: ## Where you can help? - ### 💻 Contributing to the development if you have programming skills that can be helpful: Pick up issues/tasks from [issues page](https://github.com/bepass-org/oblivion-desktop/issues) or the [project board](https://github.com/orgs/bepass-org/projects/4) or suggest your own features, fixes, etc. Before starting a new task, please coordinate with the collaborators to ensure no one else is working on it. - #### Setup your development environment - Fork this [repository](https://github.com/bepass-org/oblivion-desktop) - Follow the instructions in the [DOCS.md](DOCS.md) to set up your development environment and bring up your development server - Make your changes and create a pull request (PR) - #### Code Style Guide To maintain code quality, ensure your PR follows our TypeScript style guidelines and passes type checking (`tsc`) - we use Prettier (2 spaces, single quotes) for consistent formatting and Husky hooks (automatically installed via `npm install` or manually with `npm run prepare`) to run `prettier` on commit and type checking on push, while you can manually fix style issues with `npm run format`. - ### 🌐 Translating to other languages The current translation is done from English to other languages. Persian and English are translated manually, while other languages are translated with the help of AI. Your contributions to improving the translation and providing feedback are welcome. - We prioritize translations for languages spoken in regions with systemic internet censorship that broadly restricts access to information, similar to the digital environments in Iran, China, and Russia. Our program focuses on supporting communities where: (a) Internet restrictions disproportionately impacts general freedoms of expression and access to information (b) Censorship extends beyond targeted law enforcement against illegal activities. For countries where online restrictions are primarily applied to combat criminal content (e.g., child exploitation materials, terrorist propaganda, or hate speech), English remains the supported interface language. This allows us to concentrate our limited resources where they can have the greatest impact in promoting open access to information. We evaluate all translation requests against these criteria to ensure alignment with our mission of fighting unjust censorship. - If you want to edit the translated text in this application, you can directly edit the files in the [src/localization](src/localization) directory. - If you're interested in translating markdown (.md) files for new languages, simply create the language file next to the original file. (We will take care of the directory structure if needed.) - ### 💬 Discussions If you have any questions, suggestions, complaints, or concerns that are not addressed in our document(s), please feel free to open an [issue](https://github.com/bepass-org/oblivion-desktop/issues) or directory contact us as we are willing to discuss about this project at any time. ### ⚠️ Warning! We recommend not to use your real legal name while contributing to this project; it may find you trouble in many countries that have problems with free circulation of information. ================================================ FILE: DOCS.md ================================================ # DEVELOPER DOCUMENTATION Oblivion Desktop is an [Electron](https://www.electronjs.org/) project bootstrapped with [Electron React Boilerplate. ](https://github.com/electron-react-boilerplate/electron-react-boilerplate) In a nutshell, Oblivion Desktop is a GUI program that interacts with "[WARP-Plus](https://github.com/bepass-org/warp-plus/)"'s binary executable and changes the system's proxy settings. ## Getting Started 1. Make sure you have [Node.js](https://nodejs.org/) and [NPM](https://www.npmjs.com/) installed on your system. 2. Clone this repository (`$ git clone https://github.com/bepass-org/oblivion-desktop.git`) 3. Install the program's dependencies: ```shell npm install ``` 4. Run the development server: ```shell npm run dev # or npm start ``` ## Packaging for Production (build from the source) To package for your local platform: ```shell npm run package ``` for faster production build (test purposes) use one of the following: ```shell npm run package:linux npm run package:windows npm run package:mac ``` When the command(s) finish, you are to have your production build(s) at `release/build`! for more specific builds checkout: https://www.electron.build/cli For more specific builds, take a look at [this](https://electron.build/cli)! ## IPC (sending data between main and renderer) as you may be familiar with electron already. As you are probably familiar with [Electron](https://electron.build/) already; We need to use [IPC](https://www.electronjs.org/docs/latest/tutorial/ipc) in order to send and receive data between main and renderer. Take a loot at `src/main/ipc.ts` and `src/renderer/index.tsx` for an in-action example. # Codebase Terminology For clarity when working with the TypeScript codebase: ### Dependencies After Warp-Plus (wp) updates, always refresh dependencies: ```bash npm install Code Abbreviations ``` wp: WARP-Plus module (Cloudflare integration) od: Oblivion Desktop core functionality hp: OblivionHelper utility package TypeScript Conventions - All abbreviations should be typed explicitly: ```ts interface WpConfig { /* Warp-plus settings */ } type OdState = /* OblivionDesktop state */; Avoid inline abbreviations - use proper type aliases ``` - Document abbreviations in JSDoc: ```ts /** @param wpConfig - Warp-plus configuration object */ Maintenance; ``` The project uses: Strict TypeScript (strict: true) Consistent ESLint rules Pre-commit type checking## Notes - (After WP updates;) to get the latest WP version, that app is using. run: `npm i`. - `wp` refers to `warp-plus` in the source code. - `od` refers to `oblivion desktop` in the source code. - `hp` refers to `oblivion helper` in the source code. Note: On Linux/MacOS, configuration files for the program are in `~/.config/oblivion-desktop` (that's `/home/user/.config/oblivion-desktop` as realpath) - **do not touch these files.** Happy hacking! 😉 ================================================ FILE: FAQ.md ================================================ # سوالات متداول - [کاربرد اپ Oblivion Desktop چیه؟](#کاربرد-اپ-oblivion-desktop-چیه) - [این برنامه چطور کار می‌کنه؟](#این-برنامه-چطور-کار-میکنه) - [چرا گاهی اوقات IP ایران نمایش داده می‌شه؟](#چرا-گاهی-اوقات-ip-ایران-نمایش-داده-میشه) - [برای تغییر IP وارپ چکار کنم؟](#برای-تغییر-ip-وارپ-چکار-کنم) - [تفاوت اپ Warp با Warp-Plus و Oblivion چیست؟](#تفاوت-اپ-warp-با-warp-plus-و-oblivion-چیست) - [سازنده اصلی WarpInWarp کجاست؟](#سازنده-اصلی-warpinwarp-کجاست) - [آیا Oblivion امنه؟](#آیا-oblivion-امنه) - [آیا سرویس وارپ کلودفلر امنه؟](#آیا-سرویس-وارپ-کلودفلر-امنه) - [هدف از این‌سرویس رایگان چیه؟](#هدف-از-این‌سرویس-رایگان-چیه) # Frequently Asked Questions - [What is the purpose of the Oblivion Desktop app?](#what-is-the-purpose-of-the-oblivion-desktop-app) - [How does this application work?](#how-does-this-application-work) - [Why does my IP sometimes show as Iranian?](#why-does-my-ip-sometimes-show-as-iranian) - [How can I change my Warp IP?](#how-can-i-change-my-warp-ip) - [Where is the original creator of WarpInWarp?](#where-is-the-original-creator-of-warpinwarp) - [Is Oblivion secure?](#is-oblivion-secure) - [Is the Warp Cloudflare service secure?](#is-the-warp-cloudflare-service-secure) - [What is the aim of this free service?](#what-is-the-aim-of-this-free-service) ## کاربرد اپ Oblivion Desktop چیه؟ این‌برنامه یک نسخه غیررسمی، اما قابل اطمینان از اپ Oblivion یا فراموشی است که برای ویندوز، لینوکس و مک ارائه گردیده است.
برنامه Oblivion Desktop با الگو گرفتن از رابط کاربری نسخه اصلی که توسط یوسف قبادی برنامه‌نویسی شده بود، با هدف دسترسی آزاد به اینترنت تهیه گردیده است. ## این برنامه چطور کار می‌کنه؟ اپ Oblivion Desktop عملاً یک VPN امن و مطمئن برای عبور از فیلترینگ و دورزدن تحریم‌ها هست، که از هسته Warp-Plus استفاده می‌کنه. ## چرا گاهی اوقات IP ایران نمایش داده می‌شه؟ آی‌پی مذکور، IP اصلی کاربر نبوده و سرویس Warp کلودفلر متناسب با لوکیشن کاربر یکی‌از آی‌پی‌های خودش رو برگشت میده و لوکیشن ایران رو براش جا می‌زنه، که عملن در این‌حالت نمی‌شه از سد تحریم‌ها گذشت. ## برای تغییر IP وارپ چکار کنم؟ متدهای گول و سایفون در تنظیمات برنامه این‌امکان رو به‌کاربر میدن که بتونه IP خودش رو به لوکیشنی غیراز ایران تغییر بده و امتحان‌کردن این‌گزینه‌ها توی شرایط پراختلال اینترنت توصیه می‌شه. ## تفاوت اپ Warp با Warp-Plus و Oblivion چیست؟ اپ Oblivion یا هسته Warp-Plus با استفاده از سرویس وارپ کلودفلر کار کرده، اما برتری Warp-Plus و نرم‌افزار Oblivion استفاده از متدهای گول و سایفون است، که امکان تغییر آی‌پی را برای کاربر فراهم می‌کنند. ## سازنده اصلی WarpInWarp کجاست؟ از وضعیت #یوسف_قبادی که چندماه قبل بازداشت شده‌بود اطلاعی در دسترس نیست. البته درحال‌حاضر مسیر توسعه Oblivion و Warp-Plus توسط سایر توسعه‌دهندگان به پیش میره. ## آیا Oblivion امنه؟ اپ‌های Oblivion و Oblivion Desktop و همینطور هسته Warp-Plus متن‌باز هستن و سورسشون در گیت‌هاب قرار گرفته، ضمن اینکه بیلد پروژه‌ها هم به‌صورت خودکار توسط گیت‌هاب اکشن انجام می‌شن. ## آیا سرویس وارپ کلودفلر امنه؟ وارپ به‌وسیله رمزنگاری مقدار بیشتری از ترافیک خروجی از دستگاهتون، اجازه نمیده هیچ‌کس در کار شما سرک بکشه. ## هدف از این‌سرویس رایگان چیه؟ تحقق شعار اینترنت برای همه یا هیچ‌کس؛ و همینطور دسترسی آزاد مردم، به‌خصوص قشر کم‌برخوردار به اینترنت. ## What is the purpose of the Oblivion Desktop app? This application is an unofficial yet reliable version of the Oblivion or forgetfulness app provided for Windows, Linux, and MacOS. The Oblivion Desktop program is designed with the purpose of providing unrestricted access to the internet by adopting the user interface pattern of the original version developed by Youssef Ghobadi. ## How does this application work? The Oblivion Desktop app essentially functions as a secure VPN for bypassing filtering and circumventing sanctions, utilizing the WARP-Plus core. ## Why does my IP sometimes show as Iranian? The displayed IP is not the user's actual IP; rather, the Cloudflare WARP service sometimes assigns one of its own IPs to correspond with the user's location and may display Iran as the location. In such cases, it's practically impossible to bypass sanctions. ## How can I change my WARP IP? Methods such as Gool and Psiphon in the application settings provide users with the capability to change their IP to a location other than Iran. It is recommended to explore these options, especially in situations of internet disruption. ## Where is the original creator of WarpInWarp? There is currently no information available regarding the status of Youssef Ghobadi, who was detained several months ago. However, development of Oblivion and Warp-Plus is currently progressing under the guidance of other developers. ## Is Oblivion secure? Both the Oblivion and Oblivion Desktop programs, as well as the WARP-Plus core, are open-source and their source code is available on GitHub. Additionally, project builds are automatically performed by GitHub Actions. ## Is the WARP Cloudflare service secure? WARP encrypts a larger amount of outgoing traffic from your device, preventing anyone from interfering with your work. ## What is the purpose of this program? To realize the slogan of "Internet for all or for none," as well as to provide free access to the internet, especially for underserved populations. ================================================ FILE: LICENSE.md ================================================ # Restrictive License - No Sale and Modification Limitation Copyright © 2025 Oblivion Desktop This software is provided under a specific license. by using this software or accessing its source code, you agree that: - No Sale: You are not allowed to sell or distribute modified versions of it. - Modification Limitation: You may not alter the name, about page, copyright notice, logo, or other fundamental details of this software. However, distribution of the software in binary form is allowed, provided the original license and copyright notice remain intact. Development of the software is only possible through project participation. --- # لایسنس محدود - عدم امکان فروش و محدودیت تغییرات کپی‌رایت © ۱۴۰۴ Oblivion Desktop این نرم‌افزار طبق یک لایسنس خاص ارائه می‌شود. با استفاده از این نرم‌افزار یا دسترسی به منابع آن، شما موافقت می‌کنید که: - عدم امکان فروش: شما مجاز به فروش یا ارائه نسخه‌های تغییر یافته آن نیستید. - محدودیت تغییرات: شما نمی‌توانید نام، صفحه درباره، اطلاعات کپی‌رایت، لوگو یا جزئیات بنیادین دیگر این نرم‌افزار را تغییر دهید. با این حال، توزیع نرم‌افزار به صورت باینری مجاز است، به شرطی که مجوز اصلی و اطلاعیه کپی‌رایت دست‌نخورده باقی بماند. توسعه این نرم‌افزار تنها از طریق مشارکت در پروژه امکان‌پذیر است. ================================================ FILE: README-fa.md ================================================

Oblivion Desktop

English English | Persian فارسی

اوبلیویون دسترسی امن و بهینه به اینترنت را از طریق یک اپلیکیشن کاربرپسند برای ویندوز، مک و لینوکس با استفاده از فناوری Cloudflare Warp فراهم می‌کند. > نسخه‌ی غیررسمی از [Oblivion](https://github.com/bepass-org/oblivion) "اینترنت, برای همه یا هیچکس!"

Version Downloads Stars License

oblivion.png

## ویژگی‌ها - **وی‌پی‌ان امن**: پیاده‌سازی اختصاصی WireGuard با زبان Go. - **متن‌باز**: به‌صورت اپن‌سورس، با تأکید بر شفافیت و مشارکت اجتماعی؛ ضمن استفاده از گیت‌هاب اکشن برای بیلد خودکار. - **کاربرپسند**: رابط کاربری ساده و آسان.

oblivion.jpg

## بررسی سریع
ویژگی وضعیت
روش :white_check_mark: وارپ
:white_check_mark: گول
:white_check_mark: سیفون (سایفون)
:white_check_mark: مسک
پیکربندی شبکه :white_check_mark: پروکسی (بدون تغییر)
:white_check_mark: پروکسی سیستم (با PAC)
:white_check_mark: TUN (با سینگ‌باکس)
قوانین مسیریابی :white_check_mark: پروکسی سیستم
:white_check_mark: دیتابیس Geo
نوار سیستم :white_check_mark: کمینه‌کردن
:white_check_mark: راه‌اندازی خودکار
:white_check_mark: میانبرها
زبان‌ها :white_check_mark: فارسی
:white_check_mark: انگلیسی
:white_check_mark: چینی
:white_check_mark: روسی
:white_check_mark: ترکی
:white_check_mark: اندونزیایی
:white_check_mark: عربی
:white_check_mark: پرتغالی
:white_check_mark: ویتنامی
:white_check_mark: اردو
:white_check_mark: اسپانیایی
:white_check_mark: برمه‌ای
:white_check_mark: امهری
پوسته :white_check_mark: روشن
:white_check_mark: تاریک
:white_check_mark: راست‌چین
:white_check_mark: چپ‌چین
:white_check_mark: خودکار
سایر :white_check_mark: اسکنر
:white_check_mark: پینگ
:white_check_mark: دسترس‌پذیری
:white_check_mark: تست سرعت
:white_check_mark: بروزرسانی داخلی (ویندوز)
:white_large_square: قطع اضطراری
## دانلود
سیستم‌عامل / معماری سازگاری


10+

10.15+

گنوم (جی‌ستینگز)
کی‌دی‌ای (کیو)
جی‌لیبیسی (glibc)
6+
## به مشکل برخوردید 🐞؟ قسمت [ویکی](https://github.com/bepass-org/oblivion-desktop/wiki) را بررسی کرده و در [مشکلات](https://github.com/bepass-org/oblivion-desktop/issues) (باز و [بسته](https://github.com/bepass-org/oblivion-desktop/issues?q=is%3Aissue+is%3Aclosed)!) جستجو کنید؛ اگر پاسخ خود را پیدا نکردید، [یک مشکل جدید](https://github.com/bepass-org/oblivion-desktop/issues/new/choose) گزارش کنید. ## مشارکت کنید ما یک پروژه جامعه‌محور هستیم که هدف آن دسترسی همه به اینترنت است. چه بخواهید کد بزنید، چه ویژگی‌های جدید پیشنهاد دهید یا نیاز به کمک داشته باشید، خوشحال می‌شویم از شما بشنویم! برای اطلاعات بیشتر به [ایشوهای GitHub](https://github.com/bepass-org/oblivion-desktop/issues)، [راهنمای مشارکت](CONTRIBUTING.md) و [مستندات توسعه‌دهندگان](DOCS.md) مراجعه کنید. [![Stargazers over time](https://starchart.cc/bepass-org/oblivion-desktop.svg?variant=adaptive)](https://starchart.cc/bepass-org/oblivion-desktop) ## بیشتر بدانید - [سوالات متداول](FAQ.md) - [لاینسس](LICENSE.md) - [امنیت](SECURITY.md) ![virustotal.jpg](screenshot/virustotal.jpg) ## تقدیر و تشکر این پروژه بر شانه‌های اشخاصی ایستاده است و ما از کمک‌ها و الهام‌بخشی دوستان زیر به‌ شدت سپاسگزاریم: - [Cloudflare Warp](https://www.cloudflare.com/application/terms/) - [warp-plus](https://github.com/bepass-org/warp-plus/) (یوسف قبادی و مارک پشم‌فروش) - [Oblivion](https://github.com/bepass-org/oblivion) - [Oblivion Helper](https://github.com/ShadowZagrosDev/oblivion-helper) (GPLv3) - [Masque-Plus](https://github.com/ircfspace/masque-plus) - [Electron](https://www.electronjs.org/) - [React](https://github.com/facebook/react) - [electron-react-boilerplate](https://github.com/electron-react-boilerplate/electron-react-boilerplate) - [electron-builder](https://github.com/electron-userland/electron-builder) - [regedit](https://www.npmjs.com/package/regedit) - [Iran Sing-box rules](https://github.com/Chocolate4U/Iran-sing-box-rules) - [Shabnam Font](https://rastikerdar.github.io/shabnam-font/) (Saber Rastikerdar) - [Zag-NetStats](https://github.com/ShadowZagrosDev/Zag-NetStats) - [و دیگران 🧡](package.json) ## مشارکت‌کنندگان ابلیویون دسکتاپ اینجاست، چون شما کمک کردین. دمتون گرم ✌️🧡 ([راهنمای مشارکت](CONTRIBUTING.md)) Contributors ================================================ FILE: README.md ================================================

Oblivion Desktop

فارسی persian | English English

Oblivion provides a secure, optimised internet access through a user-friendly Windows/Linux/MacOS app using Cloudflare WARP's technology. > Unofficial Desktop version of the mobile VPN: [Oblivion](https://github.com/bepass-org/oblivion) "Internet, for all or none!" [![Version](https://img.shields.io/github/v/release/bepass-org/oblivion-desktop?label=Version&color=blue)](https://github.com/bepass-org/oblivion-desktop/releases/latest) [![Download](https://img.shields.io/github/downloads/bepass-org/oblivion-desktop/total?label=Downloads)](https://github.com/bepass-org/oblivion-desktop/releases/latest) [![Stars](https://img.shields.io/github/stars/bepass-org/oblivion-desktop?style=flat&label=Stars&color=tomato )](https://github.com/bepass-org/oblivion-desktop) [![License](https://img.shields.io/badge/License-Restrictive-f84e29.svg?color=white)](LICENSE.md) ![oblivion.png](screenshot/oblivion.png) ## Features - **High-grade VPN Security**: Custom WireGuard® implementation in Golang providing enterprise-level encryption with minimal overhead. - **Free and Open Source**: Built with transparency & community contribution in mind, leveraging the power of GitHub Actions for automated builds.Community-driven development with transparent builds via GitHub Actions. - **Intuitive Yet Powerful**: Clean interface designed for ease of use while offering advanced configuration options. - **Modern Tech Stack**: TypeScript frontend with a high-performance Golang backend featuring our optimized WireGuard® implementation. ![oblivion.jpg](screenshot/oblivion.jpg) ## Overview
Feature Status
Method :white_check_mark: WARP
:white_check_mark: Gool
:white_check_mark: Cfon (Psiphon)
:white_check_mark: Masque
Network configurations :white_check_mark: Proxy (No changes)
:white_check_mark: System Proxy (With PAC)
:white_check_mark: TUN With Sing-Box
Routing rules :white_check_mark: System Proxy
:white_check_mark: GeoDB
System tray :white_check_mark: Minimize
:white_check_mark: BootUp
:white_check_mark: Shortcuts
Languages :white_check_mark: Persian (Farsi)
:white_check_mark: English
:white_check_mark: Chinese
:white_check_mark: Russian
:white_check_mark: Turkish
:white_check_mark: Indonesian
:white_check_mark: Arabic
:white_check_mark: Portuguese
:white_check_mark: Vietnamese
:white_check_mark: Urdu
:white_check_mark: Spanish
:white_check_mark: Burmese
:white_check_mark: Amharic
Theme :white_check_mark: Light
:white_check_mark: Dark
:white_check_mark: RTL
:white_check_mark: LTR
:white_check_mark: Auto
Other :white_check_mark: Scanner
:white_check_mark: Ping
:white_check_mark: Accessibility
:white_check_mark: SpeedTest
:white_check_mark: In-App Update (Win)
:white_large_square: Kill Switch
## Download
OS / Arch Compatibility


10+

10.15+

Gnome (gsettings)
KDE (kio)
GNU/Linux (glibc)
6+
## Faced a bug? 🐛 Take a look at our comprehensive [wiki](https://github.com/bepass-org/oblivion-desktop/wiki) and search in [issues](https://github.com/bepass-org/oblivion-desktop/issues) (open and [closed](https://github.com/bepass-org/oblivion-desktop/issues?q=is%3Aissue+is%3Aclosed) ones!) and if you did not find your answer, then [create a new issue](https://github.com/bepass-org/oblivion-desktop/issues/new/choose)! ## Get involved! We're a community-driven project, aiming to make the internet accessible for all. Whether you want to contribute code, suggest features, or need some help, we'd love to hear from you! Check out our [GitHub Issues](https://github.com/bepass-org/oblivion-desktop/issues), [Contribution Guide](CONTRIBUTING.md) and [Developer Docs](DOCS.md). [![Stargazers over time](https://starchart.cc/bepass-org/oblivion-desktop.svg?variant=adaptive)](https://starchart.cc/bepass-org/oblivion-desktop) ## Read more... * [FAQ.md](FAQ.md) * [License.md](LICENSE.md) * [SECURITY.md](SECURITY.md) ![virustotal.jpg](screenshot/virustotal.jpg) ## Acknowledgements This project owes its existence to the groundbreaking work of others. We extend our sincere gratitude to the following contributors and innovators whose work inspired and enabled our efforts: - [Cloudflare Warp](https://www.cloudflare.com/application/terms/) - [warp-plus](https://github.com/bepass-org/warp-plus/) (Yousef Ghobadi & Mark Pashmfouroush) - [Oblivion](https://github.com/bepass-org/oblivion) - [Oblivion Helper](https://github.com/ShadowZagrosDev/oblivion-helper) (GPLv3) - [Masque-Plus](https://github.com/ircfspace/masque-plus) - [Electron](https://www.electronjs.org/) - [React](https://github.com/facebook/react) - [electron-react-boilerplate](https://github.com/electron-react-boilerplate/electron-react-boilerplate) - [electron-builder](https://github.com/electron-userland/electron-builder) - [regedit](https://www.npmjs.com/package/regedit) - [Iran Sing-Box Rules](https://github.com/Chocolate4U/Iran-sing-box-rules) - [Shabnam Font](https://rastikerdar.github.io/shabnam-font/) (Saber Rastikerdar) - [Zag-NetStats](https://github.com/ShadowZagrosDev/Zag-NetStats) - [And others! 🧡](package.json) ## Contributors Oblivion Desktop made possible by contributors of Bepass and you! ✌️ We all appreciate your help and support. Here is a comprehensive contribution guide: ([Contribution Guide](CONTRIBUTING.md)) Contributors ================================================ FILE: SECURITY.md ================================================ # امنیت برنامه اپ متن‌باز Oblivion به‌عنوان یکی‌از امن‌ترین و مطمئن‌ترین VPNها، در روزهای پراختلال اخیر نقش مهمی برای دسترسی آزاد کاربران ایرانی به اینترنت ایفا کرده. - اپ‌های Oblivion و Oblivion Deskop و همینطور هسته Warp-Plus متن‌باز هستن و سورسشون در گیت‌هاب قرار گرفته، ضمن اینکه بیلد پروژه‌ها به‌صورت خودکار توسط گیت‌هاب اکشن انجام می‌شن. - وارپ به‌وسیله رمزنگاری مقدار بیشتری از ترافیک خروجی از دستگاهتون، اجازه نمیده هیچ‌کس در کار شما سرک بکشه. - نتیجه بررسی برنامه در VirusTotal فاقد هرگونه ایراد بوده است. ![virustotal.jpg](screenshot/virustotal.jpg) # Application Security The open-source Oblivion application, recognized as one of the most secure and reliable VPN providers, has played a crucial role in providing unrestricted internet access for Iranian users during recent disruptions going on in Iran. - The Oblivion and Oblivion Desktop programs, as well as the WARP-Plus core, are open-source and their source code is available on their respective GitHub repositories. Additionally, project builds are automatically handled by GitHub Actions. - WARP uses encryption to protect a larger portion of your device's outgoing traffic, ensuring that no one can monitor your activities. - The program's results on VirusTotal have shown no issues. ================================================ FILE: assets/assets.d.ts ================================================ type Styles = Record; declare module '*.svg' { import React = require('react'); export const ReactComponent: React.FC>; const content: string; export default content; } declare module '*.png' { const content: string; export default content; } declare module '*.jpg' { const content: string; export default content; } declare module '*.scss' { const content: Styles; export default content; } declare module '*.sass' { const content: Styles; export default content; } declare module '*.css' { const content: Styles; export default content; } ================================================ FILE: assets/css/materialIcons.css ================================================ @font-face { font-family: 'Material Icons'; font-style: normal; font-weight: 400; src: url('../font/material/MaterialIcons-Regular.woff2') format('woff2'); } .material-icons { font-family: 'Material Icons'; font-weight: normal; font-style: normal; font-size: 24px; display: inline-block; line-height: 1; text-transform: none; letter-spacing: normal; word-wrap: normal; white-space: nowrap; direction: ltr; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; -moz-osx-font-smoothing: grayscale; font-feature-settings: 'liga'; } .material-icons.md-18 { font-size: 18px; } .material-icons.md-24 { font-size: 24px; } .material-icons.md-28 { font-size: 28px; } .material-icons.md-36 { font-size: 36px; } .material-icons.md-48 { font-size: 48px; } .material-icons.md-dark { color: rgba(0, 0, 0, 0.54); } .material-icons.md-dark.md-inactive { color: rgba(0, 0, 0, 0.26); } .material-icons.md-light { color: rgba(255, 255, 255, 1); } .material-icons.md-light.md-inactive { color: rgba(255, 255, 255, 0.3); } ================================================ FILE: assets/css/noto.css ================================================ @font-face { font-family: 'Noto Color Emoji'; font-style: normal; font-weight: 400; font-display: swap; src: url('../font/noto/NotoColorEmoji-Regular0.woff2') format('woff2'); unicode-range: U+1f1e6-1f1ff; } ================================================ FILE: assets/css/shabnam.css ================================================ @font-face { font-family: shabnam; src: url('../font/shabnam/Shabnam-Thin.eot'); src: url('../font/shabnam/Shabnam-Thin.eot?#iefix') format('embedded-opentype'), url('../font/shabnam/Shabnam-Thin.woff') format('woff'), url('../font/shabnam/Shabnam-Thin.ttf') format('truetype'); font-weight: 100; } @font-face { font-family: shabnam; src: url('../font/shabnam/Shabnam-Light.eot'); src: url('../font/shabnam/Shabnam-Light.eot?#iefix') format('embedded-opentype'), url('../font/shabnam/Shabnam-Light.woff') format('woff'), url('../font/shabnam/Shabnam-Light.ttf') format('truetype'); font-weight: 200; } @font-face { font-family: shabnam; src: url('../font/shabnam/Shabnam.eot'); src: url('../font/shabnam/Shabnam.eot?#iefix') format('embedded-opentype'), url('../font/shabnam/Shabnam.woff') format('woff'), url('../font/shabnam/Shabnam.ttf') format('truetype'); font-weight: 300; } @font-face { font-family: shabnam; src: url('../font/shabnam/Shabnam-Medium.eot'); src: url('../font/shabnam/Shabnam-Medium.eot?#iefix') format('embedded-opentype'), url('../font/shabnam/Shabnam-Medium.woff') format('woff'), url('../font/shabnam/Shabnam-Medium.ttf') format('truetype'); font-weight: 400; } @font-face { font-family: shabnamDigits; src: url('../font/shabnam/Shabnam-Medium-FD-WOL.eot'); src: url('../font/shabnam/Shabnam-Medium-FD-WOL.eot?#iefix') format('embedded-opentype'), url('../font/shabnam/Shabnam-Medium-FD-WOL.woff') format('woff'), url('../font/shabnam/Shabnam-Medium-FD-WOL.ttf') format('truetype'); font-weight: 400; } @font-face { font-family: shabnam; src: url('../font/shabnam/Shabnam-Bold.eot'); src: url('../font/shabnam/Shabnam-Bold.eot?#iefix') format('embedded-opentype'), url('../font/shabnam/Shabnam-Bold.woff') format('woff'), url('../font/shabnam/Shabnam-Bold.ttf') format('truetype'); font-weight: 900; } ================================================ FILE: assets/css/style.css ================================================ body { font: 300 17px shabnam; background: #fff; color: #4c4b4b; line-height: 20px; word-wrap: break-word; position: relative; transition: all 0.2s ease-in-out; } [dir='ltr'] body { text-align: left; direction: ltr; } [data-bs-theme='dark'] body { background: #0a0e24; color: #dcdcdc; } body:before { height: 40vh; background: linear-gradient(#ffde06 0%, rgba(128, 128, 128, 0) 100%); opacity: 0.2; content: ''; float: right; width: 100%; position: absolute; transition: all 0.2s ease-in-out; } [data-bs-theme='dark'] body:after { background: linear-gradient(#ffde06 0%, rgba(128, 128, 128, 0) 100%); } .dirLeft { direction: ltr; text-align: left; } .dirLeft[dir='rtl'] { direction: rtl; } .dirRight { direction: rtl; text-align: right; } nav.header { float: right; width: 100%; position: fixed; top: 0; right: 0; padding: 20px 0; transition: all 0.2s ease-in-out; z-index: 2; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } nav.isSticky { background: rgba(253, 248, 209, 0.9); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); backdrop-filter: blur(5px); --webkit-backdrop-filter: blur(5px); } [data-bs-theme='dark'] nav.isSticky { background: rgba(53, 50, 33, 0.9); } nav.header i { color: #676767; font-size: 30px !important; transition: all 0.2s ease-in-out; cursor: pointer; } [data-bs-theme='dark'] nav.header i { color: #dcdcdc; } nav.header i:hover { color: #4f4f4f; } [data-bs-theme='dark'] nav.header i:hover { color: #eaeaea; } nav.header i.navPaste { float: left; margin: 2px 0 0 15px; font-size: 26px !important; } [dir='ltr'] nav.header i.navPaste { float: right; margin: 2px 15px 0 0; } nav.header i.log { color: #a1a1a1; margin: 0 0 0 15px; font-size: 31px !important; float: left; } [dir='ltr'] nav.header i.log { float: right; margin: 0 15px 0 0; } nav.header i.navLeft { float: left; } [dir='ltr'] nav.header i.navLeft { float: right; } nav.header i.log:hover { color: #e15959; } [data-bs-theme='dark'] nav.header i.log:hover { color: #ed6a6a; } nav.header a.disabled { pointer-events: none; } nav.header h3 { font-size: 19px; font-weight: 200; color: #303030; margin: 4px 0 0 0; float: right; } [dir='ltr'] nav.header h3 { float: left; margin: 4px 0 0 0; } [data-bs-theme='dark'] nav.header h3 { color: #dcdcdc; } nav.header i.backBtn { font-size: 30px; float: left; margin-left: -5px; } [dir='ltr'] nav.header i.backBtn { float: right; rotate: 180deg; margin-left: 0; margin-right: -5px; } nav.header i.backBtn:hover { color: #1058ff; } nav.header .navMenu { position: relative; float: right; transition: all 0.2s ease-in-out; } [dir='ltr'] nav.header .navMenu { float: left; } nav.header .navMenu .indicator { position: absolute; width: 10px; height: 10px; border-radius: 50%; background: #ffa200; top: 1px; left: -3px; animation: indicator 1s linear infinite; } @keyframes indicator { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } } ::-webkit-scrollbar { background: #fff; width: 7px; height: 3px; } ::-webkit-scrollbar-thumb { background: rgba(253, 248, 209, 0.9); } ::-webkit-scrollbar-thumb:hover { background: rgb(236, 230, 184); } [data-bs-theme='dark'] ::-webkit-scrollbar { background: #212121; } [data-bs-theme='dark'] ::-webkit-scrollbar-thumb { background: rgba(53, 50, 33, 0.9); } [data-bs-theme='dark'] ::-webkit-scrollbar-thumb:hover { background: rgb(83, 78, 54); } .container { max-width: 400px; } .myApp { float: right; width: 100%; } .myApp.normalPage { margin: 80px 0 25px 0; z-index: 1; position: relative; } .myApp.normalPage.withScroll { margin: 80px 0; } .myApp.normalPage p { float: right; width: 100%; line-height: 26px; font-weight: 300; font-size: 15px; } .verticalAlign { display: flex; justify-content: center; align-items: center; min-height: 80vh; } .myApp .iframe { float: right; width: 100%; position: relative; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .myApp .iframe:after { float: right; width: 100%; position: absolute; content: ''; top: 0; left: 0; height: 300px; background: transparent; z-index: 1; } .myApp .iframe iframe { float: right; width: 100%; height: calc(100vh - 200px); border: none; border-radius: 250px 250px 25px 25px; } .homeScreen { float: right; width: 100%; margin-top: 150px; display: flex; flex-direction: column; align-items: center; justify-content: space-evenly; flex-wrap: nowrap; align-content: center; user-select: none; position: relative; } .homeScreen .title { float: right; width: 227px; } .homeScreen .title h1 { float: right; width: 100%; color: #707070; font-size: 50px; font-weight: 900; text-align: center; margin: 0; padding: 0; } [data-bs-theme='dark'] .homeScreen .title h1 { color: #c5c5c5; } .homeScreen .title h2 { float: right; width: 100%; padding: 0; color: #ffa200; font-size: 25px; font-weight: 300; text-align: right; margin: 0; } [data-bs-theme='dark'] .homeScreen .title h2 { color: #ffa200; } [dir='ltr'] .homeScreen .title h2 { text-align: left; } .homeScreen .title h2 .badge { font-weight: 100; margin: 6px 3px 0 3px; font-size: 12px; line-height: 12px; padding: 4px 5px 2px 5px; background: rgba(203, 203, 203, 0.3); transition: all 0.2s ease-in-out; color: #858585; float: left; opacity: 0; } .homeScreen .title:hover h2 .badge { opacity: 1; } [data-bs-theme='dark'] .homeScreen .title h2 .badge { background: rgba(107, 107, 107, 0.3); color: #a5a5a5; } [dir='ltr'] .homeScreen .title h2 .badge { float: right; margin: 3px 3px 0 3px; } @media screen and (min-width: 600px) { .homeScreen .title h2 { text-align: center; } } .homeScreen form { float: right; width: 100%; } .homeScreen form .connector { margin: auto; float: right; width: 100%; display: flex; justify-content: center; align-items: center; } .homeScreen form .connector .switch { border-radius: 50px; background: #d9d9d9; width: 175px; height: 75px; margin: 100px 0 50px 0; box-shadow: 0 0 15px rgba(0, 0, 0, 0.11); float: right; position: relative; cursor: pointer; transition: all 0.2s ease-in-out; border: none; } [data-bs-theme='dark'] .homeScreen form .connector .switch { background: #333643; } .homeScreen form .connector .switch.active { background: #ffad0a; } .homeScreen form .connector .switch .circle { width: 65px; height: 65px; background: #fff; position: absolute; left: 5px; top: 5px; border-radius: 50%; transition: all 0.2s ease-in-out; } .homeScreen form .connector .switch.active .circle { left: calc(100% - 70px); } .homeScreen form .connector .switch.isLoading { cursor: wait; } .homeScreen form .connector .switch.isLoading .circle .spinner { animation: spinner 0.8s linear infinite; border: 4px solid #9b9b9b; border-right-color: transparent; border-radius: 100%; display: inline-block; position: relative; overflow: hidden; text-indent: -9999px; vertical-align: middle; margin: 5px; width: 55px; height: 55px; } @keyframes spinner { from { transform: rotate(0deg); } to { transform: rotate(1turn); } } .homeScreen .status { width: 100%; float: right; text-align: center; color: #4b4b4b; } [data-bs-theme='dark'] .homeScreen .status { color: #dcdcdc; } .homeScreen .status.active { color: #63bc0a; } .homeScreen .ip { color: #aeaeae; margin: 10px 0 0 0; } .homeScreen .ip img, .homeScreen .ip svg { width: 17px; height: 12px; border-radius: 3px; margin: 0 0 0 7px; object-fit: cover; transition: all 0.2s ease-in-out; opacity: 0; } .homeScreen .ip.connected img, .homeScreen .ip.connected svg { opacity: 1; } .homeScreen .ip span { opacity: 0; transition: all 0.2s ease-in-out; } .homeScreen .ip span:not(:empty) { cursor: pointer; } .homeScreen .ip.connected span { opacity: 1; } .homeScreen .inFoot { position: fixed; bottom: -90px; min-width: 200px; max-width: 80%; background: #f1f1f1; border-radius: 15px 15px 0 0; padding: 10px; line-height: 15px; text-align: center; transition: all 0.5s ease-in-out; opacity: 0; left: 50%; transform: translateX(-50%); max-width: 300px; } .homeScreen .inFoot.withIp { min-width: 230px; } .homeScreen .inFoot.active { opacity: 1; bottom: -5px; } html[data-bs-theme='dark'] .homeScreen .inFoot { background: #171c2b; } .homeScreen .inFoot small { text-align: center; font-weight: 200; opacity: 0; } html[data-bs-theme='dark'] .homeScreen .inFoot small { font-weight: 100; } .homeScreen .inFoot small { opacity: 0.8; } .homeScreen .inFoot small:not(:empty) { cursor: pointer; } .homeScreen .inFoot .item { float: left; width: 100%; margin: 5px 0 6px 0; } .homeScreen .inFoot .item img { float: left; width: 17px; height: 12px; border-radius: 3px; margin: 1px 0 0 7px; object-fit: cover; } .homeScreen .inFoot .item img[src*="flagsapi"] { object-fit: none; } .homeScreen .inFoot .item img[alt='false Flag'], .homeScreen .inFoot .item img[alt='xx Flag'] { filter: brightness(77%); } html[data-bs-theme='dark'] .homeScreen .inFoot .item img[alt='false Flag'], html[data-bs-theme='dark'] .homeScreen .inFoot .item img[alt='xx Flag'] { filter: brightness(33%); } .homeScreen .inFoot .item i { float: left; margin: -4px -1px 0 5px; font-size: 20px; opacity: 0.5; } @keyframes placeHolderShimmer { 0% { background-position: -468px 0; } 100% { background-position: 468px 0; } } .shimmer { -webkit-animation-duration: 3s; animation-duration: 3s; -webkit-animation-fill-mode: forwards; animation-fill-mode: forwards; -webkit-animation-iteration-count: infinite; animation-iteration-count: infinite; -webkit-animation-name: placeHolderShimmer; animation-name: placeHolderShimmer; -webkit-animation-timing-function: linear; animation-timing-function: linear; background: #bebebe; background: linear-gradient(to left, #dddddd 8%, #bebebe 18%, #dddddd 33%); background-size: 800px 104px; position: relative; color: transparent !important; border-radius: 25px; height: 15px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } html[data-bs-theme='dark'] .shimmer { background: linear-gradient(to left, #0a0e24 8%, #171c2b 18%, #0a0e24 33%); background-size: 800px 104px; } .homeScreen .inFoot .item span { float: left; margin-left: 10px; min-width: 150px; width: calc(100% - 43px); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; direction: ltr; font-size: 14px; text-align: left; cursor: pointer; } .homeScreen .inFoot .item.ping span { opacity: 0.8; font-size: 13px; height: 13px; min-width: auto; width: auto; } .homeScreen .inFoot .item.speed { margin: 1px 0 5px 0; cursor: default; } .homeScreen .inFoot .item.speed .download { float: left; } .homeScreen .inFoot .item.speed .upload { float: right; margin-right: 4px; } .homeScreen .inFoot .item.speed .download i, .homeScreen .inFoot .item.speed .upload i { cursor: default; font-size: 17px; margin: -3px 0px 0 6px; } .homeScreen .inFoot .item.speed .download span, .homeScreen .inFoot .item.speed .upload span { opacity: 0.8; font-size: 13px; height: 13px; min-width: auto; width: auto; cursor: default; } .homeScreen .inFoot .item.speed .isPing span { cursor: pointer; } .homeScreen .inFoot .item.speed .download span small, .homeScreen .inFoot .item.speed .upload span small { cursor: default; } .homeScreen .inFoot .item .hasTooltip { position: relative; } .homeScreen .inFoot .item .hasTooltip .isTooltip { position: absolute; content: ''; background: #fff; border-radius: 5px; color: #000; min-width: 105px; padding: 7px; font-size: 12px; font-weight: 200; line-height: 17px; bottom: 20px; right: -3px; transition: all 0.2s ease-in-out; box-shadow: 0 -5px 15px rgba(0, 0, 0, 0.1); opacity: 0; } [data-bs-theme='dark'] .homeScreen .inFoot .item .hasTooltip .isTooltip { background: #01030a; color: #c3c3c3; } .homeScreen .inFoot .item .hasTooltip:hover .isTooltip { opacity: 1; } .homeScreen .inFoot .item .hasTooltip .isTooltip i { float: left; font-style: normal; opacity: 0.3; position: absolute; left: 5px; top: 5px; margin: 0; } .homeScreen .inFoot .item .hasTooltip .isTooltip i.latest { top: inherit; bottom: 6px; } .homeScreen .inFoot .item .hasTooltip .isTooltip span { direction: ltr; float: right; width: auto; text-align: left; overflow: inherit; white-space: inherit; text-overflow: inherit; margin: 0; } .homeScreen .inFoot .item .hasTooltip .isTooltip .clearfix { float: left; width: 100%; height: 4px; } .logPage .logText { font-size: 14px !important; line-height: 24px !important; white-space: pre-line; } [dir='ltr'] .logPage .logText { direction: ltr; text-align: left; } .logPage .logOptions { float: left; margin: 0; position: fixed; bottom: 20px; right: 20px; background: rgba(253, 248, 209, 0.9); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); padding: 10px 15px; border-radius: 25px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } [data-bs-theme='dark'] .logPage .logOptions { background: rgba(53, 50, 33, 0.9); } .logPage .logOptions div[role] { float: left; margin-right: 10px; } .logPage .logOptions div[role]:last-child { margin-right: 0; } .logPage .logOptions i { float: left; font-size: 20px; cursor: pointer; transition: all 0.2s ease-in-out; } .logPage .logOptions i:hover { opacity: 0.7; } .settings { float: right; width: 100%; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .settings .lottie { display: flex; justify-content: center; align-items: center; float: right; width: 100%; height: 100vh; } .settings .grouped { float: right; width: 100%; } .settings .grouped .item { border-bottom: none; padding-bottom: 0; } .settings .grouped .item:last-child { border-bottom: 1px solid #dcdcdc; padding-bottom: 15px; } .settings .item { float: right; width: 100%; border-bottom: 1px solid #dcdcdc; transition: all 0.2s ease-in-out; padding: 14px 0 15px 0; } [data-bs-theme='dark'] .settings .item, [data-bs-theme='dark'] .settings .grouped .item:last-child { border-color: #2b2e3b; } .settings .item .loader { float: left; width: 10px; aspect-ratio: 1; border-radius: 50%; background: radial-gradient(farthest-side, #ffa516 94%, #0000) top/2px 2px no-repeat, conic-gradient(#0000 30%, #ffa516); mask: radial-gradient(farthest-side, #0000 calc(100% - 2px), #000 0); animation: loaderAnim 1s infinite linear; margin: 8px 0 0 7px; } [dir='ltr'] .settings .item .loader { float: right; margin: 8px 7px 0 0; } @keyframes loaderAnim { 100% { transform: rotate(1turn); } } .settings .item.disabled { opacity: 0.3; } .settings .item.highlight { background: #fffa9b; } [data-bs-theme='dark'] .settings .item.highlight { background: #2d2e53; } .settings .item:first-child { padding-top: 0; } .settings .item:last-child { border-bottom: none; } .settings .item .key { float: right; width: 40%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 22px; font-weight: 300; color: #303030; margin: 3px 0 0 0; padding: 2px 0; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } [dir='ltr'] .settings .item .key { float: left; } [data-bs-theme='dark'] .settings .item .key { color: #e8e8e8; } .settings .item .value { float: left; margin: 0; width: 60%; padding-top: 1px; } [dir='ltr'] .settings .item .value { float: right; } .settings .item .value span { float: left; color: #ffa200; margin: 6px 0 0 0; font-size: 22px; font-weight: 300; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; width: 100%; cursor: pointer; padding-top: 1px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } [dir='ltr'] .settings .item .value span { float: right; text-align: right; } .settings .item.disabled .value span { cursor: default; } .settings .item .value span[dir='auto'] { direction: rtl; } [dir='ltr'] .settings .item .value span[dir='auto'] { direction: ltr; } .settings .item .value select { float: left; border: none; background: none; direction: ltr; font-size: 22px; font-weight: 300; color: #ffa200; cursor: pointer; appearance: none; line-height: 24px; padding: 0; margin: 4px 0 -2px 0; font-family: 'Noto Color Emoji', 'shabnam'; } [dir='ltr'] .settings .item .value select { float: right; text-align: right; } .settings .item.disabled .value select { cursor: default; } .settings .item .value select option { direction: ltr; font-size: 18px; font-weight: 300; color: #666666; } .settings .item .value i { float: left; font-size: 28px; cursor: pointer; color: #ffa200; } [dir='ltr'] .settings .item .value i { float: right; } .settings .item .value .checkbox { float: left; border: 3px solid #a1a1a1; background: transparent; width: 27px; height: 27px; border-radius: 4px; padding: 0; margin: 2px 0 -1px 0; cursor: pointer; transition: all 0.2s ease-in-out; } [dir='ltr'] .settings .item .value .checkbox { float: right; } [data-bs-theme='dark'] .settings .item .value .checkbox { border-color: #dcdcdc; } .settings .item.disabled .value .checkbox, .settings .item.disabled .value .checkbox i { cursor: default; } .settings .item .value .checkbox.checked { background: #ffa200; border-color: #ffa200; } .settings .item .value .checkbox i { float: left; color: #fff; font-size: 27px; margin: -3px 0 0 -3px; opacity: 0; transition: all 0.2s ease-in-out; rotate: -25deg; } .settings .item .value .checkbox.checked i { opacity: 1; rotate: 0deg; } .settings .item .value .switch { float: left; border: 3px solid #a1a1a1; background: transparent; width: 27px; height: 27px; border-radius: 50%; padding: 0; margin: 2px 0 -1px 0; cursor: pointer; transition: all 0.2s ease-in-out; } [dir='ltr'] .settings .item .value .switch { float: right; } [data-bs-theme='dark'] .settings .item .value .switch { border-color: #dcdcdc; } .settings .item .value .switch.checked { position: relative; border-color: #ffa200; } .settings .item .value .switch.checked:after { position: absolute; content: ''; top: 5px; right: 5px; width: 11px; height: 11px; background: #ffa200; border-radius: 50%; } .settings .item .info { float: right; width: 100%; color: #717171; font-size: 14px; margin: 5px 0 0 0; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } [dir='ltr'] .settings .item .info { float: left; } [data-bs-theme='dark'] .settings .item .info { color: #a0a0a0; font-weight: 200; } .settings .item[role='button'], .settings .item[role='button'].disabled { cursor: default; } .moreSettings { float: right; width: 100%; padding: 0; margin: 30px 0 20px 0; font-size: 15px; opacity: 0.5; font-weight: 200; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } [dir='ltr'] .moreSettings { float: left; } .moreSettings i { float: right; margin: -2px -8px 0 5px; opacity: 0.5; } [dir='ltr'] .moreSettings i { float: left; margin: -3px 5px 0 -8px; } .appToast { float: right; width: 100%; padding: 0; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; margin-bottom: 10px; } .appToast.inModal { margin: 10px 0 0 0; padding: 0; } .appToast div { float: right; width: 100%; padding: 10px; border-radius: 5px; font-size: 14px; font-weight: 200; line-height: 24px; background: #f1f1f1; } [data-bs-theme='dark'] .appToast div { background: #2a273f; } .appToast div i { float: right; height: 100%; padding: 10px 0 10px 10px; opacity: 0.3; } [dir='ltr'] .appToast div i { float: left; padding: 10px 10px 10px 0; } .toastHelp { float: left; border: none; font-size: 13px; font-weight: 200; transition: all 0.2s ease-in-out; opacity: 0.7; padding: 2px 7px; line-height: 18px; background: #1e1e1e; color: #fff; border-radius: 4px; margin: -1px 10px 0 0; } [dir='ltr'] .toastHelp { float: right; margin: -1px 0 0 10px; } .toastHelp:focus, .toastHelp:hover { opacity: 1; text-decoration: none; color: orange; } .splashScreen { float: left; width: 100%; background: #fff; color: #4c4b4b; z-index: 3; position: absolute; height: 100vh; } [data-bs-theme='dark'] .splashScreen { background-color: #0a0e24; color: #dcdcdc; } .splashScreen:before { height: 40vh; background: linear-gradient(#ffde06 0%, rgba(128, 128, 128, 0) 100%); opacity: 0.2; content: ''; float: right; width: 100%; position: absolute; transition: all 0.2s ease-in-out; } [data-bs-theme='dark'] .splashScreen:after { background: linear-gradient(#ffde06 0%, rgba(128, 128, 128, 0) 100%); } .splashScreen .splashScreenImg { background: transparent url('../img/splashScreen/light.png') no-repeat center center; background-size: 100%; width: 100%; height: 100vh; z-index: 4; position: absolute; } @media (min-width: 400px) { .splashScreen .splashScreenImg { background-size: 400px; } } html:not([lang='fa']) .splashScreen .splashScreenImg { background-image: url('../img/splashScreen/light-en.png'); } [data-bs-theme='dark'] .splashScreen .splashScreenImg { background-image: url('../img/splashScreen/dark.png'); } html:not([lang='fa'])[data-bs-theme='dark'] .splashScreen .splashScreenImg { background-image: url('../img/splashScreen/dark-en.png'); } .splashScreen .loading { display: flex; justify-content: center; align-items: center; position: absolute; bottom: 35px; float: right; width: 100%; } .splashScreen .loading svg path:nth-child(1) { stroke-dasharray: 2.42777px, 242.77666px; stroke-dashoffset: 0; -webkit-animation: anim 1.6s linear infinite; animation: anim 1.6s linear infinite; stroke: #727272; } .splashScreen .loading svg path:nth-child(2) { stroke: #000; opacity: 0.1; } [data-bs-theme='dark'] .splashScreen .loading svg path:nth-child(1) { stroke: #fff; } [data-bs-theme='dark'] .splashScreen .loading svg path:nth-child(2) { stroke: #ededed; opacity: 0.1; } @keyframes anim { 12.5% { stroke-dasharray: 33.98873px, 242.77666px; stroke-dashoffset: -26.70543px; } 43.75% { stroke-dasharray: 84.97183px, 242.77666px; stroke-dashoffset: -84.97183px; } 100% { stroke-dasharray: 2.42777px, 242.77666px; stroke-dashoffset: -240.34889px; } } .socialMedia { float: left; width: 100%; margin: 25px 0 15px 0; } .socialMedia .item { float: left; width: 100%; background: #f3f3f3; border-radius: 25px; padding: 10px; margin-bottom: 7px; } [data-bs-theme='dark'] .socialMedia .item { background: #252525; } .socialMedia a { color: #4c4b4b; transition: all 0.2s ease-in-out; } [data-bs-theme='dark'] .socialMedia a { color: #dcdcdc; } .socialMedia a:hover { color: #90610f; } [data-bs-theme='dark'] .socialMedia a:hover { color: #ffa200; } .socialMedia .item .icon { float: left; } .socialMedia .item .icon img { float: left; width: 25px; height: 25px; margin: 0 0 0 5px; border-radius: 50%; } [data-bs-theme='dark'] .socialMedia .item .icon img[alt='github'] { filter: contrast(0%); } .socialMedia .item .icon i { float: left; font-size: 25px; margin: 0 0 0 5px; } .socialMedia .item .host { float: left; font-size: 16px; padding: 4px 0 0 10px; opacity: 0.7; } .socialMedia .item .name { float: right; padding: 3px 10px 0 0; font-weight: 400; font-size: 16px; } .socialMedia hr { border-color: #dcdcdc; margin-top: 0; } .socialMedia .starBadge { margin-right: 10px; width: 70px; height: 20px; } [dir='ltr'] .socialMedia .starBadge { margin-right: 0; margin-left: 10px; } [data-bs-theme='dark'] .socialMedia hr { border-color: #2b2e3b; } .dialog { position: fixed; bottom: 0; left: 0; width: 100%; z-index: 2; transition: opacity 0.15s ease-in-out; } .no-opacity { opacity: 0; } .dialog .dialogBg { position: fixed; width: 100%; height: 100vh; box-shadow: 0 5px 10px rgba(0, 0, 0, 0.16); backdrop-filter: blur(4px); --webkit-backdrop-filter: blur(4px); background: rgba(0, 0, 0, 0.25); z-index: 2; top: 0; } [data-bs-theme='dark'] .dialog .dialogBg { box-shadow: 0 5px 10px rgba(0, 0, 0, 0.16); backdrop-filter: blur(3px); --webkit-backdrop-filter: blur(3px); background-color: rgba(0, 0, 0, 0.25); } .dialog .dialogBox { position: fixed; bottom: 0; left: 0; width: 100%; background: #fff; z-index: 3; border-radius: 25px 25px 0 0; box-shadow: 0 0 20px rgba(0, 0, 0, 0.16); padding: 10px 10px 15px 10px; left: 50%; transform: translateX(-50%); max-width: 385px; } .dialog .dialogBox .container { width: 100%; } [data-bs-theme='dark'] .dialog .dialogBox { background: #14172d; } .dialog .dialogBox .line { display: flex; justify-content: center; align-items: center; margin: 0 0 5px 0; } .dialog .dialogBox .line .miniLine { width: 62px; height: 5px; background: #a1a1a1; opacity: 0.37; border-radius: 25px; } .dialog .dialogBox h3 { float: right; width: 100%; font-weight: 300; font-size: 17px; margin: 10px 0 20px 0; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } [data-bs-theme='dark'] .dialog .dialogBox h3 { color: #dcdcdc; } .dialog .dialogBox p { float: right; width: 100%; margin: 0; font-size: 15px; line-height: 26px; } .dialog .dialogBox p.withMargin { margin-bottom: 20px; } .dialog .dialogBox p a { color: #ffa200; } .dialog .dialogBox label { float: left; width: 100%; font-size: 13px; font-weight: 300; } .dialog .dialogBox label:not(.firstItem) { margin-top: 20px; } .dialog .dialogBox label[dir='ltr'] { text-align: left; } .dialog .dialogBox input, .dialog .dialogBox textarea { float: right; width: 100%; padding: 15px; height: auto; direction: ltr; font-weight: 400; border-width: 2px; border-radius: 5px; } .dialog .dialogBox textarea { min-height: 100px; border-radius: 5px; height: 150px; max-height: 200px; resize: vertical; } /* .dialog .dialogBox input[type=number] { -moz-appearance: textfield; } */ .dialog .dialogBox input::-webkit-outer-spin-button, .dialog .dialogBox input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .dialog .dialogBox .input-group { float: left; width: 100%; } .dialog .dialogBox .input-group input { float: left; } .dialog .dialogBox .input-group input:nth-child(1) { border-radius: 5px 0 0 5px; width: 30%; border-right-width: 1px; } .dialog .dialogBox .input-group input:nth-child(2) { border-radius: 0; width: 55%; border-left-width: 1px; border-right-width: 1px; } .dialog .dialogBox .input-group .input-group-btn { float: right; width: 15%; } .dialog .dialogBox .input-group .input-group-btn button { float: right; width: 100%; border-radius: 0 5px 5px 0; margin: 0; height: 54px; background: #619fd3; outline: none; color: #fff; font-size: 18px; } [data-bs-theme='dark'] .dialog .dialogBox .input-group .input-group-btn button { background: #316491; } .dialog .dialogBox .input-group .input-group-btn button[disabled] { background: #cbcbcb; } [data-bs-theme='dark'] .dialog .dialogBox .input-group .input-group-btn button[disabled] { background: #5a6269; } .dialog .dialogBox .input-group .input-group-btn button i { float: right; width: 100%; text-align: center; font-size: 18px; } [data-bs-theme='dark'] .dialog .dialogBox input, [data-bs-theme='dark'] .dialog .dialogBox textarea { background: #0a0e24; border-color: #486097; color: #ffffff; } [data-bs-theme='dark'] .dialog .dialogBox input:focus, [data-bs-theme='dark'] .dialog .dialogBox textarea:focus { border-color: #5a78bc; } .dialog .dialogBox .labels { float: left; margin: 0; } [dir='ltr'] .dialog .dialogBox .labels { float: right; } .dialog .dialogBox .labels .label { float: right; border-radius: 25px; font-weight: 200; padding: 3px 7px; margin: 0 5px 0 0; cursor: pointer; position: relative; } .dialog .dialogBox .labels .label.disabled { opacity: 0.2; cursor: default; } [dir='ltr'] .dialog .dialogBox .labels .label { float: left; margin: 0 0 0 5px; } .dialog .dialogBox .labels .label .dropDownInLabel { position: absolute; top: 25px; left: 5px; background: #fff; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); border-radius: 3px; direction: ltr; float: left; width: 80px; height: 120px; overflow: hidden; overflow-y: auto; z-index: 1; cursor: default; } [dir='ltr'] .dialog .dialogBox .labels .label .dropDownInLabel { left: inherit; right: 5px; } .dialog .dialogBox .labels .label .dropDownInLabel.splitter { float: left; width: 162px; overflow: hidden; } .dialog .dialogBox .labels .label .dropDownInLabel.splitter[data-list='1'] { width: 80px; } .dialog .dialogBox .labels .label .dropDownInLabel.splitter[data-list='3'] { width: 240px; } .dialog .dialogBox .labels .label .dropDownInLabel.splitter .split { float: left; width: 80px; height: 120px; overflow: hidden; overflow-y: auto; } .dialog .dialogBox .labels .label .dropDownInLabel.splitter .split::-webkit-scrollbar-thumb { background: rgba(190, 190, 190, 0.9); } .dialog .dialogBox .labels .label .dropDownInLabel.splitter .split::-webkit-scrollbar { background: #fff; width: 5px; } [data-bs-theme='dark'] .dialog .dialogBox .labels .label .dropDownInLabel.splitter .split::-webkit-scrollbar { background: #fff; } .dialog .dialogBox .labels .label .dropDownInLabel.splitter .split:first-child { border-right: 1px solid #ededed; } .dialog .dialogBox .labels .label .dropDownInLabel .item { float: right; width: 100%; padding: 9px 15px; font-size: 12px; font-weight: 400; color: #2c2c2c; cursor: pointer; border-radius: 3px; transition: all 0.2s ease-in-out; text-align: left; } .dialog .dialogBox .labels .label .dropDownInLabel .item:hover { background: #f8f8f8; color: #1c1c1c; } .dialog .dialogBox .labels .label .dropDownInLabel.splitter .split:nth-child(3) .item { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .dialog .dialogBox .labels .label .dropDownInLabel .item.disabled { opacity: 0.3; cursor: default; } .dialog .dialogBox .labels .label .dropDownInLabel .item.disabled:hover { background: #fff; } .dialog .dialogBox .labels .label .dropDownInLabel .item small { font-size: 12px; padding-right: 1px; opacity: 0.7; font-weight: normal; } [data-bs-theme='dark'] .dialog .dialogBox .labels .label.label-warning { background-color: #8b5100; } [data-bs-theme='dark'] .dialog .dialogBox .labels .label.label-danger { background-color: #9b2623; } .dialog .dialogBox .labels .label.label-default { background-color: #9b9b9b; } [data-bs-theme='dark'] .dialog .dialogBox .labels .label.label-default { background-color: #7b7b7b; } .dialog .dialogBox .labels .label i { float: right; font-size: 14px; margin: -1px -2px 0 3px; } [dir='ltr'] .dialog .dialogBox .labels .label i { float: left; margin: -1px 3px 0 -2px; } .dialog .dialogBox .tagList { float: left; width: 100%; margin: 10px 0 0 0; } .dialog .dialogBox .tagList .tagItem { float: left; border: 1px solid #d5d5d5; padding: 4px 10px 2px 7px; border-radius: 25px; line-height: 18px; cursor: default; margin: 0 7px 7px 0; } [data-bs-theme='dark'] .dialog .dialogBox .tagList .tagItem { border-color: #337ab7; } .dialog .dialogBox .tagList .tagItem i { float: left; font-size: 16px; margin: 0 5px 0 0; cursor: pointer; transition: all 0.2s ease-in-out; color: #adadad; } [data-bs-theme='dark'] .dialog .dialogBox .tagList .tagItem i { color: #7a7a7a; } .dialog .dialogBox .tagList .tagItem i:hover { color: #fff; } .dialog .dialogBox .tagList .tagItem i.closeIco { border-right: 1px solid #d3d3d3; padding-right: 5px; } [data-bs-theme='dark'] .dialog .dialogBox .tagList .tagItem i.closeIco { border-color: #273643; } .dialog .dialogBox .tagList .tagItem i.closeIco:hover { color: tomato; } .dialog .dialogBox .tagList .tagItem span { float: left; font-size: 13px; font-weight: 300; text-transform: capitalize; } [data-bs-theme='dark'] .dialog .dialogBox .tagList .tagItem span { font-weight: 200; } .dialog .dialogBox .btn { float: left; padding: 10px; margin: 15px 0 0 0; border: 1px solid transparent; transition: all 0.2s ease-in-out; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } [dir='ltr'] .dialog .dialogBox .btn { float: right; } .dialog .dialogBox .btn.btn-save { background: #8d8c8c; color: #fff; font-weight: 300; } [data-bs-theme='dark'] .dialog .dialogBox .btn.btn-save { background: #050920; } .dialog .dialogBox .btn.btn-save:hover { background: #7f7f7f; } [data-bs-theme='dark'] .dialog .dialogBox .btn.btn-save:hover { background: #010315; } .dialog .dialogBox .btn.btn-cancel { border-color: #acacac; margin-right: 10px; font-weight: 300; } [dir='ltr'] .dialog .dialogBox .btn.btn-cancel { margin-right: 0; margin-left: 10px; } [data-bs-theme='dark'] .dialog .dialogBox .btn.btn-cancel { border-color: #303666; font-weight: 200; } .dialog .dialogBox .btn.btn-cancel:hover { border-color: #979797; } [data-bs-theme='dark'] .dialog .dialogBox .btn.btn-cancel:hover { border-color: #464d8b; color: #dadada; } .dialog .dialogBox .btn.btn-update { border-color: #acacac; margin-left: 10px; font-weight: 300; float: right; } [dir='ltr'] .dialog .dialogBox .btn.btn-update { margin-left: 0; margin-right: 10px; float: left; } [data-bs-theme='dark'] .dialog .dialogBox .btn.btn-update { border-color: #303666; } .dialog .dialogBox .btn.btn-update:hover { border-color: #979797; } [data-bs-theme='dark'] .dialog .dialogBox .btn.btn-update:hover { border-color: #464d8b; color: #dadada; } .dialog .dialogBox .btn.btn-update i { float: right; margin: 0 1px 0 1px; transition: all 0.2s ease-in-out; opacity: 0.5; user-select: none; cursor: pointer; line-height: 20px; } [dir='ltr'] .dialog .dialogBox .btn.btn-update i { float: left; } .dialog .dialogBox .btn.btn-update i:hover { opacity: 0.8; } .customToast { float: right; width: 100%; } .customToast p { float: right; width: 100%; font-size: 14px; line-height: 21px; font-weight: 200; margin: 0 0 10px 0; } .customToast button { float: left; border: none; border-radius: 25px; background: transparent; font-size: 14px; font-weight: 200; transition: all 0.2s ease-in-out; opacity: 0.7; padding: 4px 7px; } .customToast button:hover { opacity: 1; } .drawer { padding: 15px 0; margin: 0; max-width: 310px; user-select: none; } [data-bs-theme='dark'] .drawer { background: #0a0e24 !important; } .drawerOverlay { backdrop-filter: blur(4px); --webkit-backdrop-filter: blur(4px); background-color: rgba(0, 0, 0, 0.25) !important; } @media (min-width: 1050px) { .navMenu, .tabs.inHome, .drawerOverlay { display: none !important; } .EZDrawer .EZDrawer__container { background: transparent !important; box-shadow: none !important; border-left: 1px solid #f3f3f3; } [data-bs-theme='dark'] .EZDrawer .EZDrawer__container { border-left: 1px solid #2b2e3b; } [dir='ltr'] .EZDrawer .EZDrawer__container { border-left: none; border-right: 1px solid #f3f3f3; } [dir='ltr'][data-bs-theme='dark'] .EZDrawer .EZDrawer__container { border-left: none; border-right: 1px solid #2b2e3b; } } [data-bs-theme='dark'] .drawerOverlay { backdrop-filter: blur(3px); --webkit-backdrop-filter: blur(3px); } .drawer .list { float: right; width: 100%; } .drawer .list .appName { float: right; width: 100%; margin: 0 0 15px 0; padding: 0 20px 15px 0; } [dir='ltr'] .drawer .list .appName { float: left; padding: 0 0 15px 20px; } [dir='ltr'] .drawer .list .appName { float: left; } .drawer .list .appName img { float: right; width: 80px; height: 80px; object-fit: cover; margin: 0 -5px 8px 0; } [dir='ltr'] .drawer .list .appName img { float: left; margin: 0 0 8px -5px; } .drawer .list .appName h3 { float: right; width: 100%; margin: 0; padding: 0; font-size: 22px; font-weight: 400; color: #ffa200; } [dir='ltr'] .drawer .list .appName h3 { float: left; text-align: left; } .drawer .list .appName h3 small { font-weight: 200; font-size: 80%; } [data-bs-theme='dark'] .drawer .list .appName h3 small { color: #b0aeae; } .drawer .list ul { float: right; width: 100%; margin: 0; padding: 0; } .drawer .list ul li { float: right; width: 100%; list-style: none; transition: all 0.2s ease-in-out; } [dir='ltr'] .drawer .list ul li { float: left; } .drawer .list ul li:hover { background: #ededed; } [data-bs-theme='dark'] .drawer .list ul li:hover { background: #161c3e; } .drawer .list ul li.divider { border-bottom: 1px solid #f3f3f3; padding: 0; margin: 10px 0; } [data-bs-theme='dark'] .drawer .list ul li.divider { border-color: #2b2e3b; } .drawer .list ul li.divider:hover { background: transparent; } .drawer .list ul li a { float: right; width: 100%; padding: 10px 20px; cursor: pointer; } [dir='ltr'] .drawer .list ul li a { float: left; } .drawer .list ul li i { float: right; color: #b0b0b0; font-size: 22px !important; padding-left: 20px; transition: all 0.2s ease-in-out; } [dir='ltr'] .drawer .list ul li i { float: left; padding-left: 0; padding-right: 20px; margin-top: -2px; } .drawer .list ul li:hover i { color: #8d8d8d; } [data-bs-theme='dark'] .drawer .list ul li:hover i { color: #d0d0d0; } .drawer .list ul li span { float: right; color: #4d4d4d; padding: 1px 0 0 0; font-weight: 300; font-size: 16px; } [dir='ltr'] .drawer .list ul li span { float: left; } [data-bs-theme='dark'] .drawer .list ul li span { color: #e1e1e1; font-weight: 200; } .drawer .list ul li .label { float: left; font-weight: 200; border-radius: 25px; padding: 3px 5px; font-size: 12px; margin: 2px 0 0 0; background: #ffa200; } [dir='ltr'] .drawer .list ul li .label { float: right; } .drawer .loader { float: left; position: relative; width: 7px; height: 7px; margin-top: 8px; margin-inline-end: 9px; border-radius: 4px; background-color: #ffa200; color: #ffa200; animation: dotFlashing 1s infinite linear alternate; animation-delay: 0.5s; } [dir='ltr'] .drawer .loader { float: right; } .drawer .loader::before, .drawer .loader::after { content: ''; display: inline-block; position: absolute; top: 0; } .drawer .loader::before { left: -10px; width: 7px; height: 7px; border-radius: 4px; background-color: #ffa200; color: #ffa200; animation: dotFlashing 1s infinite alternate; animation-delay: 0s; } .drawer .loader::after { left: 10px; width: 7px; height: 7px; border-radius: 4px; background-color: #ffa200; color: #ffa200; animation: dotFlashing 1s infinite alternate; animation-delay: 1s; } @keyframes dotFlashing { 0% { background-color: #ffa200; } 50%, 100% { background-color: #ffa20033; } } .drawer .appVersion { float: right; width: 100%; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; padding: 20px; font-size: 12px; color: #c0c0c0; } .drawer .appVersion b { font-size: 13px; } .tabs { position: fixed; bottom: 0; left: 50%; transform: translateX(-50%); width: 100%; max-width: 385px; margin: 0; z-index: 2; padding: 1px 0; background: rgba(255, 255, 255, 0.5); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); user-select: none; } [data-bs-theme='dark'] .tabs { background: rgba(10, 14, 36, 0.8); } .tabs.inSettings { box-shadow: 0 0 30px rgba(0, 0, 0, 0.1); } [data-bs-theme='dark'] .tabs.inSettings { box-shadow: 0 0 30px rgba(255, 255, 255, 0.02); } @media (min-width: 400px) { .tabs.inSettings, [data-bs-theme='dark'] .tabs.inSettings { box-shadow: none; } } .tabs ul { float: right; width: 100%; margin: 0; padding: 0; transition: all 0.2s ease-in-out; max-height: 61px; } .tabs ul li { float: right; width: 20%; list-style: none; margin: 0; } .tabs.withSingbox ul li { width: 16.66%; } [dir='ltr'] .tabs ul li { float: left; } .tabs ul li a { float: right; width: 100%; padding: 7px 10px; text-decoration: none; transition: all 0.2s ease-in-out; font-size: 13px; font-weight: 300; color: #898989; text-align: center; line-height: 17px; } .tabs.withSingbox ul li a { padding: 15px 10px; } [data-bs-theme='dark'] .tabs ul li a { color: #909090; } .tabs ul li.active a { color: #af8b4c; } [data-bs-theme='dark'] .tabs ul li.active a { color: #749af8; } .tabs ul li a span { float: right; width: 100%; text-align: center; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; transition: all 0.2s ease-in-out; } .tabs.withSingbox ul li a span { opacity: 0; height: 0; } .tabs ul li a i { font-size: 22px; padding: 2px 10px; border-radius: 25px; color: #c0c0c0; transition: all 0.2s ease-in-out; } [data-bs-theme='dark'] .tabs ul li a i { color: #5f5f5f; } .tabs ul li.active a i { background: #b9975b; color: #fff; } [data-bs-theme='dark'] .tabs ul li.active a i { background: #273286; } .contextMenu { background: #fff; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); border-radius: 3px; width: 75px; direction: ltr; z-index: 3; } .contextMenu .menuItem { float: right; width: 100%; padding: 3px 15px; font-size: 12px; color: #2c2c2c; cursor: pointer; border-radius: 3px; transition: all 0.2s ease-in-out; } .contextMenu .menuItem:hover { background: #f8f8f8; color: #1c1c1c; } .speedTest { display: flex; flex-direction: column; align-items: center; justify-content: center; transition: all 0.5s ease; } .speedTest button.startButton { position: relative; width: 120px; height: 120px; margin-top: 100px; border: none; border-radius: 50%; color: #757575; font-size: 55px; font-weight: bold; border: 1px solid #ffa200; transition: all 0.5s ease; animation-name: startButton; animation-iteration-count: infinite; animation-duration: 3s; } [data-bs-theme='dark'] .speedTest button.startButton { color: #fff; } @keyframes startButton { 0% { box-shadow: 0 0 0 rgba(245, 156, 10, 0.4); } 50% { box-shadow: -1px 0px 0 12px rgba(245, 156, 10, 0.15); } 100% { box-shadow: 0 0 0 rgba(245, 156, 10, 0.4); } } .speedTest button.startButton:hover { color: #535353; } [data-bs-theme='dark'] .speedTest button.startButton:hover { color: #ffa200; } .speedTest button.startButton[data-type='disabled'] { background: #fdfdfd; box-shadow: 0 0 5px 5px rgba(117, 116, 116, 0.36); animation: inherit; box-shadow: none; color: #757575 !important; } [data-bs-theme='dark'] .speedTest button.startButton[data-type='disabled'] { background: #0a0e24; color: #fff !important; } .speedTest button.startButton[data-type='enabled'] { background: transparent; cursor: pointer; } .speedTest button.startButton[data-type='finished'] { background: #e1e1e1; box-shadow: 0 0 5px 5px rgba(68, 73, 69, 0.39); cursor: pointer; } [data-bs-theme='dark'] .speedTest button.startButton[data-type='finished'] { background: #3d3d3d; } .testRunning button.startButton::before { content: ''; position: absolute; top: -10px; left: -10px; right: -10px; bottom: -10px; border-radius: 50%; border: 5px solid transparent; border-top: 5px solid #ffa200; border-right: 5px solid #ffa200; animation: spinner 0.8s linear infinite; } .speedTest.testRunning button.startButton, .speedTest.testDone button.startButton { width: 80px; height: 80px; margin-top: 80px; transform: translateY(-80%); box-shadow: none; animation-name: inherit; border-color: #e3e3e3; font-size: 33px; } .speedTest.testDone button.startButton { border-color: #bfbfbf; } [data-bs-theme='dark'] .speedTest.testRunning button.startButton, [data-bs-theme='dark'] .speedTest.testDone button.startButton { border-color: #757575; } .speedTest span.statusMessage { font-size: 15px; margin-top: 70px; font-weight: 200; text-align: center; color: #303030; } [data-bs-theme='dark'] .speedTest span.statusMessage { color: #dcdcdc; } .speedTest .results { display: flex; flex-direction: column; align-items: center; padding-top: 20px; } .speedTest .results .resultRow { display: flex; flex-wrap: wrap; justify-content: center; width: 100%; } .speedTest .results .resultCard { width: 150px; margin: 7px; padding: 12px; text-align: center; background: #f9f9f9; border-radius: 8px; border: 1px solid rgba(0, 0, 0, 0.1); } [data-bs-theme='dark'] .speedTest .results .resultCard { background: #1b2337; border: 1px solid rgba(234, 229, 229, 0.15); } .speedTest .results .resultCard p { color: #666; font-size: 13px; } [data-bs-theme='dark'] .speedTest .results .resultCard p { color: #afaeae; } .speedTest .results .resultCard h2 { color: #333; font-size: 16px; direction: ltr; margin-bottom: 5px; } [data-bs-theme='dark'] .speedTest .results .resultCard h2 { color: #e3e3e3; } .speedTest .results .resultCard h2 small { font-size: 14px; } .progressBar { width: 100%; height: 3px; position: fixed; top: 0; left: 0; overflow: hidden; background: linear-gradient( 135deg, #c4c4c4 25%, transparent 25%, transparent 50%, #c4c4c4 50%, #c4c4c4 75%, transparent 75%, transparent ); background-size: 30px 30px; animation: moveStripes 0.5s linear infinite; will-change: background-position; } [data-bs-theme='dark'] .progressBar { background: linear-gradient( 135deg, #5e5e5e 25%, transparent 25%, transparent 50%, #5e5e5e 50%, #5e5e5e 75%, transparent 75%, transparent ); background-size: 30px 30px; } @keyframes moveStripes { 0% { background-position: 0 0; } 100% { background-position: 30px 0; } } .progressBar div { height: 3px; transition: width 0.3s ease-in-out; float: left; } .progressBar.green div { background: #4caf50; } .progressBar.red div { background: tomato; } ================================================ FILE: assets/entitlements.mac.plist ================================================ com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.allow-jit ================================================ FILE: assets/json/1713988096625.json ================================================ {"nm":"Ella Shimmer","ddd":0,"h":300,"w":300,"meta":{"g":"LottieFiles AE 3.0.2"},"layers":[{"ty":3,"nm":"Null 1","sr":1,"st":0,"op":94,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[-100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[150,150,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":0,"ix":11}},"ef":[],"ind":1},{"ty":0,"nm":"Comp 1","sr":1,"st":62,"op":152,"ip":62,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[100,50,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[150,175,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"w":200,"h":100,"refId":"comp_0","ind":2},{"ty":0,"nm":"Comp 1","sr":1,"st":30,"op":120,"ip":30,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[100,50,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.069,"y":0.995},"s":[100,100,100],"t":62},{"o":{"x":0.333,"y":0},"i":{"x":0.833,"y":1},"s":[80,80,100],"t":76},{"s":[80,80,100],"t":94}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.182,"y":1},"s":[0,25,0],"t":62,"ti":[0,8.583,0],"to":[0,-8.583,0]},{"o":{"x":0.167,"y":0.167},"i":{"x":0.182,"y":0.182},"s":[0,-26.5,0],"t":76,"ti":[0,0,0],"to":[0,0,0]},{"s":[0,-26.5,0],"t":94}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":62},{"o":{"x":0.167,"y":0},"i":{"x":0.833,"y":1},"s":[60],"t":76},{"s":[60],"t":94}],"ix":11}},"ef":[],"w":200,"h":100,"refId":"comp_0","ind":3,"parent":1},{"ty":0,"nm":"Comp 1","sr":1,"st":-2,"op":88,"ip":-2,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[100,50,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.069,"y":0.995},"s":[100,100,100],"t":30},{"o":{"x":0.333,"y":0},"i":{"x":0.833,"y":1},"s":[80,80,100],"t":44},{"o":{"x":0.167,"y":0},"i":{"x":0.833,"y":1},"s":[80,80,100],"t":62},{"s":[50,50,100],"t":76}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.182,"y":1},"s":[150,175,0],"t":30,"ti":[0,8.583,0],"to":[0,-8.583,0]},{"o":{"x":0.167,"y":0.167},"i":{"x":0.182,"y":0.182},"s":[150,123.5,0],"t":44,"ti":[0,0,0],"to":[0,0,0]},{"o":{"x":0.167,"y":0},"i":{"x":0.182,"y":1},"s":[150,123.5,0],"t":62,"ti":[0,6.167,0],"to":[0,-6.167,0]},{"s":[150,86.5,0],"t":76}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":30},{"o":{"x":0.167,"y":0},"i":{"x":0.833,"y":1},"s":[60],"t":44},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[60],"t":62},{"s":[0],"t":76}],"ix":11}},"ef":[],"w":200,"h":100,"refId":"comp_0","ind":4},{"ty":0,"nm":"Comp 1","sr":1,"st":-33,"op":57,"ip":-33,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[100,50,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.069,"y":0.995},"s":[100,100,100],"t":-1},{"o":{"x":0.333,"y":0},"i":{"x":0.833,"y":1},"s":[80,80,100],"t":13},{"o":{"x":0.167,"y":0},"i":{"x":0.833,"y":1},"s":[80,80,100],"t":31},{"s":[50,50,100],"t":45}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.182,"y":1},"s":[0,25,0],"t":-1,"ti":[0,8.583,0],"to":[0,-8.583,0]},{"o":{"x":0.167,"y":0.167},"i":{"x":0.182,"y":0.182},"s":[0,-26.5,0],"t":13,"ti":[0,0,0],"to":[0,0,0]},{"o":{"x":0.167,"y":0},"i":{"x":0.182,"y":1},"s":[0,-26.5,0],"t":31,"ti":[0,6.167,0],"to":[0,-6.167,0]},{"s":[0,-63.5,0],"t":45}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":-1},{"o":{"x":0.167,"y":0},"i":{"x":0.833,"y":1},"s":[60],"t":13},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[60],"t":31},{"s":[0],"t":45}],"ix":11}},"ef":[],"w":200,"h":100,"refId":"comp_0","ind":5,"parent":1},{"ty":0,"nm":"Comp 1","sr":1,"st":-76,"op":14,"ip":-76,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[100,50,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.069,"y":0.995},"s":[100,100,100],"t":-35},{"o":{"x":0.333,"y":0},"i":{"x":0.833,"y":1},"s":[80,80,100],"t":-21},{"o":{"x":0.167,"y":0},"i":{"x":0.833,"y":1},"s":[80,80,100],"t":-1},{"s":[50,50,100],"t":13}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.182,"y":1},"s":[150,175,0],"t":-35,"ti":[0,8.583,0],"to":[0,-8.583,0]},{"o":{"x":0.167,"y":0.167},"i":{"x":0.182,"y":0.182},"s":[150,123.5,0],"t":-21,"ti":[0,0,0],"to":[0,0,0]},{"o":{"x":0.167,"y":0},"i":{"x":0.182,"y":1},"s":[150,123.5,0],"t":-1,"ti":[0,6.167,0],"to":[0,-6.167,0]},{"s":[150,86.5,0],"t":13}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":-35},{"o":{"x":0.167,"y":0},"i":{"x":0.833,"y":1},"s":[60],"t":-21},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[60],"t":-1},{"s":[0],"t":13}],"ix":11}},"ef":[],"w":200,"h":100,"refId":"comp_0","ind":6}],"v":"4.8.0","fr":30,"op":94,"ip":30,"assets":[{"nm":"","id":"comp_0","layers":[{"ty":4,"nm":"Shape Layer 3","sr":1,"st":0,"op":118,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-30,-6.544,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.651,"y":0.998},"s":[0,75.476,100],"t":9},{"o":{"x":0.379,"y":0.013},"i":{"x":0.524,"y":0.97},"s":[110,75.476,100],"t":21},{"s":[100,75.476,100],"t":29}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[81,59.26,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Rectangle 1","ix":1,"cix":2,"np":2,"it":[{"ty":"rc","bm":0,"hd":false,"mn":"ADBE Vector Shape - Rect","nm":"Rectangle Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"s":{"a":0,"k":[85.26,14.271],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.8784,0.9059,0.9373],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[12.63,-8.364],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"Shape Layer 2","sr":1,"st":0,"op":166,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-30,-6.544,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.651,"y":0.997},"s":[0,75.476,100],"t":3},{"o":{"x":0.379,"y":0.027},"i":{"x":0.524,"y":0.94},"s":[90,75.476,100],"t":15},{"s":[85,75.476,100],"t":23}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[81,41.26,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Rectangle 1","ix":1,"cix":2,"np":2,"it":[{"ty":"rc","bm":0,"hd":false,"mn":"ADBE Vector Shape - Rect","nm":"Rectangle Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"s":{"a":0,"k":[85.26,14.271],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.8784,0.9059,0.9373],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[12.63,-8.364],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2},{"ty":4,"nm":"Shape Layer 1","sr":1,"st":0,"op":166,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-66.789,-32.789,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.044,"y":0.991},"s":[0,0,100],"t":0},{"s":[93,93,100],"t":12}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[48.961,49.211,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Rectangle 1","ix":1,"cix":2,"np":2,"it":[{"ty":"rc","bm":0,"hd":false,"mn":"ADBE Vector Shape - Rect","nm":"Rectangle Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"s":{"a":0,"k":[38.422,38.422],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.8941,0],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-66.789,-32.789],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3}]}]} ================================================ FILE: assets/proto/oblivion.proto ================================================ syntax = "proto3"; package oblivionHelper; service OblivionService { rpc Start (StartRequest) returns (StartResponse); rpc Stop (StopRequest) returns (StopResponse); rpc StreamStatus (StatusRequest) returns (stream StatusResponse); rpc Exit (ExitRequest) returns (ExitResponse); } message StartRequest {} message StartResponse { string message = 1; } message StopRequest {} message StopResponse { string message = 1; } message StatusRequest {} message StatusResponse { string status = 1; } message ExitRequest {} message ExitResponse {} ================================================ FILE: package.json ================================================ { "name": "oblivion-desktop", "description": "unofficial desktop version of oblivion", "shortName": "oblivion", "author": "ircfspace+kiomarzsss (https://ircf.space/)", "homepage": "https://github.com/bepass-org/oblivion-desktop#readme", "repository": { "type": "git", "url": "git+https://github.com/bepass-org/oblivion-desktop.git" }, "bugs": { "url": "https://github.com/bepass-org/oblivion-desktop/issues", "email": "ircfspace@gmail.com" }, "version": "3.11.0", "license": "Restrictive", "main": "./.erb/dll/main.bundle.dev.js", "scripts": { "eval": "eval process.", "build": "concurrently \"npm run build:main\" \"npm run build:renderer\"", "build:dll": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts", "build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts", "build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts", "prepare": "ts-node script/dlBins.ts && husky", "postinstall": "ts-node ./script/postinstall.ts", "postinstall:darwin-linux": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll", "postinstall:windows": "ts-node script/makeRegeditVBSAvailable.ts && ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll", "lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx", "package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never && npm run build:dll", "package:linux": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder --linux --dir --publish never && npm run build:dll", "package:mac": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder --mac --dir --publish never && npm run build:dll", "package:windows": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder --windows --dir --publish never && npm run build:dll", "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app", "prestart": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.dev.ts", "start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run prestart && npm run start:renderer", "dev": "npm start", "start:main": "concurrently -k \"cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --watch --config ./.erb/configs/webpack.config.main.dev.ts\" \"electronmon .\"", "start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts", "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts", "test": "jest", "format": "prettier --write . --config .prettierrc --ignore-path .prettierignore --cache", "format:check": "prettier . --check --cache --config .prettierrc --ignore-path .prettierignore", "tsc": "tsc --noEmit --incremental", "changeVersion": "ts-node ./script/changeVersion" }, "browserslist": [], "prettier": { "singleQuote": true, "overrides": [ { "files": [ ".prettierrc", ".eslintrc" ], "options": { "parser": "json" } } ] }, "jest": { "moduleDirectories": [ "node_modules", "release/app/node_modules", "src" ], "moduleFileExtensions": [ "js", "jsx", "ts", "tsx", "json" ], "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/.erb/mocks/fileMock.js", "\\.(css|less|sass|scss)$": "identity-obj-proxy" }, "setupFiles": [ "./.erb/scripts/check-build-exists.ts" ], "testEnvironment": "jsdom", "testEnvironmentOptions": { "url": "http://localhost/" }, "testPathIgnorePatterns": [ "release/app/dist", ".erb/dll" ], "transform": { "\\.(ts|tsx|js|jsx)$": "ts-jest" } }, "dependencies": { "@cloudflare/speedtest": "^1.7.0", "@grpc/grpc-js": "^1.14.2", "@grpc/proto-loader": "^0.8.0", "axios": "^1.13.2", "classnames": "^2.5.1", "electron-log": "^5.4.3", "electron-settings": "^4.0.4", "lodash": "^4.17.21", "node-aplay": "^1.0.3", "react": "^19.2.1", "react-dom": "^19.2.1", "react-hot-toast": "^2.6.0", "react-modern-drawer": "^1.4.0", "react-router": "^7.10.1", "regedit": "^5.1.4", "serve-handler": "^6.1.5", "sound-play": "^1.1.0", "sudo-prompt": "^9.2.1", "systeminformation": "^5.27.13", "tree-kill": "^1.2.2", "zustand": "^5.0.9" }, "devDependencies": { "@electron/notarize": "^2.5.0", "@electron/rebuild": "^3.7.2", "@pmmmwh/react-refresh-webpack-plugin": "^0.6.2", "@svgr/webpack": "^8.1.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@types/decompress": "^4.2.7", "@types/detect-port": "^1.3.5", "@types/electron": "^1.4.38", "@types/jest": "^30.0.0", "@types/lodash": "^4.17.21", "@types/node": "^24.10.2", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/serve-handler": "^6.1.4", "@types/sound-play": "^1.1.3", "@types/webpack-bundle-analyzer": "^4.7.0", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", "chalk": "^4.1.2", "concurrently": "^9.2.1", "cross-env": "^10.1.0", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.3", "decompress": "^4.2.1", "detect-port": "^2.1.0", "electron": "^39.2.6", "electron-builder": "^26.0.12", "electronmon": "^2.0.4", "eslint": "^8.49.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-erb": "^4.1.0-0", "eslint-import-resolver-typescript": "^3.10.1", "eslint-import-resolver-webpack": "^0.13.10", "eslint-plugin-compat": "^4.2.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jest": "^27.4.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "file-loader": "^6.2.0", "html-webpack-plugin": "^5.6.5", "husky": "^9.1.7", "identity-obj-proxy": "^3.0.0", "jest": "^30.2.0", "mini-css-extract-plugin": "^2.9.4", "prettier": "^3.7.4", "react-refresh": "^0.18.0", "rimraf": "^6.1.2", "sass": "^1.95.1", "sass-loader": "^16.0.6", "style-loader": "^4.0.0", "terser-webpack-plugin": "^5.3.15", "ts-jest": "^29.4.6", "ts-loader": "^9.5.4", "ts-node": "^10.9.2", "tsconfig-paths-webpack-plugin": "^4.2.0", "typescript": "^5.9.3", "webpack": "^5.103.0", "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^6.0.1", "webpack-dev-server": "^5.2.2", "webpack-merge": "^6.0.1" }, "build": { "productName": "oblivion-desktop", "executableName": "oblivion-desktop", "appId": "org.desktop.oblivion", "artifactName": "oblivion-desktop-${os}-${arch}.${ext}", "asar": true, "asarUnpack": "**\\*.{node,dll}", "beforePack": "./script/beforePackHook.js", "files": [ "dist", "node_modules", "package.json", "assets/bin/**" ], "afterSign": ".erb/scripts/notarize.js", "protocols": [ { "name": "Oblivion Protocol", "schemes": [ "oblivion" ] } ], "mac": { "target": [ { "target": "dmg", "arch": [ "arm64", "x64" ] }, { "target": "zip", "arch": [ "arm64", "x64" ] } ], "identity": null }, "dmg": { "writeUpdateInfo": true }, "win": { "target": [ { "target": "nsis", "arch": [ "x64", "arm64", "ia32" ] }, { "target": "zip", "arch": [ "x64", "arm64", "ia32" ] } ] }, "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, "differentialPackage": false, "perMachine": true, "createDesktopShortcut": true, "createStartMenuShortcut": true, "runAfterFinish": true, "shortcutName": "Oblivion Desktop" }, "linux": { "target": [ { "target": "AppImage", "arch": [ "arm64", "x64" ] }, { "target": "deb", "arch": [ "arm64", "x64" ] }, { "target": "rpm", "arch": [ "arm64", "x64" ] }, { "target": "tar.xz", "arch": [ "arm64", "x64" ] } ], "description": "Unofficial Warp Client", "category": "Network", "icon": "assets" }, "directories": { "app": "release/app", "buildResources": "assets", "output": "release/build" }, "extraResources": [ "./assets/**" ], "publish": { "provider": "github", "owner": "bepass-org", "repo": "oblivion-desktop", "publishAutoUpdate": true, "private": false, "releaseType": "draft" } }, "engines": { "node": ">=20.x", "npm": ">=10.x" }, "electronmon": { "patterns": [ "!**/**", "src/main/**", ".erb/dll/**" ], "logLevel": "quiet" } } ================================================ FILE: release/app/package.json ================================================ { "name": "oblivion-desktop", "description": "unofficial desktop version of oblivion", "version": "3.11.0", "homepage": "https://github.com/bepass-org/oblivion-desktop#readme", "license": "Restrictive", "author": "ircfspace+kiomarzsss (https://ircf.space/)", "bugs": { "url": "https://github.com/bepass-org/oblivion-desktop/issues", "email": "ircfspace@gmail.com" }, "main": "./dist/main/main.js", "scripts": { "rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", "postinstall": "npm run rebuild && npm run link-modules", "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts" }, "dependencies": {} } ================================================ FILE: script/beforePackHook.js ================================================ const util = require('util'); const exec = util.promisify(require('child_process').exec); exports.default = async function (context) { const archDict = { 0: 'ia32', 1: 'x64', 3: 'arm64' }; // TODO don't force download when packaging on the local platform const { stdout, stderr } = await exec( `npm exec ts-node script/dlBins.ts force ${context.electronPlatformName} ${archDict[context.arch]}` ); if (stderr) { console.error(stderr); } console.log(stdout); }; ================================================ FILE: script/changeVersion.ts ================================================ import fs from 'fs'; import path from 'path'; import { exec } from 'child_process'; let v = process.argv[2]; if (v.charAt(0) === 'v') { v = v.substring(1); } function changeJson(filePath: string, key: string, value: string, callback: Function) { fs.readFile(filePath, 'utf8', (error, data) => { if (error) { console.error(error); return; } const json = JSON.parse(data); json[key] = value; fs.writeFile(filePath, JSON.stringify(json), 'utf8', (error2) => { if (error2) { console.error(error2); return; } callback(); }); }); } changeJson(path.resolve(__dirname, '../package.json'), 'version', v, () => { console.log('edited package.json'); changeJson(path.resolve(__dirname, '../release/app/package.json'), 'version', v, () => { console.log('edited release/app/package.json'); console.log('npm run format...'); exec('npm run format', (err) => { if (err) { console.error(err); return; } console.log('git add package.json release/app/package.json'); exec('git add package.json release/app/package.json', (err2) => { if (err2) { console.error(err2); return; } console.log(`git commit -m "🔖 ${v}"`); exec(`git commit -m "🔖 ${v}"`, (err3, stdout2) => { if (err3) { console.error(err3); return; } console.log(stdout2); console.log('git tag v' + v); exec('git tag v' + v, (err4) => { if (err4) { console.error(err4); return; } console.log(`git push`); exec(`git push`, (err5, stdout3) => { if (err5) { console.error(err5); return; } console.log(stdout3); console.log('git push --tags'); exec('git push --tags', (err6, stdout4) => { if (err6) { console.error(err6); return; } console.log(stdout4); }); }); }); }); }); }); }); }); ================================================ FILE: script/dlBins.ts ================================================ import fs from 'fs'; import axios from 'axios'; import decompress from 'decompress'; import { doesDirectoryExist, doesFileExist } from '../src/main/lib/utils'; import { wpVersion, helperVersion, netStatsVersion, proxyResetVersion, mpVersion } from '../src/main/config'; const forceDownload = process.argv[2] === 'force'; const platform = process.argv[3] || process.platform; const arch = process.argv[4] || process.arch; console.log('➡️ platform:', platform); console.log('➡️ arch:', arch); async function downloadFile(uri: string, destPath: string) { try { const response = await axios.get(uri, { responseType: 'arraybuffer', onDownloadProgress: (progressEvent) => { const percentCompleted = Math.round( (progressEvent.loaded * 100) / (progressEvent.total || 1) ); try { process?.stdout?.clearLine(0); process?.stdout?.cursorTo(0); process?.stdout?.write(`Downloading ${uri}: ${percentCompleted}%`); } catch (error) { if ( !String(error).includes( 'TypeError: process?.stdout?.clearLine is not a function' ) ) { console.error(error); } } } }); const buffer = Buffer.from(new Uint8Array(response.data)); fs.writeFileSync(destPath, buffer); console.log(); } catch (error: any) { console.error(`Failed to download ${uri}:`, error.message); } } async function dlUnzipMove(url: string, binPath: string, zipFileName: string) { const isBinDirExist = await doesDirectoryExist(binPath); if (!isBinDirExist) { fs.mkdirSync(binPath, { recursive: true }); } const zipFilePath = `./${zipFileName}`; const isZipFileExist = await doesFileExist(zipFilePath); if (!isZipFileExist || forceDownload) { console.log(`Downloading ${zipFileName} binary based on your platform and architecture...`); await downloadFile(url, zipFilePath); } else { console.log(`➡️ Skipping Download as ${zipFilePath} already exists.`); } try { await decompress(zipFilePath, binPath, { strip: 1 }); console.log(`✅ ${zipFileName} binary is ready to use.`); } catch (error) { console.error(error); } } const warpPlusUrlBase = `https://github.com/bepass-org/warp-plus/releases/download/v${wpVersion}/warp-plus_`; const helperUrlBase = `https://github.com/Dr-Bad/oblivion-helper/releases/download/v${helperVersion}/oblivion-helper-`; const netStatsUrlBase = `https://github.com/ShadowZagrosDev/Zag-NetStats/releases/download/v${netStatsVersion}/zag-netStats-`; const proxyResetUrlBase = `https://github.com/ircfspace/proxyReset/releases/download/v${proxyResetVersion}/proxy-reset-`; const masquePlusUrlBase = `https://github.com/ircfspace/masque-plus/releases/download/v${mpVersion}/masque-plus-`; const warpPlusUrls: Record> = { linux: { x64: warpPlusUrlBase + 'linux-amd64.zip', arm64: warpPlusUrlBase + 'linux-arm64.zip' }, win32: { x64: warpPlusUrlBase + 'windows-amd64.zip', arm64: warpPlusUrlBase + 'windows-arm64.zip', ia32: warpPlusUrlBase + 'windows-386.zip' }, darwin: { x64: warpPlusUrlBase + 'darwin-amd64.zip', arm64: warpPlusUrlBase + 'darwin-arm64.zip' } }; const helperUrls: Record> = { linux: { x64: helperUrlBase + 'linux-amd64.zip', arm64: helperUrlBase + 'linux-arm64.zip' }, win32: { x64: helperUrlBase + 'windows-amd64.zip', arm64: helperUrlBase + 'windows-arm64.zip', ia32: helperUrlBase + 'windows-386.zip' }, darwin: { x64: helperUrlBase + 'darwin-amd64.zip', arm64: helperUrlBase + 'darwin-arm64.zip' } }; const netStatsUrls: Record> = { linux: { x64: netStatsUrlBase + 'linux-amd64.zip', arm64: netStatsUrlBase + 'linux-arm64.zip' }, win32: { x64: netStatsUrlBase + 'windows-amd64.zip', arm64: netStatsUrlBase + 'windows-arm64.zip', ia32: netStatsUrlBase + 'windows-386.zip' }, darwin: { x64: netStatsUrlBase + 'darwin-amd64.zip', arm64: netStatsUrlBase + 'darwin-arm64.zip' } }; const proxyResetUrls: Record> = { win32: { x64: proxyResetUrlBase + 'x64.zip', arm64: proxyResetUrlBase + 'arm64.zip', ia32: proxyResetUrlBase + 'ia32.zip' } }; const masquePlusUrls: Record> = { linux: { x64: masquePlusUrlBase + 'linux_amd64.zip', arm64: masquePlusUrlBase + 'linux_arm64.zip' }, win32: { x64: masquePlusUrlBase + 'windows_amd64.zip', arm64: masquePlusUrlBase + 'windows_arm64.zip', ia32: masquePlusUrlBase + 'windows_amd64.zip' // This architecture is not supported, but it is listed to prevent errors }, darwin: { x64: masquePlusUrlBase + 'darwin_amd64.zip', arm64: masquePlusUrlBase + 'darwin_arm64.zip' } }; const removeFile = async (filePath: string) => { const isExist = await doesFileExist(filePath); if (isExist) { fs.rm(filePath, (err) => { if (err) console.error(`Error removing ${filePath}:`, err); }); } }; async function handleDownload() { await dlUnzipMove(warpPlusUrls[platform][arch], './assets/bin', `warp-plus-v${wpVersion}.zip`); await removeFile('./assets/bin/wintun.dll'); await dlUnzipMove( helperUrls[platform][arch], './assets/bin/oblivion-helper', `oblivion-helper-v${helperVersion}.zip` ); await dlUnzipMove( netStatsUrls[platform][arch], './assets/bin', `zag-netStats-v${netStatsVersion}.zip` ); if (platform === 'win32') { await dlUnzipMove( proxyResetUrls[platform][arch], './assets/bin', `proxy-reset-v${proxyResetVersion}.zip` ); } await dlUnzipMove( masquePlusUrls[platform][arch], './assets/bin', `masque-plus-v${mpVersion}.zip` ); } const notSupported = () => console.log('Your platform/architecture is not supported.'); switch (platform) { case 'linux': case 'win32': case 'darwin': switch (arch) { case 'x64': case 'arm64': case 'ia32': handleDownload().catch(notSupported); break; default: notSupported(); break; } break; default: notSupported(); break; } ================================================ FILE: script/makeRegeditVBSAvailable.ts ================================================ // https://www.npmjs.com/package/regedit#a-note-about-electron import fs from 'fs'; (async () => { const vbsAssetsPath = './node_modules/regedit/vbs'; const vbsDirPath = './assets/bin/vbs'; if (process.platform === 'win32') { fs.mkdir(vbsDirPath, { recursive: true }, (err) => { if (err) { console.error(`Error creating directory ${vbsDirPath}:`, err); } fs.cp(vbsAssetsPath, vbsDirPath, { recursive: true }, (err2) => { if (err2) throw err2; console.log('✅ regedit wsf files are ready to use.\n'); }); }); } })(); ================================================ FILE: script/playground.ts ================================================ import { doesFileExist } from '../src/main/lib/utils'; (async () => { const tmp = await doesFileExist('bin'); console.log('🚀 - tmp:', tmp); })(); ================================================ FILE: script/postinstall.ts ================================================ import { exec } from 'child_process'; (async () => { if (process.platform === 'win32') { exec('npm run postinstall:windows"', (err, stdout) => { if (err) { console.error(err); return; } console.log(stdout); }); } else { exec('npm run postinstall:darwin-linux', (err, stdout) => { if (err) { console.error(err); return; } console.log(stdout); }); } })(); ================================================ FILE: src/__tests__/App.test.tsx ================================================ import '@testing-library/jest-dom'; import { render } from '@testing-library/react'; import App from '../renderer/App'; describe('App', () => { it('should render', () => { expect(render()).toBeTruthy(); }); }); ================================================ FILE: src/constants.ts ================================================ import { app } from 'electron'; import path from 'path'; import SingBoxManager from './main/lib/sbManager'; import NetStatsManager from './main/lib/netStatsManager'; import SpeedTestManager from './main/lib/speedTestManager'; //Platforms export const isWindows = process.platform === 'win32'; export const isLinux = process.platform === 'linux'; export const isDarwin = process.platform === 'darwin'; // Constants export const appVersion = app.getVersion(); export const wpFileName = `warp-plus${isWindows ? '.exe' : ''}`; export const helperFileName = `oblivion-helper${isWindows ? '.exe' : ''}`; export const netStatsFileName = `zag-netStats${isWindows ? '.exe' : ''}`; export const proxyResetFileName = `proxy-reset${isWindows ? '.exe' : ''}`; export const usqueFileName = `usque${isWindows ? '.exe' : ''}`; export const mpFileName = `masque-plus${isWindows ? '.exe' : ''}`; export const sbConfigName = 'sbConfig.json'; export const sbExportListName = 'sbExportList.json'; export const sbCacheName = 'sbCache.db'; export const sbLogName = 'sing-box.log'; export const protoName = `oblivion.proto`; export const ruleSetBaseUrl = 'https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/'; // Paths const appPath = app.getAppPath().replace('/app.asar', '').replace('\\app.asar', ''); export const binAssetPath = path.join(appPath, 'assets', 'bin'); export const wpAssetPath = path.join(binAssetPath, wpFileName); export const helperAssetPath = path.join(binAssetPath, 'oblivion-helper', helperFileName); export const netStatsAssetPath = path.join(binAssetPath, netStatsFileName); export const proxyResetAssetPath = path.join(binAssetPath, proxyResetFileName); export const usqueAssetPath = path.join(binAssetPath, usqueFileName); export const mpAssetPath = path.join(binAssetPath, mpFileName); export const regeditVbsDirPath = path.join(binAssetPath, 'vbs'); export const protoAssetPath = path.join(appPath, 'assets', 'proto', protoName); export const workingDirPath = app.getPath('userData'); export const wpBinPath = path.join(workingDirPath, wpFileName); export const helperPath = path.join(workingDirPath, helperFileName); export const sbConfigPath = path.join(workingDirPath, sbConfigName); export const sbExportListPath = path.join(workingDirPath, sbExportListName); export const sbLogPath = path.join(workingDirPath, sbLogName); export const sbCachePath = path.join(workingDirPath, sbCacheName); export const ruleSetDirPath = path.join(workingDirPath, 'ruleset'); export const netStatsPath = path.join(workingDirPath, netStatsFileName); export const proxyResetPath = path.join(workingDirPath, proxyResetFileName); export const usquePath = path.join(workingDirPath, usqueFileName); export const mpPath = path.join(workingDirPath, mpFileName); export const versionFilePath = path.join(workingDirPath, 'ver.txt'); export const stuffPath = path.join(workingDirPath, 'stuff'); export const logPath = path.join(app?.getPath('logs'), 'main.log'); export const soundEffect = path.join(appPath, 'assets', 'sound', 'notification.wav'); export const exclusionsPath = path.join(app?.getPath('temp'), 'exclusions.bat'); export const downloadedPath = path.join(app?.getPath('temp'), `oblivion-temp`); export const updaterPath = path.join(workingDirPath, `oblivion-updater${isWindows ? '.exe' : ''}`); export const windowPosition = path.join(workingDirPath, 'windowPosition.json'); // Managers export const singBoxManager = new SingBoxManager(); export const netStatsManager = new NetStatsManager(); export const speedTestManager = new SpeedTestManager(); //Interfaces export interface INetStats { sentSpeed: { value: number; unit: string }; recvSpeed: { value: number; unit: string }; totalSent: { value: number; unit: string }; totalRecv: { value: number; unit: string }; totalUsage: { value: number; unit: string }; } export interface IConfig { socksIp: string; socksPort: number; tunMtu: number; logLevel: string; tunStack: string; tunSniff: boolean; plainDns: string; DoHDns: string; tunEndpoint: string; tunAddr: string[]; udpBlock: boolean; discordBypass: boolean; } export interface IGeoConfig { geoIp: string; geoSite: string; geoBlock: boolean; geoNSFW: boolean; } export interface IRoutingRules { ipSet: string[]; domainSet: string[]; domainSuffixSet: string[]; processSet: string[]; } export interface ICommand { command: string; args?: string[]; } export interface IPlatformHelper { start(binPath: string): ICommand; running(processName: string): ICommand; } //Lists export const defaultWarpIPs = [ '162.159.192.0/24', '162.159.193.0/24', '162.159.195.0/24', '188.114.96.0/24', '188.114.97.0/24', '188.114.98.0/24', '188.114.99.0/24', '2606:4700:d0::/64', '2606:4700:d1::/64' ]; export const gtk4Paths = [ '/usr/lib/libgtk-4.so', '/usr/lib/libgtk-4.so.1', '/usr/lib64/libgtk-4.so', '/usr/lib64/libgtk-4.so.1', '/usr/lib/x86_64-linux-gnu/libgtk-4.so', '/usr/lib/x86_64-linux-gnu/libgtk-4.so.1', '/usr/local/lib/libgtk-4.so', '/usr/local/lib/libgtk-4.so.1' ]; ================================================ FILE: src/defaultSettings.ts ================================================ import { DropdownItem } from './renderer/components/Dropdown'; export type settingsKeys = | 'scan' | 'endpoint' | 'ipType' | 'port' | 'location' | 'license' | 'theme' | 'lang' | 'systemTray' | 'flag' | 'ipData' | 'routingRules' | 'autoSetProxy' | 'proxyMode' | 'shareVPN' | 'hostIP' | 'method' | 'dns' | 'rtt' | 'openAtLogin' | 'autoConnect' | 'startMinimized' | 'reserved' | 'scanResult' | 'profiles' | 'forceClose' | 'shortcut' | 'dataUsage' | 'asn' | 'closeHelper' | 'singBoxMTU' | 'singBoxGeoIp' | 'singBoxGeoSite' | 'singBoxGeoBlock' | 'singBoxGeoNSFW' | 'singBoxLog' | 'singBoxStack' | 'singBoxSniff' | 'singBoxAddrType' | 'singBoxUdpBlock' | 'singBoxDiscordBypass' | 'restartCounter' | 'betaRelease' | 'soundEffect' | 'plainDns' | 'DoH' | 'testUrl' | 'updaterVersion' | 'networkList' | 'connectTimeout'; const date = new Date(); const getTimeZone = date?.toString().toLowerCase(); /*const platform = typeof window !== 'undefined' && window.platformAPI ? window.platformAPI.getPlatform() : 'unknown';*/ export const defaultSettings = { scan: true, endpoint: 'engage.cloudflareclient.com:2408', ipType: '', port: 8086, location: '', license: '', theme: 'light', lang: getTimeZone?.includes('iran') ? 'fa' : 'en', systemTray: false, flag: 'xx', ipData: true, routingRules: '', autoSetProxy: true, proxyMode: 'tun', shareVPN: false, hostIP: '127.0.0.1', method: 'gool', dns: '', rtt: '1s', openAtLogin: false, autoConnect: false, startMinimized: false, reserved: true, scanResult: '', profiles: '[]', forceClose: false, shortcut: true, dataUsage: false, asn: 'UNK', closeHelper: true, singBoxMTU: 9000, singBoxGeoBlock: false, singBoxGeoNSFW: false, singBoxSniff: true, singBoxUdpBlock: false, singBoxDiscordBypass: false, restartCounter: 0, betaRelease: false, soundEffect: false, testUrl: 'https://connectivity.cloudflareclient.com/cdn-cgi/trace', plainDns: '', DoH: '', updaterVersion: null, networkList: '[]', connectTimeout: '2m' }; export const countries: DropdownItem[] = [ { value: 'AU', label: '🇦🇺 Australia' }, { value: 'AT', label: '🇦🇹 Austria' }, { value: 'BE', label: '🇧🇪 Belgium' }, { value: 'BG', label: '🇧🇬 Bulgaria' }, //{ value: 'BR', label: '🇧🇷 Brazil' }, { value: 'CA', label: '🇨🇦 Canada' }, { value: 'HR', label: '🇭🇷 Croatia' }, { value: 'CH', label: '🇨🇭 Switzerland' }, { value: 'CZ', label: '🇨🇿 Czechia' }, { value: 'DE', label: '🇩🇪 Germany' }, { value: 'DK', label: '🇩🇰 Denmark' }, { value: 'EE', label: '🇪🇪 Estonia' }, { value: 'ES', label: '🇪🇸 Spain' }, { value: 'FI', label: '🇫🇮 Finland' }, { value: 'FR', label: '🇫🇷 France' }, { value: 'GB', label: '🇬🇧 United Kingdom' }, { value: 'HU', label: '🇭🇺 Hungary' }, { value: 'IE', label: '🇮🇪 Ireland' }, { value: 'IN', label: '🇮🇳 India' }, //{ value: 'ID', label: '🇮🇩 Indonesia' }, { value: 'IT', label: '🇮🇹 Italy' }, { value: 'JP', label: '🇯🇵 Japan' }, { value: 'LV', label: '🇱🇻 Latvia' }, { value: 'NL', label: '🇳🇱 Netherlands' }, { value: 'NO', label: '🇳🇴 Norway' }, { value: 'PL', label: '🇵🇱 Poland' }, { value: 'PT', label: '🇵🇹 Portugal' }, { value: 'RO', label: '🇷🇴 Romania' }, { value: 'RS', label: '🇷🇸 Serbia' }, { value: 'SE', label: '🇸🇪 Sweden' }, { value: 'SG', label: '🇸🇬 Singapore' }, { value: 'SK', label: '🇸🇰 Slovakia' }, //{ value: 'UA', label: '🇺🇦 Ukraine' }, { value: 'US', label: '🇺🇸 United States' } ]; export const languages: DropdownItem[] = [ { value: 'fa', label: 'فارسی' }, { value: 'en', label: 'English' }, { value: 'cn', label: '中文' }, { value: 'ru', label: 'Русский' }, { value: 'tr', label: 'Türkçe' }, { value: 'id', label: 'Indonesia' }, { value: 'ar', label: 'العربية' }, { value: 'pt', label: 'Português (Brasil)' }, { value: 'vi', label: 'Tiếng Việt' }, { value: 'ur', label: 'اردو' } ]; export const dnsServers: DropdownItem[] = [ { value: '1.1.1.1', label: 'Cloudflare' }, { value: '1.1.1.2', label: 'Cloudflare Security' }, { value: '1.1.1.3', label: 'Cloudflare Family' }, { value: 'local', label: 'Local Resolver' }, { value: 'custom', label: 'Custom' } ]; export const singBoxGeoIp: { label: string; geoIp: string }[] = [ { label: 'None', geoIp: 'none' }, { label: '🇮🇷 Iran', geoIp: 'ir' }, { label: '🇨🇳 China', geoIp: 'cn' }, { label: '🇷🇺 Russia', geoIp: 'ru' }, { label: '🇦🇫 Afghanistan', geoIp: 'af' }, { label: '🇹🇷 Turkey', geoIp: 'tr' }, { label: '🇮🇩 Indonesia', geoIp: 'id' }, { label: '🇧🇷 Brazil', geoIp: 'br' } ]; export const singBoxGeoSite: { label: string; geoSite: string }[] = [ { label: 'None', geoSite: 'none' }, { label: '🇮🇷 Iran', geoSite: 'ir' }, { label: '🇨🇳 China', geoSite: 'cn' }, { label: '🇷🇺 Russia', geoSite: 'category-ru' } ]; export const singBoxLog: DropdownItem[] = [ { value: 'disabled', label: 'Disabled' }, { value: 'trace', label: 'Trace' }, { value: 'debug', label: 'Debug' }, { value: 'info', label: 'Info' }, { value: 'warn', label: 'Warn' }, { value: 'error', label: 'Error' }, { value: 'fatal', label: 'Fatal' }, { value: 'panic', label: 'Panic' } ]; export const singBoxStack: DropdownItem[] = [ { value: 'mixed', label: 'Mixed' }, { value: 'system', label: 'System' }, { value: 'gvisor', label: 'gVisor' } ]; export const singBoxAddrType: DropdownItem[] = [ { value: 'v64', label: 'Automatic' }, { value: 'v4', label: 'IPv4' }, { value: 'v6', label: 'IPv6' } ]; export const defaultRoutingRules: { type: string; value: string }[] = [ { type: 'ip', value: '127.0.0.1' }, { type: 'domain', value: '!test.ir' }, { type: 'domain', value: '*.ir' }, { type: 'domain', value: 'dolat.ir' }, { type: 'domain', value: 'apps.apple.com' }, { type: 'app', value: 'Figma' }, { type: 'domain', value: 'figma.com' }, { type: 'domain', value: 'github.com' }, { type: 'domain', value: 'objects.githubusercontent.com' }, { type: 'domain', value: 'meet.google.com' }, { type: 'domain', value: 'bargheman.com' }, { type: 'domain', value: 'digikala.com' }, { type: 'domain', value: 'web.whatsapp.com' }, { type: 'domain', value: 'aparat.com' }, { type: 'domain', value: 'chatgpt.com' } ]; ================================================ FILE: src/localization/am.ts ================================================ import { Language } from './type'; const amharic: Language = { global: {}, status: { connecting: 'እንደምንም በማስተካከል ...', connected: 'ተገናኙ', connected_confirm: 'ተገናኙ', disconnecting: 'እንደምንም ስም በማስተካከል ...', disconnected: 'ተለይተዋል', ip_check: 'IP ተመልከት ...', keep_trying: 'እባኮትን አንደኛ ጊዜ ሞክሩ!', preparing_rulesets: 'እየተዘጋጀ ነው የህጎች ዝርዝር...', downloading_rulesets_failed: 'የህጎች ዝርዝር መካወት አልተሳካም።' }, home: { title_warp_based: 'Warp በመሠረት', drawer_settings_warp: 'Warp ቅኝት', drawer_settings_routing_rules: 'መንገድ ስሌት', drawer_settings_app: 'መተግበሪያ ቅኝት', drawer_settings_scanner: 'ስካነር ቅኝት', drawer_settings_network: 'መሳሪያ ቅኝት', drawer_log: 'መተግበሪያ ማህተም', drawer_update: 'ማዘመን', drawer_update_label: 'አዳዲስ ማዘመን', drawer_speed_test: 'አሳፋሪ ሙከራ', drawer_about: 'ስለ መተግበሪያ', drawer_lang: 'ቋንቋ ለማቀዝቀዝ', drawer_singbox: 'ታክክል ቅኝት' }, toast: { ip_check_please_wait: 'እባኮትን እስከ ማስተካከል በማስተካከል ጊዜ ትንሽ ጊዜ ቆይተው እባኮትን ሞክሩ!', ir_location: 'Cloudflare በኢራን ስፍራ የተገናኘ ኢፒ በአሁኑ ጊዜ ከእርስዎ እንደአማካይ ኢፒ ሲሆን፣ እርስዎ ከምስራቅ የማህበረሰብ ተቀባችሁ ይቀላቀላሉ፣ እንግዲኛም ሳንስሸክት ይሆናሉ። አሁን በማስተካከል ማነፃፀሪያ ማቀዝቀዝ በመንገድ።', btn_submit: 'ተቀባቸው', copied: 'ተተኮር፣ ተለዋል!', cleared: 'ማህተም እንደምንም አርከት!', please_wait: 'እባኮትን ቆይተው ተጠባበቁ...', offline: 'ከኢንተርኔት እንደ ተመላከት ያልሆነ!', settings_changed: 'ቅኝት ለማድረግ በግምት ይገባል!', hardware_usage: 'ይህን አማራጭ ማድረግ በማህተም ሁኔታ በሁሉም ማንሣብ አሰፋላለት!', config_added: 'እንደምንም ቅኝት ሰጠውም!', profile_added: 'አዲስ ነባሪ አውታረ ስር ያሳዩ!', endpoint_added: 'ተስተካክለት አሳዩ፣ ያስተካከልል።', new_update_notification: 'አዲስ ስሪት ተዘጋጅቷል', new_update: 'የመተግበሪያው አዲስ ስሪት ዝግጁ ነው። እንዲያው አውርዶ ለመግጠም ዝግጁ ይደረጋል?', up_to_date: 'እትሙ የመተግበሪያውን አዲስ ስሪት እየጠቀሙ ነው', exit_pending: 'መተግበሪያው የመውጣት ሂደቱን እየጨረሰ ነው፤ እባክዎ እንደገና ከመጀመሩ በፊት ጥቂት ጊዜ ይጠብቁ።', help_btn: 'እርዳታ' }, settings: { title: 'የዋርፕ ቅንብሮች', more: 'ተጨማሪ ቅንብሮች', method_warp: 'ዋርፕ', method_warp_desc: 'ዋርፕን እንዲተካ ያስተካክሉ', method_gool: 'ጉል', method_gool_desc: 'WarpInWarpን እንዲተካ ያስተካክሉ', method_masque: 'ማስክ', method_masque_desc: 'ማስክን እንዲሰራ ያንቀሳቅሱ', method_psiphon: 'ፒሲፎን', method_psiphon_desc: 'ፒሲፎንን እንዲተካ ያስተካክሉ', method_psiphon_location: 'ሀገር', method_psiphon_location_auto: 'አሰባሰብ', method_psiphon_location_desc: 'ተፈላጊ ሀገር IPን ይምረጡ', endpoint: 'ኤንድፖይንት', endpoint_desc: 'IP ወይም የድር ስም እና ፖርት ተያይዞ ይሰሩ', license: 'ፈቃድ', license_desc: 'ፈቃድ እንዲጠቀም ሁለት ጊዜ ሲሆን ተጠቃሚውን ይሳተፋል', option: 'የስምምነት ቅንብሮች', network: 'ኔትወርክ ቅንብሮች', proxy_mode: 'ኮንፊግሬሽን', proxy_mode_desc: 'እንዲሰሩ ፕሮክሲ ቅንብሮች ተይዙ', port: 'ፕሮክሲ ፖርት', port_desc: 'ስምምነት ፕሮክሲ ፖርት ተይዙ', share_vpn: 'አድራሻ መያዣ', share_vpn_desc: 'በኔትወርክ ፕሮክሲን እንዲጋሩ', dns: 'DNS', dns_desc: 'በፕሮክሲ ፍርምን አስተካክሉ', dns_error: 'ይህ በዋርፕና ጉል ሁሉም አስተካክልበት ሊሆን ነው', ip_data: 'IP ማረጋገጫ', ip_data_desc: 'ከግንኙነት በኋላ IP እና ቦታን አሳይ', data_usage: 'የመረጃ አጠቃቀም', data_usage_desc: 'መረጃን ስለአጠቃቀምና እባብነት የሚሆን ስፖርትን አሳይ', dark_mode: 'ዳርክ ሞዴ', dark_mode_desc: 'የአፕሊኬሽን እንዲታየ ሞዴን ተይዙ', lang: 'ቋንቋ', lang_desc: 'የአፕሊኬሽን ተቀባይነት ቋንቋ ቀይር', open_login: 'በማስገባት ሂደት ጀምር', open_login_desc: 'በስርዓት መነሻ እንዲክፈት', auto_connect: 'በስምምነት ተገናኝ', auto_connect_desc: 'አፕሊኬሽን በማክፈት እንዲገናኝ', start_minimized: 'እንደተቆሰረ እንደመነሻ እንቀሳቅስ', start_minimized_desc: 'ሲነሳ በመተግበሪያ ተቆም እንደተቆሰረ', system_tray: 'ስርዓት ትሬይ', system_tray_desc: 'በመስክ ላይ አፕሊኬሽን አምባልን አታሳይም', force_close: 'ግፊት ቅጣት', force_close_desc: 'በመዝጊያ ላይ ስርዓት ትሬይ ላይ አታቆማም', shortcut: 'አቀማመጥ', shortcut_desc: 'በመነሻ ገፅ ላይ ቅርጸ ተግባር ይኖራል', sound_effect: 'የድምፅ ተፅዕኖ', sound_effect_desc: 'በተሳካ መገናኘት ላይ ድምፅ ይጫወታል።', restore: 'እንደ አማራጭ ምላሽ', restore_desc: 'ተመናበረ ስምምነት ምትክ አቀርበዋል', scanner: 'እንቅስቃሴ ቅንብሮች', scanner_alert: 'እንቅስቃሴ አሳሳቢ በመምጣት ሲሆን በተለመዱት ኤንድፖይንት አድራሻ እንቅስቃሴ እንዲነበብ ተነስቷል', scanner_ip_type: 'ኤንድፖይንት አይነት', scanner_ip_type_auto: 'አሰባሰብ', scanner_ip_type_desc: 'ኤንድፖይንት IP ለማስፈን ተይዙ', scanner_rtt: 'ስብስብ', scanner_rtt_default: 'ተለመዱ', scanner_rtt_desc: 'እንቅስቃሴ RTT ውጤት', scanner_reserved: 'እንቅስቃሴ የሚያስተናግድ', scanner_reserved_desc: 'የwireguard ማስተናገድ እቅድ አሳይ', routing_rules: 'ግምገማ', routing_rules_desc: 'እባኮት ከመማሪያ እንቅስቃሴ በመሆኑ የማሳተፍ አሳሳቢ አቅም ይገናኝ', routing_rules_disabled: 'እንቅስቃሴ አልተገበረም', routing_rules_items: 'አሁን የታወቀ አንባሳት', profile: 'መለያ', profile_desc: 'እንዲሁም የታወቀ ተመልከት ማስተላለፊያ', singbox: 'ታክክል ቅኝት', close_singbox: 'ተዘግተው ይቆሟል', close_singbox_desc: 'በአቅም የማስተናገድ ተቋማት ተቀንሳል', close_helper: 'ማሳሰቢያ ተቆራከር', close_helper_desc: 'በማቅረብ እንዲቆረጠሉ በማንነት አሳይ', mtu: 'MTU የመለኪያ መጠን', mtu_desc: 'መች ቅንብሮች ማቅረብ ተይዙ', geo_block: 'ገምታት', geo_block_desc: 'ማህበረሰብ፣ ማስተናገድ፣ ማተሚያ እና ኪሪፕቶ ማስተናገድ', geo_rules_ip: 'IP መለያየት', geo_rules_ip_desc: 'GeoIP ስም ማስተናገድ', geo_rules_site: 'የአውትባ ወሳኝ', geo_rules_site_desc: 'GeoSite አማካኝነት ስምንበት', geo_nsfw_block: 'የማጣሪያ ይዘት', geo_nsfw_block_desc: 'የ NSFW ድረ-ገጾችን መገደብ', more_helper: 'ማሳሰቢያ ቅንብሮች', singbox_log: 'እትም አንደኛ', singbox_log_desc: 'እትም ሁለትን በመመን የምሳሌዎች', singbox_stack: 'እትም', singbox_stack_desc: 'እትም መመን ያሳይ', singbox_sniff: 'እንቅስቃሴ ይሳተፋል', singbox_sniff_desc: 'እንቅስቃሴ ላይ የማስተናገድ ትኩረት ተቀንሷል', singbox_addressing: 'መድረሻ', singbox_addressing_desc: 'የበገጠ በይነገጽ አይነት ማሰናጃ ይስተካከሉ', singbox_udp_block: 'የUDP መንገድን መዝጋት', singbox_udp_block_desc: 'የUDP ትራፊክን በፍጹም መንገድ መዝጋት', singbox_discord_bypass: 'Discord', singbox_discord_bypass_desc: 'የ Discord መከላከያን መዝገብ ማስፈታት', more_duties: 'ተጨማሪ ዕርምጃዎች', beta_release: 'እባብ እንቅስቃሴ', beta_release_desc: 'ስለ እንቅስቃሴ ከፊት በፊት ማህበረሰብ መረጃ ማግኘት' }, tabs: { home: 'เชื่อมต่อ', warp: 'วาร์ป', network: 'เครือข่าย', scanner: 'เครื่องสแกน', app: 'แอป', singbox: 'ቱኔል' }, modal: { endpoint_title: 'እንቅስቃሴ', license_title: 'ፈቃድ', license_desc: 'እባኮትን፣ ፕሮግራሙ እንደሆነ በዚህ ላይ ፈቃድዎን በመጠቀም ሊሄድ እንደሚችል እንዲሆን ፈቃድ አሳምንት አላስፈላጊ ነው።', form_clear: 'አጽዳ', test_url_title: 'ቴስት ዩአርኤል', test_url_desc: 'ግንኙነት ሙከራ አድራሻ', test_url_update: 'የሐሳብ አቀራረብ መቀበያ', port_title: 'ፕሮክሲ ፖርት', restore_title: 'ለመመንገድ ስለሚሞከሩት ትንታኔ', restore_desc: 'ለመንገዶች እባኮትን እትም ማነተኝነት', routing_rules_sample: 'በአተምታም', routing_rules_alert_tun: 'እባኮትን የስርእንኳን፣ አዳዲስ እና አመለካከት የሚገባውን በፍተሻ አማካኝነትን ተቀናበር።', routing_rules_alert_system: 'የስምምነት አማካኝነት የአትመልከትና ስምምነት ሲገባን ተግባር።', form_default: 'አማካኝ', endpoint_suggested: 'ምክር', endpoint_latest: 'ሳምንት', endpoint_update: 'ምክሩን ይጠቀሙ', endpoint_paste: 'የሚገባ አትመልከት', profile_title: 'ፕሮፌል', profile_name: 'አካባቢ', profile_endpoint: 'አትመልከት', profile_limitation: (value) => `${value} አማካኝነቶችን በአንደኛ አካባቢ ይጠቀሙ`, mtu_title: 'እምነት ምክር', mtu_desc: 'እምነትን እንዲበረከት ማንኛውም ምክር በምርጥ ቁጥር።', custom_dns_title: 'የተለየ DNS', confirm: 'እባኮትን ይዘው', update: 'አዳዲስ ይደርሳሉ', cancel: 'ተመካከተ', yes: 'አዎን', no: 'አይ' }, log: { title: 'አፕ ሎግ', desc: 'በፕሮግራም ስራ ላይ ከሆነ ሎግ እዚህ ይታያል።', error_invalid_license: 'የተጠቃሚ ፈቃድ ስለማይታወቅ፣ እባኮትን አምጣት።', error_too_many_connected: 'ፈቃድ ማንበብ ይተለምታል፣ እባኮትን አምጣት።', error_access_denied: 'ፕሮግራሙን እንደ አስተዳዳሪ ተቀባበር እንደሚሰሩ ይሞክሩ።', error_failed_set_endpoint: 'እባኮትን ቅንብርን ተመልከቱ ወይም ሙሉ ተመልከቱ።', error_warp_identity: 'የእይታ ማረጋገጫ ስህተት በክላውድፍሌር!', error_script_failed: 'ፕሮግራሙ ስህተት ተከስቷል፣ እባኮትን ተመልከቱ።', error_object_null: 'ፕሮግራሙ ስህተት ተከስቷል፣ እባኮትን ተመልከቱ።', error_port_already_in_use: (value) => `ፖርት ${value} ከሌላ ፕሮግራም ተጠቃሚ ነው፣ ለማሻሻል ተቀይሩ።`, error_port_socket: 'ሌላ ፖርት ይጠቀሙ።', error_port_restart: 'ፖርት ተጠቃሚ ነው፣ እንደገና እንደ ምታሞች ...', error_unknown_flag: 'በመሳሪያው ውስጥ ስህተት የተከሰተ ነው።', error_deadline_exceeded: 'መጠባበቂያ ጊዜ ተሻሽሎታል፣ እባኮትን ተመልከቱ።', error_configuration_encountered: 'ፕሮክሲ ቅንብር ስህተት ተከሰተ።', error_desktop_not_supported: 'የእንተም ማብቂያ ስርዓት አልተደገፈም።', error_configuration_not_supported: 'ፕሮክሲ ቅንብር በስርዓትዎ ላይ አልተስፋፋም፣ ነገር ግን Warp Proxy በማንበብ ትጠቀሙ ይችላሉ።', error_configuring_proxy: (value) => `${value} ስለማይተስፋፋ፣ ፕሮክሲ ቅንብር ስህተት።`, error_wp_not_found: 'የwarp-plus ፋይል በፕሮግራም ፓኬጅ ባለማግኘት።', error_mp_not_found: 'masque-plus ፋይል ከመተግበሪያ ጥቅም ጋር አልተገኘም!', error_usque_not_found: 'usque ፋይል ከመተግበሪያ ጥቅም ጋር አልተገኘም!', error_wp_exclusions: 'ፋይል ወርፕ-ፕላስ በተለምዶ ስለ ተሳሳተ ፋልስ ፖዚቲቭ ማስታወቂያ እና ትክክለኛ ተመን አንቲቫይሩስ ተመልከተ፣ ስለዚህም እትም ስለማንኛውም መሳሪያ ኢንተርኔትን ማግኘት በችግኝ እንደሆነ።\nእባኮትን ፕሮግራሙ ተወላጅ መንገድ ሲሰጠው ፋይሉን በአንቲቫይሩስ ተከታታይ ዝርዝሮች ላይ እንዲጨምር ማንበብ አለበት። እንዲሆን ይኖርብማል?', error_wp_stopped: 'የwarp-plus ፋይል ስህተት ተከሰተ ነው።', error_connection_failed: '1.1.1.1 ጋር ተเชም ማድረግ አልቻልኩም።', error_country_failed: 'ተመርጠው ያሉበት ሀገር ማድረግ አልቻልኩም።', error_singbox_failed_stop: 'Sing-Box እንደገና ማቆም አልቻልኩም።', error_singbox_failed_start: 'Sing-Box እንደገና መጀመር አልቻልኩም።', error_wp_reset_peer: 'ከCloudflare ጋር መገናኘት አቋረጠ።', error_failed_connection: 'ግንኙነት አልተቋረጠም።', error_canceled_by_user: 'እንዲሁ ተለዋዋጭ በተጠቃሚ ተሰርዟል።', error_helper_not_found: 'የረዳት ፋይል ከመተግበሪያ ጥቅል ጋር አልተገኘም!', error_singbox_ipv6_address: 'አንደኛ የእርስዎ ኦፕሬቲንግ ሲስተም IPv6 አይደግፍም። እባክዎ ወደ ቱነል ቅንብሮች ይሂዱና አድራሻውን ወደ IPv4 ይቀይሩ።', error_local_date: 'እባክዎ የስርዓትዎን ቀን እና ሰዓት ትክክል እንዲሆን ያዘጋጁ!' }, about: { title: 'ስለ አፕ', desc: 'ይህ ፕሮግራም በተለምዶ በእንግሊዝ አንደኛ እትም በእርግጥ ተቀባ ያላቸው በWindows፣ Linux፣ Mac እንደ ተለምዶ እሱን ይዤን አብስለት የምስሉን እንደ ማንበብ ስም እንደ እርግጥ በሆነ ባስተላለፉበት ስም አይደሉም፣ ሁሉንም በእርግጥ ይጠቀሙ ይሁን፣ በእርግጥ በተቀባ የትብብሩን በፍትሐት ይሁን፣ አማራጭ።', slogan: 'ኢንተርኔት፣ ለሁሉም ወይም ለማንም ነው!' }, systemTray: { connect: 'ገና እንደምታገኙ', connecting: 'እንደምታገኙ ...', connected: 'ተገናኙ', disconnecting: 'ተለይተለዋት ...', settings: 'ቅንብሮች', settings_warp: 'Warp', settings_network: 'መሳሪያ', settings_scanner: 'መሳሪያ ተለዋዋጭ', settings_app: 'መተግበሪያ', about: 'ስለምትሆነ', log: 'ሎግ', speed_test: 'ፍጥነት ፈተና', exit: 'መውጣት' }, update: { available: 'አስተካክል ስለምትሆነ ተገናኝ', available_message: (value) => `አዲስ ተለመድ እትም ለ${value} ተገናኝ ነው፣ በአሁኑ ጊዜ ለምትተስፋፋልት ፈርስታ ይችላሉ።`, ready: 'አስተካክል ስለምትሆነ ተቀባል', ready_message: (value) => `አዲስ ተለመድ እትም ለ${value} ተስፋፋል ነው፣ እሱ ምትሰብስቦ በመነጋገር እንደምትከኝ።` }, speedTest: { title: 'ፍጥነት ፈተና', initializing: 'ፍጥነት ፈተና በመስክ ተቀባዊ ...', click_start: 'ፍጥነት ፈተና መጀመር በመነጋገር እባኮት ሰርቶት', error_msg: 'ስህተት ተከሰትቷል፣ እባኮትን እንደገና ተመልከቱ።', server_unavailable: 'ፍጥነት ፈተና ሰርቨር አልተጠቀምም', download_speed: 'ዳውንሎድ ፍጥነት', upload_speed: 'አፕሎድ ፍጥነት', latency: 'ሳምንት', jitter: 'ጂተር' } }; export default amharic; ================================================ FILE: src/localization/ar.ts ================================================ import { Language } from './type'; const arabic: Language = { global: {}, status: { connecting: 'جارٍ الاتصال ...', connected: 'متصل', connected_confirm: 'تم الاتصال', disconnecting: 'جارٍ قطع الاتصال ...', disconnected: 'تم قطع الاتصال', ip_check: 'جارٍ فحص الـ IP ...', keep_trying: 'يرجى الانتظار قليلاً لإعادة المحاولة...', preparing_rulesets: 'جارٍ إعداد مجموعات القواعد...', downloading_rulesets_failed: 'فشل تنزيل مجموعات القواعد.' }, home: { title_warp_based: 'مبني على Warp', drawer_settings_warp: 'إعدادات Warp', drawer_settings_routing_rules: 'قواعد التوجيه', drawer_settings_app: 'إعدادات التطبيق', drawer_settings_scanner: 'إعدادات الماسح الضوئي', drawer_settings_network: 'إعدادات الشبكة', drawer_log: 'سجل التطبيق', drawer_update: 'تحديث', drawer_update_label: 'تحديث جديد', drawer_speed_test: 'اختبار السرعة', drawer_about: 'حول التطبيق', drawer_lang: 'تغيير اللغة', drawer_singbox: 'إعدادات النفق' }, toast: { ip_check_please_wait: 'يرجى الانتظار بضع ثوانٍ لإعادة المحاولة!', ir_location: 'تم الاتصال بـ Cloudflare بعنوان IP إيراني، وهو مختلف عن عنوان IP الفعلي الخاص بك. يمكنك استخدامه لتجاوز التصفية، ولكن ليس العقوبات. لا تقلق! يمكنك تغيير الموقع في الإعدادات باستخدام خيار "Gool" أو "psiphon".', btn_submit: 'فهمت', copied: 'تم النسخ!', cleared: 'تم مسح السجل!', please_wait: 'يرجى الانتظار ...', offline: 'أنت غير متصل!', settings_changed: 'يتطلب تطبيق الإعدادات إعادة الاتصال.', hardware_usage: 'تمكين هذا الخيار سيزيد من استهلاك موارد الأجهزة.', config_added: 'تم إضافة الإعداد بنجاح، وللاستفادة منه يجب عليك النقر على الاتصال.', profile_added: 'تمت إضافة نقطة النهاية بنجاح إلى الملف الشخصي.', endpoint_added: 'تم استبدال نقطة النهاية بنجاح.', new_update_notification: 'إصدار جديد متاح', new_update: 'إصدار جديد من التطبيق متاح. هل ترغب في تنزيله وتجهيزه للتثبيت؟', up_to_date: 'أنت تستخدم أحدث إصدار من التطبيق', exit_pending: 'التطبيق يُنهي عملية الإغلاق؛ يرجى الانتظار قليلاً قبل تشغيله مرة أخرى.', help_btn: 'مساعدة' }, settings: { title: 'إعدادات Warp', more: 'المزيد من الإعدادات', method_warp: 'Warp', method_warp_desc: 'تمكين Warp', method_gool: 'Gool', method_gool_desc: 'تمكين WarpInWarp', method_masque: 'ماسكي', method_masque_desc: 'تمكين ماسكي', method_psiphon: 'Psiphon', method_psiphon_desc: 'تمكين Psiphon', method_psiphon_location: 'البلد', method_psiphon_location_auto: 'عشوائي', method_psiphon_location_desc: 'اختر عنوان IP للدولة المطلوبة', endpoint: 'نقطة النهاية', endpoint_desc: 'مزيج من عنوان IP أو اسم النطاق، مع المنفذ', license: 'الرخصة', license_desc: 'يتم استهلاك الرخصة بشكل مضاعف', option: 'إعدادات التطبيق', network: 'إعدادات الشبكة', proxy_mode: 'التكوين', proxy_mode_desc: 'تعريف إعدادات البروكسي', port: 'منفذ البروكسي', port_desc: 'تعريف منفذ بروكسي التطبيق', share_vpn: 'عنوان الربط', share_vpn_desc: 'مشاركة بروكسي عبر الشبكة', dns: 'DNS', dns_desc: 'حظر الإعلانات والمحتوى للبالغين', dns_error: 'ينطبق على طرق Warp و Gool', ip_data: 'فحص IP', ip_data_desc: 'عرض IP والموقع بعد الاتصال', data_usage: 'استهلاك البيانات', data_usage_desc: 'عرض استهلاك البيانات وسرعة الاتصال في الوقت الحقيقي', dark_mode: 'الوضع الداكن', dark_mode_desc: 'تحديد وضع العرض للتطبيق', lang: 'اللغة', lang_desc: 'تغيير لغة واجهة التطبيق', open_login: 'بدء عند تسجيل الدخول', open_login_desc: 'فتح عند بدء تشغيل النظام', auto_connect: 'الاتصال التلقائي', auto_connect_desc: 'الاتصال عند فتح التطبيق', start_minimized: 'بدء مصغر', start_minimized_desc: 'تصغير عند فتح التطبيق', system_tray: 'علبة النظام', system_tray_desc: 'عدم وضع أيقونة البرنامج في شريط المهام', force_close: 'الإغلاق القسري', force_close_desc: 'عدم البقاء في علبة النظام بعد الخروج', shortcut: 'الملاحة', shortcut_desc: 'اختصارات في الصفحة الرئيسية', sound_effect: 'تأثير صوتي', sound_effect_desc: 'يشغّل صوتًا عند الاتصال الناجح', restore: 'استعادة', restore_desc: 'تطبيق الإعدادات الافتراضية للتطبيق', scanner: 'إعدادات الماسح الضوئي', scanner_alert: 'يتم تنشيط الماسح الضوئي إذا كنت تستخدم عنوان النهاية الافتراضي.', scanner_ip_type: 'نوع نقطة النهاية', scanner_ip_type_auto: 'تلقائي', scanner_ip_type_desc: 'للعثور على عنوان IP لنقطة النهاية', scanner_rtt: 'المدة', scanner_rtt_default: 'افتراضي', scanner_rtt_desc: 'حد RTT للماسح الضوئي', scanner_reserved: 'محجوز', scanner_reserved_desc: 'تجاوز القيمة المحجوزة لـ WireGuard', routing_rules: 'القائمة السوداء', routing_rules_desc: 'منع المرور عبر warp', routing_rules_disabled: 'معطل', routing_rules_items: 'العناصر', profile: 'الملف الشخصي', profile_desc: 'نقاط النهاية المحفوظة من قبلك', singbox: 'إعدادات النفق', close_singbox: 'إيقاف العملية', close_singbox_desc: 'إغلاق sing-box تلقائيًا عند قطع الاتصال', close_helper: 'إيقاف المساعد', close_helper_desc: 'إغلاق المساعد تلقائيًا عند الخروج', mtu: 'قيمة MTU', mtu_desc: 'تعيين وحدة الإرسال القصوى', geo_block: 'الحظر', geo_block_desc: 'إعلانات، برامج ضارة، تصيد وعمّال تعدين العملات الرقمية', geo_rules_ip: 'توجيه IP', geo_rules_ip_desc: 'تطبيق قواعد GeoIP', geo_rules_site: 'توجيه الويب', geo_rules_site_desc: 'تطبيق قواعد GeoSite', geo_nsfw_block: 'تصفية المحتوى', geo_nsfw_block_desc: 'حظر مواقع NSFW', more_helper: 'إعدادات المساعد', singbox_log: 'التسجيل', singbox_log_desc: 'تعيين مستوى التسجيل', singbox_stack: 'الهيكل', singbox_stack_desc: 'تعيين نوع الهيكل', singbox_sniff: 'الاستنشاق', singbox_sniff_desc: 'تمكين الاستنشاق وتجاوز الوجهة', singbox_addressing: 'العنونة', singbox_addressing_desc: 'قم بتعيين نوع عنوان الواجهة', singbox_udp_block: 'حظر UDP', singbox_udp_block_desc: 'حظر كامل لحركة مرور UDP', singbox_discord_bypass: 'ديزكورد', singbox_discord_bypass_desc: 'تجاوز حجب ديزكورد', more_duties: 'المزيد من الواجبات', beta_release: 'تحديث النسخة التجريبية', beta_release_desc: 'ابق على اطلاع حول الإصدارات قبل الإصدار' }, tabs: { home: 'اتصال', warp: 'وارب', network: 'الشبكة', scanner: 'الماسح الضوئي', app: 'التطبيق', singbox: 'نفق' }, modal: { endpoint_title: 'نقطة النهاية', license_title: 'الرخصة', license_desc: 'لا يتطلب تشغيل البرنامج رخصة Warp بالضرورة، ولكن إذا كنت ترغب، يمكنك إدخال الرخصة هنا.', form_clear: 'مسح', test_url_title: 'عنوان رابط الاختبار', test_url_desc: 'عنوان اختبار الاتصال', test_url_update: 'استقبال الاقتراحات', port_title: 'منفذ البروكسي', restore_title: 'استعادة التغييرات', restore_desc: 'بتأكيد عملية استعادة التغييرات، ستعود جميع إعدادات البرنامج إلى حالتها الافتراضية وسيتم قطع الاتصال.', routing_rules_sample: 'عينة', routing_rules_alert_tun: 'فقط قواعد التوجيه للدومين، ip والتطبيق ستؤثر على تكوين Tun.', routing_rules_alert_system: 'باستثناء قاعدة توجيه التطبيق، ستؤثر القواعد الأخرى على تكوين نظام الوكيل.', form_default: 'افتراضي', endpoint_suggested: 'مقترح', endpoint_latest: 'الأحدث', endpoint_update: 'تلقي نقاط النهاية المقترحة', endpoint_paste: 'لصق نقطة النهاية النشطة', profile_title: 'الملف الشخصي', profile_name: 'العنوان', profile_endpoint: 'نقطة النهاية', profile_limitation: (value) => `يمكنك إضافة حد أقصى من ${value} نقاط النهاية.`, mtu_title: 'قيمة MTU', mtu_desc: 'تشير وحدة الإرسال القصوى (MTU) إلى الحد الأقصى لحجم حزم البيانات، والتي يجب تعيينها بين 1000 و 9999.', custom_dns_title: 'نظام أسماء النطاقات المخصص', confirm: 'أؤكد', update: 'تحديث', cancel: 'إلغاء', yes: 'نعم', no: 'لا' }, log: { title: 'سجل التطبيق', desc: 'إذا تم إنشاء سجل بواسطة البرنامج، فسيتم عرضه هنا.', error_invalid_license: 'الرخصة المدخلة غير صالحة؛ قم بإزالتها.', error_too_many_connected: 'تم استيفاء حد استخدام الرخصة؛ قم بإزالتها.', error_access_denied: 'قم بتشغيل البرنامج كـ "تشغيل كمسؤول".', error_failed_set_endpoint: 'تحقق أو استبدل قيمة نقطة النهاية، أو حاول مرة أخرى.', error_warp_identity: 'خطأ في التحقق من الهوية على Cloudflare!', error_script_failed: 'واجه البرنامج خطأ؛ حاول مرة أخرى.', error_object_null: 'واجه البرنامج خطأ؛ حاول مرة أخرى.', error_port_already_in_use: (value) => `المنفذ ${value} يتم استخدامه بواسطة برنامج آخر؛ قم بتغييره.`, error_port_socket: 'استخدم منفذ آخر.', error_port_restart: 'المنفذ قيد الاستخدام؛ جاري إعادة التشغيل ...', error_unknown_flag: 'تم تنفيذ أمر غير صالح في الخلفية.', error_deadline_exceeded: 'انتهت مدة الاتصال؛ حاول مرة أخرى.', error_configuration_encountered: 'واجهت إعدادات البروكسي خطأ!', error_desktop_not_supported: 'بيئة سطح المكتب غير مدعومة!', error_configuration_not_supported: 'إعدادات البروكسي غير مدعومة في نظام التشغيل الخاص بك، ولكن يمكنك استخدام Warp Proxy يدويًا.', error_configuring_proxy: (value) => `حدث خطأ في تكوين البروكسي لـ ${value}!`, error_wp_not_found: 'ملف warp-plus غير موجود بجانب حزمة التطبيق!', error_mp_not_found: 'ملف masque-plus غير موجود بجانب حزمة التطبيق!', error_usque_not_found: 'ملف usque غير موجود بجانب حزمة التطبيق!', error_wp_exclusions: 'من المحتمل أن يكون ملف warp-plus قد تم وضعه في الحجر الصحي بسبب تنبيه إيجابي كاذب واكتشاف خاطئ من قبل برنامج مكافحة الفيروسات، مما تسبب في مشاكل في قدرة البرنامج على الوصول إلى الإنترنت بحرية.\nيمكن للبرنامج إضافة الملف المذكور إلى قائمة الاستثناءات في بعض برامج مكافحة الفيروسات إذا تم منح إذن الوصول. هل يجب القيام بذلك؟', error_wp_stopped: 'واجه ملف warp-plus مشكلة في التشغيل!', error_connection_failed: 'لم يكن الاتصال بـ 1.1.1.1 ممكنًا.', error_country_failed: 'لا يمكن الاتصال بالبلد المحدد.', error_singbox_failed_stop: 'فشل في إيقاف صندوق الغناء!', error_singbox_failed_start: 'فشل في بدء تشغيل صندوق الغناء!', error_wp_reset_peer: 'تم قطع الاتصال بـ Cloudflare بشكل غير متوقع!', error_failed_connection: 'فشل في إنشاء الاتصال!', error_canceled_by_user: 'تم إلغاء العملية من قبل المستخدم.', error_helper_not_found: 'لم يتم العثور على ملف المساعد بجانب حزمة التطبيق!', error_singbox_ipv6_address: 'نظام التشغيل الخاص بك لا يدعم IPv6. يرجى الذهاب إلى إعدادات النفق وتغيير العنوان إلى IPv4.', error_local_date: 'تأكد من ضبط التاريخ والوقت في نظامك بشكل صحيح!' }, about: { title: 'حول التطبيق', desc: 'هذا البرنامج هو نسخة غير رسمية ولكن موثوقة من تطبيق Oblivion لنظامي Windows، Linux، وMac.\nتم إعداد برنامج Oblivion Desktop بناءً على واجهة المستخدم من الإصدار الأصلي الذي طوره Yousef Ghobadi. تم إعداده بهدف الوصول المجاني إلى الإنترنت، وأي تغيير في الاسم أو استخدام تجاري له غير مسموح به.', slogan: 'الإنترنت، للجميع أو لا أحد!' }, systemTray: { connect: 'اتصال', connecting: 'جارٍ الاتصال ...', connected: 'متصل', disconnecting: 'جارٍ قطع الاتصال ...', settings: 'الإعدادات', settings_warp: 'وارب', settings_network: 'الشبكة', settings_scanner: 'الماسح الضوئي', settings_app: 'التطبيق', about: 'حول', log: 'السجل', speed_test: 'اختبار السرعة', exit: 'خروج' }, update: { available: 'تحديث متاح', available_message: (value) => `يتوفر إصدار جديد من ${value}. هل تريد التحديث الآن؟`, ready: 'التحديث جاهز', ready_message: (value) => `الإصدار الجديد من ${value} جاهز. سيتم تثبيته بعد إعادة التشغيل. هل تريد إعادة التشغيل الآن؟` }, speedTest: { title: 'اختبار السرعة', initializing: 'جارٍ تهيئة اختبار السرعة ...', click_start: 'انقر على الزر لبدء اختبار السرعة', error_msg: 'حدث خطأ أثناء اختبار السرعة. يرجى المحاولة مرة أخرى.', server_unavailable: 'خادم اختبار السرعة غير متاح', download_speed: 'سرعة التنزيل', upload_speed: 'سرعة الرفع', latency: 'التأخير', jitter: 'التذبذب' } }; export default arabic; ================================================ FILE: src/localization/cn.ts ================================================ import { Language } from './type'; const chinese: Language = { global: {}, status: { connecting: '连接中...', connected: '已连接', connected_confirm: '已连接', disconnecting: '断开连接中...', disconnected: '已断开连接', ip_check: '检查 IP 中...', keep_trying: '请稍等片刻,再次尝试...', preparing_rulesets: '正在准备规则集...', downloading_rulesets_failed: '下载规则集失败。' }, home: { title_warp_based: '基于 Warp', drawer_settings_warp: 'Warp 设置', drawer_settings_routing_rules: '路由规则', drawer_settings_app: '应用设置', drawer_settings_scanner: '扫描仪设置', drawer_settings_network: '网络设置', drawer_log: '应用日志', drawer_update: '更新', drawer_update_label: '新版本', drawer_speed_test: '速度测试', drawer_about: '关于应用', drawer_lang: '更改语言', drawer_singbox: '隧道设置' }, toast: { ip_check_please_wait: '请等待几秒钟后重试检查!', ir_location: 'Cloudflare 已连接到一个具有伊朗位置的 IP,与您的实际 IP 不同。您可以使用它来绕过过滤,但不会绕过制裁。不用担心!您可以在设置中使用 Gool 或 psiphon 选项更改位置。', btn_submit: '了解', copied: '已复制!', cleared: '日志已被清除!', please_wait: '请等待...', offline: '您处于离线状态!', settings_changed: '应用设置已更改,需要重新连接。', hardware_usage: '启用此选项会增加硬件资源的使用。', config_added: '配置已成功添加,要使用它,您必须点击连接。', profile_added: '端点已成功添加到个人资料中。', endpoint_added: '终端已成功替换。', new_update_notification: '有新版本可用', new_update: '有新版本的应用可用。您想下载并准备安装吗?', up_to_date: '您正在使用最新版本的应用程序', exit_pending: '应用程序正在完成退出过程;请稍等片刻后再重新启动。', help_btn: '帮助' }, settings: { title: 'Warp 设置', more: '更多设置', method_warp: 'Warp', method_warp_desc: '启用 Warp', method_gool: 'Gool', method_gool_desc: '启用 WarpInWarp', method_masque: 'Masque', method_masque_desc: '启用 Masque', method_psiphon: 'Psiphon', method_psiphon_desc: '启用 Psiphon', method_psiphon_location: '选择国家', method_psiphon_location_auto: '随机', method_psiphon_location_desc: '选择所需的国家 IP 地址', endpoint: '端点', endpoint_desc: 'IP 或域名与端口的组合', license: '许可证', license_desc: '许可证消耗翻倍', option: '应用设置', network: '网络设置', proxy_mode: '代理模式', proxy_mode_desc: '选择代理模式', port: '代理端口', port_desc: '定义应用的代理端口', share_vpn: '绑定地址', share_vpn_desc: '在局域网上共享代理', dns: 'DNS', dns_desc: '屏蔽广告和成人内容', dns_error: '适用于 Warp 和 Gool 方法', ip_data: '解析目标地址', ip_data_desc: '连接后显示 IP 和位置', data_usage: '数据使用量', data_usage_desc: '显示数据使用量和实时速度', dark_mode: '深色模式', dark_mode_desc: '定义应用主题模式', lang: '语言', lang_desc: '更改应用界面语言', open_login: '开机自启', open_login_desc: '系统启动时打开', auto_connect: '自动连接', auto_connect_desc: '应用程序打开时连接', start_minimized: '启动时最小化', start_minimized_desc: '应用程序打开时最小化', system_tray: '隐藏系统托盘', system_tray_desc: '不在任务栏显示应用图标', force_close: '强制关闭', force_close_desc: '退出时不要停留在系统托盘中', shortcut: '导航器', shortcut_desc: '主页上的快捷方式', sound_effect: '声音效果', sound_effect_desc: '成功连接时播放声音', restore: '恢复默认设置', restore_desc: '将应用设置还原为默认值', scanner: '扫描仪设置', scanner_alert: '如果您使用默认端点地址,扫描仪将被激活。', scanner_ip_type: '端点类型', scanner_ip_type_auto: '自动', scanner_ip_type_desc: '用于查找终点 IP', scanner_rtt: 'RTT 延迟', scanner_rtt_default: '默认', scanner_rtt_desc: '设置扫描仪 RTT 延迟值', scanner_reserved: '保留', scanner_reserved_desc: '覆盖 wireguard 保留值', routing_rules: '黑名单', routing_rules_desc: '阻止 Warp 的流量通过', routing_rules_disabled: '已禁用', routing_rules_items: '项目', profile: '个人资料', profile_desc: '您保存的端点', singbox: '隧道设置', close_singbox: '停止操作', close_singbox_desc: '断开连接时自动关闭 sing-box', close_helper: '停止助手', close_helper_desc: '退出时自动关闭助手', mtu: 'MTU 值', mtu_desc: '设置最大传输单元', geo_block: '阻止', geo_block_desc: '广告、恶意软件、网络钓鱼和加密货币矿工', geo_rules_ip: 'IP 路由', geo_rules_ip_desc: '应用 GeoIP 规则', geo_rules_site: '网页路由', geo_rules_site_desc: '应用 GeoSite 规则', geo_nsfw_block: '内容过滤', geo_nsfw_block_desc: '屏蔽 NSFW 网站', more_helper: '助理设置', singbox_log: '日志记录', singbox_log_desc: '设置日志级别', singbox_stack: '堆栈', singbox_stack_desc: '设置堆栈类型', singbox_sniff: '嗅探', singbox_sniff_desc: '启用嗅探并覆盖目标', singbox_addressing: '寻址', singbox_addressing_desc: '设置接口地址类型', singbox_udp_block: '阻止 UDP', singbox_udp_block_desc: '完全阻止所有 UDP 流量', singbox_discord_bypass: 'Discord', singbox_discord_bypass_desc: '绕过 Discord 过滤', more_duties: '更多职责', beta_release: 'Beta 更新', beta_release_desc: '了解预发布版本' }, tabs: { home: '连接', warp: 'Warp', network: '网络', scanner: '扫描仪', app: '应用', singbox: '隧道' }, modal: { endpoint_title: '端点', license_title: '许可证', license_desc: '应用不一定需要 Warp 许可证才能运行,但如果您愿意,可以在此处输入您的许可证。', form_clear: '清除', test_url_title: '测试 URL', test_url_desc: '连接测试地址', test_url_update: '接收建议', port_title: '代理端口', restore_title: '恢复更改', restore_desc: '确认恢复默认设置后,所有应用设置将恢复为默认值,并且您的连接将断开。', routing_rules_sample: '示例', routing_rules_alert_tun: '只有域名、ip和应用的路由规则会影响Tun配置。', routing_rules_alert_system: '除了应用路由规则,其他规则将影响系统代理配置。', form_default: '默认', endpoint_suggested: '建议', endpoint_latest: '最新的', endpoint_update: '接收建议的端点', endpoint_paste: '粘贴活动端点', profile_title: '个人资料', profile_name: '标题', profile_endpoint: '端点', profile_limitation: (value) => `您最多可以添加 ${value} 个端点。`, mtu_title: 'MTU 值', mtu_desc: '最大传输单元 (MTU) 是指数据包的最大大小,应设置在 1000 到 9999 之间。', custom_dns_title: '自定义 DNS', confirm: '确认', update: '更新', cancel: '取消', yes: '是', no: '否' }, log: { title: '应用日志', desc: '如果应用生成日志,将在此处显示。', error_invalid_license: '输入的许可证无效;去掉它。', error_too_many_connected: '许可证使用限制已满;去掉它。', error_access_denied: '以管理员身份运行程序。', error_failed_set_endpoint: '检查或替换端点值,或重试。', error_warp_identity: 'cloudflare 中的身份验证错误!', error_script_failed: '程序遇到错误;再试一次。', error_object_null: '程序遇到错误;再试一次。', error_port_already_in_use: (value) => `端口 ${value} 正在被另一个程序使用;更改。`, error_port_socket: '使用另一个端口。', error_port_restart: '端口正在使用中;正在重新启动...', error_unknown_flag: '后台执行了无效命令。', error_deadline_exceeded: '连接超时;再试一次。', error_configuration_encountered: '代理配置遇到错误!', error_desktop_not_supported: '不支持桌面环境!', error_configuration_not_supported: '您的操作系统不支持代理配置,但您可以手动使用 Warp 代理。', error_configuring_proxy: (value) => `为 ${value} 配置代理时出错!`, error_wp_not_found: 'warp-plus 文件不在应用程序包旁边。', error_mp_not_found: 'masque-plus 文件未与应用程序包放在一起!', error_usque_not_found: 'usque 文件未与应用程序包放在一起!', error_wp_exclusions: '很可能由于误报和杀毒软件错误检测,warp-plus 文件被隔离,导致程序无法正常访问互联网。\n如果授权访问,程序可以将该文件添加到某些杀毒软件的排除列表中。是否执行此操作?', error_wp_stopped: 'warp-plus 文件在运行时遇到了问题!', error_connection_failed: '无法连接到1.1.1.1。', error_country_failed: '无法连接到所选国家.', error_singbox_failed_stop: '停止 Sing-Box 失败!', error_singbox_failed_start: '启动 Sing-Box 失败!', error_wp_reset_peer: '与 Cloudflare 的连接意外中断!', error_failed_connection: '无法建立连接!', error_canceled_by_user: '操作已被用户取消。', error_helper_not_found: '未在应用程序包旁找到助手文件!', error_singbox_ipv6_address: '您的操作系统不支持 IPv6。请前往隧道设置并将地址类型更改为 IPv4。', error_local_date: '请确保您的系统日期和时间设置正确!' }, about: { title: '关于应用', desc: '该应用程序是 Warp 客户端的 Windows、Linux 和 MacOS 的非官方但可靠版本,基于 Oblivion 或 Forget 项目。\nOblivion Desktop应用旨在实现对互联网的自由访问。界面设计基于 Yousef Ghobadi 开发的原始版本。不允许任何改名或商业用途。', slogan: '互联网,联万物;不通达,何存乎!' }, systemTray: { connect: '连接', connecting: '正在连接 ...', connected: '已连接', disconnecting: '断开连接 ...', settings: '设置', settings_warp: 'Warp', settings_network: '网络', settings_scanner: '扫描仪', settings_app: '应用程序', about: '关于', log: '日志', speed_test: '速度测试', exit: '退出' }, update: { available: '有更新', available_message: (value) => `新的 ${value} 版本可用。您想现在更新吗?`, ready: '更新已准备好', ready_message: (value) => `新的 ${value} 版本已准备好。它将在重新启动后安装。您想现在重新启动吗?` }, speedTest: { title: '速度测试', initializing: '速度测试初始化中 ...', click_start: '点击按钮开始速度测试', error_msg: '速度测试期间发生错误。请再试一次。', server_unavailable: '速度测试服务器不可用', download_speed: '下载速度', upload_speed: '上传速度', latency: '延迟', jitter: '抖动' } }; export default chinese; ================================================ FILE: src/localization/electron.ts ================================================ import { ipcRenderer } from 'electron'; import enUS from './en'; import faIR from './fa'; import ruRU from './ru'; import cnCN from './cn'; import trTR from './tr'; import idID from './id'; import arSA from './ar'; import viVN from './vi'; import ptBR from './pt'; import urPK from './ur'; import esCU from './es'; import amET from './am'; import myMM from './my'; import { defaultSettings } from '../defaultSettings'; type LanguageType = | 'fa' | 'en' | 'ru' | 'cn' | 'tr' | 'id' | 'ar' | 'vi' | 'pt' | 'ur' | 'es' | 'am' | 'my'; const translate = { fa: faIR, en: enUS, ru: ruRU, cn: cnCN, tr: trTR, id: idID, ar: arSA, vi: viVN, pt: ptBR, ur: urPK, es: esCU, am: amET, my: myMM }; export const getTranslateElectron = () => { const language = ( ipcRenderer?.sendSync('lang') ? ipcRenderer?.sendSync('lang') : defaultSettings.lang ) as LanguageType; return translate[language]; }; ================================================ FILE: src/localization/en.ts ================================================ import { Language } from './type'; const english: Language = { global: {}, status: { connecting: 'Connecting ...', connected: 'Connected', connected_confirm: 'Connected', disconnecting: 'Disconnecting ...', disconnected: 'Disconnected', ip_check: 'Verifying IP ...', keep_trying: 'Please wait before attempting again ...', preparing_rulesets: 'Preparing rulesets ...', downloading_rulesets_failed: 'Downloading rulesets failed.' }, home: { title_warp_based: 'Warp Based', drawer_settings_warp: 'Warp Settings', drawer_settings_routing_rules: 'Routing Rules', drawer_settings_app: 'App Settings', drawer_settings_scanner: 'Scanner Settings', drawer_settings_network: 'Network Settings', drawer_log: 'App Log', drawer_update: 'Update', drawer_update_label: 'New Update', drawer_speed_test: 'Speed Test', drawer_about: 'About App', drawer_lang: 'Change Language', drawer_singbox: 'Tunnel Settings' }, toast: { ip_check_please_wait: 'Please wait a few seconds to retry the check!', ir_location: 'Cloudflare has connected to an Iranian IP (different from your actual IP). This can bypass filtering but not sanctions. You can change locations using "Gool" or "Psiphon" in settings.', btn_submit: 'Understood', copied: 'Copied!', cleared: 'The log has been cleared!', please_wait: 'Please Wait ...', offline: 'You Are Offline!', settings_changed: 'Applying settings requires reconnecting.', hardware_usage: 'Enabling this option will increase hardware resource usage.', config_added: 'The configuration has been successfully added, and to use it, you must click on the connection.', profile_added: 'The endpoint has been successfully added to the profile.', endpoint_added: 'The endpoint was successfully replaced.', new_update_notification: 'A new version is available', new_update: 'A new version of the app is available. Would you like to download and prepare it for installation?', up_to_date: "You're running the newest version of the app", exit_pending: 'The application is completing its exit process; please wait a moment before launching it again.', help_btn: 'Help' }, settings: { title: 'Warp Settings', more: 'More Settings', method_warp: 'Warp', method_warp_desc: 'Enable Warp', method_gool: 'Gool', method_gool_desc: 'Enable WarpInWarp', method_masque: 'Masque', method_masque_desc: 'Enable Masque', method_psiphon: 'Psiphon', method_psiphon_desc: 'Enable Psiphon', method_psiphon_location: 'Country', method_psiphon_location_auto: 'Random', method_psiphon_location_desc: 'Select the desired country IP', endpoint: 'Endpoint', endpoint_desc: 'Combination of IP or domain name, along with port', license: 'License', license_desc: 'The license consumption is doubled', option: 'Application Settings', network: 'Network Settings', proxy_mode: 'Configuration', proxy_mode_desc: 'Defining Proxy Settings', port: 'Proxy Port', port_desc: 'Set local proxy listening port', share_vpn: 'Bind Address', share_vpn_desc: 'Share a proxy on the network', dns: 'DNS', dns_desc: 'Block ads & adult content', dns_error: 'It is applicable to the Warp & Gool methods', ip_data: 'IP Check', ip_data_desc: 'Display IP & location after connection', data_usage: 'Data Usage', data_usage_desc: 'Display data usage & real-time speed', dark_mode: 'Dark Mode', dark_mode_desc: 'Specify the display mode of the application', lang: 'Language', lang_desc: 'Change application interface language', open_login: 'Start at login', open_login_desc: 'Open at system startup', auto_connect: 'Auto Connection', auto_connect_desc: 'Connect when app opens', start_minimized: 'Start Minimized', start_minimized_desc: 'Minimize when the app opens', system_tray: 'System Tray', system_tray_desc: 'Not placing the program icon in the taskbar', force_close: 'Force Close', force_close_desc: 'Do not stay in the system tray upon exit', shortcut: 'Navigator', shortcut_desc: 'Shortcuts on the home page', sound_effect: 'Sound effect', sound_effect_desc: 'Plays sound on successful connection', restore: 'Restore', restore_desc: 'Apply default application settings', scanner: 'Scanner Settings', scanner_alert: 'The scanner is activated if you are using the default endpoint address.', scanner_ip_type: 'Endpoint type', scanner_ip_type_auto: 'Automatic', scanner_ip_type_desc: 'To find endpoint IP', scanner_rtt: 'Interval', scanner_rtt_default: 'Default', scanner_rtt_desc: 'Scanner RTT limit', scanner_reserved: 'Reserved', scanner_reserved_desc: 'Override wireguard reserved value', routing_rules: 'Blacklist', routing_rules_desc: 'Prevent traffic from going through warp', routing_rules_disabled: 'Disabled', routing_rules_items: 'Items', profile: 'Profile', profile_desc: 'Endpoints saved by you', singbox: 'Tunnel Settings', close_singbox: 'Stop operation', close_singbox_desc: 'Automatically close sing-box on disconnect', close_helper: 'Stop helper', close_helper_desc: 'Automatically close helper on exit', mtu: 'MTU Value', mtu_desc: 'Set the Maximum Transmission Unit', geo_block: 'Blocking', geo_block_desc: 'Ads, Malware, Phishing & Crypto Miners', geo_rules_ip: 'IP Routing', geo_rules_ip_desc: 'Applying GeoIP rules', geo_rules_site: 'Web Routing', geo_rules_site_desc: 'Applying GeoSite rules', geo_nsfw_block: 'Content Filter', geo_nsfw_block_desc: 'Block NSFW websites', more_helper: 'Assistant Settings', singbox_log: 'Logging', singbox_log_desc: 'Set Log Level', singbox_stack: 'Stack', singbox_stack_desc: 'Set Stack Type', singbox_sniff: 'Sniff', singbox_sniff_desc: 'Enable Sniffing & Override Destination', singbox_addressing: 'Addressing', singbox_addressing_desc: 'Set Interface Address Type', singbox_udp_block: 'Block UDP', singbox_udp_block_desc: 'Completely block all UDP traffic', singbox_discord_bypass: 'Discord', singbox_discord_bypass_desc: 'Bypass Discord filtering', more_duties: 'Duties', beta_release: 'Beta Update', beta_release_desc: 'Stay informed about pre-release versions' }, tabs: { home: 'Connect', warp: 'Warp', network: 'Network', scanner: 'Scanner', app: 'App', singbox: 'Tunnel' }, modal: { endpoint_title: 'Endpoint', license_title: 'License', license_desc: 'The program does not necessarily need a Warp license to run, but if you wish, you can enter your license here.', form_clear: 'Clear', test_url_title: 'Test Url', test_url_desc: 'Connectivity test address', test_url_update: 'Receive suggestions', port_title: 'Proxy Port', restore_title: 'Restore Changes', restore_desc: 'By confirming the operation of restoring the changes, all program settings will return to the default state and your connection will be disconnected.', routing_rules_sample: 'Sample', routing_rules_alert_tun: 'Only the routing rules for domain, ip & app will affect the Tun configuration.', routing_rules_alert_system: 'Except for the app routing rule, other rules will affect the System Proxy configuration.', form_default: 'Default', endpoint_suggested: 'Suggested', endpoint_latest: 'Latest', endpoint_update: 'Receive suggested endpoints', endpoint_paste: 'Paste active endpoint', profile_title: 'Profile', profile_name: 'Title', profile_endpoint: 'Endpoint', profile_limitation: (value) => `Maximum ${value} endpoints allowed.`, mtu_title: 'MTU Value', mtu_desc: 'Maximum Transmission Unit (MTU) refers to the maximum size of data packets, which should be set between 1000 and 9999.', custom_dns_title: 'Custom DNS', confirm: 'I confirm', update: 'Update', cancel: 'Cancel', yes: 'Yes', no: 'No' }, log: { title: 'App Log', desc: 'If a log is created by the program, it will be displayed here.', error_invalid_license: 'The entered license is not valid; Remove it.', error_too_many_connected: 'The license usage limit is filled; Remove it.', error_access_denied: 'Run the program as Run as Administrator.', error_failed_set_endpoint: 'Check or replace the endpoint value, or try again.', error_warp_identity: 'Authentication error in cloudflare!', error_script_failed: 'The program encountered an error; Try again.', error_object_null: 'The program encountered an error; Try again.', error_port_already_in_use: (value) => `Port ${value} is being used by another program; Change it.`, error_port_socket: 'Use another port.', error_port_restart: 'The port is in use; restarting ...', error_unknown_flag: 'An invalid command was executed in the background.', error_deadline_exceeded: 'Connection timed out; Try again.', error_configuration_encountered: 'Proxy configuration encountered an error!', error_desktop_not_supported: 'Desktop environment is not supported!', error_configuration_not_supported: 'Proxy configuration is not supported in your operating system, but you can use Warp Proxy manually.', error_configuring_proxy: (value) => `Failed to configure proxy for ${value}!`, error_wp_not_found: 'The warp-plus file is not located alongside the application package!', error_mp_not_found: 'The masque-plus file is not located alongside the application package!', error_usque_not_found: 'The usque file is not located alongside the application package!', error_wp_exclusions: 'The warp-plus file has likely been quarantined due to a false positive alert and incorrect detection by the antivirus, causing issues with the program’s ability to access the internet freely.\nThe program can add the mentioned file to the exclusions list in certain antiviruses if access permission is granted. Should this be done?', error_wp_stopped: 'The warp-plus file has encountered an issue running!', error_connection_failed: 'Connection to 1.1.1.1 was not possible.', error_country_failed: 'Cannot connect to the selected country.', error_singbox_failed_stop: 'Failed to stop Sing-Box!', error_singbox_failed_start: 'Failed to start Sing-Box!', error_wp_reset_peer: 'The connection to Cloudflare was unexpectedly interrupted!', error_failed_connection: 'Failed to establish connection!', error_canceled_by_user: 'The operation was canceled by the user.', error_helper_not_found: 'The helper file is not located alongside the application package!', error_singbox_ipv6_address: 'Your OS does not support IPv6. Please go to the tunnel settings and change the addressing to IPv4.', error_local_date: "Make sure your system's date and time are set correctly!" }, about: { title: 'About App', desc: "This is an unofficial but reliable desktop version of Oblivion for Windows, Linux, and macOS.\nModeled after Yousef Ghobadi's original interface, this software promotes free internet access. Name changes or commercial use are prohibited.", slogan: 'Internet, for all or none!' }, systemTray: { connect: 'Connect', connecting: 'Connecting ...', connected: 'Connected', disconnecting: 'Disconnecting ...', settings: 'Settings', settings_warp: 'Warp', settings_network: 'Network', settings_scanner: 'Scanner', settings_app: 'Application', about: 'About', log: 'Logs', speed_test: 'Speed Test', exit: 'Exit' }, update: { available: 'Update Available', available_message: (value) => `New ${value} version available. Update now?`, ready: 'Update Ready', ready_message: (value) => `${value} update prepared. Restart to install now?` }, speedTest: { title: 'Speed Test', initializing: 'Speed test initializing ...', click_start: 'Click to begin speed test', error_msg: 'Speed test failed. Please try again.', server_unavailable: 'Speed test server unavailable', download_speed: 'Download Speed', upload_speed: 'Upload Speed', latency: 'Latency', jitter: 'Jitter' } }; export default english; ================================================ FILE: src/localization/es.ts ================================================ import { Language } from './type'; const spanish: Language = { global: {}, status: { connecting: 'Conectando ...', connected: 'Conectado', connected_confirm: 'Conectado', disconnecting: 'Desconectando ...', disconnected: 'Desconectado', ip_check: 'Comprobando IP ...', keep_trying: 'Por favor espera un momento para intentar nuevamente...', preparing_rulesets: 'Preparando conjuntos de reglas...', downloading_rulesets_failed: 'Error al descargar los conjuntos de reglas.' }, home: { title_warp_based: 'Basado en Warp', drawer_settings_warp: 'Configuraciones de Warp', drawer_settings_routing_rules: 'Reglas de Enrutamiento', drawer_settings_app: 'Configuraciones de la Aplicación', drawer_settings_scanner: 'Configuraciones del Escáner', drawer_settings_network: 'Configuraciones de Red', drawer_log: 'Registro de la Aplicación', drawer_update: 'Actualizar', drawer_update_label: 'Nueva Actualización', drawer_speed_test: 'Prueba de Velocidad', drawer_about: 'Acerca de la Aplicación', drawer_lang: 'Cambiar Idioma', drawer_singbox: 'Configuraciones del Túnel' }, toast: { ip_check_please_wait: 'Por favor espera unos segundos para volver a intentar la comprobación!', ir_location: 'Cloudflare se ha conectado a una IP con ubicación en Irán, lo cual es diferente a tu IP real. Puedes usarlo para eludir filtros, pero no sanciones. ¡No te preocupes! Puedes cambiar la ubicación en las configuraciones usando la opción "Gool" o "psiphon".', btn_submit: 'Entendido', copied: '¡Copiado!', cleared: '¡El registro ha sido borrado!', please_wait: 'Por favor espera ...', offline: '¡Estás Offline!', settings_changed: 'Se requiere reconectar para aplicar los cambios de configuración.', hardware_usage: 'Habilitar esta opción aumentará el uso de recursos de hardware.', config_added: 'La configuración ha sido añadida con éxito, y para usarla debes hacer clic en la conexión.', profile_added: 'El endpoint ha sido añadido correctamente al perfil.', endpoint_added: 'El endpoint fue reemplazado correctamente.', new_update_notification: 'Una nueva versión está disponible', new_update: 'Hay una nueva versión de la aplicación disponible. ¿Te gustaría descargarla y prepararla para la instalación?', up_to_date: 'Estás usando la versión más reciente de la aplicación', exit_pending: 'La aplicación está completando su proceso de salida; por favor, espere un momento antes de volver a iniciarla.', help_btn: 'Ayuda' }, settings: { title: 'Configuraciones de Warp', more: 'Más configuraciones', method_warp: 'Warp', method_warp_desc: 'Habilitar Warp', method_gool: 'Gool', method_gool_desc: 'Habilitar WarpInWarp', method_masque: 'Masque', method_masque_desc: 'Habilitar Masque', method_psiphon: 'Psiphon', method_psiphon_desc: 'Habilitar Psiphon', method_psiphon_location: 'País', method_psiphon_location_auto: 'Aleatorio', method_psiphon_location_desc: 'Seleccionar la IP del país deseado', endpoint: 'Endpoint', endpoint_desc: 'Combinación de IP o nombre de dominio, junto con el puerto', license: 'Licencia', license_desc: 'El consumo de la licencia está duplicado', option: 'Configuraciones de la Aplicación', network: 'Configuraciones de Red', proxy_mode: 'Configuración', proxy_mode_desc: 'Definir configuraciones de proxy', port: 'Puerto de Proxy', port_desc: 'Definir el puerto del proxy de la aplicación', share_vpn: 'Dirección de enlace', share_vpn_desc: 'Compartir un proxy en la red', dns: 'DNS', dns_desc: 'Bloquear anuncios y contenido para adultos', dns_error: 'Aplicable a los métodos Warp & Gool', ip_data: 'Comprobación de IP', ip_data_desc: 'Mostrar IP y ubicación después de la conexión', data_usage: 'Uso de Datos', data_usage_desc: 'Mostrar el uso de datos y la velocidad en tiempo real', dark_mode: 'Modo Oscuro', dark_mode_desc: 'Especificar el modo de visualización de la aplicación', lang: 'Idioma', lang_desc: 'Cambiar el idioma de la interfaz de la aplicación', open_login: 'Iniciar al inicio', open_login_desc: 'Abrir al inicio del sistema', auto_connect: 'Conexión Automática', auto_connect_desc: 'Conectar al abrir la aplicación', start_minimized: 'Iniciar minimizado', start_minimized_desc: 'Minimizar cuando se abre la aplicación', system_tray: 'Bandeja del sistema', system_tray_desc: 'No colocar el ícono del programa en la barra de tareas', force_close: 'Cerrar Forzadamente', force_close_desc: 'No dejar el programa en la bandeja del sistema al salir', shortcut: 'Navegador', shortcut_desc: 'Accesos directos en la página principal', sound_effect: 'efecto de sonido', sound_effect_desc: 'reproduce un sonido al conectarse con éxito', restore: 'Restaurar', restore_desc: 'Aplicar los ajustes predeterminados de la aplicación', scanner: 'Configuraciones del Escáner', scanner_alert: 'El escáner se activa si estás utilizando la dirección predeterminada del endpoint.', scanner_ip_type: 'Tipo de Endpoint', scanner_ip_type_auto: 'Automático', scanner_ip_type_desc: 'Para encontrar la IP del endpoint', scanner_rtt: 'Intervalo', scanner_rtt_default: 'Predeterminado', scanner_rtt_desc: 'Límite de RTT del escáner', scanner_reserved: 'Reservado', scanner_reserved_desc: 'Anular el valor reservado de WireGuard', routing_rules: 'Lista Negra', routing_rules_desc: 'Evitar que el tráfico pase por Warp', routing_rules_disabled: 'Deshabilitado', routing_rules_items: 'Elementos', profile: 'Perfil', profile_desc: 'Endpoints guardados por ti', singbox: 'Configuraciones del Túnel', close_singbox: 'Detener operación', close_singbox_desc: 'Cerrar automáticamente Sing-Box al desconectarse', close_helper: 'Detener asistente', close_helper_desc: 'Cerrar automáticamente el asistente al salir', mtu: 'Valor MTU', mtu_desc: 'Establecer la Unidad Máxima de Transmisión', geo_block: 'Bloqueo', geo_block_desc: 'Anuncios, Malware, Phishing & Mineros de Criptomonedas', geo_rules_ip: 'Enrutamiento por IP', geo_rules_ip_desc: 'Aplicar reglas de GeoIP', geo_rules_site: 'Enrutamiento Web', geo_rules_site_desc: 'Aplicar reglas de GeoSite', geo_nsfw_block: 'Filtro de contenido', geo_nsfw_block_desc: 'Bloquear sitios web NSFW', more_helper: 'Configuraciones del Asistente', singbox_log: 'Registro', singbox_log_desc: 'Establecer Nivel de Registro', singbox_stack: 'Pila', singbox_stack_desc: 'Establecer tipo de Pila', singbox_sniff: 'Sniff', singbox_sniff_desc: 'Habilitar Sniffing y Anular Destino', singbox_addressing: 'Direccionamiento', singbox_addressing_desc: 'Establecer tipo de dirección de interfaz', singbox_udp_block: 'Bloquear UDP', singbox_udp_block_desc: 'Bloquear completamente todo el tráfico UDP', singbox_discord_bypass: 'Discord', singbox_discord_bypass_desc: 'Bypass del filtrado de Discord', more_duties: 'Más responsabilidades', beta_release: 'Actualización Beta', beta_release_desc: 'Mantente informado sobre versiones previas al lanzamiento' }, tabs: { home: 'Conectar', warp: 'Warp', network: 'Red', scanner: 'Escáner', app: 'Aplicación', singbox: 'Túnel' }, modal: { endpoint_title: 'Endpoint', license_title: 'Licencia', license_desc: 'El programa no necesita necesariamente una licencia Warp para funcionar, pero si lo deseas, puedes ingresar tu licencia aquí.', form_clear: 'Limpiar', test_url_title: 'URL de prueba', test_url_desc: 'Dirección de prueba de conectividad', test_url_update: 'Recibir sugerencias', port_title: 'Puerto de Proxy', restore_title: 'Restaurar Cambios', restore_desc: 'Al confirmar la operación de restaurar los cambios, todas las configuraciones del programa volverán a su estado predeterminado y tu conexión será desconectada.', routing_rules_sample: 'Muestra', routing_rules_alert_tun: 'Solo las reglas de enrutamiento para dominio, IP y aplicación afectarán la configuración de Tun.', routing_rules_alert_system: 'Excepto por la regla de enrutamiento de la aplicación, las otras reglas afectarán la configuración del Proxy del Sistema.', form_default: 'Predeterminado', endpoint_suggested: 'Sugerido', endpoint_latest: 'Último', endpoint_update: 'Recibir endpoints sugeridos', endpoint_paste: 'Pegar endpoint activo', profile_title: 'Perfil', profile_name: 'Título', profile_endpoint: 'Endpoint', profile_limitation: (value) => `Puedes agregar un máximo de ${value} endpoints.`, mtu_title: 'Valor MTU', mtu_desc: 'La Unidad Máxima de Transmisión (MTU) se refiere al tamaño máximo de los paquetes de datos, que debe establecerse entre 1000 y 9999.', custom_dns_title: 'DNS personalizado', confirm: 'Confirmo', update: 'Actualizar', cancel: 'Cancelar', yes: 'Sí', no: 'No' }, log: { title: 'Registro de la Aplicación', desc: 'Si se crea un registro por el programa, se mostrará aquí.', error_invalid_license: 'La licencia ingresada no es válida; Elimínala.', error_too_many_connected: 'El límite de uso de la licencia está lleno; Elimínala.', error_access_denied: 'Ejecuta el programa como Administrador.', error_failed_set_endpoint: 'Verifica o reemplaza el valor del endpoint, o intenta nuevamente.', error_warp_identity: 'Error de autenticación en Cloudflare!', error_script_failed: 'El programa encontró un error; Intenta nuevamente.', error_object_null: 'El programa encontró un error; Intenta nuevamente.', error_port_already_in_use: (value) => `El puerto ${value} está siendo usado por otro programa; Cámbialo.`, error_port_socket: 'Usa otro puerto.', error_port_restart: 'El puerto está en uso; reiniciando ...', error_unknown_flag: 'Se ejecutó un comando inválido en segundo plano.', error_deadline_exceeded: 'Se agotó el tiempo de conexión; Intenta nuevamente.', error_configuration_encountered: '¡Hubo un error en la configuración del Proxy!', error_desktop_not_supported: '¡El entorno de escritorio no es compatible!', error_configuration_not_supported: 'La configuración del Proxy no es compatible con tu sistema operativo, pero puedes usar Warp Proxy manualmente.', error_configuring_proxy: (value) => `¡Error configurando el proxy para ${value}!`, error_wp_not_found: '¡El archivo warp-plus no está ubicado junto al paquete de la aplicación!', error_mp_not_found: '¡El archivo masque-plus no se encuentra junto al paquete de la aplicación!', error_usque_not_found: '¡El archivo usque no se encuentra junto al paquete de la aplicación!', error_wp_exclusions: 'Es probable que el archivo warp-plus haya sido puesto en cuarentena debido a una alerta de falso positivo y una detección incorrecta por parte del antivirus, lo que ha provocado problemas con la capacidad del programa para acceder libremente a Internet.\nEl programa puede agregar el archivo mencionado a la lista de exclusiones en algunos antivirus si se otorgan permisos de acceso. ¿Se debe hacer esto?', error_wp_stopped: '¡El archivo warp-plus encontró un problema al ejecutarse!', error_connection_failed: 'No se pudo establecer conexión con 1.1.1.1.', error_country_failed: 'No se puede conectar al país seleccionado.', error_singbox_failed_stop: '¡No se pudo detener Sing-Box!', error_singbox_failed_start: '¡No se pudo iniciar Sing-Box!', error_wp_reset_peer: '¡La conexión con Cloudflare se interrumpió inesperadamente!', error_failed_connection: '¡No se pudo establecer la conexión!', error_canceled_by_user: 'La operación fue cancelada por el usuario.', error_helper_not_found: '¡El archivo auxiliar no se encuentra junto al paquete de la aplicación!', error_singbox_ipv6_address: 'Tu sistema operativo no es compatible con IPv6. Por favor, ve a la configuración del túnel y cambia la dirección a IPv4.', error_local_date: '¡Asegúrate de que la fecha y la hora de tu sistema estén configuradas correctamente!' }, about: { title: 'Acerca de la Aplicación', desc: 'Este programa es una versión no oficial, pero confiable de la aplicación Oblivion para Windows, Linux y Mac.\nEl programa de escritorio Oblivion está basado en la interfaz de usuario de la versión original desarrollada por Yousef Ghobadi. Fue escrito y preparado con el propósito de un acceso libre a Internet, y no se permite el cambio de nombre ni el uso comercial de él.', slogan: '¡Internet, para todos o para nadie!' }, systemTray: { connect: 'Conectar', connecting: 'Conectando ...', connected: 'Conectado', disconnecting: 'Desconectando ...', settings: 'Configuraciones', settings_warp: 'Warp', settings_network: 'Red', settings_scanner: 'Escáner', settings_app: 'Aplicación', about: 'Acerca de', log: 'Registro', speed_test: 'Prueba de Velocidad', exit: 'Salir' }, update: { available: 'Actualización Disponible', available_message: (value) => `Una nueva versión de ${value} está disponible. ¿Quieres actualizar ahora?`, ready: 'Actualización Lista', ready_message: (value) => `Una nueva versión de ${value} está lista. Se instalará después de un reinicio. ¿Quieres reiniciar ahora?` }, speedTest: { title: 'Prueba de Velocidad', initializing: 'Inicializando prueba de velocidad ...', click_start: 'Haz clic en el botón para iniciar la prueba de velocidad', error_msg: 'Ocurrió un error durante la prueba de velocidad. Por favor, inténtalo de nuevo.', server_unavailable: 'Servidor de prueba de velocidad no disponible', download_speed: 'Velocidad de Descarga', upload_speed: 'Velocidad de Subida', latency: 'Latencia', jitter: 'Jitter' } }; export default spanish; ================================================ FILE: src/localization/fa.ts ================================================ import { Language } from './type'; const persian: Language = { global: {}, status: { connecting: 'درحال اتصال ...', connected: 'اتصال برقرار شد', connected_confirm: 'متصل هستید', disconnecting: 'قطع ارتباط ...', disconnected: 'متصل نیستید', ip_check: 'دریافت اطلاعات ...', keep_trying: 'جهت تکرار تلاش، کمی صبر کنید ...', preparing_rulesets: 'درحال آماده‌سازی مجموعه قوانین ...', downloading_rulesets_failed: 'دانلود مجموعه قوانین با خطا مواجه شد.' }, home: { title_warp_based: 'بر پایه وارپ', drawer_settings_warp: 'تنظیمات وارپ', drawer_settings_routing_rules: 'قوانین مسیریابی', drawer_settings_app: 'تنظیمات برنامه', drawer_settings_scanner: 'تنظیمات اسکنر', drawer_settings_network: 'تنظیمات شبکه', drawer_log: 'لاگ برنامه', drawer_update: 'بروزرسانی', drawer_update_label: 'نسخه جدید', drawer_speed_test: 'تست سرعت', drawer_about: 'درباره برنامه', drawer_lang: 'تغییر زبان', drawer_singbox: 'تنظیمات تانل' }, toast: { ip_check_please_wait: 'برای بررسی مجدد چندثانیه دیگر تلاش کنید!', ir_location: 'کلودفلر به یک IP با لوکیشن ایران که متفاوت از آیپی اصلیته وصلت کرده که باهاش میتونی فیلترینگ‌رو دور بزنی، اما تحریم‌هارو نه. نگران نباش! در تنظیمات میتونی توسط گزینه «گول» یا «سایفون» لوکیشن رو تغییر بدی.', btn_submit: 'متوجه شدم', copied: 'کپی شد!', cleared: 'لاگ پاکسازی شد!', please_wait: 'کمی صبر کنید ...', offline: 'به اینترنت متصل نیستید!', settings_changed: 'اعمال تنظیمات نیازمند اتصال مجدد می\u200Cباشد.', hardware_usage: 'فعال نگه‌داشتن این‌گزینه موجب افزایش مصرف منابع سخت‌افزار می‌گردد.', config_added: 'کانفیگ به درستی اضافه شده و برای استفاده از آن باید بر روی اتصال بزنید.', profile_added: 'اندپوینت به درستی به پروفایل اضافه شد.', endpoint_added: 'اندپوینت به درستی جایگزین شد.', new_update_notification: 'نسخهٔ جدیدی در دسترس است', new_update: 'نسخه جدیدی از برنامه در دسترس است. آن‌را دریافت و آماده نصب کنم؟', up_to_date: 'شما از جدیدترین نسخهٔ برنامه استفاده می‌کنید', exit_pending: 'برنامه درحال تکمیل فرایند خروج خود می‌باشد؛ برای اجرای مجدد آن کمی صبر کنید.', help_btn: 'راهنما' }, settings: { title: 'تنظیمات وارپ', more: 'سایر تنظیمات', method_warp: 'وارپ', method_warp_desc: 'فعالسازی Warp', method_gool: 'گول', method_gool_desc: 'فعالسازی WarpInWarp', method_masque: 'مسک', method_masque_desc: 'فعالسازی Masque', method_psiphon: 'سایفون', method_psiphon_desc: 'فعالسازی Psiphon', method_psiphon_location: 'انتخاب کشور', method_psiphon_location_auto: 'Random', method_psiphon_location_desc: 'انتخاب آی\u200Cپی کشور موردنظر', endpoint: 'اندپوینت', endpoint_desc: 'ترکیبی از IP یا نام دامنه، به\u200Cهمراه پورت', license: 'لایسنس', license_desc: 'لایسنس ۲ برابر مصرف می‌شود', option: 'تنظیمات برنامه', network: 'تنظیمات شبکه', proxy_mode: 'پیکربندی', proxy_mode_desc: 'انتخاب نحوه تنظیم پروکسی', port: 'پورت پروکسی', port_desc: 'تعیین پورت پروکسی برنامه', share_vpn: 'آدرس اتصال', share_vpn_desc: 'اشتراک\u200Cگذاری پروکسی بر روی شبکه', dns: 'انتخاب DNS', dns_desc: 'محدودسازی تبلیغات و محتوای بزرگسال', dns_error: 'برای متدهای Warp و Gool کاربرد دارد', ip_data: 'بررسی IP', ip_data_desc: 'نمایش آی\u200Cپی و لوکیشن پس\u200Cاز اتصال', data_usage: 'مصرف داده', data_usage_desc: 'نمایش لحظه‌ای سرعت و مصرف داده', dark_mode: 'حالت تیره', dark_mode_desc: 'مشخص\u200Cکردن حالت نمایش برنامه', lang: 'زبان برنامه', lang_desc: 'تغییر زبان رابط کاربری برنامه', open_login: 'اجرای خودکار', open_login_desc: 'بازشدن هنگام روشن\u200Cشدن سیستم', auto_connect: 'اتصال خودکار', auto_connect_desc: 'متصل‌شدن هنگام بازشدن برنامه', start_minimized: 'اجرای کمینه', start_minimized_desc: 'کمینه‌شدن هنگام بازشدن برنامه', system_tray: 'مخفی\u200Cسازی', system_tray_desc: 'قرار نگرفتن آیکون برنامه در تسک\u200Cبار', force_close: 'بستن اجباری', force_close_desc: 'با خروج در نوار اعلان قرار نگیرد', shortcut: 'پیمایشگر', shortcut_desc: 'میانبرها در صفحه نخست', sound_effect: 'جلوه صوتی', sound_effect_desc: 'پخش صدا هنگام اتصال موفق', restore: 'بازگردانی', restore_desc: 'اعمال تنظیمات پیشفرض برنامه', scanner: 'تنظیمات اسکنر', scanner_alert: 'اسکنر درصورتی فعال می\u200Cشود که درحال استفاده از آدرس اندپوینت پیشفرض برنامه باشید.', scanner_ip_type: 'نوع اندپوینت', scanner_ip_type_auto: 'Automatic', scanner_ip_type_desc: 'جهت یافتن IP اندپوینت', scanner_rtt: 'وقفه زمانی', scanner_rtt_default: 'Default', scanner_rtt_desc: 'تعیین میزان RTT', scanner_reserved: 'رزرو', scanner_reserved_desc: 'استفاده از Reserved سفارشی وایرگارد', routing_rules: 'لیست سیاه', routing_rules_desc: 'جلوگیری از عبور ترافیک از وارپ', routing_rules_disabled: 'غیرفعال', routing_rules_items: 'مورد', profile: 'پروفایل', profile_desc: 'اندپوینت‌های ذخیره‌شده توسط شما', singbox: 'تنظیمات تانل', close_singbox: 'توقف عملیات', close_singbox_desc: 'بستن خودکار سینگ‌باکس هنگام لغو اتصال', close_helper: 'توقف دستیار', close_helper_desc: 'بستن خودکار دستیار هنگام خروج', mtu: 'مقدار MTU', mtu_desc: 'تعیین حداکثر واحد انتقال', geo_block: 'مسدودسازی', geo_block_desc: 'تبلیغات، بدافزار، فیشینگ و ماینرهای رمزارز', geo_rules_ip: 'مسیریابی IP', geo_rules_ip_desc: 'به‌کارگیری قوانین GeoIP', geo_rules_site: 'مسیریابی وب', geo_rules_site_desc: 'به‌کارگیری قوانین GeoSite', geo_nsfw_block: 'فیلتر محتوا', geo_nsfw_block_desc: 'مسدودکردن وب‌سایت‌های NSFW', more_helper: 'تنظیمات دستیار', singbox_log: 'گزارش‌دهی', singbox_log_desc: 'تنظیم سطح Log', singbox_stack: 'چینش', singbox_stack_desc: 'تنظیم نوع Stack', singbox_sniff: 'تحلیل بسته‌ها', singbox_sniff_desc: 'فعالسازی Sniff و بازنویسی مقصد', singbox_addressing: 'آدرس‌دهی', singbox_addressing_desc: 'تنظیم نوع آدرس رابط', singbox_udp_block: 'بستن UDP', singbox_udp_block_desc: 'مسدودسازی کامل ترافیک UDP', singbox_discord_bypass: 'دیسکورد', singbox_discord_bypass_desc: 'دورزدن فیلترینگ دیسکورد', more_duties: 'وظایف', beta_release: 'بروزرسانی بتا', beta_release_desc: 'اطلاع از نسخه‌های پیش‌ازانتشار' }, tabs: { home: 'اتصال', warp: 'وارپ', network: 'شبکه', scanner: 'اسکنر', app: 'برنامه', singbox: 'تانل' }, modal: { endpoint_title: 'اندپوینت', license_title: 'لایسنس', license_desc: 'برنامه برای اجرا لزوماً به لایسنس وارپ نیاز نداشته، اما درصورت تمایل می\u200Cتوانید لایسنس خود را اینجا وارد کنید.', form_clear: 'حذف', test_url_title: 'مسیر تست', test_url_desc: 'آدرس اینترنتی تست اتصال', test_url_update: 'دریافت پیشنهادات', port_title: 'پورت پروکسی', restore_title: 'بازگردانی تغییرات', restore_desc: 'با تایید عملیات بازگردانی تغییرات، تمامی تنظیمات برنامه به\u200Cحالت پیشفرض بازگشته و اتصال شما قطع می\u200Cگردد.', routing_rules_sample: 'نمونه', routing_rules_alert_tun: 'فقط قوانین مسیریابی domain, ip و app بر روی پیکربندی Tun اثرگذار خواهند بود.', routing_rules_alert_system: 'به‌جز قانون مسیریابی app، سایر قوانین بر روی پیکربندی System Proxy اثرگذار هستند.', form_default: 'پیشفرض', endpoint_suggested: 'پیشنهادی', endpoint_latest: 'اخیر', endpoint_update: 'دریافت اندپوینت‌های پیشنهادی', endpoint_paste: 'جایگذاری اندپوینت فعال', profile_title: 'پروفایل', profile_name: 'عنوان', profile_endpoint: 'اندپوینت', profile_limitation: (value) => `حداکثر می‌توانید ${value} اندپوینت اضافه نمایید.`, confirm: 'تایید می\u200Cکنم', mtu_title: 'مقدار MTU', mtu_desc: 'حداکثر واحد انتقال یا MTU، به حداکثر اندازه بسته‌های داده اشاره دارد، که باید بین ۱۰۰۰ تا ۹۹۹۹ تنظیم شوند.', custom_dns_title: 'مقدار DNS', update: 'بروزرسانی', cancel: 'انصراف', yes: 'بله', no: 'خیر' }, log: { title: 'لاگ برنامه', desc: 'درصورت ایجاد لاگ توسط برنامه، اینجا نمایش داده می\u200Cشود.', error_invalid_license: 'لایسنس وارد شده معتبر نیست؛ آن‌را حذف کنید.', error_too_many_connected: 'سقف استفاده از لایسنس پر شده؛ آن‌را حذف کنید.', error_access_denied: 'برنامه را به‌صورت Run as Administrator اجرا کنید.', error_failed_set_endpoint: 'خطای تنظیم اندپوینت؛ مقدار آن‌را بررسی کرده یا دوباره تلاش کنید.', error_warp_identity: 'خطای احراز هویت در کلودفلر!', error_script_failed: 'برنامه با خطا مواجه شد؛ دوباره تلاش کنید.', error_object_null: 'برنامه با خطا مواجه شد؛ دوباره تلاش کنید.', error_port_already_in_use: (value) => `پورت ${value} توسط برنامه دیگری درحال استفاده است؛ آن‌را تغییر دهید.`, error_port_socket: 'از یک پورت دیگر استفاده نمایید.', error_port_restart: 'پورت درگیر است؛ درحال راه‌اندازی مجدد ...', error_unknown_flag: 'یک دستور نادرست در پس‌زمینه اجرا شده است.', error_deadline_exceeded: 'مهلت اتصال پایان یافت؛ دوباره تلاش کنید.', error_configuration_encountered: 'پیکربندی پروکسی با خطا مواجه شد!', error_desktop_not_supported: 'محیط دسکتاپ پشتیبانی نمی‌شود!', error_configuration_not_supported: 'پیکربندی پروکسی در سیستم عامل شما پشتیبانی نمی‌شود، اما می‌توانید به‌صورت دستی از پروکسی وارپ استفاده کنید.', error_configuring_proxy: (value) => `خطای پیکربندی پروکسی برای ${value}!`, error_wp_not_found: `فایل warp-plus در کنار بسته برنامه وجود ندارد!`, error_mp_not_found: `فایل masque-plus در کنار بسته برنامه وجود ندارد!`, error_usque_not_found: `فایل usque در کنار بسته برنامه وجود ندارد!`, error_wp_exclusions: 'احتمالا فایل وارپ‌پلاس اشتباها به‌دلیل اعلان فالس پازیتیو و تشخیص اشتباه آنتی‌ویروس قرنطینه شده و عملکرد برنامه رو برای دسترسی آزاد به اینترنت دچار مشکل کرده.\nبرنامه می‌تونه درصورت اعطای سطح دسترسی، فایل مذکور رو در برخی‌از آنتی‌ویروس‌ها به لیست استثنائات اضافه کنه. انجام بشه؟', error_wp_stopped: `فایل warp-plus برای اجرا با مشکل مواجه است!`, error_connection_failed: 'اتصال به 1.1.1.1 امکان‌پذیر نبود.', error_country_failed: 'امکان اتصال به کشور انتخابی وجود ندارد.', error_singbox_failed_stop: 'متوقف‌کردن سینگ‌باکس با خطا مواجه شد!', error_singbox_failed_start: 'فعال‌کردن سینگ‌باکس با خطا مواجه شد!', error_wp_reset_peer: 'اتصال به کلودفلر به‌طور غیرمنتظره قطع شد!', error_failed_connection: 'اتصال موفقیت‌آمیز نبود!', error_canceled_by_user: 'عملیات توسط کاربر لغو شد.', error_helper_not_found: 'فایل هلپر در کنار بسته برنامه یافت نشد!', error_singbox_ipv6_address: 'سیستم‌عامل شما از IPv6 پشتیبانی نمی‌کند؛ به تنظیمات تانل رفته و آدرس‌دهی را به IPv4 تغییر دهید.', error_local_date: 'از تنظیم بودن تاریخ و ساعت سیستم‌عامل اطمینان حاصل کنید!' }, about: { title: 'درباره برنامه', desc: 'این\u200Cبرنامه یک نسخه غیررسمی، اما قابل اطمینان از اپ Oblivion یا فراموشی است که برای ویندوز، لینوکس و مک ارائه گردیده است.\nبرنامه Oblivion Desktop با الگو گرفتن از رابط کاربری نسخه اصلی که توسط یوسف قبادی برنامه\u200Cنویسی شده بود، با هدف دسترسی آزاد به اینترنت تهیه گردیده و هرگونه تغییر نام یا استفاده تجاری از آن مجاز نمی\u200Cباشد.', slogan: 'اینترنت برای همه، یا هیچ\u200Cکس!' }, systemTray: { connect: 'اتصال', connecting: 'درحال اتصال ...', connected: 'متصل هستید', disconnecting: 'لغو اتصال ...', settings: 'تنظیمات', settings_warp: ' وارپ ', settings_network: ' شبکه ', settings_scanner: ' اسکنر ', settings_app: ' برنامه ', about: 'درباره', log: 'لاگ', speed_test: 'تست سرعت', exit: 'خروج' }, update: { available: 'بروزرسانی جدید', available_message: (value) => `بروزرسانی جدیدی از ${value} در دسترس است. دریافت می‌کنید؟`, ready: 'دریافت شد', ready_message: (value) => `برنامه ${value} برای آغاز فرایند بروزرسانی آماده است. راه‌اندازی شود؟` }, speedTest: { title: 'تست سرعت', initializing: 'درحال آماده‌سازی ...', click_start: 'برای شروع تست کلیک کنید', error_msg: 'هنگام تست سرعت خطایی رخ داده؛ لطفا دوباره تلاش کنید.', server_unavailable: 'سرور تست سرعت در دسترس نیست', download_speed: 'سرعت دانلود', upload_speed: 'سرعت آپلود', latency: 'تاخیر', jitter: 'نوسان' } }; export default persian; ================================================ FILE: src/localization/id.ts ================================================ import { Language } from './type'; const indonesia: Language = { global: {}, status: { connecting: 'Menghubungkan ...', connected: 'Terhubung', connected_confirm: 'Berhasil Terhubung', disconnecting: 'Memutuskan ...', disconnected: 'Terputus', ip_check: 'Mengecek IP ...', keep_trying: 'Silakan tunggu sebentar untuk mencoba lagi...', preparing_rulesets: 'Sedang menyiapkan set aturan...', downloading_rulesets_failed: 'Gagal mengunduh set aturan.' }, home: { title_warp_based: 'Berbasis Warp', drawer_settings_warp: 'Pengaturan Warp', drawer_settings_routing_rules: 'Aturan Perutean', drawer_settings_app: 'Pengaturan Aplikasi', drawer_settings_scanner: 'Pengaturan Pemindai', drawer_settings_network: 'Pengaturan Jaringan', drawer_log: 'Log Aplikasi', drawer_update: 'Perbarui', drawer_update_label: 'Pembaruan Baru', drawer_speed_test: 'Uji kecepatan', drawer_about: 'Tentang Aplikasi', drawer_lang: 'Ganti Bahasa', drawer_singbox: 'Pengaturan Terowongan' }, toast: { ip_check_please_wait: 'Mohon tunggu beberapa detik untuk mencoba kembali pemeriksaan!', ir_location: 'Cloudflare telah tersambung ke IP dengan lokasi Iran, yang berbeda dengan IP Anda yang sebenarnya. Anda bisa menggunakannya untuk melewati penyaringan, tetapi tidak untuk sanksi. \nJangan khawatir! Anda bisa mengubah lokasi dalam pengaturan menggunakan opsi "Gool" atau "psiphon".', btn_submit: 'Mengerti', copied: 'Tersalin!', cleared: 'Log telah dibersihkan!', please_wait: 'Mohon Tunggu ...', offline: 'Anda sedang offline!', settings_changed: 'Menerapkan pengaturan memerlukan penyambungan ulang.', hardware_usage: 'Mengaktifkan opsi ini akan meningkatkan penggunaan sumber daya perangkat keras.', config_added: 'Konfigurasi telah berhasil ditambahkan, dan untuk menggunakannya, Anda harus mengklik koneksi.', profile_added: 'Titik akhir telah berhasil ditambahkan ke profil.', endpoint_added: 'Endpoint berhasil diganti.', new_update_notification: 'Versi baru tersedia', new_update: 'Versi baru aplikasi tersedia. Apakah Anda ingin mengunduh dan menyiapkannya untuk instalasi?', up_to_date: 'Anda sedang menggunakan versi terbaru aplikasi ini', exit_pending: 'Aplikasi sedang menyelesaikan proses keluar; harap tunggu sebentar sebelum membukanya kembali.', help_btn: 'Bantuan' }, settings: { title: 'Pengaturan Warp', more: 'Pengaturan Lebih', method_warp: 'Warp', method_warp_desc: 'Aktifkan Warp', method_gool: 'Gool', method_gool_desc: 'Aktifkan WarpInWarp', method_masque: 'Masque', method_masque_desc: 'Aktifkan Masque', method_psiphon: 'Psiphon', method_psiphon_desc: 'Aktifkan Psiphon', method_psiphon_location: 'Negara', method_psiphon_location_auto: 'acak', method_psiphon_location_desc: 'Pilih IP negara yang diinginkan', endpoint: 'Titik Akhir', endpoint_desc: 'Kombinasi IP atau nama domain, bersama dengan port', license: 'Lisensi', license_desc: 'Konsumsi lisensi menjadi dua kali lipat', option: 'Pengaturan Aplikasi', network: 'Pengaturan Jaringan', proxy_mode: 'Konfigurasi', proxy_mode_desc: 'Menentukan Pengaturan Proxy', port: 'Port Proxy', port_desc: 'Tentukan port proxy aplikasi', share_vpn: 'Alamat ikat', share_vpn_desc: 'Bagikan proxy di jaringan', dns: 'DNS', dns_desc: 'Blokir iklan & konten dewasa', dns_error: 'Ini berlaku untuk metode Warp dan Gool', ip_data: 'Cek IP', ip_data_desc: 'Tampilkan IP & Lokasi setelah koneksi', data_usage: 'Penggunaan Data', data_usage_desc: 'Tampilkan penggunaan data & kecepatan waktu nyata', dark_mode: 'Mode Gelap', dark_mode_desc: 'Menentukan mode tampilan aplikasi', lang: 'Bahasa', lang_desc: 'Ganti bahasa antarmuka aplikasi', open_login: 'Mulai saat masuk', open_login_desc: 'Buka saat sistem dinyalakan', auto_connect: 'Koneksi Otomatis', auto_connect_desc: 'Hubungkan saat aplikasi dibuka', start_minimized: 'Mulai diminimalkan', start_minimized_desc: 'Minimalkan saat aplikasi dibuka', system_tray: 'Baki Sistem', system_tray_desc: 'Tidak menempatkan ikon program di bilah tugas', force_close: 'Paksa Tutup', force_close_desc: 'Jangan berada di baki sistem saat keluar', shortcut: 'Navigator', shortcut_desc: 'Pintasan di halaman beranda', sound_effect: 'efek suara', sound_effect_desc: 'memutar suara saat koneksi berhasil', restore: 'Pulihkan', restore_desc: 'Menerapkan pengaturan bawaan aplikasi', scanner: 'Pengaturan Pemindai', scanner_alert: 'Pemindai diaktifkan jika Anda menggunakan alamat titik akhir bawaan.', scanner_ip_type: 'Tipe Titik Akhir', scanner_ip_type_auto: 'Otomatis', scanner_ip_type_desc: 'Untuk mencari IP titik akhir', scanner_rtt: 'Selang Waktu', scanner_rtt_default: 'Bawaan', scanner_rtt_desc: 'Batas Pemindai RTT', scanner_reserved: 'Dicadangkan', scanner_reserved_desc: 'Mengesampingkan nilai cadangan penjaga keamanan', routing_rules: 'Daftar Hitam', routing_rules_desc: 'Mencegah lalu lintas agar tidak lewat warp', routing_rules_disabled: 'Dimatikan', routing_rules_items: 'Item', profile: 'Profil', profile_desc: 'Titik akhir yang disimpan oleh Anda', singbox: 'Pengaturan Terowongan', close_singbox: 'Hentikan operasi', close_singbox_desc: 'Otomatis tutup sing-box saat terputus', close_helper: 'Hentikan pembantu', close_helper_desc: 'Tutup otomatis pembantu saat keluar', mtu: 'Nilai MTU', mtu_desc: 'Atur Maximum Transmission Unit', geo_block: 'Pemblokiran', geo_block_desc: 'Iklan, Malware, Phishing, dan Penambang Kripto', geo_rules_ip: 'Routing IP', geo_rules_ip_desc: 'Menerapkan aturan GeoIP', geo_rules_site: 'Routing Web', geo_rules_site_desc: 'Menerapkan aturan GeoSite', geo_nsfw_block: 'Penyaring Konten', geo_nsfw_block_desc: 'Blokir situs web NSFW', more_helper: 'Pengaturan Asisten', singbox_log: 'Pencatatan', singbox_log_desc: 'Atur Tingkat Pencatatan', singbox_stack: 'Tumpukan', singbox_stack_desc: 'Atur Jenis Tumpukan', singbox_sniff: 'Penyadapan', singbox_sniff_desc: 'Aktifkan Sniffing & Override Tujuan', singbox_addressing: 'Pengalamatan', singbox_addressing_desc: 'Atur Jenis Alamat Antarmuka', singbox_udp_block: 'Blokir UDP', singbox_udp_block_desc: 'Blokir semua lalu lintas UDP sepenuhnya', singbox_discord_bypass: 'Membuka Blokir Discord', singbox_discord_bypass_desc: 'Melewati pemblokiran Discord', more_duties: 'Tugas lebih', beta_release: 'Pembaruan Beta', beta_release_desc: 'Tetap terinformasi tentang versi pra-rilis' }, tabs: { home: 'Hubungkan', warp: 'Warp', network: 'Jaringan', scanner: 'Pemindai', app: 'Aplikasi', singbox: 'Terowongan' }, modal: { endpoint_title: 'Endpoint', license_title: 'Lisensi', license_desc: 'Program ini tidak memerlukan lisensi Warp untuk menjalankannya, tetapi jika Anda mau, Anda bisa memasukkan lisensi Anda di sini.', form_clear: 'Hapus', test_url_title: 'URL Uji', test_url_desc: 'Alamat uji konektivitas', test_url_update: 'Menerima saran', port_title: 'Port Proxy', restore_title: 'Pulihkan Perubahan', restore_desc: 'Dengan mengonfirmasi operasi pemulihan perubahan, semua pengaturan program akan kembali ke kondisi bawaan dan koneksi Anda akan terputus.', routing_rules_sample: 'Sampel', routing_rules_alert_tun: 'Hanya aturan perutean untuk domain, ip, dan aplikasi yang akan mempengaruhi konfigurasi Tun.', routing_rules_alert_system: 'Kecuali aturan perutean aplikasi, aturan lainnya akan mempengaruhi konfigurasi Proxy Sistem.', form_default: 'Bawaan', endpoint_suggested: 'Disarankan', endpoint_latest: 'Terkini', endpoint_update: 'Menerima titik akhir yang disarankan', endpoint_paste: 'Menempelkan titik akhir aktif', profile_title: 'Profil', profile_name: 'Judul', profile_endpoint: 'Titik Akhir', profile_limitation: (value) => `Anda dapat menambahkan maksimal ${value} titik akhir.`, mtu_title: 'Nilai MTU', mtu_desc: 'Maximum Transmission Unit (MTU) mengacu pada ukuran maksimum paket data, yang harus diatur antara 1000 hingga 9999.', custom_dns_title: 'DNS Kustom', confirm: 'Saya mengerti', update: 'Perbarui', cancel: 'Batalkan', yes: 'Ya', no: 'Tidak' }, log: { title: 'Log Aplikasi', desc: 'Jika log dibuat oleh program, log akan ditampilkan di sini.', error_invalid_license: 'Lisensi yang dimasukkan tidak valid.', error_too_many_connected: 'Batas penggunaan lisensi sudah terisi.', error_access_denied: 'Jalankan program sebagai Administrator.', error_failed_set_endpoint: 'Periksa atau ganti nilai titik akhir, atau coba lagi.', error_warp_identity: 'Kesalahan otentikasi di cloudflare!', error_script_failed: 'Program mengalami kesalahan; Coba lagi.', error_object_null: 'Program mengalami kesalahan; Coba lagi.', error_port_already_in_use: (value) => `Port ${value} sedang digunakan program lain.`, error_port_socket: 'Gunakan port lain.', error_port_restart: 'Port sedang digunakan; memulai ulang ...', error_unknown_flag: 'Perintah yang tidak valid dieksekusi di latar belakang.', error_deadline_exceeded: 'Waktu koneksi habis; Coba Lagi.', error_configuration_encountered: 'Konfigurasi proxy mengalami kesalahan!', error_desktop_not_supported: 'Lingkungan desktop tidak didukung!', error_configuration_not_supported: 'Konfigurasi proxy tidak didukung pada sistem operasi Anda, tetapi Anda dapat menggunakan Warp Proxy secara manual.', error_configuring_proxy: (value) => `Kesalahan mengkonfigurasi proxy untuk ${value}!`, error_wp_not_found: 'File warp-plus tidak terletak di samping paket aplikasi!', error_mp_not_found: 'File masque-plus tidak ditemukan di samping paket aplikasi!', error_usque_not_found: 'File usque tidak ditemukan di samping paket aplikasi!', error_wp_exclusions: 'Kemungkinan file warp-plus telah dikarantina karena pemberitahuan positif palsu dan deteksi yang salah oleh antivirus, yang menyebabkan masalah dengan kemampuan program untuk mengakses internet secara bebas.\nProgram dapat menambahkan file tersebut ke daftar pengecualian di beberapa antivirus jika izin akses diberikan. Haruskah ini dilakukan?', error_wp_stopped: 'File warp-plus mengalami masalah saat dijalankan!', error_connection_failed: 'Koneksi ke 1.1.1.1 tidak berhasil.', error_country_failed: 'Tidak dapat terhubung ke negara yang dipilih.', error_singbox_failed_stop: 'Gagal menghentikan Sing-Box!', error_singbox_failed_start: 'Gagal memulai Sing-Box!', error_wp_reset_peer: 'Koneksi ke Cloudflare terputus secara tak terduga!', error_failed_connection: 'Gagal menjalin koneksi!', error_canceled_by_user: 'Operasi dibatalkan oleh pengguna.', error_helper_not_found: 'File pembantu tidak ditemukan di sebelah paket aplikasi!', error_singbox_ipv6_address: 'Sistem operasi Anda tidak mendukung IPv6. Silakan pergi ke pengaturan tunnel dan ubah pengalamatan ke IPv4.', error_local_date: 'Pastikan tanggal dan waktu sistem Anda diatur dengan benar!' }, about: { title: 'Tentang Aplikasi', desc: 'Program ini merupakan versi tidak resmi, namun dapat diandalkan dari aplikasi Oblivion untuk Windows, Linux, dan Mac.\nProgram Oblivion Desktop dimodelkan berdasarkan antarmuka pengguna dari versi asli yang dikembangkan oleh Yousef Ghobadi. Program ini ditulis dan dipersiapkan untuk tujuan akses gratis ke Internet, dan perubahan nama atau penggunaan komersial apa pun tidak diperbolehkan..', slogan: 'Internet, untuk semua atau tidak sama sekali!' }, systemTray: { connect: 'Hubungkan', connecting: 'Menghubungkan ...', connected: 'Terhubung', disconnecting: 'Memutuskan ...', settings: 'Pengaturan', settings_warp: 'Warp', settings_network: 'Jaringan', settings_scanner: 'Pemindai', settings_app: 'Aplikasi', about: 'Tentang', log: 'Log', speed_test: 'Uji Kecepatan', exit: 'Keluar' }, update: { available: 'Pembaruan Tersedia', available_message: (value) => `Versi terbaru dari ${value} sudah tersedia. Apakah ingin memperbarui sekarang?`, ready: 'Pembaruan telah Siap', ready_message: (value) => `Versi terbaru dari ${value} sudah siap. Ini akan diinstal setelah restart. Apakah Anda ingin memulai ulang sekarang?` }, speedTest: { title: 'Uji Kecepatan', initializing: 'Inisialisasi uji kecepatan ...', click_start: 'Klik tombol untuk memulai uji kecepatan.', error_msg: 'Terjadi kesalahan selama uji kecepatan. Silakan coba lagi.', server_unavailable: 'Server uji kecepatan tidak tersedia', download_speed: 'Kecepatan unduh', upload_speed: 'Kecepatan upload', latency: 'Latensi', jitter: 'Jitter' } }; export default indonesia; ================================================ FILE: src/localization/index.ts ================================================ import enUS from './en'; import faIR from './fa'; import ruRU from './ru'; import cnCN from './cn'; import trTR from './tr'; import idID from './id'; import arSA from './ar'; import viVN from './vi'; import ptBR from './pt'; import urPK from './ur'; import esCU from './es'; import amET from './am'; import myMM from './my'; import { defaultSettings } from '../defaultSettings'; export type LanguageType = | 'fa' | 'en' | 'ru' | 'cn' | 'tr' | 'id' | 'ar' | 'vi' | 'pt' | 'ur' | 'es' | 'am' | 'my'; type directionType = 'rtl' | 'ltr'; const lang = defaultSettings.lang as LanguageType; export { lang }; const direction = { fa: 'rtl', en: 'ltr', ru: 'ltr', cn: 'ltr', tr: 'ltr', id: 'ltr', ar: 'rtl', vi: 'ltr', pt: 'ltr', ur: 'rtl', es: 'ltr', am: 'ltr', my: 'ltr' }; const getDirection = () => { return direction[lang] as directionType; }; const getDirectionByLang = (language: LanguageType) => { return direction[language] as directionType; }; export { getDirection, getDirectionByLang }; const getLanguageName = (): LanguageType => { return lang; }; export { getLanguageName }; const translate = { fa: faIR, en: enUS, ru: ruRU, cn: cnCN, tr: trTR, id: idID, ar: arSA, vi: viVN, pt: ptBR, ur: urPK, es: esCU, am: amET, my: myMM }; const getTranslate = (forceLang?: string) => { let language; if (typeof forceLang === 'string' && forceLang !== '') { language = forceLang; } else { language = defaultSettings.lang; } return translate[language as LanguageType]; }; export { getTranslate }; const changeLang = (language: string) => { // store.set('lang', language); localStorage.setItem('lang', language); window.dispatchEvent(new Event('storage')); // window.location.reload(); }; export { changeLang }; ================================================ FILE: src/localization/my.ts ================================================ import { Language } from './type'; const myanmar: Language = { global: {}, status: { connecting: 'ဆက်သွယ်နေသည် ...', connected: 'ချိတ်ဆက်ပြီး', connected_confirm: 'ချိတ်ဆက်ပြီး', disconnecting: 'ခွဲထွက်နေသည် ...', disconnected: 'ခွဲထွက်ပြီး', ip_check: 'IP စစ်ဆေးနေသည် ...', keep_trying: 'ကျေးဇူးပြု၍ ပြန်လည်ကြိုးစားရန် များစွာ စောင့်ဆိုင်းပါ ...', preparing_rulesets: 'Rulesets များကို ပြင်ဆင်နေသည်...', downloading_rulesets_failed: 'Rulesets များကို ဒေါင်းလုပ်လုပ်ခြင်း မအောင်မြင်ပါ။' }, home: { title_warp_based: 'Warp အခြေခံ', drawer_settings_warp: 'Warp ဆက်သွယ်မှု', drawer_settings_routing_rules: 'Routing စည်းမျဉ်းများ', drawer_settings_app: 'အက်ပ်ကွက်ရင်း', drawer_settings_scanner: 'စကင်နာကွက်ရင်း', drawer_settings_network: 'ကွန်ယက်ကွက်ရင်း', drawer_log: 'အက်ပ်မှတ်တမ်း', drawer_update: 'အပ်ဒိတ်', drawer_update_label: 'အသစ် အပ်ဒိတ်', drawer_speed_test: 'မိုမိုအမြန်နှုန်း စမ်းသပ်ခြင်း', drawer_about: 'အက်ပ်အကြောင်း', drawer_lang: 'ဘာသာစကားပြောင်းလဲခြင်း', drawer_singbox: 'Tunnel ကွက်ရင်း' }, toast: { ip_check_please_wait: 'IP စစ်ဆေးရန် ပြန်လည်ကြိုးစားရန် အချိန်ခဏ စောင့်ပါ!', ir_location: 'Cloudflare သည် သင့်၏ အမှန်တရား IP နှင့် ကွာခြားသော မြောက်အီးရတ်နိုင်ငံအတွက် IP ဖြင့် ချိတ်ဆက်နေသည်။ ၎င်းကို များစွာတားဆီးခြင်းများကို ကျော်လွှားရန် အသုံးပြုနိုင်သည်။ ဒါပေမယ့်၊ အမှုဆောင်တာဝန်လည်း သတ်မှတ်နိုင်သည်။ သင့်တော်တယ်ဆိုလျှင် ဤကွက်ရင်းတွင် "Gool" သို့မဟုတ် "psiphon" ကို အသုံးပြု၍နေရာကို ပြောင်းလဲနိုင်ပါတယ်。', btn_submit: 'နားလည်ပါ', copied: 'ကူးယူပြီး!', cleared: 'မှတ်တမ်း ပစ်ခတ်ပြီး!', please_wait: 'ကျေးဇူးပြု၍ စောင့်ပါ ...', offline: 'သင်သည် အော့ဖလိုင်းဖြစ်နေပါသည်!', settings_changed: 'ပြင်ဆင်မှုများကိုလက်ခံရန် မရောက်တော့ပါ!', hardware_usage: 'ဤကွက်ရင်း ကို ဖွင့်ပါသည်။', config_added: 'အပ်ဒိတ် ပြောင်းလဲမှု ပြုလုပ်ခဲ့ပါသည်။', profile_added: 'နောက်ဆုံး လုပ်ဆောင်ထားသော End-point ၏ profile ဖွင့်နေပါသည်။', endpoint_added: 'Endpoint ရှင်းလင်းတယ်', new_update_notification: 'အသစ်သောဗားရှင်းအသစ်ရနိုင်ပါပြီ', new_update: 'အပ်ဒိတ်အသစ်ရနိုင်ပါသည်။ ဒေါင်းလုပ်ပြီး ထည့်သွင်းရန် ပြင်ဆင်လိုပါသလား။', up_to_date: 'သင်သည် အက်ပ်၏ နောက်ဆုံးဗားရှင်းကို အသုံးပြုနေပါသည်', exit_pending: 'အက်ပ်သည် ထွက်ခွာခြင်းလုပ်ငန်းစဉ်ကို ပြီးဆုံးနေသည်။ ထပ်မံဖွင့်ရန် မပြုမီ ခဏစောင့်ပေးပါ။', help_btn: 'အကူအညီ' }, settings: { title: 'Warp ဆက်တင်များ', more: 'ပိုပြီးဆက်တင်များ', method_warp: 'Warp', method_warp_desc: 'Warp ကိုဖွင့်ပါ', method_gool: 'Gool', method_gool_desc: 'WarpInWarp ကိုဖွင့်ပါ', method_masque: 'Masque', method_masque_desc: 'Masque ကို အသုံးပြုပါ', method_psiphon: 'Psiphon', method_psiphon_desc: 'Psiphon ကိုဖွင့်ပါ', method_psiphon_location: 'တိုင်းပြည်', method_psiphon_location_auto: 'စိတ်အလိုမရှိသော', method_psiphon_location_desc: 'လိုအပ်သောတိုင်းပြည် IP ကိုရွေးပါ', endpoint: 'အဆုံးသတ်ချက်', endpoint_desc: 'IP သို့မဟုတ် domain နာမည်နှင့် port ကိုပေါင်းစပ်ထားသည်', license: 'လိုင်စင်', license_desc: 'လိုင်စင်အသုံးပြုမှုသည်နှစ်ဆဖြစ်သည်', option: 'အက်ပလီကေးရှင်းဆက်တင်များ', network: 'ကွန်ယက်ဆက်တင်များ', proxy_mode: 'ကွန်ဖီဂျူးရေးရှင်း', proxy_mode_desc: 'Proxy ဆက်တင်များကိုသတ်မှတ်ပါ', port: 'Proxy Port', port_desc: 'အက်ပလီကေးရှင်း proxy port ကိုသတ်မှတ်ပါ', share_vpn: 'ဖမ်းဆုပ်သောလိပ်စာ', share_vpn_desc: 'ကွန်ယက်တွင် proxy ကိုမျှဝေပါ', dns: 'DNS', dns_desc: 'ကြော်ငြာများနှင့် ပြင်းပြသောအကြောင်းအရာများကို ပိတ်လိုက်ပါ', dns_error: 'ဒါဟာ Warp နှင့် Gool နည်းလမ်းများတွင် အသုံးပြုနိုင်သည်', ip_data: 'IP စစ်ဆေးမှု', ip_data_desc: 'ချိတ်ဆက်ပြီးနောက် IP နှင့် တည်နေရာကိုပြပါ', data_usage: 'ဒေတာအသုံးပြုမှု', data_usage_desc: 'ဒေတာအသုံးပြုမှုနှင့် အချိန်မှတ်တမ်းအရှိန်ကိုပြပါ', dark_mode: 'အမှောင်မိုဒ်', dark_mode_desc: 'အက်ပလီကေးရှင်းပြသမှုမိုဒ်ကိုသတ်မှတ်ပါ', lang: 'ဘာသာစကား', lang_desc: 'အက်ပလီကေးရှင်းကိုဘာသာစကားပြောင်းလဲပါ', open_login: 'အခေါ်စတင်ချိန်တွင်ဖွင့်ပါ', open_login_desc: 'စနစ်စတင်ခြင်းအခါတွင်ဖွင့်ပါ', auto_connect: 'အလိုအလျောက်ချိတ်ဆက်မှု', auto_connect_desc: 'အက်ပလီကေးရှင်းဖွင့်သည့်အခါချိတ်ဆက်ပါ', start_minimized: 'စတင်ရာမှာမင်းနိုက်တက်', start_minimized_desc: 'အက်ပလီကေးရှင်းဖွင့်သည့်အခါ မင်းနိုက်တက်', system_tray: 'စနစ် tray', system_tray_desc: 'အစီအစဉ်ကို taskbar တွင်မထားပါ', force_close: 'ခါးသုတ်ပိတ်ရန်', force_close_desc: 'ထွက်ပါက system tray တွင်မထိုင်ပါ', shortcut: 'နောက်ပြန်ခလုတ်', shortcut_desc: 'ပင်မစာမျက်နှာတွင် shortcut များ', sound_effect: 'အသံအကျိုးသက်ရောက်မှု', sound_effect_desc: 'ချိတ်ဆက်မှုအောင်မြင်သောအခါအသံဖွင့်ပါသည်။', restore: 'ပြန်လည်ပြုပြင်ရန်', restore_desc: 'အကူအညီအခြေအနေကိုပြန်လည်သတ်မှတ်ပါ', scanner: 'စကင်နာဆက်တင်များ', scanner_alert: 'သင့်အနေနှင့် အဆင့်သတ်မှတ်ထားသော endpoint အလိပ်လိပ်ကိုသုံးပါက စကင်နာကိုဖွင့်ပါ။', scanner_ip_type: 'အဆုံးသတ် IP အမျိုးအစား', scanner_ip_type_auto: 'အလိုအလျောက်', scanner_ip_type_desc: 'Endpoint IP ကိုရှာဖွေရန်', scanner_rtt: 'အချိန်အတော်ကြာ', scanner_rtt_default: 'အဓိက', scanner_rtt_desc: 'စကင်နာ RTT ကန့်သတ်ချက်', scanner_reserved: 'ကြောင့်မပယ်', scanner_reserved_desc: 'wireguard က reserved အတွက်မျှော်မှန်းထားသောတန်ဖိုးကို override လုပ်ပါ', routing_rules: 'Blacklist', routing_rules_desc: 'Traffic ကို warp မှတဆင့်မသွားအောင်အကာအရံထားပါ', routing_rules_disabled: 'ပိတ်ထားသည်', routing_rules_items: 'ပစ္စည်းများ', profile: 'ပရိုဖိုင်', profile_desc: 'သင်၏ saved endpoint များ', singbox: 'Tunnel ကွက်ရင်း', close_singbox: 'စနစ်တက်သည့်အခါတင်မဟုတ်ပါ', close_singbox_desc: 'Sing-box ကိုဆက်တင်ရင်လိုအပ်ချက်များကိုပိတ်ပါ', close_helper: 'ဂုဏ်မခံကောင်းပြန်ရေး', close_helper_desc: 'Exitခဲ့ခြင်းနေရာကိုပိတ်ဆို့ထားပါ', mtu: 'MTU တန်ဖိုး', mtu_desc: 'အများဆုံးပြောင်းလဲမှုအယူခံတန်ဖိုးကိုသတ်မှတ်ပါ', geo_block: 'ပိတ်ဆို့ခြင်း', geo_block_desc: 'ကြော်ငြာများ၊ Malware၊ Phishing နှင့် Crypto Miners', geo_rules_ip: 'IP Routing', geo_rules_ip_desc: 'GeoIP စည်းမျဉ်းများကိုလျှောက်ထားပါ', geo_rules_site: 'Web Routing', geo_rules_site_desc: 'GeoSite စည်းမျဉ်းများကိုလျှောက်ထားပါ', geo_nsfw_block: 'အကြောင်းအရာ ဖILTER', geo_nsfw_block_desc: 'NSFW ဝဘ်ဆိုဒ်များကို ပိတ်ဆို့မည်', more_helper: 'အကူအညီဆက်တင်များ', singbox_log: 'Logging', singbox_log_desc: 'Log အဆင့်ကိုသတ်မှတ်ပါ', singbox_stack: 'Stack', singbox_stack_desc: 'Stack အမျိုးအစားကိုသတ်မှတ်ပါ', singbox_sniff: 'Sniff', singbox_sniff_desc: 'Sniffing ကိုဖွင့်ပြီး ရောက်ရှိမှုကို Override လုပ်ပါ', singbox_addressing: 'လိပ်စာတင်ခြင်း', singbox_addressing_desc: 'Interface လိပ်စာအမျိုးအစားသတ်မှတ်ပါ', singbox_udp_block: 'UDP ကိုပိတ်မည်', singbox_udp_block_desc: 'UDP traffic အားလုံးကိုလုံးဝပိတ်မည်', singbox_discord_bypass: 'Discord', singbox_discord_bypass_desc: 'Discord ဖျက်တားမှုကိုကျော်လွှားခြင်း', more_duties: 'ပိုမိုတာဝန်များ', beta_release: 'ဘီတာအပ်ဒိတ်', beta_release_desc: 'မပြန်ပေးမီဗားရှင်းများကိုအသိပေးချက်ရယူပါ' }, tabs: { home: 'ချိတ်ဆက်ပါ', warp: 'Warp', network: 'အင်တာနက်', scanner: 'စကင်နာ', app: 'အက်ပ်', singbox: 'တန်နယ်' }, modal: { endpoint_title: 'Endpoint', license_title: 'လိုင်စင်', license_desc: 'ဒီပရိုဂရမ်ကို လည်ပတ်ရန် Warp license တစ်ခု မလိုအပ်ပါဘူး၊ သို့သော် သင်သည် ဆန္ဒရှိလျှင်၊ ဤနေရာတွင် လိုင်စင်ကို ထည့်နိုင်ပါသည်။', form_clear: 'ရှင်းလင်းပါ', test_url_title: 'စမ်းသပ် URL', test_url_desc: 'ချိတ်ဆက်မှု စမ်းသပ်ရန်လိပ်စာ', test_url_update: 'အကြံပြုချက်များလက်ခံခြင်း', port_title: 'Proxy Port', restore_title: 'ပြန်လည်ဆက်သွယ်မှု', restore_desc: 'ပြန်လည်ဆက်သွယ်မှု ပြုလုပ်ခြင်းဖြင့် ပရိုဂရမ်၏ စည်းမျဉ်းများသည် အခြားမှတ်တမ်းများမှပြန်လည်သတ်မှတ်ပြီး connection ကို ခွဲထွက်ပါသည်။', routing_rules_sample: 'နမူနာ', routing_rules_alert_tun: 'Domain, IP နှင့် App အတွက်သာ Routing စည်းမျဉ်းများသည် Tun configuration ကို အကျိုးသက်ရောက်ပါမည်။', routing_rules_alert_system: 'App Routing စည်းမျဉ်းမှ အပြောင်းအလဲသည် အခြား rules များကို ဆက်သွယ်မှု Proxy configuration တွင် တည်ရှိပါမည်။', form_default: 'ပုံမှန်', endpoint_suggested: 'အကြံပြု', endpoint_latest: 'နောက်ဆုံး', endpoint_update: 'Suggested Endpoints လက်ခံပါ', endpoint_paste: 'Paste Active Endpoint', profile_title: 'Profile', profile_name: 'အမည်', profile_endpoint: 'Endpoint', profile_limitation: (value) => `သင်သည် ${value} endpoints အထိ ပေါင်းထည့်နိုင်ပါသည်။`, mtu_title: 'MTU တန်ဖိုး', mtu_desc: 'Maximum Transmission Unit (MTU) သည် ဒေတာပုံသေ၏ အရွယ်အစားကို သတ်မှတ်သည်။ 1000 မှ 9999 အတွင်းတွင် သတ်မှတ်ပါ။', custom_dns_title: 'စိတ်ကြိုက် DNS', confirm: 'ကန့်သတ်ပါ', update: 'အပ်ဒိတ်', cancel: 'အနှိပ်ပါ', yes: 'ဟုတ်ပါတယ်', no: 'မဟုတ်ပါ' }, log: { title: 'အက်ပ်မှတ်တမ်း', desc: 'အမှားတစ်ခု ဖြစ်လာသောအခါ မှတ်တမ်းများကို ဒီနေရာတွင် ပြသပါမည်။', error_invalid_license: 'ထည့်ထားသော license မဟုတ်ပါ; ဖျက်ပစ်ပါ။', error_too_many_connected: 'License သုံးစွဲအကန့်အသတ် ပြည့်ခဲ့ပါသည်; ဖျက်ပစ်ပါ။', error_access_denied: 'ပရိုဂရမ်ကို Administrator အဖြစ် Run ပြုလုပ်ပါ။', error_failed_set_endpoint: 'Endpoint မတတ်နိုင်ပါ။ Endpoint ရွေးလဲပါ။', error_warp_identity: 'Cloudflare တွင် အတည်ပြုမှုအမှား ဖြစ်ပွားခဲ့သည်။', error_script_failed: 'ပရိုဂရမ်မှ အမှားဖြစ်ပွားနေပါသည်။ ထပ်၍ ကြိုးစားပါ။', error_object_null: 'ပရိုဂရမ်မှ အမှားဖြစ်ပွားနေပါသည်။ ထပ်၍ ကြိုးစားပါ။', error_port_already_in_use: (value) => `${value} port အတည်ပြုရန် အသုံးပြုနေပါသည်။ အခြား port ပြောင်းလဲပါ။`, error_port_socket: 'Port ကို အသုံးပြုရန် အခြား port အသုံးပြုပါ။', error_port_restart: 'Port ကို အသုံးပြုနေသည်။ ပြန်လည်စတင်ပါ ...', error_unknown_flag: 'အမှားကို command ထည့်ခဲ့ပါသည်။', error_deadline_exceeded: 'ချိတ်ဆက်ရန် အချိန်ကုန်သွားပြီ။ ထပ်၍ ကြိုးစားပါ။', error_configuration_encountered: 'Proxy configuration မှာ အမှားရှိနေပါသည်။', error_desktop_not_supported: 'Desktop ပတ်ဝန်းကျင် ကို ပံ့ပိုးမထားပါ။', error_configuration_not_supported: 'Proxy configuration သည် သင့်စက်ပေါ်တွင် မပံ့ပိုးနိုင်ပါ။ သို့သော် Warp Proxy ကို ကိုယ်တိုင်အသုံးပြုနိုင်သည်။', error_configuring_proxy: (value) => `${value} proxy အတွက် ပြဿနာ ဖြစ်ခဲ့ပါသည်။`, error_wp_not_found: 'warp-plus ဖိုင်သည် ပရိုဂရမ်အဖွဲ့နှင့်အတူ တည်ရှိမည်မဟုတ်ပါ။', error_mp_not_found: 'masque-plus ဖိုင်ကို အက်ပ်ပလီကေးရှင်းထုပ်ပိုးမှုနှင့်အတူ မတွေ့ရှိပါ!', error_usque_not_found: 'usque ဖိုင်ကို အက်ပ်ပလီကေးရှင်းထုပ်ပိုးမှုနှင့်အတူ မတွေ့ရှိပါ!', error_wp_exclusions: 'ကြားဖြတ်မမှန်ခြင်းနှင့်အန်တီဗိုင်းရပ်စ်မှမှားသောရှာဖွေမှုကြောင့် warp-plus ဖိုင်ကိုကာကွယ်ထားခဲ့ပြီး အစီအစဉ်၏အင်တာနက်မှ အခွင့်အလမ်းရရှိခြင်းတွင်ပြproblemများဖြစ်စေပါသည်။\nအစီအစဉ်သည် အချို့သောအန်တီဗိုင်းရပ်စ်များတွင် ဖိုင်ကို ပယ်ပွဲကောက်စာရင်းသို့ထည့်နိုင်သည်။ ၎င်းကို လုပ်ဆောင်သင့်ပါသလား?', error_wp_stopped: 'warp-plus ဖိုင်တွင် ပြဿနာရှိနေပါသည်။', error_connection_failed: '1.1.1.1 နှင့် ချိတ်ဆက်၍ မရနိုင်ပါ။', error_country_failed: 'ရွေးချယ်ထားသောနိုင်ငံနှင့် ချိတ်ဆက်၍ မရနိုင်ပါ။', error_singbox_failed_stop: 'Sing-Box ပိတ်ထားရန် မအောင်မြင်ပါ။', error_singbox_failed_start: 'Sing-Box စတင်ရန် မအောင်မြင်ပါ။', error_wp_reset_peer: 'Cloudflare နှင့် ချိတ်ဆက်မှု ပျက်သွားသည်!', error_failed_connection: 'ချိတ်ဆက်မှုကို တည်ဆောက်၍ မရနိုင်ပါ။', error_canceled_by_user: 'အသုံးပြုသူမှ အလုပ်လုပ်ကိုင်မှုကို မဆိုင်းငံ့သွားပါ။', error_helper_not_found: 'အကူအညီဖိုင်ကို အပလီကေးရှင်းပက်ကေ့တစ်ခုနှင့်အတူ တွေ့ရှိမရပါ!', error_singbox_ipv6_address: 'သင်၏အပလီကေးရှင်းစနစ်သည် IPv6 ကို မပံ့ပိုးပါ။ ကျေးဇူးပြုပြီး တန်နယ်ဆက်တင်များသို့သွားပြီး လိပ်စာကို IPv4 သို့ပြောင်းပါ။', error_local_date: 'သင့်စနစ်၏ ရက်စွဲနှင့် အချိန်ကို မှန်ကန်စွာ သတ်မှတ်ထားခြင်းဖြစ်ကြောင်း သေချာပါစေ။' }, about: { title: 'အက်ပ်အကြောင်း', desc: 'ဒီပရိုဂရမ်သည် Windows, Linux, Mac တို့အတွက် Oblivion app ၏ တရားမဝင်သော်လည်း ယုံကြည်မှုရှိသော ဗားရှင်းဖြစ်သည်။\nOblivion Desktop ပရိုဂရမ်သည် Yousef Ghobadi ၏ ဦးစွာဖွံ့ဖြိုးပွားသော interface မှထုတ်လုပ်ထားပြီး free access to the internet purpose အတွက်ရေးသားပြီး ငွေတောင်းသော်လည်းအမည်ပြောင်းလဲမှု နှင့် စီးပွားရေး အသုံးပြုခြင်းကို ခွင့်မပြုပါ။', slogan: 'အင်တာနက်သည် မည်သည့်လူတစ်ယောက်အတွက်မဆို ရရှိနိုင်သည်!' }, systemTray: { connect: 'ချိတ်ဆက်ပါ', connecting: 'ချိတ်ဆက်နေသည် ...', connected: 'ချိတ်ဆက်ပြီးပြီ', disconnecting: 'ခွဲထွက်နေသည် ...', settings: 'ဆက်တင်များ', settings_warp: 'Warp', settings_network: 'ကွန်ယက်', settings_scanner: 'စကင်နာ', settings_app: 'အက်ပလီကေးရှင်း', about: 'အကြောင်း', log: 'မှတ်တမ်း', speed_test: 'အရှိန် စမ်းသပ်မှု', exit: 'ထွက်ရန်' }, update: { available: 'အပ်ဒိတ် ရနိုင်သည်', available_message: (value) => `${value} ၏ အသစ်ဗားရှင်း တင်ထားရှိပါသည်။ ယခု အပ်ဒိတ်လုပ်လိုပါသလား?`, ready: 'အပ်ဒိတ် ရပြီးပြီ', ready_message: (value) => `${value} ၏ အသစ်ဗားရှင်း ရပြီးပြီ။ ပြန်လည်စတင်ပြီးနောက်တွင် တင်ဆက်မည်။ ယခု စတင်လိုပါသလား?` }, speedTest: { title: 'အမြန်နှုန်း စမ်းသပ်မှု', initializing: 'အမြန်နှုန်း စမ်းသပ်မှု စတင်နေသည် ...', click_start: 'အမြန်နှုန်း စမ်းသပ်မှု စတင်ရန် ခလုတ်ကိုနှိပ်ပါ', error_msg: 'အမြန်နှုန်း စမ်းသပ်မှုတွင်အမှားတစ်ခု ဖြစ်ပွားခဲ့ပါသည်။ ထပ်မံကြိုးစားပါ။', server_unavailable: 'အမြန်နှုန်း စမ်းသပ်မှုဆာဗာ မရရှိနိုင်ပါ', download_speed: 'ဒေါင်းလိုက် အမြန်နှုန်း', upload_speed: 'တင်လိုက် အမြန်နှုန်း', latency: 'လက်ရှိနှောင့်နှေးမှု', jitter: 'Jitter' } }; export default myanmar; ================================================ FILE: src/localization/pt.ts ================================================ import { Language } from './type'; const brazilianPortuguese: Language = { global: {}, status: { connecting: 'Conectando ...', connected: 'Conectado', connected_confirm: 'Conectado', disconnecting: 'Desconectando ...', disconnected: 'Desconectado', ip_check: 'Verificando IP ...', keep_trying: 'Por favor, espere um momento para tentar novamente...', preparing_rulesets: 'Preparando conjuntos de regras...', downloading_rulesets_failed: 'Falha ao baixar os conjuntos de regras.' }, home: { title_warp_based: 'Baseado em Warp', drawer_settings_warp: 'Configurações do Warp', drawer_settings_routing_rules: 'Regras de Roteamento', drawer_settings_app: 'Configurações do Aplicativo', drawer_settings_scanner: 'Configurações do Scanner', drawer_settings_network: 'Configurações de Rede', drawer_log: 'Log do Aplicativo', drawer_update: 'Atualizar', drawer_update_label: 'Nova Atualização', drawer_speed_test: 'Teste de Velocidade', drawer_about: 'Sobre o Aplicativo', drawer_lang: 'Alterar Idioma', drawer_singbox: 'Configurações de Túnel' }, toast: { ip_check_please_wait: 'Por favor, aguarde alguns segundos para tentar novamente!', ir_location: 'A Cloudflare se conectou a um IP com localização no Irã, diferente do seu IP real. Você pode usá-lo para contornar a filtragem, mas não as sanções. Não se preocupe! Você pode mudar a localização nas configurações usando a opção "Gool" ou "Psiphon".', btn_submit: 'Entendido', copied: 'Copiado!', cleared: 'O log foi limpo!', please_wait: 'Por favor, aguarde ...', offline: 'Você está offline!', settings_changed: 'Aplicar configurações requer reconexão.', hardware_usage: 'Ativar esta opção aumentará o uso dos recursos de hardware.', config_added: 'A configuração foi adicionada com sucesso, e para usá-la, você deve clicar na conexão.', profile_added: 'O ponto final foi adicionado com sucesso ao perfil.', endpoint_added: 'O endpoint foi substituído com sucesso.', new_update_notification: 'Uma nova versão está disponível', new_update: 'Uma nova versão do aplicativo está disponível. Você gostaria de baixá-la e prepará-la para instalação?', up_to_date: 'Você está usando a versão mais recente do aplicativo', exit_pending: 'A aplicação está a concluir o processo de saída; por favor, aguarde um momento antes de a iniciar novamente.', help_btn: 'Ajuda' }, settings: { title: 'Configurações do Warp', more: 'Mais Configurações', method_warp: 'Warp', method_warp_desc: 'Ativar Warp', method_gool: 'Gool', method_gool_desc: 'Ativar WarpInWarp', method_masque: 'Masque', method_masque_desc: 'Ativar Masque', method_psiphon: 'Psiphon', method_psiphon_desc: 'Ativar Psiphon', method_psiphon_location: 'País', method_psiphon_location_auto: 'aleatório', method_psiphon_location_desc: 'Selecione o IP do país desejado', endpoint: 'Endpoint', endpoint_desc: 'Combinação de IP ou nome de domínio, junto com porta', license: 'Licença', license_desc: 'O consumo de licença é duplicado', option: 'Configurações do Aplicativo', network: 'Configurações de Rede', proxy_mode: 'Configuração', proxy_mode_desc: 'Definir Configurações de Proxy', port: 'Porta do Proxy', port_desc: 'Definir a porta proxy do aplicativo', share_vpn: 'Endereço de ligação', share_vpn_desc: 'Compartilhar um proxy na rede', dns: 'DNS', dns_desc: 'Bloquear anúncios & conteúdo adulto', dns_error: 'Aplicável aos métodos Warp & Gool', ip_data: 'Verificação de IP', ip_data_desc: 'Exibir IP & localização após a conexão', data_usage: 'Uso de Dados', data_usage_desc: 'Exibir uso de dados & velocidade em tempo real', dark_mode: 'Modo Escuro', dark_mode_desc: 'Especificar o modo de exibição do aplicativo', lang: 'Idioma', lang_desc: 'Alterar o idioma da interface do aplicativo', open_login: 'Iniciar ao logar', open_login_desc: 'Abrir na inicialização do sistema', auto_connect: 'Conexão Automática', auto_connect_desc: 'Conectar ao abrir o aplicativo', start_minimized: 'Iniciar minimizado', start_minimized_desc: 'Minimizar quando o aplicativo é aberto', system_tray: 'Bandeja do Sistema', system_tray_desc: 'Não colocar o ícone do programa na barra de tarefas', force_close: 'Forçar Fechamento', force_close_desc: 'Não permanecer na bandeja do sistema ao sair', shortcut: 'Atalho', shortcut_desc: 'Atalhos na página inicial', sound_effect: 'efeito sonoro', sound_effect_desc: 'reproduz um som quando a conexão é bem-sucedida', restore: 'Restaurar', restore_desc: 'Aplicar configurações padrão do aplicativo', scanner: 'Configurações do Scanner', scanner_alert: 'O scanner é ativado se você estiver usando o endereço de endpoint padrão.', scanner_ip_type: 'Tipo de Endpoint', scanner_ip_type_auto: 'Automático', scanner_ip_type_desc: 'Para encontrar o IP do endpoint', scanner_rtt: 'Intervalo', scanner_rtt_default: 'Padrão', scanner_rtt_desc: 'Limite de RTT do scanner', scanner_reserved: 'Reservado', scanner_reserved_desc: 'Substituir valor reservado do wireguard', routing_rules: 'Lista Negra', routing_rules_desc: 'Impedir o tráfego de passar pelo warp', routing_rules_disabled: 'Desativado', routing_rules_items: 'Itens', profile: 'Perfil', profile_desc: 'Endpoints salvos por você', singbox: 'Configurações de Túnel', close_singbox: 'Parar operação', close_singbox_desc: 'Fechar sing-box automaticamente ao desconectar', close_helper: 'Parar assistente', close_helper_desc: 'Fechar assistente automaticamente ao sair', mtu: 'Valor MTU', mtu_desc: 'Definir a Unidade Máxima de Transmissão', geo_block: 'Bloqueio', geo_block_desc: 'Anúncios, Malware, Phishing e Mineração de Criptomoedas', geo_rules_ip: 'Roteamento IP', geo_rules_ip_desc: 'Aplicação das regras GeoIP', geo_rules_site: 'Roteamento Web', geo_rules_site_desc: 'Aplicação das regras GeoSite', geo_nsfw_block: 'Filtro de Conteúdo', geo_nsfw_block_desc: 'Bloquear sites NSFW', more_helper: 'Configurações do Assistente', singbox_log: 'Registro', singbox_log_desc: 'Definir Nível de Registro', singbox_stack: 'Pilha', singbox_stack_desc: 'Definir Tipo de Pilha', singbox_sniff: 'Sniffing', singbox_sniff_desc: 'Ativar farejamento e substituir destino', singbox_addressing: 'Endereçamento', singbox_addressing_desc: 'Definir Tipo de Endereço da Interface', singbox_udp_block: 'Bloquear UDP', singbox_udp_block_desc: 'Bloquear completamente todo o tráfego UDP', singbox_discord_bypass: 'Discord', singbox_discord_bypass_desc: 'Ignorar o bloqueio do Discord', more_duties: 'Mais deveres', beta_release: 'Atualização Beta', beta_release_desc: 'Fique informado sobre versões pré-lançamento' }, tabs: { home: 'Conectar', warp: 'Warp', network: 'Rede', scanner: 'Scanner', app: 'Aplicativo', singbox: 'Túnel' }, modal: { endpoint_title: 'Endpoint', license_title: 'Licença', license_desc: 'O programa não precisa necessariamente de uma licença Warp para rodar, mas se você quiser, pode inserir sua licença aqui.', form_clear: 'Limpar', test_url_title: 'URL de Teste', test_url_desc: 'Endereço de teste de conectividade', test_url_update: 'Receber sugestões', port_title: 'Porta do Proxy', restore_title: 'Restaurar Alterações', restore_desc: 'Ao confirmar a operação de restauração, todas as configurações do programa voltarão ao estado padrão e sua conexão será desconectada.', routing_rules_sample: 'Exemplo', routing_rules_alert_tun: 'Apenas as regras de roteamento para domínio, ip e aplicativo afetarão a configuração do Tun.', routing_rules_alert_system: 'Exceto pela regra de roteamento de aplicativo, as outras regras afetarão a configuração do Proxy do Sistema.', form_default: 'Padrão', endpoint_suggested: 'Sugerido', endpoint_latest: 'Mais Recente', endpoint_update: 'Receber endpoints sugeridos', endpoint_paste: 'Colar endpoint ativo', profile_title: 'Perfil', profile_name: 'Título', profile_endpoint: 'Endpoint', profile_limitation: (value) => `Você pode adicionar no máximo ${value} endpoints.`, mtu_title: 'Valor MTU', mtu_desc: 'A Unidade Máxima de Transmissão (MTU) refere-se ao tamanho máximo dos pacotes de dados, que deve ser definido entre 1000 e 9999.', custom_dns_title: 'DNS personalizado', confirm: 'Confirmo', update: 'Atualizar', cancel: 'Cancelar', yes: 'Sim', no: 'Não' }, log: { title: 'Log do Aplicativo', desc: 'Se um log for criado pelo programa, ele será exibido aqui.', error_invalid_license: 'A licença inserida não é válida; Remova-a.', error_too_many_connected: 'O limite de uso da licença foi atingido; Remova-a.', error_access_denied: 'Execute o programa como Administrador.', error_failed_set_endpoint: 'Verifique ou substitua o valor do endpoint, ou tente novamente.', error_warp_identity: 'Erro de autenticação no Cloudflare!', error_script_failed: 'O programa encontrou um erro; Tente novamente.', error_object_null: 'O programa encontrou um erro; Tente novamente.', error_port_already_in_use: (value) => `A porta ${value} está sendo usada por outro programa; Altere-a.`, error_port_socket: 'Use outra porta.', error_port_restart: 'A porta está em uso; reiniciando ...', error_unknown_flag: 'Um comando inválido foi executado em segundo plano.', error_deadline_exceeded: 'Tempo limite de conexão esgotado; Tente novamente.', error_configuration_encountered: 'Erro na configuração do proxy!', error_desktop_not_supported: 'Ambiente desktop não suportado!', error_configuration_not_supported: 'A configuração do proxy não é suportada no seu sistema operacional, mas você pode usar o Warp Proxy manualmente.', error_configuring_proxy: (value) => `Erro ao configurar proxy para ${value}!`, error_wp_not_found: 'O arquivo warp-plus não está localizado junto ao pacote do aplicativo!', error_mp_not_found: 'O arquivo masque-plus não está localizado junto ao pacote do aplicativo!', error_usque_not_found: 'O arquivo usque não está localizado junto ao pacote do aplicativo!', error_wp_exclusions: 'É provável que o arquivo warp-plus tenha sido colocado em quarentena devido a um alerta de falso positivo e detecção incorreta pelo antivírus, causando problemas na capacidade do programa de acessar a internet livremente.\nO programa pode adicionar o arquivo mencionado à lista de exclusões em alguns antivírus se o acesso for permitido. Isso deve ser feito?', error_wp_stopped: 'O arquivo warp-plus encontrou um problema ao rodar!', error_connection_failed: 'Não foi possível conectar-se a 1.1.1.1.', error_country_failed: 'Não foi possível conectar-se ao país selecionado.', error_singbox_failed_stop: 'Falha ao parar a Caixa de Canto!', error_singbox_failed_start: 'Falha ao iniciar a Caixa de Canto!', error_wp_reset_peer: 'A conexão com Cloudflare foi interrompida inesperadamente!', error_failed_connection: 'Falha ao estabelecer conexão!', error_canceled_by_user: 'A operação foi cancelada pelo usuário.', error_helper_not_found: 'O arquivo auxiliar não foi encontrado junto ao pacote do aplicativo!', error_singbox_ipv6_address: 'Seu sistema operacional não é compatível com IPv6. Por favor, vá até as configurações do túnel e mude o endereçamento para IPv4.', error_local_date: 'Certifique-se de que a data e a hora do seu sistema estejam configuradas corretamente!' }, about: { title: 'Sobre o Aplicativo', desc: 'Este programa é uma versão não oficial, mas confiável do aplicativo Oblivion para Windows, Linux e Mac.\nO programa Oblivion Desktop foi modelado a partir da interface de usuário da versão original desenvolvida por Yousef Ghobadi. Foi escrito, preparado com o propósito de acesso gratuito à Internet, e qualquer mudança de nome ou uso comercial não é permitida.', slogan: 'Internet, para todos ou para ninguém!' }, systemTray: { connect: 'Conectar', connecting: 'Conectando ...', connected: 'Conectado', disconnecting: 'Desconectando ...', settings: 'Configurações', settings_warp: 'Warp', settings_network: 'Rede', settings_scanner: 'Scanner', settings_app: 'Aplicativo', about: 'Sobre', log: 'Log', speed_test: 'Teste de velocidade', exit: 'Sair' }, update: { available: 'Atualização Disponível', available_message: (value) => `Uma nova versão do ${value} está disponível. Você quer atualizar agora?`, ready: 'Atualização Pronta', ready_message: (value) => `Uma nova versão do ${value} está pronta. Ela será instalada após uma reinicialização. Você quer reiniciar agora?` }, speedTest: { title: 'Teste de Velocidade', initializing: 'Inicializando teste de velocidade ...', click_start: 'Clique no botão para iniciar o teste de velocidade', error_msg: 'Ocorreu um erro durante o teste de velocidade. Por favor, tente novamente.', server_unavailable: 'Servidor de teste de velocidade indisponível', download_speed: 'Velocidade de Download', upload_speed: 'Velocidade de Upload', latency: 'Latência', jitter: 'Jitter' } }; export default brazilianPortuguese; ================================================ FILE: src/localization/ru.ts ================================================ import { Language } from './type'; const russian: Language = { global: {}, status: { connecting: 'Подключение ...', connected: 'Подключено', connected_confirm: 'Подключено', disconnecting: 'Отключение ...', disconnected: 'Отключено', ip_check: 'Проверка IP ...', keep_trying: 'Пожалуйста, подождите немного, чтобы попытаться снова...', preparing_rulesets: 'Подготовка наборов правил...', downloading_rulesets_failed: 'Не удалось загрузить наборы правил.' }, home: { title_warp_based: 'На основе Warp', drawer_settings_warp: 'Настройки Warp', drawer_settings_routing_rules: 'Правила маршрутизации', drawer_settings_app: 'Настройки приложения', drawer_settings_scanner: 'Настройки сканера', drawer_settings_network: 'Настройки сети', drawer_log: 'Журнал приложений', drawer_update: 'Обновление', drawer_update_label: 'Новое обновление', drawer_speed_test: 'Тест скорости', drawer_about: 'О приложении', drawer_lang: 'Изменить язык', drawer_singbox: 'Настройки туннеля' }, toast: { ip_check_please_wait: 'Пожалуйста, подождите несколько секунд, чтобы повторить проверку!', ir_location: 'Cloudflare подключился к IP с иранским местоположением, которое отличается от вашего фактического IP. Вы можете использовать его для обхода фильтрации, но не санкций. Не волнуйтесь! Вы можете изменить местоположение в настройках, используя опцию "Gool" или "psiphon".', btn_submit: 'Понятно', copied: 'Скопировано!', cleared: 'Журнал очищен!', please_wait: 'Пожалуйста, подождите ...', offline: 'Вы не подключены к интернету!', settings_changed: 'Применение настроек требует повторного подключения.', hardware_usage: 'Включение этой опции увеличит использование ресурсов оборудования.', config_added: 'Конфигурация успешно добавлена, и для ее использования вам нужно нажать на подключение.', profile_added: 'Точка доступа успешно добавлена в профиль.', endpoint_added: 'Конечная точка успешно заменена.', new_update_notification: 'Доступна новая версия', new_update: 'Доступна новая версия приложения. Хотите скачать и подготовить её к установке?', up_to_date: 'У вас установлена новейшая версия приложения', exit_pending: 'Приложение завершает процесс выхода; пожалуйста, подождите немного, прежде чем запустить его снова.', help_btn: 'Помощь' }, settings: { title: 'Настройки Warp', more: 'Дополнительные настройки', method_warp: 'Warp', method_warp_desc: 'Включить Warp', method_gool: 'Gool', method_gool_desc: 'Включить WarpInWarp', method_masque: 'Маск', method_masque_desc: 'Включить Masque', method_psiphon: 'Psiphon', method_psiphon_desc: 'Включить Psiphon', method_psiphon_location: 'Страна', method_psiphon_location_auto: 'случайный', method_psiphon_location_desc: 'Выберите желаемый IP-адрес страны', endpoint: 'Конечная точка', endpoint_desc: 'Комбинация IP-адреса или доменного имени вместе с портом', license: 'Лицензия', license_desc: 'Потребление лицензии удвоено', option: 'Настройки приложения', network: 'Настройки сети', proxy_mode: 'Конфигурация', proxy_mode_desc: 'Определение настроек прокси', port: 'Порт прокси', port_desc: 'Установите порт прокси для приложения', share_vpn: 'Адрес привязки', share_vpn_desc: 'Поделиться прокси в сети', dns: 'DNS', dns_desc: 'Блокировать рекламу и контент для взрослых', dns_error: 'Это применимо к методам Warp и Gool', ip_data: 'Проверка IP', ip_data_desc: 'Отображение IP-адреса и местоположения после подключения', data_usage: 'Использование данных', data_usage_desc: 'Отображение использования данных и скорости в реальном времени', dark_mode: 'Темный режим', dark_mode_desc: 'Укажите режим отображения приложения', lang: 'Язык', lang_desc: 'Изменить язык интерфейса приложения', open_login: 'Начать при входе в систему', open_login_desc: 'Открывать при запуске системы', auto_connect: 'Автоматическое подключение', auto_connect_desc: 'Подключаться при открытии приложения', start_minimized: 'Запуск с минимизированным окном', start_minimized_desc: 'Минимизировать при запуске приложения', system_tray: 'Системный трей', system_tray_desc: 'Не размещать значок программы на панели задач', force_close: 'Принудительное закрытие', force_close_desc: 'Не оставаться в системном трее после выхода', shortcut: 'Навигатор', shortcut_desc: 'Ярлыки на главной странице', sound_effect: 'звуковой эффект', sound_effect_desc: 'воспроизводит звук при успешном подключении', restore: 'Восстановить', restore_desc: 'Применить настройки приложения по умолчанию', scanner: 'Настройки сканера', scanner_alert: 'Сканер активируется, если вы используете адрес конечной точки по умолчанию.', scanner_ip_type: 'Тип конечной точки', scanner_ip_type_auto: 'Автоматически', scanner_ip_type_desc: 'Чтобы найти IP конечной точки', scanner_rtt: 'Интервал', scanner_rtt_default: 'По умолчанию', scanner_rtt_desc: 'Ограничение RTT сканера', scanner_reserved: 'Зарезервировано', scanner_reserved_desc: 'Переопределить зарезервированное значение Wireguard', routing_rules: 'Черный список', routing_rules_desc: 'Предотвращать прохождение трафика через искажение', routing_rules_disabled: 'Отключено', routing_rules_items: 'Предметы', profile: 'Профиль', profile_desc: 'Сохраненные вами конечные точки', singbox: 'Настройки туннеля', close_singbox: 'Остановка операции', close_singbox_desc: 'Автоматически закрывать Singbox при отключении', close_helper: 'Остановка помощника', close_helper_desc: 'Автоматически закрывать помощник при выходе', mtu: 'Значение MTU', mtu_desc: 'Установить максимальную единицу передачи', geo_block: 'Блокировка', geo_block_desc: 'Реклама, вредоносное ПО, фишинг и майнеры криптовалют', geo_rules_ip: 'Маршрутизация IP', geo_rules_ip_desc: 'Применение правил GeoIP', geo_rules_site: 'Веб-маршрутизация', geo_rules_site_desc: 'Применение правил GeoSite', geo_nsfw_block: 'Фильтр контента', geo_nsfw_block_desc: 'Блокировка сайтов NSFW', more_helper: 'Настройки ассистента', singbox_log: 'Журналирование', singbox_log_desc: 'Установить уровень журнала', singbox_stack: 'Стек', singbox_stack_desc: 'Установить тип стека', singbox_sniff: 'Перехват', singbox_sniff_desc: 'Включить сканирование и переопределить назначение', singbox_addressing: 'Адресация', singbox_addressing_desc: 'Установите тип адреса интерфейса', singbox_udp_block: 'Блокировать UDP', singbox_udp_block_desc: 'Полная блокировка всего UDP-трафика', singbox_discord_bypass: 'Discord', singbox_discord_bypass_desc: 'Обход фильтрации Discord', more_duties: 'Дополнительные обязанности', beta_release: 'Бета-обновление', beta_release_desc: 'Будьте в курсе предварительных версий' }, tabs: { home: 'Связь', warp: 'Warp', network: 'Сеть', scanner: 'Сканер', app: 'заявка', singbox: 'Туннель' }, modal: { endpoint_title: 'Конечная точка', license_title: 'Лицензия', license_desc: 'Для работы программы не обязательно наличие лицензии Warp, но если вы хотите, вы можете ввести свою лицензию здесь.', form_clear: 'Очистить', test_url_title: 'Тестовый URL', test_url_desc: 'Адрес тестирования подключения', test_url_update: 'Получение предложений', port_title: 'Порт прокси', restore_title: 'Восстановить изменения', restore_desc: 'Подтверждая операцию восстановления изменений, все настройки программы вернутся к состоянию по умолчанию, и ваше подключение будет отключено.', routing_rules_sample: 'Пример', routing_rules_alert_tun: 'Только правила маршрутизации для домена, ip и приложения повлияют на конфигурацию Tun.', routing_rules_alert_system: 'За исключением правила маршрутизации приложения, другие правила повлияют на конфигурацию системного прокси.', form_default: 'По умолчанию', endpoint_suggested: 'предложено', endpoint_latest: 'Последний', endpoint_update: 'Получить предложенные конечные точки', endpoint_paste: 'вставить активный endpoint', profile_title: 'Профиль', profile_name: 'Название', profile_endpoint: 'Конечная точка', profile_limitation: (value) => `Вы можете добавить максимум ${value} конечных точек.`, mtu_title: 'Значение MTU', mtu_desc: 'Максимальная единица передачи (MTU) относится к максимальному размеру пакетов данных, который должен быть установлен в пределах от 1000 до 9999.', custom_dns_title: 'Пользовательский DNS', confirm: 'Подтверждаю', update: 'Обновить', cancel: 'Отмена', yes: 'Да', no: 'Нет' }, log: { title: 'Журнал приложений', desc: 'Если программа создает журнал, он будет отображаться здесь.', error_invalid_license: 'Введенная лицензия недействительна; Убери это.', error_too_many_connected: 'Лимит использования лицензии заполнен; Убери это.', error_access_denied: 'Запустите программу от имени администратора.', error_failed_set_endpoint: 'Проверьте или замените значение конечной точки или повторите попытку.', error_warp_identity: 'Ошибка аутентификации в Cloudflare!', error_script_failed: 'В программе произошла ошибка; Попробуйте еще раз.', error_object_null: 'В программе произошла ошибка; Попробуйте еще раз.', error_port_already_in_use: (value) => `Порт ${value} используется другой программой; Измени это.`, error_port_socket: 'Использовать другой порт.', error_port_restart: 'Порт занят; перезапуск ...', error_unknown_flag: 'В фоновом режиме была выполнена недопустимая команда.', error_deadline_exceeded: 'Время ожидания соединения истекло; Попробуйте еще раз.', error_configuration_encountered: 'В конфигурации прокси произошла ошибка!', error_desktop_not_supported: 'Среда рабочего стола не поддерживается!', error_configuration_not_supported: 'Конфигурация прокси-сервера не поддерживается в вашей операционной системе, но вы можете использовать Warp Proxy вручную.', error_configuring_proxy: (value) => `Ошибка настройки прокси-сервера для ${value}!`, error_wp_not_found: 'Файл warp-plus не находится рядом с пакетом приложения.', error_mp_not_found: 'Файл masque-plus не найден рядом с пакетом приложения!', error_usque_not_found: 'Файл usque не найден рядом с пакетом приложения!', error_wp_exclusions: 'Файл warp-plus, вероятно, был помещен в карантин из-за ложного срабатывания и неправильного обнаружения антивирусом, что вызвало проблемы с доступом программы к интернету.\nПрограмма может добавить указанный файл в список исключений в некоторых антивирусах, если будет предоставлен доступ. Это должно быть сделано?', error_wp_stopped: 'Файл warp-plus столкнулся с проблемой при запуске!', error_connection_failed: 'Подключение к 1.1.1.1 невозможно.', error_country_failed: 'Невозможно подключиться к выбранной стране.', error_singbox_failed_stop: 'Не удалось остановить Sing-Box!', error_singbox_failed_start: 'Не удалось запустить Sing-Box!', error_wp_reset_peer: 'Соединение с Cloudflare было неожиданно прервано!', error_failed_connection: 'Не удалось установить соединение!', error_canceled_by_user: 'Операция была отменена пользователем.', error_helper_not_found: 'Файл помощника не найден рядом с пакетом приложения!', error_singbox_ipv6_address: 'Ваша операционная система не поддерживает IPv6. Пожалуйста, перейдите в настройки туннеля и измените тип адресации на IPv4.', error_local_date: 'Убедитесь, что дата и время вашей системы установлены правильно!' }, about: { title: 'О приложении', desc: 'Эта программа является неофициальной, но надежной версией приложения Oblivion для Windows, Linux и Mac.\nПрограмма Oblivion Desktop создана по образу и подобию пользовательского интерфейса оригинальной версии, разработанной Yousef Ghobadi. Она была написана с целью обеспечить свободный доступ в Интернет, и любое изменение имени или коммерческое использование запрещено.', slogan: 'Интернет, для всех или никого!' }, systemTray: { connect: 'Подключиться', connecting: 'Подключение ...', connected: 'Подключено', disconnecting: 'Отключение ...', settings: 'Настройки', settings_warp: 'Warp', settings_network: 'Сеть', settings_scanner: 'Сканер', settings_app: 'Приложение', about: 'О программе', log: 'Журнал', speed_test: 'Тест скорости', exit: 'Выход' }, update: { available: 'Доступно обновление', available_message: (value) => `Доступна новая версия ${value}. Хотите обновить сейчас?`, ready: 'Обновление готово', ready_message: (value) => `Новая версия ${value} готова. Она будет установлена после перезагрузки. Хотите перезагрузить сейчас?` }, speedTest: { title: 'Тест скорости', initializing: 'Инициализация теста скорости ...', click_start: 'Нажмите кнопку, чтобы начать тест скорости', error_msg: 'Произошла ошибка во время теста скорости. Пожалуйста, попробуйте снова.', server_unavailable: 'Сервер проверки скорости недоступен', download_speed: 'Скорость загрузки', upload_speed: 'Скорость выгрузки', latency: 'Задержка', jitter: 'Джиттер' } }; export default russian; ================================================ FILE: src/localization/tr.ts ================================================ import { Language } from './type'; const turkish: Language = { global: {}, status: { connecting: 'Bağlanıyor ...', connected: 'Bağlandı', connected_confirm: 'Bağlandı', disconnecting: 'Bağlantı kesiliyor ...', disconnected: 'Bağlantı kesildi', ip_check: 'IP kontrol ediliyor ...', keep_trying: 'Tekrar denemek için lütfen bir süre bekleyin...', preparing_rulesets: 'Kurallar seti hazırlanıyor...', downloading_rulesets_failed: 'Kurallar seti indirilemedi.' }, home: { title_warp_based: 'Warp Tabanlı', drawer_settings_warp: 'Warp Ayarları', drawer_settings_routing_rules: 'Yönlendirme Kuralları', drawer_settings_app: 'Uygulama Ayarları', drawer_settings_scanner: 'Tarayıcı Ayarları', drawer_settings_network: 'Ağ Ayarları', drawer_log: 'Uygulama Günlüğü', drawer_update: 'Güncelleme', drawer_update_label: 'Yeni Güncelleme', drawer_speed_test: 'Hız Testi', drawer_about: 'Uygulama Hakkında', drawer_lang: 'Dil Değişikliği', drawer_singbox: 'Tünel Ayarlar' }, toast: { ip_check_please_wait: 'Lütfen kontrolü yeniden denemek için birkaç saniye bekleyin!', ir_location: 'Cloudflare, gerçek IP\'nizden farklı olan İran konumlu bir IP\'ye bağlandı. Bunu filtrelemeyi aşmak için kullanabilirsiniz, ancak yaptırımlar için değil. Endişelenmeyin! Ayarlar bölümünde "Gool" veya "Psiphon" seçeneğini kullanarak konumu değiştirebilirsiniz.', btn_submit: 'Anladım', copied: 'Kopyalandı!', cleared: 'Günlük temizlendi!', please_wait: 'Lütfen Bekleyin ...', offline: 'Çevrimdışısınız!', settings_changed: 'Ayarları uygulamak için yeniden bağlanma gerekiyor.', hardware_usage: 'Bu seçeneği etkinleştirmek donanım kaynaklarının kullanımını artıracaktır.', config_added: 'Yapılandırma başarıyla eklendi ve kullanmak için bağlantıya tıklamanız gerekiyor.', profile_added: 'Uç nokta başarıyla profile eklendi.', endpoint_added: 'Uç nokta başarıyla değiştirildi.', new_update_notification: 'Yeni bir sürüm mevcut', new_update: 'Uygulamanın yeni bir sürümü mevcut. İndirmek ve kuruluma hazırlamak ister misiniz?', up_to_date: 'Uygulamanın en yeni sürümünü kullanıyorsunuz', exit_pending: 'Uygulama çıkış sürecini tamamlıyor; lütfen tekrar başlatmadan önce bir süre bekleyin.', help_btn: 'Yardım' }, settings: { title: 'Warp Ayarları', more: 'Diğer Ayarlar', method_warp: 'Warp', method_warp_desc: "Warp'ı etkinleştir", method_gool: 'Gool', method_gool_desc: "WarpInWarp'ı etkinleştir", method_masque: 'Masque', method_masque_desc: "Masque'i etkinleştir", method_psiphon: 'Psiphon', method_psiphon_desc: "Psiphon'u etkinleştir", method_psiphon_location: 'Ülke', method_psiphon_location_auto: 'rastgele', method_psiphon_location_desc: "İstenen ülke IP'sini seçin", endpoint: 'Endpoint', endpoint_desc: 'IP veya alan adı kombinasyonu, port ile birlikte', license: 'Lisans', license_desc: 'Lisans tüketimi iki katına çıkar', option: 'Uygulama Ayarları', network: 'Ağ Ayarları', proxy_mode: 'Yapılandırma', proxy_mode_desc: 'Proxy Ayarlarını Tanımla', port: 'Proxy Portu', port_desc: 'Uygulama proxy portunu tanımlayın', share_vpn: 'Bağlama adresi', share_vpn_desc: 'Ağda proxy paylaşımı', dns: 'DNS', dns_desc: 'Reklamları ve yetişkin içeriklerini engelle', dns_error: 'Warp ve Gool yöntemlerine uygulanabilir', ip_data: 'IP Kontrolü', ip_data_desc: 'Bağlantı sonrası IP ve konumu göster', data_usage: 'Veri Kullanımı', data_usage_desc: 'Veri kullanımını ve gerçek zamanlı hızı göster', dark_mode: 'Karanlık Mod', dark_mode_desc: 'Uygulamanın görüntüleme modunu belirtin', lang: 'Dil', lang_desc: 'Uygulama arayüzü dilini değiştir', open_login: 'Girişte Başlat', open_login_desc: 'Sistem başlangıcında aç', auto_connect: 'Otomatik Bağlantı', auto_connect_desc: 'Uygulama açıldığında bağlan', start_minimized: 'Başlatıldığında Küçült', start_minimized_desc: 'Uygulama açıldığında küçült', system_tray: 'Sistem Tepsisi', system_tray_desc: 'Görev çubuğunda program simgesi yerleştirme', force_close: 'Zorla Kapat', force_close_desc: 'Çıkışta sistem tepsisinde kalma', shortcut: 'Kısayol', shortcut_desc: 'Ana sayfadaki kısayollar', sound_effect: 'ses efekti', sound_effect_desc: 'başarılı bir bağlantıda ses çalar', restore: 'Geri Yükle', restore_desc: 'Varsayılan uygulama ayarlarını uygula', scanner: 'Tarayıcı Ayarları', scanner_alert: 'Varsayılan endpoint adresini kullanıyorsanız tarayıcı etkinleştirilir.', scanner_ip_type: 'Endpoint türü', scanner_ip_type_auto: 'Otomatik', scanner_ip_type_desc: "Endpoint IP'si bulmak için", scanner_rtt: 'Aralık', scanner_rtt_default: 'Varsayılan', scanner_rtt_desc: 'Tarayıcı RTT sınırı', scanner_reserved: 'Rezerve', scanner_reserved_desc: 'Wireguard rezerve değerini geçersiz kıl', routing_rules: 'Kara Liste', routing_rules_desc: 'Trafiğin warp üzerinden geçmesini engelle', routing_rules_disabled: 'Devre Dışı', routing_rules_items: 'Öğeler', profile: 'Profil', profile_desc: "Tarafınızdan kaydedilen endpoint'ler", singbox: 'Tünel Ayarlar', close_singbox: 'İşlemi durdur', close_singbox_desc: "Bağlantı kesildiğinde sing-box'ı otomatik olarak kapat", close_helper: 'Yardımcıyı durdur', close_helper_desc: 'Çıkışta yardımcıyı otomatik olarak kapat', mtu: 'MTU Değeri', mtu_desc: 'Maksimum İletim Birimini Ayarla', geo_block: 'Engelleme', geo_block_desc: 'Reklamlar, kötü amaçlı yazılımlar, kimlik avı ve kripto madencileri', geo_rules_ip: 'IP Yönlendirme', geo_rules_ip_desc: 'GeoIP kurallarını uygulama', geo_rules_site: 'Web Yönlendirme', geo_rules_site_desc: 'GeoSite kurallarını uygulama', geo_nsfw_block: 'İçerik Filtresi', geo_nsfw_block_desc: 'NSFW sitelerini engelle', more_helper: 'Yardımcı Ayarları', singbox_log: 'Kayıt', singbox_log_desc: 'Kayıt Seviyesi Ayarla', singbox_stack: 'Yığın', singbox_stack_desc: 'Yığın Tipini Ayarla', singbox_sniff: 'Dinleme', singbox_sniff_desc: 'Sniffing’i Etkinleştir ve Hedefi Geçersiz Kıl', singbox_addressing: 'Adresleme', singbox_addressing_desc: 'Arayüz Adres Türünü Ayarlayın', singbox_udp_block: "UDP'yi Engelle", singbox_udp_block_desc: 'Tüm UDP trafiğini tamamen engelle', singbox_discord_bypass: 'Discord', singbox_discord_bypass_desc: 'Discord engellemesini aşma', more_duties: 'Daha fazla görev', beta_release: 'Beta Güncellemesi', beta_release_desc: 'Önizleme sürümleri hakkında bilgi sahibi olun' }, tabs: { home: 'Bağlan', warp: 'Warp', network: 'Ağ', scanner: 'Tarayıcı', app: 'Uygulama', singbox: 'Tünel' }, modal: { endpoint_title: 'Endpoint', license_title: 'Lisans', license_desc: 'Program çalışması için Warp lisansı gerektirmez, ancak isterseniz lisansınızı buraya girebilirsiniz.', form_clear: 'Temizle', test_url_title: 'Test URL', test_url_desc: 'Bağlantı testi adresi', test_url_update: 'Önerileri almak', port_title: 'Proxy Portu', restore_title: 'Değişiklikleri Geri Yükle', restore_desc: 'Değişiklikleri geri yükleme işlemini onaylayarak, tüm program ayarları varsayılan duruma döner ve bağlantınız kesilir.', routing_rules_sample: 'Örnek', routing_rules_alert_tun: 'Yalnızca etki alanı, ip ve uygulama için yönlendirme kuralları Tun yapılandırmasını etkileyecektir.', routing_rules_alert_system: 'Uygulama yönlendirme kuralı dışında, diğer kurallar Sistem Proxy yapılandırmasını etkileyecektir.', form_default: 'Varsayılan', endpoint_suggested: 'Önerilen', endpoint_latest: 'En Son', endpoint_update: "Önerilen endpoint'leri al", endpoint_paste: 'Aktif endpoint yapıştır', profile_title: 'Profil', profile_name: 'Başlık', profile_endpoint: 'Endpoint', profile_limitation: (value) => `En fazla ${value} endpoint ekleyebilirsiniz.`, mtu_title: 'MTU Değeri', mtu_desc: 'Maksimum İletim Birimi (MTU), veri paketlerinin maksimum boyutunu ifade eder ve 1000 ile 9999 arasında ayarlanmalıdır.', custom_dns_title: 'Özel DNS', confirm: 'Onaylıyorum', update: 'Güncelle', cancel: 'İptal', yes: 'Evet', no: 'Hayır' }, log: { title: 'Uygulama Günlüğü', desc: 'Program tarafından oluşturulan bir günlük varsa, burada gösterilecektir.', error_invalid_license: 'Girilen lisans geçerli değil; Kaldırın.', error_too_many_connected: 'Lisans kullanım sınırı doldu; Kaldırın.', error_access_denied: 'Programı Yönetici Olarak Çalıştırın.', error_failed_set_endpoint: 'Endpoint değerini kontrol edin veya değiştirin, ardından tekrar deneyin.', error_warp_identity: 'Cloudflare kimlik doğrulama hatası!', error_script_failed: 'Program bir hata ile karşılaştı; Tekrar deneyin.', error_object_null: 'Program bir hata ile karşılaştı; Tekrar deneyin.', error_port_already_in_use: (value) => `Port ${value} başka bir program tarafından kullanılıyor; Değiştirin.`, error_port_socket: 'Başka bir port kullanın.', error_port_restart: 'Port kullanılıyor; yeniden başlatılıyor ...', error_unknown_flag: 'Arka planda geçersiz bir komut yürütüldü.', error_deadline_exceeded: 'Bağlantı zaman aşımına uğradı; Tekrar deneyin.', error_configuration_encountered: 'Proxy yapılandırması hatası ile karşılaşıldı!', error_desktop_not_supported: 'Masaüstü ortamı desteklenmiyor!', error_configuration_not_supported: 'Proxy yapılandırması işletim sisteminizde desteklenmiyor, ancak manuel olarak Warp Proxy kullanabilirsiniz.', error_configuring_proxy: (value) => `${value} için proxy yapılandırılırken hata oluştu!`, error_wp_not_found: 'Warp-plus dosyası uygulama paketiyle birlikte yer almıyor!', error_mp_not_found: 'masque-plus dosyası uygulama paketiyle birlikte bulunamadı!', error_usque_not_found: 'usque dosyası uygulama paketiyle birlikte bulunamadı!', error_wp_exclusions: 'warp-plus dosyasının yanlış pozitif bildirim ve antivirüs tarafından yanlış tespit nedeniyle karantinaya alındığı ve programın internet erişimini serbest bir şekilde sağlamakta sorunlara yol açtığı muhtemeldir.\nProgram, erişim izni verilirse, bu dosyayı bazı antivirüslerde hariç tutma listesine ekleyebilir. Bunu yapmak ister misiniz?', error_wp_stopped: 'warp-plus dosyası çalışırken bir sorunla karşılaştı!', error_connection_failed: '1.1.1.1 ile bağlantı sağlanamadı.', error_country_failed: 'Seçilen ülkeye bağlanılamıyor.', error_singbox_failed_stop: 'Sing-Box durdurulamadı!', error_singbox_failed_start: 'Sing-Box başlatılamadı!', error_wp_reset_peer: 'Cloudflare bağlantısı beklenmedik bir şekilde kesildi!', error_failed_connection: 'Bağlantı kurulamadı!', error_canceled_by_user: 'İşlem kullanıcı tarafından iptal edildi.', error_helper_not_found: 'Yardımcı dosya uygulama paketinin yanında bulunamadı!', error_singbox_ipv6_address: 'İşletim sisteminiz IPv6 desteklemiyor. Lütfen tünel ayarlarına gidin ve adreslemeyi IPv4 olarak değiştirin.', error_local_date: 'Sisteminizin tarih ve saatinin doğru ayarlandığından emin olun!' }, about: { title: 'Uygulama Hakkında', desc: 'Bu program, Windows, Linux ve Mac için Oblivion uygulamasının gayri resmi, ancak güvenilir bir sürümüdür.\nOblivion Masaüstü programı, orijinal sürümün kullanıcı arayüzünü modelleyen Yousef Ghobadi tarafından geliştirilmiştir. Ücretsiz internet erişimi sağlamak amacıyla yazılmıştır ve isim değişikliği veya ticari kullanımına izin verilmez.', slogan: 'İnternet, ya herkes için ya da hiç kimse için!' }, systemTray: { connect: 'Bağlan', connecting: 'Bağlanıyor ...', connected: 'Bağlandı', disconnecting: 'Bağlantı kesiliyor ...', settings: 'Ayarlar', settings_warp: 'Warp', settings_network: 'Ağ', settings_scanner: 'Tarayıcı', settings_app: 'Uygulama', about: 'Hakkında', log: 'Günlük', speed_test: 'Hız testi', exit: 'Çıkış' }, update: { available: 'Güncelleme Mevcut', available_message: (value) => `Yeni bir ${value} sürümü mevcut. Şimdi güncellemek ister misiniz?`, ready: 'Güncelleme Hazır', ready_message: (value) => `Yeni bir ${value} sürümü hazır. Yeniden başlatma sonrası yüklenecek. Şimdi yeniden başlatmak ister misiniz?` }, speedTest: { title: 'Hız Testi', initializing: 'Hız testi başlatılıyor ...', click_start: 'Hız testini başlatmak için düğmeye tıklayın', error_msg: 'Hız testi sırasında bir hata oluştu. Lütfen tekrar deneyin.', server_unavailable: 'Hız testi sunucusu mevcut değil', download_speed: 'İndirme Hızı', upload_speed: 'Yükleme Hızı', latency: 'Gecikme', jitter: 'Jitter' } }; export default turkish; ================================================ FILE: src/localization/type.ts ================================================ export interface Global {} export interface Status { connecting: string; connected: string; connected_confirm: string; disconnecting: string; disconnected: string; ip_check: string; keep_trying: string; preparing_rulesets: string; downloading_rulesets_failed: string; } export interface Home { title_warp_based: string; drawer_settings_warp: string; drawer_settings_routing_rules: string; drawer_settings_app: string; drawer_settings_scanner: string; drawer_settings_network: string; drawer_log: string; drawer_update: string; drawer_update_label: string; drawer_speed_test: string; drawer_about: string; drawer_lang: string; drawer_singbox: string; } export interface Toast { ip_check_please_wait: string; ir_location: string; btn_submit: string; copied: string; cleared: string; please_wait: string; offline: string; settings_changed: string; hardware_usage: string; config_added: string; profile_added: string; endpoint_added: string; new_update_notification: string; new_update: string; up_to_date: string; exit_pending: string; help_btn: string; } export interface Settings { title: string; more: string; method_warp: string; method_warp_desc: string; method_masque: string; method_masque_desc: string; method_gool: string; method_gool_desc: string; method_psiphon: string; method_psiphon_desc: string; method_psiphon_location: string; method_psiphon_location_auto: string; method_psiphon_location_desc: string; endpoint: string; endpoint_desc: string; license: string; license_desc: string; option: string; network: string; proxy_mode: string; proxy_mode_desc: string; port: string; port_desc: string; share_vpn: string; share_vpn_desc: string; dns: string; dns_desc: string; dns_error: string; ip_data: string; ip_data_desc: string; data_usage: string; data_usage_desc: string; dark_mode: string; dark_mode_desc: string; lang: string; lang_desc: string; open_login: string; open_login_desc: string; auto_connect: string; auto_connect_desc: string; start_minimized: string; start_minimized_desc: string; system_tray: string; system_tray_desc: string; force_close: string; force_close_desc: string; shortcut: string; shortcut_desc: string; sound_effect: string; sound_effect_desc: string; restore: string; restore_desc: string; scanner: string; scanner_alert: string; scanner_ip_type: string; scanner_ip_type_auto: string; scanner_ip_type_desc: string; scanner_rtt: string; scanner_rtt_default: string; scanner_rtt_desc: string; scanner_reserved: string; scanner_reserved_desc: string; routing_rules: string; routing_rules_desc: string; routing_rules_disabled: string; routing_rules_items: string; profile: string; profile_desc: string; singbox: string; close_singbox: string; close_singbox_desc: string; close_helper: string; close_helper_desc: string; mtu: string; mtu_desc: string; geo_block: string; geo_block_desc: string; geo_rules_ip: string; geo_rules_ip_desc: string; geo_rules_site: string; geo_rules_site_desc: string; geo_nsfw_block: string; geo_nsfw_block_desc: string; more_helper: string; singbox_log: string; singbox_log_desc: string; singbox_stack: string; singbox_stack_desc: string; singbox_sniff: string; singbox_sniff_desc: string; singbox_addressing: string; singbox_addressing_desc: string; singbox_udp_block: string; singbox_udp_block_desc: string; singbox_discord_bypass: string; singbox_discord_bypass_desc: string; more_duties: string; beta_release: string; beta_release_desc: string; } export interface Tabs { home: string; warp: string; network: string; scanner: string; app: string; singbox: string; } export interface Modal { endpoint_title: string; license_title: string; license_desc: string; form_clear: string; test_url_title: string; test_url_desc: string; test_url_update: string; port_title: string; restore_title: string; restore_desc: string; routing_rules_sample: string; routing_rules_alert_system: string; routing_rules_alert_tun: string; form_default: string; endpoint_suggested: string; endpoint_latest: string; endpoint_update: string; endpoint_paste: string; profile_title: string; profile_name: string; profile_endpoint: string; profile_limitation: (value: string) => string; mtu_title: string; custom_dns_title: string; mtu_desc: string; confirm: string; update: string; cancel: string; yes: string; no: string; } export interface Log { title: string; desc: string; error_invalid_license: string; error_too_many_connected: string; error_access_denied: string; error_failed_set_endpoint: string; error_warp_identity: string; error_script_failed: string; error_object_null: string; error_port_already_in_use: (value: string) => string; error_port_socket: string; error_port_restart: string; error_unknown_flag: string; error_country_failed: string; error_deadline_exceeded: string; error_configuration_encountered: string; error_desktop_not_supported: string; error_configuration_not_supported: string; error_configuring_proxy: (value: string) => string; error_wp_not_found: string; error_mp_not_found: string; error_usque_not_found: string; error_wp_exclusions: string; error_wp_stopped: string; error_connection_failed: string; error_singbox_failed_stop: string; error_singbox_failed_start: string; error_wp_reset_peer: string; error_failed_connection: string; error_canceled_by_user: string; error_helper_not_found: string; error_singbox_ipv6_address: string; error_local_date: string; } export interface About { title: string; desc: string; slogan: string; } export interface SystemTray { connect: string; connecting: string; connected: string; disconnecting: string; settings: string; settings_warp: string; settings_network: string; settings_scanner: string; settings_app: string; about: string; log: string; speed_test: string; exit: string; } export interface Update { available: string; available_message: (value: string) => string; ready: string; ready_message: (value: string) => string; } export interface SpeedTest { title: string; initializing: string; click_start: string; error_msg: string; server_unavailable: string; download_speed: string; upload_speed: string; latency: string; jitter: string; } export interface Language { global: Global; status: Status; home: Home; toast: Toast; settings: Settings; tabs: Tabs; modal: Modal; log: Log; about: About; systemTray: SystemTray; update: Update; speedTest: SpeedTest; } ================================================ FILE: src/localization/ur.ts ================================================ import { Language } from './type'; const urdu: Language = { global: {}, status: { connecting: 'کنیکٹ ہو رہا ہے...', connected: 'کنیکٹڈ', connected_confirm: 'کنیکٹڈ کی تصدیق ہو گئی', disconnecting: 'ڈسکنیکٹ ہو رہا ہے...', disconnected: 'ڈسکنیکٹڈ', ip_check: 'آئی پی چیک ہو رہا ہے...', keep_trying: 'براہ کرم دوبارہ کوشش کے لیے چند لمحے انتظار کریں...', preparing_rulesets: 'قواعد کا سیٹ تیار کیا جا رہا ہے...', downloading_rulesets_failed: 'قواعد کا سیٹ ڈاؤن لوڈ کرنے میں ناکام.' }, home: { title_warp_based: 'وارپ بیسڈ', drawer_settings_warp: 'وارپ سیٹنگز', drawer_settings_routing_rules: 'روٹنگ رولز', drawer_settings_app: 'ایپ سیٹنگز', drawer_settings_scanner: 'سکینر سیٹنگز', drawer_settings_network: 'نیٹ ورک سیٹنگز', drawer_log: 'ایپ لاگ', drawer_update: 'اپڈیٹ', drawer_update_label: 'نئی اپڈیٹ دستیاب ہے', drawer_speed_test: 'اسپیڈ ٹیسٹ', drawer_about: 'ایپ کے بارے میں', drawer_lang: 'زبان تبدیل کریں', drawer_singbox: 'ٹنل کی ترتیبات' }, toast: { ip_check_please_wait: 'براہ کرم چند سیکنڈ انتظار کریں اور دوبارہ چیک کریں!', ir_location: 'کلاؤڈ فلئیر نے ایرانی لوکیشن کے آئی پی سے کنیکٹ کیا ہے، جو آپ کے اصلی آئی پی سے مختلف ہے۔ آپ اسے فلٹرنگ بائی پاس کے لیے استعمال کر سکتے ہیں، لیکن پابندیوں کے لیے نہیں۔ پریشان نہ ہوں! آپ اسے سیٹنگز میں "گول" یا "سائفون" آپشن کا استعمال کر کے تبدیل کر سکتے ہیں۔', btn_submit: 'سمجھ گیا', copied: 'کاپی ہو گیا!', cleared: 'لاگ صاف کر دیا گیا!', please_wait: 'براہ کرم انتظار کریں...', offline: 'آپ آف لائن ہیں!', settings_changed: 'سیٹنگز کو نافذ کرنے کے لیے دوبارہ کنیکٹ ہونا ضروری ہے۔', hardware_usage: 'اس آپشن کو فعال کرنے سے ہارڈویئر وسائل کے استعمال میں اضافہ ہوگا۔', config_added: 'کنفیگریشن کامیابی سے شامل کر دی گئی ہے، اور اسے استعمال کرنے کے لیے آپ کو کنکشن پر کلک کرنا ہوگا۔', profile_added: 'اینڈپوائنٹ کامیابی سے پروفائل میں شامل کیا گیا۔', endpoint_added: 'اینڈ پوائنٹ کو کامیابی کے ساتھ تبدیل کر دیا گیا۔', new_update_notification: 'نیا ورژن دستیاب ہے', new_update: 'ایپ کا نیا ورژن دستیاب ہے۔ کیا آپ اسے ڈاؤن لوڈ کرکے انسٹالیشن کے لیے تیار کرنا چاہیں گے؟', up_to_date: 'آپ ایپ کا تازہ ترین ورژن استعمال کر رہے ہیں', exit_pending: 'ایپلیکیشن اپنے بند ہونے کے عمل کو مکمل کر رہی ہے؛ دوبارہ لانچ کرنے سے پہلے کچھ لمحے انتظار کریں۔', help_btn: 'مدد' }, settings: { title: 'وارپ کی ترتیبات', more: 'مزید ترتیبات', method_warp: 'وارپ', method_warp_desc: 'وارپ فعال کریں', method_gool: 'گول', method_gool_desc: 'وارپ ان وارپ فعال کریں', method_masque: 'ماسک', method_masque_desc: 'ماسک فعال کریں', method_psiphon: 'سائفون', method_psiphon_desc: 'سائفون فعال کریں', method_psiphon_location: 'ملک', method_psiphon_location_auto: 'بے ترتیب', method_psiphon_location_desc: 'پسندیدہ ملک کا آئی پی منتخب کریں', endpoint: 'اینڈپوائنٹ', endpoint_desc: 'آئی پی یا ڈومین نام کا مجموعہ، پورٹ کے ساتھ', license: 'لائسنس', license_desc: 'لائسنس کا استعمال دگنا ہو جاتا ہے', option: 'ایپلیکیشن ترتیبات', network: 'نیٹ ورک ترتیبات', proxy_mode: 'کنفیگریشن', proxy_mode_desc: 'پراکسی ترتیبات کی وضاحت کریں', port: 'پراکسی پورٹ', port_desc: 'ایپلیکیشن پراکسی پورٹ متعین کریں', share_vpn: 'بندش کا پتہ', share_vpn_desc: 'نیٹ ورک پر پراکسی شیئر کریں', dns: 'ڈی این ایس', dns_desc: 'اشتہارات اور بالغ مواد کو بلاک کریں', dns_error: 'یہ وارپ اور گول طریقوں پر لاگو ہوتا ہے', ip_data: 'آئی پی چیک', ip_data_desc: 'کنیکشن کے بعد آئی پی اور مقام دکھائیں', data_usage: 'ڈیٹا استعمال', data_usage_desc: 'ڈیٹا استعمال اور حقیقی وقت کی رفتار دکھائیں', dark_mode: 'ڈارک موڈ', dark_mode_desc: 'ایپلیکیشن کا ڈسپلے موڈ منتخب کریں', lang: 'زبان', lang_desc: 'ایپلیکیشن انٹرفیس کی زبان تبدیل کریں', open_login: 'لاگ ان پر شروع کریں', open_login_desc: 'سسٹم اسٹارٹ اپ پر کھولیں', auto_connect: 'خودکار کنکشن', auto_connect_desc: 'ایپ کھلتے ہی کنکٹ کریں', start_minimized: 'شروع ہونے پر کم سے کم', start_minimized_desc: 'ایپلیکیشن کھلتے ہی کم کرنا', system_tray: 'سسٹم ٹرے', system_tray_desc: 'پروگرام آئیکون کو ٹاسک بار میں نہ رکھیں', force_close: 'زبردستی بند کریں', force_close_desc: 'ایگزٹ پر سسٹم ٹرے میں نہ رہیں', shortcut: 'نیویگیٹر', shortcut_desc: 'ہوم پیج پر شارٹ کٹس', sound_effect: 'آواز کا اثر', sound_effect_desc: 'کامیاب کنکشن پر آواز بجاتا ہے', restore: 'بحال کریں', restore_desc: 'ڈیفالٹ ایپلیکیشن ترتیبات لاگو کریں', scanner: 'سکینر کی ترتیبات', scanner_alert: 'اگر آپ ڈیفالٹ اینڈپوائنٹ ایڈریس استعمال کر رہے ہیں تو سکینر فعال ہو جاتا ہے۔', scanner_ip_type: 'اینڈپوائنٹ قسم', scanner_ip_type_auto: 'خودکار', scanner_ip_type_desc: 'اینڈپوائنٹ آئی پی تلاش کرنے کے لیے', scanner_rtt: 'وقفہ', scanner_rtt_default: 'ڈیفالٹ', scanner_rtt_desc: 'سکینر RTT حد', scanner_reserved: 'ریزروڈ', scanner_reserved_desc: 'وائر گارڈ ریزروڈ ویلیو اوور رائڈ کریں', routing_rules: 'بلیک لسٹ', routing_rules_desc: 'ٹریفک کو وارپ سے گزرنے سے روکیں', routing_rules_disabled: 'غیر فعال', routing_rules_items: 'اشیاء', profile: 'پروفائل', profile_desc: 'آپ کے ذریعے محفوظ کیے گئے اینڈپوائنٹس', singbox: 'ٹنل کی ترتیبات', close_singbox: 'آپریشن بند کریں', close_singbox_desc: 'ڈسکنیکٹ پر خودکار طور پر سنگ باکس بند کریں', close_helper: 'مددگار بند کریں', close_helper_desc: 'ایگزٹ پر خودکار طور پر مددگار بند کریں', mtu: 'MTU ویلیو', mtu_desc: 'زیادہ سے زیادہ ٹرانسمیشن یونٹ مقرر کریں', geo_block: 'بلاکنگ', geo_block_desc: 'اشتہارات، میلویئر، فشنگ، اور کرپٹو مائنرز', geo_rules_ip: 'آئی پی روٹنگ', geo_rules_ip_desc: 'GeoIP قواعد لاگو کریں', geo_rules_site: 'ویب روٹنگ', geo_rules_site_desc: 'GeoSite قواعد لاگو کریں', geo_nsfw_block: 'مواد کی فلٹرنگ', geo_nsfw_block_desc: 'NSFW ویب سائٹس کو بلاک کریں', more_helper: 'مددگار کی ترتیبات', singbox_log: 'لاگنگ', singbox_log_desc: 'لاگ سطح سیٹ کریں', singbox_stack: 'اسٹیک', singbox_stack_desc: 'اسٹیک کی قسم سیٹ کریں', singbox_sniff: 'سنیفنگ', singbox_sniff_desc: 'سنیفنگ فعال کریں اور منزل کو اووررائیڈ کریں', singbox_addressing: 'ایڈریسنگ', singbox_addressing_desc: 'انٹرفیس ایڈریس کی قسم سیٹ کریں', singbox_udp_block: 'UDP بلاک کریں', singbox_udp_block_desc: 'UDP ٹریفک کو مکمل طور پر بلاک کریں', singbox_discord_bypass: 'ڈسکارڈ', singbox_discord_bypass_desc: 'ڈسکارڈ کی فلٹرنگ کو عبور کریں', more_duties: 'مزید ذمہ داریاں', beta_release: 'بیٹا اپ ڈیٹ', beta_release_desc: 'پری ریلیز ورژنز کے بارے میں آگاہ رہیں' }, tabs: { home: 'کنیکٹ', warp: 'وارپ', network: 'نیٹ ورک', scanner: 'سکینر', app: 'ایپ', singbox: 'ٹنل' }, modal: { endpoint_title: 'اینڈپوائنٹ', license_title: 'لائسنس', license_desc: 'پروگرام کو چلانے کے لیے ضروری طور پر وارپ لائسنس کی ضرورت نہیں ہے، لیکن اگر آپ چاہیں تو اپنا لائسنس یہاں داخل کر سکتے ہیں۔', form_clear: 'صاف کریں', test_url_title: 'ٹیسٹ یو آر ایل', test_url_desc: 'کنیکٹوٹی ٹیسٹ ایڈریس', test_url_update: 'تجاویز موصول کریں', port_title: 'پراکسی پورٹ', restore_title: 'تبدیلیاں بحال کریں', restore_desc: 'تبدیلیوں کو بحال کرنے کی تصدیق کرنے سے تمام پروگرام سیٹنگز ڈیفالٹ پر واپس آجائیں گی اور آپ کا کنیکشن منقطع ہو جائے گا۔', routing_rules_sample: 'نمونہ', routing_rules_alert_tun: 'صرف ڈومین، آئی پی، اور ایپ کے لیے روٹنگ رولز "تن" کنفیگریشن کو متاثر کریں گے۔', routing_rules_alert_system: 'ایپ روٹنگ رول کے علاوہ دیگر تمام رولز "سسٹم پراکسی" کنفیگریشن کو متاثر کریں گے۔', form_default: 'ڈیفالٹ', endpoint_suggested: 'تجویز کردہ', endpoint_latest: 'تازہ ترین', endpoint_update: 'تجویز کردہ اینڈپوائنٹ وصول کریں', endpoint_paste: 'فعال اینڈپوائنٹ چسپاں کریں', profile_title: 'پروفائل', profile_name: 'عنوان', profile_endpoint: 'اینڈپوائنٹ', profile_limitation: (value) => `آپ زیادہ سے زیادہ ${value} اینڈپوائنٹ شامل کر سکتے ہیں۔`, mtu_title: 'ایم ٹی یو ویلیو', mtu_desc: 'زیادہ سے زیادہ ٹرانسمیشن یونٹ (MTU) ڈیٹا پیکٹ کے زیادہ سے زیادہ سائز کو ظاہر کرتا ہے، جسے 1000 اور 9999 کے درمیان مقرر کرنا چاہیے۔', custom_dns_title: 'حسب ضرورت DNS', confirm: 'میں تصدیق کرتا ہوں', update: 'اپڈیٹ', cancel: 'منسوخ کریں', yes: 'جی ہاں', no: 'نہیں' }, log: { title: 'ایپ لاگ', desc: 'اگر پروگرام کی جانب سے کوئی لاگ تیار کیا گیا ہو، تو وہ یہاں دکھایا جائے گا۔', error_invalid_license: 'داخل کردہ لائسنس درست نہیں ہے؛ اسے ہٹا دیں۔', error_too_many_connected: 'لائسنس کا استعمال حد کو پہنچ چکا ہے؛ اسے ہٹا دیں۔', error_access_denied: 'پروگرام کو "ایڈمنسٹریٹر کے طور پر چلائیں"۔', error_failed_set_endpoint: 'اینڈپوائنٹ ویلیو کو چیک کریں یا تبدیل کریں، یا دوبارہ کوشش کریں۔', error_warp_identity: 'کلاؤڈفلئیر میں توثیق کی غلطی!', error_script_failed: 'پروگرام کو ایک مسئلہ درپیش آیا؛ دوبارہ کوشش کریں۔', error_object_null: 'پروگرام کو ایک مسئلہ درپیش آیا؛ دوبارہ کوشش کریں۔', error_port_already_in_use: (value) => `پورٹ ${value} کسی اور پروگرام کے ذریعے استعمال ہو رہی ہے؛ اسے تبدیل کریں۔`, error_port_socket: 'کوئی اور پورٹ استعمال کریں۔', error_port_restart: 'پورٹ استعمال میں ہے؛ دوبارہ شروع کیا جا رہا ہے ...', error_unknown_flag: 'بیک گراؤنڈ میں ایک غیر قانونی کمانڈ چلائی گئی۔', error_deadline_exceeded: 'کنیکشن کا وقت ختم ہو گیا؛ دوبارہ کوشش کریں۔', error_configuration_encountered: 'پراکسی کنفیگریشن کو ایک مسئلہ درپیش آیا!', error_desktop_not_supported: 'ڈیسک ٹاپ ماحول سپورٹ نہیں کرتا!', error_configuration_not_supported: 'پراکسی کنفیگریشن آپ کے آپریٹنگ سسٹم میں سپورٹ نہیں کرتا، لیکن آپ وارپ پراکسی کو دستی طور پر استعمال کر سکتے ہیں۔', error_configuring_proxy: (value) => `${value} کے لیے پراکسی کنفیگریشن میں مسئلہ!`, error_wp_not_found: 'وارپ پلس فائل ایپلیکیشن پیکیج کے ساتھ موجود نہیں ہے!', error_mp_not_found: 'masque-plus فائل ایپلیکیشن پیکیج کے ساتھ موجود نہیں ہے!', error_usque_not_found: 'usque فائل ایپلیکیشن پیکیج کے ساتھ موجود نہیں ہے!', error_wp_exclusions: 'ممکن ہے کہ وارپ پلس فائل کو ایک جھوٹے مثبت نوٹیفکیشن اور اینٹی وائرس کی غلط شناخت کی وجہ سے قرنطینہ کیا گیا ہو، جس سے پروگرام کی انٹرنیٹ تک آزاد رسائی میں مسائل پیدا ہو گئے ہوں۔\nپروگرام اگر اجازت دی جائے تو اس فائل کو کچھ اینٹی وائرسز میں استثنیات کی فہرست میں شامل کر سکتا ہے۔ کیا یہ عمل کیا جائے؟', error_wp_stopped: 'وارپ پلس فائل چلنے کے دوران مسئلہ پیدا ہوا!', error_connection_failed: '1.1.1.1 سے کنیکشن ممکن نہیں تھا۔', error_country_failed: 'منتخب شدہ ملک سے کنیکٹ نہیں ہو سکتا۔', error_singbox_failed_stop: 'سنگ باکس کو روکنے میں ناکامی!', error_singbox_failed_start: 'سنگ باکس کو شروع کرنے میں ناکامی!', error_wp_reset_peer: 'کلاؤڈفلئیر سے کنیکشن غیر متوقع طور پر منقطع ہو گیا!', error_failed_connection: 'کنکشن قائم کرنے میں ناکامی!', error_canceled_by_user: 'عملیہ صارف کے ذریعہ منسوخ کر دیا گیا۔', error_helper_not_found: 'مددگار فائل ایپلیکیشن پیکیج کے ساتھ موجود نہیں!', error_singbox_ipv6_address: 'آپریٹنگ سسٹم IPv6 کو سپورٹ نہیں کرتا۔ براہ کرم ٹنل سیٹنگز پر جائیں اور ایڈریسنگ کو IPv4 پر تبدیل کریں۔', error_local_date: 'یقین دہانی کریں کہ آپ کے سسٹم کی تاریخ اور وقت درست سیٹ کیے گئے ہیں!' }, about: { title: 'ایپ کے بارے میں', desc: 'یہ پروگرام اوبلیویئن ایپ کا غیر رسمی لیکن قابل اعتماد ورژن ہے جو ونڈوز، لینکس، اور میک کے لیے دستیاب ہے۔\nاوبلیویئن ڈیسک ٹاپ پروگرام کی صارف انٹرفیس کو ماڈل کیا گیا ہے جو یوسف غبادی کے تیار کردہ اصل ورژن پر مبنی ہے۔ یہ مفت انٹرنیٹ تک رسائی کے مقصد کے لیے تیار کیا گیا تھا، اور اس میں کسی قسم کی نام کی تبدیلی یا تجارتی استعمال کی اجازت نہیں ہے۔', slogan: 'انٹرنیٹ، سب کے لیے یا کسی کے لیے نہیں!' }, systemTray: { connect: 'کنیکٹ', connecting: 'کنیکٹ ہو رہا ہے ...', connected: 'کنیکٹڈ', disconnecting: 'ڈسکنیکٹ ہو رہا ہے ...', settings: 'سیٹنگز', settings_warp: 'وارپ', settings_network: 'نیٹ ورک', settings_scanner: 'سکینر', settings_app: 'ایپلیکیشن', about: 'ایپ کے بارے میں', log: 'لاگ', speed_test: 'اسپیڈ ٹیسٹ', exit: 'باہر نکلیں' }, update: { available: 'اپڈیٹ دستیاب ہے', available_message: (value) => `نیا ورژن ${value} دستیاب ہے۔ کیا آپ ابھی اپڈیٹ کرنا چاہتے ہیں؟`, ready: 'اپڈیٹ تیار ہے', ready_message: (value) => `نیا ورژن ${value} تیار ہے۔ یہ ری اسٹارٹ کے بعد انسٹال ہو جائے گا۔ کیا آپ ابھی ری اسٹارٹ کرنا چاہتے ہیں؟` }, speedTest: { title: 'اسپیڈ ٹیسٹ', initializing: 'اسپیڈ ٹیسٹ شروع ہو رہا ہے ...', click_start: 'اسپیڈ ٹیسٹ شروع کرنے کے لیے بٹن پر کلک کریں', error_msg: 'اسپیڈ ٹیسٹ کے دوران ایک مسئلہ پیش آیا۔ براہ کرم دوبارہ کوشش کریں۔', server_unavailable: 'اسپیڈ ٹیسٹ سرور دستیاب نہیں', download_speed: 'ڈاؤنلوڈ اسپیڈ', upload_speed: 'اپلوڈ اسپیڈ', latency: 'لیٹنسی', jitter: 'جٹر' } }; export default urdu; ================================================ FILE: src/localization/useTranslate.ts ================================================ import { useState, useEffect } from 'react'; import { defaultSettings } from '../defaultSettings'; import enUS from './en'; import faIR from './fa'; import ruRU from './ru'; import cnCN from './cn'; import trTR from './tr'; import idID from './id'; import arSA from './ar'; import viVN from './vi'; import ptBR from './pt'; import urPK from './ur'; import esCU from './es'; import amET from './am'; import myMM from './my'; import { Language } from './type'; type LanguageType = | 'fa' | 'en' | 'ru' | 'cn' | 'tr' | 'id' | 'ar' | 'vi' | 'pt' | 'ur' | 'es' | 'am' | 'my'; const useTranslate = (): Language => { const getLanguage = () => { return (localStorage.getItem('lang') || defaultSettings.lang) as LanguageType; }; const [lang, setLang] = useState(getLanguage()); const translations: Record = { fa: faIR, en: enUS, ru: ruRU, cn: cnCN, tr: trTR, id: idID, ar: arSA, vi: viVN, pt: ptBR, ur: urPK, es: esCU, am: amET, my: myMM }; useEffect(() => { const handleStorageChange = () => { setLang(getLanguage()); }; window.addEventListener('storage', handleStorageChange); return () => { window.removeEventListener('storage', handleStorageChange); }; }, []); return translations[lang]; }; export default useTranslate; ================================================ FILE: src/localization/vi.ts ================================================ import { Language } from './type'; const vietnamese: Language = { global: {}, status: { connecting: 'Đang kết nối ...', connected: 'Đã kết nối', connected_confirm: 'Đã kết nối', disconnecting: 'Đang ngắt kết nối ...', disconnected: 'Đã ngắt kết nối', ip_check: 'Đang kiểm tra IP ...', keep_trying: 'Vui lòng đợi một chút để thử lại...', preparing_rulesets: 'Đang chuẩn bị các bộ quy tắc...', downloading_rulesets_failed: 'Tải xuống các bộ quy tắc không thành công.' }, home: { title_warp_based: 'Dựa trên Warp', drawer_settings_warp: 'Cài đặt Warp', drawer_settings_routing_rules: 'Quy tắc định tuyến', drawer_settings_app: 'Cài đặt ứng dụng', drawer_settings_scanner: 'Cài đặt Máy quét', drawer_settings_network: 'Cài đặt mạng', drawer_log: 'Nhật ký ứng dụng', drawer_update: 'Cập nhật', drawer_update_label: 'Cập nhật mới', drawer_speed_test: 'Kiểm tra tốc độ', drawer_about: 'Giới thiệu về ứng dụng', drawer_lang: 'Thay đổi ngôn ngữ', drawer_singbox: 'Cài đặt Đường hầm' }, toast: { ip_check_please_wait: 'Vui lòng đợi vài giây để kiểm tra lại!', ir_location: 'Cloudflare đã kết nối với một IP có vị trí ở Iran, khác với IP thực của bạn. Bạn có thể sử dụng nó để vượt qua kiểm duyệt, nhưng không vượt qua được các lệnh trừng phạt. Đừng lo! Bạn có thể thay đổi vị trí trong cài đặt bằng cách sử dụng tùy chọn "Gool" hoặc "Psiphon".', btn_submit: 'Đã hiểu', copied: 'Đã sao chép!', cleared: 'Nhật ký đã được xóa!', please_wait: 'Vui lòng đợi ...', offline: 'Bạn đang ngoại tuyến!', settings_changed: 'Việc áp dụng cài đặt yêu cầu kết nối lại.', hardware_usage: 'Bật tùy chọn này sẽ tăng mức sử dụng tài nguyên phần cứng.', config_added: 'Cấu hình đã được thêm thành công, và để sử dụng nó, bạn phải nhấp vào kết nối.', profile_added: 'Điểm cuối đã được thêm thành công vào hồ sơ.', endpoint_added: 'Điểm cuối đã được thay thế thành công.', new_update_notification: 'Phiên bản mới đã có sẵn', new_update: 'Một phiên bản mới của ứng dụng có sẵn. Bạn có muốn tải xuống và chuẩn bị cài đặt không?', up_to_date: 'Bạn đang dùng phiên bản mới nhất của ứng dụng', exit_pending: 'Ứng dụng đang hoàn tất quá trình thoát; vui lòng đợi một chút trước khi khởi chạy lại.', help_btn: 'Trợ giúp' }, settings: { title: 'Cài đặt Warp', more: 'Thêm cài đặt', method_warp: 'Warp', method_warp_desc: 'Bật Warp', method_gool: 'Gool', method_gool_desc: 'Bật WarpInWarp', method_masque: 'Masque', method_masque_desc: 'Bật Masque', method_psiphon: 'Psiphon', method_psiphon_desc: 'Bật Psiphon', method_psiphon_location: 'Quốc gia', method_psiphon_location_auto: 'ngẫu nhiên', method_psiphon_location_desc: 'Chọn IP quốc gia mong muốn', endpoint: 'Điểm kết thúc', endpoint_desc: 'Kết hợp giữa địa chỉ IP hoặc tên miền, cùng với cổng', license: 'Giấy phép', license_desc: 'Sử dụng giấy phép tăng gấp đôi', option: 'Cài đặt ứng dụng', network: 'Cài đặt mạng', proxy_mode: 'Cấu hình', proxy_mode_desc: 'Định nghĩa cài đặt Proxy', port: 'Cổng Proxy', port_desc: 'Định nghĩa cổng proxy của ứng dụng', share_vpn: 'Địa chỉ liên kết', share_vpn_desc: 'Chia sẻ proxy qua mạng', dns: 'DNS', dns_desc: 'Chặn quảng cáo & nội dung người lớn', dns_error: 'Áp dụng cho các phương pháp Warp & Gool', ip_data: 'Kiểm tra IP', ip_data_desc: 'Hiển thị IP & vị trí sau khi kết nối', data_usage: 'Sử dụng dữ liệu', data_usage_desc: 'Hiển thị mức sử dụng dữ liệu & tốc độ thời gian thực', dark_mode: 'Chế độ tối', dark_mode_desc: 'Chọn chế độ hiển thị của ứng dụng', lang: 'Ngôn ngữ', lang_desc: 'Thay đổi ngôn ngữ giao diện ứng dụng', open_login: 'Mở khi đăng nhập', open_login_desc: 'Mở khi khởi động hệ thống', auto_connect: 'Kết nối tự động', auto_connect_desc: 'Kết nối khi mở ứng dụng', start_minimized: 'Khởi động thu nhỏ', start_minimized_desc: 'Thu nhỏ khi ứng dụng mở', system_tray: 'Khây hệ thống', system_tray_desc: 'Không đặt biểu tượng chương trình trong thanh tác vụ', force_close: 'Đóng bắt buộc', force_close_desc: 'Không ở lại trong khay hệ thống sau khi thoát', shortcut: 'Điều hướng', shortcut_desc: 'Phím tắt trên trang chủ', sound_effect: 'hiệu ứng âm thanh', sound_effect_desc: 'phát âm thanh khi kết nối thành công', restore: 'Khôi phục', restore_desc: 'Áp dụng cài đặt mặc định của ứng dụng', scanner: 'Cài đặt máy quét', scanner_alert: 'Máy quét được kích hoạt nếu bạn đang sử dụng địa chỉ điểm kết thúc mặc định.', scanner_ip_type: 'Loại điểm kết thúc', scanner_ip_type_auto: 'Tự động', scanner_ip_type_desc: 'Để tìm IP điểm kết thúc', scanner_rtt: 'Khoảng thời gian', scanner_rtt_default: 'Mặc định', scanner_rtt_desc: 'Giới hạn RTT của máy quét', scanner_reserved: 'Dành riêng', scanner_reserved_desc: 'Ghi đè giá trị dự trữ WireGuard', routing_rules: 'Danh sách đen', routing_rules_desc: 'Ngăn lưu lượng truy cập qua warp', routing_rules_disabled: 'Tắt', routing_rules_items: 'Các mục', profile: 'Hồ sơ', profile_desc: 'Điểm kết thúc được bạn lưu', singbox: 'Cài đặt Đường hầm', close_singbox: 'Dừng hoạt động', close_singbox_desc: 'Tự động đóng sing-box khi ngắt kết nối', close_helper: 'Dừng trợ lý', close_helper_desc: 'Tự động đóng trợ lý khi thoát', mtu: 'Giá trị MTU', mtu_desc: 'Đặt Đơn vị Truyền tối đa', geo_block: 'Chặn', geo_block_desc: 'Quảng cáo, Phần mềm độc hại, Lừa đảo & Đào tiền mã hóa', geo_rules_ip: 'Định tuyến IP', geo_rules_ip_desc: 'Áp dụng các quy tắc GeoIP', geo_rules_site: 'Định tuyến Web', geo_rules_site_desc: 'Áp dụng các quy tắc GeoSite', geo_nsfw_block: 'Bộ lọc nội dung', geo_nsfw_block_desc: 'Chặn trang web NSFW', more_helper: 'Đối tượng Phật', singbox_log: 'Ghi Log', singbox_log_desc: 'Cài đặt Mức Log', singbox_stack: 'Ngăn Xếp', singbox_stack_desc: 'Cài đặt Loại Ngăn Xếp', singbox_sniff: 'Nghe Gói', singbox_sniff_desc: 'Bật tính năng sniffing và ghi đè điểm đến', singbox_addressing: 'Định địa chỉ', singbox_addressing_desc: 'Đặt loại địa chỉ giao diện', singbox_udp_block: 'Chặn UDP', singbox_udp_block_desc: 'Chặn hoàn toàn tất cả lưu lượng UDP', singbox_discord_bypass: 'Discord', singbox_discord_bypass_desc: 'Vượt qua bộ lọc Discord', more_duties: 'Nhiệm vụ thêm', beta_release: 'Cập nhật Beta', beta_release_desc: 'Cập nhật thông tin về các phiên bản trước khi phát hành' }, tabs: { home: 'Kết nối', warp: 'Warp', network: 'Mạng', scanner: 'Máy quét', app: 'Ứng dụng', singbox: 'Đường hầm' }, modal: { endpoint_title: 'Điểm kết thúc', license_title: 'Giấy phép', license_desc: 'Chương trình không cần giấy phép Warp để chạy, nhưng nếu bạn muốn, bạn có thể nhập giấy phép của mình ở đây.', form_clear: 'Xóa', test_url_title: 'URL kiểm tra', test_url_desc: 'Địa chỉ kiểm tra kết nối', test_url_update: 'Nhận đề xuất', port_title: 'Cổng Proxy', restore_title: 'Khôi phục thay đổi', restore_desc: 'Xác nhận việc khôi phục sẽ đưa tất cả các cài đặt của chương trình trở về trạng thái mặc định và kết nối của bạn sẽ bị ngắt.', routing_rules_sample: 'Mẫu', routing_rules_alert_tun: 'Chỉ các quy tắc định tuyến cho tên miền, ip và ứng dụng sẽ ảnh hưởng đến cấu hình Tun.', routing_rules_alert_system: 'Ngoại trừ quy tắc định tuyến ứng dụng, các quy tắc khác sẽ ảnh hưởng đến cấu hình Proxy Hệ thống.', form_default: 'Mặc định', endpoint_suggested: 'Đề xuất', endpoint_latest: 'Mới nhất', endpoint_update: 'Nhận điểm kết thúc đề xuất', endpoint_paste: 'Dán điểm kết thúc đang hoạt động', profile_title: 'Hồ sơ', profile_name: 'Tiêu đề', profile_endpoint: 'Điểm kết thúc', profile_limitation: (value) => `Bạn có thể thêm tối đa ${value} điểm kết thúc.`, mtu_title: 'Giá trị MTU', mtu_desc: 'Đơn vị Truyền tối đa (MTU) đề cập đến kích thước tối đa của các gói dữ liệu, nên được đặt trong khoảng từ 1000 đến 9999.', custom_dns_title: 'DNS tùy chỉnh', confirm: 'Tôi xác nhận', update: 'Cập nhật', cancel: 'Hủy', yes: 'Có', no: 'Không' }, log: { title: 'Nhật ký ứng dụng', desc: 'Nếu nhật ký được tạo bởi chương trình, nó sẽ được hiển thị ở đây.', error_invalid_license: 'Giấy phép đã nhập không hợp lệ; Vui lòng xóa.', error_too_many_connected: 'Giới hạn sử dụng giấy phép đã đạt; Vui lòng xóa.', error_access_denied: 'Chạy chương trình với quyền quản trị viên.', error_failed_set_endpoint: 'Kiểm tra hoặc thay đổi giá trị điểm kết thúc, hoặc thử lại.', error_warp_identity: 'Lỗi xác thực trên Cloudflare!', error_script_failed: 'Chương trình gặp lỗi; Thử lại.', error_object_null: 'Chương trình gặp lỗi; Thử lại.', error_port_already_in_use: (value) => `Cổng ${value} đang được sử dụng bởi chương trình khác; Thay đổi cổng.`, error_port_socket: 'Sử dụng cổng khác.', error_port_restart: 'Cổng đang được sử dụng; khởi động lại ...', error_unknown_flag: 'Một lệnh không hợp lệ đã được thực thi trong nền.', error_deadline_exceeded: 'Hết thời gian kết nối; Thử lại.', error_configuration_encountered: 'Cấu hình proxy gặp lỗi!', error_desktop_not_supported: 'Môi trường máy tính để bàn không được hỗ trợ!', error_configuration_not_supported: 'Cấu hình proxy không được hỗ trợ trên hệ điều hành của bạn, nhưng bạn có thể sử dụng Warp Proxy thủ công.', error_configuring_proxy: (value) => `Lỗi cấu hình proxy cho ${value}!`, error_wp_not_found: 'Tệp warp-plus không được tìm thấy cùng với gói ứng dụng!', error_mp_not_found: 'Tệp masque-plus không nằm cùng với gói ứng dụng!', error_usque_not_found: 'Tệp usque không nằm cùng với gói ứng dụng!', error_wp_exclusions: 'Rất có thể tệp warp-plus đã bị cách ly do cảnh báo dương tính giả và phát hiện sai bởi phần mềm diệt virus, gây ra vấn đề với khả năng của chương trình để truy cập internet một cách tự do.\nChương trình có thể thêm tệp này vào danh sách loại trừ trong một số phần mềm diệt virus nếu được cấp quyền truy cập. Có nên thực hiện điều này không?', error_wp_stopped: 'Tệp warp-plus gặp vấn đề khi chạy!', error_connection_failed: 'Không thể kết nối với 1.1.1.1.', error_country_failed: 'Không thể kết nối với quốc gia đã chọn.', error_singbox_failed_stop: 'Không thể dừng Hộp hát!', error_singbox_failed_start: 'Không thể khởi động Hộp hát!', error_wp_reset_peer: 'Kết nối đến Cloudflare bị gián đoạn đột ngột!', error_failed_connection: 'Không thể thiết lập kết nối!', error_canceled_by_user: 'Hoạt động đã bị hủy bởi người dùng.', error_helper_not_found: 'Tệp trợ giúp không được tìm thấy bên cạnh gói ứng dụng!', error_singbox_ipv6_address: 'Hệ điều hành của bạn không hỗ trợ IPv6. Vui lòng vào cài đặt đường hầm và đổi địa chỉ thành IPv4.', error_local_date: 'Hãy đảm bảo rằng ngày và giờ của hệ thống của bạn được thiết lập chính xác!' }, about: { title: 'Giới thiệu về ứng dụng', desc: 'Chương trình này là một phiên bản không chính thức nhưng đáng tin cậy của ứng dụng Oblivion dành cho Windows, Linux và Mac.\nChương trình Oblivion Desktop được mô phỏng theo giao diện người dùng của phiên bản gốc do Yousef Ghobadi phát triển. Nó được chuẩn bị với mục đích truy cập miễn phí vào Internet và việc đổi tên hoặc sử dụng thương mại là không được phép.', slogan: 'Internet, dành cho tất cả hoặc không ai!' }, systemTray: { connect: 'Kết nối', connecting: 'Đang kết nối ...', connected: 'Đã kết nối', disconnecting: 'Đang ngắt kết nối ...', settings: 'Cài đặt', settings_warp: 'Warp', settings_network: 'Mạng', settings_scanner: 'Máy quét', settings_app: 'Ứng dụng', about: 'Giới thiệu', log: 'Nhật ký', speed_test: 'Kiểm tra tốc độ', exit: 'Thoát' }, update: { available: 'Cập nhật có sẵn', available_message: (value) => `Có một phiên bản mới của ${value} sẵn sàng. Bạn có muốn cập nhật ngay không?`, ready: 'Cập nhật sẵn sàng', ready_message: (value) => `Phiên bản mới của ${value} đã sẵn sàng. Nó sẽ được cài đặt sau khi khởi động lại. Bạn có muốn khởi động lại ngay không?` }, speedTest: { title: 'Kiểm tra tốc độ', initializing: 'Đang khởi tạo kiểm tra tốc độ ...', click_start: 'Nhấp vào nút để bắt đầu kiểm tra tốc độ', error_msg: 'Đã xảy ra lỗi trong quá trình kiểm tra tốc độ. Vui lòng thử lại.', server_unavailable: 'Máy chủ kiểm tra tốc độ không khả dụng', download_speed: 'Tốc độ tải xuống', upload_speed: 'Tốc độ tải lên', latency: 'Độ trễ', jitter: 'Độ dao động' } }; export default vietnamese; ================================================ FILE: src/main/config.ts ================================================ export const wpVersion = '1.2.6'; export const helperVersion = '1.3.2'; export const netStatsVersion = '1.0.2'; export const proxyResetVersion = '1.2.14'; export const mpVersion = '2.9.0'; ================================================ FILE: src/main/dxConfig.ts ================================================ // for the sake of better developer experience 🧑‍💻 // ! don't commit this file if you change it(git update-index --skip-worktree dxConfig.ts) export const useCustomWindowXY = false; // to open window on right side of the screen export const openDevToolsByDefault = false; export const openDevToolsInFullScreen = false; export const showWpLogs = true; ================================================ FILE: src/main/ipc.ts ================================================ // https://www.electronjs.org/docs/latest/tutorial/ipc import { ipcMain } from 'electron'; import './lib/wpManager'; import './ipcListeners/log'; import './ipcListeners/settings'; ipcMain.once('ipc-example', async (event, arg) => { event.reply('ipc-example', 'pong', arg); }); ================================================ FILE: src/main/ipcListeners/log.ts ================================================ import fs from 'fs'; import os from 'os'; import { osInfo } from 'systeminformation'; import { app, ipcMain } from 'electron'; import log from 'electron-log'; import settings from 'electron-settings'; import { calculateMethod, checkDataUsage, checkEndpoint, checkProxyMode, checkRoutingRules, doesFileExist, checkReserved, checkGeoStatus, checkIpType, checkTunAddrType, checkTestUrl, checkDNS } from '../lib/utils'; import packageJsonData from '../../../package.json'; import { binAssetPath, logPath } from '../../constants'; import { wpVersion, helperVersion, mpVersion } from '../config'; export function readLogFile(value: string) { return new Promise((resolve, reject) => { fs.readFile(value, 'utf8', (err, data) => { if (err) { reject(err); } else { resolve(data); } }); }); } export const getOsInfo = async () => { let getOsInfoLog = ''; await osInfo() .then((data) => { getOsInfoLog += (data.distro ? data.distro : process.platform) + ' '; getOsInfoLog += '(' + (data.release ? data.release : os.release()) + ') '; getOsInfoLog += (data.arch ? data.arch : process.arch) + ' '; getOsInfoLog += data.build ? data.build : ''; }) .catch((err) => { log.error(err); getOsInfoLog = process.platform + ' ' + os.release() + ' ' + process.arch; }); return getOsInfoLog; }; export const logMetadata = (osInfoVal: string) => { const method = settings.get('method'); const proxyMode = settings.get('proxyMode'); const license = settings.get('license'); const endpoint = settings.get('endpoint'); const routingRules = settings.get('routingRules'); const asn = settings.get('asn'); const dataUsage = settings.get('dataUsage'); const reserved = settings.get('reserved'); const singBoxGeoIp = settings.get('singBoxGeoIp'); const singBoxGeoSite = settings.get('singBoxGeoSite'); const singBoxGeoBlock = settings.get('singBoxGeoBlock'); const singBoxGeoNSFW = settings.get('singBoxGeoNSFW'); const singBoxAddrType = settings.get('singBoxAddrType'); const ipType = settings.get('ipType'); const testUrl = settings.get('testUrl'); const dns = settings.get('dns'); Promise.all([ method, proxyMode, license, endpoint, routingRules, asn, dataUsage, reserved, singBoxGeoIp, singBoxGeoSite, singBoxGeoBlock, singBoxGeoNSFW, singBoxAddrType, ipType, testUrl, dns ]) .then((data) => { log.info('------------------------MetaData------------------------'); log.info(`running on: ${osInfoVal}`); log.info(`at od: v${packageJsonData.version}`); log.info(`at wp: v${wpVersion}`); log.info(`at hp: v${helperVersion}`); log.info(`at mp: v${mpVersion}`); log.info(`ls assets/bin: ${fs.readdirSync(binAssetPath)}`); log.info('method:', calculateMethod(data[0])); log.info('proxyMode:', checkProxyMode(data[1])); log.info('routingRules:', checkRoutingRules(data[4])); log.info('endpoint:', checkEndpoint(data[3])); log.info('ipType:', checkIpType(data[13], data[3])); log.info('tunAddrType:', checkTunAddrType(data[12])); log.info('dataUsage:', checkDataUsage(data[6])); log.info('asn:', data[5] ? data[5] : 'UNK'); //log.info('license:', hasLicense(data[2])); log.info('reserved:', checkReserved(data[7])); log.info('geo', checkGeoStatus(data[8], data[9], data[10], data[11])); log.info('testUrl:', checkTestUrl(data[14])); log.info('dns:', checkDNS(data[15])); log.info(`exe: ${app.getPath('exe')}`); log.info(`userData: ${app.getPath('userData')}`); log.info(`logs: ${app.getPath('logs')}`); log.info('------------------------MetaData------------------------'); // TODO add package type(exe/dev/rpm/dmg/zip/etc...) if possible }) .catch((err) => { log.error(err); }); }; const parseLogDate = (logLine: string) => { const dateRegex = /(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2})|(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2})/; const match = logLine.match(dateRegex); if (match) { if (match[1]) { return new Date(match[1]); } else if (match[2]) { return new Date(match[2]); } } return new Date(0); }; ipcMain.on('get-logs', async (event) => { const wpLogPathExist = await doesFileExist(logPath); let wpLogs = ''; if (wpLogPathExist) { wpLogs = String(await readLogFile(logPath)); } const wpLogLines = wpLogs.split('\n'); const allLogLines = [...wpLogLines] .filter((line) => line.trim() !== '') .sort((a, b) => { const dateA = parseLogDate(a); const dateB = parseLogDate(b); return dateA.getTime() - dateB.getTime(); }); const mergedLogs = allLogLines.join('\n'); event.reply('get-logs', mergedLogs); }); ================================================ FILE: src/main/ipcListeners/settings.ts ================================================ import { ipcMain } from 'electron'; import settings from 'electron-settings'; ipcMain.handle('settings', (event, mode, arg) => { if (mode === 'get') { const res = settings.getSync(arg.key); return res; } else if (mode === 'getAll') { const res = settings.getSync(); return res; } else if (mode === 'set') { settings.setSync(arg.key, arg.value); return { key: arg.key, value: arg.value }; } }); ================================================ FILE: src/main/lib/customEvent.ts ================================================ import EventEmitter from 'events'; export const customEvent = new EventEmitter(); /* EXAMPLE: 👇 */ // listen for an event customEvent.on('greet', () => { // do something }); // trigger an event customEvent.emit('greet'); // ! also consider using ipcMain.emit('') if it suits your need. ================================================ FILE: src/main/lib/netStatsManager.ts ================================================ import { ipcMain } from 'electron'; import log from 'electron-log'; import { ChildProcess, spawn } from 'child_process'; import { networkInterfaceDefault } from 'systeminformation'; import { netStatsPath, workingDirPath } from '../../constants'; class NetStatsManager { private statsProcess?: ChildProcess | null; private isMonitoring: boolean = false; private networkInterface: string = ''; private retryCount = 0; private static readonly MAX_RETRIES = 3; private shouldRestart: boolean = false; constructor(private readonly spawnArgs: string[] = ['-t', '1', '-p', '2', '-f', 'json']) { this.initializeIpcEvents(); this.initializeNetworkInterface().catch((err) => log.error('Failed to initialize network interface:', err) ); } public initializeIpcEvents(): void { ipcMain.on('net-stats', (event, shouldStart) => { if (shouldStart && !this.isMonitoring) { this.isMonitoring = true; this.startMonitoring(event); } else if (!shouldStart && this.isMonitoring) { this.isMonitoring = false; this.stopMonitoring(); } }); } private async initializeNetworkInterface(): Promise { this.networkInterface = await networkInterfaceDefault(); } public startMonitoring(event: any): void { if (!this.statsProcess) { this.shouldRestart = true; log.info('Starting netStats...'); this.statsProcess = spawn( netStatsPath, ['-i', this.networkInterface, ...this.spawnArgs], { cwd: workingDirPath } ); this.statsProcess.stdout?.on('data', (data: Buffer) => { const output = data.toString(); try { if (output.startsWith('{"interface":')) { this.isMonitoring = true; this.retryCount = 0; const stats = JSON.parse(output); event.reply('net-stats', stats); } else { log.info('netStats output:', output); } } catch (err) { log.error('Failed to parse netStats output:', err); } }); this.statsProcess.stderr?.on('data', (data: Buffer) => { log.error('netStats error:', data.toString()); }); this.statsProcess.on('close', async (code) => { log.info(`netStats exited with code ${code}`); this.statsProcess = null; this.isMonitoring = false; if (this.shouldRestart && this.retryCount < NetStatsManager.MAX_RETRIES) { log.info('netStats exited unexpectedly. Restarting...'); this.retryCount++; await this.initializeNetworkInterface(); this.startMonitoring(event); } }); } } public stopMonitoring(): void { if (this.statsProcess) { this.shouldRestart = false; log.info('Stopping netStats...'); this.statsProcess.kill(); } } } export default NetStatsManager; ================================================ FILE: src/main/lib/pacScript.ts ================================================ // proxy setup script import handler from 'serve-handler'; import http from 'http'; import { app } from 'electron'; import { detect } from 'detect-port'; import path from 'path'; import log from 'electron-log'; import settings from 'electron-settings'; import { promises as fsPromises } from 'fs'; import { doesDirectoryExist } from './utils'; export const createPacScript = async (hostIp: string, port: string | number) => { log.info('generating pac script...'); const binPath = path.join(app?.getPath('userData'), 'pac'); const isBinDirExist = await doesDirectoryExist(binPath); if (!isBinDirExist) { await fsPromises.mkdir(binPath, { recursive: true }); } const routingRules = await settings.get('routingRules'); //console.log(routingRules); let domainRules = {}; if (typeof routingRules === 'string' && routingRules !== '') { domainRules = routingRules .replace(/\n|
/g, '') .replace(/app:[^,]+(,|$)/g, '') .split(',') .filter((rule) => rule.trim() !== '') .map((rule) => { const parts = rule.split(':'); const isException = parts[1].startsWith('!'); const value = isException ? parts[1].substring(1) : parts[1]; return { type: parts[0], value, regex: value.startsWith('*'), forceProxy: isException }; }); } await fsPromises.writeFile( path.join(app.getPath('userData'), 'pac', 'proxy.txt'), ` var FindProxyForURL = function(init, profiles) { return function(url, host) { "use strict"; var result = init, scheme = url.substr(0, url.indexOf(":")); do { result = profiles[result]; if (typeof result === "function") result = result(url, host, scheme); } while (typeof result !== "string" || result.charCodeAt(0) === 43); return result; }; }("+proxy", { "+proxy": function(url, host, scheme) { "use strict"; if (Object.keys(${JSON.stringify(domainRules)}).length > 0) { for (const rule of ${JSON.stringify(domainRules)}) { if (rule.forceProxy && rule.type === "domain" && rule.value === host) { return "SOCKS5 ${hostIp}:${port}; SOCKS ${hostIp}:${port}"; } if (rule.type === "domain" && rule.value === host) { return "DIRECT"; } if (rule.type === "ip" && rule.value === host) { return "DIRECT"; } if ((rule.type === "domain" || rule.type === "range") && rule.regex && new RegExp(rule.value.replace("*", ".*") + "$").test(host)) { return "DIRECT"; } } } if (/^127.0.0.1$/.test(host) || /^::1$/.test(host) || /^localhost$/.test(host)) { return "DIRECT"; } return "SOCKS5 ${hostIp}:${port}; SOCKS ${hostIp}:${port}"; } });` ); log.info('pac script generated.'); }; let server: http.Server; export const servePacScript = (port = 8087) => { return new Promise((resolve, reject) => { detect(port) .then((realPort) => { if (port === realPort) { const pacPath = path.join(app.getPath('userData'), 'pac'); server = http.createServer((request, response) => { return handler(request, response, { public: pacPath }); }); server.listen({ port }, () => { log.info(`serving pac script file at http://127.0.0.1:${port}`); resolve(`http://127.0.0.1:${port}`); }); } else { log.info(`port: ${port} was occupied, trying port: ${realPort}`); servePacScript(realPort); } }) .catch((err) => { log.error(err); reject(); }); }); }; export const killPacScriptServer = async () => { try { if (server) { server.close(); log.info('pac script server closed.'); } } catch (error) { log.error(error); } }; ================================================ FILE: src/main/lib/proxy.ts ================================================ import settings from 'electron-settings'; import { IpcMainEvent } from 'electron'; import log from 'electron-log'; import regeditModule, { RegistryPutItem, promisified as regedit } from 'regedit'; import { exec, spawn } from 'child_process'; import { promisify } from 'util'; import { defaultSettings } from '../../defaultSettings'; import { isSocksProxy, shouldProxySystem } from './utils'; import { createPacScript, killPacScriptServer, servePacScript } from './pacScript'; //import { getTranslateElectron } from '../../localization/electron'; import { getTranslate } from '../../localization'; import { withDefault } from '../../renderer/lib/withDefault'; import { isWindows, proxyResetPath, regeditVbsDirPath } from '../../constants'; import path from 'path'; const execPromise = promisify(exec); let oldProxyHost = ''; let oldProxyPort = ''; let appLang = getTranslate('en'); const DEFAULT_ROUTING_RULES = 'localhost,127.*,10.*,172.16.*,172.17.*,172.18.*,172.19.*,172.20.*,172.21.*,172.22.*,172.23.*,172.24.*,172.25.*,172.26.*,172.27.*,172.28.*,172.29.*,172.30.*,172.31.*,192.168.*,'; const REGEDIT_PATH = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'; const setRoutingRules = (value: any) => { if (typeof value !== 'string' || value === '') return DEFAULT_ROUTING_RULES; return ( DEFAULT_ROUTING_RULES + ',' + value .replace(/app:[^,]+/g, '') .replace(/(domain|geoip|ip|range):/g, '') .replace(/\n|
/g, '') .trim() ); }; // TODO reset to prev proxy settings on disable // TODO refactor (move each os functions to it's own file) async function registerStartupProxyReset(): Promise { if (!isWindows) return; const appPath = `"${proxyResetPath}"`; const registryPath = `HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run`; const valueName = 'OblivionProxyReset'; try { regeditModule.setExternalVBSLocation(regeditVbsDirPath); const result = await regedit.list([registryPath]); const existingValue = result[registryPath]?.values?.[valueName]?.value; if (existingValue === appPath) { //log.info('Proxy reset script already registered in startup.'); return; } const values: RegistryPutItem = { [valueName]: { value: appPath, type: 'REG_SZ' } }; await regedit.putValue({ [registryPath]: values }); log.info('Proxy reset script registered in startup.'); } catch (err) { console.error('Failed to register proxy reset:', err); } } async function removeStartupProxyReset(): Promise { if (!isWindows) return; const registryPath = `HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run`; const valueName = 'OblivionProxyReset'; try { regeditModule.setExternalVBSLocation(regeditVbsDirPath); const result = await regedit.list([registryPath]); const existingValue = result[registryPath]?.values?.[valueName]?.value; if (existingValue === undefined) { return; } await (regedit as any).deleteValue([path.join(registryPath, valueName)]); log.info('Proxy reset script removed.'); } catch (err) { console.error('Failed to remove proxy reset:', err); } } // tweaking windows proxy settings using regedit const windowsProxySettings = (config: RegistryPutItem, regeditVbsDirPath: string) => { regeditModule.setExternalVBSLocation(regeditVbsDirPath); return regedit.putValue({ [REGEDIT_PATH]: config }); }; const isGnome = (): boolean => { try { exec('gsettings --version'); log.info(`gsettings found!`); return true; } catch (error) { return false; } }; const isKDE = async () => { const checkKwriteConfig = async (v = '5') => { return new Promise((resolve) => { try { exec(`kwriteconfig${v} --help`, (err) => { if (!err) { log.info(`kwriteconfig${v} found!`); resolve(v); } else { resolve(false); } }); } catch (error) { resolve(false); } }); }; return new Promise(async (resolve) => { const isPlasma5 = await checkKwriteConfig('5'); const isPlasma6 = await checkKwriteConfig('6'); if (typeof isPlasma5 === 'string' && isPlasma5 === '5') resolve(isPlasma5); else if (typeof isPlasma6 === 'string' && isPlasma6 === '6') resolve(isPlasma6); else resolve(false); }); }; // TODO refactor const enableGnomeProxy = async (ip: string, port: string, routingRules: any): Promise => { const proxySettings = { mode: 'manual', socks: `socks5://${ip}:${port}`, host: ip, port: port }; exec(`gsettings get org.gnome.system.proxy.socks host`, (err, stdout) => { oldProxyHost = stdout; //log.info(`Gnome old proxy host : ` + stdout); }); exec(`gsettings get org.gnome.system.proxy.socks port`, (err, stdout) => { oldProxyPort = stdout; //log.info(`Gnome old proxy port : ` + stdout); }); try { await execPromise(`gsettings set org.gnome.system.proxy mode '${proxySettings.mode}'`); // reset other proxies in case user set it await execPromise(`gsettings set org.gnome.system.proxy.http host ""`); await execPromise(`gsettings set org.gnome.system.proxy.http port 0`); await execPromise(`gsettings set org.gnome.system.proxy.https host ""`); await execPromise(`gsettings set org.gnome.system.proxy.https port 0`); await execPromise(`gsettings set org.gnome.system.proxy.ftp host ""`); await execPromise(`gsettings set org.gnome.system.proxy.ftp port 0`); // set socks proxy await execPromise(`gsettings set org.gnome.system.proxy.socks host ${proxySettings.host}`); await execPromise(`gsettings set org.gnome.system.proxy.socks port ${proxySettings.port}`); // https://wiki.archlinux.org/title/Proxy_server#Proxy_settings_on_GNOME3 const normalizeRoutingRules = (rules: any) => { let str = `"[`; const arrRules = String(rules).split(','); arrRules.forEach((r, index) => { if (index === arrRules.length - 1) { str += `'${r}'`; } else { str += `'${r}', `; } }); str += `]"`; return str; }; const normalizedRoutingRules = normalizeRoutingRules(routingRules); await execPromise( `gsettings set org.gnome.system.proxy ignore-hosts ${normalizedRoutingRules}` ); } catch (err) { log.error(`Error setting proxy: ${err}`); throw err; } }; const disableGNOMEProxy = async (): Promise => { try { await execPromise(`gsettings set org.gnome.system.proxy mode 'none'`); await execPromise(`gsettings set org.gnome.system.proxy.socks host ${oldProxyHost}`); await execPromise(`gsettings set org.gnome.system.proxy.socks port ${oldProxyPort}`); log.info('Proxy settings disabled for GNOME'); } catch (err) { log.error(`Error disabling proxy settings for GNOME: ${err}`); throw err; } }; // TODO refactor const enableKDEProxy = async ( host: string, port: string, routingRules: any, v = '5' ): Promise => { try { await execPromise( `kwriteconfig${v} --file kioslaverc --group "Proxy Settings" --key ProxyType 1` ); // reset other proxies in case user set it await execPromise( `kwriteconfig${v} --file kioslaverc --group "Proxy Settings" --key httpProxy '':0` ); await execPromise( `kwriteconfig${v} --file kioslaverc --group "Proxy Settings" --key httpsProxy '':0` ); await execPromise( `kwriteconfig${v} --file kioslaverc --group "Proxy Settings" --key ftpProxy '':0` ); // set socks proxy await execPromise( `kwriteconfig${v} --file kioslaverc --group "Proxy Settings" --key socksProxy "${host}:${port}"` ); await execPromise( `kwriteconfig${v} --file kioslaverc --group "Proxy Settings" --key Authmode 0` ); await execPromise( `kwriteconfig${v} --file kioslaverc --group "Proxy Settings" --key NoProxyFor '${routingRules}'` ); } catch (err) { log.error(`Error setting SOCKS proxy for KDE: ${err}`); throw err; } }; const disableKDEProxy = async (v = '5'): Promise => { try { await execPromise( `kwriteconfig${v} --file kioslaverc --group "Proxy Settings" --key ProxyType 0` ); log.info('Proxy settings disabled for KDE'); return 'Proxy disabled for KDE'; } catch (error) { log.error(`Error disabling SOCKS proxy for KDE: ${error}`); throw error; } }; // https://github.com/SagerNet/sing-box/blob/dev-next/common/settings/proxy_darwin.go const macOSNetworkSetup = (args: string[]) => { const child = spawn('networksetup', args); return new Promise((resolve, reject) => { let output = ''; child.stdout.on('data', async (data) => { const strData = data.toString(); output += strData; }); child.on('exit', () => { resolve(output); }); child.stderr.on('data', (err) => { log.error(`Error: ${err.toString()}`); reject(err); }); child.on('error', (err) => { log.error(`Spawn Error: ${err}`); reject(err); }); }); }; const getMacOSActiveNetworkHardwarePorts = async ( requireIP: boolean = false ): Promise => { console.log('getMacOSActiveNetworkHardwarePorts'); try { const { stdout } = await execPromise('networksetup -listallnetworkservices'); log.info('networksetup -listallnetworkservices:', stdout); const lines = stdout.trim().split('\n'); const hardwarePorts: string[] = []; await Promise.all( lines.map(async (line) => { if ( line === '' || line.startsWith('*') || line === 'An asterisk (*) denotes that a network service is disabled.' ) return; if (requireIP) { const { stdout: serviceContent } = await execPromise( `networksetup -getinfo "${line}"` ); const ipAddressRegex = /IP address:\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/; const ipv6AddressRegex = /IPv6 IP address:\s*([a-fA-F0-9:]+)/; const hasValidIpAddress = ipAddressRegex.test(serviceContent); const hasValidIpv6Address = ipv6AddressRegex.test(serviceContent); if (hasValidIpAddress || hasValidIpv6Address) { hardwarePorts.push(line); } } else { hardwarePorts.push(line); } }) ); if (hardwarePorts.length === 0) throw new Error('No active network ports found'); return hardwarePorts; } catch (error) { log.error(`Error getting active network hardware ports: ${error}`); throw error; } }; export const checkProxyState = async (): Promise => { try { if (process.platform === 'win32') { const { stdout } = await execPromise(`reg query "${REGEDIT_PATH}" /v ProxyEnable`); return stdout.includes('0x1'); } else if (process.platform === 'darwin') { const hardwarePorts: string[] = await getMacOSActiveNetworkHardwarePorts(); let isSet = true; await Promise.all( hardwarePorts.map(async (hardwarePort) => { const { stdout } = await execPromise( `networksetup -getsocksfirewallproxy '${hardwarePort}'` ); log.info(`networksetup -getsocksfirewallproxy ${hardwarePort}:`, stdout); const hostIP = await settings.get('hostIP'); const port = await settings.get('port'); if ( stdout.includes(`Server: ${hostIP}`) && stdout.includes(`Port: ${port}`) && stdout.includes('Enabled: Yes') ) { log.info('Proxy is enabled for ', hardwarePort); } else { log.info('Proxy is not enabled for ', hardwarePort); isSet = false; } }) ); return isSet; } else if (process.platform === 'linux') { if (isGnome()) { const { stdout: gnomeStdout } = await execPromise( 'gsettings get org.gnome.system.proxy mode' ); return gnomeStdout.trim() !== "'none'"; } const plasmaVersion = await isKDE(); if (typeof plasmaVersion === 'string') { const { stdout: plasmaStdout } = await execPromise( 'kwriteconfig5 --file kioslaverc --group "Proxy Settings" --key "socksProxy"' ); return plasmaStdout.trim() !== ''; } log.error('Desktop Environment not supported.'); return false; } else { log.error('system proxy is not supported on your platform yet...'); return false; } } catch (error) { log.error(`Error checking proxy state: ${error}`); return false; } }; export const enableProxy = async (regeditVbsDirPath: string, ipcEvent?: IpcMainEvent) => { const proxyMode = await settings.get('proxyMode'); //const psiphon = (await settings.get('psiphon')) || defaultSettings.psiphon; const method = (await settings.get('method')) || defaultSettings.method; //const proxyMode = (await settings.get('proxyMode')) || defaultSettings.proxyMode; const hostIP = (await settings.get('hostIP')) || defaultSettings.hostIP; const port = (await settings.get('port')) || defaultSettings.port; const routingRules = await settings.get('routingRules'); const lang = await settings.get('lang'); appLang = getTranslate(String(withDefault(lang, defaultSettings.lang))); const isSocks = isSocksProxy(String(method)); if (!shouldProxySystem(proxyMode)) { log.info('skipping set system proxy'); return; } log.info('trying to set system proxy...'); if (process.platform === 'win32') { return new Promise(async (resolve, reject) => { try { let pacServeUrl = ''; if (isSocks) { await createPacScript(String(hostIP), String(port)); pacServeUrl = await servePacScript(Number(port) + 1); log.info('PAC server URL:', pacServeUrl); } await registerStartupProxyReset(); await windowsProxySettings( { ProxyServer: { type: 'REG_SZ', value: `${isSocks ? 'socks=' : ''}${hostIP.toString()}:${port.toString()}` }, ProxyOverride: { type: 'REG_SZ', value: String(setRoutingRules(routingRules)) }, AutoConfigURL: { type: 'REG_SZ', value: `${isSocks ? pacServeUrl + '/proxy.txt' : ''}` }, ProxyEnable: { type: 'REG_DWORD', value: 1 } }, regeditVbsDirPath ); log.info('system proxy has been set.'); resolve(); } catch (error) { log.error(`error while trying to set system proxy: , ${error}`); reject(error); ipcEvent?.reply('guide-toast', appLang.log.error_configuration_encountered); } }); } else if (process.platform === 'darwin') { return new Promise(async (resolve) => { const hardwarePorts: string[] = (await getMacOSActiveNetworkHardwarePorts()) as string[]; log.info('using hardwarePort:', hardwarePorts); hardwarePorts.forEach(async (hardwarePort) => { log.info('using hardwarePort:', hardwarePort); try { await macOSNetworkSetup([ '-setsocksfirewallproxy', hardwarePort, hostIP.toString(), port.toString() ]); await macOSNetworkSetup([ '-setproxybypassdomains', hardwarePort, String(setRoutingRules(routingRules)) ]); await macOSNetworkSetup(['-setsocksfirewallproxystate', hardwarePort, 'on']); log.info(`System proxy has been set for ${hardwarePort}.`); } catch (error) { log.error( `Error while trying to set system proxy for ${hardwarePort}: ${error}` ); ipcEvent?.reply( 'guide-toast', appLang.log.error_configuring_proxy(hardwarePort) ); } }); resolve(); }); } else if (process.platform === 'linux') { let notSupported = true; let shouldResolve = false; return new Promise(async (resolve, reject) => { if (isGnome()) { await enableGnomeProxy( hostIP.toString(), port.toString(), setRoutingRules(routingRules) ) .then(() => { notSupported = false; log.info('Enabled proxy for GNOME.'); shouldResolve = true; }) .catch(() => { log.error('Failed to enable proxy for GNOME'); ipcEvent?.reply('guide-toast', appLang.log.error_configuration_encountered); }); } const plasmaVersion = await isKDE(); if (typeof plasmaVersion === 'string') { await enableKDEProxy( hostIP.toString(), port.toString(), setRoutingRules(routingRules), plasmaVersion ) .then(() => { notSupported = false; log.info('Enabled proxy for KDE.'); shouldResolve = true; }) .catch(() => { log.error('Failed to enable proxy for KDE'); ipcEvent?.reply('guide-toast', appLang.log.error_configuration_encountered); reject(); }); } if (shouldResolve) { resolve(); } else { reject(); } if (notSupported) { log.error('Desktop Environment not supported.'); ipcEvent?.reply('guide-toast', appLang.log.error_desktop_not_supported); reject(); } }); } else { return new Promise((resolve) => { log.error('system proxy is not supported on your platform.'); ipcEvent?.reply('guide-toast', appLang.log.error_configuration_not_supported); resolve(); }); } }; export const disableProxy = async (regeditVbsDirPath: string, ipcEvent?: IpcMainEvent) => { const proxyMode = await settings.get('proxyMode'); const method = (await settings.get('method')) || defaultSettings.method; const lang = await settings.get('lang'); appLang = getTranslate(String(withDefault(lang, defaultSettings.lang))); const isSocks = isSocksProxy(String(method)); if (proxyMode === 'none') { log.info('skipping system proxy disable.'); return; } log.info('trying to disable system proxy...'); if (process.platform === 'win32') { return new Promise(async (resolve, reject) => { if (isSocks) killPacScriptServer(); try { await windowsProxySettings( { ProxyServer: { type: 'REG_SZ', value: '' }, ProxyOverride: { type: 'REG_SZ', value: '' }, AutoConfigURL: { type: 'REG_SZ', value: '' }, ProxyEnable: { type: 'REG_DWORD', value: 0 } }, regeditVbsDirPath ); await removeStartupProxyReset(); log.info('system proxy has been disabled on your system.'); resolve(); } catch (error) { log.error(`error while trying to disable system proxy: , ${error}`); reject(error); ipcEvent?.reply('guide-toast', appLang.log.error_configuration_encountered); } }); } else if (process.platform === 'darwin') { return new Promise(async (resolve, reject) => { const hardwarePorts: string[] = (await getMacOSActiveNetworkHardwarePorts()) as string[]; log.info('using hardwarePort:', hardwarePorts); hardwarePorts.forEach(async (hardwarePort) => { log.info('using hardwarePort:', hardwarePort); try { await macOSNetworkSetup(['-setsocksfirewallproxy', hardwarePort, '']); await macOSNetworkSetup(['-setsocksfirewallproxystate', hardwarePort, 'off']); log.info('system proxy has been disabled on your system.'); resolve(); } catch (error) { log.error(`error while trying to disable system proxy: , ${error}`); reject(error); ipcEvent?.reply('guide-toast', appLang.log.error_configuration_encountered); } }); resolve(); }); } else if (process.platform === 'linux') { let notSupported = true; let shouldResolve = false; return new Promise(async (resolve, reject) => { if (isGnome()) { await disableGNOMEProxy() .then(() => { notSupported = false; log.info('Disabled proxy for GNOME.'); shouldResolve = true; }) .catch(() => { log.error('Failed to disabled proxy for GNOME'); shouldResolve = false; }); } const plasmaVersion = await isKDE(); if (typeof plasmaVersion === 'string') { await disableKDEProxy(plasmaVersion) .then(() => { notSupported = false; log.info('Disabled proxy for KDE.'); shouldResolve = true; }) .catch(() => { log.error('Failed to disabled proxy for KDE'); shouldResolve = false; }); } if (shouldResolve) resolve(); if (notSupported) { log.error('Desktop Environment not supported.'); ipcEvent?.reply('guide-toast', appLang.log.error_desktop_not_supported); reject(); } }); } else { return new Promise((resolve) => { log.error('Unsupported platform for disabling proxy.'); resolve(); }); } }; ================================================ FILE: src/main/lib/sbConfig.ts ================================================ import fs from 'fs'; import log from 'electron-log'; import path from 'path'; import { sbConfigPath, sbLogPath, sbCachePath, ruleSetDirPath, IConfig, IGeoConfig, IRoutingRules, defaultWarpIPs, isLinux, isWindows, isDarwin } from '../../constants'; import { defaultSettings } from '../../defaultSettings'; import { formatEndpointForConfig, isIpBasedDoH } from './utils'; export function createSbConfig(config: IConfig, geoConfig: IGeoConfig, rulesConfig: IRoutingRules) { const domainSetDirect = rulesConfig.domainSet.filter((d) => !d.startsWith('!')); const domainSetException = rulesConfig.domainSet .filter((d) => d.startsWith('!')) .map((d) => d.slice(1)); const logConfig = config.logLevel === 'disabled' ? { disabled: true } : { level: config.logLevel, timestamp: true, output: sbLogPath }; const DoHDns = new URL(config.DoHDns); const configuration = { log: logConfig, dns: { independent_cache: true, strategy: 'prefer_ipv4', final: 'dns-remote', servers: [ { tag: 'dns-remote', type: 'https', server: DoHDns.hostname, server_port: DoHDns.port === '' ? 443 : DoHDns.port, path: DoHDns.pathname, detour: 'proxy', ...(!isIpBasedDoH(config.DoHDns) && { domain_resolver: 'dns-cf' }) }, { tag: 'dns-direct', ...(config.plainDns === '' ? { type: 'local' } : { type: 'udp', server: config.plainDns }), detour: 'direct' }, ...(!isIpBasedDoH(config.DoHDns) ? [ { tag: 'dns-cf', type: 'udp', server: '1.1.1.1', detour: 'proxy' } ] : []), { tag: 'dns-hosts', type: 'hosts' } ], rules: [ ...(domainSetException.length > 0 ? [ { action: 'route', domain: domainSetException, server: 'dns-remote' } ] : []), { server: 'dns-hosts', ip_accept_any: true }, ...(domainSetDirect.length > 0 ? [ { action: 'route', domain: domainSetDirect, server: 'dns-direct' } ] : []), ...(rulesConfig.domainSuffixSet.length > 0 ? [ { action: 'route', domain_suffix: rulesConfig.domainSuffixSet, server: 'dns-direct' } ] : []) ] }, inbounds: [ { type: 'tun', tag: 'tun-in', mtu: config.tunMtu, address: config.tunAddr, auto_route: true, strict_route: true, stack: config.tunStack, endpoint_independent_nat: true, sniff: config.tunSniff, sniff_override_destination: config.tunSniff, route_exclude_address: config.tunEndpoint === defaultSettings.endpoint ? defaultWarpIPs : [formatEndpointForConfig(config.tunEndpoint)] } ], outbounds: [ { type: 'socks', tag: 'proxy', server: config.socksIp, server_port: config.socksPort, version: '5' }, { type: 'direct', tag: 'direct', domain_resolver: 'dns-direct' } ], route: { rules: [ { action: 'sniff' }, { protocol: 'dns', action: 'hijack-dns' }, { action: 'resolve' }, { action: 'route', ip_is_private: true, outbound: 'direct' }, ...(config.socksIp ? [ { action: 'route', ip_cidr: [ config.socksIp === '0.0.0.0' ? '0.0.0.0/0' : `${config.socksIp}/32` ], outbound: 'direct' } ] : []), ...(config.discordBypass ? [ { action: 'route', domain: ['full:updates.discord.com'], outbound: 'proxy' }, { action: 'route', process_name: [ 'Discord' + (isWindows ? '.exe' : ''), 'discord' + (isWindows ? '.exe' : '') ], network: 'udp', outbound: 'direct' }, { action: 'route', domain: [ 'full:discord.com', 'full:*.discord.com', 'full:discordapp.com', 'full:*.discordapp.com', 'full:discord.gg', 'full:*.discord.gg' ], outbound: 'direct' } ] : []), // Universal required ports for all platforms { action: 'route', port: [ 123, // NTP (Network Time Protocol) for system time sync 1900, // SSDP (Simple Service Discovery Protocol) for device discovery 5353 // mDNS (Multicast DNS) for local network service discovery ], outbound: 'direct' }, // Windows-specific ports ...(isWindows ? [ { action: 'route', network: 'udp', port: [ 123, // The NTP port uses UDP and UDP may be blocked by the user 68, // DHCP client for network configuration 137, // NetBIOS name service 138, // NetBIOS datagram service 88, // Kerberos authentication - conditionally direct 389, // LDAP for Active Directory - conditionally direct 5355 // LLMNR (Link-Local Multicast Name Resolution) ], outbound: 'direct' }, { action: 'route', network: 'tcp', port: [ 445, // SMB (Server Message Block) for file sharing 139, // NetBIOS session service 88, // Kerberos authentication - conditionally direct 389 // LDAP for Active Directory - conditionally direct ], outbound: 'direct' } ] : []), // macOS-specific ports ...(isDarwin ? [ { action: 'route', network: 'tcp', port: [ 631, // IPP (Internet Printing Protocol) 3283, // Apple Net Assistant for screen sharing 633, // Apple Configurator for device management 548, // AFP (Apple Filing Protocol) - conditionally direct 5900, // VNC/Screen sharing 427 // SLP (Service Location Protocol) ], outbound: 'direct' }, { action: 'route', network: 'udp', port: [ 514, // Syslog for system logging 631, // IPP (UDP variant) 427 // SLP (Service Location Protocol) ], outbound: 'direct' } ] : []), // Linux-specific ports ...(isLinux ? [ { action: 'route', network: 'tcp', port: [ 111, // Portmapper/RPC for service registration 2049, // NFS (Network File System) 427 // SLP (Service Location Protocol) ], outbound: 'direct' }, { action: 'route', network: 'udp', port: [ 68, // DHCP client 111, // Portmapper/RPC (UDP variant) 427 // SLP (Service Location Protocol) ], outbound: 'direct' } ] : []), { action: 'route', domain: [ 'full:time.windows.com', 'full:*.pool.ntp.org', 'full:time.apple.com', 'full:time.nist.gov', 'full:ntp.ubuntu.com', 'full:ntp2.aliyun.com', 'full:ntp.aliyun.com', 'full:*.ntp.org' ], outbound: 'direct' }, ...(config.udpBlock ? [ { action: 'reject', network: 'udp' } ] : []), ...(geoConfig.geoBlock ? [ { action: 'reject', rule_set: [ 'geosite-category-ads-all', 'geosite-malware', 'geosite-phishing', 'geosite-cryptominers', 'geoip-malware', 'geoip-phishing' ] } ] : []), ...(geoConfig.geoNSFW ? [ { action: 'reject', rule_set: 'geosite-nsfw' } ] : []), ...(rulesConfig.ipSet.length > 0 ? [ { action: 'route', ip_cidr: rulesConfig.ipSet, outbound: 'direct' } ] : []), ...(domainSetException.length > 0 ? [ { action: 'route', domain: domainSetException, outbound: 'proxy' } ] : []), ...(domainSetDirect.length > 0 ? [ { action: 'route', domain: domainSetDirect, outbound: 'direct' } ] : []), ...(rulesConfig.domainSuffixSet.length > 0 ? [ { action: 'route', domain_suffix: rulesConfig.domainSuffixSet, outbound: 'direct' } ] : []), ...(rulesConfig.processSet.length > 0 ? [ { action: 'route', process_name: rulesConfig.processSet, outbound: 'direct' } ] : []), ...(geoConfig.geoIp !== 'none' ? [ { action: 'route', rule_set: `geoip-${geoConfig.geoIp}`, outbound: 'direct' } ] : []), ...(geoConfig.geoSite !== 'none' ? [ { action: 'route', rule_set: `geosite-${geoConfig.geoSite}`, outbound: 'direct' } ] : []) ], ...(geoConfig.geoIp !== 'none' || geoConfig.geoSite !== 'none' || geoConfig.geoBlock || geoConfig.geoNSFW ? { rule_set: [ ...(geoConfig.geoIp !== 'none' ? [ { tag: `geoip-${geoConfig.geoIp}`, type: 'local', format: 'binary', path: path.join( ruleSetDirPath, `geoip-${geoConfig.geoIp}.srs` ) } ] : []), ...(geoConfig.geoSite !== 'none' ? [ { tag: `geosite-${geoConfig.geoSite}`, type: 'local', format: 'binary', path: path.join( ruleSetDirPath, `geosite-${geoConfig.geoSite}.srs` ) } ] : []), ...(geoConfig.geoBlock ? [ { tag: 'geosite-category-ads-all', type: 'local', format: 'binary', path: path.join( ruleSetDirPath, 'geosite-category-ads-all.srs' ) }, { tag: 'geosite-malware', type: 'local', format: 'binary', path: path.join(ruleSetDirPath, 'geosite-malware.srs') }, { tag: 'geosite-phishing', type: 'local', format: 'binary', path: path.join(ruleSetDirPath, 'geosite-phishing.srs') }, { tag: 'geosite-cryptominers', type: 'local', format: 'binary', path: path.join(ruleSetDirPath, 'geosite-cryptominers.srs') }, { tag: 'geoip-malware', type: 'local', format: 'binary', path: path.join(ruleSetDirPath, 'geoip-malware.srs') }, { tag: 'geoip-phishing', type: 'local', format: 'binary', path: path.join(ruleSetDirPath, 'geoip-phishing.srs') } ] : []), ...(geoConfig.geoNSFW ? [ { tag: 'geosite-nsfw', type: 'local', format: 'binary', path: path.join(ruleSetDirPath, 'geosite-nsfw.srs') } ] : []) ] } : undefined), default_domain_resolver: 'dns-direct', final: 'proxy', auto_detect_interface: true }, experimental: { cache_file: { enabled: true, path: sbCachePath, store_fakeip: true } } }; fs.writeFileSync(sbConfigPath, JSON.stringify(configuration, null, 2), 'utf-8'); log.info(`Sing-Box config file has been created at ${sbConfigPath}`); } ================================================ FILE: src/main/lib/sbHelper.ts ================================================ // eslint-disable-next-line max-classes-per-file import { isWindows, ICommand, IPlatformHelper, IRoutingRules } from '../../constants'; export class WindowsHelper implements IPlatformHelper { start(binPath: string): ICommand { return { command: 'powershell.exe', args: [ '-Command', `Start-Process -FilePath '${binPath.replace(/'/g, "''")}' -Verb RunAs -WindowStyle Hidden;` ] }; } running(): ICommand { return { command: `tasklist` }; } } export class DarwinHelper implements IPlatformHelper { start(binPath: string): ICommand { return { command: 'osascript', args: [ '-e', `do shell script "\\"${binPath}\\" > /dev/null 2>&1 & echo $! &" with prompt "Oblivion Desktop requires administrator privileges to run Oblivion-Helper, which is needed to manage network connections." with administrator privileges` ] }; } running(processName: string): ICommand { return { command: `pgrep -l ${processName} | awk '{ print $2 }'` }; } } export class LinuxHelper implements IPlatformHelper { start(binPath: string): ICommand { return { command: 'pkexec', args: [binPath] }; } running(processName: string): ICommand { return { command: `pgrep -l ${processName} | awk '{ print $2 }'` }; } } export class RoutingRuleParser { parse(routingRules: any): IRoutingRules { const result: IRoutingRules = { ipSet: [], domainSet: ['api.cloudflareclient.com'], domainSuffixSet: [], processSet: [] }; if (typeof routingRules !== 'string' || routingRules.trim() === '') { return result; } routingRules .split('\n') .map((line: string) => line.trim().replace(/,$/, '')) .filter(Boolean) .forEach((line: string) => { const [prefix, value] = line.split(':').map((part) => part.trim()); if (!value) return; this.processRule(prefix, value, result); }); return result; } private processRule(prefix: string, value: string, result: IRoutingRules): void { switch (prefix) { case 'ip': result.ipSet.push(value); break; case 'domain': this.processDomainRule(value, result); break; case 'app': this.processAppRule(value, result); break; default: break; } } private processDomainRule(value: string, result: IRoutingRules): void { if (value.startsWith('*')) { result.domainSuffixSet.push(value.substring(2)); } else { result.domainSet.push(value.replace('www.', '')); } } private processAppRule(value: string, result: IRoutingRules): void { const app = isWindows && !value.endsWith('.exe') ? `${value}.exe` : value; result.processSet.push(app); } } ================================================ FILE: src/main/lib/sbManager.ts ================================================ import { ipcMain, IpcMainEvent } from 'electron'; import log from 'electron-log'; import settings from 'electron-settings'; import { spawn, execSync } from 'child_process'; import fs from 'fs'; import * as grpc from '@grpc/grpc-js'; import * as protoLoader from '@grpc/proto-loader'; import { defaultSettings, dnsServers, singBoxGeoIp, singBoxGeoSite, singBoxLog, singBoxStack } from '../../defaultSettings'; import { createSbConfig } from './sbConfig'; import { Language } from '../../localization/type'; import { helperFileName, helperPath, sbExportListPath, protoAssetPath, workingDirPath, isLinux, IConfig, IGeoConfig, ruleSetBaseUrl, IPlatformHelper, regeditVbsDirPath } from '../../constants'; import { WindowsHelper, LinuxHelper, DarwinHelper, RoutingRuleParser } from './sbHelper'; import { mapGrpcErrorCodeToLabel } from './utils'; import { disableProxy } from './proxy'; import { customEvent } from './customEvent'; // Types type GrpcMethod = 'Start' | 'Stop'; interface GrpcResponse { message: T; } interface GrpcError { message: string; code?: number; } // Configuration const CONFIG = { delays: { statusMonitor: 2000, success: 1500, connectionCheck: 3000, connectionTimeout: 3000 }, connection: { maxRetries: 15, grpcEndpoint: '127.0.0.1:50051' }, status: { preparing: 'preparing', downloadFailed: 'download-failed' } } as const; class SingBoxManager { private readonly helperClient: any; private readonly platformHelper: IPlatformHelper; private readonly routingRuleParser: RoutingRuleParser; private event?: IpcMainEvent; private appLang?: Language; private isSBRunning = false; private isListeningToHelper = false; private shouldBreakConnectionTest = false; private responseStatus = ''; constructor() { this.helperClient = this.createGrpcClient(); this.platformHelper = this.createPlatformHelper(); this.routingRuleParser = new RoutingRuleParser(); } // Public API public async startSingBox(appLang?: Language, event?: IpcMainEvent): Promise { if (!this.event) this.event = event; if (this.isSBRunning) return true; try { this.isSBRunning = true; this.appLang = appLang; await this.setupConfigs(); if (!(await this.ensureHelperIsRunning())) return false; await this.monitorHelperStatus(); return this.startService(); } catch (error) { this.isSBRunning = false; this.replyEvent(`${this.appLang?.log.error_singbox_failed_start}\n${error}`); log.error('Failed to start Sing-Box:', error); return false; } } public async stopSingBox(): Promise { if (!this.isSBRunning) return true; try { this.isSBRunning = false; return this.stopService(); } catch (error) { this.isSBRunning = true; this.replyEvent(`${this.appLang?.log.error_singbox_failed_stop}\n${error}`); log.error('Failed to stop Sing-Box:', error); return false; } } public async stopHelper(): Promise { if (!this.isProcessRunning(helperFileName)) return; log.info('Stopping Oblivion-Helper...'); this.helperClient.Exit({}, (err: GrpcError) => { if (err) { const messagePart = err.message.substring(err.message.indexOf(':') + 1).trim(); const errorCodeLabel = mapGrpcErrorCodeToLabel(err.code); const errorMessage = `Helper Error: [${errorCodeLabel}] ${messagePart}`; log.error(errorMessage); } }); await this.delay(CONFIG.delays.statusMonitor); } public async stopHelperOnStart(): Promise { if (!this.isProcessRunning(helperFileName)) return; log.info('Stopping Oblivion-Helper on startup...'); this.helperClient.Exit({}, (err: GrpcError) => { if (err) { const messagePart = err.message.substring(err.message.indexOf(':') + 1).trim(); const errorCodeLabel = mapGrpcErrorCodeToLabel(err.code); const errorMessage = `Helper Error: [${errorCodeLabel}] ${messagePart}`; log.error(errorMessage); } }); await this.delay(CONFIG.delays.statusMonitor * 2); this.isListeningToHelper = false; } public async checkConnectionStatus(): Promise { log.info('Waiting for connection...'); const savedTestUrl = await settings.get('testUrl'); const testUrl = this.getSettingOrDefault(savedTestUrl, defaultSettings.testUrl); log.info(`Testing connection via ${testUrl}`); const checkAttempt = async (attempt: number): Promise => { if (this.shouldBreakConnectionTest) return false; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), CONFIG.delays.connectionTimeout); try { const response = await fetch(testUrl, { signal: controller.signal }); if (response.ok && !this.shouldBreakConnectionTest) { await this.delay(CONFIG.delays.success); log.info(`Connection established after ${attempt} attempts`); return true; } } catch { log.info(`Connection attempt ${attempt}/${CONFIG.connection.maxRetries} failed`); } finally { clearTimeout(timeoutId); } if (attempt >= CONFIG.connection.maxRetries) { this.handleConnectionFailure(); return false; } await this.delay(CONFIG.delays.connectionCheck); return checkAttempt(attempt + 1); }; return checkAttempt(1); } // Private Helper Methods private createGrpcClient() { const packageDefinition = protoLoader.loadSync(protoAssetPath, { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true }); const proto: any = grpc.loadPackageDefinition(packageDefinition).oblivionHelper; return new proto.OblivionService( CONFIG.connection.grpcEndpoint, grpc.credentials.createInsecure() ); } private async ensureHelperIsRunning(): Promise { return this.isProcessRunning(helperFileName) || this.startHelper(); } private async startHelper(): Promise { return new Promise((resolve, reject) => { if (!fs.existsSync(helperPath)) { reject(`${this.appLang?.log.error_helper_not_found}`); return; } log.info('Starting Oblivion-Helper...'); try { disableProxy(regeditVbsDirPath); } catch (error) { log.error('Error managing system proxy:', error); } const command = this.platformHelper.start(helperPath); const helperProcess = spawn(command.command, command.args, { cwd: workingDirPath }); helperProcess.stdout?.on('data', (data: Buffer) => { if (isLinux && data.toString().includes('Server started on')) { resolve(true); } }); helperProcess.stderr?.on('data', (err: Buffer) => { const errorMessage = err.toString().toLowerCase(); if ( errorMessage.includes('denied') || errorMessage.includes('dismissed') || errorMessage.includes('canceled') || errorMessage.includes('no tty present') || errorMessage.includes('a password is required') || errorMessage.includes('no askpass program specified') || errorMessage.includes('operation was cancelled') || errorMessage.includes('authentication is required') || errorMessage.includes('not authorized') ) { customEvent.emit('tray-icon', 'disconnected'); reject(`${this.appLang?.log.error_canceled_by_user}`); } if (errorMessage.includes('command was found in the module')) { log.error( 'The `Start-Process` command exists in the `Microsoft.PowerShell.Management` module, but PowerShell was unable to load this module.' ); customEvent.emit('tray-icon', 'disconnected'); reject('PowerShell module error detected.'); } }); helperProcess.on('close', async (code) => { if (!isLinux && code === 0) { resolve(true); } }); }); } private createPlatformHelper(): IPlatformHelper { const helpers = { darwin: DarwinHelper, win32: WindowsHelper, linux: LinuxHelper }; const Helper = helpers[process.platform as keyof typeof helpers]; if (!Helper) throw new Error(`Unsupported platform: ${process.platform}`); return new Helper(); } private async setupConfigs(): Promise { log.info('Setting up configs...'); const [config, geoConfig] = await Promise.all([ this.loadConfiguration(), this.loadGeoConfiguration() ]); await this.setupGeoLists(geoConfig); const routingRules = await settings.get('routingRules'); const rulesConfig = this.routingRuleParser.parse(routingRules); createSbConfig(config, geoConfig, rulesConfig); } private async loadConfiguration(): Promise { const [ hostIP, port, dns, mtu, loglevel, stack, sniff, address, endpoint, plainDns, DoH, singBoxUdpBlock, singBoxDiscordBypass ] = await Promise.all([ settings.get('hostIP'), settings.get('port'), settings.get('dns'), settings.get('singBoxMTU'), settings.get('singBoxLog'), settings.get('singBoxStack'), settings.get('singBoxSniff'), settings.get('singBoxAddrType'), settings.get('endpoint'), settings.get('plainDns'), settings.get('DoH'), settings.get('singBoxUdpBlock'), settings.get('singBoxDiscordBypass') ]); return { socksIp: this.getSettingOrDefault(hostIP, defaultSettings.hostIP), socksPort: this.getSettingOrDefault(port, defaultSettings.port), tunMtu: this.getSettingOrDefault(mtu, defaultSettings.singBoxMTU), logLevel: this.getSettingOrDefault(loglevel, singBoxLog[0].value), tunStack: this.getSettingOrDefault(stack, singBoxStack[0].value), tunSniff: this.getSettingOrDefault(sniff, defaultSettings.singBoxSniff), tunAddr: this.getTunAddr(address), plainDns: this.getPlainDns(dns, plainDns), DoHDns: this.getDoHDns(dns, DoH), tunEndpoint: this.getSettingOrDefault(endpoint, defaultSettings.endpoint), udpBlock: this.getSettingOrDefault(singBoxUdpBlock, defaultSettings.singBoxUdpBlock), discordBypass: this.getSettingOrDefault( singBoxDiscordBypass, defaultSettings.singBoxDiscordBypass ) }; } private async loadGeoConfiguration(): Promise { const [ip, site, block, nsfw] = await Promise.all([ settings.get('singBoxGeoIp'), settings.get('singBoxGeoSite'), settings.get('singBoxGeoBlock'), settings.get('singBoxGeoNSFW') ]); return { geoIp: this.getSettingOrDefault(ip, singBoxGeoIp[0].geoIp), geoSite: this.getSettingOrDefault(site, singBoxGeoSite[0].geoSite), geoBlock: this.getSettingOrDefault(block, defaultSettings.singBoxGeoBlock), geoNSFW: this.getSettingOrDefault(nsfw, defaultSettings.singBoxGeoNSFW) }; } private getPlainDns(dns: any, plainDns: any): string { if (typeof dns !== 'string') return dnsServers[0].value; if (dns === 'local') return ''; if (dns === 'custom' && plainDns === '') return dnsServers[0].value; return dns === 'custom' ? plainDns : dns; } private getDoHDns(dns: any, doh: any): string { if (typeof dns !== 'string') return `https://${dnsServers[0].value}/dns-query`; if (dns === 'local' || (dns === 'custom' && (typeof doh !== 'string' || doh === ''))) return `https://${dnsServers[0].value}/dns-query`; return dns === 'custom' ? doh : `https://${dns}/dns-query`; } private getTunAddr(addrType: any): string[] { if (typeof addrType !== 'string') return ['172.19.0.1/30', 'fdfe:dcba:9876::1/126']; switch (addrType) { case 'v4': return ['172.19.0.1/30']; case 'v6': return ['fdfe:dcba:9876::1/126']; default: return ['172.19.0.1/30', 'fdfe:dcba:9876::1/126']; } } private getSettingOrDefault(value: any, defaultValue: T): T { return typeof value === typeof defaultValue ? value : defaultValue; } private async setupGeoLists(geoConfig: IGeoConfig): Promise { const urls: Record = {}; const addRuleSet = (filename: string) => { urls[filename] = `${ruleSetBaseUrl}${filename}`; }; if (geoConfig.geoIp !== 'none') addRuleSet(`geoip-${geoConfig.geoIp}.srs`); if (geoConfig.geoSite !== 'none') addRuleSet(`geosite-${geoConfig.geoSite}.srs`); if (geoConfig.geoBlock) { ['category-ads-all', 'malware', 'phishing', 'cryptominers'].forEach((type) => addRuleSet(`geosite-${type}.srs`) ); ['malware', 'phishing'].forEach((type) => addRuleSet(`geoip-${type}.srs`)); } if (geoConfig.geoNSFW) addRuleSet(`geosite-nsfw.srs`); fs.writeFileSync(sbExportListPath, JSON.stringify({ interval: 7, urls }, null, 2), 'utf-8'); log.info(`ExportList config created at ${sbExportListPath}`); } private async monitorHelperStatus(): Promise { if (this.isListeningToHelper) return; await this.delay(CONFIG.delays.statusMonitor); log.info('Monitoring helper status...'); const call = this.helperClient.StreamStatus({}); this.isListeningToHelper = true; call.on('data', this.handleStatusUpdate); call.on('end', this.handleStreamEnd); call.on('error', this.handleStreamError); } private handleStatusUpdate = (response: { status: string }): void => { this.responseStatus = response.status; log.info('Helper Status:', this.responseStatus); if (this.responseStatus === CONFIG.status.preparing) { this.replyEvent('sb_preparing'); } else if (this.responseStatus === CONFIG.status.downloadFailed) { this.replyEvent('sb_download_failed'); this.isSBRunning = false; ipcMain.emit('wp-end'); } }; private handleStreamEnd = (): void => { log.info('Helper service ended'); this.isListeningToHelper = false; this.isSBRunning = false; ipcMain.emit('wp-end'); }; private handleStreamError = (err: Error): void => { log.warn('Helper Error:', err.message); this.isSBRunning = false; ipcMain.emit('wp-end'); }; private handleConnectionFailure(): void { log.error(`Failed to establish connection after ${CONFIG.connection.maxRetries} attempts`); this.replyEvent(`${this.appLang?.log.error_failed_connection}`); ipcMain.emit('wp-end'); } private delay(ms: number): Promise { return new Promise((resolve) => { setTimeout(resolve, ms); }); } private replyEvent(message: string): void { this.event?.reply('guide-toast', message); } private isProcessRunning(processName: string): boolean { return execSync(this.platformHelper.running(processName).command) .toString('utf-8') .toLowerCase() .includes(processName.toLowerCase()); } private async executeGrpcCall(method: GrpcMethod, request: any): Promise { return new Promise((resolve, reject) => { this.helperClient[method](request, (err: GrpcError, response: GrpcResponse) => { if (err) { const failureType = method === 'Start' ? 'sb_start_failed' : 'sb_stop_failed'; this.replyEvent(failureType); this.isSBRunning = method !== 'Start'; //const errorMessage = `Helper Error: ${err.code} ${err.message}`; const messagePart = err.message.substring(err.message.indexOf(':') + 1).trim(); const errorCodeLabel = mapGrpcErrorCodeToLabel(err.code); const errorMessage = `Helper Error: [${errorCodeLabel}] ${messagePart}`; log.error(errorMessage); if (errorMessage.includes('set ipv6 address: Element not found')) { this.replyEvent('sb_error_ipv6'); } else if ( errorMessage.includes('Cannot create a file') || errorMessage.includes('system cannot find the file specified') ) { this.replyEvent('sb_error_tun0'); } else { this.replyEvent(errorMessage); } reject(`Helper: ${err.message}`); return; } log.info(`Helper: ${response.message}`); resolve(true); }); }); } private startService(): Promise { log.info('Starting Sing-Box...'); this.shouldBreakConnectionTest = false; return this.executeGrpcCall('Start', {}); } private stopService(): Promise { log.info('Stopping Sing-Box...'); this.shouldBreakConnectionTest = true; return this.executeGrpcCall('Stop', {}); } } export default SingBoxManager; ================================================ FILE: src/main/lib/speedTestManager.ts ================================================ import { IpcMainEvent, ipcMain } from 'electron'; import log from 'electron-log'; import SpeedTest, { MeasurementConfig } from '@cloudflare/speedtest'; const testMeasurements: MeasurementConfig[] = [ { type: 'latency', numPackets: 1 }, { type: 'download', bytes: 1e5, count: 1, bypassMinDuration: true }, { type: 'latency', numPackets: 20 }, { type: 'download', bytes: 1e5, count: 9 }, { type: 'download', bytes: 1e6, count: 8 }, { type: 'upload', bytes: 1e5, count: 8 }, { type: 'upload', bytes: 1e6, count: 6 }, { type: 'download', bytes: 1e7, count: 6 } ]; class SpeedTestManager { private speedTest: SpeedTest | null = null; private event: IpcMainEvent | null = null; constructor() { this.initializeIpcEvents(); } public initializeIpcEvents(): void { ipcMain.on('speed-test', (event, command: string) => { if (!this.event) { this.event = event; } switch (command) { case 'play': if (this.speedTest?.isFinished) { this.speedTest.restart(); } else { this.setupSpeedTest(); this.speedTest?.play(); } break; case 'pause': this.speedTest?.pause(); this.broadcastResults('paused'); break; case 'restart': this.speedTest?.restart(); break; default: break; } }); } private setupSpeedTest() { if (!this.speedTest) { this.speedTest = new SpeedTest({ autoStart: false, measurements: testMeasurements }); this.speedTest.onResultsChange = () => this.broadcastResults('started'); this.speedTest.onFinish = () => this.broadcastResults('finished'); this.speedTest.onError = (error) => { log.error('Speed Test Error:', error); }; } } private broadcastResults(status: string) { this.event?.reply('speed-test', status, this.speedTest?.results.getSummary()); } } export default SpeedTestManager; ================================================ FILE: src/main/lib/utils.ts ================================================ import fs from 'fs'; import { app, ipcMain } from 'electron'; import log from 'electron-log'; import { defaultSettings, dnsServers } from '../../defaultSettings'; import { isAnyUndefined, typeIsNotUndefined, typeIsUndefined } from '../../renderer/lib/isAnyUndefined'; export const isDev = () => process.env.NODE_ENV === 'development'; export const isDebug = () => process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; export function doesFileExist(filePath: string) { return new Promise((resolve, reject) => { fs.access(filePath, fs.constants.F_OK, (err: any) => { if (err && err.code === 'ENOENT') { resolve(false); } else if (err) { reject(err); } else { resolve(true); } }); }); } export const doesDirectoryExist = doesFileExist; export const doesFolderExist = doesFileExist; export function removeFileIfExists(filePath: string) { return new Promise(async (resolve, reject) => { if (await doesFileExist(filePath)) { fs.unlink(filePath, (err2: any) => { if (err2) { reject(err2); } else { resolve(true); } }); } else { resolve(true); } }); } export function removeDirIfExists(dirPath: string) { return new Promise(async (resolve, reject) => { if (await doesDirectoryExist(dirPath)) { fs.rm(dirPath, { recursive: true, force: true }, (err2: any) => { if (err2) { reject(err2); } else { resolve(true); } }); } }); } export function shouldProxySystem(proxyMode: any) { return isAnyUndefined(proxyMode) || (typeof proxyMode === 'string' && proxyMode === 'system'); } export function hasLicense(license: any) { return typeIsNotUndefined(license) && license !== ''; } export function checkRoutingRules(value: any) { return typeof value === 'string' && value !== '' ? 'Customized' : 'Default'; } export function checkEndpoint(endpoint: any) { return typeIsUndefined(endpoint) || (typeof endpoint === 'string' && endpoint === defaultSettings.endpoint) ? 'default' : 'custom'; } export function checkDataUsage(value: any) { return typeof value === 'boolean' ? value ? 'true' : 'false' : defaultSettings.dataUsage ? 'true' : 'false'; } export function checkProxyMode(value: any) { return typeof value === 'string' ? value : defaultSettings.proxyMode; } export function checkReserved(value: any) { return typeof value === 'boolean' ? value ? 'true' : 'false' : defaultSettings.reserved ? 'true' : 'false'; } export function checkGeoStatus(ip: any, site: any, block: any, nsfw: any) { let status = ''; status = 'Ip: ' + (typeof ip === 'string' ? String(ip) : 'none') + ', '; status += 'Site: ' + (typeof site === 'string' ? String(site) : 'none') + ', '; status += 'Block: ' + (typeof block === 'boolean' ? block ? 'true' : 'false' : defaultSettings.singBoxGeoBlock ? 'true' : 'false') + ', '; status += 'NSFW: ' + (typeof nsfw === 'boolean' ? nsfw ? 'true' : 'false' : defaultSettings.singBoxGeoNSFW ? 'true' : 'false'); return status; } export function calculateMethod(method: any) { if (typeIsUndefined(method)) { return defaultSettings.method; } switch (method) { case 'gool': return 'gool'; case 'psiphon': return 'psiphon'; case 'masque': return 'masque'; default: return 'warp'; } } export function isSocksProxy(method: any) { if (typeIsUndefined(method)) { return defaultSettings.method; } switch (method) { case 'psiphon': return true; case 'masque': return true; default: return false; } } export function checkIpType(value: any, endpoint: any) { if (checkEndpoint(endpoint) !== 'custom') { switch (value) { case '-6': return 'v6'; case '-4': return 'v4'; default: return 'v4/v6'; } } else { if (endpoint.startsWith('[')) { return 'v6'; } else { return 'v4'; } } } export function checkTunAddrType(addrType: any): string { if (typeof addrType !== 'string') return 'v64'; return addrType; } export function checkTestUrl(value: any) { return typeof value === 'string' ? value : defaultSettings.testUrl; } export function checkDNS(value: any) { return typeof value === 'string' ? value : dnsServers[0].value; } export const exitTheApp = async () => { log.info('exiting the app...'); // Emit 'wp-end' and wait ipcMain.emit('wp-end', true); // Emit 'end-wp-and-exit-app' and wait ipcMain.emit('end-wp-and-exit-app'); // make sure to kill wp process before exit (for linux(windows and mac kill child processes by default)) if (process.platform === 'darwin') { log.info('Exiting the application...'); app.exit(0); } else { ipcMain.on('exit', () => { log.info('Exiting the application...'); app.exit(0); }); } }; export function extractPortsFromEndpoints(strData: string): number[] { const endpointsRegex = /endpoints="\[(.*?)]"/; const endpointsMatch = strData.match(endpointsRegex); if (endpointsMatch) { const endpointsStr = endpointsMatch[1]; const portRegex = /(?:\b(?:\d{1,3}\.){3}\d{1,3}|\[[a-fA-F0-9:]+]|[a-fA-F0-9:]+):(\d{1,5})/g; const ports = new Set(); let match = portRegex.exec(endpointsStr); do { if (match) { ports.add(parseInt(match[1], 10)); } match = portRegex.exec(endpointsStr); } while (match); if (ports.size > 0) { return Array.from(ports); } } return []; } export function formatEndpointForConfig(endpoint: string): string { const ip = endpoint.replace(/:\d+$/, '').replace(/^\[/, '').replace(/\]$/, ''); return ip.includes(':') ? `${ip}/128` : `${ip}/32`; } export function mapGrpcErrorCodeToLabel(code: number | undefined): string { if (code === undefined) { return 'Unknown Error'; } switch (code) { case 0: return 'OK'; case 1: return 'Cancelled'; case 2: return 'Unknown Error'; case 3: return 'Invalid Argument'; case 4: return 'Deadline Exceeded'; case 5: return 'Not Found'; case 6: return 'Already Exists'; case 7: return 'Permission Denied'; case 8: return 'Resource Exhausted'; case 9: return 'Failed Precondition'; case 10: return 'Aborted'; case 11: return 'Out of Range'; case 12: return 'Unimplemented'; case 13: return 'Internal Error'; case 14: return 'Unavailable'; case 15: return 'Data Loss'; case 16: return 'Unauthenticated'; default: return 'Unknown Error'; } } export function isIpBasedDoH(url: string): boolean { try { const parsedUrl = new URL(url); const hostname = parsedUrl.hostname; const ipv4Pattern = /^(?:\d{1,3}\.){3}\d{1,3}$/; const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/; return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname); } catch (error) { return false; } } export function versionComparison(localVersion: any, apiVersion: any): boolean { const parts1 = localVersion .toLowerCase() .replace('v', '') .replace('-beta', '') .split('.') .map(Number); const parts2 = apiVersion .toLowerCase() .replace('v', '') .replace('-beta', '') .split('.') .map(Number); for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { const part1 = parts1[i] || 0; const part2 = parts2[i] || 0; if (part1 > part2) { return false; } else if (part1 < part2) { return true; } } return false; } ================================================ FILE: src/main/lib/wpHelper.ts ================================================ import { IpcMainEvent } from 'electron'; import settings from 'electron-settings'; import { countries, defaultSettings } from '../../defaultSettings'; import { removeDirIfExists } from './utils'; import { getTranslate } from '../../localization'; import { stuffPath } from '../../constants'; import WarpPlusManager from './wpManager'; import { typeIsUndefined } from '../../renderer/lib/isAnyUndefined'; import { withDefault } from '../../renderer/lib/withDefault'; //import { customEvent } from './customEvent'; //import { getTranslateElectron } from '../../localization/electron'; //import fs from 'fs'; let appLang = getTranslate('en'); const randomCountry = () => countries[Math.floor(Math.random() * countries.length)]?.value || 'DE'; export const getUserSettings = async () => { const [ endpoint, ipType, port, location, license, method, hostIP, rtt, reserved, lang, dns, plainDns, testUrl, connectTimeout ] = await Promise.all([ settings.get('endpoint'), settings.get('ipType'), settings.get('port'), settings.get('location'), settings.get('license'), settings.get('method'), settings.get('hostIP'), settings.get('rtt'), settings.get('reserved'), settings.get('lang'), settings.get('dns'), settings.get('plainDns'), settings.get('testUrl'), settings.get('connectTimeout') ]); appLang = getTranslate(String(withDefault(lang, defaultSettings.lang))); const finalDns = typeof dns !== 'string' || dns === 'local' ? '' : dns === 'custom' ? typeof plainDns === 'string' ? plainDns : '' : dns; return [ '--bind', `${typeof hostIP === 'string' && hostIP.length > 0 ? hostIP : defaultSettings.hostIP}:${typeof port === 'string' || typeof port === 'number' ? port : defaultSettings.port}`, /*...(typeof license === 'string' && license !== '' ? ['--key', license] : []),*/ ...(typeof testUrl === 'string' && testUrl !== '' && testUrl !== defaultSettings.testUrl ? ['--test-url', testUrl] : []), ...(typeof method === 'string' ? method === 'gool' ? ['--gool'] : method === 'psiphon' ? [ '--cfon', '--country', typeof location === 'string' && location !== '' ? location : randomCountry() ] : [] : ['--gool']), ...((typeof endpoint === 'string' && (endpoint === '' || endpoint === defaultSettings.endpoint)) || typeIsUndefined(endpoint) ? [ '--scan', ...(typeof ipType === 'string' && ipType !== '' ? [ipType] : []), ...(typeof rtt === 'string' ? ['--rtt', rtt] : []) ] : [ '--endpoint', typeof endpoint === 'string' && endpoint.length > 0 ? endpoint : defaultSettings.endpoint ]), ...(typeof reserved === 'boolean' && !reserved ? ['--reserved', '0,0,0'] : []), /*...[ '--connect-timeout', typeof connectTimeout === 'string' ? connectTimeout : defaultSettings.connectTimeout ],*/ ...(finalDns !== '' ? ['--dns', finalDns] : []) ]; }; // The setStuffPath function is currently not used anywhere in the codebase. // It's been commented out to avoid unused code. If needed in the future, // uncomment and update its usage accordingly. /*export const setStuffPath = async (args: string[]) => { args.push('--cache-dir', stuffPath); if (!(await doesDirectoryExist(stuffPath))) { await fs.mkdir(stuffPath, { recursive: true }).catch(console.error); } };*/ const wpErrorTranslation: Record string> = { 'bind: address already in use': ({ port }) => appLang.log.error_port_already_in_use(port), 'Only one usage of each socket address': () => { WarpPlusManager.restartApp(); //return appLang.log.error_port_socket; return 'error_port_restart'; }, 'Invalid license': () => appLang.log.error_invalid_license, 'Too many connected devices': () => appLang.log.error_too_many_connected, 'Access is denied': () => appLang.log.error_access_denied, 'failed to set endpoint': () => appLang.log.error_failed_set_endpoint, 'load primary warp identity': () => { return 'error_warp_identity'; }, 'script failed to run': () => appLang.log.error_script_failed, 'object null is not iterable': () => appLang.log.error_object_null, 'parse args: unknown flag': () => appLang.log.error_unknown_flag, 'context deadline exceeded': () => appLang.log.error_deadline_exceeded, 'connection test failed': () => appLang.log.error_connection_failed, 'parse args: --country': () => appLang.log.error_country_failed /*'connection reset by peer': () => { //disconnectApp(); return appLang.log.error_wp_reset_peer; }*/ }; export const handleWpErrors = (strData: string, port: string, ipcEvent?: IpcMainEvent) => { Object.entries(wpErrorTranslation).forEach(([errorMsg, translator]) => { if (strData.includes(errorMsg)) { ipcEvent?.reply('guide-toast', translator({ port })); removeDirIfExists(stuffPath).catch((err) => console.log('removeDirIfExists Error:', err.message) ); } }); }; ================================================ FILE: src/main/lib/wpManager.ts ================================================ import { app, ipcMain, BrowserWindow, dialog, shell } from 'electron'; import { spawn, ChildProcess, execSync, execFile } from 'child_process'; import toast from 'react-hot-toast'; import treeKill from 'tree-kill'; import settings from 'electron-settings'; import log from 'electron-log'; import fs from 'fs'; import sound from 'sound-play'; import Aplay from 'node-aplay'; import { isDev, removeFileIfExists, shouldProxySystem } from './utils'; import { disableProxy as disableSystemProxy, enableProxy as enableSystemProxy } from './proxy'; import { getOsInfo, logMetadata } from '../ipcListeners/log'; import { getUserSettings, handleWpErrors } from './wpHelper'; import { defaultSettings } from '../../defaultSettings'; import { customEvent } from './customEvent'; import { showWpLogs } from '../dxConfig'; import { getTranslate } from '../../localization'; import { wpAssetPath, wpBinPath, workingDirPath, regeditVbsDirPath, singBoxManager, netStatsManager, logPath, isLinux, soundEffect, isWindows, exclusionsPath, mpPath, mpAssetPath, usquePath, usqueAssetPath } from '../../constants'; // Types and Enums enum ConnectionState { DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING } interface WarpPlusState { child: ChildProcess | null; exitOnWpEnd: boolean; appLang: ReturnType; // eslint-disable-next-line no-undef event?: Electron.IpcMainEvent; shouldStartSingBox: boolean; shouldStopSingBox: boolean; shouldApplySystemProxy: boolean; connectionState: ConnectionState; settings: { proxyMode: string; port: number; hostIP: string; ipData: boolean; dataUsage: boolean; restartCounter: number; soundEffect: boolean; }; } // Constants const endpointRegex = /msg="(?:scan results|using warp endpoints)" endpoints="\[.*?(?:AddrPort:)?(\d{1,3}(?:\.\d{1,3}){3}:\d{1,5})/; const MAX_RETRIES = 2; const SUCCESS_MSG_PREFIX = 'level=INFO msg="serving proxy" address='; // State management const state: WarpPlusState = { child: null, exitOnWpEnd: false, appLang: getTranslate('en'), shouldStartSingBox: true, shouldStopSingBox: true, shouldApplySystemProxy: true, connectionState: ConnectionState.DISCONNECTED, settings: { ...defaultSettings } }; // Logger configuration const simpleLog = log.create({ logId: 'simpleLog' }); simpleLog.transports.console.format = '{text}'; simpleLog.transports.file.format = '{text}'; class WarpPlusManager { //Public-Methods static restartApp(delay = 5000) { let retryCount = 0; const attemptRestart = async () => { try { BrowserWindow.getAllWindows().forEach((win) => { if (!win.isDestroyed()) win.destroy(); }); log.info('Relaunching app due to warp-plus error.'); app.relaunch(); app.exit(0); } catch (error) { retryCount++; log.error(`Error during app restart (attempt ${retryCount}):`, error); if (retryCount < MAX_RETRIES) { log.info('Retrying app quit...'); setTimeout(attemptRestart, 3000); } else { log.error('Max retry limit reached. Could not restart app.'); } } }; setTimeout(attemptRestart, delay); } private static getWinDrive() { try { const drive = execSync('powershell -Command "$env:SystemDrive"', { encoding: 'utf8' }).trim(); return drive || 'C:\\'; } catch (error) { return 'C:\\'; } } static async addToExclusions() { const exclusionPaths = [ wpAssetPath, wpBinPath, mpAssetPath, mpPath, usqueAssetPath, usquePath ]; const result: any = await dialog.showMessageBox({ type: 'question', buttons: ['No', 'Yes', 'View Guide'], defaultId: 1, title: 'Add to Exceptions', message: state.appLang.log.error_wp_exclusions }); if (typeof result.response === 'number') { if (result.response === 0) { this.restartApp(1000); return; } if (result.response === 2) { try { await shell.openExternal( 'https://github.com/bepass-org/oblivion-desktop/wiki/The-warp-plus-file-is-not-found' ); } catch (err) { log.error('Failed to open link:', (err as Error).message || err); } this.restartApp(); return; } } const windowsDrive = this.getWinDrive(); const defenderCommands = exclusionPaths .map((p) => `powershell -Command "Add-MpPreference -ExclusionPath '${p}'"`) .join('\n'); const bitdefenderCommands = exclusionPaths .map( (p) => `"${windowsDrive}\\Program Files\\Bitdefender\\Bitdefender Security\\bdagent.exe" /addexclusion "${p}"` ) .join('\n'); const batContent = `@echo off chcp 65001 >nul set win_drive=${windowsDrive} :: Define color codes set COLOR_GREEN=powershell -NoProfile -Command "Write-Host '[Success]' -ForegroundColor Green" set COLOR_RED=powershell -NoProfile -Command "Write-Host '[Error]' -ForegroundColor Red" set COLOR_RESET=REM :: Check for admin privileges net session >nul 2>&1 if %errorLevel% neq 0 ( powershell -Command "Start-Process '%~f0' -Verb RunAs" exit /b ) %COLOR_GREEN% Running with administrator privileges. :: === Windows Defender Check === powershell -Command "Get-MpComputerStatus" >nul 2>&1 if %errorLevel% neq 0 ( set defender_missing=1 ) else ( ${defenderCommands} %COLOR_GREEN% Windows Defender exclusions added. timeout /t 3 >nul ) :: === Bitdefender Check === if exist "%win_drive%\\Program Files\\Bitdefender\\Bitdefender Security\\bdagent.exe" ( ${bitdefenderCommands} %COLOR_GREEN% Bitdefender exclusions added. timeout /t 3 >nul ) else ( set bitdefender_missing=1 ) :: Exit if both security programs are missing if defined defender_missing if defined bitdefender_missing ( %COLOR_RED% No compatible antivirus found. Exiting ... timeout /t 3 >nul exit /b ) exit`; fs.writeFileSync(exclusionsPath, batContent, { encoding: 'utf8' }); const process = execFile( 'powershell', ['-Command', `Start-Process '${exclusionsPath}' -Verb RunAs -Wait`], (err) => { if (err) { log.error('⚠️ Failed to execute exclusion script:', err); this.restartApp(1500); } } ); process.on('close', (code) => { if (code !== 0) { log.error(`❌ Script exited with code ${code}`); } if (fs.existsSync(exclusionsPath)) { fs.rm(exclusionsPath, { force: true }, (unlinkErr) => { log.warn('⚠️ Could not delete exclusionsPath file:', unlinkErr); }); } this.restartApp(1500); }); } static async handleSystemProxy(enable: boolean) { if (!shouldProxySystem(state.settings.proxyMode)) { state.connectionState = enable ? ConnectionState.CONNECTING : ConnectionState.DISCONNECTED; return this.sendConnectionSignal(); } try { const proxyFunc = enable ? enableSystemProxy : disableSystemProxy; await proxyFunc(regeditVbsDirPath, state.event); state.connectionState = enable ? ConnectionState.CONNECTING : ConnectionState.DISCONNECTING; this.sendConnectionSignal(); } catch (error) { log.error('Error managing system proxy:', error); state.event?.reply('wp-end', true); if (enable) await this.handleSystemProxy(false); } } static async handleMissingFile(assetPath: string, errorMsg: string) { state.event?.reply('guide-toast', errorMsg); state.event?.reply('wp-end', true); if (fs.existsSync(assetPath) && state.settings.restartCounter < 2) { await settings.setSync('restartCounter', state.settings.restartCounter + 1); if (isWindows) { this.addToExclusions(); } else { this.restartApp(); } } return true; } static async startWarpPlus() { const method = (await settings.get('method')) || defaultSettings.method; if (method === 'masque') { if (!fs.existsSync(mpPath)) { if (await this.handleMissingFile(mpAssetPath, state.appLang.log.error_mp_not_found)) return; } if (!fs.existsSync(usquePath)) { if ( await this.handleMissingFile( usqueAssetPath, state.appLang.log.error_usque_not_found ) ) return; } } else { if (!fs.existsSync(wpBinPath)) { if (await this.handleMissingFile(wpAssetPath, state.appLang.log.error_wp_not_found)) return; } } try { const args = await getUserSettings(); if (method === 'masque') { log.info('Starting MasquePlus process ...'); log.info(`${mpPath} ${args.join(' ')}`); state.child = spawn(mpPath, args, { cwd: workingDirPath }); } else { log.info('Starting WarpPlus process ...'); log.info(`${wpBinPath} ${args.join(' ')}`); state.child = spawn(wpBinPath, args, { cwd: workingDirPath }); } this.setupChildProcessHandlers(); } catch (error) { log.error('Error while starting Core:', error); this.handleStartupError(); } } static killChild() { if (state.child?.pid) { treeKill(state.child.pid, 'SIGKILL'); } } private static playSoundEffect() { if (!state.settings.soundEffect) return; try { if (isLinux) { const music = new Aplay(soundEffect); music.play(); } else { sound.play(soundEffect); } } catch (error) { console.error('Error playing sound:', error); } } //Private-Methods private static sendConnectionSignal() { // eslint-disable-next-line default-case switch (state.connectionState) { case ConnectionState.CONNECTING: customEvent.emit('tray-icon', 'connecting'); break; case ConnectionState.DISCONNECTING: customEvent.emit('tray-icon', 'disconnecting'); break; case ConnectionState.CONNECTED: state.event?.reply('wp-start', true); customEvent.emit('tray-icon', `connected-${state.settings.proxyMode}`); this.playSoundEffect(); toast.remove('GUIDE'); if ( state.settings.ipData && state.settings.dataUsage && state.settings.proxyMode !== 'none' ) { netStatsManager.startMonitoring(state.event); } break; case ConnectionState.DISCONNECTED: netStatsManager.stopMonitoring(); state.event?.reply('wp-end', true); if (state.exitOnWpEnd) ipcMain.emit('exit'); customEvent.emit('tray-icon', 'disconnected'); break; } } private static setupChildProcessHandlers() { if (!state.child) return; state.child.stdout?.on('data', async (data: Buffer) => { const strData = data.toString(); if (strData.includes(`${SUCCESS_MSG_PREFIX}${state.settings.hostIP}`)) { await this.handleSuccessMessage(); } await this.handleEndpointUpdates(strData); handleWpErrors(strData, String(state.settings.port), state.event); if (showWpLogs || isDev()) { simpleLog.info(strData); } }); state.child.stderr?.on('data', (err: Buffer) => { if (showWpLogs || isDev()) { simpleLog.error(`WarpPlus error: ${err.toString()}`); } }); state.child.on('exit', () => this.handleChildExit()); } private static async handleSuccessMessage() { if (state.settings.proxyMode === 'tun' && !(await singBoxManager.checkConnectionStatus())) { state.event?.reply('wp-end', true); this.killChild(); } else { state.connectionState = ConnectionState.CONNECTED; this.sendConnectionSignal(); await settings.setSync('restartCounter', 0); } } private static async handleEndpointUpdates(strData: string) { const endpointMatch = strData.match(endpointRegex); if (endpointMatch) { await settings.setSync('scanResult', endpointMatch[1]); } } private static async handleChildExit() { if ( state.settings.proxyMode === 'tun' && state.shouldStopSingBox && !(await singBoxManager.stopSingBox()) ) { state.event?.reply('wp-end', false); } else { if (!state.shouldApplySystemProxy) { log.info( 'The commands to disable and then re-enable the systemProxy were skipped, which occurred under conditions where the location had reverted from the gool method to Iran.' ); } else { await this.handleSystemProxy(false); } state.connectionState = ConnectionState.DISCONNECTED; this.sendConnectionSignal(); log.info('WarpPlus process exited.'); state.child = null; } } private static handleStartupError() { state.event?.reply('guide-toast', state.appLang.log.error_wp_stopped); state.event?.reply('wp-end', true); if (fs.existsSync(wpBinPath)) { fs.rm(wpBinPath, (err) => { if (err) throw err; log.info('Deleted corrupt WarpPlus binary. Restarting app...'); this.restartApp(); }); } } } // IPC Handlers ipcMain.on('wp-start', async (event, arg) => { state.shouldStartSingBox = arg !== 'start-from-gool'; state.exitOnWpEnd = false; state.event = event; const settingsValues = settings.getSync(); state.appLang = getTranslate(String(settingsValues?.lang || defaultSettings.lang)); state.settings = { proxyMode: String(settingsValues.proxyMode || defaultSettings.proxyMode), port: Number(settingsValues.port || defaultSettings.port), hostIP: String(settingsValues.hostIP || defaultSettings.hostIP), ipData: Boolean(settingsValues.ipData || defaultSettings.ipData), dataUsage: Boolean(settingsValues.dataUsage || defaultSettings.dataUsage), restartCounter: Number(settingsValues.restartCounter || defaultSettings.restartCounter), soundEffect: Boolean(settingsValues.soundEffect || defaultSettings.soundEffect) }; await removeFileIfExists(logPath); log.info('Deleted past logs for new connection.'); const osInfo = await getOsInfo(); logMetadata(osInfo); await WarpPlusManager.handleSystemProxy(true); if ( state.settings.proxyMode === 'tun' && state.shouldStartSingBox && !(await singBoxManager.startSingBox(state.appLang, event)) ) { event.reply('wp-end', true); WarpPlusManager.killChild(); } else { await WarpPlusManager.startWarpPlus(); } }); ipcMain.on('wp-end', async (event, arg) => { state.shouldStopSingBox = arg !== 'stop-from-gool'; state.shouldApplySystemProxy = arg !== 'stop-from-gool'; try { WarpPlusManager.killChild(); } catch (error) { log.error('Error killing WarpPlus:', error); event.reply('wp-end', false); } }); ipcMain.on('end-wp-and-exit-app', async (event) => { const closeHelper = (await settings.get('closeHelper')) ?? defaultSettings.closeHelper; try { if (state.child?.pid) { state.exitOnWpEnd = true; WarpPlusManager.killChild(); } else { ipcMain.emit('exit'); } if (closeHelper) { await singBoxManager.stopHelper(); } } catch (error) { log.error('Error exiting app:', error); event.reply('wp-end', false); } }); export default WarpPlusManager; ================================================ FILE: src/main/main.ts ================================================ import { app, BrowserWindow, ipcMain, screen, shell, Menu, Tray, nativeImage, IpcMainEvent, globalShortcut, BrowserWindowConstructorOptions, Event, NativeImage, Notification, MenuItemConstructorOptions, dialog } from 'electron'; import path from 'path'; import fs, { existsSync } from 'fs'; import settings from 'electron-settings'; import log from 'electron-log'; //import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer'; //import debug from 'electron-debug'; import { rimrafSync } from 'rimraf'; import sudo from 'sudo-prompt'; import https from 'https'; import { networkInterfaces } from 'systeminformation'; import MenuBuilder from './menu'; import { exitTheApp, isDev, isDebug, versionComparison } from './lib/utils'; import { openDevToolsByDefault, openDevToolsInFullScreen, useCustomWindowXY } from './dxConfig'; import './ipc'; import { devPlayground } from './playground'; import { getOsInfo, logMetadata } from './ipcListeners/log'; import { customEvent } from './lib/customEvent'; import { getTranslate } from '../localization'; import { defaultSettings } from '../defaultSettings'; import { appVersion, wpAssetPath, wpBinPath, helperAssetPath, helperPath, versionFilePath, netStatsPath, netStatsAssetPath, singBoxManager, downloadedPath, updaterPath, windowPosition, proxyResetPath, proxyResetAssetPath, isWindows, isDarwin, isLinux, gtk4Paths, usquePath, usqueAssetPath, mpPath, mpAssetPath, workingDirPath } from '../constants'; import packageJsonData from '../../package.json'; import { spawnSync } from 'child_process'; const APP_TITLE = `Oblivion Desktop${isDev() ? ' ᴅᴇᴠ' : ''}`; const WINDOW_DIMENSIONS = { width: 400, height: 650 }; if (isLinux) { const hasGtk4 = gtk4Paths.some((p) => existsSync(p)); if (!hasGtk4) { app.commandLine.appendSwitch('gtk-version', '3'); } //app.commandLine.appendSwitch('no-sandbox'); } if (isWindows) { app.setAppUserModelId(packageJsonData.build.appId); } process.on('uncaughtException', (err) => { log.error('Uncaught Exception:', err); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection occurred!'); console.error('Reason:', reason ?? 'No reason provided'); console.error('Promise:', promise); if (reason instanceof Error) { console.error(reason.stack); } }); interface WindowState { mainWindow: BrowserWindow | null; appIcon: Tray | null; connectionStatus: string; trayMenuEvent?: IpcMainEvent; userLang: string; proxyMode: string | null; appLang: ReturnType; isFirstRun: boolean; updateNotification: Notification | undefined; isCheckingForUpdates: boolean; checkForUpdatesIntervalId: NodeJS.Timeout | undefined; hasNewUpdate: boolean; } class OblivionDesktop { private state: WindowState = { mainWindow: null, appIcon: null, connectionStatus: 'disconnected', userLang: 'en', proxyMode: null, appLang: getTranslate('en'), isFirstRun: false, updateNotification: undefined, isCheckingForUpdates: false, checkForUpdatesIntervalId: undefined, hasNewUpdate: false }; constructor() { this.initialize().catch((err) => log.error('Initialization failed:', err)); } private async initialize(): Promise { if (!app.requestSingleInstanceLock()) { /*if (!isDev()) { dialog.showMessageBox({ type: 'info', title: APP_TITLE, message: this.state.appLang.toast.exit_pending, buttons: ['Ok'] }); }*/ log.info('Instance already running.'); app.exit(0); return; } try { if (!existsSync(path.join(workingDirPath, 'config.json'))) { spawnSync(usquePath, ['register', '-a', '-n', 'masque-plus'], { cwd: workingDirPath }); } } catch {} await this.setupInitialConfiguration(); this.setupIpcEvents(); this.setupAppEvents(); this.setupCheckForUpdates(); //this.handleShutdown(); } private async setupInitialConfiguration(): Promise { devPlayground(); log.info('Creating new od instance...'); this.state.proxyMode = (await settings.get('proxyMode')) as string; await this.handleVersionCheck(); this.copyRequiredFiles(); } private async handleVersionCheck(): Promise { this.state.isFirstRun = false; try { const dirPath = path.dirname(versionFilePath); await fs.promises.mkdir(dirPath, { recursive: true }); let savedVersion: string | null = null; try { await fs.promises.access(versionFilePath, fs.constants.F_OK); savedVersion = await fs.promises.readFile(versionFilePath, 'utf-8'); } catch (readErr) { log.error('Version file not found or unreadable:', (readErr as Error).message); } if (savedVersion === null || savedVersion !== appVersion) { this.state.isFirstRun = true; await singBoxManager.stopHelperOnStart(); await this.cleanupOldFiles(); } try { await fs.promises.writeFile(versionFilePath, appVersion, 'utf-8'); } catch (writeErr) { log.error('Failed to write version file:', (writeErr as Error).message); throw writeErr; } } catch (err) { this.state.isFirstRun = false; log.error('Error during version check:', err); } } private async cleanupOldFiles(): Promise { const filesToClean = [wpBinPath, helperPath, netStatsPath, usquePath, mpPath]; if (isWindows) { filesToClean.push(proxyResetPath); } filesToClean.forEach((file) => { if (fs.existsSync(file)) { rimrafSync(file); } }); } private copyRequiredFiles(): void { const filePairs = [ { src: wpAssetPath, dest: wpBinPath, name: 'wp' }, { src: helperAssetPath, dest: helperPath, name: 'helper' }, { src: netStatsAssetPath, dest: netStatsPath, name: 'netStats' }, { src: usqueAssetPath, dest: usquePath, name: 'usque' }, { src: mpAssetPath, dest: mpPath, name: 'mp' } ]; if (isWindows) { filePairs.push({ src: proxyResetAssetPath, dest: proxyResetPath, name: 'proxyReset' }); } filePairs.forEach(({ src, dest, name }) => { if (fs.existsSync(src) && !fs.existsSync(dest)) { fs.copyFile(src, dest, (err) => { if (err) { log.error(`Error copying ${name} binary:`, err); return; } log.info(`${name} binary was copied to userData directory.`); }); } else if (!fs.existsSync(src)) { log.info(`Copy halted: ${name} file does not exist.`); } }); } private getLastWindowPosition(): { x: number; y: number } | null { if (fs.existsSync(windowPosition)) { try { const positionData = fs.readFileSync(windowPosition, 'utf-8'); const { x, y } = JSON.parse(positionData); return { x, y }; } catch (err) { console.error('Error reading window position:', err); } } return null; } private saveWindowPosition(x: number, y: number) { const positionData = JSON.stringify({ x, y }); fs.writeFileSync(windowPosition, positionData, 'utf-8'); } private getValidWindowPosition(x: number, y: number): { x: number; y: number } | null { const displays = screen.getAllDisplays(); const windowWidth = WINDOW_DIMENSIONS.width; const windowHeight = WINDOW_DIMENSIONS.height; const windowRect = { left: x, top: y, right: x + windowWidth, bottom: y + windowHeight }; const MIN_VISIBLE_AREA = windowWidth * windowHeight * 0.3; const isVisibleEnough = displays.some((display) => { const bounds = display.workArea; const overlapWidth = Math.max( 0, Math.min(windowRect.right, bounds.x + bounds.width) - Math.max(windowRect.left, bounds.x) ); const overlapHeight = Math.max( 0, Math.min(windowRect.bottom, bounds.y + bounds.height) - Math.max(windowRect.top, bounds.y) ); const overlapArea = overlapWidth * overlapHeight; return overlapArea >= MIN_VISIBLE_AREA; }); if (!isVisibleEnough) { return null; } return { x, y }; } private createWindowConfig(): BrowserWindowConstructorOptions { const config: any = { title: APP_TITLE, show: false, width: WINDOW_DIMENSIONS.width, height: WINDOW_DIMENSIONS.height, autoHideMenuBar: true, transparent: false, center: true, frame: true, resizable: true, fullscreenable: true, icon: this.getAssetPath(packageJsonData.shortName + '.png'), webPreferences: { nativeWindowOpen: false, devTools: isDev(), devToolsKeyCombination: isDev(), contextIsolation: true, enableRemoteModule: false, preload: app.isPackaged ? path.join(__dirname, 'preload.js') : path.join(__dirname, '../../.erb/dll/preload.js') } }; const lastPosition = this.getLastWindowPosition(); if (lastPosition) { const validPosition = this.getValidWindowPosition(lastPosition.x, lastPosition.y); if (validPosition) { config.x = validPosition.x; config.y = validPosition.y; config.center = false; } } else if (isDev() && useCustomWindowXY && !openDevToolsInFullScreen) { const primaryDisplay = screen.getPrimaryDisplay(); const { width: displayWidth, height: displayHeight } = primaryDisplay.workAreaSize; config.x = displayWidth - WINDOW_DIMENSIONS.width - 60; config.y = displayHeight - WINDOW_DIMENSIONS.height - 160; } return config; } private getAssetPath(...paths: string[]): string { const RESOURCES_PATH = app.isPackaged ? path.join(process.resourcesPath, 'assets') : path.join(__dirname, '../../assets'); return path.join(RESOURCES_PATH, ...paths); } private resolveHtmlPath(htmlFileName: string): string { if (isDev()) { const port = process.env.PORT || 1212; const url = new URL(`http://localhost:${port}`); url.pathname = htmlFileName; return url.href; } return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; } private async createWindow(): Promise { // we don't have this package installed. anything missing? // if (!isDev()) { // const sourceMapSupport = require('source-map-support'); // sourceMapSupport.install(); // } // seems we don't need this. check openDevTools method. // if (isDebug() && openDevToolsByDefault) { // debug(); // } /* if (isDebug()) { await this.installDevTools(); } */ const config = this.createWindowConfig(); this.state.mainWindow = new BrowserWindow(config); this.state.mainWindow.on('move', () => { if (this.state.mainWindow) { const position = this.state.mainWindow.getBounds(); this.saveWindowPosition(position.x, position.y); } }); await this.setupWindowEvents(); await this.state.mainWindow.loadURL(this.resolveHtmlPath('index.html')); const menuBuilder = new MenuBuilder(this.state.mainWindow); menuBuilder.buildMenu(); } /* private async installDevTools(): Promise { // we used import instead of require. everything ok? // const installer = require('electron-devtools-installer'); // const extensions = ['REACT_DEVELOPER_TOOLS']; // await installer // .default( // extensions.map((name) => installer[name]), // !!process.env.UPGRADE_EXTENSIONS // ) // .catch((err: Error) => log.error('InstallDevTools Error:', err.message)); installExtension(REACT_DEVELOPER_TOOLS) .then((name) => log.info(`Added Extension: ${name}`)) .catch((err: Error) => log.error('InstallDevTools Error:', err.message)); } */ private openDevTools(): void { if (isDebug() && openDevToolsByDefault) { this.state.mainWindow?.webContents.openDevTools(); if (openDevToolsInFullScreen) { this.state.mainWindow?.setFullScreen(true); } } } private async setupWindowEvents(): Promise { if (!this.state.mainWindow) return; this.state.mainWindow.setMinimumSize(WINDOW_DIMENSIONS.width, WINDOW_DIMENSIONS.height); this.state.mainWindow.on('will-resize', (event) => { event.preventDefault(); }); this.state.mainWindow.on('ready-to-show', async () => { const startMinimized = await settings.get('startMinimized'); if (typeof startMinimized === 'boolean' && startMinimized) { this.state.mainWindow?.hide(); this.state.mainWindow?.setSkipTaskbar(true); } else { this.state.mainWindow?.show(); this.state.mainWindow?.setSkipTaskbar(false); } this.openDevTools(); }); this.state.mainWindow.on('close', this.handleWindowClose.bind(this)); this.state.mainWindow.on('closed', async () => { this.state.mainWindow = null; }); this.state.mainWindow.on('leave-full-screen', async () => { this.state.mainWindow?.setSize(WINDOW_DIMENSIONS.width, WINDOW_DIMENSIONS.height); this.state.mainWindow?.center(); }); (this.state.mainWindow as any).on('minimize', async (e: Event) => { e.preventDefault(); //this.state.mainWindow?.setSkipTaskbar(false); }); this.state.mainWindow.on('focus', () => this.registerQuitShortcut()); this.state.mainWindow.on('blur', () => this.unregisterQuitShortcut()); this.state.mainWindow.webContents.setWindowOpenHandler((e) => { shell.openExternal(e.url); return { action: 'deny' }; }); } private async exitProcess() { try { this.state.connectionStatus = 'disconnecting'; this.state.mainWindow?.hide(); this.state.appIcon?.destroy(); this.state.appIcon = null; await exitTheApp(); this.state.connectionStatus = 'disconnected'; app.quit(); } catch (error) { log.error('Error while exiting the app:', error); app.exit(1); } } private async handleWindowClose(e: Event): Promise { e.preventDefault(); const forceClose = await settings.get('forceClose'); if (typeof forceClose === 'boolean' && forceClose) { try { this.exitProcess(); } catch (err) { log.error('Error while exiting the app:', err); } } else { this.state.mainWindow?.hide(); /*if (isDarwin) { app?.dock?.hide(); }*/ } } private registerQuitShortcut(): void { const shortcut = isDarwin ? 'CommandOrControl+Q' : 'Ctrl+Q'; globalShortcut.register(shortcut, async () => { this.exitProcess(); }); } private unregisterQuitShortcut(): void { const shortcut = isDarwin ? 'CommandOrControl+Q' : 'Ctrl+Q'; globalShortcut.unregister(shortcut); } private async checkForUpdates(downloadUpdate?: boolean) { if (isDev()) return; if (this.state.isCheckingForUpdates) return; try { this.state.isCheckingForUpdates = true; const betaRelease = await settings.get('betaRelease'); const isBetaVersionChecking = typeof betaRelease == 'undefined' ? defaultSettings.betaRelease : betaRelease; const response = await fetch( `https://api.github.com/repos/${packageJsonData.build.publish.owner}/${packageJsonData.build.publish.repo}/releases${isBetaVersionChecking ? '?per_page=1' : '/latest'}` ); if (response.ok) { const data = await response.json(); const latestVersion = String( isBetaVersionChecking ? data?.[0]?.tag_name : data?.tag_name ); const hasNewUpdate = latestVersion != null && versionComparison(String(packageJsonData?.version), latestVersion); const hasNewUpdateState = hasNewUpdate != this.state.hasNewUpdate; this.state.hasNewUpdate = hasNewUpdate; if (hasNewUpdateState) { this.resetCheckForUpdatesInterval(); customEvent.emit('tray-icon', this.state.connectionStatus); } if (this.state.hasNewUpdate) { if (hasNewUpdateState) this.state.updateNotification?.show(); if (!downloadUpdate) return; if (!isWindows) { shell.openExternal( `https://github.com/${packageJsonData.build.publish.owner}/${packageJsonData.build.publish.repo}/releases/${latestVersion}#download` ); return; } try { const result: any = await dialog.showMessageBox({ type: 'question', title: APP_TITLE, buttons: [this.state.appLang.modal.no, this.state.appLang.modal.yes], defaultId: 0, message: this.state.appLang.toast.new_update }); if (result.response === 1) { const launchUpdater = (filePath: string) => { setTimeout(() => { const options = { name: 'Oblivion Desktop' }; sudo.exec(`"${filePath}"`, options, (error, stdout, stderr) => { if (error) { if ( error.message.includes( 'User did not grant permission' ) ) { log.warn('⚠️ UAC prompt not shown or denied.'); shell.openExternal( `https://github.com/${packageJsonData.build.publish.owner}/${packageJsonData.build.publish.repo}/releases/latest#download` ); } else { log.error('⚠️ Updater failed:', error); } return; } log.info('✅ Updater executed successfully with sudo.'); this.exitProcess(); }); }, 2500); }; if (fs.existsSync(updaterPath)) { const updaterVersion = await settings.get('updaterVersion'); if (updaterVersion === latestVersion) { launchUpdater(updaterPath); return; } } this.redirectTo('/'); this.state.mainWindow?.setProgressBar(0); await this.downloadUpdate( `https://github.com/${packageJsonData.build.publish.owner}/${packageJsonData.build.publish.repo}/releases/download/${latestVersion}/${packageJsonData.name}-${isWindows ? 'win' : ''}-${process.arch}.exe`, (percent) => { log.info(`Download: ${percent}%`); this.state.mainWindow?.setProgressBar(percent / 100); }, async () => { log.info('Download completed!'); this.state.mainWindow?.setProgressBar(-1); fs.copyFile(downloadedPath, updaterPath, (copyErr) => { if (copyErr) { log.error('⚠️ Failed to copy updater file:', copyErr); return; } log.info(`✅ Updater copied successfully: ${updaterPath}`); try { fs.chmodSync(updaterPath, 0o755); log.info( '✅ Executable permissions applied to updater.' ); } catch (chmodErr) { log.warn( '⚠️ Failed to set executable permissions:', chmodErr ); } fs.rm(downloadedPath, { force: true }, (unlinkErr) => { if (unlinkErr) { log.warn( '⚠️ Could not delete old updater file:', unlinkErr ); } else { log.info('✅ Old updater file deleted.'); } settings.setSync('updaterVersion', latestVersion); launchUpdater(updaterPath); }); }); } ); } } catch (error) { log.error('Failure in update process:', error); } } else if (downloadUpdate) { await dialog.showMessageBox({ type: 'info', title: APP_TITLE, defaultId: 0, message: this.state.appLang.toast.up_to_date }); } } else { console.log('Failed to fetch release version:', response.statusText); return false; } } catch (error) { console.log('Failed to fetch release version:', error); return false; } finally { this.state.isCheckingForUpdates = false; this.state.mainWindow?.webContents.send('new-update', this.state.hasNewUpdate); } } private resetCheckForUpdatesInterval(): void { clearInterval(this.state.checkForUpdatesIntervalId); this.state.checkForUpdatesIntervalId = undefined; if (this.state.hasNewUpdate) return; this.state.checkForUpdatesIntervalId = setInterval( this.checkForUpdates, 3 * 60 * 60 * 1_000 ); } private setupCheckForUpdates(): void { this.state.updateNotification = new Notification({ title: APP_TITLE, body: this.state.appLang.toast.new_update_notification, icon: this.getAssetPath('oblivion.png') }); this.state.updateNotification.on('click', () => { this.checkForUpdates(true); }); this.resetCheckForUpdatesInterval(); } private setupIpcEvents(): void { ipcMain.on('tray-menu', (event) => { try { this.state.trayMenuEvent = event; } catch (err) { log.error('Error handling tray-menu event:', err); } }); ipcMain.on('localization', async (_, newLang) => { this.state.userLang = newLang; this.state.appLang = getTranslate(this.state.userLang); this.updateTrayMenu(); this.state.appIcon?.focus(); }); ipcMain.on('tray-state', async (event, value: any) => { if (!this.state.mainWindow) return; if ('proxyMode' in value) { this.state.proxyMode = value.proxyMode; } this.updateTrayMenu(); }); ipcMain.on('startup', async (_, newStatus) => { if (!isDev()) { app.setLoginItemSettings({ openAtLogin: newStatus }); } }); ipcMain.on('open-devtools', async () => { this.state.mainWindow?.webContents.openDevTools(); }); customEvent.on('tray-icon', (newStatus: string) => { if (!this.state.appIcon) return; if (newStatus.startsWith('connected') || newStatus === 'disconnected') { const newIcon = this.createTrayIcon(newStatus); this.state.appIcon.setImage(newIcon); } this.state.connectionStatus = newStatus; this.updateTrayMenu(); }); ipcMain.on('check-update', async (_event, downloadUpdate?: boolean) => this.checkForUpdates(downloadUpdate) ); } private async downloadUpdate( url: string, onProgress: (percent: number) => void, onDone: () => void ) { const file = fs.createWriteStream(downloadedPath); let receivedBytes = 0; let totalBytes = 0; let lastUpdateTime = Date.now(); const request = https.get(url, (response) => { if ( response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location ) { log.info(`Redirecting to: ${response.headers.location}`); return this.downloadUpdate(response.headers.location, onProgress, onDone); } totalBytes = parseInt(response.headers['content-length'] || '0', 10); if (!totalBytes) { log.warn('Warning: Content-Length header is missing or zero.'); return; } response.pipe(file); response.on('data', (chunk) => { receivedBytes += chunk.length; const percent = totalBytes ? ((receivedBytes / totalBytes) * 100).toFixed(2) : '0'; if (Date.now() - lastUpdateTime >= 1500) { lastUpdateTime = Date.now(); onProgress(Number(percent)); this.state.mainWindow?.webContents.send('download-progress', { status: 'pending', percent: Number(percent) }); } }); file.on('finish', () => { log.info('File stream finished, closing...'); //this.onDownloadError(); if (file && typeof file.close === 'function') { file.close(); } else { log.warn( 'Tried to close file, but file is undefined or close is not a function' ); } }); file.on('close', () => { fs.stat(downloadedPath, (err, stats) => { if (err) { log.error('File stat error:', err); this.onDownloadError(); return; } if (stats.size === 0) { log.error('Download failed: File size is 0 bytes.'); this.onDownloadError(); return; } log.info(`File successfully written to disk: ${downloadedPath}`); setTimeout(() => { onDone(); }, 2500); }); }); response.on('error', (err) => { log.error('Download error:', err); this.onDownloadError(); }); file.on('error', (err) => { log.error('File write error:', err); this.onDownloadError(); }); }); request.on('error', (err) => { log.error('Request error:', err); this.onDownloadError(); }); request.end(); } private onDownloadError() { this.state.mainWindow?.webContents.send('download-progress', { status: 'error', percent: 0 }); this.state.mainWindow?.setProgressBar(0); } private setupAppEvents(): void { app.on('second-instance', () => { if (this.state.mainWindow) { if (this.state.mainWindow.isMinimized()) { this.state.mainWindow.restore(); } this.state.mainWindow.focus(); } }); app.on('activate', () => { // On macOS, it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (this.state.mainWindow === null) this.createWindow().catch((err) => log.error('Create Window Error:', err)); }); let isQuitting = false; app.on('window-all-closed', async () => { if (!isQuitting) { isQuitting = true; await this.exitProcess(); app.quit(); } }); process.on('SIGTERM', async () => { if (!isQuitting) { isQuitting = true; await this.exitProcess(); app.quit(); } }); app.on('before-quit', async (event) => { if (!isQuitting) { event.preventDefault(); isQuitting = true; await this.exitProcess(); app.quit(); } }); app.on('will-quit', async (event) => { /*event.preventDefault(); try { await this.exitProcess(); } catch (error) { console.error('Error during cleanup:', error); } app.exit(0);*/ }); /*app.on('session-end', async () => { await this.disableProxyQuickly(); }); app.on('quit', async () => { await this.disableProxyQuickly(); });*/ app.setAsDefaultProtocolClient(packageJsonData.shortName); app.on('open-url', (event: Event) => { event.preventDefault(); if (this.state.mainWindow) { this.state.mainWindow.show(); } }); } private async setupTray(): Promise { try { this.state.userLang = String((await settings.get('lang')) || defaultSettings.lang); this.state.appLang = getTranslate(this.state.userLang); this.state.connectionStatus = 'disconnected'; const trayIcon = this.createTrayIcon('disconnected'); this.state.appIcon = new Tray(trayIcon); this.state.appIcon.setToolTip(APP_TITLE); this.state.appIcon.on('click', () => { this.redirectTo(''); }); /*this.state.appIcon.on('right-click', () => { this.state.appIcon?.popUpContextMenu(); });*/ this.updateTrayMenu(); } catch (err) { log.error('Error setting up tray:', err); } } private createTrayIcon(status: string): NativeImage { const iconPath = this.getAssetPath( `img/status/${this.state.hasNewUpdate ? 'badge/' : ''}${status}.png` ); const icon = nativeImage.createFromPath(iconPath); if (icon.isEmpty()) log.error(`Failed to load trayIcon: ${iconPath}`); return !isWindows ? icon.resize({ width: 16, height: 16 }) : icon; } private updateTrayMenu(): void { if (!this.state.appIcon) return; if (this.state.appIcon.isDestroyed()) return; try { const template = this.createTrayMenuTemplate(); this.state.appIcon.setContextMenu(Menu.buildFromTemplate(template)); } catch (err) { log.error('Error updating tray menu:', err); } } private createTrayMenuTemplate(): MenuItemConstructorOptions[] { const connectLabel = this.getConnectionLabel(); const canToggleConnection = !( this.state.connectionStatus === 'disconnecting' || this.state.connectionStatus === 'connecting' ); const changeProxyMode = (value: string) => { if (!this.state.mainWindow) return; this.state.mainWindow.webContents.send('change-proxy-mode', value); //this.state.proxyMode = value; //this.updateTrayMenu(); this.redirectTo('/network'); }; const proxyModeLabel = (mode: string): string => { const labels: Record = { system: 'System proxy', tun: 'Tun' }; const label = labels[mode] ?? 'None'; const prefix = this.state.proxyMode === mode ? '✓ ' : ' '; return `${prefix}${label}`; }; return [ { label: APP_TITLE, type: 'normal', click: () => this.redirectTo('/') }, { type: 'separator' }, { id: 'connectToggle', label: connectLabel, type: 'normal', enabled: canToggleConnection, click: () => { this.handleConnectionToggle(); this.state.connectionStatus = this.state.connectionStatus === 'disconnected' ? 'connecting' : 'disconnecting'; this.updateTrayMenu(); } }, { id: 'proxyMode', label: this.state.appLang.settings.proxy_mode, enabled: this.state.connectionStatus === 'disconnected', submenu: [ { label: proxyModeLabel('none'), type: 'normal', click: () => changeProxyMode('none') }, { label: proxyModeLabel('system'), type: 'normal', click: () => changeProxyMode('system') }, { label: proxyModeLabel('tun'), type: 'normal', click: () => changeProxyMode('tun') } ] }, { label: '', type: 'separator' }, { label: this.state.appLang.systemTray.settings, submenu: [ { label: this.state.appLang.systemTray.settings_warp, type: 'normal', click: () => { this.redirectTo('/settings'); } }, { label: this.state.appLang.systemTray.settings_network, type: 'normal', click: () => { this.redirectTo('/network'); } }, { label: this.state.appLang.systemTray.settings_scanner, type: 'normal', click: () => { this.redirectTo('/scanner'); } }, { label: this.state.appLang.systemTray.settings_app, type: 'normal', click: () => { this.redirectTo('/options'); } } ] }, { label: '', type: 'separator' }, { label: this.state.appLang.systemTray.speed_test, type: 'normal', click: () => { this.redirectTo('/speed'); } }, { label: this.state.appLang.systemTray.about, type: 'normal', click: () => { this.redirectTo('/about'); } }, { label: this.state.appLang.systemTray.log, type: 'normal', click: () => { this.redirectTo('/debug'); } }, { label: '', type: 'separator' }, { label: this.state.appLang.systemTray.exit, click: () => { this.exitProcess(); } } ]; } private getConnectionLabel(): string { const connectionStatus = this.state.connectionStatus.split('-')[0]; const labels = { connected: `✓ ${this.state.appLang.systemTray.connected}`, disconnected: this.state.appLang.systemTray.connect, connecting: this.state.appLang.systemTray.connecting, disconnecting: this.state.appLang.systemTray.disconnecting }; return labels[connectionStatus as keyof typeof labels] || labels.disconnected; } private handleConnectionToggle(): void { const isConnected = this.state.connectionStatus.startsWith('connected'); const event = isConnected ? 'disconnect' : 'connect'; this.state.trayMenuEvent?.reply('tray-menu', { key: event, msg: 'Connect Tray Click!' }); this.redirectTo('/'); } private redirectTo(route: string): void { if (!this.state.mainWindow) { this.createWindow().catch((err) => log.error('Create Window Error:', err)); } else { this.state.mainWindow.show(); this.state.mainWindow?.setSkipTaskbar(false); /*if (isDarwin) { app?.dock?.show(); }*/ if (route) { this.state.trayMenuEvent?.reply('tray-menu', { key: 'changePage', msg: route }); } } } private async checkStartUp(): Promise { if (isDev()) return; const checkOpenAtLogin = await settings.get('openAtLogin'); const loginItemSettings = app.getLoginItemSettings(); if ( typeof checkOpenAtLogin === 'boolean' && checkOpenAtLogin && !loginItemSettings.openAtLogin ) { app.setLoginItemSettings({ openAtLogin: true }); } } private async autoConnect(): Promise { if (isDev()) return; const checkAutoConnect = await settings.get('autoConnect'); if (typeof checkAutoConnect === 'boolean' && checkAutoConnect) { this.state.trayMenuEvent?.reply('tray-menu', { key: 'connect', msg: 'Connect Tray Click!' }); } } private async setupMetaData(): Promise { const osInfo = await getOsInfo(); logMetadata(osInfo); } /*private async exitStrategy(): Promise { try { if (isWindows) { const { systemEvents } = require('electron'); systemEvents.on('session-end', async () => { return new Promise((resolve) => { setTimeout(() => { this.exitProcess(); resolve(); }, 1000); }); }); } } catch (error) { log.error('Error setting up shutdown handlers:', error); } }*/ /*private async disableProxyQuickly(): Promise { if (!isWindows) return; try { const registryPath = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'; const proxySettings: RegistryPutItem = { ProxyServer: { type: 'REG_SZ', value: '' }, ProxyOverride: { type: 'REG_SZ', value: '' }, AutoConfigURL: { type: 'REG_SZ', value: '' }, ProxyEnable: { type: 'REG_DWORD', value: 0 } }; regeditModule.setExternalVBSLocation(regeditVbsDirPath); await regedit.putValue({ [registryPath]: proxySettings }); } catch (error) { log.error(`Error while disabling system proxy: ${error}`); } }*/ private async getNetworkList() { const netData = await networkInterfaces(); const interfaces = Array.isArray(netData) ? netData : netData ? [netData] : []; const getList = interfaces .filter((i) => i.ip4 && !i.internal && !i.ip4.startsWith('169.254.')) .map((i) => i.ip4); await settings.setSync('networkList', JSON.stringify(getList)); return getList; } private sleep(ms: number): Promise { // eslint-disable-next-line no-promise-executor-return return new Promise((resolve) => setTimeout(resolve, ms)); } public async handleAppReady(): Promise { app.whenReady().then(async () => { try { await this.createWindow(); if (this.state.isFirstRun) { this.state.isFirstRun = false; app.relaunch(); await this.sleep(1500); app.exit(0); return; } await this.setupTray(); await this.checkStartUp(); await this.autoConnect(); await this.setupMetaData(); await this.getNetworkList(); //await this.exitStrategy(); log.info('od is ready!'); } catch (error) { log.error('Error during app ready handling:', error); } }); } } const oblivionDesktop = new OblivionDesktop(); oblivionDesktop.handleAppReady().catch((error) => { log.error('Failed to start application:', error); process.exit(1); }); ================================================ FILE: src/main/menu.ts ================================================ import { Menu, shell, BrowserWindow } from 'electron'; import { exitTheApp } from './lib/utils'; //import { regeditVbsDirPath } from '../constants'; export default class MenuBuilder { mainWindow: BrowserWindow; constructor(mainWindow: BrowserWindow) { this.mainWindow = mainWindow; } buildMenu(): Menu { if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { this.setupDevelopmentEnvironment(); } const template = this.buildDefaultTemplate(); const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); return menu; } setupDevelopmentEnvironment(): void { this.mainWindow.webContents.on('context-menu', (_, props) => { const { x, y } = props; Menu.buildFromTemplate([ { label: 'Inspect element', click: () => { this.mainWindow.webContents.inspectElement(x, y); } } ]).popup({ window: this.mainWindow }); }); } buildDefaultTemplate() { const templateDefault = [ { // TODO locale label: '&File', submenu: [ { // TODO locale label: '&Exit', accelerator: 'Ctrl+Q', click: async () => { await exitTheApp(); } } ] }, { // TODO locale label: 'Help', submenu: [ { // TODO locale label: 'Learn More', click() { shell.openExternal('https://github.com/bepass-org/oblivion-desktop'); } } ] } ]; return templateDefault; } } ================================================ FILE: src/main/playground.ts ================================================ import { isDev } from './lib/utils'; export const devPlayground = () => { if (!isDev()) return; console.log('-----------------------------------'); // // serving pac script file // const servePacScript = (port: number) => { // detectPort(port) // .then((_port) => { // if (port === _port) { // // console.log(`port: ${port} was not occupied`); // const pacPath = path.join(app.getPath('userData'), 'pac'); // const server = http.createServer((request, response) => { // return handler(request, response, { // public: pacPath // }); // }); // server.listen(port, () => { // log.info(`Serving static files(pac script) at http://{host}:${port}`); // }); // // server.close() // } else { // log.info(`port: ${port} was occupied, trying port: ${_port}`); // servePacScript(_port); // } // }) // .catch((err) => { // log.error(err); // }); // }; // servePacScript(8087); // regedit async function main() { // const listResult = await regedit.list(['HKCU\\SOFTWARE']); // console.log(listResult); // await regedit.createKey(['HKLM\\SOFTWARE\\MyApp2', 'HKCU\\SOFTWARE\\MyApp']); // await regedit.putValue({ // 'HKCU\\SOFTWARE\\MyApp': { // Company: { // value: 'Moo corp', // type: 'REG_SZ' // }, // Name: { // type: 'REG_SZ', // value: 'mmd' // } // }, // 'HKLM\\SOFTWARE\\MyApp2': { // test: { // value: '123', // type: 'REG_SZ' // } // } // }); } main(); console.log('-----------------------------------'); }; ================================================ FILE: src/main/preload.ts ================================================ import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; export type Channels = | 'ipc-example' | 'open-devtools' | 'wp-start' | 'wp-end' | 'get-logs' | 'settings' | 'guide-toast' | 'tray-icon' | 'tray-menu' | 'localization' | 'startup' | 'net-stats' | 'speed-test' | 'process-url' | 'new-update' | 'check-update' | 'download-progress' | 'local-ips' | 'change-proxy-mode' | 'tray-state'; const electronHandler = { ipcRenderer: { sendMessage(channel: Channels, ...args: any[]) { ipcRenderer.send(channel, ...args); }, on( channel: Channels, func: (...args: any[]) => void ): (_event: Electron.IpcRendererEvent, ...args: any[]) => void { const subscription = (_event: IpcRendererEvent, ...args: any[]) => func(...args); ipcRenderer.on(channel, subscription); return subscription; }, once( channel: Channels, func: (...args: any[]) => void ): (_event: Electron.IpcRendererEvent, ...args: any[]) => void { const subscription = (_event: IpcRendererEvent, ...args: any[]) => func(...args); ipcRenderer.once(channel, subscription); return subscription; }, removeListener(channel: Channels, func: (...args: any[]) => void) { ipcRenderer.removeListener(channel, func); }, removeAllListeners(channel: Channels) { ipcRenderer.removeAllListeners(channel); }, invoke(channel: Channels, ...args: any[]) { return ipcRenderer.invoke(channel, ...args); }, clean() { ipcRenderer.removeAllListeners('settings'); ipcRenderer.removeAllListeners('guide-toast'); ipcRenderer.removeAllListeners('tray-menu'); ipcRenderer.removeAllListeners('wp-start'); ipcRenderer.removeAllListeners('wp-end'); ipcRenderer.removeAllListeners('net-stats'); } }, NODE_ENV: process.env.NODE_ENV, platform: process.platform, username: process.env.USER || process.env.USERNAME || null, arch: process.arch }; //ipcRenderer.setMaxListeners(20); contextBridge.exposeInMainWorld('electron', electronHandler); contextBridge.exposeInMainWorld('platformAPI', { getPlatform: () => process.platform }); export type ElectronHandler = typeof electronHandler; ================================================ FILE: src/renderer/App.tsx ================================================ import { MemoryRouter as Router } from 'react-router'; import { useEffect } from 'react'; import 'assets/css/bootstrap.min.css'; import 'assets/css/bootstrap-rtl.min.css'; import 'assets/css/shabnam.css'; import 'assets/css/materialIcons.css'; import 'assets/css/noto.css'; import 'assets/css/style.css'; import SplashScreen from './pages/SplashScreen'; import { loadLang, loadTheme, loadSettings } from './lib/loaders'; import { getIspName } from './lib/getIspName'; import AppRoutes from './routes'; export default function App() { useEffect(() => { loadTheme(); loadLang(); loadSettings(); getIspName(); }, []); return ( <> ); } ================================================ FILE: src/renderer/components/BackButton.tsx ================================================ import { Link } from 'react-router'; import classNames from 'classnames'; export default function BackButton() { return ( ); } ================================================ FILE: src/renderer/components/Card/index.tsx ================================================ import { FC, memo } from 'react'; interface ResultCardProps { label: string; value: string; unit: string; } const ResultCard: FC = memo(({ label, value, unit }: ResultCardProps) => (

{label}

{value} {unit}

)); export default ResultCard; ================================================ FILE: src/renderer/components/ConfigHandler.tsx ================================================ import { FC, useEffect } from 'react'; import { saveConfig } from '../lib/inputSanitizer'; import { Language } from '../../localization/type'; interface ConfigHandlerProps { isConnected: boolean; isLoading: boolean; appLang: Language; } const ConfigHandler: FC = ({ isConnected, isLoading, appLang }) => { useEffect(() => { const handlePaste = (event: ClipboardEvent) => { event.preventDefault(); const pastedText = event.clipboardData?.getData('Text') || ''; saveConfig(pastedText, isConnected, isLoading, appLang); setTimeout(async () => { try { await navigator.clipboard?.writeText(''); } catch (err) { // } }, 200); }; window.addEventListener('paste', handlePaste); return () => { window.removeEventListener('paste', handlePaste); }; }, [isConnected, isLoading, appLang]); return null; }; export default ConfigHandler; ================================================ FILE: src/renderer/components/Dropdown/index.tsx ================================================ import { ChangeEvent, FC } from 'react'; export type DropdownItem = { label: string; value: string; }; interface DropdownProps { label?: string; id: string; items: DropdownItem[]; value: string; onChange: (event: ChangeEvent) => void; disabled?: boolean; tabIndex?: number; isLoading?: boolean; } const Dropdown: FC = ({ id, items, onChange, value, label, disabled, tabIndex = 0, isLoading = false }) => { return ( <> {label && ( )}
{isLoading &&
}
); }; export default Dropdown; ================================================ FILE: src/renderer/components/Input/index.tsx ================================================ import React, { ChangeEvent, FC } from 'react'; import useInput from './useInput'; interface InputProps { id?: string; value: string; onChange: (event: ChangeEvent) => void; tabIndex?: number; type?: string; placeholder?: string; } const Input: FC = ({ id, onChange, value, tabIndex = 0, type = 'text', placeholder = '' }) => { const { contextMenuStyle, handleCloseContextMenu, handleContextMenu, handleCopy, handleCut, handlePaste, inputRef } = useInput(value, onChange); return ( <> {contextMenuStyle && (
Copy
Cut
Paste
)} ); }; export default Input; ================================================ FILE: src/renderer/components/Input/useInput.ts ================================================ import React, { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react'; type ContextMenuStyleType = { left: number; top: number; }; const useInput = (value: string, onChange: (event: ChangeEvent) => void) => { const [contextMenuStyle, setContextMenuStyle] = useState(null); const inputRef = useRef(null); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (inputRef.current && !inputRef.current.contains(event.target as Node)) { setContextMenuStyle(null); } }; document.addEventListener('click', handleClickOutside); return () => { document.removeEventListener('click', handleClickOutside); }; }, []); const handleContextMenu = useCallback((event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); /*const inputElement = event.currentTarget; inputElement.select();*/ const positionX = event.clientX; const positionY = event.clientY; const menuWidth = 100; const menuHeight = 100; const maxX = window.innerWidth - menuWidth; const parent = inputRef.current?.offsetParent as HTMLElement; const parentHeight = parent?.clientHeight ?? 0; const maxY = parentHeight + 15 - menuHeight; const left = Math.min(positionX, maxX); const top = Math.min(positionY, maxY); setContextMenuStyle({ left, top }); }, []); const handleCopy = useCallback(async () => { try { const selectedText = inputRef.current?.value.substring( inputRef.current?.selectionStart || 0, inputRef.current?.selectionEnd || 0 ); if (selectedText) { await navigator.clipboard.writeText(selectedText); } } catch (err) { console.error('Failed to copy:', err); } setContextMenuStyle(null); }, [inputRef]); const handleCut = useCallback(async () => { try { const selectionStart = inputRef.current?.selectionStart || 0; const selectionEnd = inputRef.current?.selectionEnd || 0; const selectedText = inputRef.current?.value.substring(selectionStart, selectionEnd); if (selectedText) { await navigator.clipboard.writeText(selectedText); const newValue = value.substring(0, selectionStart) + value.substring(selectionEnd); onChange({ target: { value: newValue } } as ChangeEvent); } } catch (err) { console.error('Failed to cut:', err); } setContextMenuStyle(null); }, [value, onChange, inputRef]); const handlePaste = useCallback(async () => { try { const text = await navigator.clipboard.readText(); const selectionStart = inputRef.current?.selectionStart || 0; const selectionEnd = inputRef.current?.selectionEnd || 0; const newValue = value.substring(0, selectionStart) + text + value.substring(selectionEnd); onChange({ target: { value: newValue } } as ChangeEvent); } catch (err) { console.error('Failed to paste:', err); } setContextMenuStyle(null); }, [value, onChange, inputRef]); const handleCloseContextMenu = useCallback(() => { setContextMenuStyle(null); }, []); return { inputRef, contextMenuStyle, handleContextMenu, handleCopy, handleCut, handlePaste, handleCloseContextMenu }; }; export default useInput; ================================================ FILE: src/renderer/components/Modal/DNS/index.tsx ================================================ import classNames from 'classnames'; import useDnsModal from './useDnsModal'; import Input from '../../Input'; interface DnsModalProps { title: string; isOpen: boolean; plainDns: string; DoH: string; onClose: () => void; setDefaultDns: () => void; cleanDns: () => void; setCustomDns: (plainDns: string, doh: string) => void; } export default function DnsModal({ title, isOpen, plainDns, DoH, onClose, setDefaultDns, cleanDns, setCustomDns }: DnsModalProps) { const { appLang, plainDnsInput, dohInput, showModal, handleCancelButtonClick, handleCancelButtonKeyDown, handlePlainDnsInputChange, handleDoHInputChange, handleOnClose, onSaveModalClick, onSaveModalKeyDown, handleClearInputs, handleSetDefault } = useDnsModal({ isOpen, plainDns, DoH, onClose, setDefaultDns, cleanDns, setCustomDns }); if (!isOpen) return <>; return (

{title}
{appLang?.modal?.form_default}
{appLang?.modal?.form_clear}

{appLang?.modal?.cancel}
{} : onSaveModalClick } tabIndex={0} role='button' onKeyDown={onSaveModalKeyDown} > {appLang?.modal?.update}
); } ================================================ FILE: src/renderer/components/Modal/DNS/useDnsModal.ts ================================================ import { ChangeEvent, useCallback, useEffect, useState } from 'react'; import useTranslate from '../../../../localization/useTranslate'; import useButtonKeyDown from '../../../hooks/useButtonKeyDown'; interface DnsModalProps { isOpen: boolean; plainDns: string; DoH: string; onClose: () => void; setDefaultDns: () => void; cleanDns: () => void; setCustomDns: (plainDns: string, doh: string) => void; } const useDnsModal = (props: DnsModalProps) => { const { isOpen, plainDns, DoH, onClose, setDefaultDns, cleanDns, setCustomDns } = props; const [showModal, setShowModal] = useState(isOpen); const [plainDnsInput, setPlainDnsInput] = useState(plainDns); const [dohInput, setDohInput] = useState(DoH); const appLang = useTranslate(); useEffect(() => setShowModal(isOpen), [isOpen]); const handleOnClose = useCallback(() => { setShowModal(false); setTimeout(onClose, 300); }, [setShowModal, onClose]); const onSaveModalClick = useCallback(() => { const cleanedPlain = plainDnsInput ?.replace(/https?:\/\//gi, '') ?.split('/')[0] ?.replace(/\//g, ''); let fixedDoh = dohInput.trim(); if (!/^https?:\/\//i.test(fixedDoh)) { fixedDoh = `https://${fixedDoh}`; } fixedDoh = fixedDoh?.replace(/\/+$/, ''); setPlainDnsInput(cleanedPlain); setDohInput(fixedDoh); setCustomDns(cleanedPlain, fixedDoh); handleOnClose(); }, [plainDnsInput, dohInput, plainDns, DoH, setCustomDns, handleOnClose]); const onSaveModalKeyDown = useButtonKeyDown(onSaveModalClick); const handleClearInputs = useCallback(() => { setPlainDnsInput(''); setDohInput(''); cleanDns(); handleOnClose(); }, []); const handleSetDefault = useCallback(() => { setDefaultDns(); handleOnClose(); }, []); const handleCancelButtonClick = useCallback(() => { setPlainDnsInput(plainDns); setDohInput(DoH); handleOnClose(); }, [handleOnClose, plainDns, DoH]); const handleCancelButtonKeyDown = useButtonKeyDown(handleCancelButtonClick); const handlePlainDnsInputChange = useCallback( (e: ChangeEvent) => { setPlainDnsInput(e.target.value.trim()); }, [setPlainDnsInput] ); const handleDoHInputChange = useCallback( (e: ChangeEvent) => { setDohInput(e.target.value.trim()); }, [setDohInput] ); return { appLang, plainDnsInput, dohInput, showModal, handleCancelButtonClick, handleCancelButtonKeyDown, handlePlainDnsInputChange, handleDoHInputChange, handleOnClose, onSaveModalClick, onSaveModalKeyDown, handleClearInputs, handleSetDefault }; }; export default useDnsModal; ================================================ FILE: src/renderer/components/Modal/Endpoint/index.tsx ================================================ import classNames from 'classnames'; import { FC } from 'react'; import { defaultSettings } from '../../../../defaultSettings'; import useEndpointModal from './useEndpointModal'; import Input from '../../Input'; import { Profile } from '../../../pages/Scanner/useScanner'; interface EndpointModalProps { title: string; isOpen: boolean; onClose: () => void; defValue?: string; endpoint: string; profiles: Profile[]; setEndpoint: (value: string) => void; } const EndpointModal: FC = ({ title, isOpen, onClose, defValue = defaultSettings.endpoint, endpoint, setEndpoint, profiles }) => { const { appLang, endpointInput, handleCancelButtonClick, handleCancelButtonKeyDown, handleEndpointInputChange, handleOnClose, onSaveModal, onUpdateKeyDown, setEndpointDefault, setEndpointSuggestion, setShowSuggestion, fetchEndpoints, updaterRef, showModal, scanResult, showSuggestion, suggestion, suggestionRef, method } = useEndpointModal({ isOpen, onClose, defValue, endpoint, setEndpoint, profiles }); if (!isOpen) return <>; return (

{title}
{scanResult && !suggestion[method]?.ipv4.includes(scanResult) && !suggestion[method]?.ipv6.includes(scanResult) && ( <>
{ setEndpointSuggestion(scanResult); }} > {appLang?.modal?.endpoint_latest}
)}
{ setShowSuggestion((pre) => !pre); }} ref={suggestionRef} > {appLang?.modal?.endpoint_suggested}
0 && suggestion[method].ipv6.length > 0 ? 'splitter' : '' )} data-list={profiles.length > 0 ? 3 : 2} >
{[...(suggestion[method]?.ipv4 ?? [])] .sort((a, b) => b.localeCompare(a)) .slice(0, 25) .map((item, index) => (
setEndpointSuggestion(item)} > #{index + 1} IPv4
))}{' '}
{[...(suggestion[method]?.ipv6 ?? [])] .sort((a, b) => b.localeCompare(a)) .slice(0, 15) .map((item, index) => (
setEndpointSuggestion(item)} > #{index + 1} IPv6
))}
{profiles?.length > 0 && (
{profiles.map( (item: Profile, key: number) => typeof item.endpoint === 'string' && item.endpoint.length > 7 && ( <>
{ setEndpointSuggestion( item.endpoint ); //setShowSuggestion(false); }} > {item.name}
) )}
)}
{appLang?.modal?.form_default}

{appLang?.modal?.cancel}
{appLang?.modal?.update}
fetchEndpoints(true)} role='button' ref={updaterRef} tabIndex={0} >
); }; export default EndpointModal; ================================================ FILE: src/renderer/components/Modal/Endpoint/useEndpointModal.ts ================================================ import { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useStore } from '../../../store'; import { settings } from '../../../lib/settings'; import useTranslate from '../../../../localization/useTranslate'; import { defaultSettings } from '../../../../defaultSettings'; import { loadingToast, settingsHaveChangedToast, stopLoadingToast } from '../../../lib/toasts'; import { Profile } from '../../../pages/Scanner/useScanner'; import useButtonKeyDown from '../../../hooks/useButtonKeyDown'; import { withDefault } from '../../../lib/withDefault'; type EndpointModalProps = { isOpen: boolean; onClose: () => void; defValue: string; endpoint: string; setEndpoint: (value: string) => void; profiles: Profile[]; }; type Method = keyof Suggestion; type Suggestion = { warp: { ipv4: string[]; ipv6: string[] }; masque: { ipv4: string[]; ipv6: string[] }; }; const useEndpointModal = (props: EndpointModalProps) => { const { isConnected, isLoading } = useStore(); const appLang = useTranslate(); const { endpoint, isOpen, onClose, setEndpoint, defValue } = props; const suggestionRef = useRef(null); const updaterRef = useRef(null); const [endpointInput, setEndpointInput] = useState(endpoint); const [showModal, setShowModal] = useState(isOpen); const [showSuggestion, setShowSuggestion] = useState(false); const [scanResult, setScanResult] = useState(''); const [method, setMethod] = useState('warp'); const removeDuplicates = (endpoints: Suggestion): Suggestion => { return { warp: { ipv4: Array.from(new Set(endpoints.warp?.ipv4 ?? [])), ipv6: Array.from(new Set(endpoints.warp?.ipv6 ?? [])) }, masque: { ipv4: Array.from(new Set(endpoints.masque?.ipv4 ?? [])), ipv6: Array.from(new Set(endpoints.masque?.ipv6 ?? [])) } }; }; const defEndpoint = { warp: { ipv4: [ '162.159.195.1:500', '162.159.195.1:1701', '162.159.195.1:2408', '162.159.195.1:4500', '162.159.193.3:500', '162.159.193.3:1701', '162.159.193.3:2408', '162.159.193.3:4500', '162.159.192.1:500', '162.159.192.1:1701', '162.159.192.1:2408', '162.159.192.1:4500' ], ipv6: [ '[2606:4700:d0::a29f:c001]:500', '[2606:4700:d0::a29f:c001]:1701', '[2606:4700:d0::a29f:c001]:4500', '[2606:4700:d0::a29f:c001]:2408' ] }, masque: { ipv4: ['162.159.198.1:443', '162.159.198.2:443'], ipv6: ['2606:4700:103::1', '2606:4700:103::2'] } }; const initSuggestion: Suggestion = useMemo(() => { const storedSuggestion = localStorage?.getItem('OBLIVION_SUGGESTION'); let data: Suggestion = storedSuggestion ? JSON.parse(storedSuggestion) : defEndpoint; return removeDuplicates(data); }, []); const [suggestion, setSuggestion] = useState(initSuggestion); const fetchEndpoints = async (openInEnd: boolean = true) => { loadingToast(appLang?.toast?.please_wait); try { const response = await fetch( 'https://api.github.com/repos/ircfspace/endpoint/contents/v2.json', { headers: { accept: 'application/vnd.github.raw+json' } } ); if (!response.ok) { console.error('Failed to fetch Endpoints:', response.statusText); return; } const data = await response.json(); const normalized: Suggestion = { warp: { ipv4: Array.from(new Set(data.warp?.ipv4 ?? [])), ipv6: Array.from(new Set(data.warp?.ipv6 ?? [])) }, masque: { ipv4: Array.from(new Set(data.masque?.ipv4 ?? [])), ipv6: Array.from(new Set(data.masque?.ipv6 ?? [])) } }; setSuggestion(normalized); try { localStorage.setItem('OBLIVION_SUGGESTION', JSON.stringify(normalized)); console.log('Saved to localStorage'); } catch (e) { console.error('Failed to save localStorage', e); } } catch (error) { console.log('Fetch error:', error); } finally { if (openInEnd) setTimeout(() => setShowSuggestion(true), 1000); updaterRef.current?.classList.add('hidden'); stopLoadingToast(); } }; useEffect(() => { settings.get('scanResult').then((value) => { setScanResult(withDefault(value, defaultSettings.scanResult)); }); settings.get('method').then((value) => { const checkMethod = withDefault(value, defaultSettings.method); setMethod(checkMethod === 'masque' ? 'masque' : 'warp'); }); const handleClickOutside = (event: MouseEvent) => { if (suggestionRef.current && !suggestionRef.current.contains(event.target as Node)) { setShowSuggestion(false); } }; document.addEventListener('click', handleClickOutside); return () => { document.removeEventListener('click', handleClickOutside); }; }, []); useEffect(() => { setShowModal(isOpen); if (!isOpen) return; if (suggestion[method]?.ipv4?.length === defEndpoint[method]?.ipv4?.length) { fetchEndpoints(false); } }, [isOpen]); const handleOnClose = useCallback(() => { setShowModal(false); setTimeout(onClose, 300); }, [onClose]); const onSaveModal = useCallback(() => { const endpointInputModified = endpointInput.replace(/^https?:\/\//, '').replace(/\/$/, ''); let regex = /^(?:(?:\d{1,3}\.){3}\d{1,3}|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})(?::\d{1,5})$/; if (endpointInput.startsWith('[')) { regex = /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))/; } const tmp = regex.test(endpointInputModified) ? endpointInputModified : defValue; setEndpointInput(tmp); setEndpoint(tmp); settings.set('endpoint', tmp); settingsHaveChangedToast({ ...{ isConnected, isLoading, appLang } }); handleOnClose(); }, [endpointInput, defValue, setEndpoint, isConnected, isLoading, appLang, handleOnClose]); const onUpdateKeyDown = useButtonKeyDown(onSaveModal); const setEndpointSuggestion = useCallback((item: string) => { setEndpointInput(item); }, []); const setEndpointDefault = useCallback(() => { setEndpointInput(defValue); }, [defValue]); const handleCancelButtonClick = useCallback(() => { setEndpointInput(endpoint); handleOnClose(); }, [endpoint, handleOnClose]); const handleCancelButtonKeyDown = useButtonKeyDown(handleCancelButtonClick); const handleEndpointInputChange = useCallback( (e: ChangeEvent) => { setEndpointInput(e.target.value.toLowerCase().trim()); }, [setEndpointInput] ); return { endpointInput, showModal, appLang, suggestion, showSuggestion, scanResult, suggestionRef, onSaveModal, onUpdateKeyDown, setEndpointSuggestion, setEndpointDefault, handleCancelButtonClick, handleCancelButtonKeyDown, handleEndpointInputChange, handleOnClose, setShowSuggestion, fetchEndpoints, updaterRef, method }; }; export default useEndpointModal; ================================================ FILE: src/renderer/components/Modal/License/index.tsx ================================================ import classNames from 'classnames'; import useLicenseModal from './useLicenseModal'; import Input from '../../Input'; interface LicenseModalProps { title: string; isOpen: boolean; onClose: () => void; license: string; setLicense: (value: string) => void; } export default function LicenseModal({ title, isOpen, onClose, license, setLicense }: LicenseModalProps) { const { appLang, handleCancelButtonClick, handleCancelButtonKeyDown, handleLicenseInputChange, handleOnClose, licenseInput, onSaveModalClick, onSaveModalKeyDown, showModal, handleClearLicenseInput } = useLicenseModal({ isOpen, onClose, license, setLicense }); if (!isOpen) return <>; return (

{title}
{appLang?.modal?.form_clear}

{appLang?.modal?.license_desc}

{appLang?.modal?.cancel}
{appLang?.modal?.update}
); } ================================================ FILE: src/renderer/components/Modal/License/useLicenseModal.ts ================================================ import { ChangeEvent, useCallback, useEffect, useState } from 'react'; import { settings } from '../../../lib/settings'; import { useStore } from '../../../store'; import useTranslate from '../../../../localization/useTranslate'; import { settingsHaveChangedToast } from '../../../lib/toasts'; import { validateLicense } from '../../../lib/inputSanitizer'; import useButtonKeyDown from '../../../hooks/useButtonKeyDown'; interface LicenseModalProps { isOpen: boolean; onClose: () => void; license: string; setLicense: (value: string) => void; } const useLicenseModal = (props: LicenseModalProps) => { const { isOpen, license, onClose, setLicense } = props; const [licenseInput, setLicenseInput] = useState(license); const [showModal, setShowModal] = useState(isOpen); useEffect(() => setShowModal(isOpen), [isOpen]); const { isConnected, isLoading } = useStore(); const appLang = useTranslate(); const handleOnClose = useCallback(() => { setShowModal(false); setTimeout(onClose, 300); }, [onClose]); const onSaveModalClick = useCallback(() => { const tmp = validateLicense(licenseInput) ? licenseInput : ''; setLicenseInput(tmp); setLicense(tmp); settings.set('license', tmp); settingsHaveChangedToast({ ...{ isConnected, isLoading, appLang } }); handleOnClose(); }, [handleOnClose, licenseInput, setLicense, isConnected, isLoading, appLang]); const onSaveModalKeyDown = useButtonKeyDown(onSaveModalClick); const handleCancelButtonClick = useCallback(() => { setLicenseInput(license); handleOnClose(); }, [license, handleOnClose]); const handleCancelButtonKeyDown = useButtonKeyDown(handleCancelButtonClick); const handleLicenseInputChange = useCallback( (e: ChangeEvent) => { setLicenseInput(e.target.value.trim()); }, [setLicenseInput] ); const handleClearLicenseInput = useCallback(() => { setLicenseInput(''); setLicense(''); settings.set('license', ''); settingsHaveChangedToast({ ...{ isConnected, isLoading, appLang } }); handleOnClose(); }, [setLicense, isConnected, isLoading, appLang, handleOnClose]); return { appLang, handleCancelButtonClick, handleLicenseInputChange, handleOnClose, licenseInput, onSaveModalClick, showModal, handleCancelButtonKeyDown, onSaveModalKeyDown, handleClearLicenseInput }; }; export default useLicenseModal; ================================================ FILE: src/renderer/components/Modal/MTU/index.tsx ================================================ import classNames from 'classnames'; import { defaultSettings } from '../../../../defaultSettings'; import useMTUModal from './useMTUModal'; //import { lang } from '../../../../localization'; interface MTUModalProps { title: string; isOpen: boolean; onClose: () => void; defValue?: number; mtu: number; setMtu: (value: number) => void; } export default function MTUModal({ title, isOpen, onClose, defValue = defaultSettings.singBoxMTU, mtu, setMtu }: MTUModalProps) { const { appLang, handleCancelButtonClick, handleCancelButtonKeyDown, handleOnClose, handleMtuInputChange, onSaveModalClick, onSaveModalKeyDown, mtuInput, showModal } = useMTUModal({ isOpen, onClose, defValue, mtu, setMtu }); if (!isOpen) return <>; return (

{title}

{appLang.modal.mtu_desc}

{appLang?.modal?.cancel}
{appLang?.modal?.update}
); } ================================================ FILE: src/renderer/components/Modal/MTU/useMTUModal.ts ================================================ import { ChangeEvent, useCallback, useEffect, useState } from 'react'; //import { settings } from '../../../lib/settings'; import useTranslate from '../../../../localization/useTranslate'; import { removeLeadingZeros } from '../../../lib/inputSanitizer'; import useButtonKeyDown from '../../../hooks/useButtonKeyDown'; //import { useStore } from '../../../store'; //import { settingsHaveChangedToast } from '../../../lib/toasts'; interface MtuModalProps { isOpen: boolean; onClose: () => void; defValue: number; mtu: number; setMtu: (value: number) => void; } const useMTUModal = (props: MtuModalProps) => { const { isOpen, onClose, mtu, setMtu, defValue } = props; const [mtuInput, setMtuInput] = useState(mtu); const [showModal, setShowModal] = useState(isOpen); useEffect(() => setShowModal(isOpen), [isOpen]); useEffect(() => { if (mtuInput.toString().startsWith('0')) { setMtuInput(removeLeadingZeros(mtuInput)); } }, [mtuInput]); const appLang = useTranslate(); const handleOnClose = useCallback(() => { setShowModal(false); setTimeout(onClose, 300); }, [onClose]); const isValidMtu = useCallback((value: number) => { return /^\d{1,4}$/.test(value.toString()) && value >= 1000 && value <= 9999; }, []); const onSaveModalClick = useCallback(() => { const tmp = isValidMtu(mtuInput) ? mtuInput : defValue; setMtuInput(tmp); setMtu(tmp); //settings.set('singBoxMTU', tmp); //settingsHaveChangedToast({ ...{ isConnected, isLoading, appLang } }); handleOnClose(); }, [isValidMtu, mtuInput, defValue, setMtu, handleOnClose]); const onSaveModalKeyDown = useButtonKeyDown(onSaveModalClick); const handleCancelButtonClick = useCallback(() => { setMtuInput(mtu); handleOnClose(); }, [mtu, handleOnClose]); const handleCancelButtonKeyDown = useButtonKeyDown(handleCancelButtonClick); const handleMtuInputChange = useCallback((event: ChangeEvent) => { setMtuInput(Number(event.target.value)); }, []); return { appLang, handleCancelButtonClick, handleCancelButtonKeyDown, handleMtuInputChange, handleOnClose, mtuInput, onSaveModalClick, onSaveModalKeyDown, showModal }; }; export default useMTUModal; ================================================ FILE: src/renderer/components/Modal/Port/index.tsx ================================================ import classNames from 'classnames'; import { defaultSettings } from '../../../../defaultSettings'; import usePortModal from './usePortModal'; interface PortModalProps { title: string; isOpen: boolean; onClose: () => void; defValue?: number; port: number; setPort: (value: number) => void; } export default function PortModal({ title, isOpen, onClose, defValue = defaultSettings.port, port, setPort }: PortModalProps) { const { appLang, handleCancelButtonClick, handleCancelButtonKeyDown, handleOnClose, handlePortInputChange, onSaveModalClick, onSaveModalKeyDown, portInput, showModal } = usePortModal({ isOpen, onClose, defValue, port, setPort }); if (!isOpen) return <>; return (

{title}

{appLang?.modal?.cancel}
{appLang?.modal?.update}
); } ================================================ FILE: src/renderer/components/Modal/Port/usePortModal.ts ================================================ import { ChangeEvent, KeyboardEvent, useCallback, useEffect, useState } from 'react'; import { settings } from '../../../lib/settings'; import useTranslate from '../../../../localization/useTranslate'; import { useStore } from '../../../store'; import { settingsHaveChangedToast } from '../../../lib/toasts'; import { removeLeadingZeros } from '../../../lib/inputSanitizer'; import useButtonKeyDown from '../../../hooks/useButtonKeyDown'; interface PortModalProps { isOpen: boolean; onClose: () => void; defValue: number; port: number; setPort: (value: number) => void; } const usePortModal = (props: PortModalProps) => { const { isConnected, isLoading } = useStore(); const { isOpen, onClose, port, setPort, defValue } = props; const [portInput, setPortInput] = useState(port); const [showModal, setShowModal] = useState(isOpen); useEffect(() => setShowModal(isOpen), [isOpen]); useEffect(() => { if (portInput.toString().startsWith('0')) { setPortInput(removeLeadingZeros(portInput)); } }, [portInput]); const appLang = useTranslate(); const handleOnClose = useCallback(() => { setShowModal(false); setTimeout(onClose, 300); }, [onClose]); const isValidPort = useCallback((value: number) => { return /^\d{1,5}$/.test(value.toString()) && value >= 20 && value <= 65535; }, []); const onSaveModalClick = useCallback(() => { const tmp = isValidPort(portInput) ? portInput : defValue; setPortInput(tmp); setPort(tmp); settings.set('port', tmp); settingsHaveChangedToast({ ...{ isConnected, isLoading, appLang } }); handleOnClose(); }, [isValidPort, portInput, defValue, setPort, isConnected, isLoading, appLang, handleOnClose]); const onSaveModalKeyDown = useButtonKeyDown(onSaveModalClick); const handleCancelButtonClick = useCallback(() => { setPortInput(port); handleOnClose(); }, [port, handleOnClose]); const handleCancelButtonKeyDown = useButtonKeyDown(handleCancelButtonClick); const handlePortInputChange = useCallback((event: ChangeEvent) => { setPortInput(Number(event.target.value)); }, []); return { appLang, handleCancelButtonClick, handleCancelButtonKeyDown, handlePortInputChange, handleOnClose, portInput, onSaveModalClick, onSaveModalKeyDown, showModal }; }; export default usePortModal; ================================================ FILE: src/renderer/components/Modal/Profile/index.tsx ================================================ import { FC } from 'react'; import classNames from 'classnames'; import useProfileModal from './useProfileModal'; import { defaultSettings } from '../../../../defaultSettings'; import Input from '../../Input'; import { Profile } from '../../../pages/Scanner/useScanner'; interface ProfileModalProps { title: string; isOpen: boolean; onClose: () => void; profiles: Profile[]; endpoint: string; setProfiles: (value: Profile[]) => void; } const ProfileModal: FC = ({ title, isOpen, onClose, profiles, endpoint, setProfiles }) => { const { appLang, handleCancelButtonClick, handleCancelButtonKeyDown, profilesInput, profileName, setProfileName, profileEndpoint, setProfileEndpoint, handleAddProfile, handleRemoveProfile, handleEditProfile, validEndpoint, handleOnClose, onSaveModal, onUpdateKeyDown, showModal, isEditing, cancelEdit, sanitizeName } = useProfileModal({ isOpen, onClose, profiles, setProfiles }); if (!isOpen) return <>; return (

{title}

{ setProfileName(sanitizeName(e.target.value)); }} /> { setProfileEndpoint(e.target.value); }} type='text' placeholder={appLang?.modal?.profile_endpoint} />
{typeof profilesInput !== 'string' && profilesInput.length > 0 && ( <>
{profilesInput.map( (item: Profile, index: number) => typeof item.endpoint === 'string' && item.endpoint.length > 7 && (
{ handleRemoveProfile(index); }} >  { handleEditProfile(index); }} >  {item.name}
) )}
)}
{appLang?.modal?.cancel}
{appLang?.modal?.update}
{ setProfileEndpoint(endpoint); }} tabIndex={0} >
); }; export default ProfileModal; ================================================ FILE: src/renderer/components/Modal/Profile/useProfileModal.ts ================================================ import { useCallback, useEffect, useState } from 'react'; import toast from 'react-hot-toast'; import { settings } from '../../../lib/settings'; import useTranslate from '../../../../localization/useTranslate'; import { defaultSettings } from '../../../../defaultSettings'; import { defaultToast } from '../../../lib/toasts'; import { Profile } from '../../../pages/Scanner/useScanner'; import { sanitizeProfileName, validEndpoint } from '../../../lib/inputSanitizer'; import useButtonKeyDown from '../../../hooks/useButtonKeyDown'; import { withDefault } from '../../../lib/withDefault'; type ProfileModalProps = { isOpen: boolean; onClose: () => void; profiles: Profile[]; setProfiles: (value: Profile[]) => void; }; const useProfileModal = (props: ProfileModalProps) => { const { isOpen, onClose, profiles, setProfiles } = props; const [showModal, setShowModal] = useState(isOpen); const [profileName, setProfileName] = useState(''); const [profileEndpoint, setProfileEndpoint] = useState(''); const [profilesInput, setProfilesInput] = useState(profiles); const [editingIndex, setEditingIndex] = useState(null); const appLang = useTranslate(); const handleAddProfile = useCallback(() => { if (editingIndex === null && profilesInput?.length > 6) { defaultToast(appLang.modal.profile_limitation('7'), 'PROFILE_LIMITATION', 5000); } else if ( profileName !== '' && validEndpoint(profileEndpoint) !== '' && profileEndpoint.length > 7 ) { const newProfile = { name: profileName, endpoint: profileEndpoint }; if (editingIndex !== null) { const updatedProfiles = profilesInput.map((profile: Profile, index: number) => index === editingIndex ? newProfile : profile ); setProfilesInput(updatedProfiles); setEditingIndex(null); } else { const isDuplicate = Array.isArray(profilesInput) && profilesInput.some( (item: Profile) => item?.name === profileName && item?.endpoint === profileEndpoint ); if (!isDuplicate) { setProfilesInput([...profilesInput, newProfile]); } } toast.remove('PROFILE_LIMITATION'); } setProfileName(''); setProfileEndpoint(''); }, [appLang.modal, editingIndex, profileEndpoint, profileName, profilesInput]); const handleRemoveProfile = (key: number) => { const updatedProfiles = profilesInput.filter((_, index: number) => index !== key); setProfilesInput(updatedProfiles); }; const handleEditProfile = (index: number) => { const profile = profilesInput[index]; setProfileName(profile.name); setProfileEndpoint(profile.endpoint); setEditingIndex(index); }; const cancelEdit = () => { setProfileName(''); setProfileEndpoint(''); setEditingIndex(null); }; useEffect(() => { settings.get('profiles').then((value) => { setProfiles(JSON.parse(withDefault(value, defaultSettings.profiles))); }); }, []); useEffect(() => setShowModal(isOpen), [isOpen]); const handleOnClose = useCallback(() => { setShowModal(false); setTimeout(onClose, 300); }, [onClose]); const onSaveModal = useCallback(() => { if ( profileName !== '' && validEndpoint(profileEndpoint) !== '' && profileEndpoint.length > 7 ) { handleAddProfile(); } else { settings.set('profiles', JSON.stringify(profilesInput)); setProfilesInput(profilesInput); setProfiles(profilesInput); setProfileName(''); setProfileEndpoint(''); handleOnClose(); } }, [ profilesInput, setProfilesInput, setProfiles, handleOnClose, //setProfileName, //setProfileEndpoint, handleAddProfile, profileName, profileEndpoint ]); const onUpdateKeyDown = useButtonKeyDown(onSaveModal); const handleCancelButtonClick = useCallback(() => { setProfiles(profiles); setProfilesInput(profiles); setProfileName(''); setProfileEndpoint(''); handleOnClose(); }, [profiles, setProfiles, handleOnClose, setProfileName, setProfileEndpoint]); const handleCancelButtonKeyDown = useButtonKeyDown(handleCancelButtonClick); return { showModal, appLang, onSaveModal, onUpdateKeyDown, handleCancelButtonClick, handleCancelButtonKeyDown, profilesInput, profileName, setProfileName, profileEndpoint, setProfileEndpoint, handleAddProfile, handleRemoveProfile, handleEditProfile, cancelEdit, validEndpoint, handleOnClose, isEditing: editingIndex !== null, sanitizeName: sanitizeProfileName }; }; export default useProfileModal; ================================================ FILE: src/renderer/components/Modal/Restore/index.tsx ================================================ import classNames from 'classnames'; import useRestoreModal from './useRestoreModal'; interface RestoreModalProps { title: string; isOpen: boolean; onClose: () => void; setTheme: (value: string) => void; setLang: (value: string) => void; setOpenAtLogin: (value: boolean) => void; setStartMinimized: (value: boolean) => void; setAutoConnect: (value: boolean) => void; setForceClose: (value: boolean) => void; setShortcut: (value: boolean) => void; } export default function RestoreModal({ title, isOpen, onClose, setTheme, setLang, setOpenAtLogin, setStartMinimized, setAutoConnect, setForceClose, setShortcut }: RestoreModalProps) { const { appLang, handleOnClose, onSaveModal, onCancelKeyDown, onConfirmKeyDown, showModal } = useRestoreModal({ isOpen, onClose, setTheme, setLang, setOpenAtLogin, setStartMinimized, setAutoConnect, setForceClose, setShortcut }); if (!isOpen) return null; return (

{title}

{appLang?.modal?.restore_desc}

{appLang?.modal?.cancel}
); } ================================================ FILE: src/renderer/components/Modal/Restore/useRestoreModal.ts ================================================ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router'; import { defaultSettings, dnsServers, singBoxGeoIp, singBoxGeoSite, singBoxLog, singBoxStack, singBoxAddrType } from '../../../../defaultSettings'; import { settings } from '../../../lib/settings'; import { ipcRenderer } from '../../../lib/utils'; import { changeLang, getDirectionByLang, LanguageType } from '../../../../localization'; import useTranslate from '../../../../localization/useTranslate'; import { loadingToast, stopLoadingToast } from '../../../lib/toasts'; import useButtonKeyDown from '../../../hooks/useButtonKeyDown'; interface RestoreModalProps { isOpen: boolean; onClose: () => void; setTheme: (value: string) => void; setLang: (value: string) => void; setOpenAtLogin: (value: boolean) => void; setStartMinimized: (value: boolean) => void; setAutoConnect: (value: boolean) => void; setForceClose: (value: boolean) => void; setShortcut: (value: boolean) => void; } const useRestoreModal = (props: RestoreModalProps) => { const { isOpen, onClose, setTheme, setLang, setOpenAtLogin, setStartMinimized, setAutoConnect, setForceClose, setShortcut } = props; const detectingSystemTheme = useMemo( () => window?.matchMedia('(prefers-color-scheme: dark)')?.matches, [] ); const [showModal, setShowModal] = useState(isOpen); const navigate = useNavigate(); useEffect(() => setShowModal(isOpen), [isOpen]); const appLang = useTranslate(); const handleOnClose = useCallback(() => { setShowModal(false); setTimeout(onClose, 300); }, [onClose]); const onCancelKeyDown = useButtonKeyDown(handleOnClose); const onSaveModal = useCallback(async () => { loadingToast(appLang?.toast?.please_wait); // in this page setForceClose(defaultSettings.forceClose); //setShortcut(defaultSettings.shortcut); setLang(defaultSettings.lang); setOpenAtLogin(defaultSettings.openAtLogin); setStartMinimized(defaultSettings.startMinimized); setAutoConnect(defaultSettings.autoConnect); // TODO Promise.all await settings.set('theme', detectingSystemTheme ? 'dark' : 'light'); setTheme(detectingSystemTheme ? 'dark' : 'light'); document.documentElement.setAttribute( 'data-bs-theme', detectingSystemTheme ? 'dark' : 'light' ); await settings.set('forceClose', defaultSettings.forceClose); //await settings.set('shortcut', defaultSettings.shortcut); await settings.set('lang', defaultSettings.lang); changeLang(defaultSettings.lang); document.documentElement.setAttribute('lang', defaultSettings.lang); document.documentElement.setAttribute( 'dir', getDirectionByLang(defaultSettings.lang as LanguageType) ); await settings.set('openAtLogin', defaultSettings.openAtLogin); await settings.set('startMinimized', defaultSettings.startMinimized); await settings.set('autoConnect', defaultSettings.autoConnect); handleOnClose(); // other settings //await settings.set('scan', defaultSettings.scan); await settings.set('endpoint', defaultSettings.endpoint); //await settings.set('psiphon', defaultSettings.psiphon); await settings.set('location', defaultSettings.location); await settings.set('license', defaultSettings.license); //await settings.set('gool', defaultSettings.gool); await settings.set('method', defaultSettings.method); await settings.set('hostIP', defaultSettings.hostIP); await settings.set('ipType', defaultSettings.ipType); await settings.set('rtt', defaultSettings.rtt); await settings.set('ipData', defaultSettings.ipData); await settings.set('port', defaultSettings.port); await settings.set('proxyMode', defaultSettings.proxyMode); //await settings.set('shareVPN', defaultSettings.shareVPN); await settings.set('routingRules', defaultSettings.routingRules); await settings.set('reserved', defaultSettings.reserved); await settings.set('scanResult', defaultSettings.scanResult); await settings.set('profiles', defaultSettings.profiles); await settings.set('dns', dnsServers[0].value); await settings.set('dataUsage', defaultSettings.dataUsage); await settings.set('asn', defaultSettings.asn); await settings.set('closeHelper', defaultSettings.closeHelper); await settings.set('singBoxMTU', defaultSettings.singBoxMTU); await settings.set('singBoxGeoIp', singBoxGeoIp[0].geoIp); await settings.set('singBoxGeoSite', singBoxGeoSite[0].geoSite); await settings.set('singBoxGeoBlock', defaultSettings.singBoxGeoBlock); await settings.set('singBoxGeoNSFW', defaultSettings.singBoxGeoNSFW); await settings.set('singBoxLog', singBoxLog[0].value); await settings.set('singBoxStack', singBoxStack[0].value); await settings.set('singBoxSniff', defaultSettings.singBoxSniff); await settings.set('singBoxAddrType', singBoxAddrType[0].value); await settings.set('restartCounter', defaultSettings.restartCounter); await settings.set('testUrl', defaultSettings.testUrl); await settings.set('soundEffect', defaultSettings.soundEffect); await settings.set('betaRelease', defaultSettings.betaRelease); await settings.set('plainDns', defaultSettings.plainDns); await settings.set('DoH', defaultSettings.DoH); // ipcRenderer.sendMessage('wp-end'); ipcRenderer.sendMessage('localization', defaultSettings.lang); ipcRenderer.sendMessage('startup', defaultSettings.openAtLogin); // setTimeout(function () { stopLoadingToast(); navigate('/'); }, 1500); }, [ setForceClose, setShortcut, setLang, setOpenAtLogin, setAutoConnect, detectingSystemTheme, setTheme, handleOnClose ]); const onConfirmKeyDown = useButtonKeyDown(onSaveModal); return { showModal, handleOnClose, onSaveModal, onConfirmKeyDown, onCancelKeyDown, appLang }; }; export default useRestoreModal; ================================================ FILE: src/renderer/components/Modal/RoutingRules/index.tsx ================================================ import classNames from 'classnames'; import useRoutingRulesModal from './useRoutingRulesModal'; import Textarea from '../../Textarea'; interface RoutingRulesModalProps { title: string; isOpen: boolean; setIsOpen: (isOpen: boolean) => void; onClose: () => void; routingRules: string; setRoutingRules: (value: string) => void; } export default function RoutingRulesModal({ title, isOpen, setIsOpen, onClose, routingRules, setRoutingRules }: RoutingRulesModalProps) { const { appLang, handleCancelButtonClick, handleCancelButtonKeyDown, handleOnClose, handleRoutingRulesInput, handleSetRoutingRulesSimple, onSaveModal, onUpdateKeyDown, routingRulesInput, proxyMode } = useRoutingRulesModal({ setIsOpen, onClose, routingRules, setRoutingRules }); if (!isOpen) return <>; return (

{title}
{appLang?.modal?.routing_rules_sample}
{appLang?.toast?.help_btn}