├── src ├── renderer │ ├── renderer.ts │ ├── loading_window │ │ ├── renderer.ts │ │ ├── loading.html │ │ └── sc.tsx │ ├── interfaces │ │ ├── react.interface.ts │ │ ├── activty.interface.ts │ │ └── servers-context.interface.ts │ ├── context │ │ ├── update.context.ts │ │ ├── activty.context.ts │ │ └── servers.context.ts │ ├── renderer.d.ts │ ├── index.html │ ├── index.tsx │ ├── vite-env.d.ts │ ├── component │ │ ├── buttons │ │ │ ├── edit-btn.component.tsx │ │ │ ├── select-interface-btn-component.tsx │ │ │ ├── flush-dns-btn-component.tsx │ │ │ ├── addDns-btn.component.tsx │ │ │ ├── interfaces-dialog-btn-component.tsx │ │ │ ├── togglePin-btn.component.tsx │ │ │ ├── updateList-btn.component.tsx │ │ │ ├── delete-btn.component.tsx │ │ │ ├── connect-btn.component.tsx │ │ │ └── update-btn.component.tsx │ │ ├── dropdowns │ │ │ ├── server-options │ │ │ │ └── server-options.component.tsx │ │ │ └── serverlist-options │ │ │ │ ├── serverlist-options.component.tsx │ │ │ │ └── updatelist.item.tsx │ │ ├── servers │ │ │ ├── servers.tsx │ │ │ └── server.component.tsx │ │ ├── selectes │ │ │ └── servers │ │ │ │ └── index.tsx │ │ ├── head │ │ │ └── navbar.component.tsx │ │ ├── cards │ │ │ ├── advertisement.card.component.tsx │ │ │ └── server-info │ │ │ │ └── index.tsx │ │ └── modals │ │ │ ├── wmic-helper.modal.tsx │ │ │ └── network-options.component.tsx │ ├── Wrappers │ │ └── pages.wrapper.tsx │ ├── utils │ │ ├── icons.util.tsx │ │ └── theme.util.ts │ ├── notifications │ │ └── appNotif.tsx │ ├── index.css │ ├── app.tsx │ └── pages │ │ ├── home.page.tsx │ │ ├── shutdown.page.tsx │ │ └── setting.page.tsx ├── main │ ├── constant │ │ └── messages.constant.ts │ ├── shared │ │ ├── platform.ts │ │ ├── isDev.ts │ │ ├── overlayIcon.ts │ │ ├── logger.ts │ │ ├── getIconPath.ts │ │ ├── file.ts │ │ ├── __test__ │ │ │ └── getIconPath.spec.ts │ │ ├── serve.ts │ │ └── get-port.ts │ ├── electron-env.d.ts │ ├── ipc │ │ ├── ui.ts │ │ ├── notif.ts │ │ ├── setting.ts │ │ ├── shutdown.ts │ │ └── dialogs.ts │ ├── platforms │ │ ├── windows │ │ │ ├── interfaces │ │ │ │ └── interface.ts │ │ │ ├── __test__ │ │ │ │ └── windows.platform.spec.ts │ │ │ └── windows.platform.ts │ │ ├── platform.ts │ │ ├── linux │ │ │ └── linux.platform.ts │ │ └── mac │ │ │ └── mac.platform.ts │ ├── store │ │ └── store.ts │ ├── services │ │ └── dns.service.ts │ ├── config.ts │ ├── update.ts │ └── index.ts ├── shared │ ├── validators │ │ ├── dns.validator.ts │ │ └── __test__ │ │ │ └── dns.validator.spec.ts │ ├── interfaces │ │ ├── advertisement.interface.ts │ │ ├── server.interface.ts │ │ ├── network.interface.ts │ │ └── settings.interface.ts │ └── constants │ │ ├── default-setting.contant.ts │ │ ├── urls.constant.ts │ │ ├── languages.constant.ts │ │ ├── servers.cosntant.ts │ │ └── eventsKeys.constant.ts ├── i18n │ ├── formatters.ts │ ├── i18n-node.ts │ ├── i18n-react.tsx │ ├── i18n-util.sync.ts │ ├── i18n-util.async.ts │ ├── fa │ │ └── index.ts │ ├── eng │ │ └── index.ts │ ├── ru │ │ └── index.ts │ ├── i18n-util.ts │ └── i18n-types.ts └── preload │ └── index.ts ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── DNS.yml │ ├── Feature_Request.yml │ └── Bug_Report.yml ├── 1.png ├── banner.png └── workflows │ ├── build.yml │ ├── pull.yml │ ├── pre-release.yml │ └── release.yml ├── public ├── icons │ ├── icon.ico │ ├── icon.png │ ├── show.png │ ├── icon.icns │ ├── power.png │ └── icon-connected.png └── servers-icon │ ├── 403.png │ ├── def.png │ ├── radar.png │ ├── electro.png │ ├── shecan.png │ ├── asiatech.png │ └── cloudflare.png ├── postcss.config.js ├── .gitmodules ├── assets ├── fonts │ ├── inter │ │ ├── Inter-Black.ttf │ │ ├── Inter-Bold.ttf │ │ ├── Inter-Light.ttf │ │ ├── Inter-Thin.ttf │ │ ├── Inter-Medium.ttf │ │ ├── Inter-Regular.ttf │ │ ├── Inter-ExtraBold.ttf │ │ ├── Inter-ExtraLight.ttf │ │ └── Inter-SemiBold.ttf │ └── BalooTamma │ │ └── BalooTamma2-Bold.ttf └── flags │ ├── russia.svg │ ├── usa.svg │ └── iran.svg ├── .typesafe-i18n.json ├── .prettierrc ├── jest.config.ts ├── tsconfig.node.json ├── CONTRIBUTING.md ├── tsconfig.json ├── removeLocales.js ├── index.html ├── .eslintrc.js ├── tailwind.config.js ├── biome.json ├── LICENSE ├── .gitignore ├── electron-builder.yml ├── README.md ├── vite.config.ts ├── package.json └── changelog.md /src/renderer/renderer.ts: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | import './index' 3 | -------------------------------------------------------------------------------- /src/renderer/loading_window/renderer.ts: -------------------------------------------------------------------------------- 1 | import '../index.css' 2 | import './sc' 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | ## Do allow blank issues 2 | blank_issues_enabled: true -------------------------------------------------------------------------------- /.github/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/.github/1.png -------------------------------------------------------------------------------- /.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/.github/banner.png -------------------------------------------------------------------------------- /public/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/public/icons/icon.ico -------------------------------------------------------------------------------- /public/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/public/icons/icon.png -------------------------------------------------------------------------------- /public/icons/show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/public/icons/show.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('tailwindcss'), require('autoprefixer')], 3 | } 4 | -------------------------------------------------------------------------------- /public/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/public/icons/icon.icns -------------------------------------------------------------------------------- /public/icons/power.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/public/icons/power.png -------------------------------------------------------------------------------- /public/servers-icon/403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/public/servers-icon/403.png -------------------------------------------------------------------------------- /public/servers-icon/def.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/public/servers-icon/def.png -------------------------------------------------------------------------------- /public/servers-icon/radar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/public/servers-icon/radar.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "store"] 2 | path = store 3 | url = https://github.com/DnsChanger/dnsChanger-desktop/tree/store 4 | -------------------------------------------------------------------------------- /public/icons/icon-connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/public/icons/icon-connected.png -------------------------------------------------------------------------------- /public/servers-icon/electro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/public/servers-icon/electro.png -------------------------------------------------------------------------------- /public/servers-icon/shecan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/public/servers-icon/shecan.png -------------------------------------------------------------------------------- /assets/fonts/inter/Inter-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/assets/fonts/inter/Inter-Black.ttf -------------------------------------------------------------------------------- /assets/fonts/inter/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/assets/fonts/inter/Inter-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/inter/Inter-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/assets/fonts/inter/Inter-Light.ttf -------------------------------------------------------------------------------- /assets/fonts/inter/Inter-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/assets/fonts/inter/Inter-Thin.ttf -------------------------------------------------------------------------------- /public/servers-icon/asiatech.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/public/servers-icon/asiatech.png -------------------------------------------------------------------------------- /public/servers-icon/cloudflare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/public/servers-icon/cloudflare.png -------------------------------------------------------------------------------- /assets/fonts/inter/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/assets/fonts/inter/Inter-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/inter/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/assets/fonts/inter/Inter-Regular.ttf -------------------------------------------------------------------------------- /src/main/constant/messages.constant.ts: -------------------------------------------------------------------------------- 1 | export enum ResponseMessage { 2 | CONNECTION_FAILED = 'اتصالات خودتون رو بررسی کنید.', 3 | } 4 | -------------------------------------------------------------------------------- /assets/fonts/inter/Inter-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/assets/fonts/inter/Inter-ExtraBold.ttf -------------------------------------------------------------------------------- /assets/fonts/inter/Inter-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/assets/fonts/inter/Inter-ExtraLight.ttf -------------------------------------------------------------------------------- /assets/fonts/inter/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/assets/fonts/inter/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /src/main/shared/platform.ts: -------------------------------------------------------------------------------- 1 | import os from 'node:os' 2 | 3 | export function isWindows() { 4 | return os.platform() === 'win32' 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/interfaces/react.interface.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export type setState = React.Dispatch> 4 | -------------------------------------------------------------------------------- /assets/fonts/BalooTamma/BalooTamma2-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/HEAD/assets/fonts/BalooTamma/BalooTamma2-Bold.ttf -------------------------------------------------------------------------------- /src/shared/validators/dns.validator.ts: -------------------------------------------------------------------------------- 1 | import { isIPv4 } from 'net' 2 | export function isValidDnsAddress(value: string) { 3 | return isIPv4(value) 4 | } 5 | -------------------------------------------------------------------------------- /.typesafe-i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "adapter": "react", 3 | "baseLocale": "fa", 4 | "$schema": "https://unpkg.com/typesafe-i18n@5.24.2/schema/typesafe-i18n.json" 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/interfaces/advertisement.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Advertisement { 2 | name: string 3 | banner: string 4 | url: string 5 | disabled: boolean 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/context/update.context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | export const updateContext = createContext({ 4 | downloading: false, 5 | setDownloading: () => {}, 6 | }) 7 | -------------------------------------------------------------------------------- /src/renderer/renderer.d.ts: -------------------------------------------------------------------------------- 1 | import { ipcPreload, uiPreload } from '../preload' 2 | 3 | declare global { 4 | interface Window { 5 | ipc: typeof ipcPreload 6 | ui: typeof uiPreload 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/interfaces/activty.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ActivityContext { 2 | isWaiting: boolean 3 | setIsWaiting: any //setState 4 | status: string 5 | setStatus: any 6 | reqPing: boolean | null 7 | setReqPing: any 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSameLine": true, 4 | "bracketSpacing": true, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "semi": false, 8 | "printWidth": 110, 9 | "tabWidth": 2, 10 | "endOfLine": "auto" 11 | } 12 | -------------------------------------------------------------------------------- /assets/flags/russia.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | clearMocks: true, 3 | testRegex: '.*\\.spec\\.ts$', 4 | transform: { 5 | '^.+\\.(t|j)s$': 'ts-jest', 6 | }, 7 | rootDir: 'src', 8 | moduleNameMapper: { 9 | '^src/(.*)$': '/$1', 10 | }, 11 | testEnvironment: 'node', 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "resolveJsonModule": true, 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["./vite.config.ts", "package.json"] 10 | } 11 | -------------------------------------------------------------------------------- /src/main/electron-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace NodeJS { 4 | interface ProcessEnv { 5 | VSCODE_DEBUG?: 'true' 6 | DIST_ELECTRON: string 7 | DIST: string 8 | /** /dist/ or /public/ */ 9 | PUBLIC: string 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/ipc/ui.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain, nativeTheme } from 'electron' 2 | 3 | import { EventsKeys } from '../../shared/constants/eventsKeys.constant' 4 | 5 | ipcMain.handle(EventsKeys.TOGGLE_THEME, (_event, data) => { 6 | nativeTheme.themeSource = data 7 | return nativeTheme.shouldUseDarkColors 8 | }) 9 | -------------------------------------------------------------------------------- /src/main/platforms/windows/interfaces/interface.ts: -------------------------------------------------------------------------------- 1 | export interface Interface { 2 | name: string 3 | mac_address: string | undefined 4 | ip_address: string | undefined 5 | vendor: string 6 | model: string 7 | type: string 8 | netmask: string | null 9 | gateway_ip: string | null 10 | } 11 | -------------------------------------------------------------------------------- /src/shared/interfaces/server.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Server extends Record { 2 | key: string 3 | name: string 4 | servers: string[] 5 | avatar: string 6 | rate: number 7 | tags: string[] 8 | } 9 | export interface ServerStore extends Server { 10 | isPin: boolean 11 | } 12 | -------------------------------------------------------------------------------- /src/main/shared/isDev.ts: -------------------------------------------------------------------------------- 1 | import electron from 'electron' 2 | 3 | const { env } = process 4 | const isEnvSet = 'ELECTRON_IS_DEV' in env 5 | const getFromEnv = Number.parseInt(env.ELECTRON_IS_DEV, 10) === 1 6 | 7 | const isDev = isEnvSet ? getFromEnv : !electron.app.isPackaged 8 | 9 | export default isDev 10 | -------------------------------------------------------------------------------- /src/shared/interfaces/network.interface.ts: -------------------------------------------------------------------------------- 1 | interface NetworkAdapter { 2 | address: string 3 | netmask: string 4 | family: string 5 | mac: string 6 | internal: boolean 7 | cidr: string 8 | scopeid?: number 9 | } 10 | 11 | export interface NetworkAdapters { 12 | [key: string]: NetworkAdapter[] 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DNS Changer (OpenSource) 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/shared/constants/default-setting.contant.ts: -------------------------------------------------------------------------------- 1 | import { SettingInStore } from '../interfaces/settings.interface' 2 | 3 | export const defaultSetting: SettingInStore = { 4 | lng: 'eng', 5 | startUp: false, 6 | autoUpdate: true, 7 | minimize_tray: false, 8 | network_interface: 'Auto', 9 | use_analytic: true, 10 | } 11 | -------------------------------------------------------------------------------- /src/shared/constants/urls.constant.ts: -------------------------------------------------------------------------------- 1 | export enum UrlsConstant { 2 | STORE = 'https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/store/servers_DB.json', 3 | STORE_SERVER = 'https://dnschanger-store.liara.run/', 4 | ADS_STORE = 'https://raw.githubusercontent.com/DnsChanger/dnsChanger-desktop/store/ads_db.json', 5 | } 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/DNS.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F4A5 New DNS" 2 | description: "suggest to add new DNS" 3 | title: "[New DNS]:" 4 | labels: ["type: DNS :sob:"] 5 | body: 6 | - type: input 7 | attributes: 8 | label: "IP address of DNS" 9 | description: "example: 4.2.2.4, 8.8.8.8" 10 | placeholder: "your suggested DNS" 11 | -------------------------------------------------------------------------------- /src/main/shared/overlayIcon.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, nativeImage } from 'electron' 2 | 3 | export function updateOverlayIcon( 4 | win: BrowserWindow, 5 | filePath: string | null, 6 | description: string | 'connected' | 'disconnect', 7 | ) { 8 | const icon = filePath ? nativeImage.createFromPath(filePath) : null 9 | win.setOverlayIcon(icon, description) 10 | } 11 | -------------------------------------------------------------------------------- /src/i18n/formatters.ts: -------------------------------------------------------------------------------- 1 | import type { FormattersInitializer } from 'typesafe-i18n' 2 | import type { Locales, Formatters } from './i18n-types' 3 | 4 | export const initFormatters: FormattersInitializer = ( 5 | locale: Locales, 6 | ) => { 7 | const formatters: Formatters = { 8 | // add your formatter functions here 9 | } 10 | 11 | return formatters 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/interfaces/servers-context.interface.ts: -------------------------------------------------------------------------------- 1 | import { ServerStore } from '../../shared/interfaces/server.interface' 2 | 3 | export interface ServersContext { 4 | servers: ServerStore[] 5 | setServers: any 6 | currentActive: ServerStore | null 7 | setCurrentActive: any 8 | 9 | selected: ServerStore | null 10 | setSelected: any 11 | 12 | network: string 13 | setNetwork: any 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import { App } from './app' 4 | import { Button } from 'react-daisyui' 5 | import './index.css' 6 | // eslint-disable-next-line import/no-unresolved 7 | 8 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 9 | // @ts-ignore 10 | const root = ReactDOM.createRoot(document.getElementById('root')) 11 | 12 | root.render() 13 | -------------------------------------------------------------------------------- /src/main/shared/logger.ts: -------------------------------------------------------------------------------- 1 | import electronLog from 'electron-log' 2 | 3 | export enum LogId { 4 | USER = 'user', 5 | } 6 | 7 | function createLogger(logId: string): electronLog.Logger { 8 | return electronLog.create({ logId }) 9 | } 10 | export function getLoggerPathFile(logId: string): string { 11 | return electronLog.transports.file.getFile().path 12 | } 13 | 14 | export const userLogger: electronLog.Logger = createLogger(LogId.USER) 15 | -------------------------------------------------------------------------------- /src/shared/constants/languages.constant.ts: -------------------------------------------------------------------------------- 1 | export interface Language { 2 | name: string 3 | value: string 4 | svg: string 5 | } 6 | export const languages: Array = [ 7 | { 8 | name: 'فارسی', 9 | value: 'fa', 10 | svg: '../assets/flags/iran.svg', 11 | }, 12 | { 13 | name: 'English', 14 | value: 'eng', 15 | svg: '../assets/flags/usa.svg', 16 | }, 17 | { 18 | name: 'Russian', 19 | value: 'ru', 20 | svg: '../assets/flags/russia.svg', 21 | }, 22 | ] 23 | -------------------------------------------------------------------------------- /src/renderer/context/activty.context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ActivityContext } from '../interfaces/activty.interface' 4 | 5 | export const activityContext = React.createContext({ 6 | isWaiting: false, 7 | // eslint-disable-next-line @typescript-eslint/no-empty-function 8 | setIsWaiting: () => {}, 9 | status: '', 10 | // eslint-disable-next-line @typescript-eslint/no-empty-function 11 | setStatus: () => {}, 12 | reqPing: null, 13 | setReqPing: () => {}, 14 | }) 15 | -------------------------------------------------------------------------------- /src/renderer/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { uiPreload, ipcPreload, osItems, storePreload } from '../preload' 3 | 4 | interface ImportMetaEnv { 5 | readonly PACKAGE_VERSION: string 6 | // more env variables... 7 | } 8 | 9 | interface ImportMeta { 10 | readonly env: ImportMetaEnv 11 | } 12 | declare global { 13 | interface Window { 14 | ipc: typeof ipcPreload 15 | ui: typeof uiPreload 16 | os: typeof osItems 17 | storePreload: typeof storePreload 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/component/buttons/edit-btn.component.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'react-daisyui' 2 | import { AiOutlineEdit } from 'react-icons/ai' 3 | 4 | export function EditButtonComponent() { 5 | return ( 6 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/store/store.ts: -------------------------------------------------------------------------------- 1 | import electronStore from 'electron-store' 2 | 3 | import { serversConstant } from '../../shared/constants/servers.cosntant' 4 | import { StoreKey } from '../../shared/interfaces/settings.interface' 5 | import { defaultSetting } from '../../shared/constants/default-setting.contant' 6 | 7 | export const store = new electronStore({ 8 | defaults: { 9 | dnsList: serversConstant, 10 | settings: defaultSetting, 11 | defaultServer: null, 12 | }, 13 | name: 'dnsChangerStore_1.9.0', 14 | }) 15 | -------------------------------------------------------------------------------- /src/shared/validators/__test__/dns.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals' 2 | import { isValidDnsAddress } from '../dns.validator' 3 | describe('DnsValidator', () => { 4 | it('should return false', () => { 5 | expect(isValidDnsAddress('1.1.1.1.1')).toBeFalsy() 6 | expect(isValidDnsAddress('1 1 1 1')).toBeFalsy() 7 | expect(isValidDnsAddress('xx.xx.xx.xx')).toBeFalsy() 8 | }) 9 | 10 | it('should return true', () => { 11 | expect(isValidDnsAddress('1.1.1.1')).toBeTruthy() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/shared/interfaces/settings.interface.ts: -------------------------------------------------------------------------------- 1 | import { Locales } from '../../i18n/i18n-types' 2 | import { ServerStore } from './server.interface' 3 | 4 | export interface Settings { 5 | startUp: boolean 6 | lng: Locales 7 | autoUpdate: boolean 8 | minimize_tray: boolean 9 | network_interface: string | 'Auto' 10 | use_analytic: boolean 11 | } 12 | 13 | export type SettingInStore = Settings 14 | 15 | export type StoreKey = { 16 | dnsList: ServerStore[] 17 | settings: SettingInStore 18 | defaultServer: ServerStore | null 19 | } 20 | -------------------------------------------------------------------------------- /src/i18n/i18n-node.ts: -------------------------------------------------------------------------------- 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. 2 | /* eslint-disable */ 3 | 4 | import { i18n } from './i18n-util' 5 | import { loadAllLocales } from './i18n-util.sync' 6 | import type { LocaleTranslationFunctions } from 'typesafe-i18n' 7 | import type { Locales, Translations, TranslationFunctions } from './i18n-types' 8 | 9 | loadAllLocales() 10 | 11 | export const L: LocaleTranslationFunctions< 12 | Locales, 13 | Translations, 14 | TranslationFunctions 15 | > = i18n() 16 | 17 | export default L 18 | -------------------------------------------------------------------------------- /src/main/shared/getIconPath.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | export function getIconPath(): string { 4 | let icon: string 5 | switch (process.platform) { 6 | case 'win32': 7 | icon = path.join(process.env.PUBLIC, 'icons/icon.ico') 8 | break 9 | case 'darwin': 10 | icon = path.join(process.env.PUBLIC, 'icons/icon.ico') 11 | break 12 | case 'linux': 13 | icon = path.join(process.env.PUBLIC, 'icons/icon.png') 14 | break 15 | default: 16 | icon = path.join(process.env.PUBLIC, 'icons/icon.ico') 17 | break 18 | } 19 | return icon 20 | } 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Folder structure: 2 | 3 | - [UI](https://github.com/DnsChanger/dnsChanger-desktop/tree/main/src/renderer) 4 | - [Main](https://github.com/DnsChanger/dnsChanger-desktop/tree/main/src/main) 5 | 6 | --- 7 | 8 | 1. Fork this repo (Button at top Right Corner) and clone 9 | 10 | 2. 11 | 12 | ```bash 13 | npm install 14 | ``` 15 | 16 | 3. developing... 17 | 18 | 4. Commit your Changes to your Fork and then Create a Pull Request to this Repo. 19 | 20 | 5. Sit Back and Relax... and kudos for your contibution! Your effort and contribution will benefit lots of people. 🙏😊 21 | -------------------------------------------------------------------------------- /src/main/ipc/notif.ts: -------------------------------------------------------------------------------- 1 | import { Notification, ipcMain, nativeImage } from 'electron' 2 | 3 | import { getIconPath } from '../shared/getIconPath' 4 | import { EventsKeys } from '../../shared/constants/eventsKeys.constant' 5 | 6 | ipcMain.on(EventsKeys.NOTIFICATION, (_event, data) => { 7 | // new Notification({ title: "DNS Changer", body: data, icon }).show(); 8 | notfi('DNS Changer', data) 9 | }) 10 | 11 | export function notfi(title: string, message: string) { 12 | const icon = nativeImage.createFromPath(getIconPath()) 13 | new Notification({ title, body: message, icon }).show() 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/Wrappers/pages.wrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import { useI18nContext } from '../../i18n/i18n-react' 4 | import { NavbarComponent } from '../component/head/navbar.component' 5 | 6 | interface Props { 7 | children: JSX.Element 8 | } 9 | 10 | export function PageWrapper(prop: Props) { 11 | const [currentPage] = useState('/') 12 | const { LL, locale } = useI18nContext() 13 | return ( 14 |
15 | 16 | {React.cloneElement(prop.children, { currentPage })} 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/main/services/dns.service.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from '../platforms/platform' 2 | 3 | export class DnsService { 4 | constructor(private platform: Platform) {} 5 | 6 | async setDns(nameServers: Array) { 7 | return this.platform.setDns(nameServers) 8 | } 9 | 10 | async getActiveDns() { 11 | return this.platform.getActiveDns() 12 | } 13 | 14 | async clearDns() { 15 | return this.platform.clearDns() 16 | } 17 | 18 | async getInterfacesList() { 19 | return this.platform.getInterfacesList() 20 | } 21 | 22 | async flushDns() { 23 | return this.platform.flushDns() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/context/servers.context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ServersContext } from '../interfaces/servers-context.interface' 4 | 5 | export const serversContext = React.createContext({ 6 | servers: [], 7 | // eslint-disable-next-line @typescript-eslint/no-empty-function 8 | setServers: () => {}, 9 | 10 | currentActive: null, 11 | // eslint-disable-next-line @typescript-eslint/no-empty-function 12 | setCurrentActive: () => {}, 13 | 14 | selected: null, 15 | // eslint-disable-next-line @typescript-eslint/no-empty-function 16 | setSelected: () => {}, 17 | 18 | network: '', 19 | setNetwork: () => {}, 20 | }) 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": "./" 19 | }, 20 | "include": ["src", "electron"], 21 | "references": [{ "path": "./tsconfig.node.json" }] 22 | } 23 | -------------------------------------------------------------------------------- /removeLocales.js: -------------------------------------------------------------------------------- 1 | //https://www.electron.build/configuration/configuration#afterpack 2 | exports.default = async function (context) { 3 | //console.log(context) 4 | var fs = require('fs') 5 | var localeDir = context.appOutDir + '/locales/' 6 | 7 | fs.readdir(localeDir, function (err, files) { 8 | //files is array of filenames (basename form) 9 | if (!(files && files.length)) return 10 | for (var i = 0, len = files.length; i < len; i++) { 11 | var matchEn = files[i].match(/en-US\.pak/) 12 | var matchFa = files[i].match(/fa\.pak/) 13 | if (matchEn === null && matchFa === null) { 14 | fs.unlinkSync(localeDir + files[i]) 15 | } 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/main/shared/file.ts: -------------------------------------------------------------------------------- 1 | import { access, constants as fileConstant } from 'node:fs/promises' 2 | import { Server } from '../../shared/interfaces/server.interface' 3 | import { join } from 'node:path' 4 | 5 | export async function checkFileExists(filePath: string): Promise { 6 | try { 7 | await access(filePath, fileConstant.F_OK) 8 | return true 9 | } catch (e) { 10 | return false 11 | } 12 | } 13 | 14 | export async function getOverlayIcon(server: Server): Promise { 15 | return getPublicFilePath('icons/icon-connected.png') 16 | } 17 | 18 | export function getPublicFilePath(filePath: string): string { 19 | return join(process.env.PUBLIC, filePath) 20 | } 21 | -------------------------------------------------------------------------------- /src/main/platforms/platform.ts: -------------------------------------------------------------------------------- 1 | import sudo from 'sudo-prompt' 2 | 3 | export abstract class Platform { 4 | public abstract setDns(nameServers: string[]): Promise 5 | 6 | public abstract getActiveDns(): Promise 7 | 8 | public abstract clearDns(): Promise 9 | 10 | public abstract getInterfacesList(): Promise 11 | 12 | public abstract flushDns(): Promise 13 | 14 | protected execCmd(cmd: string): Promise { 15 | return new Promise((resolve, reject) => { 16 | sudo.exec(cmd, { name: 'dnsChanger' }, (error, stdout) => { 17 | if (error) { 18 | reject(error) 19 | return 20 | } 21 | resolve(stdout) 22 | }) 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/component/dropdowns/server-options/server-options.component.tsx: -------------------------------------------------------------------------------- 1 | import { Dropdown } from 'react-daisyui' 2 | import { IoEllipsisHorizontalSharp } from 'react-icons/io5' 3 | 4 | import { Server } from '../../../../shared/interfaces/server.interface' 5 | import { useI18nContext } from '../../../../i18n/i18n-react' 6 | 7 | interface Props { 8 | server: Server 9 | } 10 | 11 | export function ServerOptionsComponent(props: Props) { 12 | const { LL, locale } = useI18nContext() 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | DNS Changer 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/renderer/utils/icons.util.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | MdOutlineSignalCellularAlt, 3 | MdOutlineSignalCellularAlt1Bar, 4 | MdOutlineSignalCellularAlt2Bar, 5 | } from 'react-icons/md' 6 | 7 | export function getPingIcon(ping: number): JSX.Element { 8 | switch (true) { 9 | case ping === -1: 10 | return ( 11 | 12 | ) 13 | case ping <= 100: 14 | return 15 | case ping <= 180: 16 | return ( 17 | 18 | ) 19 | default: 20 | return ( 21 | 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/utils/theme.util.ts: -------------------------------------------------------------------------------- 1 | export async function themeChanger( 2 | theme: 'dark' | 'light' | 'system', 3 | ): Promise { 4 | const res = await window.ui.toggleTheme(theme) 5 | const newTheme = res ? 'dark' : 'light' 6 | 7 | const doc = document.querySelector('html') 8 | for (const c of doc.classList) { 9 | doc.classList.remove(c) 10 | } 11 | document.querySelector('html').classList.add(newTheme) 12 | } 13 | 14 | type returnTheme = 'dark' | 'light' 15 | 16 | export function getThemeSystem(): returnTheme { 17 | if ( 18 | // biome-ignore lint/complexity/useOptionalChain: 19 | window.matchMedia && 20 | window.matchMedia('(prefers-color-scheme: dark)').matches 21 | ) { 22 | return 'dark' 23 | } 24 | 25 | return 'light' 26 | } 27 | -------------------------------------------------------------------------------- /src/main/shared/__test__/getIconPath.spec.ts: -------------------------------------------------------------------------------- 1 | import { getIconPath } from '../getIconPath' 2 | 3 | describe('getIconPath()', () => { 4 | it("should return 'win32' path", () => { 5 | Object.defineProperty(process, 'platform', { 6 | value: 'win32', 7 | }) 8 | 9 | expect(getIconPath()).toMatch(/\\src\\main\\shared\\assets\\icon.ico/) 10 | }) 11 | it("should return 'darwin' path", () => { 12 | Object.defineProperty(process, 'platform', { 13 | value: 'darwin', 14 | }) 15 | 16 | expect(getIconPath()).toMatch(/\\src\\main\\shared\\assets\\icon.ico/) 17 | }) 18 | it("should return 'linux' path", () => { 19 | Object.defineProperty(process, 'platform', { 20 | value: 'linux', 21 | }) 22 | 23 | expect(getIconPath()).toMatch(/\\src\\main\\shared\\assets\\icon.png/) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/renderer/component/servers/servers.tsx: -------------------------------------------------------------------------------- 1 | import 'react-daisyui' 2 | import React, { useContext } from 'react' 3 | import { ServerComponent } from './server.component' 4 | import { serversContext } from '../../context/servers.context' 5 | import { ServersContext } from '../../interfaces/servers-context.interface' 6 | 7 | interface Props {} 8 | 9 | export function ServersComponent(props: Props) { 10 | const serversContextData = useContext(serversContext) 11 | 12 | return ( 13 |
14 | {serversContextData.servers.map((server) => ( 15 | 21 | ))} 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin', 'prettier'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | browser: true, 18 | }, 19 | ignorePatterns: ['.eslintrc.js'], 20 | rules: { 21 | '@typescript-eslint/interface-name-prefix': 'off', 22 | '@typescript-eslint/explicit-function-return-type': 'off', 23 | '@typescript-eslint/explicit-module-boundary-types': 'off', 24 | '@typescript-eslint/no-explicit-any': 'off', 25 | 'prettier/prettier': 'error', // Added this rule to enforce Prettier 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /src/i18n/i18n-react.tsx: -------------------------------------------------------------------------------- 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. 2 | /* eslint-disable */ 3 | 4 | import { useContext } from 'react' 5 | import { initI18nReact } from 'typesafe-i18n/react' 6 | import type { I18nContextType } from 'typesafe-i18n/react' 7 | import type { 8 | Formatters, 9 | Locales, 10 | TranslationFunctions, 11 | Translations, 12 | } from './i18n-types' 13 | import { loadedFormatters, loadedLocales } from './i18n-util' 14 | 15 | const { component: TypesafeI18n, context: I18nContext } = initI18nReact< 16 | Locales, 17 | Translations, 18 | TranslationFunctions, 19 | Formatters 20 | >(loadedLocales, loadedFormatters) 21 | 22 | const useI18nContext = (): I18nContextType< 23 | Locales, 24 | Translations, 25 | TranslationFunctions 26 | > => useContext(I18nContext) 27 | 28 | export { I18nContext, useI18nContext } 29 | 30 | export default TypesafeI18n 31 | -------------------------------------------------------------------------------- /src/i18n/i18n-util.sync.ts: -------------------------------------------------------------------------------- 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. 2 | /* eslint-disable */ 3 | 4 | import { initFormatters } from './formatters' 5 | import type { Locales, Translations } from './i18n-types' 6 | import { loadedFormatters, loadedLocales, locales } from './i18n-util' 7 | 8 | import eng from './eng' 9 | import fa from './fa' 10 | import ru from './ru' 11 | 12 | const localeTranslations = { 13 | eng, 14 | fa, 15 | ru, 16 | } 17 | 18 | export const loadLocale = (locale: Locales): void => { 19 | if (loadedLocales[locale]) return 20 | 21 | loadedLocales[locale] = localeTranslations[locale] as unknown as Translations 22 | loadFormatters(locale) 23 | } 24 | 25 | export const loadAllLocales = (): void => locales.forEach(loadLocale) 26 | 27 | export const loadFormatters = (locale: Locales): void => 28 | void (loadedFormatters[locale] = initFormatters(locale)) 29 | -------------------------------------------------------------------------------- /src/main/ipc/setting.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain } from 'electron' 2 | import { store } from '../store/store' 3 | import { EventsKeys } from '../../shared/constants/eventsKeys.constant' 4 | 5 | import { Settings } from '../../shared/interfaces/settings.interface' 6 | 7 | ipcMain.handle(EventsKeys.SAVE_SETTINGS, (event, data: Settings) => { 8 | store.set('settings', data) 9 | return { success: true, data } 10 | }) 11 | 12 | ipcMain.handle(EventsKeys.TOGGLE_START_UP, async () => { 13 | try { 14 | const setting = store.get('settings') 15 | 16 | setting.startUp = !setting.startUp //toggle 17 | 18 | app.setLoginItemSettings({ 19 | openAtLogin: setting.startUp, 20 | path: app.getPath('exe'), 21 | }) 22 | 23 | store.set('settings', setting) 24 | 25 | return setting.startUp 26 | } catch {} 27 | }) 28 | 29 | ipcMain.handle(EventsKeys.GET_SETTINGS, async () => { 30 | const settings: Settings = store.get('settings') 31 | return settings 32 | }) 33 | -------------------------------------------------------------------------------- /assets/flags/usa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const withMT = require('@material-tailwind/react/utils/withMT') 2 | 3 | module.exports = withMT({ 4 | content: [ 5 | './src/**/*.{js,jsx,ts,tsx}', 6 | './node_modules/@material-tailwind/react/components/**/*.{js,ts,jsx,tsx}', 7 | './node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}', 8 | 'node_modules/daisyui/dist/**/*.js', 9 | 'node_modules/react-daisyui/dist/**/*.js', 10 | ], 11 | darkMode: 'class', 12 | theme: { 13 | extend: { 14 | colors: {}, 15 | keyframes: { 16 | fadeIn: { 17 | from: { 18 | opacity: '0', 19 | }, 20 | to: { 21 | opacity: '1', 22 | }, 23 | }, 24 | }, 25 | animation: { 26 | fadeIn: 'fadeIn 0.3s ease-in-out', 27 | }, 28 | }, 29 | }, 30 | daisyui: { 31 | styled: true, 32 | base: true, 33 | utils: true, 34 | logs: false, 35 | rtl: false, 36 | 37 | themes: ['light', 'dark'], 38 | }, 39 | 40 | plugins: [require('daisyui'), require('tailwindcss-flip')], 41 | }) 42 | -------------------------------------------------------------------------------- /src/shared/constants/servers.cosntant.ts: -------------------------------------------------------------------------------- 1 | import { Server, ServerStore } from '../interfaces/server.interface' 2 | 3 | export const serversConstant: Array = [ 4 | { 5 | key: 'SHECAN', 6 | name: 'Shecan', 7 | servers: ['178.22.122.100', '185.51.200.2'], 8 | avatar: 'shecan.png', 9 | rate: 10, 10 | tags: ['Gaming', 'Web', 'Ai'], 11 | isPin: false, 12 | }, 13 | { 14 | key: 'ELECTRO', 15 | name: 'Electro Team', 16 | servers: ['78.157.42.100', '78.157.42.101'], 17 | avatar: 'electro.png', 18 | rate: 9, 19 | tags: ['Gaming', 'Web', 'Ai'], 20 | isPin: false, 21 | }, 22 | { 23 | key: 'RADAR_GAME', 24 | name: 'Radar game', 25 | servers: ['10.202.10.10', '10.202.10.11'], 26 | avatar: 'radar.png', 27 | rate: 5, 28 | tags: ['Gaming'], 29 | isPin: false, 30 | }, 31 | 32 | { 33 | key: 'ClOUD_FLARE', 34 | name: 'Cloudflare', 35 | servers: ['1.1.1.1', '1.0.0.1'], 36 | avatar: 'cloudflare.png', 37 | rate: 0, 38 | tags: ['Web'], 39 | isPin: false, 40 | }, 41 | ] 42 | -------------------------------------------------------------------------------- /src/main/config.ts: -------------------------------------------------------------------------------- 1 | import os from 'os' 2 | import { app } from 'electron' 3 | import AutoLaunch from 'auto-launch' 4 | 5 | import { Platform } from './platforms/platform' 6 | import { DnsService } from './services/dns.service' 7 | import { LinuxPlatform } from './platforms/linux/linux.platform' 8 | import { WindowsPlatform } from './platforms/windows/windows.platform' 9 | import { MacPlatform } from './platforms/mac/mac.platform' 10 | 11 | let platform: Platform 12 | 13 | switch (os.platform()) { 14 | case 'win32': 15 | platform = new WindowsPlatform() 16 | break 17 | case 'linux': 18 | platform = new LinuxPlatform() 19 | break 20 | case 'darwin': 21 | platform = new MacPlatform() 22 | break 23 | default: 24 | throw new Error('INVALID_PLATFORM') 25 | } 26 | 27 | export const dnsService: DnsService = new DnsService(platform) 28 | 29 | export const autoLauncher = new AutoLaunch({ 30 | name: app.getName(), 31 | isHidden: false, // show the app on startup 32 | mac: { 33 | useLaunchAgent: true, // use a launch agent instead of a launch daemon (macOS only) 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_Request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Feature Request" 2 | description: "I have a suggestion \U0001F63B!" 3 | title: "[Parser]:" 4 | labels: ["type: enhancement :wolf:"] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: "Is there an existing issue that is already proposing this?" 9 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the feature you are requesting" 10 | options: 11 | - label: "I have searched the existing issues" 12 | required: true 13 | 14 | - type: textarea 15 | validations: 16 | required: true 17 | attributes: 18 | label: "🪄 describe your feature or idea" 19 | description: "A clear and concise description of what you need to implement" 20 | placeholder: | 21 | I have an idea ... 22 | 23 | - type: checkboxes 24 | validations: 25 | required: false 26 | attributes: 27 | label: "💻 In which operating systems have you tested?" 28 | options: 29 | - label: macOS 30 | - label: Windows 31 | - label: Linux 32 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "tab" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true, 23 | "a11y": { 24 | "useAltText": "off", 25 | "useButtonType": "off", 26 | "useKeyWithClickEvents": "off", 27 | "noSvgWithoutTitle": "off", 28 | "noLabelWithoutControl": "off" 29 | }, 30 | "style": { 31 | "useSelfClosingElements": "off", 32 | "useImportType": "off" 33 | }, 34 | "suspicious": { 35 | "noExplicitAny": "off", 36 | "noArrayIndexKey": "off", 37 | "noDoubleEquals": "off" 38 | }, 39 | "complexity": { 40 | "noUselessCatch": "off" 41 | } 42 | } 43 | }, 44 | "javascript": { 45 | "formatter": { 46 | "quoteStyle": "single", 47 | "semicolons": "asNeeded" 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 DnsChanger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/renderer/component/buttons/select-interface-btn-component.tsx: -------------------------------------------------------------------------------- 1 | import { TbCloudDataConnection } from 'react-icons/tb' 2 | import { Button } from 'react-daisyui' 3 | import { useContext, useState } from 'react' 4 | import { AddDnsModalComponent } from '../modals/add-dns.component' 5 | import { serversContext } from '../../context/servers.context' 6 | 7 | export function SelectInterfaceBtnComponent() { 8 | const [isOpenModal, setIsOpenModal] = useState(false) 9 | const serversStateContext = useContext(serversContext) 10 | 11 | function toggleOpenModal() { 12 | setIsOpenModal(!isOpenModal) 13 | } 14 | 15 | return ( 16 |
17 | 27 | { 31 | serversStateContext.servers.push(va) 32 | serversStateContext.setServers([...serversStateContext.servers]) 33 | }} 34 | /> 35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/renderer/notifications/appNotif.tsx: -------------------------------------------------------------------------------- 1 | import toast from 'react-hot-toast' 2 | import { BiErrorAlt } from 'react-icons/bi' 3 | import { TiInputChecked } from 'react-icons/ti' 4 | 5 | export function appNotif( 6 | title: string, 7 | msg: string, 8 | type: 'SUCCESS' | 'ERROR' = 'ERROR', 9 | ) { 10 | return toast.custom( 11 | (t) => ( 12 |
15 |
16 |
17 |
18 | {type == 'SUCCESS' ? ( 19 | 20 | ) : ( 21 | 22 | )} 23 |
24 |
25 |

{title}

26 |

27 | {msg} 28 |

29 |
30 |
31 |
32 |
33 | ), 34 | { 35 | position: 'top-center', 36 | duration: 200, 37 | }, 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/i18n/i18n-util.async.ts: -------------------------------------------------------------------------------- 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. 2 | /* eslint-disable */ 3 | 4 | import { initFormatters } from './formatters' 5 | import type { Locales, Translations } from './i18n-types' 6 | import { loadedFormatters, loadedLocales, locales } from './i18n-util' 7 | 8 | const localeTranslationLoaders = { 9 | eng: () => import('./eng'), 10 | fa: () => import('./fa'), 11 | ru: () => import('./ru'), 12 | } 13 | 14 | const updateDictionary = ( 15 | locale: Locales, 16 | dictionary: Partial, 17 | ): Translations => 18 | (loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary }) 19 | 20 | export const importLocaleAsync = async ( 21 | locale: Locales, 22 | ): Promise => 23 | (await localeTranslationLoaders[locale]()).default as unknown as Translations 24 | 25 | export const loadLocaleAsync = async (locale: Locales): Promise => { 26 | updateDictionary(locale, await importLocaleAsync(locale)) 27 | loadFormatters(locale) 28 | } 29 | 30 | export const loadAllLocalesAsync = (): Promise => 31 | Promise.all(locales.map(loadLocaleAsync)) 32 | 33 | export const loadFormatters = (locale: Locales): void => 34 | void (loadedFormatters[locale] = initFormatters(locale)) 35 | -------------------------------------------------------------------------------- /src/main/shared/serve.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import http from 'node:http' 3 | import type { BrowserWindow } from 'electron' 4 | import handler from 'serve-handler' 5 | import getPorts from './get-port' 6 | import Url from 'node:url' 7 | import isDev from './isDev' 8 | 9 | // Internals 10 | // ========= 11 | const isDevelopment = isDev 12 | 13 | // Dynamic Renderer 14 | // ================ 15 | export default async function (mainWindow: BrowserWindow) { 16 | if (isDevelopment) { 17 | const startUrl = 18 | process.env.VITE_DEV_SERVER_URL || 19 | Url.format({ 20 | pathname: path.join(process.env.DIST, 'index.html'), 21 | protocol: 'file:', 22 | slashes: true, 23 | }) 24 | 25 | return mainWindow.loadURL(startUrl) 26 | } 27 | 28 | const port = await getPorts({ port: 55303 }) 29 | 30 | const server = http.createServer((request, response) => { 31 | return handler(request, response, { 32 | public: process.env.DIST, 33 | // public: process.env.PUBLIC, 34 | directoryListing: false, 35 | }) 36 | }) 37 | 38 | await new Promise((resolve) => { 39 | server.listen(port, () => { 40 | console.log('Dynamic-Renderer Listening on', port) 41 | mainWindow.loadURL(`http://localhost:${port}`) 42 | resolve(true) 43 | }) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/renderer/loading_window/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DNS Changer (OpenSource) 7 | 40 | 41 | 42 | 43 |
44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/renderer/component/buttons/flush-dns-btn-component.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Button, Tooltip } from 'react-daisyui' 3 | import { FaBroom } from 'react-icons/fa' 4 | import { appNotif } from '../../notifications/appNotif' 5 | 6 | export function FlushDNS_BtnComponent() { 7 | const [loading, setLoading] = useState(false) 8 | 9 | async function handleClick() { 10 | if (loading) return 11 | setLoading(true) 12 | const result = await window.ipc.flushDns() 13 | setLoading(false) 14 | if (result.success) { 15 | appNotif('Success', `DNS Flushed Successfully`, 'SUCCESS') 16 | } else { 17 | appNotif('Failed', `Failed to Flush DNS`, 'ERROR') 18 | } 19 | } 20 | 21 | return ( 22 |
23 | 24 | 38 | 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/shared/constants/eventsKeys.constant.ts: -------------------------------------------------------------------------------- 1 | export enum EventsKeys { 2 | SET_DNS = 'dialogs:set-dns', 3 | CLEAR_DNS = 'dialogs:clear-dns', 4 | ADD_DNS = 'dialogs:add-dns', 5 | RELOAD_SERVER_LIST = 'reloadServerList', 6 | FETCH_DNS_LIST = 'dialogs:fetch_dns_list', 7 | NOTIFICATION = 'notification', 8 | DIALOG_ERROR = 'dialogs:d_error', 9 | OPEN_BROWSER = 'dialogs:open_browser', 10 | GET_CURRENT_ACTIVE = 'dialogs:get_current_active', 11 | DELETE_DNS = 'DELETE_DNS', 12 | TOGGLE_THEME = 'ui:toggleTheme', 13 | GET_SETTINGS = 'setting:get', 14 | SET_NETWORK_INTERFACE = 'setting:set_network_interface', 15 | GET_NETWORK_INTERFACE_LIST = 'setting:get_network_interface_list', 16 | SAVE_SETTINGS = 'setting:save', 17 | TOGGLE_START_UP = 'setting:toggleStartUp', 18 | FLUSHDNS = 'dialogs:flushDns', 19 | PING = 'dialogs:ping', 20 | TOGGLE_PIN = 'dialog:togglePin', 21 | CHECK_UPDATE = 'CHECK_UPDATE', 22 | START_UPDATE = 'START_UPDATE', 23 | UPDATE_PROGRESS = 'UPDATE_PROGRESS', 24 | UPDATE_ERROR = 'UPDATE_ERROR', 25 | CLOSE = 'close', 26 | MINIMIZE = 'MINIMIZE_APP', 27 | OPEN_LOG_FILE = 'dialogs:open_log_file', 28 | OPEN_DEV_TOOLS = 'dialogs:open_dev_tools', 29 | SCHEDULE_SHUTDOWN = 'shutdown:schedule', 30 | CANCEL_SCHEDULED_SHUTDOWN = 'shutdown:cancel', 31 | CLEAR_ALL_SHUTDOWNS = 'shutdown:clear_all', 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build on push 2 | 3 | on: 4 | push: 5 | branches-ignore: ["main"] 6 | paths-ignore: 7 | - "README.md" 8 | - "README-*.md" 9 | - ".github/ISSUE_TEMPLATE/*" 10 | 11 | jobs: 12 | build_on_linux: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | 18 | - name: Install Node v20 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 20 22 | cache: npm 23 | 24 | - name: Install dependencies 25 | run: npm ci 26 | 27 | - name: Install electron-builder 28 | run: npm i electron-builder -g 29 | 30 | - name: Build typescript files 31 | run: npm run build:code 32 | 33 | 34 | 35 | build_on_win: 36 | runs-on: windows-latest 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v3 40 | 41 | - name: Install Node v20 42 | uses: actions/setup-node@v3 43 | with: 44 | node-version: 20 45 | cache: npm 46 | 47 | - name: Install dependencies 48 | run: npm ci 49 | 50 | - name: Install electron-builder 51 | run: npm i electron-builder -g 52 | 53 | - name: Build typescript files 54 | run: npm run build:code 55 | -------------------------------------------------------------------------------- /src/renderer/component/dropdowns/serverlist-options/serverlist-options.component.tsx: -------------------------------------------------------------------------------- 1 | import { Dropdown } from 'react-daisyui' 2 | import { useState, useContext } from 'react' 3 | import { IoEllipsisVertical } from 'react-icons/io5' 4 | 5 | import { UpdateListItemComponent } from './updatelist.item' 6 | import { serversContext } from '../../../context/servers.context' 7 | import { AddDnsModalComponent } from '../../modals/add-dns.component' 8 | import { ServersContext } from '../../../interfaces/servers-context.interface' 9 | 10 | export function ServerListOptionsDropDownComponent() { 11 | const [isOpenModal, setIsOpenModal] = useState() 12 | const serversContextData: ServersContext = 13 | useContext(serversContext) 14 | 15 | function toggleOpenModal() { 16 | setIsOpenModal(!isOpenModal) 17 | } 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | { 31 | serversContextData.servers.push(va) 32 | serversContextData.setServers([...serversContextData.servers]) 33 | }} 34 | /> 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/pull.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Workflow 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | paths-ignore: 9 | - "*.md" 10 | 11 | jobs: 12 | build_on_linux: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | 18 | - name: Install Node v20 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 20 22 | cache: npm 23 | 24 | - name: Install dependencies 25 | run: npm ci 26 | 27 | - name: Install electron-builder 28 | run: npm i electron-builder -g 29 | 30 | - name: Build typescript files 31 | run: npm run build:code 32 | 33 | 34 | 35 | build_on_win: 36 | runs-on: windows-latest 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v3 40 | 41 | - name: Install Node v20 42 | uses: actions/setup-node@v3 43 | with: 44 | node-version: 20 45 | cache: npm 46 | 47 | - name: Install dependencies 48 | run: npm ci 49 | 50 | - name: Install electron-builder 51 | run: npm i electron-builder -g 52 | 53 | - name: Build typescript files 54 | run: npm run build:code 55 | -------------------------------------------------------------------------------- /src/main/platforms/linux/linux.platform.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from '../platform' 2 | 3 | export class LinuxPlatform extends Platform { 4 | async clearDns(): Promise { 5 | try { 6 | await this.setDns(['1.1.1.1', '8.8.8.8', '192.168.1.1', '127.0.0.1']) 7 | } catch (e) { 8 | throw e 9 | } 10 | } 11 | 12 | async getActiveDns(): Promise> { 13 | try { 14 | const cmd = "grep nameserver /etc/resolv.conf | awk '{print $2}'" 15 | const text = (await this.execCmd(cmd)) as string 16 | 17 | const regex = /(?<=nameserver\s)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g 18 | return text.trim().match(regex) 19 | } catch (e) { 20 | throw e 21 | } 22 | } 23 | 24 | async getInterfacesList(): Promise { 25 | return [] 26 | } 27 | 28 | async setDns(nameServers: Array): Promise { 29 | try { 30 | let lines = '' 31 | 32 | for (let i = 0; i < nameServers.length; i++) { 33 | lines += `nameserver ${nameServers[i]}\n` 34 | } 35 | 36 | const cmd = `echo '${lines.trim()}' > /etc/resolv.conf` 37 | await this.execCmd(cmd) 38 | 39 | const cmdRestart = 'systemctl restart systemd-networkd' 40 | await this.execCmd(cmdRestart) 41 | } catch (e) { 42 | throw e 43 | } 44 | } 45 | 46 | public async flushDns(): Promise { 47 | await this.execCmd('systemd-resolve --flush-caches') 48 | } 49 | } 50 | // Powered by ChatGpt 51 | -------------------------------------------------------------------------------- /src/renderer/component/buttons/addDns-btn.component.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from 'react' 2 | import { Button, Tooltip } from 'react-daisyui' 3 | import { MdOutlineAddModerator } from 'react-icons/md' 4 | import { serversContext } from '../../context/servers.context' 5 | import { AddDnsModalComponent } from '../modals/add-dns.component' 6 | 7 | export function AddCustomDnsButton() { 8 | const [isOpenModal, setIsOpenModal] = useState(false) 9 | const serversStateContext = useContext(serversContext) 10 | 11 | function toggleOpenModal() { 12 | setIsOpenModal(!isOpenModal) 13 | } 14 | 15 | return ( 16 |
17 | 18 | 31 | { 35 | serversStateContext.servers.push(va) 36 | serversStateContext.setServers([...serversStateContext.servers]) 37 | }} 38 | /> 39 | 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/loading_window/sc.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | 4 | declare const VERSION: string 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root')) 7 | 8 | root.render(LoadingCom()) 9 | 10 | function LoadingCom() { 11 | const notes: string[] = [ 12 | 'این برنامه به صورت متن باز می باشد.', 13 | 'برای استفاده بهینه از این برنامه، آخرین ورژن مرورگر خود را داشته باشید.', 14 | 'شما میتونید سرور دلخواه خود را اضافه کنید.', 15 | ] 16 | const note = notes[Math.floor(Math.random() * notes.length)] 17 | return ( 18 |
22 |
23 |
24 |

{note}

25 |
26 |
27 |

31 | در حال بارگذاری ... 32 |

33 |
34 |
35 |

36 | {getVersion()} 37 |

38 |
39 |
40 | ) 41 | } 42 | 43 | function getVersion() { 44 | let versionToDisplay = 'unknown' 45 | try { 46 | versionToDisplay = VERSION 47 | } catch (error) { 48 | } finally { 49 | return versionToDisplay 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/component/buttons/interfaces-dialog-btn-component.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from 'react' 2 | import { Button, Tooltip } from 'react-daisyui' 3 | import { BsHddNetwork } from 'react-icons/bs' 4 | import { serversContext } from '../../context/servers.context' 5 | import { NetworkOptionsModalComponent } from '../modals/network-options.component' 6 | 7 | export function InterfacesDialogButtonComponent() { 8 | const [isOpenModal, setIsOpenModal] = useState(false) 9 | const serversStateContext = useContext(serversContext) 10 | 11 | function toggleOpenModal() { 12 | setIsOpenModal(!isOpenModal) 13 | } 14 | 15 | return ( 16 |
17 | 18 | 31 | { 35 | serversStateContext.servers.push(va) 36 | serversStateContext.setServers([...serversStateContext.servers]) 37 | }} 38 | /> 39 | 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /assets/flags/iran.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_Report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41B Bug Report" 2 | description: "If something isn't working as expected \U0001F914" 3 | title: "[Bug]:" 4 | labels: ["type: potential issue :broken_heart:"] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: "🤔 Is there an existing issue for this?" 9 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the bug you encountered" 10 | options: 11 | - label: "I have searched the existing issues" 12 | required: true 13 | 14 | - type: textarea 15 | validations: 16 | required: true 17 | attributes: 18 | label: "📝 Discription" 19 | description: | 20 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in 21 | 22 | - type: markdown 23 | attributes: 24 | value: | 25 | --- 26 | 27 | - type: checkboxes 28 | validations: 29 | required: true 30 | attributes: 31 | label: "💻 In which operating systems have you tested?" 32 | options: 33 | - label: macOS 34 | - label: Windows 35 | - label: Linux 36 | 37 | - type: markdown 38 | attributes: 39 | value: | 40 | --- 41 | - type: textarea 42 | attributes: 43 | label: "Other" 44 | description: | 45 | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. 46 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in -------------------------------------------------------------------------------- /src/main/platforms/mac/mac.platform.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'node:child_process' 2 | import { promisify } from 'node:util' 3 | import { Platform } from '../platform' 4 | 5 | const execPromise = promisify(exec) 6 | 7 | export class MacPlatform extends Platform { 8 | async clearDns(): Promise { 9 | try { 10 | await this.setDns(['8.8.8.8', '8.8.4.4']) 11 | } catch (e) { 12 | throw e 13 | } 14 | } 15 | 16 | async getActiveDns(): Promise { 17 | try { 18 | const { stdout } = await execPromise( 19 | "scutil --dns | awk '/nameserver/ { print $3 }'", 20 | ) 21 | 22 | return stdout.trim().split('\n') 23 | } catch (e) { 24 | throw e 25 | } 26 | } 27 | 28 | async getInterfacesList(): Promise { 29 | // Implement the logic to retrieve the list of interfaces for macOS here 30 | // Example: You can use the "networksetup" command to get the list of interfaces 31 | // For simplicity, I'm returning an empty array for now 32 | return [] 33 | } 34 | 35 | async setDns(nameServers: string[]): Promise { 36 | try { 37 | const dnsServers = nameServers.join(' ') 38 | 39 | await execPromise(`networksetup -setdnsservers Wi-Fi ${dnsServers}`) 40 | 41 | try { 42 | await execPromise(`networksetup -setdnsservers Ethernet ${dnsServers}`) 43 | } catch (e) { 44 | // ignore if device don't have Ethernet 45 | } 46 | } catch (e) { 47 | throw e 48 | } 49 | } 50 | 51 | public async flushDns(): Promise { 52 | try { 53 | await execPromise('sudo killall -HUP mDNSResponder') 54 | } catch (e) { 55 | throw e 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/renderer/component/buttons/togglePin-btn.component.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react' 2 | import { Button as ButtonDaisyui } from 'react-daisyui' 3 | 4 | import { serversContext } from '../../context/servers.context' 5 | import { ServersContext } from '../../interfaces/servers-context.interface' 6 | import { BsPin, BsPinFill } from 'react-icons/bs' 7 | 8 | export function ToggleButtonComponent() { 9 | const serversStateContext = useContext(serversContext) 10 | const server = serversStateContext.selected 11 | const [isPin, setIsPin] = useState() 12 | 13 | useEffect(() => { 14 | if (serversStateContext.selected) 15 | setIsPin(serversStateContext.selected.isPin) 16 | }, [serversStateContext.selected]) 17 | 18 | if (!server) return null 19 | 20 | async function handleClick() { 21 | const res = await window.ipc.togglePinServer(server) 22 | if (res.success) { 23 | server.isPin = !server.isPin 24 | setIsPin(server.isPin) 25 | console.log(server.isPin) 26 | serversStateContext.setServers(res.servers) 27 | } 28 | } 29 | 30 | return ( 31 | <> 32 | 40 | {isPin ? ( 41 | 42 | ) : ( 43 | 44 | )} 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # TypeScript cache 43 | *.tsbuildinfo 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env.test 62 | 63 | # parcel-bundler cache (https://parceljs.org/) 64 | .cache 65 | 66 | # next.js build output 67 | .next 68 | 69 | # nuxt.js build output 70 | .nuxt 71 | 72 | # vuepress build output 73 | .vuepress/dist 74 | 75 | # Serverless directories 76 | .serverless/ 77 | 78 | # FuseBox cache 79 | .fusebox/ 80 | 81 | # DynamoDB Local files 82 | .dynamodb/ 83 | 84 | # Webpack 85 | .webpack/ 86 | 87 | # Electron-Forge 88 | out/ 89 | 90 | .idea 91 | 92 | ./store 93 | 94 | dist-electron 95 | release 96 | dist 97 | .env 98 | dev-app-update.yml 99 | diff.txt 100 | -------------------------------------------------------------------------------- /src/i18n/fa/index.ts: -------------------------------------------------------------------------------- 1 | import { BaseTranslation } from '../i18n-types' 2 | 3 | const fa: BaseTranslation = { 4 | pages: { 5 | home: { 6 | homeTitle: 'بهترین های رفع تحریم', 7 | connectedHTML: 'شما به {currentActive} متصل شدید', 8 | connected: 'شما به {currentActive} متصل شدید', 9 | disconnected: 'قطع شد.', 10 | unknownServer: 'به یک سرور ناشناخته متصل هستید.', 11 | }, 12 | settings: { 13 | title: 'تنظیمات', 14 | autoRunningTitle: 'اجرا شدن خودکار برنامه با روشن شدن سیستم', 15 | langChanger: 'تغییر زبـان', 16 | themeChanger: 'تغییر پوسته', 17 | }, 18 | addCustomDns: { 19 | NameOfServer: 'نام سرور', 20 | serverAddr: 'آدرس سرور', 21 | }, 22 | }, 23 | themeChanger: { 24 | dark: 'تاریک', 25 | light: 'روشـن', 26 | }, 27 | buttons: { 28 | update: 'بروز رسانی لیست', 29 | favDnsServer: 'افزودن سرور (DNS) دلخواه', 30 | add: 'افزودن', 31 | flushDns: 'پاکسازی (Flush)', 32 | ping: 'پیـنگ سرورها', 33 | }, 34 | dialogs: { 35 | fetching_data_from_repo: 'درحال دریافت دیتا از مخزن', 36 | added_server: 'سرور {serverName} با موفقیت اضافه شد.', 37 | removed_server: 'سرور {serverName} با موفقیت حذف شد.', 38 | flush_successful: 'پاکسازی با موفقیت انجام شد.', 39 | flush_failure: 'پاکسازی ناموفق بود.', 40 | }, 41 | errors: { 42 | error_fetching_data: 'خطا در دریافت دیتا از {target}', 43 | }, 44 | connecting: 'درحال اتصال...', 45 | disconnecting: 'قطع شدن...', 46 | waiting: 'کمی صبر کنید...', 47 | successful: 'موفقیت آمیز', 48 | help_connect: 'برای اتصال کلیک کنید', 49 | help_disconnect: 'برای قطع اتصال کلیک کنید', 50 | validator: { 51 | invalid_dns1: 'آدرس سرور 1 نامعتبر است.', 52 | invalid_dns2: 'آدرس سرور 2 نامعتبر است.', 53 | dns1_dns2_duplicates: 'آدرس سرورهای 1 و 2 نباید تکراری باشند.', 54 | }, 55 | version: 'نسخه', 56 | } 57 | 58 | export default fa 59 | -------------------------------------------------------------------------------- /src/i18n/eng/index.ts: -------------------------------------------------------------------------------- 1 | import type { Translation } from '../i18n-types' 2 | 3 | const eng: Translation = { 4 | pages: { 5 | home: { 6 | homeTitle: 'DNS Changer', 7 | connectedHTML: 'Connected to {currentActive}', 8 | connected: 'Connected to {currentActive}', 9 | disconnected: 'Disconnected', 10 | unknownServer: 'connected to an unknown server.', 11 | }, 12 | settings: { 13 | title: 'Settings', 14 | autoRunningTitle: 15 | 'Automatic execution of the program when the system is turned on', 16 | langChanger: 'Language Changer', 17 | themeChanger: 'Theme', 18 | }, 19 | addCustomDns: { 20 | NameOfServer: 'Server name', 21 | serverAddr: 'Server address', 22 | }, 23 | }, 24 | themeChanger: { 25 | dark: 'Dark', 26 | light: 'Light', 27 | }, 28 | buttons: { 29 | update: 'Update the list', 30 | favDnsServer: 'Adding a custom (DNS) server', 31 | add: 'Add', 32 | flushDns: 'Flush', 33 | ping: 'Ping', 34 | }, 35 | waiting: 'Please wait...', 36 | disconnecting: 'disconnecting...', 37 | connecting: 'connecting...', 38 | successful: 'successful', 39 | help_connect: 'Click to connect', 40 | help_disconnect: 'Click to disconnect', 41 | dialogs: { 42 | fetching_data_from_repo: 'fetching data from repository...', 43 | removed_server: '{serverName} was successfully removed from the list.', 44 | added_server: 'Server {serverName} successfully added.', 45 | flush_successful: 'The flush was successful.', 46 | flush_failure: 'The flush failed.', 47 | }, 48 | errors: { 49 | error_fetching_data: 'Error in receiving data from the {target}', 50 | }, 51 | validator: { 52 | invalid_dns1: 'DNS value 1 is not valid.', 53 | invalid_dns2: 'DNS value 2 is not valid.', 54 | dns1_dns2_duplicates: 'DNS 1 and DNS 2 values must not be duplicates.', 55 | }, 56 | version: 'version', 57 | } 58 | 59 | export default eng 60 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: dnschanger.github.io 2 | productName: DNS Changer 3 | afterPack: "removeLocales.js" 4 | artifactName: "${productName}-${os}-${arch}-${version}.${ext}" 5 | files: 6 | - "**/*" 7 | - "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme,test,__tests__,tests,powered-test,example,examples}" 8 | - "!**/node_modules/.bin" 9 | - "!**/*.{o,hprof,orig,pyc,pyo,rbc}" 10 | - "!**/._*" 11 | - "!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,thumbs.db,.gitignore,.gitattributes,.editorconfig,.flowconfig,.yarn-metadata.json,.idea,appveyor.yml,.travis.yml,circle.yml,npm-debug.log,.nyc_output,yarn.lock,.yarn-integrity}" 12 | - "!**/node_modules/search-index/si${/*}" 13 | - '!**/*.md' # ignore md files 14 | - '!**/*.yml' # ignore yml files 15 | - '!**/.github' 16 | - '!**/release' 17 | asar: true 18 | compression: maximum 19 | directories: 20 | output: "release/${version}" 21 | win: 22 | icon: "public/icons/icon.ico" 23 | requestedExecutionLevel: "highestAvailable" 24 | publish: 25 | - github 26 | target: 27 | - zip 28 | - msi 29 | - nsis 30 | linux: 31 | icon: "public/icons/icon.icns" 32 | maintainer: "dnschanger.github.io" 33 | target: 34 | - rpm 35 | - AppImage 36 | - deb 37 | category: Utilities 38 | publish: 39 | - github 40 | mac: 41 | icon: "public/icons/icon.png" 42 | target: 43 | - dmg 44 | - zip 45 | category: Utilities 46 | publish: 47 | - github 48 | nsis: 49 | perMachine: true 50 | oneClick: false 51 | installerIcon: "public/icons/icon.ico" 52 | deleteAppDataOnUninstall: true 53 | runAfterFinish: true 54 | createDesktopShortcut: true 55 | allowToChangeInstallationDirectory: true 56 | shortcutName: "DNS Changer" 57 | npmRebuild: true 58 | nodeGypRebuild: false 59 | 60 | publish: 61 | provider: github 62 | owner: "DnsChanger" 63 | repo: "dnsChanger-desktop" 64 | -------------------------------------------------------------------------------- /src/renderer/component/buttons/updateList-btn.component.tsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import axios from 'axios' 3 | import { Button } from 'react-daisyui' 4 | import { useContext, useState } from 'react' 5 | import { FaRedoAlt } from 'react-icons/fa' 6 | 7 | import { setState } from '../../interfaces/react.interface' 8 | import { activityContext } from '../../context/activty.context' 9 | import { ActivityContext } from '../../interfaces/activty.interface' 10 | import { Server } from '../../../shared/interfaces/server.interface' 11 | import { useI18nContext } from '../../../i18n/i18n-react' 12 | import { UrlsConstant } from '../../../shared/constants/urls.constant' 13 | import { TfiReload } from 'react-icons/tfi' 14 | 15 | const cacheBuster = (url: string) => `${url}?cb=${Date.now()}` 16 | 17 | interface Props { 18 | servers: Server[] 19 | setServers: setState 20 | } 21 | 22 | export function UpdateListBtnComponent(prop: Props) { 23 | const [isLoading, setIsLoading] = useState(false) 24 | const activityContextData = useContext(activityContext) 25 | const { LL } = useI18nContext() 26 | 27 | async function updateHandler() { 28 | if (isLoading) return alert(LL.waiting()) //todo add toast 29 | try { 30 | setIsLoading(true) 31 | 32 | const response = await axios.get( 33 | cacheBuster(UrlsConstant.STORE), 34 | ) 35 | const servers = prop.servers.concat(response.data) 36 | const uniqList = _.uniqWith(servers, _.isEqual) 37 | 38 | prop.setServers(uniqList) 39 | } catch (error) { 40 | } finally { 41 | setIsLoading(false) 42 | } 43 | } 44 | 45 | return ( 46 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/i18n/ru/index.ts: -------------------------------------------------------------------------------- 1 | import { BaseTranslation, Translation } from '../i18n-types' 2 | 3 | const ru: Translation = { 4 | pages: { 5 | home: { 6 | homeTitle: 'Лучшее снятие санкций', 7 | connectedHTML: 'Вы подключены к {currentActive} ', 8 | connected: 'Вы подключены к {currentActive}', 9 | disconnected: 'Прервано', 10 | unknownServer: 'Вы подключены к неизвестному серверу', 11 | }, 12 | settings: { 13 | title: 'Настройки', 14 | autoRunningTitle: 15 | 'Автоматическое выполнение программы при включении системы', 16 | langChanger: 'Изменить язык', 17 | themeChanger: 'менять тему', 18 | }, 19 | addCustomDns: { 20 | NameOfServer: 'имя сервера', 21 | serverAddr: 'адрес сервера', 22 | }, 23 | }, 24 | themeChanger: { 25 | dark: 'темный', 26 | light: 'свет', 27 | }, 28 | buttons: { 29 | update: 'список обновлений', 30 | favDnsServer: 'добавить собственный (DNS) сервер', 31 | add: 'добавлять', 32 | flushDns: 'очистить (Flush)', 33 | ping: 'серверы пингуются', 34 | }, 35 | dialogs: { 36 | fetching_data_from_repo: 'получение данных из репозитория', 37 | added_server: '{serverName} сервер успешно добавлен', 38 | removed_server: '{serverName} сервер успешно удален', 39 | flush_successful: 'очистка успешно завершена', 40 | flush_failure: 'очистка не удалась', 41 | }, 42 | errors: { 43 | error_fetching_data: 'Ошибка при получении данных от {target}', 44 | }, 45 | connecting: 'подключение...', 46 | disconnecting: 'отключение...', 47 | waiting: 'пожалуйста, подождите...', 48 | successful: 'успешный', 49 | help_connect: 'нажмите, чтобы подключиться', 50 | help_disconnect: 'нажмите, чтобы отключить', 51 | validator: { 52 | invalid_dns1: 'DNS-значение 1 недопустимо.', 53 | invalid_dns2: 'DNS-значение 2 недопустимо.', 54 | dns1_dns2_duplicates: 'Значения DNS 1 и DNS 2 не должны совпадать.', 55 | }, 56 | version: 'версия', 57 | } 58 | 59 | export default ru 60 | -------------------------------------------------------------------------------- /src/i18n/i18n-util.ts: -------------------------------------------------------------------------------- 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. 2 | /* eslint-disable */ 3 | 4 | import { 5 | i18n as initI18n, 6 | i18nObject as initI18nObject, 7 | i18nString as initI18nString, 8 | } from 'typesafe-i18n' 9 | import type { LocaleDetector } from 'typesafe-i18n/detectors' 10 | import type { 11 | LocaleTranslationFunctions, 12 | TranslateByString, 13 | } from 'typesafe-i18n' 14 | import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors' 15 | import { initExtendDictionary } from 'typesafe-i18n/utils' 16 | import type { 17 | Formatters, 18 | Locales, 19 | Translations, 20 | TranslationFunctions, 21 | } from './i18n-types' 22 | 23 | export const baseLocale: Locales = 'fa' 24 | 25 | export const locales: Locales[] = ['eng', 'fa', 'ru'] 26 | 27 | export const isLocale = (locale: string): locale is Locales => 28 | locales.includes(locale as Locales) 29 | 30 | export const loadedLocales: Record = {} as Record< 31 | Locales, 32 | Translations 33 | > 34 | 35 | export const loadedFormatters: Record = {} as Record< 36 | Locales, 37 | Formatters 38 | > 39 | 40 | export const extendDictionary = initExtendDictionary() 41 | 42 | export const i18nString = (locale: Locales): TranslateByString => 43 | initI18nString(locale, loadedFormatters[locale]) 44 | 45 | export const i18nObject = (locale: Locales): TranslationFunctions => 46 | initI18nObject( 47 | locale, 48 | loadedLocales[locale], 49 | loadedFormatters[locale], 50 | ) 51 | 52 | export const i18n = (): LocaleTranslationFunctions< 53 | Locales, 54 | Translations, 55 | TranslationFunctions 56 | > => 57 | initI18n( 58 | loadedLocales, 59 | loadedFormatters, 60 | ) 61 | 62 | export const detectLocale = (...detectors: LocaleDetector[]): Locales => 63 | detectLocaleFn(baseLocale, locales, ...detectors) 64 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: ["pre-release"] 5 | paths-ignore: 6 | - "README.md" 7 | - "README-*.md" 8 | - ".github/ISSUE_TEMPLATE/*" 9 | jobs: 10 | publish_on_linux: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Install Node v18 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | cache: npm 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Install electron-builder 26 | run: npm i electron-builder -g 27 | 28 | - name: Publish 29 | env: 30 | GH_TOKEN: ${{ secrets.TOKEN }} 31 | run: npm run publish:linux 32 | publish_on_win: 33 | runs-on: windows-latest 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v3 37 | 38 | - name: Install Node v18 39 | uses: actions/setup-node@v3 40 | with: 41 | node-version: 18 42 | cache: npm 43 | 44 | - name: Install dependencies 45 | run: npm ci 46 | 47 | - name: Install electron-builder 48 | run: npm i electron-builder -g 49 | 50 | - name: Publish 51 | env: 52 | GH_TOKEN: ${{ secrets.TOKEN }} 53 | run: npm run publish:win 54 | publish_on_macos: 55 | runs-on: macOS-latest 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v3 59 | 60 | - name: Install Node v18 61 | uses: actions/setup-node@v3 62 | with: 63 | node-version: 18 64 | cache: npm 65 | 66 | - name: Install dependencies 67 | run: npm ci 68 | 69 | - name: Install dmg-license 70 | run: npm i dmg-license 71 | 72 | - name: Install electron-builder 73 | run: npm i electron-builder -g 74 | 75 | - name: Publish 76 | env: 77 | GH_TOKEN: ${{ secrets.TOKEN }} 78 | run: npm run publish:mac 79 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: ["main"] 5 | paths-ignore: 6 | - "README.md" 7 | - "README-*.md" 8 | - ".github/ISSUE_TEMPLATE/*" 9 | jobs: 10 | publish_on_linux: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Install Node v20 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 20 20 | cache: npm 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Install electron-builder 26 | run: npm i electron-builder -g 27 | 28 | - name: Publish 29 | env: 30 | GH_TOKEN: ${{ secrets.TOKEN }} 31 | run: npm run publish:linux 32 | publish_on_win: 33 | runs-on: windows-latest 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v3 37 | 38 | - name: Install Node v20 39 | uses: actions/setup-node@v3 40 | with: 41 | node-version: 20 42 | cache: npm 43 | 44 | - name: Install dependencies 45 | run: npm ci 46 | 47 | - name: Install electron-builder 48 | run: npm i electron-builder -g 49 | 50 | - name: Publish 51 | env: 52 | GH_TOKEN: ${{ secrets.TOKEN }} 53 | run: npm run publish:win 54 | publish_on_macos: 55 | runs-on: macOS-latest 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v3 59 | 60 | - name: Install Node v20 61 | uses: actions/setup-node@v3 62 | with: 63 | node-version: 20 64 | cache: npm 65 | 66 | - name: Install Python 67 | run: brew install python@3.11 68 | 69 | - name: "Install Python setuptools" 70 | run: brew install python-setuptools 71 | 72 | - name: Install Build Tools 73 | run: | 74 | brew install libtool automake autoconf 75 | sudo xcode-select --reset 76 | 77 | - name: Update node-gyp 78 | run: npm install -g node-gyp 79 | 80 | - name: Install dependencies 81 | run: npm ci 82 | 83 | - name: Install electron-builder 84 | run: npm i electron-builder -g 85 | 86 | - name: Publish 87 | env: 88 | GH_TOKEN: ${{ secrets.TOKEN }} 89 | run: npm run publish:mac -------------------------------------------------------------------------------- /src/renderer/component/dropdowns/serverlist-options/updatelist.item.tsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import axios from 'axios' 3 | import { Dropdown } from 'react-daisyui' 4 | import { RxUpdate } from 'react-icons/rx' 5 | import { useContext, useState } from 'react' 6 | 7 | import { serversContext } from '../../../context/servers.context' 8 | import { activityContext } from '../../../context/activty.context' 9 | import { ActivityContext } from '../../../interfaces/activty.interface' 10 | import { Server } from '../../../../shared/interfaces/server.interface' 11 | import { UrlsConstant } from '../../../../shared/constants/urls.constant' 12 | import { ServersContext } from '../../../interfaces/servers-context.interface' 13 | import { useI18nContext } from '../../../../i18n/i18n-react' 14 | 15 | const cacheBuster = (url: string) => `${url}?cb=${Date.now()}` 16 | 17 | export function UpdateListItemComponent() { 18 | const serversContextData: ServersContext = 19 | useContext(serversContext) 20 | const activityContextData = useContext(activityContext) 21 | const [isLoading, setIsLoading] = useState(false) 22 | const { LL } = useI18nContext() 23 | 24 | async function updateHandler() { 25 | if (activityContextData.isWaiting) return alert(LL.waiting()) //todo add toast 26 | try { 27 | setIsLoading(true) 28 | 29 | activityContextData.setStatus(LL.dialogs.fetching_data_from_repo()) 30 | activityContextData.setIsWaiting(true) 31 | 32 | const response = await axios.get( 33 | cacheBuster(UrlsConstant.STORE), 34 | ) 35 | const servers = serversContextData.servers.concat(response.data as any) 36 | const uniqList: Server[] = _.uniqWith(servers, _.isEqual) 37 | 38 | serversContextData.setServers(uniqList) 39 | 40 | await window.ipc.reloadServerList(uniqList) 41 | } catch (error: any) { 42 | console.log(error) 43 | window.ipc.dialogError( 44 | 'fetching error', 45 | LL.errors.error_fetching_data({ target: 'repository' }), 46 | ) 47 | } finally { 48 | activityContextData.setIsWaiting(false) 49 | activityContextData.setStatus('') 50 | setIsLoading(false) 51 | } 52 | } 53 | 54 | return ( 55 | updateHandler()}> 56 | 57 | {LL.buttons.update()} 58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/renderer/component/selectes/servers/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { Select } from 'react-daisyui' 3 | import { ServerStore } from '../../../../shared/interfaces/server.interface' 4 | import { serversContext } from '../../../context/servers.context' 5 | import { ServersContext } from '../../../interfaces/servers-context.interface' 6 | 7 | export function ServersListSelectComponent() { 8 | const serversStateContext = useContext(serversContext) 9 | const selectedDef = 10 | serversStateContext.selected?.key == 'unknown' || 11 | !serversStateContext.selected 12 | function onChange(key: string) { 13 | const server = serversStateContext.servers.find((ser) => ser.key == key) 14 | serversStateContext.setSelected(server) 15 | } 16 | return ( 17 | 29 | ) 30 | } 31 | 32 | function servers(serversStateContext: ServersContext): any { 33 | const pinsServers = serversStateContext.servers.filter((ser) => ser.isPin) 34 | 35 | const renderServer = (server: ServerStore) => { 36 | const isConnect = serversStateContext.currentActive?.key === server.key 37 | return ( 38 | 43 | {isConnect ? '🟢' : '🔴'} {server.name} 44 | 45 | ) 46 | } 47 | 48 | const pins = pinsServers.map(renderServer) 49 | 50 | if (pins.length > 0) { 51 | pins.unshift( 52 | 58 | Pinned 59 | , 60 | ) 61 | } 62 | 63 | const allServers = serversStateContext.servers.filter((ser) => !ser.isPin) 64 | const all = allServers.map(renderServer) 65 | all.unshift( 66 | 72 | All 73 | , 74 | ) 75 | 76 | return [...pins, ...all] 77 | } 78 | -------------------------------------------------------------------------------- /src/renderer/component/head/navbar.component.tsx: -------------------------------------------------------------------------------- 1 | import { IoClose } from 'react-icons/io5' 2 | import { VscChromeMinimize } from 'react-icons/vsc' 3 | import { BsDiscord, BsGithub } from 'react-icons/bs' 4 | 5 | import { Button, Navbar, Tooltip } from 'react-daisyui' 6 | import { useEffect, useState } from 'react' 7 | import { FiWifiOff } from 'react-icons/fi' 8 | 9 | export function NavbarComponent() { 10 | const [isOnline, setIsOnline] = useState(navigator.onLine) 11 | useEffect(() => { 12 | window.addEventListener('offline', function (e) { 13 | setIsOnline(false) 14 | }) 15 | window.addEventListener('online', function (e) { 16 | setIsOnline(true) 17 | }) 18 | }, []) 19 | return ( 20 |
21 | 22 | 23 |

24 | DNS Changer 25 |

26 |
27 | 28 |
29 | 39 | 48 | 49 | 59 | 71 | 72 | {!isOnline && ( 73 | 82 | )} 83 |
84 |
85 |
86 |
87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ✍ Introduction 2 | 3 |

4 | dnsChanger banner 5 |

6 | 7 |

8 | 9 | total 10 | 11 |
12 | languages 13 | stars 14 | total 15 | prettier 16 | antivirus 17 |
18 | 19 |

20 | 21 | This is an open-source DNS Changer for Windows, Mac, and Linux operating systems. Its goal is to gather the best DNS servers in a secure application. 22 | 23 | - [✍ Introduction](#-introduction) 24 | - [📥 Download](#-download) 25 | - [🦠 Antivirus Result](#-antivirus-result) 26 | - [🖼 Images](#-images) 27 | - [📝 Changelog](#-changelog) 28 | - [🛠 Collaboration](#-collaboration) 29 | - [❤️ Donate](#-donate) 30 | 31 | ## 📥 Download 32 | 33 | Please visit the [releases](https://github.com/DnsChanger/dnsChanger-desktop/releases) section. 34 | | Platform | Status | 35 | |----------|----------| 36 | | Windows | ✅ Stable| 37 | | MacOS | ✅ Stable | 38 | | Linux | ✅ Stable | 39 | 40 | ## 🦠 Antivirus Result 41 | 42 | You can check the antivirus result [here](https://www.virustotal.com/gui/file/3d50c66394a4b620ce874b0520db73a5049ec42142f262c9460d6cdb72e74fe3?nocache=1). 43 | 44 | ## 🖼 Images 45 | 46 | ![dns changer](https://github.com/DnsChanger/dnsChanger-desktop/assets/66132114/957ba956-af75-4c6f-b3a1-b604c9853e42) 47 | 48 | 49 | ## 📝 Changelog 50 | 51 | You can view the [changelog](changelog.md) for more information and recent changes. 52 | 53 | # 🛡️ Privacy 54 | This app has analytics that will track the number of users and servers only and nothing more. 55 | ## 🛠 Collaboration 56 | 57 | [CONTRIBUTING.md](./CONTRIBUTING.md) 58 | 59 | ## ❤️ Donate 60 | - tether: `0x4BE63320940fe4190ea34d5D855E6261395ac836` 61 | 62 | - or 63 | 64 | -------------------------------------------------------------------------------- /src/renderer/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: Inter; 7 | 8 | src: url("../../assets/fonts/inter/Inter-Medium.ttf"); 9 | } 10 | 11 | @font-face { 12 | font-family: balooTamma; 13 | 14 | src: url("../../assets/fonts/BalooTamma/BalooTamma2-Bold.ttf"); 15 | } 16 | 17 | body { 18 | font-family: Inter, serif; 19 | -webkit-user-select: none; 20 | -moz-user-select: none; 21 | -ms-user-select: none; 22 | user-select: none; 23 | } 24 | 25 | img { 26 | pointer-events: none; 27 | } 28 | 29 | /* scrolbar */ 30 | ::-webkit-scrollbar { 31 | width: 0.3em; 32 | background-color: #1d1d1d5b; 33 | } 34 | 35 | /* scrolbar backgrond with theme */ 36 | html[data-theme="dark"]::-webkit-scrollbar { 37 | background-color: #1d1d1d; 38 | } 39 | 40 | /* scrolbar thumb */ 41 | ::-webkit-scrollbar-thumb { 42 | background-color: #1d1c1cbe; 43 | border-radius: 40px; 44 | } 45 | 46 | /* scrolbar thumb on hover */ 47 | ::-webkit-scrollbar-thumb:hover { 48 | background-color: #1d1d1d; 49 | } 50 | 51 | .spinner { 52 | -webkit-animation: spin 4s linear infinite; 53 | -moz-animation: spin 4s linear infinite; 54 | animation: spin 4s linear infinite; 55 | } 56 | 57 | @-moz-keyframes spin { 58 | 100% { 59 | -moz-transform: rotate(360deg); 60 | } 61 | } 62 | 63 | @-webkit-keyframes spin { 64 | 100% { 65 | -webkit-transform: rotate(360deg); 66 | } 67 | } 68 | 69 | @keyframes spin { 70 | 100% { 71 | -webkit-transform: rotate(360deg); 72 | transform: rotate(360deg); 73 | } 74 | } 75 | 76 | html.dark { 77 | background-color: #212121; 78 | } 79 | 80 | html.light { 81 | background-color: #e6e6e6; 82 | } 83 | 84 | html.dark .btm-nav { 85 | background-color: #262626; 86 | } 87 | 88 | .disconnectedBtn.relative::before { 89 | color: #b3b3b3b5; 90 | content: ""; 91 | border: 1px solid; 92 | border-radius: 90px; 93 | position: absolute; 94 | top: -8px; 95 | right: -8px; 96 | bottom: -8px; 97 | left: -8px; 98 | animation: pulse 3s infinite; 99 | } 100 | 101 | @keyframes pulse { 102 | 0% { 103 | transform: scale(1); 104 | opacity: 1; 105 | } 106 | 107 | 50% { 108 | transform: scale(1.2); 109 | opacity: 0.5; 110 | } 111 | 112 | 100% { 113 | transform: scale(1); 114 | opacity: 1; 115 | } 116 | } 117 | 118 | .navbar { 119 | -webkit-app-region: drag; 120 | } 121 | 122 | .navbar button { 123 | -webkit-app-region: no-drag; 124 | } 125 | 126 | .dark .select-material ul { 127 | background: #262626; 128 | border: solid #353535; 129 | box-shadow: none; 130 | } 131 | 132 | .dark .select-material ul li:hover { 133 | background: #b7b3b3; 134 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { rmSync } from 'node:fs' 2 | // @ts-ignore 3 | import path from 'node:path' 4 | import { defineConfig } from 'vite' 5 | import react from '@vitejs/plugin-react' 6 | // eslint-disable-next-line import/default 7 | import electron from 'vite-plugin-electron' 8 | import renderer from 'vite-plugin-electron-renderer' 9 | // @ts-ignore 10 | import pkg from './package.json' 11 | 12 | process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL 13 | ? path.join(process.env.DIST_ELECTRON, '../public') 14 | : process.env.DIST 15 | // https://vitejs.dev/config/ 16 | export default defineConfig(({ command }) => { 17 | rmSync('dist-electron', { recursive: true, force: true }) 18 | 19 | const isServe = command === 'serve' 20 | const isBuild = command === 'build' 21 | const sourcemap = isServe || !!process.env.VSCODE_DEBUG 22 | 23 | return { 24 | resolve: { 25 | alias: { 26 | '@': path.join(__dirname, 'src'), 27 | }, 28 | }, 29 | plugins: [ 30 | react(), 31 | electron([ 32 | { 33 | // Main-Process entry file of the Electron App. 34 | entry: 'src/main/index.ts', 35 | onstart(options) { 36 | if (process.env.VSCODE_DEBUG) { 37 | console.log( 38 | /* For `.vscode/.debug.script.mjs` */ '[startup] Electron App', 39 | ) 40 | } else { 41 | options.startup() 42 | } 43 | }, 44 | vite: { 45 | build: { 46 | sourcemap, 47 | minify: isBuild, 48 | outDir: 'dist-electron/main', 49 | rollupOptions: { 50 | external: Object.keys( 51 | 'dependencies' in pkg ? pkg.dependencies : {}, 52 | ), 53 | }, 54 | }, 55 | }, 56 | }, 57 | { 58 | entry: 'src/preload/index.ts', 59 | onstart(options) { 60 | // Notify the Renderer-Process to reload the page when the Preload-Scripts build is complete, 61 | // instead of restarting the entire Electron App. 62 | options.reload() 63 | }, 64 | vite: { 65 | build: { 66 | sourcemap: sourcemap ? 'inline' : undefined, // #332 67 | minify: isBuild, 68 | outDir: 'dist-electron/preload', 69 | rollupOptions: { 70 | external: Object.keys( 71 | 'dependencies' in pkg ? pkg.dependencies : {}, 72 | ), 73 | }, 74 | }, 75 | }, 76 | }, 77 | ]), 78 | // Use Node.js API in the Renderer-process 79 | renderer(), 80 | ], 81 | server: 82 | process.env.VSCODE_DEBUG && 83 | (() => { 84 | // @ts-ignore 85 | const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL) 86 | return { 87 | host: url.hostname, 88 | port: +url.port, 89 | } 90 | })(), 91 | clearScreen: false, 92 | define: { 93 | 'import.meta.env.PACKAGE_VERSION': JSON.stringify(pkg.version), 94 | }, 95 | } 96 | }) 97 | -------------------------------------------------------------------------------- /src/renderer/component/cards/advertisement.card.component.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { useEffect, useState } from 'react' 3 | import { UrlsConstant } from '../../../shared/constants/urls.constant' 4 | import { Advertisement } from '../../../shared/interfaces/advertisement.interface' 5 | 6 | export function AdvertisementCardComponent() { 7 | const [ad, setAd] = useState(null) 8 | const [loading, setLoading] = useState(true) 9 | const [error, setError] = useState(false) 10 | useEffect(() => { 11 | async function fetchAd() { 12 | try { 13 | setLoading(true) 14 | setError(false) 15 | const response = await axios.get( 16 | `${UrlsConstant.ADS_STORE}?t=${Date.now()}`, 17 | ) 18 | if (!response.data || response.data.disabled) { 19 | setAd(null) 20 | setError(true) 21 | } else { 22 | setAd(response.data) 23 | } 24 | } catch (err) { 25 | console.error('Failed to load advertisement:', err) 26 | setError(true) 27 | } finally { 28 | setLoading(false) 29 | } 30 | } 31 | 32 | fetchAd() 33 | }, []) 34 | 35 | function handleAdClick() { 36 | if (ad?.url) { 37 | window.ipc.openBrowser(ad.url) 38 | } else { 39 | window.ipc.openBrowser('https://discord.gg/p9TZzEV39e') 40 | } 41 | } 42 | 43 | if (loading) { 44 | return ( 45 |
50 |
51 | 52 | 53 | Loading advertisement... 54 | 55 |
56 |
57 | ) 58 | } 59 | if (error || !ad) { 60 | return ( 61 |
67 |
68 | Ad 69 |
70 |
71 | ) 72 | } 73 | return ( 74 |
80 |
81 | {ad.name} { 86 | currentTarget.onerror = null 87 | currentTarget.style.display = 'none' 88 | setError(true) 89 | }} 90 | /> 91 |
92 |
93 | Ad 94 |
95 |
96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dnschanger", 3 | "productName": "dnschanger", 4 | "version": "2.3.5", 5 | "debug": { 6 | "env": { 7 | "VITE_DEV_SERVER_URL": "http://127.0.0.1:7777/" 8 | } 9 | }, 10 | "description": "DNS Changer for Windows, Mac and Linux operating systems", 11 | "main": "./dist-electron/main/index.js", 12 | "repository": { 13 | "url": "https://github.com/DnsChanger/dnsChanger-desktop" 14 | }, 15 | "scripts": { 16 | "i18n": "typesafe-i18n", 17 | "dev": "vite", 18 | "build:code": "tsc && vite build", 19 | "build": "tsc && vite build && electron-builder", 20 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.tsx\"", 21 | "preview": "vite preview", 22 | "pree2e": "vite build --mode=test", 23 | "e2e": "playwright test", 24 | "publish:linux": "tsc && vite build && electron-builder --linux -p always", 25 | "publish:win": "tsc && vite build && electron-builder --win -p always", 26 | "publish:auto": "tsc && vite build && electron-builder -p always", 27 | "publish:mac": "tsc && vite build && electron-builder --mac -p always" 28 | }, 29 | "keywords": [], 30 | "author": { 31 | "name": "sajjadmrx", 32 | "email": "sajjadzibaee102@gmail.com" 33 | }, 34 | "license": "MIT", 35 | "devDependencies": { 36 | "@heroicons/react": "2.1.5", 37 | "@jest/globals": "29.7.0", 38 | "@material-tailwind/react": "2.1.10", 39 | "@types/auto-launch": "5.0.5", 40 | "@types/dotenv": "8.2.0", 41 | "@types/jest": "29.5.14", 42 | "@types/lodash": "4.17.13", 43 | "@types/ms": "0.7.34", 44 | "@types/ping": "0.4.4", 45 | "@types/react": "^18.2.19", 46 | "@types/react-dom": "18.3.1", 47 | "@types/uuid": "10.0.0", 48 | "@vercel/webpack-asset-relocator-loader": "1.7.4", 49 | "@vitejs/plugin-react": "4.3.3", 50 | "copy-webpack-plugin": "12.0.2", 51 | "css-loader": "7.1.2", 52 | "daisyui": "4.12.14", 53 | "electron": "33.2.0", 54 | "electron-builder": "25.1.8", 55 | "jest": "29.7.0", 56 | "node-loader": "2.0.0", 57 | "postcss": "8.4.47", 58 | "postcss-loader": "8.1.1", 59 | "react-hot-toast": "^2.5.1", 60 | "sass": "^1.89.2", 61 | "tailwindcss-flip": "1.0.0", 62 | "ts-jest": "29.2.5", 63 | "ts-loader": "9.5.1", 64 | "ts-node": "10.9.2", 65 | "typescript": "~5.6.3", 66 | "vite": "5.4.10", 67 | "vite-plugin-electron": "0.28.8", 68 | "vite-plugin-electron-renderer": "0.14.6" 69 | }, 70 | "dependencies": { 71 | "@biomejs/biome": "^1.9.4", 72 | "auto-launch": "5.0.6", 73 | "autoprefixer": "10.4.20", 74 | "axios": "1.7.7", 75 | "dotenv": "16.4.5", 76 | "electron-log": "5.2.2", 77 | "electron-squirrel-startup": "1.0.1", 78 | "electron-store": "8.2.0", 79 | "electron-updater": "6.3.9", 80 | "electron-windows-badge": "1.1.0", 81 | "lodash": "4.17.21", 82 | "ms": "2.1.3", 83 | "network": "0.7.0", 84 | "ping": "0.4.4", 85 | "react": "18.3.1", 86 | "react-daisyui": "5.0.5", 87 | "react-dom": "18.3.1", 88 | "react-ga4": "^2.1.0", 89 | "react-icons": "5.3.0", 90 | "serve-handler": "^6.1.6", 91 | "style-loader": "4.0.0", 92 | "sudo-prompt": "9.2.1", 93 | "sweetalert2": "11.14.5", 94 | "typesafe-i18n": "5.26.2", 95 | "update-electron-app": "3.0.0", 96 | "uuid": "11.0.2" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/platforms/windows/__test__/windows.platform.spec.ts: -------------------------------------------------------------------------------- 1 | import { WindowsPlatform } from '../windows.platform' 2 | import { Interface } from '../interfaces/interface' 3 | import sudo from 'sudo-prompt' 4 | 5 | describe('WinPlatform()', () => { 6 | let windowsPlatform: WindowsPlatform 7 | 8 | beforeEach(() => { 9 | windowsPlatform = new WindowsPlatform() 10 | }) 11 | 12 | it("should return '1.1.1.1' for active server", () => { 13 | const currentServer: Array = ['1.1.1.1', '1.1.1.1'] 14 | 15 | jest 16 | .spyOn(WindowsPlatform.prototype as any, 'extractDns') 17 | .mockImplementation(() => currentServer) 18 | 19 | const activeInterface: Interface = { 20 | name: '', 21 | gateway_ip: '', 22 | ip_address: '', 23 | model: '', 24 | type: '', 25 | vendor: '', 26 | netmask: '', 27 | mac_address: '', 28 | } 29 | jest 30 | .spyOn(WindowsPlatform.prototype as any, 'getValidateInterface') 31 | .mockImplementation(() => activeInterface) 32 | 33 | jest 34 | .spyOn(WindowsPlatform.prototype as any, 'execCmd') 35 | .mockImplementation(() => '') 36 | 37 | expect(windowsPlatform.getActiveDns()).resolves.toBe(currentServer) 38 | }) 39 | 40 | describe('SetDns', () => { 41 | it('should called execCmd', async () => { 42 | const validatedInterface = { 43 | name: 'xx', 44 | } as any 45 | jest 46 | .spyOn(WindowsPlatform.prototype as any, 'getValidateInterface') 47 | .mockImplementation(() => validatedInterface) 48 | 49 | jest 50 | .spyOn(WindowsPlatform.prototype as any, 'execCmd') 51 | .mockImplementation() 52 | 53 | await windowsPlatform.setDns(['1.1.1.1', '1.1.1.1']) 54 | 55 | // @ts-ignore 56 | expect(WindowsPlatform.prototype.execCmd).toHaveBeenCalledTimes(2) 57 | }) 58 | 59 | it('should called once execCmd', async () => { 60 | const validatedInterface = { 61 | name: 'xx', 62 | } as any 63 | jest 64 | .spyOn(WindowsPlatform.prototype as any, 'getValidateInterface') 65 | .mockImplementation(() => validatedInterface) 66 | 67 | jest 68 | .spyOn(WindowsPlatform.prototype as any, 'execCmd') 69 | .mockImplementation() 70 | 71 | await windowsPlatform.setDns(['1.1.1.1']) 72 | 73 | // @ts-ignore 74 | expect(WindowsPlatform.prototype.execCmd).toHaveBeenCalledTimes(1) 75 | }) 76 | }) 77 | 78 | describe('flushDns()', () => { 79 | let windowsPlatform: WindowsPlatform 80 | 81 | beforeEach(() => { 82 | windowsPlatform = new WindowsPlatform() 83 | }) 84 | 85 | it('should execute ipconfig /flushdns command', async () => { 86 | jest 87 | .spyOn(sudo, 'exec') 88 | .mockImplementation((command, options, callback) => { 89 | expect(command).toBe('ipconfig /flushdns') 90 | expect(options).toEqual({ name: 'DnsChanger' }) 91 | callback(null) 92 | }) 93 | 94 | await windowsPlatform.flushDns() 95 | 96 | expect(sudo.exec).toHaveBeenCalledTimes(1) 97 | }) 98 | 99 | it('should reject with error message if command execution fails', async () => { 100 | const errorMessage = 'Failed to execute command' 101 | jest 102 | .spyOn(sudo, 'exec') 103 | .mockImplementation((command, options, callback) => { 104 | callback(new Error(errorMessage)) 105 | }) 106 | 107 | await expect(windowsPlatform.flushDns()).rejects.toThrow(errorMessage) 108 | expect(sudo.exec).toHaveBeenCalledTimes(1) 109 | }) 110 | }) 111 | }) 112 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | // See the Electron documentation for details on how to use preload scripts: 2 | // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts 3 | import { contextBridge, ipcRenderer } from 'electron' 4 | 5 | import os from 'node:os' 6 | import { store } from '../main/store/store' 7 | import { EventsKeys } from '../shared/constants/eventsKeys.constant' 8 | import { Server, ServerStore } from '../shared/interfaces/server.interface' 9 | import { 10 | SettingInStore, 11 | StoreKey, 12 | } from '../shared/interfaces/settings.interface' 13 | 14 | export const ipcPreload = { 15 | setDns: (server: Server) => ipcRenderer.invoke(EventsKeys.SET_DNS, server), 16 | clearDns: () => ipcRenderer.invoke(EventsKeys.CLEAR_DNS), 17 | notif: (message: string) => 18 | ipcRenderer.send(EventsKeys.NOTIFICATION, message), 19 | dialogError: (title: string, message: string) => 20 | ipcRenderer.send(EventsKeys.DIALOG_ERROR, title, message), 21 | openBrowser: (url: string) => ipcRenderer.send(EventsKeys.OPEN_BROWSER, url), 22 | addDns: (data: Partial) => 23 | ipcRenderer.invoke(EventsKeys.ADD_DNS, data), 24 | deleteDns: (server: Server) => 25 | ipcRenderer.invoke(EventsKeys.DELETE_DNS, server), 26 | reloadServerList: (servers: Array) => 27 | ipcRenderer.invoke(EventsKeys.RELOAD_SERVER_LIST, servers), 28 | fetchDnsList: () => ipcRenderer.invoke(EventsKeys.FETCH_DNS_LIST), 29 | getCurrentActive: () => ipcRenderer.invoke(EventsKeys.GET_CURRENT_ACTIVE), 30 | getSettings: () => ipcRenderer.invoke(EventsKeys.GET_SETTINGS), 31 | toggleStartUP: () => ipcRenderer.invoke(EventsKeys.TOGGLE_START_UP), 32 | flushDns: () => ipcRenderer.invoke(EventsKeys.FLUSHDNS), 33 | saveSettings: (settings: SettingInStore) => 34 | ipcRenderer.invoke(EventsKeys.SAVE_SETTINGS, settings), 35 | ping: (server: Server) => ipcRenderer.invoke(EventsKeys.PING, server), 36 | checkUpdate: () => ipcRenderer.invoke(EventsKeys.CHECK_UPDATE), 37 | startUpdate: () => ipcRenderer.invoke(EventsKeys.START_UPDATE), 38 | on: (string: string, cb: any) => ipcRenderer.on(string, cb), 39 | off: (string: string, cb: any) => ipcRenderer.on(string, cb), 40 | close: () => ipcRenderer.send(EventsKeys.CLOSE), 41 | minimize: () => ipcRenderer.send(EventsKeys.MINIMIZE), 42 | togglePinServer: (server: ServerStore) => 43 | ipcRenderer.invoke(EventsKeys.TOGGLE_PIN, server), 44 | openLogFile: () => ipcRenderer.send(EventsKeys.OPEN_LOG_FILE), 45 | openDevTools: () => ipcRenderer.send(EventsKeys.OPEN_DEV_TOOLS), 46 | scheduleShutdown: (data: { 47 | delay: number 48 | scheduledTime: Date 49 | description?: string 50 | }) => ipcRenderer.invoke(EventsKeys.SCHEDULE_SHUTDOWN, data), 51 | cancelScheduledShutdown: (shutdownId: string) => 52 | ipcRenderer.invoke(EventsKeys.CANCEL_SCHEDULED_SHUTDOWN, shutdownId), 53 | clearAllShutdowns: () => ipcRenderer.invoke(EventsKeys.CLEAR_ALL_SHUTDOWNS), 54 | } 55 | 56 | export const uiPreload = { 57 | toggleTheme: (newTheme: string) => 58 | ipcRenderer.invoke(EventsKeys.TOGGLE_THEME, newTheme), 59 | } 60 | 61 | export const osItems = { 62 | os: os.platform(), 63 | getInterfaces: () => 64 | ipcRenderer.invoke(EventsKeys.GET_NETWORK_INTERFACE_LIST), 65 | } 66 | 67 | export const storePreload = { 68 | get: (key: T) => store.get(key), 69 | } 70 | 71 | contextBridge.exposeInMainWorld('ui', uiPreload) 72 | contextBridge.exposeInMainWorld('ipc', ipcPreload) 73 | contextBridge.exposeInMainWorld('os', osItems) 74 | contextBridge.exposeInMainWorld('storePreload', storePreload) 75 | -------------------------------------------------------------------------------- /src/renderer/component/buttons/delete-btn.component.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogBody, 5 | DialogFooter, 6 | DialogHeader, 7 | Typography, 8 | Badge, 9 | } from '@material-tailwind/react' 10 | import { useContext, useState } from 'react' 11 | import { Button as ButtonDaisyui } from 'react-daisyui' 12 | import { AiOutlineDelete } from 'react-icons/ai' 13 | import { TbInfoHexagon } from 'react-icons/tb' 14 | import { serversContext } from '../../context/servers.context' 15 | import { ServersContext } from '../../interfaces/servers-context.interface' 16 | import { appNotif } from '../../notifications/appNotif' 17 | 18 | export function DeleteButtonComponent() { 19 | const [open, setOpen] = useState(false) 20 | const serversStateContext = useContext(serversContext) 21 | const server = serversStateContext.selected 22 | const handleOpen = () => setOpen(!open) 23 | 24 | const handleDelete = async () => { 25 | if ( 26 | server.key == serversStateContext.currentActive?.key && 27 | server.name == server.name 28 | ) { 29 | setOpen(false) 30 | appNotif('Error', 'cannot delete the active server') 31 | return 32 | } 33 | 34 | const response = await window.ipc.deleteDns(server) 35 | setOpen(false) 36 | appNotif('Success', `${server.name} Deleted!`, 'SUCCESS') 37 | serversStateContext.setSelected(null) 38 | serversStateContext.setServers(response.servers) 39 | if (serversStateContext.currentActive) { 40 | serversStateContext.setSelected(serversStateContext.currentActive) 41 | } 42 | } 43 | if (!server) return null 44 | return ( 45 | <> 46 | 54 | 58 | 59 | 65 | 66 | } 68 | overlap="square" 69 | color={'red'} 70 | > 71 | 76 | Confirm Delete 77 | 78 | 79 | 83 | 84 | Are you sure you want to delete the server{' '} 85 | {server.name}? 86 | 87 | 88 | 89 | 97 | 105 | 106 | 107 | 108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /src/main/update.ts: -------------------------------------------------------------------------------- 1 | import { 2 | autoUpdater, 3 | UpdateCheckResult, 4 | UpdateDownloadedEvent, 5 | } from 'electron-updater' 6 | import { ipcMain } from 'electron' 7 | import { EventsKeys } from '../shared/constants/eventsKeys.constant' 8 | import { ProgressInfo } from 'electron-builder' 9 | import * as path from 'node:path' 10 | import { store } from './store/store' 11 | import ms from 'ms' 12 | import isDev from './shared/isDev' 13 | export function update(win: Electron.BrowserWindow, app: Electron.App) { 14 | autoUpdater.autoDownload = store.get('settings').autoUpdate 15 | autoUpdater.disableWebInstaller = false 16 | 17 | if (isDev) autoUpdater.updateConfigPath = path.resolve('dev-app-update.yml') 18 | 19 | autoUpdater.allowDowngrade = false 20 | autoUpdater.fullChangelog = true 21 | 22 | autoUpdater.logger = require('electron-log') 23 | 24 | autoUpdater.setFeedURL({ 25 | provider: 'github', 26 | owner: 'DnsChanger', 27 | repo: 'dnsChanger-desktop', 28 | }) 29 | 30 | autoUpdater.on('checking-for-update', () => { 31 | autoUpdater.logger.info('checking....') 32 | }) 33 | 34 | autoUpdater.on('update-available', (arg) => { 35 | win.webContents.send('update-can-available', { 36 | update: true, 37 | version: app.getVersion(), 38 | notes: arg.releaseNotes, 39 | newVersion: arg?.version, 40 | }) 41 | }) 42 | 43 | autoUpdater.on('update-not-available', (arg) => { 44 | win.webContents.send('update-can-available', { 45 | update: false, 46 | version: app.getVersion(), 47 | newVersion: arg?.version, 48 | }) 49 | autoUpdater.logger.info('update not found') 50 | }) 51 | 52 | autoUpdater.on('error', (x) => { 53 | autoUpdater.logger.error(x.message) 54 | }) 55 | 56 | autoUpdater.on('update-downloaded', (info) => { 57 | autoUpdater.quitAndInstall(false, true) 58 | }) 59 | 60 | ipcMain.handle(EventsKeys.CHECK_UPDATE, async () => { 61 | if (!app.isPackaged) { 62 | const error = new Error( 63 | 'The update feature is only available after the package.', 64 | ) 65 | return { message: error.message, error } 66 | } 67 | 68 | try { 69 | const update: UpdateCheckResult = await autoUpdater.checkForUpdates() 70 | 71 | return { updateInfo: update?.updateInfo || null } 72 | } catch (error: any) { 73 | autoUpdater.logger.error(error.message) 74 | return { message: 'Network error', error, isError: true } 75 | } 76 | }) 77 | 78 | ipcMain.handle(EventsKeys.START_UPDATE, async (event, args) => { 79 | startDownload( 80 | (error: Error | null, progressInfo: ProgressInfo) => { 81 | if (error) { 82 | event.sender.send(EventsKeys.UPDATE_ERROR, { 83 | message: error.message, 84 | error, 85 | }) 86 | } else { 87 | autoUpdater.logger.info(progressInfo) 88 | event.sender.send(EventsKeys.UPDATE_PROGRESS, progressInfo) 89 | } 90 | }, 91 | () => autoUpdater.quitAndInstall(false, true), 92 | ) 93 | }) 94 | async function checkUpdate() { 95 | try { 96 | if (autoUpdater.autoDownload) { 97 | autoUpdater.logger.info('start Checking Update...') 98 | return await autoUpdater.checkForUpdates() 99 | } 100 | } catch (error: any) { 101 | autoUpdater.logger.error(error.message) 102 | } 103 | } 104 | if (autoUpdater.autoDownload && !isDev) { 105 | checkUpdate() 106 | setInterval(() => { 107 | autoUpdater.autoDownload = store.get('settings').autoUpdate 108 | checkUpdate() 109 | }, ms('2h')) 110 | } 111 | } 112 | 113 | type cb = (error: Error | null, info: ProgressInfo | null) => void 114 | type complete = (event: UpdateDownloadedEvent) => void 115 | function startDownload(callback: cb, complete: complete) { 116 | autoUpdater.on('download-progress', (info) => callback(null, info)) 117 | autoUpdater.on('error', (err) => callback(err, null)) 118 | autoUpdater.on('update-downloaded', complete) 119 | autoUpdater.downloadUpdate().catch((e) => callback(e, null)) 120 | } 121 | -------------------------------------------------------------------------------- /src/renderer/component/modals/wmic-helper.modal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Card, 4 | CardBody, 5 | CardFooter, 6 | Dialog, 7 | } from '@material-tailwind/react' 8 | import React, { useState } from 'react' 9 | import { BiCommentError } from 'react-icons/bi' 10 | import { FaCheck, FaCopy } from 'react-icons/fa' 11 | 12 | interface Props { 13 | isOpen: boolean 14 | setIsOpen: React.Dispatch> 15 | } 16 | 17 | export function WmicHelperModal(props: Props) { 18 | const handleOpen = () => props.setIsOpen((cur) => !cur) 19 | const [copied, setCopied] = useState(false) 20 | 21 | const handleCopy = async () => { 22 | await navigator.clipboard.writeText( 23 | 'DISM /Online /Add-Capability /CapabilityName:WMIC~~~~', 24 | ) 25 | setCopied(true) 26 | setTimeout(() => setCopied(false), 2000) 27 | } 28 | 29 | return ( 30 | 36 | 37 |
38 |
39 | 40 | WMIC Not Installed 41 |
42 |
43 | 44 | 45 |

46 | It looks like WMIC is not installed on your system. WMIC is required 47 | for this application to function properly. Please follow the 48 | instructions below to install WMIC. 49 |

50 |
51 |

52 | 1. Open Command Prompt as Administrator. 53 |

54 |

55 | 2. Run the following command: 56 |

57 |
58 |
59 | 71 |
72 | 73 |
 74 | 								DISM /Online /Add-Capability /CapabilityName:WMIC~~~~​
 75 | 							
76 |
77 |

78 | 3. Restart your computer. 79 |

80 |
81 |
82 | 83 | 84 | 93 | 107 | 108 |
109 |
110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /src/main/ipc/shutdown.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'node:child_process' 2 | import { ipcMain } from 'electron' 3 | import { EventsKeys } from '../../shared/constants/eventsKeys.constant' 4 | 5 | interface ScheduledShutdown { 6 | id: string 7 | scheduledTime: Date 8 | isActive: boolean 9 | description?: string 10 | timeoutId?: NodeJS.Timeout 11 | } 12 | 13 | const activeShutdowns: Map = new Map() 14 | 15 | ipcMain.handle( 16 | EventsKeys.SCHEDULE_SHUTDOWN, 17 | async ( 18 | _, 19 | data: { 20 | delay: number 21 | scheduledTime: Date 22 | description?: string 23 | }, 24 | ) => { 25 | try { 26 | const id = Date.now().toString() 27 | 28 | await cancelAllScheduledShutdowns() 29 | 30 | const shutdownCommand = getShutdownCommand(Math.floor(data.delay / 1000)) 31 | 32 | return new Promise((resolve, reject) => { 33 | exec(shutdownCommand, (error) => { 34 | if (error) { 35 | console.error('Failed to schedule shutdown:', error) 36 | reject(new Error('Failed to schedule shutdown')) 37 | return 38 | } 39 | 40 | const shutdown: ScheduledShutdown = { 41 | id, 42 | scheduledTime: new Date(data.scheduledTime), 43 | isActive: true, 44 | description: data.description, 45 | } 46 | 47 | activeShutdowns.set(id, shutdown) 48 | resolve(id) 49 | }) 50 | }) 51 | } catch (error) { 52 | throw new Error(`Failed to schedule shutdown: ${error}`) 53 | } 54 | }, 55 | ) 56 | 57 | ipcMain.handle( 58 | EventsKeys.CANCEL_SCHEDULED_SHUTDOWN, 59 | async (_, shutdownId: string) => { 60 | try { 61 | const shutdown = activeShutdowns.get(shutdownId) 62 | if (!shutdown) { 63 | throw new Error('Shutdown not found') 64 | } 65 | 66 | const cancelCommand = getCancelShutdownCommand() 67 | 68 | return new Promise((resolve, reject) => { 69 | exec(cancelCommand, (error) => { 70 | if (error) { 71 | console.error('Failed to cancel shutdown:', error) 72 | reject(new Error('Failed to cancel shutdown')) 73 | return 74 | } 75 | 76 | activeShutdowns.delete(shutdownId) 77 | resolve(true) 78 | }) 79 | }) 80 | } catch (error) { 81 | throw new Error(`Failed to cancel shutdown: ${error}`) 82 | } 83 | }, 84 | ) 85 | 86 | ipcMain.handle(EventsKeys.CLEAR_ALL_SHUTDOWNS, async () => { 87 | try { 88 | await cancelAllScheduledShutdowns() 89 | return { success: true } 90 | } catch (error) { 91 | throw new Error(`Failed to clear all shutdowns: ${error}`) 92 | } 93 | }) 94 | 95 | async function cancelAllScheduledShutdowns(): Promise { 96 | const cancelCommand = getCancelShutdownCommand() 97 | 98 | return new Promise((resolve) => { 99 | exec(cancelCommand, (error) => { 100 | if (error) { 101 | console.log('No existing shutdown to cancel or failed to cancel') 102 | } 103 | // Clear our tracking 104 | activeShutdowns.clear() 105 | resolve() 106 | }) 107 | }) 108 | } 109 | 110 | function getShutdownCommand(delayInSeconds: number): string { 111 | const platform = process.platform 112 | 113 | switch (platform) { 114 | case 'win32': 115 | return `shutdown /s /t ${delayInSeconds} /c "Scheduled shutdown from DNS Changer"` 116 | case 'darwin': // macOS 117 | return `sudo shutdown -h +${Math.ceil(delayInSeconds / 60)}` // macOS uses minutes 118 | case 'linux': 119 | return `shutdown -h +${Math.ceil(delayInSeconds / 60)}` // Linux uses minutes 120 | default: 121 | throw new Error(`Unsupported platform: ${platform}`) 122 | } 123 | } 124 | 125 | function getCancelShutdownCommand(): string { 126 | const platform = process.platform 127 | 128 | switch (platform) { 129 | case 'win32': 130 | return 'shutdown /a' 131 | case 'darwin': // macOS 132 | return 'sudo killall shutdown' 133 | case 'linux': 134 | return 'shutdown -c' 135 | default: 136 | throw new Error(`Unsupported platform: ${platform}`) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/renderer/component/modals/network-options.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react' 2 | 3 | import { 4 | Button, 5 | Card, 6 | CardBody, 7 | CardFooter, 8 | Dialog, 9 | } from '@material-tailwind/react' 10 | import { Select } from 'react-daisyui' 11 | import { settingStore } from '../../app' 12 | import { serversContext } from '../../context/servers.context' 13 | import { setState } from '../../interfaces/react.interface' 14 | import { ServersContext } from '../../interfaces/servers-context.interface' 15 | 16 | interface Props { 17 | isOpen: boolean 18 | setIsOpen: setState 19 | cb: (val: any) => void 20 | } 21 | 22 | export function NetworkOptionsModalComponent(props: Props) { 23 | const { setNetwork, network } = useContext(serversContext) 24 | 25 | const handleOpen = () => props.setIsOpen((cur) => !cur) 26 | 27 | const [loading, setLoading] = useState(true) 28 | 29 | const [networkInterface, setNetworkInterfaceInterface] = useState() 30 | 31 | const [networkAdapters, setNetworkAdapters] = useState([]) 32 | 33 | useEffect(() => { 34 | const fetchNetworkInterfaces = async () => { 35 | try { 36 | const interfaces = await window.os.getInterfaces() 37 | if ('success' in interfaces) { 38 | props.setIsOpen(false) 39 | const event = new Event('wmic-helper-modal') 40 | window.dispatchEvent(event) 41 | return 42 | } 43 | 44 | const networks = interfaces.map((d) => d.name) 45 | networks.unshift('Auto') 46 | setNetworkAdapters(networks) 47 | } finally { 48 | setLoading(false) 49 | } 50 | } 51 | 52 | if (props.isOpen) { 53 | const current = window.storePreload.get('settings').network_interface 54 | setNetwork(current) 55 | fetchNetworkInterfaces() 56 | } 57 | }, [props.isOpen]) 58 | 59 | useEffect(() => { 60 | if (networkInterface) { 61 | settingStore.network_interface = networkInterface 62 | window.ipc.saveSettings(settingStore).catch() 63 | 64 | setNetwork(networkInterface) 65 | } 66 | }, [networkInterface]) 67 | 68 | return ( 69 | 79 | 80 |
81 |
Network Options
82 |
83 | 84 |
85 |
86 |
87 | 88 | Network Interface 89 | 90 |
91 | 92 |
93 | {loading ? ( 94 |
95 | 96 | fetching interfaces... 97 |
98 | ) : ( 99 | 110 | )} 111 |
112 |
113 |
114 |
115 | 116 | 124 | 125 |
126 |
127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /src/main/platforms/windows/windows.platform.ts: -------------------------------------------------------------------------------- 1 | import network from 'network' 2 | import sudo from 'sudo-prompt' 3 | 4 | import { store } from '../../store/store' 5 | import { Platform } from '../platform' 6 | import { Interface } from './interfaces/interface' 7 | 8 | export class WindowsPlatform extends Platform { 9 | async clearDns(): Promise { 10 | try { 11 | let networkInterface = store.get('settings').network_interface 12 | if (networkInterface === 'Auto') 13 | networkInterface = (await this.getValidateInterface()).name 14 | 15 | return new Promise((resolve, reject) => { 16 | sudo.exec( 17 | `netsh interface ip set dns "${networkInterface}" dhcp`, 18 | { 19 | name: 'DnsChanger', 20 | }, 21 | (error) => { 22 | if (error) { 23 | reject(error) 24 | return 25 | } 26 | resolve() 27 | }, 28 | ) 29 | }) 30 | } catch (e) { 31 | throw e 32 | } 33 | } 34 | 35 | async getActiveDns(): Promise> { 36 | try { 37 | let networkInterface = store.get('settings').network_interface 38 | if (networkInterface === 'Auto') 39 | networkInterface = (await this.getValidateInterface()).name 40 | 41 | const cmd = `netsh interface ip show dns "${networkInterface}"` 42 | const text = (await this.execCmd(cmd)) as string 43 | 44 | return this.extractDns(text) 45 | } catch (e) { 46 | throw e 47 | } 48 | } 49 | 50 | getInterfacesList(): Promise { 51 | return new Promise((resolve, reject) => { 52 | network.get_interfaces_list((err: unknown, obj: unknown) => { 53 | if (err) reject(err) 54 | else resolve(obj as Interface[]) 55 | }) 56 | }) 57 | } 58 | 59 | async setDns(nameServers: Array): Promise { 60 | try { 61 | let networkInterface = store.get('settings').network_interface 62 | if (networkInterface === 'Auto') 63 | networkInterface = (await this.getValidateInterface()).name 64 | const cmdServer1 = `netsh interface ip set dns "${networkInterface}" static ${nameServers[0]}` 65 | 66 | await this.execCmd(cmdServer1) 67 | 68 | if (nameServers[1]) { 69 | const cmdServer2 = `netsh interface ip add dns "${networkInterface}" ${nameServers[1]} index=2` 70 | await this.execCmd(cmdServer2) 71 | } 72 | } catch (e) { 73 | throw e 74 | } 75 | } 76 | 77 | async isWmicAvailable(): Promise { 78 | return new Promise((resolve) => { 79 | sudo.exec( 80 | 'wmic os get caption', 81 | { 82 | name: 'DnsChanger', 83 | }, 84 | (error, stdout, stderr) => { 85 | if (error || stderr) { 86 | console.log('WMIC is not installed or not recognized') 87 | console.log('Error:', error?.message || stderr) 88 | resolve(false) 89 | return 90 | } 91 | 92 | console.log('WMIC is available') 93 | resolve(true) 94 | }, 95 | ) 96 | }) 97 | } 98 | 99 | private async getValidateInterface() { 100 | try { 101 | const interfaces: Interface[] = await this.getInterfacesList() 102 | const activeInterface: Interface | null = interfaces.find( 103 | (inter: Interface) => inter.gateway_ip != null, 104 | ) 105 | 106 | if (!activeInterface) throw new Error('CONNECTION_FAILED') 107 | return activeInterface 108 | } catch (error) { 109 | throw error 110 | } 111 | } 112 | 113 | private extractDns(input: string): Array { 114 | const regex = /Statically Configured DNS Servers:\s+([\d.]+)\s+([\d.]+)/gm 115 | const matches = regex.exec(input) || [] 116 | if (!matches.length) return [] 117 | return [matches[1].trim(), matches[2].trim()] 118 | } 119 | 120 | public async flushDns(): Promise { 121 | return new Promise((resolve, reject) => { 122 | sudo.exec( 123 | 'ipconfig /flushdns', 124 | { 125 | name: 'DnsChanger', 126 | }, 127 | (error) => { 128 | if (error) { 129 | reject(error) 130 | return 131 | } 132 | resolve() 133 | }, 134 | ) 135 | }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/renderer/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from 'react' 2 | import { BottomNavigation, Tooltip } from 'react-daisyui' 3 | 4 | import { Toaster } from 'react-hot-toast' 5 | import { IconType } from 'react-icons' 6 | import { BsPower } from 'react-icons/bs' 7 | import { MdOutlineExplore } from 'react-icons/md' 8 | import { TbSettings, TbSmartHome } from 'react-icons/tb' 9 | import TypesafeI18n from '../i18n/i18n-react' 10 | import { loadLocaleAsync } from '../i18n/i18n-util.async' 11 | import { 12 | SettingInStore, 13 | Settings, 14 | } from '../shared/interfaces/settings.interface' 15 | import { PageWrapper } from './Wrappers/pages.wrapper' 16 | import { ExplorePage } from './pages/explore.page' 17 | import { HomePage } from './pages/home.page' 18 | import { SettingPage } from './pages/setting.page' 19 | import { ShutdownPage } from './pages/shutdown.page' 20 | import { getThemeSystem, themeChanger } from './utils/theme.util' 21 | export let settingStore: SettingInStore = window.storePreload.get('settings') 22 | import ReactGA from 'react-ga4' 23 | 24 | interface Page { 25 | key: string 26 | element: JSX.Element 27 | icon: IconType 28 | name: string 29 | } 30 | 31 | export function App() { 32 | const [wasLoaded, setWasLoaded] = useState(false) 33 | 34 | const pages: Array = [ 35 | { key: '/', element: , icon: TbSmartHome, name: 'Home' }, 36 | { 37 | key: '/explore', 38 | element: , 39 | icon: MdOutlineExplore, 40 | name: 'Explore', 41 | }, 42 | { 43 | key: '/shutdown', 44 | element: , 45 | icon: BsPower, 46 | name: 'Shutdown', 47 | }, 48 | { 49 | key: '/setting', 50 | element: , 51 | icon: TbSettings, 52 | name: 'Setting', 53 | }, 54 | ] 55 | const [currentPage, setCurrentPage] = useState(pages[0]) 56 | const [currentPath, setCurrentPath] = useState('/') 57 | 58 | useEffect(() => { 59 | let page = pages.find((p) => p.key == currentPath) 60 | if (!page) page = pages[0] 61 | 62 | setCurrentPage(page) 63 | }, [currentPath]) 64 | 65 | useEffect(() => { 66 | ReactGA.initialize('G-XJBQXCR24P') 67 | async function getSetting() { 68 | settingStore = (await window.ipc.getSettings()) as Settings 69 | } 70 | 71 | getSetting().then(() => { 72 | loadLocaleAsync(settingStore.lng).then(() => setWasLoaded(true)) 73 | }) 74 | 75 | let theme = localStorage.getItem('theme') || 'dark' 76 | if (theme == 'system') theme = getThemeSystem() 77 | 78 | window 79 | .matchMedia('(prefers-color-scheme: dark)') 80 | .addEventListener('change', ({ matches }) => { 81 | if (theme == 'system') { 82 | if (matches) { 83 | themeChanger('dark') 84 | } else { 85 | themeChanger('light') 86 | } 87 | } 88 | }) 89 | 90 | themeChanger(theme as any) 91 | return () => { 92 | window 93 | .matchMedia('(prefers-color-scheme: dark)') 94 | .removeEventListener('change', () => {}) 95 | } 96 | }, []) 97 | 98 | if (!wasLoaded) return null 99 | function InPath(target: string): boolean { 100 | return currentPath == target 101 | } 102 | 103 | return ( 104 |
105 | 106 | {currentPage.element} 107 | 112 | {pages.map((page) => { 113 | return ( 114 |
setCurrentPath(page.key)} key={page.key}> 115 | 116 |
123 | {React.createElement(page.icon, { 124 | className: `${InPath(page.key) ? 'text-[#5B64A4]' : 'text-[#8D8D8D] '}`, 125 | size: 30, 126 | })} 127 |
128 |
129 |
130 | ) 131 | })} 132 |
133 | 134 |
135 |
136 | ) 137 | } 138 | -------------------------------------------------------------------------------- /src/renderer/pages/home.page.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import ReactGA from 'react-ga4' 3 | import { ServerStore } from '../../shared/interfaces/server.interface' 4 | import { AddCustomDnsButton } from '../component/buttons/addDns-btn.component' 5 | import { ConnectButtonComponent } from '../component/buttons/connect-btn.component' 6 | import { FlushDNS_BtnComponent } from '../component/buttons/flush-dns-btn-component' 7 | import { InterfacesDialogButtonComponent } from '../component/buttons/interfaces-dialog-btn-component' 8 | import { AdvertisementCardComponent } from '../component/cards/advertisement.card.component' 9 | import { ServerInfoCardComponent } from '../component/cards/server-info' 10 | import { WmicHelperModal } from '../component/modals/wmic-helper.modal' 11 | import { ServersListSelectComponent } from '../component/selectes/servers' 12 | import { serversContext } from '../context/servers.context' 13 | 14 | export function HomePage() { 15 | const [serversState, setServers] = useState([]) 16 | const [currentActive, setCurrentActive] = useState(null) 17 | const [network, setNetwork] = useState() 18 | const [selectedServer, setSelectedServer] = useState(null) 19 | const [loadingCurrentActive, setLoadingCurrentActive] = 20 | useState(true) 21 | const [isWmicModalOpen, setIsWmicModalOpen] = useState(false) 22 | 23 | const osType = window.os.os 24 | useEffect(() => { 25 | async function fetchDnsList() { 26 | const response = await window.ipc.fetchDnsList() 27 | setServers(response.servers) 28 | } 29 | 30 | const handleOpenModal = () => { 31 | setIsWmicModalOpen(true) 32 | ReactGA.event({ 33 | category: 'User', 34 | action: 'WMIC_HELPER_MODAL', 35 | label: 'WMIC_HELPER_MODAL', 36 | value: 1, 37 | }) 38 | } 39 | window.addEventListener('wmic-helper-modal', handleOpenModal) 40 | 41 | fetchDnsList() 42 | 43 | return () => { 44 | window.removeEventListener('wmic-helper-modal', handleOpenModal) 45 | } 46 | }, []) 47 | 48 | useEffect(() => { 49 | async function getCurrentActive() { 50 | if (!network) { 51 | setNetwork(window.storePreload.get('settings').network_interface) 52 | return 53 | } 54 | try { 55 | setSelectedServer(null) 56 | const response = await window.ipc.getCurrentActive() 57 | setCurrentActive(response.server) 58 | setSelectedServer(response.server) 59 | } finally { 60 | setLoadingCurrentActive(false) 61 | } 62 | } 63 | 64 | getCurrentActive() 65 | }, [network]) 66 | return ( 67 |
68 | 80 | {/* Main layout container - using flex instead of absolute positioning */} 81 |
82 | {/* Left section - Connect button */} 83 |
84 |
85 | 86 |
87 |
88 | 89 | {/* Middle section - Server controls */} 90 | 91 | {/* Right section - Server info and options */} 92 |
93 |
94 |
95 | 96 |
97 |
98 | 101 |
102 | 103 | {osType == 'win32' && } 104 | 105 |
106 |
107 | 108 |
109 |
110 |
111 | 112 | 116 |
117 |
118 | ) 119 | } 120 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # v2.3.5 4 | - feat: add shutdown scheduling functionality with IPC integration 5 | - refactor: use sass instead of node-sass by @brutalchrist in https://github.com/DnsChanger/dnsChanger-desktop/pull/129 6 | 7 | # v2.3.4 8 | - improved: improve UI 9 | 10 | # v2.3.3 11 | - feat: added WMIC availability check and helper modal for Windows users [#74](https://github.com/DnsChanger/dnsChanger-desktop/issues/74) 12 | - improved: removed close button from alert notification 13 | 14 | # v2.3.2 15 | - chore: update dependencies 16 | - fix: integration with google analytics (serve static files) 17 | 18 | # v2.3.1 19 | - feat: integrate with the google analytics for future improvements 20 | 21 | # v2.3.0 22 | 23 | - feat: Flush DNS cache [#58](https://github.com/DnsChanger/dnsChanger-desktop/issues/58) 24 | - feat: Enable changing installation directory for NSIS installer [#69](https://github.com/DnsChanger/dnsChanger-desktop/issues/69) 25 | - improved: Add custom server dialog 26 | - layout(home): Reposition and align homepage action buttons 27 | 28 | 29 | # v2.2.0 30 | - feat: Default Server, set the default DNS server for your system. This will be used when no custom server is set. 31 | (Optional) 32 | - feat(explore): implement search for servers 33 | - feat(explore): implement refresh for servers 34 | - feat(explore): improve fetching servers 35 | - feat(settings): added open log file 36 | - feat(settings): added open dev tools 37 | 38 | 39 | # v2.1.12 40 | 41 | - feat: Default Server, set the default DNS server for your system. This will be used when no custom server is set. 42 | (Optional) 43 | - feat: added open log file 44 | - Refactor setting page layout 45 | 46 | # v2.1.12 47 | 48 | - feat(explore): implement dynamic ping for servers 49 | - Fix UI layout and update loading message 50 | - fixed: Startup #66 51 | - improved(settings): improve UI 52 | 53 | # v2.1.11 54 | 55 | - feat: pin favorite server 56 | - ui: dialogs improvement [issues #57](https://github.com/DnsChanger/dnsChanger-desktop/issues/57) 57 | 58 | ## v2.1.10 59 | 60 | - ui: setting page improvement 61 | - ui: main page improvement 62 | - feat: enable/disable analytic 63 | - feat: change & switch Network Interface [ Windows ] 64 | - fixed: error when macOS not support Ethernet [PR #51](https://github.com/DnsChanger/dnsChanger-desktop/pull/51) 65 | 66 | ## v2.1.9 67 | 68 | - feat: Ethernet for mac mini and mac studio [PR #49](https://github.com/DnsChanger/dnsChanger-desktop/pull/49) 69 | - feat: Change Network Interface [windows] 70 | 71 | ## v2.1.8 72 | 73 | - feat: delete favorite server 74 | - added zip target #43 75 | - fixed: custom server modal styles & add close button 76 | - fixed: #44 77 | 78 | ## v2.1.7 79 | 80 | - Fixing the issue of a white screen on Windows 81 | 82 | ## v2.1.6 83 | 84 | - Fixing the display of default Mac buttons 85 | 86 | ## v2.1.5 87 | 88 | - Visual improvements 89 | 90 | ## v2.1.4 91 | 92 | - Visual improvements on the main page and explorer 93 | 94 | ## v2.1.3 95 | 96 | - Adding ping refresh 97 | - Changing the appearance of server address copy 98 | - Improvements in theme selection appearance 99 | 100 | ## v2.1.2 101 | 102 | - Visual changes on the main page 103 | - Visual changes in the environment for adding custom servers 104 | 105 | ## v2.1.1 106 | 107 | - Adding connection status to the taskbar 108 | - Adding an icon to the tray menu 109 | - Fixing the automatic update issue 110 | 111 | ## v2.1.0 112 | 113 | - Disabling text selection 114 | - Detecting the operating system theme 115 | - Adding "Minimize to tray" 116 | - Making server changes easier 117 | - Changing the program name 118 | - Visual improvements 119 | 120 | ## v2.0.0 121 | 122 | - Adding support for macOS operating systems 123 | - Adding server score display 124 | - Improving the explorer page 125 | 126 | ## v1.9.0 127 | 128 | - Visual changes 129 | - Optimization 130 | - Adding the explorer section 131 | - Quick launch/run 132 | - Automatic updates 133 | 134 | ## v1.7.0 135 | 136 | - ⏲️ Adding a ping test 137 | - 🎨 Visual changes on the main page 138 | - 🎨 ⚙️ Visual changes in the settings page 139 | - 🖼️ Changing the logo 140 | 141 | ## v1.6.0 142 | 143 | - 🎨 Visual changes in the servers section 144 | - 🌍 Full multilingual support 145 | 146 | ## v1.5.0 147 | 148 | - 🧹 Improving program performance 149 | - 🎨 Visual changes 150 | - ⏳ Adding a loading page 151 | 152 | ## v1.4.0 153 | 154 | - Adding multiple languages (Farsi, English) 155 | - Fixing several minor issues 156 | 157 | ## v1.3.0 158 | 159 | - Fixing the issue of icon display in all modes 160 | - Fixing the automatic update issue 161 | - Adding a settings section and automatic program execution settings 162 | - Visual changes 163 | 164 | ## v1.2.0 165 | 166 | - Adding a repository to get the latest DNS information 167 | - Removing a server from the list 168 | - Visual changes 169 | 170 | ## v1.1.0 171 | 172 | - Adding the ability to add custom DNS servers 173 | - Detecting the current DNS during program execution 174 | - Adding theme changes 175 | - Adding Linux support 176 | 177 | ## v1.0.0 178 | 179 | - Initial release 180 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { release } from 'node:os' 2 | import os from 'node:os' 3 | import { join } from 'node:path' 4 | import Url from 'node:url' 5 | import { config } from 'dotenv' 6 | import { 7 | BrowserWindow, 8 | Menu, 9 | Tray, 10 | app, 11 | ipcMain, 12 | nativeImage, 13 | shell, 14 | } from 'electron' 15 | import { EventsKeys } from '../shared/constants/eventsKeys.constant' 16 | import { getPublicFilePath } from './shared/file' 17 | import { getIconPath } from './shared/getIconPath' 18 | import serve from './shared/serve' 19 | import { store } from './store/store' 20 | import { update } from './update' 21 | 22 | config() 23 | if (isDev) 24 | Object.defineProperty(app, 'isPackaged', { 25 | get() { 26 | return true 27 | }, 28 | }) 29 | 30 | process.env.DIST_ELECTRON = join(__dirname, '../') 31 | process.env.DIST = join(process.env.DIST_ELECTRON, '../dist') 32 | process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL 33 | ? join(process.env.DIST_ELECTRON, '../public') 34 | : process.env.DIST 35 | 36 | if (release().startsWith('6.1')) app.disableHardwareAcceleration() 37 | 38 | if (process.platform === 'win32') app.setAppUserModelId(app.getName()) 39 | 40 | if (!app.requestSingleInstanceLock()) { 41 | app.quit() 42 | process.exit(0) 43 | } 44 | 45 | let win: BrowserWindow | null = null 46 | const preload = join(__dirname, '../preload/index.js') 47 | 48 | const startUrl = 49 | process.env.VITE_DEV_SERVER_URL || 50 | Url.format({ 51 | pathname: join(process.env.DIST, 'index.html'), 52 | protocol: 'file:', 53 | slashes: true, 54 | }) 55 | 56 | // const indexHtml = join(process.env.DIST, 'index.html') 57 | const icon = nativeImage.createFromPath(getIconPath()) 58 | 59 | async function createWindow() { 60 | win = new BrowserWindow({ 61 | title: 'DNS Changer', 62 | icon: icon, 63 | height: 500, 64 | width: 743, 65 | webPreferences: { 66 | preload, 67 | nodeIntegration: true, 68 | contextIsolation: true, 69 | devTools: true, 70 | }, 71 | darkTheme: true, 72 | resizable: false, 73 | center: isDev === false, 74 | show: true, 75 | 76 | alwaysOnTop: isDev, 77 | movable: true, 78 | frame: false, 79 | titleBarStyle: 'hidden', 80 | }) 81 | // hides the traffic lights 82 | 83 | if (os.platform() === 'darwin') win.setWindowButtonVisibility(false) 84 | 85 | win.setMenu(null) 86 | 87 | await serve(win) 88 | 89 | if (isDev) win.webContents.openDevTools() 90 | 91 | win.webContents.on('did-finish-load', () => { 92 | win?.webContents.send('main-process-message', new Date().toLocaleString()) 93 | }) 94 | 95 | win.webContents.setWindowOpenHandler(({ url }) => { 96 | if (url.startsWith('https:')) shell.openExternal(url) 97 | return { action: 'deny' } 98 | }) 99 | 100 | let tray = null 101 | ipcMain.on('close', (event) => { 102 | if (!store.get('settings').minimize_tray) return app.exit(0) 103 | event.preventDefault() 104 | win.setSkipTaskbar(false) 105 | if (!tray) tray = createTray() 106 | win.hide() 107 | }) 108 | 109 | update(win, app) 110 | return win 111 | } 112 | ipcMain.on(EventsKeys.MINIMIZE, () => { 113 | app.focus() 114 | win.isMinimized() ? win.focus() : win.minimize() 115 | }) 116 | 117 | app.whenReady().then(createWindow) 118 | 119 | app.on('window-all-closed', () => { 120 | win = null 121 | if (process.platform !== 'darwin') app.quit() 122 | }) 123 | 124 | app.on('second-instance', () => { 125 | if (win) { 126 | // Focus on the main window if the user tried to open another 127 | if (win.isMinimized()) win.restore() 128 | win.focus() 129 | } 130 | }) 131 | 132 | app.on('activate', () => { 133 | const allWindows = BrowserWindow.getAllWindows() 134 | if (allWindows.length) { 135 | allWindows[0].focus() 136 | } else { 137 | createWindow() 138 | } 139 | }) 140 | 141 | // New window example arg: new windows url 142 | ipcMain.handle('open-win', (_, arg) => { 143 | const childWindow = new BrowserWindow({ 144 | webPreferences: { 145 | preload, 146 | nodeIntegration: true, 147 | contextIsolation: false, 148 | }, 149 | }) 150 | 151 | childWindow.loadURL(`${startUrl}#${arg}`) 152 | }) 153 | 154 | import './ipc/setting' 155 | import './ipc/ui' 156 | import './ipc/notif' 157 | import './ipc/dialogs' 158 | import './ipc/shutdown' 159 | import isDev from './shared/isDev' 160 | 161 | function createTray() { 162 | const appIcon = new Tray(icon) 163 | const showIcon = nativeImage.createFromPath( 164 | getPublicFilePath('icons/show.png'), 165 | ) 166 | const powerIcon = nativeImage.createFromPath( 167 | getPublicFilePath('icons/power.png'), 168 | ) 169 | 170 | const contextMenu = Menu.buildFromTemplate([ 171 | { 172 | label: 'DNS Changer', 173 | enabled: false, 174 | icon: icon.resize({ height: 19, width: 19 }), 175 | }, 176 | { 177 | label: 'Show', 178 | icon: showIcon, 179 | click: () => { 180 | win.show() 181 | ipcMain.emit(EventsKeys.GET_CURRENT_ACTIVE) 182 | }, 183 | }, 184 | { 185 | label: 'Quit DNS Changer', 186 | icon: powerIcon, 187 | click: () => { 188 | app.exit(1) 189 | }, 190 | }, 191 | ]) 192 | 193 | appIcon.on('double-click', (event) => { 194 | win.show() 195 | ipcMain.emit(EventsKeys.GET_CURRENT_ACTIVE) 196 | }) 197 | appIcon.setToolTip('DNS Changer') 198 | appIcon.setContextMenu(contextMenu) 199 | return appIcon 200 | } 201 | -------------------------------------------------------------------------------- /src/renderer/component/buttons/connect-btn.component.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from 'react' 2 | import { Button } from 'react-daisyui' 3 | import ReactGA from 'react-ga4' 4 | import { AiOutlineLoading } from 'react-icons/ai' 5 | import { CiPower } from 'react-icons/ci' 6 | import { serversContext } from '../../context/servers.context' 7 | import { appNotif } from '../../notifications/appNotif' 8 | 9 | enum statusStep { 10 | CONNECTED = 0, 11 | DISCONNECT = 1, 12 | } 13 | 14 | export function ConnectButtonComponent() { 15 | const serversStateContext = useContext(serversContext) 16 | 17 | const [loading, setLoading] = useState(false) 18 | async function clickHandler(step: statusStep) { 19 | if (loading) return 20 | if (!serversStateContext.selected) { 21 | appNotif('Error', 'please first pick your favorite server') 22 | return 23 | } 24 | 25 | setLoading(true) 26 | if (step == statusStep.CONNECTED) { 27 | // req disconnect 28 | const response = await window.ipc.clearDns() 29 | if (response.success) { 30 | serversStateContext.setCurrentActive(null) 31 | window.ipc.notif(response.message) 32 | } else { 33 | if (response.message == 'wmic_not_available') { 34 | const event = new Event('wmic-helper-modal') 35 | window.dispatchEvent(event) 36 | } else { 37 | window.ipc.dialogError('Error', response.message) 38 | } 39 | } 40 | } else if (step == statusStep.DISCONNECT) { 41 | // req connect 42 | const response = await window.ipc.setDns(serversStateContext.selected) 43 | if (response.success) { 44 | serversStateContext.setCurrentActive(serversStateContext.selected) 45 | window.ipc.notif(response.message) 46 | ReactGA.event({ 47 | category: 'User', 48 | action: 'CONNECTED', 49 | label: serversStateContext.selected.name, 50 | value: 1, 51 | }) 52 | } else { 53 | if (response.message == 'wmic_not_available') { 54 | const event = new Event('wmic-helper-modal') 55 | window.dispatchEvent(event) 56 | } else { 57 | window.ipc.dialogError('Error', response.message) 58 | } 59 | } 60 | } 61 | 62 | setLoading(false) 63 | } 64 | 65 | //loading buttons 66 | if (loading) { 67 | if ( 68 | serversStateContext.currentActive && 69 | serversStateContext.currentActive?.key == 70 | serversStateContext.selected?.key 71 | ) { 72 | //disconnecting 73 | return ( 74 |
75 | {' '} 76 |
80 | 81 |
82 |
87 | Disconnecting... 88 |
89 |
90 | ) 91 | } 92 | 93 | //connecting 94 | return ( 95 |
96 | {' '} 97 |
101 | 102 |
103 |
108 | Connecting... 109 |
110 |
111 | ) 112 | } 113 | 114 | if ( 115 | serversStateContext.currentActive && 116 | serversStateContext.currentActive?.key == serversStateContext.selected?.key 117 | ) { 118 | //isConnect 119 | return ( 120 |
121 | 129 |
134 | Connected 135 |
136 |
137 | ) 138 | } 139 | 140 | //disconnect Btn 141 | return ( 142 |
143 | 156 |
161 | Disconnected 162 |
163 |
164 | ) 165 | } 166 | -------------------------------------------------------------------------------- /src/renderer/component/servers/server.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { Tooltip } from 'react-daisyui' 3 | import { AiOutlineCloudServer } from 'react-icons/ai' 4 | 5 | import { setState } from '../../interfaces/react.interface' 6 | import { activityContext } from '../../context/activty.context' 7 | import { ActivityContext } from '../../interfaces/activty.interface' 8 | import { Server } from '../../../shared/interfaces/server.interface' 9 | import { ServerOptionsComponent } from '../dropdowns/server-options/server-options.component' 10 | import { useI18nContext } from '../../../i18n/i18n-react' 11 | import { useState } from 'react' 12 | 13 | interface Props { 14 | server: Server 15 | currentActive: Server 16 | setCurrentActive: setState 17 | } 18 | 19 | export function ServerComponent(prop: Props) { 20 | const { LL, locale } = useI18nContext() 21 | const server = prop.server 22 | const isConnect = server.key == prop.currentActive?.key 23 | const activityContextData = React.useContext(activityContext) 24 | const setCurrentActive: setState = prop.setCurrentActive 25 | const [connecting, setConnecting] = useState(false) 26 | const [currentPing, setPing] = useState(0) 27 | 28 | const serverName = server.names[locale] || server.names.eng 29 | 30 | async function clickHandler() { 31 | try { 32 | if (activityContextData.isWaiting) { 33 | window.ipc.notif(LL.waiting()) 34 | return 35 | } 36 | 37 | activityContextData.setIsWaiting(true) 38 | 39 | let response 40 | 41 | if (isConnect) { 42 | activityContextData.setStatus(LL.disconnecting()) 43 | 44 | response = await window.ipc.clearDns() 45 | response.success && setCurrentActive(null) 46 | } else { 47 | setConnecting(true) 48 | activityContextData.setStatus(LL.connecting()) 49 | 50 | response = await window.ipc.setDns(server) 51 | 52 | if (response.success) setCurrentActive(server) 53 | } 54 | if (!response.success) throw response 55 | 56 | setConnecting(false) 57 | window.ipc.notif(response.message) 58 | } catch (e) { 59 | window.ipc.dialogError('Error', e.message) 60 | } finally { 61 | activityContextData.setIsWaiting(false) 62 | activityContextData.setStatus('') 63 | setConnecting(false) 64 | } 65 | } 66 | 67 | useEffect(() => { 68 | if (typeof activityContextData.reqPing == 'boolean') { 69 | window.ipc 70 | .ping(server) 71 | .then((res) => res.success && setPing(res.data.time)) 72 | } 73 | }, [activityContextData.reqPing]) 74 | 75 | return ( 76 |
77 |
87 |
88 |
!activityContextData.isWaiting && clickHandler()} 91 | > 92 | {typeof activityContextData.reqPing == 'boolean' && 93 | Number(currentPing) ? ( 94 |
99 |
103 | 104 | {currentPing > 500 ? '500+' : currentPing} 105 | 106 |
107 |
108 | ) : ( 109 |
114 |
118 | 122 |
123 |
124 | )} 125 |
126 |
!activityContextData.isWaiting && clickHandler()} 129 | > 130 | 134 |

{serverName}

135 |
136 |
137 |
138 |
139 | 140 |
141 |
142 |
143 |
144 |
145 | ) 146 | } 147 | 148 | function getColor(ping: number): string { 149 | switch (true) { 150 | case ping <= 100: 151 | return 'bg-teal-900/50' 152 | case ping <= 180: 153 | return 'bg-yellow-300/50' 154 | default: 155 | return 'bg-red-900/40' 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/renderer/component/buttons/update-btn.component.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'react-daisyui' 2 | import React, { useCallback, useEffect, useState } from 'react' 3 | import { 4 | Dialog, 5 | DialogBody, 6 | DialogFooter, 7 | DialogHeader, 8 | Typography, 9 | Progress, 10 | } from '@material-tailwind/react' 11 | import { CgSoftwareDownload } from 'react-icons/cg' 12 | import { ProgressInfo } from 'electron-builder' 13 | import { UpdateInfo } from 'electron-updater' 14 | import { EventsKeys } from '../../../shared/constants/eventsKeys.constant' 15 | export function UpdateBtnComponent() { 16 | const [checking, setChecking] = useState(false) 17 | const [updateInfo, setUpdateInfo] = useState(null) 18 | async function checkUpdate() { 19 | if (checking) return alert('please wait') 20 | try { 21 | setChecking(true) 22 | const { updateInfo, isError } = await window.ipc.checkUpdate() 23 | if (isError) { 24 | window.ipc.notif('Unable to find any new updates') 25 | return 26 | } 27 | if (updateInfo) { 28 | setUpdateInfo(updateInfo) 29 | } 30 | } catch (e) { 31 | window.ipc.dialogError('Checking failed', 'Something went wrong') 32 | } finally { 33 | setChecking(false) 34 | } 35 | } 36 | 37 | return ( 38 |
39 | 48 | 49 |
50 | ) 51 | } 52 | interface Props { 53 | updateInfo: UpdateInfo 54 | } 55 | function DetailsModal(prop: Props): JSX.Element { 56 | const [openDialog, setOpenDialog] = useState(false) 57 | const [progressInfo, setProgressInfo] = useState>() 58 | const [progressLabel, setProgressLabel] = useState() 59 | const [speedLabel, setSpeedLabel] = useState() 60 | useEffect(() => { 61 | if (prop.updateInfo) { 62 | setOpenDialog(true) 63 | ;(async () => { 64 | await window.ipc.startUpdate() 65 | })() 66 | 67 | window.ipc.on(EventsKeys.UPDATE_PROGRESS, onDownloadProgress) 68 | window.ipc.on(EventsKeys.UPDATE_ERROR, onError) 69 | return () => { 70 | window.ipc.off(EventsKeys.UPDATE_PROGRESS, onDownloadProgress) 71 | window.ipc.on(EventsKeys.UPDATE_ERROR, onError) 72 | } 73 | } 74 | }, [prop.updateInfo]) 75 | 76 | const onDownloadProgress = useCallback( 77 | (_event: Electron.IpcRendererEvent, arg1: ProgressInfo) => { 78 | setProgressInfo(arg1) 79 | updateProgressInfo(arg1.transferred, arg1.total) 80 | updateSpeedInfo(arg1.bytesPerSecond) 81 | }, 82 | [], 83 | ) 84 | function onError( 85 | _event: Electron.IpcRendererEvent, 86 | arg1: { message: string }, 87 | ) { 88 | setOpenDialog(false) 89 | } 90 | 91 | function updateProgressInfo(transferred, total) { 92 | const formattedTransferred = formatSize(transferred) 93 | const formattedTotal = formatSize(total) 94 | const progress = calculateProgress(transferred, total) 95 | setProgressLabel( 96 | `Progress: ${formattedTransferred} / ${formattedTotal} (${progress}%)`, 97 | ) 98 | } 99 | 100 | function updateSpeedInfo(bytesPerSecond) { 101 | const formattedSpeed = formatSpeed(bytesPerSecond) 102 | setSpeedLabel(`Speed: ${formattedSpeed}`) 103 | } 104 | 105 | if (!prop.updateInfo) return null 106 | return ( 107 | {}} 111 | className="bg-[#282828] rounded-2xl" 112 | > 113 | 116 |

117 | 🎉 Found version {prop.updateInfo.version} 118 |

119 |
120 | 121 |
122 |
123 | 124 | {progressLabel || 'calculate...'} 125 | 126 | 127 | {speedLabel || 'calculate...'} 128 | 129 |
130 | 131 |
132 |
133 | 134 | 142 | 143 |
144 | ) 145 | } 146 | 147 | function formatSize(sizeInBytes: number): string { 148 | if (sizeInBytes >= 1000000000) { 149 | return `${(sizeInBytes / 1000000000).toFixed(2)} GB` 150 | } 151 | 152 | if (sizeInBytes >= 1000000) { 153 | return `${(sizeInBytes / 1000000).toFixed(2)} MB` 154 | } 155 | 156 | if (sizeInBytes >= 1000) { 157 | return `${(sizeInBytes / 1000).toFixed(2)} KB` 158 | } 159 | 160 | return `${sizeInBytes} B` 161 | } 162 | 163 | function calculateProgress(transferred, total) { 164 | const progress = (transferred / total) * 100 165 | return progress.toFixed(2) 166 | } 167 | 168 | function formatSpeed(speedInBytes) { 169 | if (speedInBytes >= 1000000000) { 170 | return `${(speedInBytes / 1000000000).toFixed(2)} GB/s` 171 | } 172 | 173 | if (speedInBytes >= 1000000) { 174 | return `${(speedInBytes / 1000000).toFixed(2)} MB/s` 175 | } 176 | 177 | if (speedInBytes >= 1000) { 178 | return `${(speedInBytes / 1000).toFixed(2)} KB/s` 179 | } 180 | 181 | return `${speedInBytes} B/s` 182 | } 183 | -------------------------------------------------------------------------------- /src/renderer/pages/shutdown.page.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input, Typography } from '@material-tailwind/react' 2 | import React, { useState, useEffect } from 'react' 3 | import { toast } from 'react-hot-toast' 4 | import { BsClock, BsPower } from 'react-icons/bs' 5 | import { MdClear } from 'react-icons/md' 6 | 7 | export function ShutdownPage() { 8 | const [scheduledTime, setScheduledTime] = useState('') 9 | const [scheduledDate, setScheduledDate] = useState('') 10 | const [loading, setLoading] = useState(false) 11 | const [clearingAll, setClearingAll] = useState(false) 12 | 13 | useEffect(() => { 14 | const now = new Date() 15 | const currentDate = now.toISOString().split('T')[0] 16 | const currentTime = now.toTimeString().slice(0, 5) 17 | setScheduledDate(currentDate) 18 | setScheduledTime(currentTime) 19 | }, []) 20 | 21 | const handleScheduleShutdown = async () => { 22 | if (!scheduledDate || !scheduledTime) { 23 | toast.error('Please select date and time') 24 | return 25 | } 26 | 27 | const scheduleDateTime = new Date(`${scheduledDate}T${scheduledTime}`) 28 | const now = new Date() 29 | 30 | if (scheduleDateTime <= now) { 31 | toast.error('Please select a future time') 32 | return 33 | } 34 | 35 | setLoading(true) 36 | try { 37 | const delay = scheduleDateTime.getTime() - now.getTime() 38 | await window.ipc.scheduleShutdown({ 39 | delay, 40 | scheduledTime: scheduleDateTime, 41 | description: `Shutdown at ${scheduleDateTime.toLocaleString()}`, 42 | }) 43 | 44 | toast.success('Shutdown scheduled successfully!') 45 | 46 | // Reset to +1 hour 47 | const futureTime = new Date(now.getTime() + 60 * 60 * 1000) 48 | setScheduledDate(futureTime.toISOString().split('T')[0]) 49 | setScheduledTime(futureTime.toTimeString().slice(0, 5)) 50 | } catch (error) { 51 | toast.error('Failed to schedule shutdown') 52 | console.error(error) 53 | } finally { 54 | setLoading(false) 55 | } 56 | } 57 | 58 | const handleClearAllShutdowns = async () => { 59 | setClearingAll(true) 60 | try { 61 | await window.ipc.clearAllShutdowns() 62 | toast.success('All scheduled shutdowns cleared') 63 | } catch (error) { 64 | toast.error('Failed to clear all shutdowns') 65 | console.error(error) 66 | } finally { 67 | setClearingAll(false) 68 | } 69 | } 70 | 71 | return ( 72 |
73 |
74 | {/* Header */} 75 |
76 |
77 | 78 |
79 |
80 | 84 | Shutdown Control 85 | 86 | 87 | Schedule or clear shutdown operations 88 | 89 |
90 |
91 | 92 |
93 | {/* Schedule Section */} 94 |
95 | 99 | Schedule Shutdown 100 | {' '} 101 |
102 |
103 | 104 | Date 105 | 106 | setScheduledDate(e.target.value)} 110 | className="w-full dark:text-gray-300 font-[Inter]" 111 | crossOrigin={undefined} 112 | /> 113 |
114 |
115 | 116 | Time 117 | 118 | setScheduledTime(e.target.value)} 122 | className="w-full dark:text-gray-300 font-[Inter]" 123 | crossOrigin={undefined} 124 | /> 125 |
126 |
127 | 144 |
145 | 146 | {/* Clear All Section */} 147 |
148 | 152 | Clear All Shutdowns 153 | 154 | 155 | 156 | Cancel all scheduled shutdown operations 157 | 158 | 159 | 176 |
177 |
178 |
179 |
180 | ) 181 | } 182 | -------------------------------------------------------------------------------- /src/renderer/component/cards/server-info/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react' 2 | import { Avatar } from 'react-daisyui' 3 | import { FiCopy } from 'react-icons/fi' 4 | import { TfiReload } from 'react-icons/tfi' 5 | import icon from '../../../../../public/icons/icon.png' 6 | import { useI18nContext } from '../../../../i18n/i18n-react' 7 | import { serversContext } from '../../../context/servers.context' 8 | import { getPingIcon } from '../../../utils/icons.util' 9 | import { DeleteButtonComponent } from '../../buttons/delete-btn.component' 10 | import { ToggleButtonComponent } from '../../buttons/togglePin-btn.component' 11 | interface Prop { 12 | loadingCurrentActive: boolean 13 | } 14 | 15 | const ServerCardWrapper = ({ children }) => { 16 | return ( 17 |
18 | {children} 19 |
20 | ) 21 | } 22 | 23 | export function ServerInfoCardComponent(prop: Prop) { 24 | const serversStateContext = useContext(serversContext) 25 | const [isCopyAdds, setIsCopyAdds] = useState(true) 26 | const [ping, setPing] = useState() 27 | const { LL } = useI18nContext() 28 | 29 | useEffect(() => { 30 | if (isCopyAdds) { 31 | setTimeout(() => { 32 | setIsCopyAdds(false) 33 | }, 700) 34 | } 35 | }, [isCopyAdds]) 36 | 37 | useEffect(() => { 38 | if (serversStateContext.selected) getPing() 39 | }, [serversStateContext.selected, serversStateContext.currentActive]) 40 | 41 | function getPing() { 42 | setPing(0) 43 | window.ipc 44 | .ping(serversStateContext.selected) 45 | .then((res) => res.success && setPing(res.data.time)) 46 | } 47 | if (!serversStateContext.selected) { 48 | return ( 49 | 50 |
51 |
52 | 53 |

54 | {LL.pages.home.homeTitle()} 55 |

56 |
57 | {prop.loadingCurrentActive && ( 58 |
59 | 60 | 61 | Fetching current active... 62 | 63 |
64 | )} 65 | 66 | {LL.version()} {import.meta.env.PACKAGE_VERSION} 67 | 68 |
69 |
70 | ) 71 | } 72 | const isConnect = 73 | serversStateContext.currentActive?.key == serversStateContext.selected.key 74 | const name = 75 | serversStateContext.selected.name?.length > 14 76 | ? `${serversStateContext.selected.name.slice(0, 12)}...` 77 | : serversStateContext.selected.name 78 | const network = 79 | serversStateContext.network?.length > 14 80 | ? `${serversStateContext.network.slice(0, 12)}...` 81 | : serversStateContext.network 82 | 83 | return ( 84 | 85 |
86 |
87 |
88 | { 93 | currentTarget.onerror = null 94 | currentTarget.src = './servers-icon/def.png' 95 | }} 96 | /> 97 |
98 |
99 |

100 | {name || 'Unknown'} 101 |

102 | DNS Server 103 |
104 |
105 | 106 | {/* Connection Status */} 107 |
110 | 113 | {isConnect ? 'Connected' : 'Disconnected'} 114 |
115 |
116 | 117 | {/* Server Details Section */} 118 |
119 |
120 |
121 |
122 |

123 | {window.os.os === 'win32' ? 'Network' : 'Performance'} 124 |

125 | 131 |
132 | 133 |
134 | {window.os.os === 'win32' && ( 135 |
136 | Interface: 137 | 138 | {network} 139 | 140 |
141 | )} 142 | 143 |
144 | Ping: 145 |
146 | {ping > 0 && getPingIcon(ping)} 147 | 148 | {ping ? `${ping}ms` : 'Testing...'} 149 | 150 |
151 |
152 |
153 | DNS Address: 154 |
155 | {isCopyAdds ? ( 156 | 157 | Copied! 158 | 159 | ) : ( 160 | 171 | )} 172 |
173 |
174 |
175 |
176 |
177 | 178 | {/* Action Buttons */} 179 |
180 |
181 | 182 | 183 |
184 |
185 |
186 |
187 | ) 188 | } 189 | -------------------------------------------------------------------------------- /src/main/shared/get-port.ts: -------------------------------------------------------------------------------- 1 | import net, { ListenOptions } from 'node:net' 2 | import os from 'node:os' 3 | 4 | export interface Options extends Omit { 5 | /** 6 | A preferred port or an iterable of preferred ports to use. 7 | */ 8 | readonly port?: number | Iterable 9 | 10 | /** 11 | Ports that should not be returned. 12 | 13 | You could, for example, pass it the return value of the `portNumbers()` function. 14 | */ 15 | readonly exclude?: Iterable 16 | 17 | /** 18 | The host on which port resolution should be performed. Can be either an IPv4 or IPv6 address. 19 | 20 | By default, it checks availability on all local addresses defined in [OS network interfaces](https://nodejs.org/api/os.html#os_os_networkinterfaces). If this option is set, it will only check the given host. 21 | */ 22 | readonly host?: string 23 | } 24 | 25 | class Locked extends Error { 26 | constructor(port: number) { 27 | super(`${port} is locked`) 28 | } 29 | } 30 | 31 | const lockedPorts = { 32 | old: new Set(), 33 | young: new Set(), 34 | } 35 | 36 | // On this interval, the old locked ports are discarded, 37 | // the young locked ports are moved to old locked ports, 38 | // and a new young set for locked ports are created. 39 | const releaseOldLockedPortsIntervalMs = 1000 * 15 40 | 41 | const minPort = 1024 42 | const maxPort = 65_535 43 | 44 | // Lazily create interval on first use 45 | let interval: NodeJS.Timer | undefined 46 | 47 | const getLocalHosts = () => { 48 | const interfaces = os.networkInterfaces() 49 | 50 | // Add undefined value for createServer function to use default host, 51 | // and default IPv4 host in case createServer defaults to IPv6. 52 | const results = new Set([undefined, '0.0.0.0']) 53 | 54 | for (const _interface of Object.values(interfaces)) { 55 | if (!_interface) continue 56 | for (const config of _interface) { 57 | results.add(config.address) 58 | } 59 | } 60 | 61 | return results 62 | } 63 | 64 | const checkAvailablePort = (options: Options): Promise => 65 | new Promise((resolve, reject) => { 66 | const server = net.createServer() 67 | server.unref() 68 | server.on('error', reject) 69 | 70 | server.listen(options, () => { 71 | const { port } = server.address() as net.AddressInfo 72 | server.close(() => { 73 | resolve(port) 74 | }) 75 | }) 76 | }) 77 | 78 | const getAvailablePort = async ( 79 | options: Options, 80 | hosts: Set, 81 | ): Promise => { 82 | if (options.host || options.port === 0) { 83 | return checkAvailablePort(options) 84 | } 85 | 86 | for (const host of hosts) { 87 | try { 88 | await checkAvailablePort({ port: options.port, host }) 89 | } catch (error: any) { 90 | if (!['EADDRNOTAVAIL', 'EINVAL'].includes(error.code)) { 91 | throw error 92 | } 93 | } 94 | } 95 | 96 | return options.port as number 97 | } 98 | 99 | const portCheckSequence = function* (ports?: Iterable) { 100 | if (ports) { 101 | yield* ports 102 | } 103 | 104 | yield 0 // Fall back to 0 if anything else failed 105 | } 106 | 107 | /** 108 | Get an available TCP port number. 109 | 110 | @returns Port number. 111 | 112 | @example 113 | ``` 114 | import getPort from 'get-port'; 115 | 116 | console.log(await getPort()); 117 | //=> 51402 118 | 119 | // Pass in a preferred port 120 | console.log(await getPort({port: 3000})); 121 | // Will use 3000 if available, otherwise fall back to a random port 122 | 123 | // Pass in an array of preferred ports 124 | console.log(await getPort({port: [3000, 3001, 3002]})); 125 | // Will use any element in the preferred ports array if available, otherwise fall back to a random port 126 | ``` 127 | */ 128 | export default async function getPorts(options?: Options): Promise { 129 | let ports 130 | let exclude = new Set() 131 | 132 | if (options) { 133 | if (options.port) { 134 | ports = typeof options.port === 'number' ? [options.port] : options.port 135 | } 136 | 137 | if (options.exclude) { 138 | const excludeIterable = options.exclude 139 | 140 | if (typeof excludeIterable[Symbol.iterator] !== 'function') { 141 | throw new TypeError('The `exclude` option must be an iterable.') 142 | } 143 | 144 | for (const element of excludeIterable) { 145 | if (typeof element !== 'number') { 146 | throw new TypeError( 147 | 'Each item in the `exclude` option must be a number corresponding to the port you want excluded.', 148 | ) 149 | } 150 | 151 | if (!Number.isSafeInteger(element)) { 152 | throw new TypeError( 153 | `Number ${element} in the exclude option is not a safe integer and can't be used`, 154 | ) 155 | } 156 | } 157 | 158 | exclude = new Set(excludeIterable) 159 | } 160 | } 161 | 162 | if (interval === undefined) { 163 | interval = setInterval(() => { 164 | lockedPorts.old = lockedPorts.young 165 | lockedPorts.young = new Set() 166 | }, releaseOldLockedPortsIntervalMs) 167 | 168 | // Does not exist in some environments (Electron, Jest jsdom env, browser, etc). 169 | if (interval.unref) { 170 | interval.unref() 171 | } 172 | } 173 | 174 | const hosts = getLocalHosts() 175 | 176 | for (const port of portCheckSequence(ports)) { 177 | try { 178 | if (exclude.has(port)) { 179 | continue 180 | } 181 | 182 | let availablePort = await getAvailablePort({ ...options, port }, hosts) 183 | while ( 184 | lockedPorts.old.has(availablePort) || 185 | lockedPorts.young.has(availablePort) 186 | ) { 187 | if (port !== 0) { 188 | throw new Locked(port) 189 | } 190 | 191 | availablePort = await getAvailablePort({ ...options, port }, hosts) 192 | } 193 | 194 | lockedPorts.young.add(availablePort) 195 | 196 | return availablePort 197 | } catch (error: any) { 198 | if ( 199 | !['EADDRINUSE', 'EACCES'].includes(error.code) && 200 | !(error instanceof Locked) 201 | ) { 202 | throw error 203 | } 204 | } 205 | } 206 | 207 | throw new Error('No available ports found') 208 | } 209 | 210 | /** 211 | Generate port numbers in the given range `from`...`to`. 212 | 213 | @param from - The first port of the range. Must be in the range `1024`...`65535`. 214 | @param to - The last port of the range. Must be in the range `1024`...`65535` and must be greater than `from`. 215 | @returns The port numbers in the range. 216 | 217 | @example 218 | ``` 219 | import getPort, {portNumbers} from 'get-port'; 220 | 221 | console.log(await getPort({port: portNumbers(3000, 3100)})); 222 | // Will use any port from 3000 to 3100, otherwise fall back to a random port 223 | ``` 224 | */ 225 | export function portNumbers(from: number, to: number): Iterable { 226 | if (!Number.isInteger(from) || !Number.isInteger(to)) { 227 | throw new TypeError('`from` and `to` must be integer numbers') 228 | } 229 | 230 | if (from < minPort || from > maxPort) { 231 | throw new RangeError(`'from' must be between ${minPort} and ${maxPort}`) 232 | } 233 | 234 | if (to < minPort || to > maxPort) { 235 | throw new RangeError(`'to' must be between ${minPort} and ${maxPort}`) 236 | } 237 | 238 | if (from > to) { 239 | throw new RangeError('`to` must be greater than or equal to `from`') 240 | } 241 | 242 | const generator = function* (from: number, to: number) { 243 | for (let port = from; port <= to; port++) { 244 | yield port 245 | } 246 | } 247 | 248 | return generator(from, to) 249 | } 250 | -------------------------------------------------------------------------------- /src/renderer/pages/setting.page.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | 3 | import { useI18nContext } from '../../i18n/i18n-react' 4 | import { Option, Select, Switch, Typography } from '@material-tailwind/react' 5 | import { getThemeSystem, themeChanger } from '../utils/theme.util' 6 | import { CgDarkMode } from 'react-icons/cg' 7 | import { HiMoon, HiSun } from 'react-icons/hi' 8 | import { SettingInStore } from '../../shared/interfaces/settings.interface' 9 | import { MdBrowserUpdated } from 'react-icons/md' 10 | import { VscRunAbove } from 'react-icons/vsc' 11 | import { TbWindowMinimize } from 'react-icons/tb' 12 | import { Button } from 'react-daisyui' 13 | import { FaFileAlt, FaLaptop } from 'react-icons/fa' 14 | 15 | export function SettingPage() { 16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 17 | const [_, setStartUp] = useState(false) 18 | const { LL, locale } = useI18nContext() 19 | const [settingState, setSettingState] = useState( 20 | window.storePreload.get('settings'), 21 | ) 22 | 23 | function toggleStartUp() { 24 | window.ipc.toggleStartUP().then((res) => setStartUp(res)) 25 | } 26 | function toggleAutoUpdate() { 27 | setSettingState((prevState) => ({ 28 | ...prevState, 29 | autoUpdate: !prevState.autoUpdate, 30 | })) 31 | } 32 | 33 | function toggleMinimize_tray() { 34 | setSettingState((prevState) => ({ 35 | ...prevState, 36 | minimize_tray: !prevState.minimize_tray, 37 | })) 38 | } 39 | 40 | useEffect(() => { 41 | window.ipc.saveSettings(settingState).catch() 42 | }, [settingState]) 43 | 44 | return ( 45 |
49 |
50 |
51 |
52 |
53 | 54 |
55 |
56 |
57 | 63 | 68 | 69 | Start up 70 | 71 | 76 | {LL.pages.settings.autoRunningTitle()} 77 | 78 |
79 | } 80 | containerProps={{ 81 | className: '-mt-5 mr-2', 82 | }} 83 | onChange={toggleStartUp} 84 | defaultChecked={settingState.startUp} 85 | /> 86 |
87 |
88 | 94 | 99 | 100 | Automatic Update 101 | 102 | 107 | Get updates automatically 108 | 109 |
110 | } 111 | containerProps={{ 112 | className: '-mt-5 mr-2', 113 | }} 114 | onChange={toggleAutoUpdate} 115 | defaultChecked={settingState.autoUpdate} 116 | /> 117 |
118 |
119 | 125 | 130 | 131 | Minimize to Tray 132 | 133 | 138 | The app move to try in background 139 | 140 |
141 | } 142 | containerProps={{ 143 | className: '-mt-5 mr-2', 144 | }} 145 | onChange={toggleMinimize_tray} 146 | defaultChecked={settingState.minimize_tray} 147 | /> 148 |
149 |
150 |
151 | 161 | 171 |
172 |
173 | 174 | 175 | 176 | ) 177 | } 178 | 179 | const ThemeChanger = () => { 180 | const [currentTheme, setCurrentTheme] = useState( 181 | localStorage.getItem('theme') || getThemeSystem(), 182 | ) 183 | const { LL } = useI18nContext() 184 | 185 | useEffect(() => { 186 | themeChanger(currentTheme as any) 187 | localStorage.setItem('theme', currentTheme) 188 | }, [currentTheme]) 189 | 190 | return ( 191 |
192 | 222 |
223 | ) 224 | } 225 | -------------------------------------------------------------------------------- /src/i18n/i18n-types.ts: -------------------------------------------------------------------------------- 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. 2 | /* eslint-disable */ 3 | import type { 4 | BaseTranslation as BaseTranslationType, 5 | LocalizedString, 6 | RequiredParams, 7 | } from 'typesafe-i18n' 8 | 9 | export type BaseTranslation = BaseTranslationType 10 | export type BaseLocale = 'fa' 11 | 12 | export type Locales = 'eng' | 'fa' | 'ru' 13 | 14 | export type Translation = RootTranslation 15 | 16 | export type Translations = RootTranslation 17 | 18 | type RootTranslation = { 19 | pages: { 20 | home: { 21 | /** 22 | * ب​ه​ت​ر​ی​ن​ ​ه​ا​ی​ ​ر​ف​ع​ ​ت​ح​ر​ی​م 23 | */ 24 | homeTitle: string 25 | /** 26 | * ش​م​ا​ ​ب​ه​ ​ ​<​u​>​{​c​u​r​r​e​n​t​A​c​t​i​v​e​}​<​/​u​>​ ​م​ت​ص​ل​ ​ش​د​ی​د 27 | * @param {unknown} currentActive 28 | */ 29 | connectedHTML: RequiredParams<'currentActive'> 30 | /** 31 | * ش​م​ا​ ​ب​ه​ ​ ​{​c​u​r​r​e​n​t​A​c​t​i​v​e​}​ ​م​ت​ص​ل​ ​ش​د​ی​د 32 | * @param {unknown} currentActive 33 | */ 34 | connected: RequiredParams<'currentActive'> 35 | /** 36 | * ق​ط​ع​ ​ش​د​. 37 | */ 38 | disconnected: string 39 | /** 40 | * ب​ه​ ​ی​ک​ ​س​ر​و​ر​ ​ ​ن​ا​ش​ن​ا​خ​ت​ه​ ​م​ت​ص​ل​ ​ه​س​ت​ی​د​. 41 | */ 42 | unknownServer: string 43 | } 44 | settings: { 45 | /** 46 | * ت​ن​ظ​ی​م​ا​ت 47 | */ 48 | title: string 49 | /** 50 | * ا​ج​ر​ا​ ​ش​د​ن​ ​خ​و​د​ک​ا​ر​ ​ب​ر​ن​ا​م​ه​ ​ب​ا​ ​ر​و​ش​ن​ ​ش​د​ن​ ​س​ی​س​ت​م 51 | */ 52 | autoRunningTitle: string 53 | /** 54 | * ت​غ​ی​ی​ر​ ​ز​ب​ـ​ا​ن 55 | */ 56 | langChanger: string 57 | /** 58 | * ت​غ​ی​ی​ر​ ​پ​و​س​ت​ه 59 | */ 60 | themeChanger: string 61 | } 62 | addCustomDns: { 63 | /** 64 | * ن​ا​م​ ​س​ر​و​ر 65 | */ 66 | NameOfServer: string 67 | /** 68 | * آ​د​ر​س​ ​س​ر​و​ر 69 | */ 70 | serverAddr: string 71 | } 72 | } 73 | themeChanger: { 74 | /** 75 | * ت​ا​ر​ی​ک 76 | */ 77 | dark: string 78 | /** 79 | * ر​و​ش​ـ​ن 80 | */ 81 | light: string 82 | } 83 | buttons: { 84 | /** 85 | * ب​ر​و​ز​ ​ر​س​ا​ن​ی​ ​ل​ی​س​ت 86 | */ 87 | update: string 88 | /** 89 | * ا​ف​ز​و​د​ن​ ​س​ر​و​ر​ ​(​D​N​S​)​ ​د​ل​خ​و​ا​ه 90 | */ 91 | favDnsServer: string 92 | /** 93 | * ا​ف​ز​و​د​ن 94 | */ 95 | add: string 96 | /** 97 | * پ​ا​ک​س​ا​ز​ی​ ​(​F​l​u​s​h​) 98 | */ 99 | flushDns: string 100 | /** 101 | * پ​ی​ـ​ن​گ​ ​س​ر​و​ر​ه​ا 102 | */ 103 | ping: string 104 | } 105 | dialogs: { 106 | /** 107 | * د​ر​ح​ا​ل​ ​د​ر​ی​ا​ف​ت​ ​د​ی​ت​ا​ ​ا​ز​ ​م​خ​ز​ن 108 | */ 109 | fetching_data_from_repo: string 110 | /** 111 | * س​ر​و​ر​ ​{​s​e​r​v​e​r​N​a​m​e​}​ ​ب​ا​ ​م​و​ف​ق​ی​ت​ ​ا​ض​ا​ف​ه​ ​ش​د​. 112 | * @param {unknown} serverName 113 | */ 114 | added_server: RequiredParams<'serverName'> 115 | /** 116 | * س​ر​و​ر​ ​{​s​e​r​v​e​r​N​a​m​e​}​ ​ب​ا​ ​م​و​ف​ق​ی​ت​ ​ح​ذ​ف​ ​ش​د​. 117 | * @param {unknown} serverName 118 | */ 119 | removed_server: RequiredParams<'serverName'> 120 | /** 121 | * پ​ا​ک​س​ا​ز​ی​ ​ب​ا​ ​م​و​ف​ق​ی​ت​ ​ا​ن​ج​ا​م​ ​ش​د​. 122 | */ 123 | flush_successful: string 124 | /** 125 | * پ​ا​ک​س​ا​ز​ی​ ​ن​ا​م​و​ف​ق​ ​ب​و​د​. 126 | */ 127 | flush_failure: string 128 | } 129 | errors: { 130 | /** 131 | * خ​ط​ا​ ​د​ر​ ​د​ر​ی​ا​ف​ت​ ​د​ی​ت​ا​ ​ا​ز​ ​{​t​a​r​g​e​t​} 132 | * @param {unknown} target 133 | */ 134 | error_fetching_data: RequiredParams<'target'> 135 | } 136 | /** 137 | * د​ر​ح​ا​ل​ ​ا​ت​ص​ا​ل​.​.​. 138 | */ 139 | connecting: string 140 | /** 141 | * ق​ط​ع​ ​ش​د​ن​.​.​. 142 | */ 143 | disconnecting: string 144 | /** 145 | * ک​م​ی​ ​ص​ب​ر​ ​ک​ن​ی​د​.​.​. 146 | */ 147 | waiting: string 148 | /** 149 | * م​و​ف​ق​ی​ت​ ​آ​م​ی​ز 150 | */ 151 | successful: string 152 | /** 153 | * ب​ر​ا​ی​ ​ا​ت​ص​ا​ل​ ​ک​ل​ی​ک​ ​ک​ن​ی​د 154 | */ 155 | help_connect: string 156 | /** 157 | * ب​ر​ا​ی​ ​ق​ط​ع​ ​ا​ت​ص​ا​ل​ ​ک​ل​ی​ک​ ​ک​ن​ی​د 158 | */ 159 | help_disconnect: string 160 | validator: { 161 | /** 162 | * آ​د​ر​س​ ​س​ر​و​ر​ ​1​ ​ن​ا​م​ع​ت​ب​ر​ ​ا​س​ت​. 163 | */ 164 | invalid_dns1: string 165 | /** 166 | * آ​د​ر​س​ ​س​ر​و​ر​ ​2​ ​ن​ا​م​ع​ت​ب​ر​ ​ا​س​ت​. 167 | */ 168 | invalid_dns2: string 169 | /** 170 | * آ​د​ر​س​ ​س​ر​و​ر​ه​ا​ی​ ​1​ ​و​ ​2​ ​ن​ب​ا​ی​د​ ​ت​ک​ر​ا​ر​ی​ ​ب​ا​ش​ن​د​. 171 | */ 172 | dns1_dns2_duplicates: string 173 | } 174 | /** 175 | * ن​س​خ​ه 176 | */ 177 | version: string 178 | } 179 | 180 | export type TranslationFunctions = { 181 | pages: { 182 | home: { 183 | /** 184 | * بهترین های رفع تحریم 185 | */ 186 | homeTitle: () => LocalizedString 187 | /** 188 | * شما به {currentActive} متصل شدید 189 | */ 190 | connectedHTML: (arg: { currentActive: unknown }) => LocalizedString 191 | /** 192 | * شما به {currentActive} متصل شدید 193 | */ 194 | connected: (arg: { currentActive: unknown }) => LocalizedString 195 | /** 196 | * قطع شد. 197 | */ 198 | disconnected: () => LocalizedString 199 | /** 200 | * به یک سرور ناشناخته متصل هستید. 201 | */ 202 | unknownServer: () => LocalizedString 203 | } 204 | settings: { 205 | /** 206 | * تنظیمات 207 | */ 208 | title: () => LocalizedString 209 | /** 210 | * اجرا شدن خودکار برنامه با روشن شدن سیستم 211 | */ 212 | autoRunningTitle: () => LocalizedString 213 | /** 214 | * تغییر زبـان 215 | */ 216 | langChanger: () => LocalizedString 217 | /** 218 | * تغییر پوسته 219 | */ 220 | themeChanger: () => LocalizedString 221 | } 222 | addCustomDns: { 223 | /** 224 | * نام سرور 225 | */ 226 | NameOfServer: () => LocalizedString 227 | /** 228 | * آدرس سرور 229 | */ 230 | serverAddr: () => LocalizedString 231 | } 232 | } 233 | themeChanger: { 234 | /** 235 | * تاریک 236 | */ 237 | dark: () => LocalizedString 238 | /** 239 | * روشـن 240 | */ 241 | light: () => LocalizedString 242 | } 243 | buttons: { 244 | /** 245 | * بروز رسانی لیست 246 | */ 247 | update: () => LocalizedString 248 | /** 249 | * افزودن سرور (DNS) دلخواه 250 | */ 251 | favDnsServer: () => LocalizedString 252 | /** 253 | * افزودن 254 | */ 255 | add: () => LocalizedString 256 | /** 257 | * پاکسازی (Flush) 258 | */ 259 | flushDns: () => LocalizedString 260 | /** 261 | * پیـنگ سرورها 262 | */ 263 | ping: () => LocalizedString 264 | } 265 | dialogs: { 266 | /** 267 | * درحال دریافت دیتا از مخزن 268 | */ 269 | fetching_data_from_repo: () => LocalizedString 270 | /** 271 | * سرور {serverName} با موفقیت اضافه شد. 272 | */ 273 | added_server: (arg: { serverName: unknown }) => LocalizedString 274 | /** 275 | * سرور {serverName} با موفقیت حذف شد. 276 | */ 277 | removed_server: (arg: { serverName: unknown }) => LocalizedString 278 | /** 279 | * پاکسازی با موفقیت انجام شد. 280 | */ 281 | flush_successful: () => LocalizedString 282 | /** 283 | * پاکسازی ناموفق بود. 284 | */ 285 | flush_failure: () => LocalizedString 286 | } 287 | errors: { 288 | /** 289 | * خطا در دریافت دیتا از {target} 290 | */ 291 | error_fetching_data: (arg: { target: unknown }) => LocalizedString 292 | } 293 | /** 294 | * درحال اتصال... 295 | */ 296 | connecting: () => LocalizedString 297 | /** 298 | * قطع شدن... 299 | */ 300 | disconnecting: () => LocalizedString 301 | /** 302 | * کمی صبر کنید... 303 | */ 304 | waiting: () => LocalizedString 305 | /** 306 | * موفقیت آمیز 307 | */ 308 | successful: () => LocalizedString 309 | /** 310 | * برای اتصال کلیک کنید 311 | */ 312 | help_connect: () => LocalizedString 313 | /** 314 | * برای قطع اتصال کلیک کنید 315 | */ 316 | help_disconnect: () => LocalizedString 317 | validator: { 318 | /** 319 | * آدرس سرور 1 نامعتبر است. 320 | */ 321 | invalid_dns1: () => LocalizedString 322 | /** 323 | * آدرس سرور 2 نامعتبر است. 324 | */ 325 | invalid_dns2: () => LocalizedString 326 | /** 327 | * آدرس سرورهای 1 و 2 نباید تکراری باشند. 328 | */ 329 | dns1_dns2_duplicates: () => LocalizedString 330 | } 331 | /** 332 | * نسخه 333 | */ 334 | version: () => LocalizedString 335 | } 336 | 337 | export type Formatters = {} 338 | -------------------------------------------------------------------------------- /src/main/ipc/dialogs.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, dialog, ipcMain, shell } from 'electron' 2 | import _ from 'lodash' 3 | import pingLib from 'ping' 4 | import { v4 as uuid } from 'uuid' 5 | 6 | import LN from '../../i18n/i18n-node' 7 | import { Locales } from '../../i18n/i18n-types' 8 | import { EventsKeys } from '../../shared/constants/eventsKeys.constant' 9 | import { Server, ServerStore } from '../../shared/interfaces/server.interface' 10 | import { isValidDnsAddress } from '../../shared/validators/dns.validator' 11 | import { dnsService } from '../config' 12 | import { WindowsPlatform } from '../platforms/windows/windows.platform' 13 | import { getOverlayIcon } from '../shared/file' 14 | import { LogId, getLoggerPathFile, userLogger } from '../shared/logger' 15 | import { updateOverlayIcon } from '../shared/overlayIcon' 16 | import { isWindows } from '../shared/platform' 17 | import { store } from '../store/store' 18 | 19 | ipcMain.handle(EventsKeys.SET_DNS, async (event, server: Server) => { 20 | try { 21 | if (isWindows()) { 22 | const winPlatform = new WindowsPlatform() 23 | const isAvailableWmic = await winPlatform.isWmicAvailable() 24 | if (!isAvailableWmic) { 25 | return { 26 | server, 27 | success: false, 28 | message: 'wmic_not_available', 29 | } 30 | } 31 | } 32 | 33 | await dnsService.setDns(server.servers) 34 | const currentLng = LN[getCurrentLng()] 35 | const win = BrowserWindow.getAllWindows()[0] 36 | const filepath = await getOverlayIcon(server) 37 | updateOverlayIcon(win, filepath, 'connected') 38 | 39 | return { 40 | server, 41 | success: true, 42 | message: currentLng.pages.home.connected({ 43 | currentActive: server.name, 44 | }), 45 | } 46 | } catch (e) { 47 | userLogger.error(e.stack, e.message) 48 | return { 49 | server, 50 | success: false, 51 | message: 'Unknown error while connecting', 52 | } 53 | } 54 | }) 55 | 56 | ipcMain.handle(EventsKeys.CLEAR_DNS, async (event, server: Server) => { 57 | try { 58 | if (isWindows()) { 59 | const winPlatform = new WindowsPlatform() 60 | const isAvailableWmic = await winPlatform.isWmicAvailable() 61 | if (!isAvailableWmic) { 62 | return { 63 | server, 64 | success: false, 65 | message: 'wmic_not_available', 66 | } 67 | } 68 | } 69 | 70 | await dnsService.clearDns() 71 | 72 | const currentLng = LN[getCurrentLng()] 73 | const win = BrowserWindow.getAllWindows()[0] 74 | 75 | updateOverlayIcon(win, null, 'disconnect') 76 | const defaultServer = store.get('defaultServer') 77 | 78 | if (defaultServer) { 79 | const servers = defaultServer.servers 80 | dnsService.setDns(servers).catch((err) => { 81 | userLogger.error(err.stack, err.message) 82 | }) 83 | } 84 | 85 | return { 86 | server, 87 | success: true, 88 | message: currentLng.pages.home.disconnected(), 89 | } 90 | } catch (e) { 91 | userLogger.error(e.stack, e.message) 92 | return { server, success: false, message: 'Unknown error while clear DNS' } 93 | } 94 | }) 95 | 96 | ipcMain.handle(EventsKeys.ADD_DNS, async (event, data: Partial) => { 97 | if (data.name === 'default') { 98 | const defaultServer = store.get('defaultServer') 99 | const server: Server = { 100 | key: 'default', 101 | servers: data.servers, 102 | name: 'default', 103 | tags: [], 104 | avatar: '', 105 | rate: 0, 106 | } 107 | 108 | if (!defaultServer) { 109 | store.set('defaultServer', server) 110 | } else { 111 | defaultServer.servers = data.servers 112 | store.set('defaultServer', defaultServer) 113 | } 114 | 115 | return { success: true, server: server } 116 | } 117 | 118 | const nameServer1 = data.servers[0] 119 | const nameServer2 = data.servers[1] 120 | if (!nameServer1) return { success: false, message: 'DNS1 is required' } 121 | 122 | const currentLng = LN[getCurrentLng()] 123 | 124 | if (!isValidDnsAddress(nameServer1)) 125 | return { success: false, message: currentLng.validator.invalid_dns1 } 126 | 127 | if (nameServer2 && !isValidDnsAddress(nameServer2)) 128 | return { success: false, message: currentLng.validator.invalid_dns2 } 129 | 130 | if (nameServer1.toString() === nameServer2.toString()) 131 | return { 132 | success: false, 133 | message: currentLng.validator.dns1_dns2_duplicates, 134 | } 135 | 136 | const list: Server[] = store.get('dnsList') || [] 137 | 138 | const newServer: ServerStore = { 139 | key: data.key || uuid(), 140 | name: data.name, 141 | avatar: data.avatar, 142 | servers: data.servers, 143 | rate: data.rate || 0, 144 | tags: data.tags || [], 145 | isPin: false, 146 | } 147 | 148 | const isDupKey = list.find((s) => s.key === newServer.key) 149 | if (isDupKey) newServer.key = uuid() 150 | 151 | list.push(newServer) 152 | 153 | store.set('dnsList', list) 154 | return { success: true, server: newServer, servers: list } 155 | }) 156 | 157 | ipcMain.handle(EventsKeys.DELETE_DNS, (ev, server: Server) => { 158 | const dnsList = store.get('dnsList') 159 | 160 | _.remove(dnsList, (dns) => dns.key === server.key) 161 | 162 | store.set('dnsList', dnsList) 163 | 164 | return { 165 | success: true, 166 | servers: dnsList, 167 | } 168 | }) 169 | 170 | ipcMain.handle( 171 | EventsKeys.RELOAD_SERVER_LIST, 172 | async (event, servers: Server[]) => { 173 | store.set('dnsList', servers) 174 | return { success: true } 175 | }, 176 | ) 177 | 178 | ipcMain.handle(EventsKeys.FETCH_DNS_LIST, () => { 179 | const servers = store.get('dnsList') || [] 180 | return { success: true, servers: servers } 181 | }) 182 | 183 | ipcMain.on(EventsKeys.GET_CURRENT_ACTIVE, getCurrentActive) 184 | 185 | ipcMain.handle(EventsKeys.GET_CURRENT_ACTIVE, getCurrentActive) 186 | 187 | ipcMain.on(EventsKeys.OPEN_BROWSER, (ev, url) => { 188 | shell.openExternal(url) 189 | }) 190 | 191 | ipcMain.on(EventsKeys.OPEN_DEV_TOOLS, () => { 192 | try { 193 | const win = BrowserWindow.getAllWindows()[0] 194 | win.webContents.openDevTools() 195 | } catch (e) {} 196 | }) 197 | 198 | // open log file 199 | ipcMain.on(EventsKeys.OPEN_LOG_FILE, () => { 200 | const logPathFile = getLoggerPathFile(LogId.USER) 201 | shell.openPath(logPathFile).catch((e) => { 202 | userLogger.error(e.stack, e.message) 203 | }) 204 | }) 205 | 206 | ipcMain.on(EventsKeys.DIALOG_ERROR, (ev, title: string, message: string) => { 207 | dialog.showErrorBox(title, message) 208 | }) 209 | 210 | ipcMain.handle(EventsKeys.FLUSHDNS, async () => { 211 | try { 212 | await dnsService.flushDns() 213 | return { success: true } 214 | } catch (error) { 215 | userLogger.error(error.stack, error.message) 216 | return { success: false } 217 | } 218 | }) 219 | 220 | ipcMain.handle(EventsKeys.PING, async (event, server: Server) => { 221 | try { 222 | const result = await pingLib.promise.probe(server.servers[0], { 223 | timeout: 10, 224 | }) 225 | return { 226 | success: true, 227 | data: { 228 | alive: result.alive, 229 | time: result.time, 230 | }, 231 | } 232 | } catch { 233 | return { 234 | success: false, 235 | } 236 | } 237 | }) 238 | ipcMain.handle(EventsKeys.TOGGLE_PIN, async (event, server: Server) => { 239 | const dnsList: ServerStore[] = store.get('dnsList') 240 | 241 | const serverStore = dnsList.find((ser) => ser.key === server.key) 242 | if (serverStore) { 243 | serverStore.isPin = !serverStore.isPin 244 | store.set('dnsList', dnsList) 245 | 246 | return { 247 | success: true, 248 | servers: dnsList, 249 | } 250 | } 251 | }) 252 | 253 | ipcMain.handle(EventsKeys.GET_NETWORK_INTERFACE_LIST, async () => { 254 | if (isWindows()) { 255 | const winPlatform = new WindowsPlatform() 256 | const isAvailableWmic = await winPlatform.isWmicAvailable() 257 | if (!isAvailableWmic) { 258 | return { 259 | success: false, 260 | message: 'wmic_not_available', 261 | } 262 | } 263 | } 264 | 265 | return dnsService.getInterfacesList() 266 | }) 267 | 268 | function getCurrentLng(): Locales { 269 | return store.get('settings').lng 270 | } 271 | 272 | async function getCurrentActive(): Promise<{ 273 | success: boolean 274 | server?: Partial 275 | isDefault?: boolean 276 | message?: string 277 | }> { 278 | try { 279 | const dns: string[] = await dnsService.getActiveDns() 280 | 281 | if (!dns.length) return { success: false, server: null } 282 | 283 | const servers = store.get('dnsList') || [] 284 | const server: ServerStore | null = servers.find( 285 | (server) => server.servers.toString() === dns.toString(), 286 | ) 287 | const defaultServer = store.get('defaultServer') 288 | if (defaultServer) { 289 | // if default server is connected, then return it as not connected 290 | if (defaultServer.servers.toString() === dns.toString()) { 291 | return { 292 | success: false, 293 | server: null, 294 | isDefault: true, 295 | } 296 | } 297 | } 298 | if (!server) { 299 | return { 300 | success: true, 301 | server: { 302 | key: 'unknown', 303 | servers: dns, 304 | names: { 305 | eng: 'unknown', 306 | fa: 'unknown', 307 | }, 308 | avatar: '', 309 | isPin: false, 310 | }, 311 | } 312 | } 313 | 314 | const win = BrowserWindow.getAllWindows()[0] 315 | 316 | const filepath = await getOverlayIcon(server) 317 | 318 | updateOverlayIcon(win, filepath, 'connected') 319 | 320 | return { success: true, server } 321 | } catch (e) { 322 | userLogger.error(e.stack, e.message) 323 | return { success: false, message: 'Unknown error while clear DNS' } 324 | } 325 | } 326 | --------------------------------------------------------------------------------