├── renderer ├── src │ ├── assets │ │ ├── .gitkeep │ │ └── top-bar │ │ │ ├── chrome-minimize.svg │ │ │ ├── chrome-maximize.svg │ │ │ ├── chrome-restore.svg │ │ │ └── chrome-close.svg │ ├── favicon.ico │ ├── scss │ │ ├── components │ │ │ ├── index.scss │ │ │ ├── _dropdown.scss │ │ │ ├── _modal.scss │ │ │ ├── _custom.scss │ │ │ ├── _theme.scss │ │ │ ├── _bottom-bar.scss │ │ │ └── _top-bar.scss │ │ ├── styles.scss │ │ └── variables.scss │ ├── main.ts │ ├── app │ │ ├── common │ │ │ ├── interfaces │ │ │ │ └── electron.ts │ │ │ ├── functions │ │ │ │ ├── utils.ts │ │ │ │ └── time.ts │ │ │ ├── pipes │ │ │ │ └── time-duration.pipe.ts │ │ │ ├── directives │ │ │ │ └── auto-focus.directive.ts │ │ │ └── icons.ts │ │ ├── components │ │ │ ├── bottom-bar.component.html │ │ │ ├── bottom-bar-component.ts │ │ │ ├── bottom-bar-syncs-component.html │ │ │ ├── top-bar-buttons.component.ts │ │ │ ├── bottom-bar-syncs.component.ts │ │ │ ├── bottom-bar-downloads.component.html │ │ │ └── modal-server.component.html │ │ ├── app.config.ts │ │ ├── app.component.html │ │ └── app.component.ts │ ├── index.html │ └── i18n │ │ ├── lib │ │ ├── bs.i18n.ts │ │ └── dayjs.i18n.ts │ │ └── l10n.ts ├── tsconfig.app.json ├── .browserslistrc ├── tsconfig.json ├── package.json ├── eslint.config.js └── angular.json ├── Dockerfile ├── .dockerignore ├── main ├── assets │ ├── app │ │ ├── icon.icns │ │ ├── icon.png │ │ ├── background.png │ │ └── background@2x.png │ └── tray │ │ ├── sync.png │ │ ├── enabled.png │ │ ├── disabled.png │ │ ├── dock │ │ ├── sync.png │ │ ├── enabled.png │ │ └── disabled.png │ │ └── states │ │ ├── ok.png │ │ └── failed.png ├── constants │ ├── themes.ts │ ├── autoupdater.ts │ ├── paths.ts │ ├── settings.ts │ ├── downloads.ts │ ├── windows.ts │ ├── events.ts │ └── menus.ts ├── interfaces │ ├── settings.interface.ts │ ├── counter.interface.ts │ ├── app-web-contents-view.ts │ ├── ipc-main-event.interface.ts │ └── download.interface.ts ├── package.json ├── preload.ts ├── components │ ├── settings.ts │ ├── translate.ts │ ├── transfers.ts │ ├── notifications.ts │ ├── windows.ts │ └── utils.ts └── main.ts ├── .prettierrc ├── .github └── FUNDING.yml ├── .gitignore ├── core ├── components │ ├── constants │ │ ├── errors.ts │ │ ├── permissions.ts │ │ ├── auth.ts │ │ ├── server.ts │ │ ├── requests.ts │ │ ├── handlers.ts │ │ └── diff.ts │ ├── interfaces │ │ ├── server.interface.ts │ │ ├── sync-client-auth.interface.ts │ │ ├── request.interface.ts │ │ ├── sync-client-info.interface.ts │ │ ├── sync-status.interface.ts │ │ ├── sync-path-settings.interface.ts │ │ ├── sync-diff.interface.ts │ │ └── sync-transfer.interface.ts │ ├── handlers │ │ ├── events.ts │ │ ├── settings.ts │ │ ├── tasks.ts │ │ └── loggers.ts │ ├── models │ │ ├── sync.ts │ │ └── server.ts │ ├── utils │ │ └── normalizedMap.ts │ └── main.ts ├── package.json └── constants.ts ├── cli ├── package.json ├── cmds │ ├── index.ts │ ├── cmd-run.ts │ └── cmd-servers.ts └── main.ts ├── tsconfig.json ├── i18n ├── index.ts ├── zh.json ├── ko.json ├── ja.json ├── en.json ├── hi.json ├── tr.json ├── pl.json ├── pt.json ├── ru.json ├── pt-BR.json ├── es.json ├── it.json ├── de.json └── fr.json ├── eslint.config.mjs ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CHANGELOG.md ├── webpack.config.mjs └── README.md /renderer/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | COPY releases/ /releases -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | ** 3 | 4 | # Except the releases directory 5 | !releases/ -------------------------------------------------------------------------------- /main/assets/app/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sync-in/desktop/HEAD/main/assets/app/icon.icns -------------------------------------------------------------------------------- /main/assets/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sync-in/desktop/HEAD/main/assets/app/icon.png -------------------------------------------------------------------------------- /main/assets/tray/sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sync-in/desktop/HEAD/main/assets/tray/sync.png -------------------------------------------------------------------------------- /renderer/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sync-in/desktop/HEAD/renderer/src/favicon.ico -------------------------------------------------------------------------------- /main/assets/tray/enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sync-in/desktop/HEAD/main/assets/tray/enabled.png -------------------------------------------------------------------------------- /main/assets/app/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sync-in/desktop/HEAD/main/assets/app/background.png -------------------------------------------------------------------------------- /main/assets/tray/disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sync-in/desktop/HEAD/main/assets/tray/disabled.png -------------------------------------------------------------------------------- /main/assets/tray/dock/sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sync-in/desktop/HEAD/main/assets/tray/dock/sync.png -------------------------------------------------------------------------------- /main/assets/tray/states/ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sync-in/desktop/HEAD/main/assets/tray/states/ok.png -------------------------------------------------------------------------------- /main/assets/app/background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sync-in/desktop/HEAD/main/assets/app/background@2x.png -------------------------------------------------------------------------------- /main/assets/tray/dock/enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sync-in/desktop/HEAD/main/assets/tray/dock/enabled.png -------------------------------------------------------------------------------- /main/assets/tray/dock/disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sync-in/desktop/HEAD/main/assets/tray/dock/disabled.png -------------------------------------------------------------------------------- /main/assets/tray/states/failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sync-in/desktop/HEAD/main/assets/tray/states/failed.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "semi": false, 5 | "printWidth": 150 6 | } 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Sync-in 4 | patreon: sync_in 5 | liberapay: sync-in 6 | custom: https://sync-in.com/support/ 7 | -------------------------------------------------------------------------------- /renderer/src/assets/top-bar/chrome-minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /renderer/src/assets/top-bar/chrome-maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | releases 4 | .angular 5 | renderer/.angular 6 | tmp 7 | out-tsc 8 | bazel-out 9 | npm-debug.log 10 | 11 | # Others 12 | .idea 13 | *.DS_Store 14 | Thumbs.db 15 | electron-builder.env 16 | -------------------------------------------------------------------------------- /renderer/src/assets/top-bar/chrome-restore.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/components/constants/errors.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export const CONNECTION_ERRORS = new Set(['ECONNREFUSED', 'ECONNRESET']) 8 | -------------------------------------------------------------------------------- /main/constants/themes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export enum THEME { 8 | LIGHT = 'theme-light', 9 | DARK = 'theme-dark' 10 | } 11 | -------------------------------------------------------------------------------- /core/components/constants/permissions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export enum SYNC_PATH_PERMISSION { 8 | ADD = 'a', 9 | MODIFY = 'm', 10 | DELETE = 'd' 11 | } 12 | -------------------------------------------------------------------------------- /renderer/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [ 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "./src/main.ts" 12 | ], 13 | "include": [ 14 | "./src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-in-desktop/core", 3 | "description": "Core", 4 | "license": "AGPL-3.0-or-later", 5 | "private": true, 6 | "author": { 7 | "name": "Johan Legrand", 8 | "email": "johan.legrand@sync-in.com" 9 | }, 10 | "dependencies": { 11 | "axios": "^1.8.1", 12 | "winston": "^3.8.2", 13 | "js-yaml": "^4.1.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /renderer/src/scss/components/index.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | @import 'theme'; 8 | @import 'dropdown'; 9 | @import 'top-bar'; 10 | @import 'bottom-bar'; 11 | @import 'modal'; 12 | @import 'custom'; -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-in-desktop/cli", 3 | "description": "Command Line", 4 | "license": "AGPL-3.0-or-later", 5 | "private": true, 6 | "author": { 7 | "name": "Johan Legrand", 8 | "email": "johan.legrand@sync-in.com" 9 | }, 10 | "dependencies": { 11 | "yargs": "^17.6.2" 12 | }, 13 | "devDependencies": { 14 | "@types/yargs": "^17.0.19" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /main/constants/autoupdater.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export const APP_STORE_RELEASES_URL = 'releases' 8 | 9 | export enum APP_STORE_REPOSITORY { 10 | LOCAL = 'local', 11 | REMOTE = 'remote' 12 | } 13 | -------------------------------------------------------------------------------- /main/constants/paths.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export enum PATH_ACTION { 8 | LIST = 'list', 9 | SYNC = 'sync', 10 | ADD = 'add', 11 | FLUSH = 'flush', 12 | SET = 'set', 13 | REMOVE = 'remove' 14 | } 15 | -------------------------------------------------------------------------------- /main/constants/settings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import path from 'node:path' 8 | import { CONF_PATH } from '@sync-in-desktop/core/constants' 9 | 10 | export const APP_CONF_FILE = path.join(CONF_PATH, 'app.json') 11 | -------------------------------------------------------------------------------- /main/interfaces/settings.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export interface AppConfiguration { 8 | launchAtStartup: boolean 9 | startHidden: boolean 10 | // Only for macOS 11 | hideDockIcon: boolean 12 | } 13 | -------------------------------------------------------------------------------- /core/components/interfaces/server.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export interface SyncServer { 8 | id: number 9 | name: string 10 | url: string 11 | available: boolean 12 | authTokenExpired: boolean 13 | } 14 | -------------------------------------------------------------------------------- /renderer/src/assets/top-bar/chrome-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/components/interfaces/sync-client-auth.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { SyncClientInfo } from './sync-client-info.interface' 8 | 9 | export class SyncClientAuth { 10 | clientId: string 11 | token: string 12 | info: SyncClientInfo 13 | } 14 | -------------------------------------------------------------------------------- /main/interfaces/counter.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export type ApplicationCounter = 'notifications' | 'tasks' | 'syncs' 8 | 9 | export interface ServerAppCounter { 10 | id: number 11 | name: string 12 | applications: Record 13 | } 14 | -------------------------------------------------------------------------------- /core/components/interfaces/request.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { AxiosRequestConfig } from 'axios' 8 | import { ReadStream } from 'node:fs' 9 | 10 | export interface AxiosExtendedRequestConfig extends AxiosRequestConfig { 11 | getData?: () => ReadStream 12 | } 13 | -------------------------------------------------------------------------------- /core/components/constants/auth.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export const CLIENT_TOKEN_EXPIRED_ERROR = 'Client token is expired' 8 | 9 | export enum SYNC_CLIENT_TYPE { 10 | CLI = 'sync-in-cli', 11 | DESKTOP = 'sync-in-desktop' 12 | } 13 | 14 | export const SYNC_SERVER = 'sync-in' 15 | -------------------------------------------------------------------------------- /cli/cmds/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { getRunCMD } from './cmd-run' 8 | import { getServerCMD } from './cmd-servers' 9 | import { getPathsCMD } from './cmd-paths' 10 | 11 | export function getCMDS(yargs): any[] { 12 | return [getRunCMD(), getServerCMD(yargs), getPathsCMD(yargs)] 13 | } 14 | -------------------------------------------------------------------------------- /main/interfaces/app-web-contents-view.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { WebContents, WebContentsView } from 'electron' 8 | 9 | export interface AppWebContentsView extends WebContentsView { 10 | webContents: WebContents & { 11 | serverId?: number 12 | serverName?: string 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /renderer/src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { bootstrapApplication } from '@angular/platform-browser' 8 | import { AppComponent } from './app/app.component' 9 | import { appConfig } from './app/app.config' 10 | 11 | bootstrapApplication(AppComponent, appConfig).catch((e) => console.error(e)) 12 | -------------------------------------------------------------------------------- /renderer/src/scss/styles.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | @import 'variables'; 8 | // Load Bootstrap 5 9 | @import '../../../node_modules/bootstrap/scss/bootstrap'; 10 | @import 'components/index'; 11 | // used for date picker 12 | @import '../../../node_modules/ngx-bootstrap/datepicker/bs-datepicker'; 13 | -------------------------------------------------------------------------------- /core/components/interfaces/sync-client-info.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { SYNC_CLIENT_TYPE } from '../constants/auth' 8 | 9 | export interface SyncClientInfo { 10 | node: string 11 | os: string 12 | osRelease: string 13 | user: string 14 | type: SYNC_CLIENT_TYPE 15 | version: string 16 | } 17 | -------------------------------------------------------------------------------- /renderer/src/app/common/interfaces/electron.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export interface ElectronIpcRenderer { 8 | on: (channel: string, listener: (event: any, ...args: any[]) => void) => this 9 | invoke: (channel: string, ...args: any[]) => Promise 10 | send: (channel: string, ...args: any[]) => void 11 | } 12 | -------------------------------------------------------------------------------- /core/components/constants/server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export enum SERVER_ACTION { 8 | ADD = 'add', 9 | EDIT = 'edit', 10 | REMOVE = 'remove', 11 | AUTHENTICATE = 'authenticate' 12 | } 13 | 14 | export enum SERVER_SCHEDULER_STATE { 15 | DISABLED = 'disabled', 16 | ASYNC = 'async', 17 | SEQ = 'seq' 18 | } 19 | -------------------------------------------------------------------------------- /renderer/src/index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | Sync-in 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /renderer/src/app/components/bottom-bar.component.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /core/components/interfaces/sync-status.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import type { SyncTransfer } from './sync-transfer.interface' 8 | 9 | export interface SyncStatus { 10 | serverId?: number 11 | syncPathId: number 12 | state?: boolean 13 | reportOnly?: boolean 14 | mainError?: string 15 | lastErrors?: SyncTransfer[] 16 | } 17 | -------------------------------------------------------------------------------- /renderer/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | -------------------------------------------------------------------------------- /main/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-in-desktop/main", 3 | "description": "Desktop Application Main", 4 | "license": "AGPL-3.0-or-later", 5 | "author": { 6 | "name": "Johan Legrand", 7 | "email": "johan.legrand@sync-in.com" 8 | }, 9 | "private": true, 10 | "dependencies": { 11 | "mime-types": "^3.0.1" 12 | }, 13 | "devDependencies": { 14 | "@electron/notarize": "^3.0.1", 15 | "@types/mime-types": "^3.0.1", 16 | "electron": "^38.0.0", 17 | "electron-builder": "^26.0.12", 18 | "electron-updater": "^6.3.9" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /main/interfaces/ipc-main-event.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import type { IpcMainEvent, IpcMainInvokeEvent } from 'electron' 8 | 9 | export type IpcMainEventServer = IpcMainEvent & { sender: IpcMainEvent['sender'] & { serverId: number } } 10 | 11 | export type IpcMainInvokeEventServer = IpcMainInvokeEvent & { sender: IpcMainInvokeEvent['sender'] & { serverId: number } } 12 | -------------------------------------------------------------------------------- /main/constants/downloads.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export enum DOWNLOAD_ACTION { 8 | OPEN = 'open', 9 | PAUSE = 'pause', 10 | CANCEL = 'cancel', 11 | RESUME = 'resume', 12 | REMOVE = 'remove' 13 | } 14 | 15 | export enum DOWNLOAD_STATE { 16 | PROGRESSING = 'progressing', 17 | COMPLETED = 'completed', 18 | CANCELLED = 'cancelled', 19 | INTERRUPTED = 'interrupted', 20 | PAUSED = 'paused' 21 | } 22 | -------------------------------------------------------------------------------- /main/interfaces/download.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import type { DOWNLOAD_STATE } from '../constants/downloads' 8 | 9 | export interface IDownload { 10 | id: string 11 | name: string 12 | state: DOWNLOAD_STATE 13 | progress: number 14 | humanSpeed: { value: number; unit: string } 15 | humanSize: { done: number; total: number; unit: string } 16 | timeLeft: number 17 | icon?: any 18 | } 19 | -------------------------------------------------------------------------------- /core/components/handlers/events.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import EventEmitter from 'events' 8 | 9 | // constants 10 | export const CORE = { 11 | TASKS_COUNT: 'core-tasks-count', 12 | SYNC_STATUS: 'core-sync-status', 13 | SYNC_START: 'core-sync-start', 14 | SYNC_STOP: 'core-sync-stop', 15 | SAVE_SETTINGS: 'core-save-settings', 16 | EXIT: 'core-exit' 17 | } 18 | 19 | export const coreEvents = new EventEmitter() 20 | -------------------------------------------------------------------------------- /renderer/src/app/common/functions/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { THEME } from '../../../../../main/constants/themes' 8 | 9 | export function stopEventPropagation(ev: Event) { 10 | ev.preventDefault() 11 | ev.stopPropagation() 12 | } 13 | 14 | export function getTheme(): THEME { 15 | if (window.matchMedia) { 16 | return window.matchMedia('(prefers-color-scheme: dark)').matches ? THEME.DARK : THEME.LIGHT 17 | } 18 | return THEME.LIGHT 19 | } 20 | -------------------------------------------------------------------------------- /renderer/src/app/common/functions/time.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import dayjs from 'dayjs/esm' 8 | import duration from 'dayjs/esm/plugin/duration' 9 | import localizedFormat from 'dayjs/esm/plugin/localizedFormat' 10 | import relativeTime from 'dayjs/esm/plugin/relativeTime' 11 | import utc from 'dayjs/esm/plugin/utc' 12 | 13 | dayjs.extend(relativeTime) 14 | dayjs.extend(localizedFormat) 15 | dayjs.extend(utc) 16 | dayjs.extend(duration) 17 | 18 | export { dayjs as dJs } 19 | -------------------------------------------------------------------------------- /renderer/src/scss/components/_dropdown.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | .dropdown-menu { 8 | min-width: 0 !important; 9 | font-size: 0.8rem; 10 | background-clip: border-box; 11 | padding: 0; 12 | overflow: hidden; 13 | 14 | .dropdown-divider { 15 | margin: 0; 16 | border-top-width: .5px; 17 | } 18 | 19 | .dropdown-item { 20 | padding: 0.5rem; 21 | cursor: pointer; 22 | min-width: 200px; 23 | 24 | .menu-item { 25 | cursor: pointer; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/components/interfaces/sync-path-settings.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { CONFLICT_MODE, DIFF_MODE, SYNC_MODE } from '../constants/diff' 8 | 9 | export interface SyncPathSettings { 10 | name: string 11 | localPath: string 12 | remotePath: string 13 | permissions: string 14 | mode: SYNC_MODE 15 | enabled: boolean 16 | diffMode: DIFF_MODE 17 | conflictMode: CONFLICT_MODE 18 | filters: string[] 19 | scheduler: { value: number; unit: string } 20 | timestamp: number 21 | lastSync: Date 22 | } 23 | -------------------------------------------------------------------------------- /renderer/src/app/components/bottom-bar-component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { Component } from '@angular/core' 8 | import { BottomBarDownloadsComponent } from './bottom-bar-downloads.component' 9 | import { BottomBarSyncsComponent } from './bottom-bar-syncs.component' 10 | 11 | @Component({ 12 | selector: 'app-bottom-bar-component', 13 | templateUrl: 'bottom-bar.component.html', 14 | imports: [BottomBarDownloadsComponent, BottomBarSyncsComponent], 15 | standalone: true 16 | }) 17 | export class BottomBarComponent {} 18 | -------------------------------------------------------------------------------- /renderer/src/app/common/pipes/time-duration.pipe.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { Pipe, PipeTransform } from '@angular/core' 8 | import { dJs } from '../functions/time' 9 | 10 | @Pipe({ name: 'amDuration', pure: false, standalone: true }) 11 | export class TimeDurationPipe implements PipeTransform { 12 | transform(value: any, unit: string): string { 13 | if (!unit) { 14 | throw new Error('TimeDurationPipe: missing required time unit argument') 15 | } 16 | return dJs.duration({ [unit]: value }).humanize() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/components/models/sync.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { RequestsManager } from '../handlers/requests' 8 | import { Sync } from '../handlers/sync' 9 | import { FilesParser } from '../handlers/parser' 10 | import { Report } from '../handlers/report' 11 | 12 | export class SyncInstance { 13 | syncPathId: number 14 | req: RequestsManager 15 | instance: Sync | FilesParser | Report 16 | reportOnly = false 17 | 18 | constructor(syncPathId: number, req: RequestsManager, reportOnly = false) { 19 | this.syncPathId = syncPathId 20 | this.req = req 21 | this.reportOnly = reportOnly 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/components/interfaces/sync-diff.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { F_SPECIAL_STAT } from '../constants/handlers' 8 | import { NormalizedMap } from '../utils/normalizedMap' 9 | 10 | export type SyncFileStats = [boolean, number, number, number, string | null, number?] 11 | 12 | export type SyncFileSpecialStats = [F_SPECIAL_STAT, string | boolean] 13 | 14 | export type SyncSnapShot = NormalizedMap 15 | 16 | export class SyncDiff { 17 | secureDiff: boolean 18 | firstSync: boolean 19 | defaultFilters: string[] 20 | pathFilters: string 21 | snapshot?: Record 22 | } 23 | -------------------------------------------------------------------------------- /core/components/interfaces/sync-transfer.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { F_ACTION, SIDE } from '../constants/handlers' 8 | 9 | export interface SyncTransfer { 10 | ok?: boolean 11 | name?: string 12 | side: SIDE.LOCAL | SIDE.REMOTE 13 | action: F_ACTION 14 | file: string 15 | isDir: boolean 16 | fileDst?: string 17 | mime?: string 18 | error?: string 19 | serverId?: number 20 | syncPathId?: number 21 | progress?: { currentSize: string; totalSize: string; percent: string } 22 | } 23 | 24 | export interface SyncTransferContext { 25 | server: { id: number; name: string } 26 | path: { id: number; name: string } 27 | } 28 | -------------------------------------------------------------------------------- /renderer/src/app/common/directives/auto-focus.directive.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { Directive, ElementRef, inject, Input, OnInit } from '@angular/core' 8 | 9 | @Directive({ selector: '[appAutofocus]' }) 10 | export class AutofocusDirective implements OnInit { 11 | private readonly elementRef = inject(ElementRef) 12 | @Input() autoFocus = true 13 | @Input() autoSelect = true 14 | 15 | ngOnInit() { 16 | setTimeout(() => { 17 | if (this.autoFocus) { 18 | this.elementRef.nativeElement.focus() 19 | } 20 | if (this.autoSelect) { 21 | this.elementRef.nativeElement.select() 22 | } 23 | }, 0) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/components/constants/requests.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | const API_BASE = '/api/app/sync' 8 | export const API = { 9 | HANDSHAKE: `${API_BASE}/handshake`, 10 | REGISTER: `${API_BASE}/register`, 11 | UNREGISTER: `${API_BASE}/unregister`, 12 | AUTH_TOKEN: `${API_BASE}/auth/token`, 13 | AUTH_TOKEN_REFRESH: '/api/auth/token/refresh', 14 | PATHS: `${API_BASE}/paths`, 15 | DIFF: `${API_BASE}/operation/diff`, 16 | OPERATION: `${API_BASE}/operation`, 17 | APP_STORE: `${API_BASE}/app-store`, 18 | MAKE: 'make' 19 | } 20 | 21 | export interface TOKEN_RESPONSE { 22 | access: string 23 | refresh: string 24 | refresh_expiration: number 25 | client_token_update?: string 26 | } 27 | -------------------------------------------------------------------------------- /main/preload.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { contextBridge, ipcRenderer, IpcRenderer, IpcRendererEvent, webUtils } from 'electron' 8 | 9 | //exposing ipcRenderer to the window in renderer process 10 | contextBridge.exposeInMainWorld('ipcRenderer', { 11 | send: ipcRenderer.send, 12 | on: (channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): IpcRenderer => 13 | ipcRenderer.on(channel, (_, ...args) => listener(null, ...args)), 14 | removeAllListeners: (channel: string) => ipcRenderer.removeAllListeners(channel), 15 | invoke: ipcRenderer.invoke, 16 | showFilePath: (file: File) => webUtils.getPathForFile(file) 17 | }) 18 | 19 | contextBridge.exposeInMainWorld('process', { platform: process.platform }) 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": false, 9 | "esModuleInterop": true, 10 | "noImplicitOverride": false, 11 | "noPropertyAccessFromIndexSignature": false, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "sourceMap": true, 15 | "declaration": false, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "resolveJsonModule": true, 20 | "target": "ES2022", 21 | "module": "ES2022", 22 | "lib": [ 23 | "es2022", 24 | "dom" 25 | ], 26 | "useDefineForClassFields": false 27 | }, 28 | "exclude": [ 29 | "node_modules", 30 | "renderer" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /i18n/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export type i18nLocaleSupported = 'de' | 'en' | 'es' | 'fr' | 'hi' | 'it' | 'ja' | 'ko' | 'pl' | 'pt' | 'pt-BR' | 'ru' | 'tr' | 'zh' 8 | export const LANG_SUPPORTED = new Set(['de', 'en', 'es', 'fr', 'hi', 'it', 'ja', 'ko', 'pl', 'pt', 'pt-BR', 'ru', 'tr', 'zh']) 9 | export const LANG_DEFAULT: i18nLocaleSupported = 'en' 10 | 11 | export function normalizeLanguage(language: string): i18nLocaleSupported | null { 12 | if (!language) return null 13 | if (LANG_SUPPORTED.has(language as i18nLocaleSupported)) { 14 | return language as i18nLocaleSupported 15 | } 16 | const code = language.split('-')[0] 17 | return LANG_SUPPORTED.has(code as i18nLocaleSupported) ? (code as i18nLocaleSupported) : null 18 | } 19 | -------------------------------------------------------------------------------- /cli/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Copyright (C) 2012-2025 Johan Legrand 4 | * This file is part of Sync-in | The open source file sync and share solution 5 | * See the LICENSE file for licensing details 6 | */ 7 | 8 | import _yargs from 'yargs/yargs' 9 | import { hideBin } from 'yargs/helpers' 10 | import { getLogger } from '../core/components/handlers/loggers' 11 | import { getCMDS } from './cmds' 12 | 13 | const logger = getLogger('CommandLine') 14 | 15 | const yargs = _yargs(hideBin(process.argv)) 16 | 17 | yargs.command(getCMDS(yargs) as any) 18 | yargs.fail((msg: string, e: Error | string) => { 19 | if (msg) { 20 | yargs.showHelp() 21 | logger.error(msg) 22 | } else if (typeof e === 'string' && e.includes('argument')) { 23 | // hook to show help when handler manages argv options 24 | yargs.showHelp() 25 | logger.error(e) 26 | } else { 27 | logger.error(e) 28 | } 29 | process.exit() 30 | }) 31 | yargs.strict() 32 | yargs.parse() 33 | -------------------------------------------------------------------------------- /main/components/settings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { loadJsonFile, writeToFileSync } from '@sync-in-desktop/core/components/utils/functions' 8 | import { APP_CONF_FILE } from '../constants/settings' 9 | import { AppConfiguration } from '../interfaces/settings.interface' 10 | 11 | class AppSettings { 12 | public configuration: AppConfiguration = { launchAtStartup: true, startHidden: false, hideDockIcon: false } 13 | 14 | constructor() { 15 | this.loadSettings() 16 | } 17 | 18 | writeSettings() { 19 | try { 20 | writeToFileSync(APP_CONF_FILE, this.configuration) 21 | } catch (e) { 22 | console.error(`${APP_CONF_FILE} not written: ${e}`) 23 | } 24 | } 25 | 26 | private loadSettings() { 27 | this.configuration = loadJsonFile(APP_CONF_FILE, this.configuration) 28 | } 29 | } 30 | 31 | export const appSettings = new AppSettings() 32 | -------------------------------------------------------------------------------- /renderer/src/app/components/bottom-bar-syncs-component.html: -------------------------------------------------------------------------------- 1 | 6 | @if (transfer) { 7 |
8 | 9 | 10 | 11 | 12 | {{ transfer.name }} 13 |
14 | @if (transferProgress) { 15 |
16 | 17 |
{{ transferProgress.currentSize }} • {{ transferProgress.totalSize }}
18 |
19 |
20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": false, 9 | "esModuleInterop": true, 10 | "noImplicitOverride": false, 11 | "noPropertyAccessFromIndexSignature": false, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "sourceMap": true, 15 | "declaration": false, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "bundler", 18 | "importHelpers": true, 19 | "resolveJsonModule": true, 20 | "target": "ES2022", 21 | "module": "ES2022", 22 | "lib": [ 23 | "es2022", 24 | "dom" 25 | ], 26 | "useDefineForClassFields": false 27 | }, 28 | "angularCompilerOptions": { 29 | "enableI18nLegacyMessageIdFormat": false, 30 | "strictInjectionParameters": true, 31 | "strictInputAccessModifiers": true, 32 | "strictTemplates": false, 33 | "strictPropertyInitialization": false, 34 | "allowSyntheticDefaultImports": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /renderer/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { HashLocationStrategy, LocationStrategy } from '@angular/common' 8 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core' 9 | import { provideAnimations } from '@angular/platform-browser/animations' 10 | import { provideL10nIntl, provideL10nTranslation } from 'angular-l10n' 11 | import { BsModalService } from 'ngx-bootstrap/modal' 12 | import { l10nConfig, TranslateLocaleResolver, TranslationLoader, TranslationStorage } from '../i18n/l10n' 13 | 14 | export const appConfig: ApplicationConfig = { 15 | providers: [ 16 | provideZoneChangeDetection({ eventCoalescing: true }), 17 | { provide: LocationStrategy, useClass: HashLocationStrategy }, 18 | provideL10nTranslation(l10nConfig, { 19 | localeResolver: TranslateLocaleResolver, 20 | storage: TranslationStorage, 21 | translationLoader: TranslationLoader 22 | }), 23 | provideL10nIntl(), 24 | provideAnimations(), 25 | BsModalService 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /renderer/src/i18n/lib/bs.i18n.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { 8 | defineLocale, 9 | deLocale, 10 | enGbLocale, 11 | esLocale, 12 | frLocale, 13 | hiLocale, 14 | itLocale, 15 | jaLocale, 16 | koLocale, 17 | LocaleData, 18 | plLocale, 19 | ptBrLocale, 20 | ruLocale, 21 | trLocale, 22 | zhCnLocale 23 | } from 'ngx-bootstrap/chronos' 24 | import { i18nLocaleSupported } from '../../../../i18n' 25 | 26 | const BOOTSTRAP_LOCALES: Record = { 27 | de: deLocale, 28 | en: enGbLocale, 29 | es: esLocale, 30 | fr: frLocale, 31 | hi: hiLocale, 32 | it: itLocale, 33 | ja: jaLocale, 34 | ko: koLocale, 35 | pl: plLocale, 36 | pt: ptBrLocale, 37 | 'pt-BR': ptBrLocale, 38 | ru: ruLocale, 39 | tr: trLocale, 40 | zh: zhCnLocale 41 | } 42 | 43 | export function loadBootstrapLocale(language: string): void { 44 | const locale: LocaleData = BOOTSTRAP_LOCALES[language] 45 | if (!locale) return 46 | defineLocale(language.toLowerCase(), locale) 47 | } 48 | -------------------------------------------------------------------------------- /renderer/src/i18n/lib/dayjs.i18n.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { dJs } from '../../app/common/functions/time' 8 | import { i18nLocaleSupported } from '../../../../i18n' 9 | 10 | const DAYJS_LOADER: Record Promise> = { 11 | de: () => import('dayjs/esm/locale/de'), 12 | en: () => import('dayjs/esm/locale/en'), 13 | es: () => import('dayjs/esm/locale/es'), 14 | fr: () => import('dayjs/esm/locale/fr'), 15 | hi: () => import('dayjs/esm/locale/hi'), 16 | it: () => import('dayjs/esm/locale/it'), 17 | ja: () => import('dayjs/esm/locale/ja'), 18 | ko: () => import('dayjs/esm/locale/ko'), 19 | pl: () => import('dayjs/esm/locale/pl'), 20 | pt: () => import('dayjs/esm/locale/pt'), 21 | 'pt-BR': () => import('dayjs/esm/locale/pt-br'), 22 | ru: () => import('dayjs/esm/locale/ru'), 23 | tr: () => import('dayjs/esm/locale/tr'), 24 | zh: () => import('dayjs/esm/locale/zh') 25 | } 26 | 27 | export async function loadDayjsLocale(language: string) { 28 | const loader = DAYJS_LOADER[language] 29 | if (!loader) return 30 | await loader() 31 | dJs.locale(language) 32 | } 33 | -------------------------------------------------------------------------------- /main/components/translate.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import fs from 'node:fs' 8 | import path from 'node:path' 9 | import { i18nLocaleSupported, LANG_DEFAULT, normalizeLanguage } from '../../i18n' 10 | 11 | class i18nManager { 12 | i18nPath = path.join(__dirname, 'i18n') 13 | language: i18nLocaleSupported = LANG_DEFAULT 14 | dictionary: any = {} 15 | 16 | constructor() { 17 | this.load() 18 | } 19 | 20 | tr(phrase: string) { 21 | const translation = this.dictionary[phrase] 22 | return translation === undefined ? phrase : translation 23 | } 24 | 25 | updateLanguage(language: string) { 26 | language = normalizeLanguage(language) || LANG_DEFAULT 27 | if (this.language !== language) { 28 | this.language = language as i18nLocaleSupported 29 | this.load() 30 | } 31 | } 32 | 33 | private load() { 34 | const lang = fs.existsSync(path.join(this.i18nPath, `${this.language}.json`)) ? `${this.language}.json` : `${LANG_DEFAULT}.json` 35 | this.dictionary = JSON.parse(fs.readFileSync(path.join(this.i18nPath, lang), 'utf8')) 36 | } 37 | } 38 | 39 | export const i18n: i18nManager = new i18nManager() 40 | -------------------------------------------------------------------------------- /core/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import path from 'node:path' 8 | import { SYNC_CLIENT_TYPE } from './components/constants/auth' 9 | import packageJson from '../package.json' 10 | 11 | export const ENVIRONMENT = { 12 | appVersion: packageJson.version, 13 | appName: process.versions.electron ? SYNC_CLIENT_TYPE.DESKTOP : SYNC_CLIENT_TYPE.CLI, 14 | appID: packageJson.productName || null, 15 | appHomePage: packageJson.homepage, 16 | appReleasesPage: packageJson.releasespage 17 | } 18 | export const USER_AGENT = `${ENVIRONMENT.appName}/${ENVIRONMENT.appVersion}` 19 | export const IS_MACOS = process.platform === 'darwin' 20 | export const IS_WINDOWS = process.platform === 'win32' 21 | export const IS_PROD_ENV = process.env.NODE_ENV === 'production' 22 | export const IS_DEV_ENV = process.env.NODE_ENV === 'development' 23 | export const HAS_TTY = (IS_WINDOWS && IS_DEV_ENV) || Boolean(process.stdout.isTTY) 24 | export const CONF_PATH = path.join(...(IS_WINDOWS ? [process.env['APPDATA'], 'Sync-in-Profile'] : [process.env['HOME'], '.sync-in'])) 25 | export const SNAPSHOTS_PATH = path.join(CONF_PATH, 'snapshots') 26 | export const SERVERS_SETTINGS = path.join(CONF_PATH, 'servers.json') 27 | export const MAIN_LOGS_FILE = path.join(CONF_PATH, 'sync.log') 28 | export const SYNC_LOGS_PATH = path.join(CONF_PATH, 'logs') 29 | -------------------------------------------------------------------------------- /core/components/utils/normalizedMap.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | export class NormalizedMap extends Map { 8 | // NFC-normalized path → actual key 9 | private index = new Map() 10 | 11 | constructor(entries?: readonly (readonly [K, V])[] | null) { 12 | super() 13 | if (entries) { 14 | for (const [k, v] of entries) { 15 | this.set(k, v) 16 | } 17 | } 18 | } 19 | 20 | private normalizeKey(key: string): string { 21 | return key.normalize('NFC') 22 | } 23 | 24 | override set(key: K, value: V): this { 25 | this.index.set(this.normalizeKey(key), key) // store the "real" key used 26 | return super.set(key, value) 27 | } 28 | 29 | getResolvedKey(input: string): K | undefined { 30 | return this.index.get(this.normalizeKey(input)) 31 | } 32 | 33 | override get(key: string): V | undefined { 34 | const resolved = this.getResolvedKey(key) 35 | return resolved ? super.get(resolved) : undefined 36 | } 37 | 38 | override has(key: string): boolean { 39 | return this.index.has(this.normalizeKey(key)) 40 | } 41 | 42 | override delete(key: string): boolean { 43 | const resolved = this.getResolvedKey(key) 44 | if (resolved) { 45 | this.index.delete(this.normalizeKey(resolved)) 46 | return super.delete(resolved) 47 | } 48 | return false 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /renderer/src/app/common/icons.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { 8 | faArrowDown, 9 | faArrowRotateRight, 10 | faArrowsRotate, 11 | faArrowUp, 12 | faChevronDown, 13 | faCircle as fasCircle, 14 | faCircleExclamation, 15 | faCopy, 16 | faDownload, 17 | faEllipsisVertical, 18 | faFolderOpen, 19 | faGlobe, 20 | faKey, 21 | faLock, 22 | faPause, 23 | faPencil, 24 | faPlay, 25 | faPlus, 26 | faQrcode, 27 | faRss, 28 | faServer, 29 | faStop, 30 | faSyncAlt, 31 | faTrashAlt, 32 | faTriangleExclamation, 33 | faUpDownLeftRight, 34 | faUserAlt, 35 | faXmark 36 | } from '@fortawesome/free-solid-svg-icons' 37 | import { faBell, faCircle as farCircle, faComments, faFlag } from '@fortawesome/free-regular-svg-icons' 38 | 39 | export const faIcons = { 40 | fasCircle, 41 | farCircle, 42 | faDownload, 43 | faPause, 44 | faStop, 45 | faFolderOpen, 46 | faPlay, 47 | faTrashAlt, 48 | faArrowUp, 49 | faArrowDown, 50 | faXmark, 51 | faCopy, 52 | faUpDownLeftRight, 53 | faCircleExclamation, 54 | faArrowsRotate, 55 | faEllipsisVertical, 56 | faServer, 57 | faChevronDown, 58 | faArrowRotateRight, 59 | faPlus, 60 | faPencil, 61 | faRss, 62 | faComments, 63 | faBell, 64 | faFlag, 65 | faTriangleExclamation, 66 | faSyncAlt, 67 | faGlobe, 68 | faUserAlt, 69 | faLock, 70 | faKey, 71 | faQrcode 72 | } 73 | -------------------------------------------------------------------------------- /cli/cmds/cmd-run.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { setLevelLogger } from '../../core/components/handlers/loggers' 8 | import { CommandModule } from 'yargs' 9 | import { RunManager } from '../../core/main' 10 | 11 | const runCMD: CommandModule = { 12 | command: 'run', 13 | describe: 'Synchronize (help for options)', 14 | builder: { 15 | report: { 16 | alias: 'r', 17 | describe: 'Only report (dry run)', 18 | default: false, 19 | boolean: true, 20 | type: 'boolean' 21 | }, 22 | debug: { 23 | alias: 'd', 24 | describe: 'Enable debug mode', 25 | default: false, 26 | boolean: true, 27 | type: 'boolean' 28 | }, 29 | async: { 30 | alias: 'a', 31 | describe: 'Run syncs in parallel', 32 | default: false, 33 | boolean: true, 34 | type: 'boolean' 35 | }, 36 | server: { 37 | alias: 's', 38 | describe: 'Given id or name to sync the target server' 39 | }, 40 | path: { 41 | alias: 'p', 42 | array: true, 43 | describe: 'Given id or name to sync the target path, "server" must be specified' 44 | } 45 | }, 46 | handler: async (argv: any) => { 47 | try { 48 | if (argv.debug) { 49 | setLevelLogger('debug') 50 | } 51 | await new RunManager().run({ server: argv.server, paths: argv.path }, argv.report, argv.async) 52 | } catch (e) { 53 | console.error(e) 54 | } 55 | } 56 | } 57 | 58 | export function getRunCMD() { 59 | return runCMD 60 | } 61 | -------------------------------------------------------------------------------- /renderer/src/app/components/top-bar-buttons.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { Component, inject } from '@angular/core' 8 | import { AppService } from '../app.service' 9 | import { LOCAL_RENDERER } from '../../../../main/constants/events' 10 | 11 | @Component({ 12 | selector: 'app-top-bar-buttons', 13 | template: `@if (!appService.isMacOS) { 14 |
15 |
16 |
17 | @if (appService.isMaximized() || appService.isFullScreen()) { 18 |
19 | } @else { 20 |
21 | } 22 |
23 |
24 |
25 | }`, 26 | standalone: true 27 | }) 28 | export class TopBarButtonsComponent { 29 | protected readonly appService = inject(AppService) 30 | 31 | onMinimize() { 32 | this.appService.ipcRenderer.send(LOCAL_RENDERER.WINDOW.MINIMIZE) 33 | } 34 | 35 | onMaximize() { 36 | this.appService.ipcRenderer.send(LOCAL_RENDERER.WINDOW.MAXIMIZE) 37 | } 38 | 39 | onUnMaximize() { 40 | this.appService.ipcRenderer.send(LOCAL_RENDERER.WINDOW.UNMAXIMIZE) 41 | } 42 | 43 | onClose() { 44 | this.appService.ipcRenderer.send(LOCAL_RENDERER.WINDOW.CLOSE) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | * Sync-in | The open source file sync and share solution 3 | * Copyright (C) 2024-2025 Johan Legrand 4 | * This program is licensed under the Affero General Public License (AGPL) version 3. 5 | * See the LICENSE file or for more details. 6 | */ 7 | 8 | // @ts-check 9 | import eslint from '@eslint/js' 10 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' 11 | import globals from 'globals' 12 | import tseslint from 'typescript-eslint' 13 | 14 | export default tseslint.config( 15 | { 16 | ignores: ['eslint.config.mjs'] 17 | }, 18 | eslint.configs.recommended, 19 | tseslint.configs.recommended, 20 | tseslint.configs.stylistic, 21 | eslintPluginPrettierRecommended, 22 | //tseslint.configs.recommendedTypeChecked, 23 | { 24 | languageOptions: { 25 | globals: globals.node, 26 | ecmaVersion: 'latest', 27 | sourceType: 'module' 28 | }, 29 | rules: { 30 | 'prefer-const': ['error', { destructuring: 'all' }], 31 | '@typescript-eslint/interface-name-prefix': 'off', 32 | '@typescript-eslint/explicit-function-return-type': 'off', 33 | '@typescript-eslint/explicit-module-boundary-types': 'off', 34 | '@typescript-eslint/no-explicit-any': 'off', 35 | '@typescript-eslint/no-inferrable-types': ['error', { ignoreParameters: true, ignoreProperties: true }], 36 | '@typescript-eslint/no-unused-vars': [ 37 | 'error', 38 | { 39 | args: 'all', 40 | argsIgnorePattern: '^_', 41 | caughtErrors: 'all', 42 | caughtErrorsIgnorePattern: '^_', 43 | destructuredArrayIgnorePattern: '^_', 44 | varsIgnorePattern: '^_', 45 | ignoreRestSiblings: true 46 | } 47 | ] 48 | } 49 | } 50 | ) -------------------------------------------------------------------------------- /renderer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-in-desktop/renderer", 3 | "description": "Desktop Application Renderer", 4 | "license": "AGPL-3.0-or-later", 5 | "private": true, 6 | "author": { 7 | "name": "Johan Legrand", 8 | "email": "johan.legrand@sync-in.com" 9 | }, 10 | "homepage": "https://sync-in.com", 11 | "scripts": { 12 | "ng": "ng", 13 | "angular:prod": "ng build renderer", 14 | "angular:dev": "ng build renderer --configuration development", 15 | "angular:update": "ng update @angular/cli @angular/core --allow-dirty --force", 16 | "lint": "ng lint", 17 | "lint:fix": "ng lint --fix" 18 | }, 19 | "dependencies": { 20 | "@angular/animations": "^20.2.4", 21 | "@angular/common": "^20.2.4", 22 | "@angular/compiler": "^20.2.4", 23 | "@angular/core": "^20.2.4", 24 | "@angular/forms": "^20.2.4", 25 | "@angular/platform-browser": "^20.2.4", 26 | "@angular/platform-browser-dynamic": "^20.2.4", 27 | "@fortawesome/angular-fontawesome": "^3.0.0", 28 | "@fortawesome/fontawesome-svg-core": "^7.0.1", 29 | "@fortawesome/free-regular-svg-icons": "^7.0.1", 30 | "@fortawesome/free-solid-svg-icons": "^7.0.1", 31 | "angular-l10n": "^17.0.0", 32 | "dayjs": "^1.11.13", 33 | "ngx-bootstrap": "^20.0.1", 34 | "rxjs": "^7.8.2", 35 | "tslib": "^2.8.1", 36 | "zone.js": "^0.15.0" 37 | }, 38 | "devDependencies": { 39 | "@angular/build": "^20.2.2", 40 | "@angular/cli": "^20.2.2", 41 | "@angular/router": "^20.2.4", 42 | "@angular/compiler-cli": "^20.2.4", 43 | "@eslint/js": "^9.18.0", 44 | "angular-eslint": "^20.2.0", 45 | "bootstrap": "^5.3.5", 46 | "eslint": "^9.21.0", 47 | "eslint-config-prettier": "^10.1.1", 48 | "eslint-plugin-prettier": "^5.2.3", 49 | "typescript": "^5.9.2", 50 | "typescript-eslint": "^8.26.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /main/constants/windows.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import path from 'node:path' 8 | import { BrowserWindowConstructorOptions, nativeTheme, WebContentsViewConstructorOptions } from 'electron' 9 | import { THEME } from './themes' 10 | 11 | // Renderer constants 12 | const DEFAULT_WINDOW_WIDTH = 1128 13 | const DEFAULT_WINDOW_HEIGHT = 650 14 | const MINIMUM_WINDOW_WIDTH = 700 15 | const MINIMUM_WINDOW_HEIGHT = 550 16 | 17 | export const THEMES = { [THEME.LIGHT]: '#0b2640', [THEME.DARK]: '#222d32' } 18 | 19 | export const TAB_BAR_HEIGHT = 40 20 | export const WRAPPER_VIEW_OFFSET_HEIGHT = 80 21 | export const RENDERER_FILE = './dist/renderer/index.html' 22 | const PRELOAD_FILE = path.join(__dirname, 'preload.js') 23 | 24 | export const defaultWindowProps: BrowserWindowConstructorOptions = { 25 | width: DEFAULT_WINDOW_WIDTH, 26 | height: DEFAULT_WINDOW_HEIGHT, 27 | minWidth: MINIMUM_WINDOW_WIDTH, 28 | minHeight: MINIMUM_WINDOW_HEIGHT, 29 | useContentSize: true, 30 | show: false, // don't start the window until it is ready and only if it isn't hidden 31 | paintWhenInitiallyHidden: true, // we want it to start painting to get info from the webapp 32 | frame: false, 33 | titleBarStyle: 'hidden', 34 | trafficLightPosition: { x: 12, y: 12 }, 35 | backgroundColor: THEMES[nativeTheme.shouldUseDarkColors ? THEME.DARK : THEME.LIGHT] 36 | } 37 | 38 | export const defaultViewProps: WebContentsViewConstructorOptions = { 39 | webPreferences: { 40 | // Disabled Node integration 41 | nodeIntegration: false, 42 | // protect against prototype pollution 43 | contextIsolation: true, 44 | preload: PRELOAD_FILE, 45 | transparent: true 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core/components/handlers/settings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import fs from 'node:fs' 8 | import { getLogger } from './loggers' 9 | import { isPathExists, loadJsonFile, writeToFileSync } from '../utils/functions' 10 | import { Server } from '../models/server' 11 | import { CONF_PATH, SERVERS_SETTINGS, SNAPSHOTS_PATH, SYNC_LOGS_PATH } from '../../constants' 12 | import { CORE, coreEvents } from './events' 13 | 14 | const logger = getLogger('Settings') 15 | 16 | class SettingsManager { 17 | public servers: Server[] = [] 18 | 19 | constructor() { 20 | SettingsManager.checkDirectories() 21 | this.loadSettings() 22 | this.listeners() 23 | } 24 | 25 | private static checkDirectories() { 26 | for (const path of [CONF_PATH, SNAPSHOTS_PATH, SYNC_LOGS_PATH]) { 27 | isPathExists(path).catch((e) => { 28 | logger.warn(e) 29 | fs.mkdirSync(path) 30 | logger.debug(`${path} created`) 31 | }) 32 | } 33 | } 34 | 35 | writeServersSettings(reloadConf = false, exit = false) { 36 | try { 37 | writeToFileSync( 38 | SERVERS_SETTINGS, 39 | this.servers.map((s: Server) => s.export()) 40 | ) 41 | logger.debug(`${SERVERS_SETTINGS} saved (reload: ${reloadConf}, exited: ${exit})`) 42 | } catch (e) { 43 | logger.error(e) 44 | throw e 45 | } 46 | } 47 | 48 | private listeners() { 49 | coreEvents.on(CORE.SAVE_SETTINGS, (reloadConf?: boolean, exit?: boolean) => this.writeServersSettings(reloadConf, exit)) 50 | } 51 | 52 | private loadSettings() { 53 | this.servers = loadJsonFile(SERVERS_SETTINGS, []).map((data: Partial) => new Server(data)) 54 | } 55 | } 56 | 57 | export const settingsManager: SettingsManager = new SettingsManager() 58 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | ## 1. Our Commitment 4 | We are committed to creating a **welcoming, inclusive, and respectful** environment for everyone, regardless of background, experience, or identity. All contributors, maintainers, and users are expected to follow this Code of Conduct. 5 | 6 | ## 2. Expected Behavior 7 | To foster a positive and productive community, contributors are expected to: 8 | - Be **respectful and considerate** in all interactions. 9 | - Give **constructive feedback** and engage in **professional discussions**. 10 | - Respect **differing viewpoints** and experiences. 11 | - Accept **graciously given feedback** and work towards improvement. 12 | - Show **kindness and encouragement** to others in the community. 13 | 14 | ## 3. Unacceptable Behavior 15 | The following behavior will **not be tolerated**: 16 | - **Harassment, intimidation, or discrimination** in any form. 17 | - **Offensive, derogatory, or inappropriate** language or imagery. 18 | - **Personal attacks, trolling, or excessive criticism**. 19 | - **Deliberate disruption** of discussions, issues, or contributions. 20 | - **Publishing private information** (such as personal addresses) without permission. 21 | 22 | ## 4. Reporting Violations 23 | If you witness or experience any violations of this Code of Conduct: 24 | - Contact the project maintainers at **[Your Contact Email]**. 25 | - Provide details about the incident, including where and when it occurred. 26 | - Reports will be handled **confidentially and fairly**. 27 | 28 | ## 5. Enforcement 29 | Violations of this Code of Conduct may result in: 30 | - **A private warning** from the maintainers. 31 | - **Temporary or permanent suspension** from contributing. 32 | - **Removal from the community** in severe cases. 33 | 34 | ## 6. Acknowledgment 35 | This Code of Conduct is inspired by the [Contributor Covenant](https://www.contributor-covenant.org/), a widely adopted code for open-source communities. 36 | 37 | --- 38 | 39 | Thank you for being part of a **respectful and welcoming** community! 🚀 -------------------------------------------------------------------------------- /renderer/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 6 |
7 | 8 | @if (!activeServer.id || !activeServer.available || activeServer.authTokenExpired) { 9 |
10 | @if (!activeServer.available) { 11 |
12 |
13 |
14 | @if (isRetrying) { 15 | Trying to connect to the server... 16 | } @else { 17 | Unable to connect to this server ! 18 | } 19 |
20 | 24 |
25 |
26 | } @else if (activeServer.authTokenExpired) { 27 |
28 |
29 |
30 | For security reasons, please log in again 31 |
32 | 36 |
37 |
38 | } 39 |
40 | } 41 | 42 |
43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide for Sync-in 2 | 3 | Welcome to **Sync-in**! 4 | 5 | We appreciate your contributions and efforts in making this project better. 6 | 7 | Before you contribute, please read the following guidelines to ensure a smooth collaboration. 8 | 9 | ## 1. Contributor License Agreement (CLA) 10 | 11 | 🛡️ **No CLA Required to Use the Project** 12 | You’re free to use, modify, fork, and redistribute Sync-in under the AGPL v3 license — no agreement needed for that. 13 | 14 | ✍️ **CLA Required to Contribute** 15 | If you wish to contribute (e.g., by opening a pull request), you must agree to our [Contributor License Agreement (CLA)](CLA.md). 16 | 17 | The CLA ensures that: 18 | - Your contribution remains open source 19 | - The project can also be offered under a commercial license 20 | - And you won’t need to be contacted again for permission in the future 21 | 22 | If you do not agree with its terms, please refrain from contributing code. 23 | 24 | --- 25 | 26 | ## 2. How to Contribute 27 | 28 | ### 🛠 Process 29 | - Understand the project’s goals, technical direction, and contribution practices. 30 | - Make sure your work fits with the existing architecture, code style, and documentation standards. 31 | - All contributions must comply with the [CLA](CLA.md). 32 | 33 | ### 🧪 Code Quality 34 | - Write clean, maintainable, and documented code. 35 | - Keep documentation in sync with your changes. 36 | - Add or update tests as needed to preserve stability. 37 | 38 | --- 39 | 40 | ## 3. Reporting Issues & Suggesting Features 41 | 42 | ### 🐛 Bugs 43 | - Check existing issues before opening a new one. 44 | - Include detailed steps, logs, and screenshots when possible. 45 | 46 | ### 🌟 Feature Requests 47 | - Describe the problem the feature solves. 48 | - Provide use cases and, if possible, implementation suggestions. 49 | 50 | --- 51 | 52 | ## 4. Code of Conduct 53 | 54 | All contributors must follow our [Code of Conduct](CODE_OF_CONDUCT.md) to ensure a friendly and inclusive environment. 55 | 56 | --- 57 | 58 | _Thanks again for helping make **Sync-in** better! 🚀_ -------------------------------------------------------------------------------- /renderer/src/scss/components/_modal.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | /* 8 | * Component: modal 9 | * ---------------- 10 | */ 11 | .modal-lg { 12 | max-width: 50% !important; 13 | } 14 | 15 | .modal-header { 16 | @include border-top-radius(4px); 17 | line-height: 1; 18 | } 19 | 20 | .modal-title { 21 | @extend .truncate; 22 | font-size: $font-size-sm; 23 | line-height: 2; 24 | i { 25 | margin-right: 5px; 26 | } 27 | } 28 | 29 | .modal-body { 30 | font-size: $font-size-xs; 31 | padding: 8px 12px; 32 | background-color: $body-bg; 33 | } 34 | 35 | .modal-full { 36 | max-width: 100%; 37 | &.modal-dialog { 38 | margin: 0; 39 | } 40 | .modal-content { 41 | border: none; 42 | .modal-header { 43 | border: none; 44 | } 45 | .modal-body { 46 | padding: 0; 47 | } 48 | } 49 | } 50 | 51 | //Modal variants 52 | .modal-primary { 53 | .modal-header { 54 | @extend .bg-primary; 55 | color: white; 56 | } 57 | .modal-footer { 58 | background-color: $gray-lightest; 59 | } 60 | } 61 | 62 | .modal-warning { 63 | .modal-header { 64 | @extend .bg-warning; 65 | color: white; 66 | } 67 | .modal-footer { 68 | background-color: $gray-lightest; 69 | } 70 | } 71 | 72 | .modal-info { 73 | .modal-header { 74 | @extend .bg-info; 75 | color: white; 76 | } 77 | .modal-footer { 78 | background-color: $gray-lightest; 79 | } 80 | } 81 | 82 | .modal-success { 83 | .modal-header { 84 | @extend .bg-success; 85 | color: white; 86 | } 87 | .modal-footer { 88 | background-color: $gray-lightest; 89 | } 90 | } 91 | 92 | .modal-danger { 93 | .modal-header { 94 | @extend .bg-danger; 95 | color: white; 96 | } 97 | .modal-footer { 98 | background-color: $gray-lightest; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /renderer/eslint.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | // @ts-check 8 | const eslint = require('@eslint/js') 9 | const tseslint = require('typescript-eslint') 10 | const angular = require('angular-eslint') 11 | const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended') 12 | 13 | module.exports = tseslint.config( 14 | { 15 | files: ['**/*.ts'], 16 | extends: [ 17 | eslint.configs.recommended, 18 | ...tseslint.configs.recommended, 19 | ...tseslint.configs.stylistic, 20 | ...angular.configs.tsRecommended, 21 | eslintPluginPrettierRecommended 22 | ], 23 | processor: angular.processInlineTemplates, 24 | rules: { 25 | '@angular-eslint/directive-selector': [ 26 | 'error', 27 | { 28 | type: 'attribute', 29 | prefix: 'app', 30 | style: 'camelCase' 31 | } 32 | ], 33 | '@angular-eslint/component-selector': [ 34 | 'error', 35 | { 36 | type: 'element', 37 | prefix: 'app', 38 | style: 'kebab-case' 39 | } 40 | ], 41 | '@typescript-eslint/no-explicit-any': 'off', 42 | '@typescript-eslint/no-inferrable-types': ['error', { ignoreParameters: true, ignoreProperties: false }], 43 | '@typescript-eslint/no-unused-vars': [ 44 | 'error', 45 | { 46 | args: 'all', 47 | argsIgnorePattern: '^_', 48 | caughtErrors: 'all', 49 | caughtErrorsIgnorePattern: '^_', 50 | destructuredArrayIgnorePattern: '^_', 51 | varsIgnorePattern: '^_', 52 | ignoreRestSiblings: true 53 | } 54 | ] 55 | } 56 | }, 57 | { 58 | files: ['**/*.html'], 59 | extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility], 60 | rules: { 61 | '@angular-eslint/template/click-events-have-key-events': 'off', 62 | '@angular-eslint/template/interactive-supports-focus': 'off' 63 | } 64 | } 65 | ) 66 | -------------------------------------------------------------------------------- /core/components/constants/handlers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { IS_WINDOWS } from '../../constants' 8 | 9 | export enum SIDE { 10 | LOCAL = 'local', 11 | REMOTE = 'remote', 12 | BOTH = 'both' 13 | } 14 | 15 | export const INVERSE_SIDE = { [SIDE.LOCAL]: SIDE.REMOTE, [SIDE.REMOTE]: SIDE.LOCAL } 16 | 17 | // symbols 18 | export const SYMBOLS = IS_WINDOWS 19 | ? { [SIDE.LOCAL]: '↓', [SIDE.REMOTE]: '↑', [SIDE.BOTH]: '↓↑' } 20 | : { [SIDE.LOCAL]: '⬇', [SIDE.REMOTE]: '⬆', [SIDE.BOTH]: '⬇⬆' } 21 | 22 | // data index of the value array of a file 23 | export enum F_STAT { 24 | IS_DIR = 0, 25 | SIZE = 1, 26 | MTIME = 2, 27 | INO = 3, 28 | CHECKSUM = 4, 29 | INCOMPLETE_SIZE = 5 30 | } 31 | 32 | export enum F_SPECIAL_STAT { 33 | FILTERED = 'filtered', 34 | ERROR = 'error' 35 | } 36 | 37 | // actions on files 38 | export enum F_ACTION { 39 | NEW = 'NEW', 40 | DIFF = 'DIFF', 41 | RM = 'RM', 42 | RMDIR = 'RMDIR', 43 | MOVE = 'MOVE', 44 | COPY = 'COPY', 45 | MKDIR = 'MKDIR', 46 | MKFILE = 'MKFILE', 47 | PROPS = 'PROPS', 48 | FILTERED = 'FILTERED' 49 | } 50 | 51 | export const SYNC_CHECKSUM_ALG = 'sha512-256' 52 | export const DEFAULT_HIGH_WATER_MARK = 512 * 1024 53 | // min size to track progress and put the task in slow queue 54 | export const TRANSFER_MIN_SIZE = 10 * 1024 ** 2 // 10MB 55 | // parsers filters 56 | export const SYNC_DIFF_DONE = 'done' 57 | export const INCOMPLETE_PREFIX = '.sync-in.' 58 | export const INCOMPLETE_REGEXP = new RegExp(`^${INCOMPLETE_PREFIX}`) 59 | export const INCOMPLETE_RETENTION = 172800 // 48 hours in seconds 60 | export const DEFAULT_FILTERS = new Set([ 61 | '.DS_Store', 62 | '.swp', 63 | '.AppleDouble', 64 | '.AppleDesktop', 65 | 'Thumbs.db', 66 | '.Spotlight-V100', 67 | '.DocumentRevisions-V100', 68 | '.fseventsd', 69 | '.MobileBackups', 70 | 'Icon?', 71 | '__MACOSX', 72 | '.thumbnails', 73 | '.DAV', 74 | '.desktop', 75 | 'desktop.ini', 76 | '.TemporaryItems', 77 | '.localized', 78 | '__pycache__' 79 | ]) 80 | -------------------------------------------------------------------------------- /core/components/constants/diff.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { SIDE } from './handlers' 8 | 9 | export enum SYNC_MODE { 10 | DOWNLOAD = 'download', 11 | UPLOAD = 'upload', 12 | BOTH = 'both' 13 | } 14 | 15 | export enum CONFLICT_MODE { 16 | RECENT = 'recent', 17 | LOCAL = 'local', 18 | REMOTE = 'remote' 19 | } 20 | 21 | export enum DIFF_MODE { 22 | FAST = 'fast', 23 | SECURE = 'secure' 24 | } 25 | 26 | export enum SIDE_STATE { 27 | UPLOAD = 'upload', 28 | UPLOAD_DIFF = 'uploadDiff', 29 | DOWNLOAD = 'download', 30 | DOWNLOAD_DIFF = 'downloadDiff', 31 | LOCAL_MOVE = 'localMove', 32 | REMOTE_MOVE = 'remoteMove', 33 | LOCAL_MK = 'localMk', 34 | REMOTE_MK = 'remoteMk', 35 | LOCAL_COPY = 'localCopy', 36 | REMOTE_COPY = 'remoteCopy', 37 | LOCAL_RM = 'localRemove', 38 | REMOTE_RM = 'remoteRemove', 39 | LOCAL_PROPS = 'localProperties', 40 | REMOTE_PROPS = 'remoteProperties' 41 | } 42 | 43 | export const DOWNLOAD_MODE = { 44 | added: SIDE_STATE.LOCAL_RM, 45 | changed: SIDE_STATE.DOWNLOAD_DIFF, 46 | removed: SIDE_STATE.DOWNLOAD, 47 | properties: SIDE_STATE.LOCAL_PROPS 48 | } as const 49 | 50 | export const UPLOAD_MODE = { 51 | added: SIDE_STATE.UPLOAD, 52 | changed: SIDE_STATE.UPLOAD_DIFF, 53 | removed: SIDE_STATE.REMOTE_RM, 54 | properties: SIDE_STATE.REMOTE_PROPS 55 | } as const 56 | 57 | export const BOTH_MODE = { 58 | localAdded: SIDE_STATE.UPLOAD, 59 | remoteAdded: SIDE_STATE.DOWNLOAD, 60 | localChanged: SIDE_STATE.UPLOAD_DIFF, 61 | remoteChanged: SIDE_STATE.DOWNLOAD_DIFF, 62 | localRemoved: SIDE_STATE.REMOTE_RM, 63 | remoteRemoved: SIDE_STATE.LOCAL_RM, 64 | localProperties: SIDE_STATE.REMOTE_PROPS, 65 | remoteProperties: SIDE_STATE.LOCAL_PROPS 66 | } as const 67 | 68 | export const ALL_MODES: Record = { 69 | [SIDE.LOCAL]: [...Object.values(DOWNLOAD_MODE), SIDE_STATE.LOCAL_MOVE, SIDE_STATE.LOCAL_MK, SIDE_STATE.LOCAL_COPY], 70 | [SIDE.REMOTE]: [...Object.values(UPLOAD_MODE), SIDE_STATE.REMOTE_MOVE, SIDE_STATE.REMOTE_MK, SIDE_STATE.REMOTE_COPY] 71 | } 72 | -------------------------------------------------------------------------------- /core/components/models/server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { SyncPath } from './syncpath' 8 | import type { SyncServer } from '../interfaces/server.interface' 9 | import { SERVER_SCHEDULER_STATE } from '../constants/server' 10 | 11 | export class Server implements SyncServer { 12 | public id: number = 0 13 | public name: string 14 | public url: string 15 | public available: boolean 16 | public authID: string 17 | public authToken: string 18 | public authTokenExpired: boolean 19 | public syncScheduler: SERVER_SCHEDULER_STATE = SERVER_SCHEDULER_STATE.ASYNC 20 | public syncPaths: SyncPath[] = [] 21 | // tokens 22 | public accessToken: string = null 23 | public refreshToken: string = null 24 | public refreshTokenExpiration = 0 25 | 26 | constructor(data: any) { 27 | this.id = data.id || this.id 28 | this.name = data.name 29 | this.url = data.url?.trim().replace(/\/+$/, '').toLowerCase() 30 | this.available = data.available || false 31 | this.authID = data.authID || undefined 32 | this.authToken = data.authToken || undefined 33 | this.authTokenExpired = data.authTokenExpired || false 34 | this.syncScheduler = data.syncScheduler || this.syncScheduler 35 | if (data.syncPaths) { 36 | this.syncPaths = data.syncPaths.map((data: Partial) => new SyncPath(data)) 37 | } 38 | } 39 | 40 | toString() { 41 | return { id: this.id, name: this.name, url: this.url } 42 | } 43 | 44 | identity() { 45 | return { id: this.id, name: this.name } 46 | } 47 | 48 | export() { 49 | return { 50 | id: this.id, 51 | name: this.name, 52 | url: this.url, 53 | available: this.available, 54 | authID: this.authID, 55 | authToken: this.authToken, 56 | authTokenExpired: this.authTokenExpired, 57 | syncScheduler: this.syncScheduler, 58 | syncPaths: this.syncPaths 59 | } 60 | } 61 | 62 | printName() { 63 | return `${'*'.repeat(5)} Server: ${this.name} (${this.id}) ${this.url} ${'*'.repeat(5)}` 64 | } 65 | 66 | addSync(sync: SyncPath) { 67 | if (this.syncPaths.map((s) => s.id).indexOf(sync.id) > -1) { 68 | throw 'Sync already exists' 69 | } 70 | this.syncPaths.push(sync) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /renderer/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { Component, inject, OnInit } from '@angular/core' 8 | import { AppService } from './app.service' 9 | import { LOCAL_RENDERER } from '../../../main/constants/events' 10 | import { L10nTranslateDirective } from 'angular-l10n' 11 | import { FaIconComponent } from '@fortawesome/angular-fontawesome' 12 | import { TopBarComponent } from './components/top-bar.component' 13 | import { BottomBarComponent } from './components/bottom-bar-component' 14 | import { faIcons } from './common/icons' 15 | import type { SyncServer } from '@sync-in-desktop/core/components/interfaces/server.interface' 16 | import { ModalServerComponent } from './components/modal-server.component' 17 | import { SERVER_ACTION } from '@sync-in-desktop/core/components/constants/server' 18 | 19 | @Component({ 20 | selector: 'app-root', 21 | imports: [TopBarComponent, BottomBarComponent, L10nTranslateDirective, FaIconComponent], 22 | templateUrl: './app.component.html', 23 | standalone: true 24 | }) 25 | export class AppComponent implements OnInit { 26 | public activeServer = null 27 | public isRetrying = false 28 | protected readonly appService = inject(AppService) 29 | protected icons = faIcons 30 | 31 | constructor() { 32 | this.appService.activeServer.subscribe((server: SyncServer) => this.setActiveServer(server)) 33 | } 34 | 35 | ngOnInit() { 36 | this.appService.updateServers() 37 | } 38 | 39 | retryLoad() { 40 | if (this.isRetrying) { 41 | return 42 | } 43 | this.isRetrying = true 44 | this.appService.ipcRenderer.invoke(LOCAL_RENDERER.SERVER.RETRY, this.activeServer.id).then((state: boolean) => { 45 | this.retryServer(state) 46 | }) 47 | } 48 | 49 | reAuthenticateOnServer() { 50 | this.appService.openDialog(ModalServerComponent, { 51 | initialState: { config: { type: SERVER_ACTION.AUTHENTICATE, server: this.activeServer } } as ModalServerComponent 52 | }) 53 | } 54 | 55 | private retryServer(state: boolean) { 56 | if (this.isRetrying) { 57 | this.isRetrying = false 58 | this.activeServer.available = state 59 | } 60 | } 61 | 62 | private setActiveServer(server: SyncServer) { 63 | this.activeServer = server 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /renderer/src/scss/components/_custom.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | body { 7 | -webkit-font-smoothing: antialiased; 8 | margin: 0; 9 | background-color: transparent; 10 | height: 100%; 11 | 12 | .app-main { 13 | &.theme-dark { 14 | @include app-theme($dark-theme-bg-color, $dark-theme-border-color, $dark-theme-font-color, $dark-theme-dropdown-bg-color, 15 | $dark-theme-dropdown-item-color); 16 | } 17 | 18 | &.theme-light { 19 | @include app-theme($light-theme-bg-color, $light-theme-border-color, $light-theme-font-color, $light-theme-dropdown-bg-color, 20 | $light-theme-dropdown-item-color); 21 | } 22 | } 23 | } 24 | 25 | .truncate { 26 | white-space: nowrap; 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | } 30 | 31 | .btn-dark { 32 | background-color: $dark-theme-bg-color; 33 | } 34 | 35 | 36 | .no-text-select { 37 | -webkit-touch-callout: none; 38 | -webkit-user-select: none; 39 | user-select: none; 40 | } 41 | 42 | .bg-maroon { 43 | background-color: $maroon; 44 | } 45 | 46 | .bg-purple { 47 | background-color: $purple; 48 | } 49 | 50 | .fs-lg { 51 | font-size: $font-size-lg; 52 | } 53 | 54 | .fs-xs { 55 | font-size: $font-size-xs; 56 | } 57 | 58 | .fs-xxs { 59 | font-size: $font-size-xxs; 60 | } 61 | 62 | .fs-xxxs { 63 | font-size: $font-size-xxxs; 64 | } 65 | 66 | .fs-xxxxs { 67 | font-size: $font-size-xxxxs; 68 | } 69 | 70 | .fs-md { 71 | font-size: $font-size-md; 72 | } 73 | 74 | .text-gray { 75 | color: $gray; 76 | } 77 | 78 | .circle-icon { 79 | display: flex; 80 | align-items: center; 81 | justify-content: center; 82 | min-width: 23px; 83 | min-height: 23px; 84 | font-size: $font-size-md; 85 | border-radius: 50%; 86 | } 87 | 88 | .circle-primary-icon { 89 | @extend .circle-icon; 90 | background: linear-gradient(#0086d7, #00456f); 91 | color: whitesmoke; 92 | } 93 | 94 | .circle-purple-icon { 95 | @extend .circle-icon; 96 | background: linear-gradient(#965ab4, #5a3778); 97 | color: whitesmoke; 98 | } 99 | 100 | .circle-gray-icon { 101 | @extend .circle-icon; 102 | background-color: darken($gray-light, 5%); 103 | color: black; 104 | } 105 | 106 | 107 | .circle-error-icon { 108 | @extend .circle-icon; 109 | background: linear-gradient(#e63228, #c80f0a); 110 | color: whitesmoke; 111 | } 112 | -------------------------------------------------------------------------------- /renderer/src/scss/components/_theme.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | @mixin app-theme($bg-color, $border-color, $font-color, $dropdown-bg-color, $dropdown-item-color) { 8 | .topBar { 9 | background-color: $bg-color; 10 | 11 | .topBar-content { 12 | > div > button { 13 | color: $font-color; 14 | border-color: $border-color; 15 | 16 | &:hover, &:active, &:focus { 17 | background: rgba(0, 0, 0, .2); 18 | } 19 | } 20 | } 21 | 22 | .topBar-buttons { 23 | color: rgba(61, 60, 64, 0.7); 24 | 25 | > .button { 26 | 27 | &:hover { 28 | background: rgba(255, 255, 255, 0.1); 29 | } 30 | 31 | &:active { 32 | background: rgba(255, 255, 255, 0.2); 33 | } 34 | 35 | } 36 | 37 | > .close-button:hover { 38 | background: #E81123 !important; 39 | } 40 | 41 | > .close-button:active { 42 | background: #f1707a !important; 43 | } 44 | 45 | 46 | } 47 | } 48 | 49 | .bottomBar { 50 | background-color: $bg-color; 51 | 52 | .bottomBar-content { 53 | .bottomBar-syncs { 54 | color: white; 55 | 56 | .bottomBar-syncProgress { 57 | background-color: $border-color; 58 | border-color: $border-color; 59 | 60 | .progress { 61 | .progress-bar { 62 | background-color: darken($orange, 3%); 63 | } 64 | } 65 | } 66 | } 67 | 68 | .bottomBar-downloads { 69 | .btn-group { 70 | .progress { 71 | .progress-bar { 72 | background-color: #034d78; 73 | } 74 | } 75 | } 76 | 77 | > div > button { 78 | color: $font-color; 79 | border-color: $border-color; 80 | 81 | &:hover, &:active, &:focus { 82 | background: rgba(0, 0, 0, .2); 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | .dropdown-menu { 90 | background-color: $dropdown-bg-color; 91 | 92 | .dropdown-divider { 93 | border-color: lighten($dropdown-bg-color, 5%); 94 | } 95 | 96 | .dropdown-item { 97 | color: $dropdown-item-color; 98 | 99 | &:hover, &:active, &:focus { 100 | color: white; 101 | background-color: darken($dropdown-bg-color, 8%); 102 | } 103 | } 104 | } 105 | 106 | .modal { 107 | background: rgba(0, 0, 0, .3); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /i18n/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "文件", 3 | "Hide": "隐藏", 4 | "Hide Others": "隐藏其他", 5 | "Show All": "显示全部", 6 | "&Edit": "编辑", 7 | "Undo": "撤销", 8 | "Redo": "重做", 9 | "Cut": "剪切", 10 | "Copy": "复制", 11 | "Paste": "粘贴", 12 | "Select All": "全选", 13 | "Reload": "重新加载", 14 | "Clear Cache and Reload": "清除缓存并重新加载", 15 | "Toggle Full Screen": "切换全屏", 16 | "Actual Size": "实际大小", 17 | "Zoom In": "放大", 18 | "Zoom Out": "缩小", 19 | "Developer Tools for Application Wrapper": "应用封装程序的开发者工具", 20 | "Developer Tools for Current Server": "当前服务器的开发者工具", 21 | "View": "视图", 22 | "Window": "窗口", 23 | "Minimize": "最小化", 24 | "Zoom": "缩放", 25 | "Close": "关闭", 26 | "Close Window": "关闭窗口", 27 | "Bring All to Front": "全部置于最前", 28 | "Help": "帮助", 29 | "Show logs": "显示日志", 30 | "Version": "版本", 31 | "Sync": "同步", 32 | "Open": "打开", 33 | "Quit": "退出", 34 | "Retry": "重试", 35 | "Add": "添加", 36 | "Edit": "编辑", 37 | "Remove": "删除", 38 | "Cancel": "取消", 39 | "Confirm": "确认", 40 | "All": "全部", 41 | "Please wait": "请稍候", 42 | "Server display name": "服务器显示名称", 43 | "No server configured": "未配置服务器", 44 | "For security reasons, please log in again": "出于安全原因,请重新登录", 45 | "Authenticate": "认证", 46 | "Connect to a server": "连接到服务器", 47 | "Authenticate on the server": "在服务器上进行认证", 48 | "Delete the server": "删除服务器", 49 | "Edit the server": "编辑服务器", 50 | "Login or Email": "登录名或电子邮件", 51 | "Password": "密码", 52 | "Server not found": "找不到服务器", 53 | "Server not reachable": "服务器无法访问", 54 | "Remove_Server_Warning": "将删除服务器 {{ server }} 的所有配置,请确认删除!", 55 | "Wrong login or password": "登录名或密码错误", 56 | "Missing permission": "缺少权限", 57 | "Missing TWO-FA code": "缺少双重身份验证代码", 58 | "Check server protocol (http/https)": "检查服务器协议(http/https)", 59 | "Incorrect code or password": "代码或密码不正确", 60 | "Account suspended or not authorized": "账户已被暂停或未授权", 61 | "Authentication code (if required)": "验证码(如需)", 62 | "Unable to connect to this server !": "无法连接到此服务器!", 63 | "Trying to connect to the server...": "正在尝试连接到服务器...", 64 | "Download complete": "下载完成", 65 | "element synchronized": "元素已同步", 66 | "elements synchronized": "元素已同步", 67 | "Do update": "更新", 68 | "Downloading the update": "正在下载更新", 69 | "A new update is ready !": "有新的更新可用!", 70 | "No new version is available": "没有可用的新版本", 71 | "You have the latest version": "您已是最新版本", 72 | "will be automatically installed on application exit": "将在退出应用时自动安装", 73 | "Check for Updates": "检查更新", 74 | "Back": "后退", 75 | "Forward": "前进", 76 | "Launch at startup": "启动时运行", 77 | "Start Hidden": "启动时隐藏", 78 | "Version History": "版本历史", 79 | "Documentation": "文档", 80 | "Support": "支持", 81 | "Hide Dock Icon": "隐藏程序坞图标", 82 | "Preferences": "偏好设置", 83 | "About": "关于" 84 | } 85 | -------------------------------------------------------------------------------- /renderer/src/app/components/bottom-bar-syncs.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { Component, inject } from '@angular/core' 8 | import { AppService } from '../app.service' 9 | import { ProgressbarComponent } from 'ngx-bootstrap/progressbar' 10 | import { faIcons } from '../common/icons' 11 | import { FaIconComponent, IconDefinition } from '@fortawesome/angular-fontawesome' 12 | import type { SyncTransfer } from '@sync-in-desktop/core/components/interfaces/sync-transfer.interface' 13 | 14 | const sideIcon: Record = { 15 | local: faIcons.faArrowDown, 16 | remote: faIcons.faArrowUp 17 | } 18 | 19 | const sideIconClass: Record = { 20 | local: 'circle-purple-icon', 21 | remote: 'circle-primary-icon' 22 | } 23 | 24 | const iconActions: Record = { 25 | NEW: faIcons.faPlus, 26 | MKDIR: faIcons.faPlus, 27 | MKFILE: faIcons.faPlus, 28 | RM: faIcons.faXmark, 29 | RMDIR: faIcons.faXmark, 30 | DIFF: faIcons.faPencil, 31 | COPY: faIcons.faCopy, 32 | MOVE: faIcons.faUpDownLeftRight, 33 | ERROR: faIcons.faCircleExclamation 34 | } 35 | 36 | @Component({ 37 | selector: 'app-bottom-bar-syncs', 38 | templateUrl: 'bottom-bar-syncs-component.html', 39 | imports: [ProgressbarComponent, FaIconComponent], 40 | standalone: true 41 | }) 42 | export class BottomBarSyncsComponent { 43 | public transfer: { name: string; sideIcon: IconDefinition; sideIconClass: string; actionIcon: IconDefinition; ok: boolean } = null 44 | public transferProgress: { currentSize: string; totalSize: string; percent: number } = null 45 | protected readonly appService = inject(AppService) 46 | 47 | constructor() { 48 | this.appService.syncTransfer.subscribe((transfer: SyncTransfer) => this.setTransfer(transfer)) 49 | } 50 | 51 | private setTransfer(tr: SyncTransfer) { 52 | if (tr) { 53 | this.transfer = { 54 | ok: tr.ok, 55 | name: (tr.fileDst ? tr.fileDst : tr.file).split('/').pop(), 56 | sideIcon: sideIcon[tr.side], 57 | sideIconClass: sideIconClass[tr.side], 58 | actionIcon: tr.ok ? iconActions[tr.action] : iconActions.ERROR 59 | } 60 | if (tr.progress) { 61 | const percent = parseInt(tr.progress.percent) 62 | if (percent === 100) { 63 | this.transferProgress = null 64 | return 65 | } 66 | this.transferProgress = { currentSize: tr.progress.currentSize, totalSize: tr.progress.totalSize, percent: percent } 67 | } 68 | } else { 69 | setTimeout(() => (this.transfer = null), 3000) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /main/constants/events.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | // LOCAL RENDERER 8 | export const LOCAL_RENDERER = { 9 | DEVTOOLS: { 10 | SHOW_WRAPPER: 'devtools-show-wrapper', 11 | SHOW_SERVER: 'devtools-show-server' 12 | }, 13 | UI: { 14 | TOP_VIEW_FOCUS: 'top-view-focus', 15 | APP_MENU_OPEN: 'app-menu-open', 16 | MODAL_TOGGLE: 'modal-toggle', 17 | NAV_BACK: 'nav-back', 18 | NAV_FORWARD: 'nav-forward' 19 | }, 20 | SETTINGS: { 21 | LAUNCH_AT_STARTUP: 'launch-at-startup', 22 | START_HIDDEN: 'start-hidden', 23 | HIDE_DOCK_ICON: 'hide-dock-icon' 24 | }, 25 | WINDOW: { 26 | SHOW: 'window-show', 27 | MAXIMIZE: 'window-maximize', 28 | MINIMIZE: 'window-minimize', 29 | UNMAXIMIZE: 'window-unmaximize', 30 | IS_MAXIMIZED: 'window-is-maximized', 31 | IS_FULLSCREEN: 'window-is-fullscreen', 32 | CLOSE: 'window-close', 33 | ZOOM: { IN: 'window-zoom-in', OUT: 'window-zoom-out', RESET: 'window-zoom-reset' } 34 | }, 35 | UPDATE: { 36 | DOWNLOADED: 'update-available', 37 | RESTART: 'update-restart', 38 | CHECK: 'update-check' 39 | }, 40 | DOWNLOAD: { 41 | GLOBAL_PROGRESS: 'download-global-progress', 42 | PROGRESS: 'download-progress', 43 | ACTION: 'download-action', 44 | LIST: 'downloads-list' 45 | }, 46 | POWER: { 47 | PREVENT_APP_SUSPENSION: 'power-prevent-app-suspension', 48 | SUSPENSION_EVENT: 'power-suspension-event' 49 | }, 50 | SERVER: { 51 | LIST: 'servers-list', 52 | SET_ACTIVE: 'server-set-active', 53 | ACTION: 'server-action', 54 | RETRY: 'server-retry', 55 | RELOAD: 'server-reload' 56 | }, 57 | SYNC: { 58 | MSG: 'sync-msg' 59 | } 60 | } 61 | // REMOTE RENDERER 62 | export const REMOTE_RENDERER = { 63 | // authentication 64 | SERVER: { 65 | AUTHENTICATION: 'server-authentication', 66 | AUTHENTICATION_FAILED: 'server-authentication-failed', 67 | AUTHENTICATION_TOKEN_UPDATE: 'server-authentication-token-update', 68 | AUTHENTICATION_TOKEN_EXPIRED: 'server-authentication-token-expired' 69 | }, 70 | // sync 71 | SYNC: { 72 | PATH_OPERATION: 'sync-path-operation', 73 | TASKS_COUNT: 'sync-tasks-count', 74 | ERRORS: 'sync-errors', 75 | TRANSFER: 'sync-transfer', 76 | REPORT_TRANSFER: 'sync-report-transfer', 77 | TRANSFER_LOGS: 'sync-transfer-logs', 78 | SCHEDULER_STATE: 'sync-scheduler-state' 79 | }, 80 | // tasks & notifications & chats 81 | APPLICATIONS: { 82 | MSG: 'applications-msg', 83 | COUNTER: 'applications-counter' 84 | }, 85 | MISC: { 86 | DIALOG_OPEN: 'dialog-open', 87 | URL_OPEN: 'url-open', 88 | FILE_OPEN: 'file-open', 89 | SWITCH_THEME: 'switch-theme', 90 | NETWORK_IS_ONLINE: 'network-is-online' 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /renderer/src/scss/components/_bottom-bar.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | .bottomBar { 8 | -webkit-user-select: none; 9 | -webkit-app-region: drag; 10 | position: absolute; 11 | bottom: 0; 12 | width: 100%; 13 | 14 | .bottomBar-syncs { 15 | display: flex; 16 | 17 | } 18 | 19 | .bottomBar-content { 20 | display: flex; 21 | padding-left: 0; 22 | 23 | .bottomBar-syncs { 24 | @extend .truncate; 25 | display: flex; 26 | align-items: center; 27 | height: $bottom-bar-height - 1px; 28 | flex-wrap: nowrap; 29 | font-size: $font-size-xs; 30 | width: 100%; 31 | padding: 0 0 0 10px; 32 | 33 | .bottomBar-syncProgress { 34 | position: relative; 35 | min-width: 125px; 36 | height: 100%; 37 | border-width: 0 0 0 .5px; 38 | border-style: solid; 39 | 40 | .progress { 41 | background-color: transparent; 42 | border-radius: 0; 43 | height: 100%; 44 | } 45 | 46 | .progress-text { 47 | display: flex; 48 | justify-content: center; 49 | position: absolute; 50 | width: 100%; 51 | font-size: $font-size-xxxs; 52 | } 53 | } 54 | } 55 | 56 | .bottomBar-downloads { 57 | display: flex; 58 | align-items: center; 59 | justify-content: flex-start; 60 | flex-wrap: wrap; 61 | height: $bottom-bar-height - 1px; 62 | 63 | .btn-group { 64 | height: 100%; 65 | 66 | .dropdown-menu { 67 | // dropup fix 68 | transform: translateY(-100%) !important; 69 | 70 | .dropdown-item { 71 | width: 350px 72 | } 73 | } 74 | 75 | .progress { 76 | position: absolute; 77 | background-color: transparent; 78 | border-radius: 0; 79 | height: $bottom-bar-height - 1px; 80 | } 81 | } 82 | 83 | > div > button { 84 | cursor: default !important; 85 | box-shadow: none !important; 86 | -webkit-app-region: no-drag; 87 | border-width: 0 .5px; 88 | border-style: solid; 89 | padding: 0.65rem 0.5rem; 90 | border-radius: 0; 91 | font-size: 0.8rem; 92 | min-width: 50px; 93 | max-width: 300px; 94 | overflow: hidden; 95 | } 96 | 97 | li { 98 | min-width: 200px; 99 | max-width: 400px; 100 | 101 | a { 102 | height: $bottom-bar-height; 103 | } 104 | 105 | button { 106 | padding: .2rem; 107 | border: none; 108 | } 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /i18n/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "파일", 3 | "Hide": "숨기기", 4 | "Hide Others": "다른 항목 숨기기", 5 | "Show All": "모두 보기", 6 | "&Edit": "편집", 7 | "Undo": "실행 취소", 8 | "Redo": "다시 실행", 9 | "Cut": "잘라내기", 10 | "Copy": "복사", 11 | "Paste": "붙여넣기", 12 | "Select All": "모두 선택", 13 | "Reload": "다시 로드", 14 | "Clear Cache and Reload": "캐시 지우고 다시 로드", 15 | "Toggle Full Screen": "전체 화면 전환", 16 | "Actual Size": "실제 크기", 17 | "Zoom In": "확대", 18 | "Zoom Out": "축소", 19 | "Developer Tools for Application Wrapper": "애플리케이션 래퍼용 개발자 도구", 20 | "Developer Tools for Current Server": "현재 서버용 개발자 도구", 21 | "View": "보기", 22 | "Window": "창", 23 | "Minimize": "최소화", 24 | "Zoom": "확대/축소", 25 | "Close": "닫기", 26 | "Close Window": "창 닫기", 27 | "Bring All to Front": "모두 맨 앞으로 가져오기", 28 | "Help": "도움말", 29 | "Show logs": "로그 표시", 30 | "Version": "버전", 31 | "Sync": "동기화", 32 | "Open": "열기", 33 | "Quit": "종료", 34 | "Retry": "다시 시도", 35 | "Add": "추가", 36 | "Edit": "편집", 37 | "Remove": "제거", 38 | "Cancel": "취소", 39 | "Confirm": "확인", 40 | "All": "모두", 41 | "Please wait": "잠시만 기다려 주세요", 42 | "Server display name": "서버 표시 이름", 43 | "No server configured": "구성된 서버가 없습니다", 44 | "For security reasons, please log in again": "보안상의 이유로 다시 로그인해 주세요", 45 | "Authenticate": "인증", 46 | "Connect to a server": "서버에 연결", 47 | "Authenticate on the server": "서버에서 인증", 48 | "Delete the server": "서버 삭제", 49 | "Edit the server": "서버 편집", 50 | "Login or Email": "로그인 또는 이메일", 51 | "Password": "비밀번호", 52 | "Server not found": "서버를 찾을 수 없습니다", 53 | "Server not reachable": "서버에 연결할 수 없습니다", 54 | "Remove_Server_Warning": "서버 {{ server }}에 대한 모든 설정이 삭제됩니다. 삭제를 확인해 주세요!", 55 | "Wrong login or password": "로그인 또는 비밀번호가 잘못되었습니다", 56 | "Missing permission": "권한이 없습니다", 57 | "Missing TWO-FA code": "2단계 인증 코드가 없습니다", 58 | "Check server protocol (http/https)": "서버 프로토콜(http/https)을 확인하세요", 59 | "Incorrect code or password": "코드 또는 비밀번호가 올바르지 않습니다", 60 | "Account suspended or not authorized": "계정이 정지되었거나 권한이 없습니다", 61 | "Authentication code (if required)": "인증 코드(필요한 경우)", 62 | "Unable to connect to this server !": "이 서버에 연결할 수 없습니다!", 63 | "Trying to connect to the server...": "서버에 연결을 시도하는 중...", 64 | "Download complete": "다운로드 완료", 65 | "element synchronized": "항목이 동기화됨", 66 | "elements synchronized": "항목들이 동기화됨", 67 | "Do update": "업데이트 수행", 68 | "Downloading the update": "업데이트 다운로드 중", 69 | "A new update is ready !": "새로운 업데이트가 준비되었습니다!", 70 | "No new version is available": "새 버전이 없습니다", 71 | "You have the latest version": "최신 버전을 사용 중입니다", 72 | "will be automatically installed on application exit": "애플리케이션 종료 시 자동으로 설치됩니다", 73 | "Check for Updates": "업데이트 확인", 74 | "Back": "뒤로", 75 | "Forward": "앞으로", 76 | "Launch at startup": "시작 시 실행", 77 | "Start Hidden": "숨김으로 시작", 78 | "Version History": "버전 기록", 79 | "Documentation": "문서", 80 | "Support": "지원", 81 | "Hide Dock Icon": "Dock 아이콘 숨기기", 82 | "Preferences": "환경설정", 83 | "About": "정보" 84 | } 85 | -------------------------------------------------------------------------------- /main/constants/menus.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { MenuItemConstructorOptions, shell } from 'electron' 8 | import { i18n } from '../components/translate' 9 | import { ENVIRONMENT, IS_MACOS, MAIN_LOGS_FILE } from '@sync-in-desktop/core/constants' 10 | import { appEvents } from '../components/events' 11 | import { LOCAL_RENDERER } from './events' 12 | import { appSettings } from '../components/settings' 13 | 14 | export const separatorItem: MenuItemConstructorOptions = { 15 | type: 'separator' 16 | } 17 | 18 | export const checkUpdateMenu: () => MenuItemConstructorOptions = () => ({ 19 | label: i18n.tr('Check for Updates'), 20 | click: () => appEvents.emit(LOCAL_RENDERER.UPDATE.CHECK) 21 | }) 22 | 23 | export const supportMenu: (withAppName?: boolean) => MenuItemConstructorOptions = (withAppName = true) => ({ 24 | label: `${i18n.tr('Support')}${withAppName ? ` ${ENVIRONMENT.appID} ` : ' '}💛`, 25 | click: () => shell.openExternal(`${ENVIRONMENT.appHomePage}${i18n.language === 'fr' ? '/fr' : ''}/support/`) 26 | }) 27 | 28 | export const preferencesMenu: () => MenuItemConstructorOptions[] = () => [ 29 | { 30 | label: i18n.tr('Launch at startup'), 31 | type: 'checkbox', 32 | checked: appSettings.configuration.launchAtStartup, 33 | click: () => appEvents.emit(LOCAL_RENDERER.SETTINGS.LAUNCH_AT_STARTUP) 34 | }, 35 | { 36 | label: i18n.tr('Start Hidden'), 37 | type: 'checkbox', 38 | checked: appSettings.configuration.startHidden, 39 | click: () => appEvents.emit(LOCAL_RENDERER.SETTINGS.START_HIDDEN) 40 | }, 41 | ...(IS_MACOS 42 | ? [ 43 | { 44 | label: i18n.tr('Hide Dock Icon'), 45 | type: 'checkbox', 46 | checked: appSettings.configuration.hideDockIcon, 47 | click: () => appEvents.emit(LOCAL_RENDERER.SETTINGS.HIDE_DOCK_ICON) 48 | } satisfies MenuItemConstructorOptions 49 | ] 50 | : []) 51 | ] 52 | export const helpMenu: (name?: string, withCheckUpdate?: boolean) => MenuItemConstructorOptions = (name = 'Help', withCheckUpdate = false) => ({ 53 | label: i18n.tr(name), 54 | submenu: [ 55 | { 56 | label: i18n.tr('Documentation'), 57 | click: () => shell.openExternal(`${ENVIRONMENT.appHomePage}${i18n.language === 'fr' ? '/fr' : ''}/docs/`) 58 | }, 59 | { 60 | label: i18n.tr('Version History'), 61 | click: () => shell.openExternal(ENVIRONMENT.appReleasesPage) 62 | }, 63 | ...(withCheckUpdate ? [separatorItem] : []), 64 | { 65 | label: `${i18n.tr('Version')} ${ENVIRONMENT.appVersion}`, 66 | role: 'about' 67 | }, 68 | ...(withCheckUpdate ? [checkUpdateMenu()] : []), 69 | separatorItem, 70 | { 71 | id: 'Show logs', 72 | label: i18n.tr('Show logs'), 73 | click: () => shell.showItemInFolder(MAIN_LOGS_FILE) 74 | } 75 | ] 76 | }) 77 | -------------------------------------------------------------------------------- /i18n/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "ファイル", 3 | "Hide": "隠す", 4 | "Hide Others": "その他を隠す", 5 | "Show All": "すべてを表示", 6 | "&Edit": "編集", 7 | "Undo": "元に戻す", 8 | "Redo": "やり直し", 9 | "Cut": "切り取り", 10 | "Copy": "コピー", 11 | "Paste": "貼り付け", 12 | "Select All": "すべて選択", 13 | "Reload": "再読み込み", 14 | "Clear Cache and Reload": "キャッシュを消去して再読み込み", 15 | "Toggle Full Screen": "フルスクリーン切り替え", 16 | "Actual Size": "実際のサイズ", 17 | "Zoom In": "拡大", 18 | "Zoom Out": "縮小", 19 | "Developer Tools for Application Wrapper": "アプリケーションラッパー用開発者ツール", 20 | "Developer Tools for Current Server": "現在のサーバー用開発者ツール", 21 | "View": "表示", 22 | "Window": "ウィンドウ", 23 | "Minimize": "最小化", 24 | "Zoom": "ズーム", 25 | "Close": "閉じる", 26 | "Close Window": "ウィンドウを閉じる", 27 | "Bring All to Front": "すべてを前面に移動", 28 | "Help": "ヘルプ", 29 | "Show logs": "ログを表示", 30 | "Version": "バージョン", 31 | "Sync": "同期", 32 | "Open": "開く", 33 | "Quit": "終了", 34 | "Retry": "再試行", 35 | "Add": "追加", 36 | "Edit": "編集", 37 | "Remove": "削除", 38 | "Cancel": "キャンセル", 39 | "Confirm": "確認", 40 | "All": "すべて", 41 | "Please wait": "お待ちください", 42 | "Server display name": "サーバー表示名", 43 | "No server configured": "サーバーが設定されていません", 44 | "For security reasons, please log in again": "セキュリティ上の理由により、再度ログインしてください", 45 | "Authenticate": "認証", 46 | "Connect to a server": "サーバーに接続", 47 | "Authenticate on the server": "サーバーで認証", 48 | "Delete the server": "サーバーを削除", 49 | "Edit the server": "サーバーを編集", 50 | "Login or Email": "ログイン名またはメール", 51 | "Password": "パスワード", 52 | "Server not found": "サーバーが見つかりません", 53 | "Server not reachable": "サーバーに接続できません", 54 | "Remove_Server_Warning": "サーバー {{ server }} のすべての設定が削除されます。削除を確認してください!", 55 | "Wrong login or password": "ログイン名またはパスワードが間違っています", 56 | "Missing permission": "権限がありません", 57 | "Missing TWO-FA code": "二要素認証コードがありません", 58 | "Check server protocol (http/https)": "サーバーのプロトコル (http/https) を確認してください", 59 | "Incorrect code or password": "コードまたはパスワードが正しくありません", 60 | "Account suspended or not authorized": "アカウントが停止されているか、権限がありません", 61 | "Authentication code (if required)": "認証コード(必要な場合)", 62 | "Unable to connect to this server !": "このサーバーに接続できません!", 63 | "Trying to connect to the server...": "サーバーに接続を試みています...", 64 | "Download complete": "ダウンロードが完了しました", 65 | "element synchronized": "要素が同期されました", 66 | "elements synchronized": "要素が同期されました", 67 | "Do update": "更新を実行", 68 | "Downloading the update": "更新をダウンロード中", 69 | "A new update is ready !": "新しい更新が準備できました!", 70 | "No new version is available": "新しいバージョンはありません", 71 | "You have the latest version": "最新バージョンをご利用中です", 72 | "will be automatically installed on application exit": "アプリケーション終了時に自動的にインストールされます", 73 | "Check for Updates": "アップデートを確認", 74 | "Back": "戻る", 75 | "Forward": "進む", 76 | "Launch at startup": "起動時に起動", 77 | "Start Hidden": "非表示で起動", 78 | "Version History": "バージョン履歴", 79 | "Documentation": "ドキュメント", 80 | "Support": "サポート", 81 | "Hide Dock Icon": "Dockアイコンを隠す", 82 | "Preferences": "環境設定", 83 | "About": "このアプリについて" 84 | } 85 | -------------------------------------------------------------------------------- /renderer/src/scss/components/_top-bar.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | .topBar { 8 | -webkit-app-region: drag; 9 | background-color: transparent; 10 | 11 | .topBar-container { 12 | display: flex; 13 | padding-left: 0; 14 | 15 | &.macOS { 16 | padding-left: 80px; 17 | } 18 | 19 | &:not(.macOS) { 20 | .topBar-content .dropdown-menu { 21 | inset: 100% auto auto 50px !important; 22 | } 23 | } 24 | 25 | .topBar-content { 26 | display: flex; 27 | margin-right: auto; 28 | align-items: center; 29 | justify-content: flex-start; 30 | flex-wrap: wrap; 31 | height: $top-bar-height; 32 | 33 | .btn-group { 34 | height: 100%; 35 | 36 | &:first-child { 37 | min-width: 135px; 38 | } 39 | } 40 | 41 | > div > button { 42 | cursor: default !important; 43 | box-shadow: none !important; 44 | -webkit-app-region: no-drag; 45 | border-width: 0 .5px; 46 | border-style: solid; 47 | padding: 0.65rem 0.5rem; 48 | border-radius: 0; 49 | font-size: 0.8rem; 50 | min-width: 120px; 51 | max-width: 400px; 52 | overflow: hidden; 53 | 54 | 55 | &.app-btn { 56 | min-width: 50px; 57 | max-width: 100px; 58 | } 59 | } 60 | 61 | li { 62 | min-width: 200px; 63 | max-width: 400px; 64 | 65 | a { 66 | height: $top-bar-height; 67 | } 68 | 69 | button { 70 | padding: .2rem; 71 | border: none; 72 | } 73 | } 74 | } 75 | 76 | .topBar-buttons { 77 | position: relative; 78 | line-height: $top-bar-height; 79 | height: $top-bar-height; 80 | z-index: 9; 81 | -webkit-app-region: no-drag; 82 | display: grid; 83 | grid-template-columns: repeat(3, 46px); 84 | 85 | > .button { 86 | grid-row: 1 / span 1; 87 | display: flex; 88 | justify-content: center; 89 | align-items: center; 90 | width: 100%; 91 | height: 100%; 92 | user-select: none; 93 | } 94 | 95 | > .close-button:hover { 96 | > img { 97 | filter: invert(100%); 98 | -webkit-filter: invert(100%); 99 | opacity: 1; 100 | } 101 | } 102 | 103 | > .close-button:active { 104 | > img { 105 | filter: invert(100%); 106 | -webkit-filter: invert(100%); 107 | } 108 | } 109 | 110 | img { 111 | filter: invert(100%); 112 | -webkit-filter: invert(100%); 113 | } 114 | 115 | > .min-button { 116 | grid-column: 1; 117 | } 118 | 119 | > .max-button, .restore-button { 120 | grid-column: 2; 121 | } 122 | 123 | > .close-button { 124 | grid-column: 3; 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "File", 3 | "Hide": "Hide", 4 | "Hide Others": "Hide Others", 5 | "Show All": "Show All", 6 | "&Edit": "Edit", 7 | "Undo": "Undo", 8 | "Redo": "Redo", 9 | "Cut": "Cut", 10 | "Copy": "Copy", 11 | "Paste": "Paste", 12 | "Select All": "Select All", 13 | "Reload": "Reload", 14 | "Clear Cache and Reload": "Clear Cache and Reload", 15 | "Toggle Full Screen": "Toggle Full Screen", 16 | "Actual Size": "Actual Size", 17 | "Zoom In": "Zoom In", 18 | "Zoom Out": "Zoom Out", 19 | "Developer Tools for Application Wrapper": "Developer Tools for Application Wrapper", 20 | "Developer Tools for Current Server": "Developer Tools for Current Server", 21 | "View": "View", 22 | "Window": "Window", 23 | "Minimize": "Minimize", 24 | "Zoom": "Zoom", 25 | "Close": "Close", 26 | "Close Window": "Close Window", 27 | "Bring All to Front": "Bring All to Front", 28 | "Help": "Help", 29 | "Show logs": "Show logs", 30 | "Version": "Version", 31 | "Sync": "Sync", 32 | "Quit": "Quit", 33 | "Retry": "Retry", 34 | "Add": "Add", 35 | "Edit": "Edit", 36 | "Remove": "Remove", 37 | "Cancel": "Cancel", 38 | "Confirm": "Confirm", 39 | "Please wait": "Please wait", 40 | "Server display name": "Server display name", 41 | "No server configured": "No server configured", 42 | "For security reasons, please log in again": "For security reasons, please log in again", 43 | "Authenticate": "Authenticate", 44 | "Connect to a server": "Connect to a server", 45 | "Authenticate on the server": "Authenticate on the server", 46 | "Delete the server": "Delete the server", 47 | "Edit the server": "Edit the server", 48 | "Login or Email": "Login or Email", 49 | "Password": "Password", 50 | "Server not found": "Server not found", 51 | "Server not reachable": "Server not reachable", 52 | "Remove_Server_Warning": "All configurations will be deleted for server {{ server }}, please confirm deletion !", 53 | "Wrong login or password": "Wrong login or password", 54 | "Account suspended or not authorized": "Account suspended or not authorized", 55 | "Unable to connect to this server !": "Unable to connect to this server !", 56 | "Trying to connect to the server...": "Trying to connect to the server...", 57 | "Download complete": "Download complete", 58 | "element synchronized": "element synchronized", 59 | "elements synchronized": "elements synchronized", 60 | "Do update": "Mettre à jour", 61 | "Downloading the update": "Downloading the update", 62 | "A new update is ready !": "A new update is ready !", 63 | "No new version is available": "No new version is available", 64 | "You have the latest version": "You have the latest version", 65 | "will be automatically installed on application exit": "will be automatically installed on application exit", 66 | "Check for Updates": "Check for Updates", 67 | "Back": "Back", 68 | "Forward": "Forward", 69 | "Launch at startup": "Launch at startup", 70 | "Start Hidden": "Start Hidden", 71 | "Version History": "Version History", 72 | "Documentation": "Documentation", 73 | "Support": "Support", 74 | "Hide Dock Icon": "Hide Dock Icon", 75 | "Preferences": "Preferences", 76 | "About": "About" 77 | } 78 | -------------------------------------------------------------------------------- /main/components/transfers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { ServersManager } from '../../core/components/handlers/servers' 8 | import fs from 'node:fs/promises' 9 | import readline from 'node:readline' 10 | import { setMimeType, sortObjsByDate } from './utils' 11 | import { createReadStream, existsSync } from 'node:fs' 12 | import type { SyncTransfer } from '@sync-in-desktop/core/components/interfaces/sync-transfer.interface' 13 | import { DEFAULT_HIGH_WATER_MARK } from '@sync-in-desktop/core/components/constants/handlers' 14 | 15 | export class TransfersLogs { 16 | private maxLines = 1000 17 | private readonly search: RegExp 18 | private syncPaths: any[] = [] 19 | private logs: any[] = [] 20 | 21 | constructor(serverId: number, syncPathId: number, query: string) { 22 | this.search = query ? new RegExp(query, 'i') : null 23 | const server = ServersManager.find(serverId) 24 | if (syncPathId) { 25 | const syncPath = server.syncPaths.find((s) => s.id === syncPathId) 26 | this.syncPaths.push({ id: syncPath.id, name: syncPath.name, log: syncPath.getLogsPath(server.id) }) 27 | } else { 28 | this.syncPaths = server.syncPaths.map((s) => ({ id: s.id, name: s.name, log: s.getLogsPath(server.id) })) 29 | } 30 | } 31 | 32 | async get(): Promise { 33 | this.syncPaths = this.syncPaths.filter((syncPath: any) => existsSync(syncPath.log)) 34 | if (!this.syncPaths.length) { 35 | return this.logs 36 | } 37 | const logs: SyncTransfer[] = [] 38 | for (const arrayOfLogs of await Promise.all(this.syncPaths.map((logFile: string) => this.readLogs(logFile)))) { 39 | logs.push(...arrayOfLogs) 40 | } 41 | sortObjsByDate(logs, 'timestamp') 42 | if (logs.length > this.maxLines) { 43 | logs.splice(-(logs.length - this.maxLines)) 44 | } 45 | return logs.map((l: SyncTransfer) => setMimeType(l)) 46 | } 47 | 48 | async delete() { 49 | for (const syncPath of this.syncPaths) { 50 | await fs.truncate(syncPath.log, 0) 51 | } 52 | } 53 | 54 | private async readLogs(syncPath: any): Promise { 55 | let nbLines = 0 56 | let counter = 0 57 | const lines = [] 58 | const rl = readline.createInterface({ 59 | input: createReadStream(syncPath.log, { encoding: 'utf-8', highWaterMark: DEFAULT_HIGH_WATER_MARK }), 60 | terminal: false, 61 | crlfDelay: Infinity 62 | }) 63 | for await (const line of rl) { 64 | nbLines++ 65 | if (line.includes('file') && (!this.search || this.search.test(line))) { 66 | lines.unshift([`${syncPath.id}${nbLines}`, line]) 67 | counter++ 68 | if (counter > this.maxLines * 2) { 69 | lines.splice(-this.maxLines) 70 | counter = lines.length 71 | } 72 | } 73 | } 74 | if (lines.length > this.maxLines) { 75 | lines.splice(-(lines.length - this.maxLines)) 76 | } 77 | return lines.map(([id, line]) => ({ ...{ id: Number(id), syncPathName: syncPath.name }, ...JSON.parse(line) })) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /main/components/notifications.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { app, ipcMain, Notification } from 'electron' 8 | import { LOCAL_RENDERER, REMOTE_RENDERER } from '../constants/events' 9 | import { ViewsManager } from './views' 10 | import { appEvents } from './events' 11 | import { i18n } from './translate' 12 | import { ApplicationCounter, ServerAppCounter } from '../interfaces/counter.interface' 13 | 14 | export class NotifyManager { 15 | viewsManager: ViewsManager 16 | removeHtmlTags = /(<([^>]+)>)/gi 17 | notificationIsSupported = true 18 | serversAppsCounter: ServerAppCounter[] = [] // [{'id': 1, 'name': 'test', 'applications': {'notifications': 2, 'tasks': 4, 'syncs': 2}}, ...] 19 | 20 | constructor(viewsManager: ViewsManager) { 21 | this.viewsManager = viewsManager 22 | this.notificationIsSupported = Notification.isSupported() 23 | ipcMain.on(REMOTE_RENDERER.APPLICATIONS.MSG, (ev, msg: { title: string; body: string }) => this.receivedMsgFromRenderer(ev, msg)) 24 | ipcMain.on(REMOTE_RENDERER.APPLICATIONS.COUNTER, (ev, application: ApplicationCounter, count: number) => 25 | this.storeUnreadCounter(ev, application, count) 26 | ) 27 | appEvents.on(LOCAL_RENDERER.SYNC.MSG, (msg: { title: string; body: string; nb?: number }) => this.receivedMsgFromSync(msg)) 28 | } 29 | 30 | send(title: string, body: string, callback = null) { 31 | if (this.notificationIsSupported) { 32 | const notification = new Notification({ title: title, body: body, silent: true }) 33 | if (callback) { 34 | notification.once('click', callback) 35 | } 36 | notification.show() 37 | } 38 | } 39 | 40 | private receivedMsgFromSync(msg: { title: string; body: string; nb?: number }) { 41 | if (msg.nb) { 42 | this.send(msg.title, `${msg.nb} ${i18n.tr(msg.body)}`) 43 | } else { 44 | this.send(msg.title, msg.body) 45 | } 46 | } 47 | 48 | private receivedMsgFromRenderer(ev: any, msg: { title: string; body: string }): void { 49 | this.send(`${ev.sender.serverName} - ${msg.title}`, msg?.body?.replaceAll(this.removeHtmlTags, '')) 50 | } 51 | 52 | private storeUnreadCounter(ev: any, application: ApplicationCounter, count: number) { 53 | let globalCount = 0 54 | let serverFound = false 55 | for (const server of this.serversAppsCounter) { 56 | if (ev.sender.serverId === server.id) { 57 | server.applications[application] = count 58 | serverFound = true 59 | } 60 | for (const appCount of (Object as any).values(server.applications)) { 61 | globalCount += appCount 62 | } 63 | } 64 | if (!serverFound) { 65 | this.serversAppsCounter.push({ 66 | id: ev.sender.serverId, 67 | name: ev.sender.serverName, 68 | applications: { [application]: count } as ServerAppCounter['applications'] 69 | }) 70 | globalCount += count 71 | } 72 | this.viewsManager.sendToWrapperRenderer(REMOTE_RENDERER.APPLICATIONS.COUNTER, this.serversAppsCounter) 73 | app.setBadgeCount(globalCount) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /main/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { app, Menu } from 'electron' 8 | import { i18n } from './components/translate' 9 | import { WindowManager } from './components/windows' 10 | import { TrayManager } from './components/tray' 11 | import { DownloadManager } from './components/downloads' 12 | import { NotifyManager } from './components/notifications' 13 | import { EventsManager } from './components/events' 14 | import { UpdateManager } from './components/autoupdater' 15 | import { ENVIRONMENT, IS_MACOS, IS_PROD_ENV, IS_WINDOWS } from '../core/constants' 16 | import { createMenu } from './components/menus' 17 | import { appSettings } from './components/settings' 18 | 19 | class MainManager { 20 | trayManager: TrayManager 21 | windowManager: WindowManager 22 | downloadManager: DownloadManager 23 | notifyManager: NotifyManager 24 | eventsManager: EventsManager 25 | updateManager: UpdateManager 26 | 27 | constructor() { 28 | // app.disableHardwareAcceleration() 29 | // app.commandLine.appendSwitch('log-file', MAIN_LOGS_FILE) 30 | // app.commandLine.appendSwitch('enable-logging') 31 | // app.commandLine.appendSwitch('lang', 'zh-CN') 32 | app.commandLine.appendSwitch('ignore-certificate-errors') 33 | app.whenReady().then(() => this.appIsReady()) 34 | } 35 | 36 | appIsReady() { 37 | if (!app.requestSingleInstanceLock()) { 38 | console.log('Sync-in App is already started') 39 | app.quit() 40 | return 41 | } 42 | if (IS_WINDOWS) { 43 | app.setAppUserModelId(ENVIRONMENT.appID) 44 | } 45 | this.checkStartUp() 46 | // `app.getLocale()` returns English by default if the language is not listed in `electronLanguages` in the main package.json 47 | i18n.updateLanguage(app.getLocale()) 48 | Menu.setApplicationMenu(createMenu()) 49 | this.trayManager = new TrayManager() 50 | this.windowManager = new WindowManager() 51 | this.eventsManager = new EventsManager(this.windowManager.viewsManager) 52 | this.notifyManager = new NotifyManager(this.windowManager.viewsManager) 53 | this.downloadManager = new DownloadManager(this.windowManager, this.notifyManager) 54 | this.updateManager = new UpdateManager() 55 | app.on('activate', () => this.windowManager.show()) 56 | app.on('window-all-closed', () => console.log('all windows closed')) 57 | app.on('before-quit', (e: Event) => { 58 | e.preventDefault() 59 | this.eventsManager.runManager.exitGracefully() 60 | this.windowManager.setAppIsQuitting(true) 61 | }) 62 | this.windowManager.setAppIsQuitting(false) 63 | } 64 | 65 | private checkStartUp() { 66 | if (IS_MACOS && appSettings.configuration.hideDockIcon) { 67 | app.dock.hide() 68 | } 69 | if (!IS_PROD_ENV) return 70 | const loginItem = app.getLoginItemSettings() 71 | if (appSettings.configuration.launchAtStartup !== loginItem.openAtLogin) { 72 | app.setLoginItemSettings({ openAtLogin: appSettings.configuration.launchAtStartup }) 73 | } 74 | if (app.commandLine.hasSwitch('hidden') || process.env.SYNC_IN_HIDDEN === '1') { 75 | appSettings.configuration.startHidden = true 76 | } 77 | } 78 | } 79 | 80 | new MainManager() 81 | -------------------------------------------------------------------------------- /main/components/windows.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { BrowserWindow, ipcMain } from 'electron' 8 | import { defaultWindowProps } from '../constants/windows' 9 | import { ViewsManager } from './views' 10 | import { LOCAL_RENDERER } from '../constants/events' 11 | import { appEvents } from './events' 12 | import { ENVIRONMENT, IS_MACOS } from '../../core/constants' 13 | import { appSettings } from './settings' 14 | 15 | export class WindowManager { 16 | appIsQuitting = false 17 | mainWindow: BrowserWindow 18 | viewsManager: ViewsManager 19 | 20 | constructor() { 21 | this.mainWindow = new BrowserWindow(defaultWindowProps) 22 | this.mainWindow.setTitle(ENVIRONMENT.appID) 23 | this.mainWindow.setMenuBarVisibility(false) 24 | this.viewsManager = new ViewsManager(this.mainWindow, !appSettings.configuration.startHidden) 25 | this.mainWindow.on('close', (e: Event) => this.close(e)) 26 | this.mainWindow.on('maximize', () => this.viewsManager.sendToWrapperRenderer(LOCAL_RENDERER.WINDOW.IS_MAXIMIZED, true)) 27 | this.mainWindow.on('unmaximize', () => this.viewsManager.sendToWrapperRenderer(LOCAL_RENDERER.WINDOW.IS_MAXIMIZED, false)) 28 | this.mainWindow.on('enter-full-screen', () => this.viewsManager.sendToWrapperRenderer(LOCAL_RENDERER.WINDOW.IS_FULLSCREEN, true)) 29 | this.mainWindow.on('leave-full-screen', () => this.viewsManager.sendToWrapperRenderer(LOCAL_RENDERER.WINDOW.IS_FULLSCREEN, false)) 30 | this.mainWindow.on('resize', () => this.viewsManager.resizeViews()) 31 | this.initIPC() 32 | } 33 | 34 | private initIPC() { 35 | appEvents.on(LOCAL_RENDERER.WINDOW.SHOW, () => this.mainWindow.show()) 36 | appEvents.on(LOCAL_RENDERER.UPDATE.RESTART, () => this.setAppIsQuitting(true)) 37 | ipcMain.on(LOCAL_RENDERER.WINDOW.CLOSE, () => this.close()) 38 | ipcMain.on(LOCAL_RENDERER.WINDOW.MINIMIZE, () => this.mainWindow.minimize()) 39 | ipcMain.on(LOCAL_RENDERER.WINDOW.MAXIMIZE, () => this.mainWindow.maximize()) 40 | ipcMain.on(LOCAL_RENDERER.WINDOW.UNMAXIMIZE, () => this.unmaximize()) 41 | } 42 | 43 | sendToWrapperRenderer(channel: string, ...args: any[]) { 44 | this.viewsManager.sendToWrapperRenderer(channel, ...args) 45 | } 46 | 47 | private close(e?: Event) { 48 | if (this.appIsQuitting) { 49 | this.mainWindow = null 50 | } else { 51 | e?.preventDefault() 52 | if (IS_MACOS) { 53 | if (this.mainWindow.isFullScreen()) { 54 | this.mainWindow.once('leave-full-screen', this.hide) 55 | this.mainWindow.setFullScreen(false) 56 | return 57 | } 58 | } 59 | this.hide() 60 | } 61 | } 62 | 63 | show() { 64 | if (this.mainWindow.isVisible()) { 65 | this.mainWindow.focus() 66 | } else { 67 | this.mainWindow.show() 68 | } 69 | } 70 | 71 | private unmaximize() { 72 | if (IS_MACOS) { 73 | if (this.mainWindow.isFullScreen()) { 74 | this.mainWindow.once('leave-full-screen', this.mainWindow.unmaximize) 75 | this.mainWindow.setFullScreen(false) 76 | return 77 | } 78 | } 79 | this.mainWindow.unmaximize() 80 | } 81 | 82 | private hide() { 83 | this.mainWindow?.blur() 84 | this.mainWindow?.hide() 85 | } 86 | 87 | setAppIsQuitting(status: boolean) { 88 | this.appIsQuitting = status 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /i18n/hi.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "फ़ाइल", 3 | "Hide": "छिपाएँ", 4 | "Hide Others": "अन्य को छिपाएँ", 5 | "Show All": "सभी दिखाएँ", 6 | "&Edit": "संपादन", 7 | "Undo": "पूर्ववत करें", 8 | "Redo": "पुनः करें", 9 | "Cut": "काटें", 10 | "Copy": "कॉपी करें", 11 | "Paste": "चिपकाएँ", 12 | "Select All": "सभी चुनें", 13 | "Reload": "पुनः लोड करें", 14 | "Clear Cache and Reload": "कैश साफ़ करें और पुनः लोड करें", 15 | "Toggle Full Screen": "पूर्ण स्क्रीन टॉगल करें", 16 | "Actual Size": "वास्तविक आकार", 17 | "Zoom In": "ज़ूम इन", 18 | "Zoom Out": "ज़ूम आउट", 19 | "Developer Tools for Application Wrapper": "एप्लिकेशन रैपर के लिए डेवलपर टूल्स", 20 | "Developer Tools for Current Server": "वर्तमान सर्वर के लिए डेवलपर टूल्स", 21 | "View": "दृश्य", 22 | "Window": "विंडो", 23 | "Minimize": "मिनिमाइज़ करें", 24 | "Zoom": "ज़ूम", 25 | "Close": "बंद करें", 26 | "Close Window": "विंडो बंद करें", 27 | "Bring All to Front": "सभी को सामने लाएँ", 28 | "Help": "सहायता", 29 | "Show logs": "लॉग दिखाएँ", 30 | "Version": "संस्करण", 31 | "Sync": "सिंक", 32 | "Open": "खोलें", 33 | "Quit": "बाहर निकलें", 34 | "Retry": "पुनः प्रयास करें", 35 | "Add": "जोड़ें", 36 | "Edit": "संपादित करें", 37 | "Remove": "हटाएँ", 38 | "Cancel": "रद्द करें", 39 | "Confirm": "पुष्टि करें", 40 | "All": "सभी", 41 | "Please wait": "कृपया प्रतीक्षा करें", 42 | "Server display name": "सर्वर का प्रदर्शित नाम", 43 | "No server configured": "कोई सर्वर कॉन्फ़िगर नहीं है", 44 | "For security reasons, please log in again": "सुरक्षा कारणों से कृपया दोबारा लॉग इन करें", 45 | "Authenticate": "प्रमाणित करें", 46 | "Connect to a server": "किसी सर्वर से कनेक्ट करें", 47 | "Authenticate on the server": "सर्वर पर प्रमाणित करें", 48 | "Delete the server": "सर्वर हटाएँ", 49 | "Edit the server": "सर्वर संपादित करें", 50 | "Login or Email": "लॉगिन या ईमेल", 51 | "Password": "पासवर्ड", 52 | "Server not found": "सर्वर नहीं मिला", 53 | "Server not reachable": "सर्वर तक पहुँचा नहीं जा सका", 54 | "Remove_Server_Warning": "सर्वर {{ server }} के लिए सभी कॉन्फ़िगरेशन हटा दिए जाएँगे, कृपया हटाने की पुष्टि करें!", 55 | "Wrong login or password": "गलत लॉगिन या पासवर्ड", 56 | "Missing permission": "अनुमति गायब है", 57 | "Missing TWO-FA code": "दो-कारक प्रमाणीकरण कोड गायब है", 58 | "Check server protocol (http/https)": "सर्वर प्रोटोकॉल (http/https) जाँचें", 59 | "Incorrect code or password": "गलत कोड या पासवर्ड", 60 | "Account suspended or not authorized": "खाता निलंबित है या अधिकृत नहीं है", 61 | "Authentication code (if required)": "प्रमाणीकरण कोड (यदि आवश्यक हो)", 62 | "Unable to connect to this server !": "इस सर्वर से कनेक्ट नहीं हो सका!", 63 | "Trying to connect to the server...": "सर्वर से कनेक्ट करने का प्रयास कर रहे हैं...", 64 | "Download complete": "डाउनलोड पूरा हुआ", 65 | "element synchronized": "तत्व समकालित हुआ", 66 | "elements synchronized": "तत्व समकालित हुए", 67 | "Do update": "अपडेट करें", 68 | "Downloading the update": "अपडेट डाउनलोड हो रहा है", 69 | "A new update is ready !": "एक नया अपडेट तैयार है!", 70 | "No new version is available": "कोई नया संस्करण उपलब्ध नहीं है", 71 | "You have the latest version": "आपके पास नवीनतम संस्करण है", 72 | "will be automatically installed on application exit": "एप्लिकेशन से बाहर निकलने पर स्वतः स्थापित हो जाएगा", 73 | "Check for Updates": "अपडेट की जाँच करें", 74 | "Back": "पीछे", 75 | "Forward": "आगे", 76 | "Launch at startup": "स्टार्टअप पर लॉन्च करें", 77 | "Start Hidden": "प्रारंभ पर छिपाएँ", 78 | "Version History": "संस्करण इतिहास", 79 | "Documentation": "प्रलेखन", 80 | "Support": "समर्थन", 81 | "Hide Dock Icon": "डॉक आइकन छिपाएँ", 82 | "Preferences": "प्राथमिकताएँ", 83 | "About": "के बारे में" 84 | } 85 | -------------------------------------------------------------------------------- /core/components/handlers/tasks.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | import { getLogger } from './loggers' 7 | import { setTimeout } from 'timers/promises' 8 | import { TRANSFER_MIN_SIZE } from '../constants/handlers' 9 | import type { Logger } from 'winston' 10 | import type { SyncTransferContext } from '../interfaces/sync-transfer.interface' 11 | 12 | type Task = () => Promise 13 | 14 | interface Queue { 15 | concurrency: number 16 | items: Task[] 17 | count: number 18 | isDone?: boolean 19 | } 20 | 21 | export class TasksManager { 22 | private readonly logger: Logger 23 | private readonly queues: Record 24 | private endQueue: Task[] = [] 25 | private endFilling = false 26 | private stopped = false 27 | 28 | constructor(sync: SyncTransferContext) { 29 | this.logger = getLogger('Tasks', sync) 30 | 31 | this.queues = { 32 | fast: { concurrency: 3, items: [], count: 0 }, 33 | slow: { concurrency: 1, items: [], count: 0, isDone: false }, 34 | props: { concurrency: 2, items: [], count: 0 } 35 | } 36 | } 37 | 38 | async run(): Promise { 39 | const allPipes = Object.entries(this.queues).flatMap(([name, queue]) => 40 | Array.from({ length: queue.concurrency }, (_, i) => this.queuePipe(name as keyof typeof this.queues, i)) 41 | ) 42 | await Promise.all(allPipes) 43 | this.logger.debug('All queues are done') 44 | } 45 | 46 | private async queuePipe(type: keyof typeof this.queues, id: number): Promise { 47 | const queue = this.queues[type] 48 | 49 | while (!this.stopped) { 50 | // Handle the main task queue 51 | if (queue.count > 0) { 52 | const task = queue.items.shift() 53 | if (task) { 54 | try { 55 | await task() 56 | } catch (error) { 57 | this.logger.error(error) 58 | } finally { 59 | queue.count-- 60 | } 61 | continue 62 | } 63 | } 64 | 65 | // handle end queue transfer to fast queue if needed 66 | if (type === 'fast' && this.endQueue.length > 0) { 67 | const transferredTasks = this.endQueue.splice(0) 68 | this.queues.fast.items.push(...transferredTasks) 69 | this.queues.fast.count += transferredTasks.length 70 | continue 71 | } 72 | 73 | // exit condition for queues 74 | const canExit = !queue.count && this.endFilling && (type !== 'fast' || this.queues.slow.isDone) 75 | 76 | if (canExit) { 77 | break 78 | } 79 | 80 | await setTimeout(100) // Reduced delay for faster checks 81 | } 82 | 83 | if (type === 'slow') queue.isDone = true 84 | this.logger.debug(`${type.charAt(0).toUpperCase() + type.slice(1)} queue ${id} is done`) 85 | } 86 | 87 | add(task: Task, size: number): void { 88 | const queue = size >= TRANSFER_MIN_SIZE ? this.queues.slow : this.queues.fast 89 | queue.items.push(task) 90 | queue.count++ 91 | } 92 | 93 | addToEnd(task: Task, inFirst = false): void { 94 | if (inFirst) { 95 | this.endQueue.unshift(task) 96 | } else { 97 | this.endQueue.push(task) 98 | } 99 | } 100 | 101 | addToProps(task: Task): void { 102 | this.queues.props.items.push(task) 103 | this.queues.props.count++ 104 | } 105 | 106 | fillingDone(): void { 107 | this.endFilling = true 108 | } 109 | 110 | stop(): void { 111 | this.stopped = true 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /i18n/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "Dosya", 3 | "Hide": "Gizle", 4 | "Hide Others": "Diğerlerini gizle", 5 | "Show All": "Tümünü göster", 6 | "&Edit": "Düzenle", 7 | "Undo": "Geri al", 8 | "Redo": "Yinele", 9 | "Cut": "Kes", 10 | "Copy": "Kopyala", 11 | "Paste": "Yapıştır", 12 | "Select All": "Tümünü seç", 13 | "Reload": "Yeniden yükle", 14 | "Clear Cache and Reload": "Önbelleği temizle ve yeniden yükle", 15 | "Toggle Full Screen": "Tam ekranı değiştir", 16 | "Actual Size": "Gerçek boyut", 17 | "Zoom In": "Yakınlaştır", 18 | "Zoom Out": "Uzaklaştır", 19 | "Developer Tools for Application Wrapper": "Uygulama sarmalayıcı için geliştirici araçları", 20 | "Developer Tools for Current Server": "Geçerli sunucu için geliştirici araçları", 21 | "View": "Görünüm", 22 | "Window": "Pencere", 23 | "Minimize": "Simge durumuna küçült", 24 | "Zoom": "Yakınlaştırma", 25 | "Close": "Kapat", 26 | "Close Window": "Pencereyi kapat", 27 | "Bring All to Front": "Tümünü öne getir", 28 | "Help": "Yardım", 29 | "Show logs": "Günlükleri göster", 30 | "Version": "Sürüm", 31 | "Sync": "Eşitle", 32 | "Open": "Aç", 33 | "Quit": "Çık", 34 | "Retry": "Yeniden dene", 35 | "Add": "Ekle", 36 | "Edit": "Düzenle", 37 | "Remove": "Kaldır", 38 | "Cancel": "İptal", 39 | "Confirm": "Onayla", 40 | "All": "Tümü", 41 | "Please wait": "Lütfen bekleyin", 42 | "Server display name": "Sunucu görünen adı", 43 | "No server configured": "Yapılandırılmış sunucu yok", 44 | "For security reasons, please log in again": "Güvenlik nedeniyle lütfen tekrar giriş yapın", 45 | "Authenticate": "Kimlik doğrula", 46 | "Connect to a server": "Sunucuya bağlan", 47 | "Authenticate on the server": "Sunucuda kimlik doğrula", 48 | "Delete the server": "Sunucuyu sil", 49 | "Edit the server": "Sunucuyu düzenle", 50 | "Login or Email": "Giriş adı veya E-posta", 51 | "Password": "Parola", 52 | "Server not found": "Sunucu bulunamadı", 53 | "Server not reachable": "Sunucuya erişilemiyor", 54 | "Remove_Server_Warning": "{{ server }} sunucusu için tüm yapılandırmalar silinecek, lütfen silmeyi onaylayın!", 55 | "Wrong login or password": "Hatalı giriş adı veya parola", 56 | "Missing permission": "İzin eksik", 57 | "Missing TWO-FA code": "İki aşamalı doğrulama kodu eksik", 58 | "Check server protocol (http/https)": "Sunucu protokolünü (http/https) kontrol edin", 59 | "Incorrect code or password": "Kod veya parola yanlış", 60 | "Account suspended or not authorized": "Hesap askıya alınmış veya yetkiniz yok", 61 | "Authentication code (if required)": "Kimlik doğrulama kodu (gerekiyorsa)", 62 | "Unable to connect to this server !": "Bu sunucuya bağlanılamıyor!", 63 | "Trying to connect to the server...": "Sunucuya bağlanmaya çalışılıyor...", 64 | "Download complete": "İndirme tamamlandı", 65 | "element synchronized": "öğe eşitlendi", 66 | "elements synchronized": "öğeler eşitlendi", 67 | "Do update": "Güncelleme yap", 68 | "Downloading the update": "Güncelleme indiriliyor", 69 | "A new update is ready !": "Yeni bir güncelleme hazır!", 70 | "No new version is available": "Yeni bir sürüm mevcut değil", 71 | "You have the latest version": "En son sürümü kullanıyorsunuz", 72 | "will be automatically installed on application exit": "uygulamadan çıkışta otomatik olarak yüklenecektir", 73 | "Check for Updates": "Güncellemeleri denetle", 74 | "Back": "Geri", 75 | "Forward": "İleri", 76 | "Launch at startup": "Başlangıçta çalıştır", 77 | "Start Hidden": "Gizli başlat", 78 | "Version History": "Sürüm geçmişi", 79 | "Documentation": "Belgeler", 80 | "Support": "Destek", 81 | "Hide Dock Icon": "Dock simgesini gizle", 82 | "Preferences": "Tercihler", 83 | "About": "Hakkında" 84 | } 85 | -------------------------------------------------------------------------------- /i18n/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "Plik", 3 | "Hide": "Ukryj", 4 | "Hide Others": "Ukryj inne", 5 | "Show All": "Pokaż wszystkie", 6 | "&Edit": "Edycja", 7 | "Undo": "Cofnij", 8 | "Redo": "Ponów", 9 | "Cut": "Wytnij", 10 | "Copy": "Kopiuj", 11 | "Paste": "Wklej", 12 | "Select All": "Zaznacz wszystko", 13 | "Reload": "Odśwież", 14 | "Clear Cache and Reload": "Wyczyść pamięć podręczną i odśwież", 15 | "Toggle Full Screen": "Przełącz pełny ekran", 16 | "Actual Size": "Rzeczywisty rozmiar", 17 | "Zoom In": "Powiększ", 18 | "Zoom Out": "Pomniejsz", 19 | "Developer Tools for Application Wrapper": "Narzędzia deweloperskie dla wrappera aplikacji", 20 | "Developer Tools for Current Server": "Narzędzia deweloperskie dla bieżącego serwera", 21 | "View": "Widok", 22 | "Window": "Okno", 23 | "Minimize": "Zminimalizuj", 24 | "Zoom": "Powiększenie", 25 | "Close": "Zamknij", 26 | "Close Window": "Zamknij okno", 27 | "Bring All to Front": "Przenieś wszystko na wierzch", 28 | "Help": "Pomoc", 29 | "Show logs": "Pokaż logi", 30 | "Version": "Wersja", 31 | "Sync": "Synchronizuj", 32 | "Open": "Otwórz", 33 | "Quit": "Zakończ", 34 | "Retry": "Ponów próbę", 35 | "Add": "Dodaj", 36 | "Edit": "Edytuj", 37 | "Remove": "Usuń", 38 | "Cancel": "Anuluj", 39 | "Confirm": "Potwierdź", 40 | "All": "Wszystko", 41 | "Please wait": "Proszę czekać", 42 | "Server display name": "Wyświetlana nazwa serwera", 43 | "No server configured": "Nie skonfigurowano serwera", 44 | "For security reasons, please log in again": "Ze względów bezpieczeństwa zaloguj się ponownie", 45 | "Authenticate": "Uwierzytelnij", 46 | "Connect to a server": "Połącz z serwerem", 47 | "Authenticate on the server": "Uwierzytelnij na serwerze", 48 | "Delete the server": "Usuń serwer", 49 | "Edit the server": "Edytuj serwer", 50 | "Login or Email": "Login lub e‑mail", 51 | "Password": "Hasło", 52 | "Server not found": "Nie znaleziono serwera", 53 | "Server not reachable": "Serwer nieosiągalny", 54 | "Remove_Server_Warning": "Wszystkie konfiguracje dla serwera {{ server }} zostaną usunięte, proszę potwierdzić usunięcie!", 55 | "Wrong login or password": "Nieprawidłowy login lub hasło", 56 | "Missing permission": "Brak uprawnień", 57 | "Missing TWO-FA code": "Brak kodu uwierzytelniania dwuskładnikowego", 58 | "Check server protocol (http/https)": "Sprawdź protokół serwera (http/https)", 59 | "Incorrect code or password": "Nieprawidłowy kod lub hasło", 60 | "Account suspended or not authorized": "Konto zawieszone lub brak autoryzacji", 61 | "Authentication code (if required)": "Kod uwierzytelniający (jeśli wymagany)", 62 | "Unable to connect to this server !": "Nie można połączyć się z tym serwerem!", 63 | "Trying to connect to the server...": "Próba połączenia z serwerem...", 64 | "Download complete": "Pobieranie zakończone", 65 | "element synchronized": "element zsynchronizowany", 66 | "elements synchronized": "elementy zsynchronizowane", 67 | "Do update": "Wykonaj aktualizację", 68 | "Downloading the update": "Pobieranie aktualizacji", 69 | "A new update is ready !": "Nowa aktualizacja jest gotowa!", 70 | "No new version is available": "Brak dostępnej nowej wersji", 71 | "You have the latest version": "Masz najnowszą wersję", 72 | "will be automatically installed on application exit": "zostanie automatycznie zainstalowana przy zamknięciu aplikacji", 73 | "Check for Updates": "Sprawdź aktualizacje", 74 | "Back": "Wstecz", 75 | "Forward": "Dalej", 76 | "Launch at startup": "Uruchamiaj przy starcie", 77 | "Start Hidden": "Uruchamiaj w ukryciu", 78 | "Version History": "Historia wersji", 79 | "Documentation": "Dokumentacja", 80 | "Support": "Wsparcie", 81 | "Hide Dock Icon": "Ukryj ikonę Docka", 82 | "Preferences": "Preferencje", 83 | "About": "O programie" 84 | } 85 | -------------------------------------------------------------------------------- /i18n/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "Arquivo", 3 | "Hide": "Ocultar", 4 | "Hide Others": "Ocultar outros", 5 | "Show All": "Mostrar tudo", 6 | "&Edit": "Editar", 7 | "Undo": "Desfazer", 8 | "Redo": "Refazer", 9 | "Cut": "Cortar", 10 | "Copy": "Copiar", 11 | "Paste": "Colar", 12 | "Select All": "Selecionar tudo", 13 | "Reload": "Recarregar", 14 | "Clear Cache and Reload": "Limpar cache e recarregar", 15 | "Toggle Full Screen": "Alternar tela cheia", 16 | "Actual Size": "Tamanho real", 17 | "Zoom In": "Aumentar zoom", 18 | "Zoom Out": "Reduzir zoom", 19 | "Developer Tools for Application Wrapper": "Ferramentas do desenvolvedor para o wrapper do aplicativo", 20 | "Developer Tools for Current Server": "Ferramentas do desenvolvedor para o servidor atual", 21 | "View": "Exibir", 22 | "Window": "Janela", 23 | "Minimize": "Minimizar", 24 | "Zoom": "Zoom", 25 | "Close": "Fechar", 26 | "Close Window": "Fechar janela", 27 | "Bring All to Front": "Trazer tudo para frente", 28 | "Help": "Ajuda", 29 | "Show logs": "Mostrar registros", 30 | "Version": "Versão", 31 | "Sync": "Sincronizar", 32 | "Open": "Abrir", 33 | "Quit": "Sair", 34 | "Retry": "Tentar novamente", 35 | "Add": "Adicionar", 36 | "Edit": "Editar", 37 | "Remove": "Remover", 38 | "Cancel": "Cancelar", 39 | "Confirm": "Confirmar", 40 | "All": "Todos", 41 | "Please wait": "Por favor, aguarde", 42 | "Server display name": "Nome de exibição do servidor", 43 | "No server configured": "Nenhum servidor configurado", 44 | "For security reasons, please log in again": "Por motivos de segurança, faça login novamente", 45 | "Authenticate": "Autenticar", 46 | "Connect to a server": "Conectar-se a um servidor", 47 | "Authenticate on the server": "Autenticar no servidor", 48 | "Delete the server": "Excluir o servidor", 49 | "Edit the server": "Editar o servidor", 50 | "Login or Email": "Login ou e-mail", 51 | "Password": "Senha", 52 | "Server not found": "Servidor não encontrado", 53 | "Server not reachable": "Servidor inacessível", 54 | "Remove_Server_Warning": "Todas as configurações serão removidas para o servidor {{ server }}, confirme a exclusão!", 55 | "Wrong login or password": "Login ou senha incorretos", 56 | "Missing permission": "Permissão ausente", 57 | "Missing TWO-FA code": "Código de autenticação de dois fatores ausente", 58 | "Check server protocol (http/https)": "Verifique o protocolo do servidor (http/https)", 59 | "Incorrect code or password": "Código ou senha incorretos", 60 | "Account suspended or not authorized": "Conta suspensa ou não autorizada", 61 | "Authentication code (if required)": "Código de autenticação (se necessário)", 62 | "Unable to connect to this server !": "Não é possível conectar a este servidor!", 63 | "Trying to connect to the server...": "Tentando conectar ao servidor...", 64 | "Download complete": "Download concluído", 65 | "element synchronized": "elemento sincronizado", 66 | "elements synchronized": "elementos sincronizados", 67 | "Do update": "Atualizar", 68 | "Downloading the update": "Baixando a atualização", 69 | "A new update is ready !": "Uma nova atualização está pronta!", 70 | "No new version is available": "Nenhuma nova versão disponível", 71 | "You have the latest version": "Você tem a versão mais recente", 72 | "will be automatically installed on application exit": "será instalada automaticamente ao sair do aplicativo", 73 | "Check for Updates": "Procurar atualizações", 74 | "Back": "Voltar", 75 | "Forward": "Avançar", 76 | "Launch at startup": "Iniciar na inicialização", 77 | "Start Hidden": "Iniciar oculto", 78 | "Version History": "Histórico de versões", 79 | "Documentation": "Documentação", 80 | "Support": "Apoiar", 81 | "Hide Dock Icon": "Ocultar ícone do Dock", 82 | "Preferences": "Preferências", 83 | "About": "Sobre" 84 | } 85 | -------------------------------------------------------------------------------- /i18n/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "Файл", 3 | "Hide": "Скрыть", 4 | "Hide Others": "Скрыть другие", 5 | "Show All": "Показать все", 6 | "&Edit": "Правка", 7 | "Undo": "Отменить", 8 | "Redo": "Повторить", 9 | "Cut": "Вырезать", 10 | "Copy": "Копировать", 11 | "Paste": "Вставить", 12 | "Select All": "Выделить всё", 13 | "Reload": "Перезагрузить", 14 | "Clear Cache and Reload": "Очистить кэш и перезагрузить", 15 | "Toggle Full Screen": "Переключить полноэкранный режим", 16 | "Actual Size": "Фактический размер", 17 | "Zoom In": "Увеличить", 18 | "Zoom Out": "Уменьшить", 19 | "Developer Tools for Application Wrapper": "Инструменты разработчика для оболочки приложения", 20 | "Developer Tools for Current Server": "Инструменты разработчика для текущего сервера", 21 | "View": "Вид", 22 | "Window": "Окно", 23 | "Minimize": "Свернуть", 24 | "Zoom": "Масштаб", 25 | "Close": "Закрыть", 26 | "Close Window": "Закрыть окно", 27 | "Bring All to Front": "Вывести все на передний план", 28 | "Help": "Справка", 29 | "Show logs": "Показать логи", 30 | "Version": "Версия", 31 | "Sync": "Синхронизировать", 32 | "Open": "Открыть", 33 | "Quit": "Выйти", 34 | "Retry": "Повторить попытку", 35 | "Add": "Добавить", 36 | "Edit": "Изменить", 37 | "Remove": "Удалить", 38 | "Cancel": "Отмена", 39 | "Confirm": "Подтвердить", 40 | "All": "Все", 41 | "Please wait": "Пожалуйста, подождите", 42 | "Server display name": "Отображаемое имя сервера", 43 | "No server configured": "Сервер не настроен", 44 | "For security reasons, please log in again": "В целях безопасности, пожалуйста, авторизуйтесь снова", 45 | "Authenticate": "Войти", 46 | "Connect to a server": "Подключиться к серверу", 47 | "Authenticate on the server": "Войти на сервер", 48 | "Delete the server": "Удалить сервер", 49 | "Edit the server": "Изменить сервер", 50 | "Login or Email": "Логин или электронная почта", 51 | "Password": "Пароль", 52 | "Server not found": "Сервер не найден", 53 | "Server not reachable": "Сервер недоступен", 54 | "Remove_Server_Warning": "Все настройки будут удалены для сервера {{ server }}, пожалуйста, подтвердите удаление!", 55 | "Wrong login or password": "Неверный логин или пароль", 56 | "Missing permission": "Недостаточно прав", 57 | "Missing TWO-FA code": "Отсутствует код двухфакторной аутентификации", 58 | "Check server protocol (http/https)": "Проверьте протокол сервера (http/https)", 59 | "Incorrect code or password": "Неверный код или пароль", 60 | "Account suspended or not authorized": "Учетная запись приостановлена или не авторизована", 61 | "Authentication code (if required)": "Код аутентификации (если требуется)", 62 | "Unable to connect to this server !": "Не удается подключиться к этому серверу!", 63 | "Trying to connect to the server...": "Попытка подключения к серверу...", 64 | "Download complete": "Загрузка завершена", 65 | "element synchronized": "элемент синхронизирован", 66 | "elements synchronized": "элементы синхронизированы", 67 | "Do update": "Выполнить обновление", 68 | "Downloading the update": "Загрузка обновления", 69 | "A new update is ready !": "Доступно новое обновление!", 70 | "No new version is available": "Нет доступной новой версии", 71 | "You have the latest version": "У вас установлена последняя версия", 72 | "will be automatically installed on application exit": "будет автоматически установлено при закрытии приложения", 73 | "Check for Updates": "Проверить обновления", 74 | "Back": "Назад", 75 | "Forward": "Вперёд", 76 | "Launch at startup": "Запускать при старте системы", 77 | "Start Hidden": "Запускать скрытым", 78 | "Version History": "История версий", 79 | "Documentation": "Документация", 80 | "Support": "Поддержать", 81 | "Hide Dock Icon": "Скрыть значок в Dock", 82 | "Preferences": "Настройки", 83 | "About": "О программе" 84 | } 85 | -------------------------------------------------------------------------------- /i18n/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "Arquivo", 3 | "Hide": "Ocultar", 4 | "Hide Others": "Ocultar outros", 5 | "Show All": "Mostrar tudo", 6 | "&Edit": "Editar", 7 | "Undo": "Desfazer", 8 | "Redo": "Refazer", 9 | "Cut": "Cortar", 10 | "Copy": "Copiar", 11 | "Paste": "Colar", 12 | "Select All": "Selecionar tudo", 13 | "Reload": "Recarregar", 14 | "Clear Cache and Reload": "Limpar cache e recarregar", 15 | "Toggle Full Screen": "Alternar tela cheia", 16 | "Actual Size": "Tamanho real", 17 | "Zoom In": "Ampliar", 18 | "Zoom Out": "Reduzir", 19 | "Developer Tools for Application Wrapper": "Ferramentas do desenvolvedor para o wrapper do aplicativo", 20 | "Developer Tools for Current Server": "Ferramentas do desenvolvedor para o servidor atual", 21 | "View": "Exibir", 22 | "Window": "Janela", 23 | "Minimize": "Minimizar", 24 | "Zoom": "Zoom", 25 | "Close": "Fechar", 26 | "Close Window": "Fechar janela", 27 | "Bring All to Front": "Trazer tudo para frente", 28 | "Help": "Ajuda", 29 | "Show logs": "Mostrar logs", 30 | "Version": "Versão", 31 | "Sync": "Sincronizar", 32 | "Open": "Abrir", 33 | "Quit": "Sair", 34 | "Retry": "Tentar novamente", 35 | "Add": "Adicionar", 36 | "Edit": "Editar", 37 | "Remove": "Remover", 38 | "Cancel": "Cancelar", 39 | "Confirm": "Confirmar", 40 | "All": "Todos", 41 | "Please wait": "Por favor, aguarde", 42 | "Server display name": "Nome de exibição do servidor", 43 | "No server configured": "Nenhum servidor configurado", 44 | "For security reasons, please log in again": "Por motivos de segurança, faça login novamente", 45 | "Authenticate": "Autenticar", 46 | "Connect to a server": "Conectar a um servidor", 47 | "Authenticate on the server": "Autenticar no servidor", 48 | "Delete the server": "Excluir o servidor", 49 | "Edit the server": "Editar o servidor", 50 | "Login or Email": "Login ou e-mail", 51 | "Password": "Senha", 52 | "Server not found": "Servidor não encontrado", 53 | "Server not reachable": "Servidor inacessível", 54 | "Remove_Server_Warning": "Todas as configurações serão removidas para o servidor {{ server }}, por favor, confirme a exclusão!", 55 | "Wrong login or password": "Login ou senha incorretos", 56 | "Missing permission": "Permissão ausente", 57 | "Missing TWO-FA code": "Código de autenticação de dois fatores ausente", 58 | "Check server protocol (http/https)": "Verifique o protocolo do servidor (http/https)", 59 | "Incorrect code or password": "Código ou senha incorretos", 60 | "Account suspended or not authorized": "Conta suspensa ou não autorizada", 61 | "Authentication code (if required)": "Código de autenticação (se necessário)", 62 | "Unable to connect to this server !": "Não foi possível conectar a este servidor!", 63 | "Trying to connect to the server...": "Tentando conectar ao servidor...", 64 | "Download complete": "Download concluído", 65 | "element synchronized": "elemento sincronizado", 66 | "elements synchronized": "elementos sincronizados", 67 | "Do update": "Executar atualização", 68 | "Downloading the update": "Baixando a atualização", 69 | "A new update is ready !": "Uma nova atualização está pronta!", 70 | "No new version is available": "Nenhuma nova versão disponível", 71 | "You have the latest version": "Você está com a versão mais recente", 72 | "will be automatically installed on application exit": "será instalada automaticamente ao sair do aplicativo", 73 | "Check for Updates": "Verificar atualizações", 74 | "Back": "Voltar", 75 | "Forward": "Avançar", 76 | "Launch at startup": "Iniciar na inicialização", 77 | "Start Hidden": "Iniciar oculto", 78 | "Version History": "Histórico de versões", 79 | "Documentation": "Documentação", 80 | "Support": "Suporte", 81 | "Hide Dock Icon": "Ocultar ícone do Dock", 82 | "Preferences": "Preferências", 83 | "About": "Sobre" 84 | } 85 | -------------------------------------------------------------------------------- /i18n/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "Archivo", 3 | "Hide": "Ocultar", 4 | "Hide Others": "Ocultar otros", 5 | "Show All": "Mostrar todo", 6 | "&Edit": "Editar", 7 | "Undo": "Deshacer", 8 | "Redo": "Rehacer", 9 | "Cut": "Cortar", 10 | "Copy": "Copiar", 11 | "Paste": "Pegar", 12 | "Select All": "Seleccionar todo", 13 | "Reload": "Recargar", 14 | "Clear Cache and Reload": "Borrar caché y recargar", 15 | "Toggle Full Screen": "Alternar pantalla completa", 16 | "Actual Size": "Tamaño real", 17 | "Zoom In": "Acercar", 18 | "Zoom Out": "Alejar", 19 | "Developer Tools for Application Wrapper": "Herramientas de desarrollador para el contenedor de la aplicación", 20 | "Developer Tools for Current Server": "Herramientas de desarrollador para el servidor actual", 21 | "View": "Vista", 22 | "Window": "Ventana", 23 | "Minimize": "Minimizar", 24 | "Zoom": "Zoom", 25 | "Close": "Cerrar", 26 | "Close Window": "Cerrar ventana", 27 | "Bring All to Front": "Traer todo al frente", 28 | "Help": "Ayuda", 29 | "Show logs": "Mostrar registros", 30 | "Version": "Versión", 31 | "Sync": "Sincronizar", 32 | "Open": "Abrir", 33 | "Quit": "Salir", 34 | "Retry": "Reintentar", 35 | "Add": "Agregar", 36 | "Edit": "Editar", 37 | "Remove": "Eliminar", 38 | "Cancel": "Cancelar", 39 | "Confirm": "Confirmar", 40 | "All": "Todos", 41 | "Please wait": "Por favor, espere", 42 | "Server display name": "Nombre para mostrar del servidor", 43 | "No server configured": "Ningún servidor configurado", 44 | "For security reasons, please log in again": "Por razones de seguridad, vuelva a iniciar sesión", 45 | "Authenticate": "Autenticar", 46 | "Connect to a server": "Conectarse a un servidor", 47 | "Authenticate on the server": "Autenticarse en el servidor", 48 | "Delete the server": "Eliminar el servidor", 49 | "Edit the server": "Editar el servidor", 50 | "Login or Email": "Usuario o correo electrónico", 51 | "Password": "Contraseña", 52 | "Server not found": "Servidor no encontrado", 53 | "Server not reachable": "Servidor no accesible", 54 | "Remove_Server_Warning": "Se eliminarán todas las configuraciones del servidor {{ server }}, ¡confirme la eliminación!", 55 | "Wrong login or password": "Usuario o contraseña incorrectos", 56 | "Missing permission": "Falta permiso", 57 | "Missing TWO-FA code": "Falta el código de autenticación de dos factores", 58 | "Check server protocol (http/https)": "Verifique el protocolo del servidor (http/https)", 59 | "Incorrect code or password": "Código o contraseña incorrectos", 60 | "Account suspended or not authorized": "Cuenta suspendida o no autorizada", 61 | "Authentication code (if required)": "Código de autenticación (si es necesario)", 62 | "Unable to connect to this server !": "¡No se puede conectar a este servidor!", 63 | "Trying to connect to the server...": "Intentando conectarse al servidor...", 64 | "Download complete": "Descarga completa", 65 | "element synchronized": "elemento sincronizado", 66 | "elements synchronized": "elementos sincronizados", 67 | "Do update": "Actualizar", 68 | "Downloading the update": "Descargando la actualización", 69 | "A new update is ready !": "¡Una nueva actualización está lista!", 70 | "No new version is available": "No hay una nueva versión disponible", 71 | "You have the latest version": "Tiene la última versión", 72 | "will be automatically installed on application exit": "se instalará automáticamente al salir de la aplicación", 73 | "Check for Updates": "Buscar actualizaciones", 74 | "Back": "Atrás", 75 | "Forward": "Adelante", 76 | "Launch at startup": "Iniciar al inicio", 77 | "Start Hidden": "Iniciar oculto", 78 | "Version History": "Historial de versiones", 79 | "Documentation": "Documentación", 80 | "Support": "Soporte", 81 | "Hide Dock Icon": "Ocultar el icono del Dock", 82 | "Preferences": "Preferencias", 83 | "About": "Acerca de" 84 | } 85 | -------------------------------------------------------------------------------- /i18n/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "File", 3 | "Hide": "Nascondi", 4 | "Hide Others": "Nascondi altri", 5 | "Show All": "Mostra tutto", 6 | "&Edit": "Modifica", 7 | "Undo": "Annulla", 8 | "Redo": "Ripeti", 9 | "Cut": "Taglia", 10 | "Copy": "Copia", 11 | "Paste": "Incolla", 12 | "Select All": "Seleziona tutto", 13 | "Reload": "Ricarica", 14 | "Clear Cache and Reload": "Svuota la cache e ricarica", 15 | "Toggle Full Screen": "Attiva/Disattiva schermo intero", 16 | "Actual Size": "Dimensioni reali", 17 | "Zoom In": "Ingrandisci", 18 | "Zoom Out": "Riduci", 19 | "Developer Tools for Application Wrapper": "Strumenti di sviluppo per il wrapper dell'applicazione", 20 | "Developer Tools for Current Server": "Strumenti di sviluppo per il server corrente", 21 | "View": "Vista", 22 | "Window": "Finestra", 23 | "Minimize": "Minimizza", 24 | "Zoom": "Zoom", 25 | "Close": "Chiudi", 26 | "Close Window": "Chiudi finestra", 27 | "Bring All to Front": "Porta tutto in primo piano", 28 | "Help": "Aiuto", 29 | "Show logs": "Mostra registri", 30 | "Version": "Versione", 31 | "Sync": "Sincronizza", 32 | "Open": "Apri", 33 | "Quit": "Esci", 34 | "Retry": "Riprova", 35 | "Add": "Aggiungi", 36 | "Edit": "Modifica", 37 | "Remove": "Rimuovi", 38 | "Cancel": "Annulla", 39 | "Confirm": "Conferma", 40 | "All": "Tutti", 41 | "Please wait": "Attendere, prego", 42 | "Server display name": "Nome di visualizzazione del server", 43 | "No server configured": "Nessun server configurato", 44 | "For security reasons, please log in again": "Per motivi di sicurezza, effettua nuovamente l'accesso", 45 | "Authenticate": "Autentica", 46 | "Connect to a server": "Connettiti a un server", 47 | "Authenticate on the server": "Autenticati sul server", 48 | "Delete the server": "Elimina il server", 49 | "Edit the server": "Modifica il server", 50 | "Login or Email": "Nome utente o email", 51 | "Password": "Password", 52 | "Server not found": "Server non trovato", 53 | "Server not reachable": "Server non raggiungibile", 54 | "Remove_Server_Warning": "Tutte le configurazioni verranno eliminate per il server {{ server }}, confermare l'eliminazione!", 55 | "Wrong login or password": "Nome utente o password errati", 56 | "Missing permission": "Autorizzazione mancante", 57 | "Missing TWO-FA code": "Codice di autenticazione a due fattori mancante", 58 | "Check server protocol (http/https)": "Controlla il protocollo del server (http/https)", 59 | "Incorrect code or password": "Codice o password errati", 60 | "Account suspended or not authorized": "Account sospeso o non autorizzato", 61 | "Authentication code (if required)": "Codice di autenticazione (se richiesto)", 62 | "Unable to connect to this server !": "Impossibile connettersi a questo server!", 63 | "Trying to connect to the server...": "Tentativo di connessione al server...", 64 | "Download complete": "Download completato", 65 | "element synchronized": "elemento sincronizzato", 66 | "elements synchronized": "elementi sincronizzati", 67 | "Do update": "Aggiorna", 68 | "Downloading the update": "Scaricamento dell'aggiornamento", 69 | "A new update is ready !": "È disponibile un nuovo aggiornamento!", 70 | "No new version is available": "Nessuna nuova versione disponibile", 71 | "You have the latest version": "Hai la versione più recente", 72 | "will be automatically installed on application exit": "verrà installato automaticamente alla chiusura dell'applicazione", 73 | "Check for Updates": "Verifica aggiornamenti", 74 | "Back": "Indietro", 75 | "Forward": "Avanti", 76 | "Launch at startup": "Esegui all'avvio", 77 | "Start Hidden": "Avvia nascosto", 78 | "Version History": "Cronologia delle versioni", 79 | "Documentation": "Documentazione", 80 | "Support": "Sostieni", 81 | "Hide Dock Icon": "Nascondi icona del Dock", 82 | "Preferences": "Preferenze", 83 | "About": "Informazioni" 84 | } 85 | -------------------------------------------------------------------------------- /i18n/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "Datei", 3 | "Hide": "Ausblenden", 4 | "Hide Others": "Andere ausblenden", 5 | "Show All": "Alle einblenden", 6 | "&Edit": "Bearbeiten", 7 | "Undo": "Rückgängig", 8 | "Redo": "Wiederholen", 9 | "Cut": "Ausschneiden", 10 | "Copy": "Kopieren", 11 | "Paste": "Einfügen", 12 | "Select All": "Alles auswählen", 13 | "Reload": "Neu laden", 14 | "Clear Cache and Reload": "Cache leeren und neu laden", 15 | "Toggle Full Screen": "Vollbild umschalten", 16 | "Actual Size": "Originalgröße", 17 | "Zoom In": "Vergrößern", 18 | "Zoom Out": "Verkleinern", 19 | "Developer Tools for Application Wrapper": "Entwicklerwerkzeuge für den Anwendungs-Wrapper", 20 | "Developer Tools for Current Server": "Entwicklerwerkzeuge für den aktuellen Server", 21 | "View": "Ansicht", 22 | "Window": "Fenster", 23 | "Minimize": "Minimieren", 24 | "Zoom": "Zoomen", 25 | "Close": "Schließen", 26 | "Close Window": "Fenster schließen", 27 | "Bring All to Front": "Alle nach vorne bringen", 28 | "Help": "Hilfe", 29 | "Show logs": "Protokolle anzeigen", 30 | "Version": "Version", 31 | "Sync": "Synchronisieren", 32 | "Open": "Öffnen", 33 | "Quit": "Beenden", 34 | "Retry": "Erneut versuchen", 35 | "Add": "Hinzufügen", 36 | "Edit": "Bearbeiten", 37 | "Remove": "Entfernen", 38 | "Cancel": "Abbrechen", 39 | "Confirm": "Bestätigen", 40 | "All": "Alle", 41 | "Please wait": "Bitte warten", 42 | "Server display name": "Anzeigename des Servers", 43 | "No server configured": "Kein Server konfiguriert", 44 | "For security reasons, please log in again": "Aus Sicherheitsgründen melden Sie sich bitte erneut an", 45 | "Authenticate": "Authentifizieren", 46 | "Connect to a server": "Mit einem Server verbinden", 47 | "Authenticate on the server": "Auf dem Server authentifizieren", 48 | "Delete the server": "Server löschen", 49 | "Edit the server": "Server bearbeiten", 50 | "Login or Email": "Login oder E-Mail", 51 | "Password": "Passwort", 52 | "Server not found": "Server nicht gefunden", 53 | "Server not reachable": "Server nicht erreichbar", 54 | "Remove_Server_Warning": "Alle Konfigurationen für den Server {{ server }} werden gelöscht, bitte die Löschung bestätigen!", 55 | "Wrong login or password": "Falscher Login oder falsches Passwort", 56 | "Missing permission": "Fehlende Berechtigung", 57 | "Missing TWO-FA code": "Zwei-Faktor-Code fehlt", 58 | "Check server protocol (http/https)": "Serverprotokoll prüfen (http/https)", 59 | "Incorrect code or password": "Falscher Code oder falsches Passwort", 60 | "Account suspended or not authorized": "Konto gesperrt oder nicht autorisiert", 61 | "Authentication code (if required)": "Authentifizierungscode (falls erforderlich)", 62 | "Unable to connect to this server !": "Verbindung zu diesem Server nicht möglich!", 63 | "Trying to connect to the server...": "Versuche, eine Verbindung zum Server herzustellen...", 64 | "Download complete": "Download abgeschlossen", 65 | "element synchronized": "Element synchronisiert", 66 | "elements synchronized": "Elemente synchronisiert", 67 | "Do update": "Aktualisieren", 68 | "Downloading the update": "Update wird heruntergeladen", 69 | "A new update is ready !": "Ein neues Update ist bereit!", 70 | "No new version is available": "Keine neue Version verfügbar", 71 | "You have the latest version": "Sie haben die neueste Version", 72 | "will be automatically installed on application exit": "wird beim Beenden der Anwendung automatisch installiert", 73 | "Check for Updates": "Nach Updates suchen", 74 | "Back": "Zurück", 75 | "Forward": "Vorwärts", 76 | "Launch at startup": "Beim Systemstart starten", 77 | "Start Hidden": "Beim Start ausblenden", 78 | "Version History": "Versionsverlauf", 79 | "Documentation": "Dokumentation", 80 | "Support": "Unterstützen", 81 | "Hide Dock Icon": "Dock-Symbol ausblenden", 82 | "Preferences": "Einstellungen", 83 | "About": "Über" 84 | } 85 | -------------------------------------------------------------------------------- /i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "File": "Fichier", 3 | "Hide": "Masquer", 4 | "Hide Others": "Masquer les autres", 5 | "Show All": "Afficher tout", 6 | "&Edit": "Edition", 7 | "Undo": "Annuler", 8 | "Redo": "Rétablir", 9 | "Cut": "Couper", 10 | "Copy": "Copier", 11 | "Paste": "Coller", 12 | "Select All": "Sélectionner tout", 13 | "Reload": "Recharger", 14 | "Clear Cache and Reload": "Effacer le cache et recharger", 15 | "Toggle Full Screen": "Basculer en plein écran", 16 | "Actual Size": "Taille réelle", 17 | "Zoom In": "Zoomer", 18 | "Zoom Out": "Dézoomer", 19 | "Developer Tools for Application Wrapper": "Outils de développement de l'application packagée", 20 | "Developer Tools for Current Server": "Outils de développement pour le serveur actuel", 21 | "View": "Affichage", 22 | "Window": "Fenêtre", 23 | "Minimize": "Minimiser", 24 | "Zoom": "Zoom", 25 | "Close": "Fermer", 26 | "Close Window": "Fermer la fenêtre", 27 | "Bring All to Front": "Tout mettre en avant", 28 | "Help": "Aide", 29 | "Show logs": "Afficher les journaux", 30 | "Version": "Version", 31 | "Sync": "Synchroniser", 32 | "Open": "Ouvrir", 33 | "Quit": "Quitter", 34 | "Retry": "Réessayer", 35 | "Add": "Ajouter", 36 | "Edit": "Modifier", 37 | "Remove": "Supprimer", 38 | "Cancel": "Annuler", 39 | "Confirm": "Confirmer", 40 | "All": "Tous", 41 | "Please wait": "Veuillez patienter", 42 | "Server display name": "Nom d'affichage du serveur", 43 | "No server configured": "Aucun serveur configuré", 44 | "For security reasons, please log in again": "Pour des raisons de sécurité, veuillez-vous reconnecter", 45 | "Authenticate": "S'authentifier", 46 | "Connect to a server": "Se connecter à un serveur", 47 | "Authenticate on the server": "S'authentifier sur le serveur", 48 | "Delete the server": "Supprimer le serveur", 49 | "Edit the server": "Modifier le serveur", 50 | "Login or Email": "Identifiant ou Email", 51 | "Password": "Mot de passe", 52 | "Server not found": "Serveur non trouvé", 53 | "Server not reachable": "Serveur non accessible", 54 | "Remove_Server_Warning": "Toutes les configurations seront supprimées pour le serveur {{ server }}, veuillez confirmer la suppression !", 55 | "Wrong login or password": "Identifiant ou mot de passe incorrect", 56 | "Missing permission": "Permission manquante", 57 | "Missing TWO-FA code": "Code d’authentification à deux facteurs manquant", 58 | "Check server protocol (http/https)": " Vérifiez le protocole du serveur (http/https)", 59 | "Incorrect code or password": "Code ou mot de passe incorrect", 60 | "Account suspended or not authorized": "Compte suspendu ou non autorisé", 61 | "Authentication code (if required)": "Code d'authentification (si requis)", 62 | "Unable to connect to this server !": "Impossible de se connecter à ce serveur !", 63 | "Trying to connect to the server...": "Tentative de connection au serveur...", 64 | "Download complete": "Téléchargement terminé", 65 | "element synchronized": "élément synchronisé", 66 | "elements synchronized": "éléments synchronisés", 67 | "Do update": "Mettre à jour", 68 | "Downloading the update": "Téléchargement de la mise à jour", 69 | "A new update is ready !": "Une nouvelle mise à jour est prête !", 70 | "No new version is available": "Aucune nouvelle version n'est disponible", 71 | "You have the latest version": "Vous disposez de la dernière version", 72 | "will be automatically installed on application exit": "sera installée à la fermeture de l'application", 73 | "Check for Updates": "Vérifier les mises à jour", 74 | "Back": "Retour", 75 | "Forward": "Avancer", 76 | "Launch at startup": "Lancer au démarrage", 77 | "Start Hidden": "Cacher au démarrage", 78 | "Version History": "Historique des versions", 79 | "Documentation": "Documentation", 80 | "Support": "Soutenir", 81 | "Hide Dock Icon": "Masquer l'icône du Dock", 82 | "Preferences": "Préférences", 83 | "About": "À propos" 84 | } 85 | -------------------------------------------------------------------------------- /renderer/src/scss/variables.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | // Bootstrap overrides 7 | // Colors 8 | $white: #ffffff; 9 | $gray-dark: #384042; 10 | $gray: #9faecb; 11 | $gray-mid: #b9c5da; 12 | $gray-light: #d4dbe8; 13 | $gray-lighter: #e1e6ef; 14 | $gray-lightest: #eef1f6; 15 | $light-blue: #2e92ce; 16 | $lighter-blue: #94c7e8; 17 | $red: #dc3545; 18 | $green: #06854b; 19 | $dark-green: #008000; 20 | $light-green: #37cb46; 21 | $aqua: #0dcaf0; 22 | $blue: #0576b9; 23 | $dark-mid-blue: #337abd; 24 | $dark-blue: #195d9e; 25 | $navy: #0b2640; 26 | $cyan: #17a2b8; 27 | $teal: #39CCCC; 28 | $dark-teal: #527a7a; 29 | $olive: #3D9970; 30 | $lime: #01FF70; 31 | $orange: #ff7700; 32 | $yellow: #ffc107; 33 | $fuchsia: #F012BE; 34 | $purple: #9b59b6; 35 | $maroon: #b33c00; 36 | $black: #111; 37 | 38 | $primary: #1c68b0; 39 | $success: $green; 40 | $secondary: #c3cee0; 41 | $secondary-light: #dde3ee; 42 | $info: $aqua; 43 | $warning: $orange; 44 | $danger: $red; 45 | $mark-bg: $light-green; 46 | $muted: #777; 47 | // Options 48 | // Font, line-height, and color for body text, headings, and more. 49 | $font-size-base: 0.9rem; // Assumes the browser default, typically `16px` 50 | $font-size-lg: 1rem; 51 | $font-size-md: .90rem; 52 | $font-size-sm: .80rem; 53 | $font-size-xs: .75rem; 54 | $font-size-xxs: .70rem; 55 | $font-size-xxxs: .60rem; 56 | $font-size-xxxxs: .55rem; 57 | 58 | //-------------------------------------------------------- 59 | //Body background (Affects main content background only) 60 | $body-bg: transparent; 61 | $body-color: #333; 62 | $custom-color: #212529; 63 | $custom-light-color: #384047; 64 | $custom-dark-color: #28353b; 65 | $list-group-color: $custom-color; 66 | $placeholder-color: #8c8c8c; 67 | // Themed colors 68 | $dark-theme-bg-color: #222d32; 69 | $dark-theme-font-color: $gray-light; 70 | $dark-theme-border-color: #304047; 71 | $dark-theme-dropdown-bg-color: $dark-theme-border-color; 72 | $dark-theme-dropdown-item-color: lighten($dark-theme-bg-color, 60%); 73 | 74 | $light-theme-bg-color: $navy; 75 | $light-theme-font-color: $gray-lightest; 76 | $light-theme-border-color: lighten($light-theme-bg-color, 10%); 77 | $light-theme-dropdown-bg-color: lighten($light-theme-bg-color, 10%); 78 | $light-theme-dropdown-item-color: $gray-light; 79 | 80 | 81 | // Font, line-height, and color for body text, headings, and more. 82 | $font-size-base: 0.9rem; // Assumes the browser default, typically `16px` 83 | $font-size-xl: 1.1rem; 84 | $font-size-lg: 1rem; 85 | $font-size-md: .90rem; 86 | $font-size-sm: .80rem; 87 | $font-size-xs: .75rem; 88 | $font-size-xxs: .70rem; 89 | $font-size-xxxs: .65rem; 90 | $font-size-xxxxs: .60rem; 91 | // Quickly modify global styling by enabling or disabling optional features. 92 | $enable-transitions: true; 93 | $enable-rounded: true; 94 | $enable-grid-classes: true; 95 | $enable-shadows: true; 96 | $enable-sidebar-nav-rounded: false; 97 | $border-color: $gray-lighter; 98 | $hr-color: $secondary; 99 | $btn-font-size: $font-size-xs !default; 100 | $btn-font-size-sm: $font-size-xs; 101 | $btn-close-width: .75em !default; 102 | $btn-close-padding-x: .5em !default; 103 | $btn-box-shadow: false; 104 | // Dropdowns 105 | $btn-active-box-shadow: none; 106 | // Modals 107 | $modal-inner-padding: .4rem; 108 | $modal-header-padding-x: .8rem; 109 | $modal-header-padding-y: .5rem; 110 | $modal-backdrop-opacity: 0.5; 111 | $modal-content-bg: lighten($gray-lighter, 1%); 112 | // Inputs 113 | $input-bg: $white; 114 | $input-color: $custom-color; 115 | $input-font-size: $font-size-sm; 116 | $input-font-size-sm: $font-size-xs; 117 | $input-box-shadow: none !important; 118 | $input-focus-box-shadow: none !important; 119 | $input-disabled-color: $muted; 120 | // Custom 121 | $top-bar-height: 40px; 122 | $bottom-bar-height: $top-bar-height; 123 | -------------------------------------------------------------------------------- /core/components/handlers/loggers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { createLogger, format, transports } from 'winston' 8 | import { HAS_TTY, IS_DEV_ENV, IS_PROD_ENV, MAIN_LOGS_FILE, SYNC_LOGS_PATH } from '../../constants' 9 | import path from 'node:path' 10 | import { SYMBOLS } from '../constants/handlers' 11 | import type { SyncTransferContext } from '../interfaces/sync-transfer.interface' 12 | 13 | export const LOG_MODULE_SYNC = 'Transfer' 14 | export const LOG_MODULE_REPORT = 'Report' 15 | export const LOG_LEVEL_SYNC = 'sync' 16 | export const LOG_LEVEL_REPORT = 'report' 17 | 18 | const levels = { 19 | levels: { 20 | error: 0, 21 | warn: 1, 22 | sync: 2, 23 | info: 3, 24 | report: 4, 25 | debug: 5, 26 | silly: 6 27 | }, 28 | colors: { 29 | error: 'red', 30 | warn: 'yellow', 31 | sync: 'blue', 32 | report: 'green', 33 | info: 'cyan', 34 | debug: 'magenta', 35 | silly: 'gray' 36 | } 37 | } 38 | 39 | const dateFormat = { format: 'YYYY-MM-DD HH:mm:ss' } 40 | 41 | const checkTransfer = format((info: any) => { 42 | if (info.message.tasks) { 43 | const i = info.message 44 | info.message = 45 | `[${i.tasks.done}/${i.tasks.count}]${SYMBOLS[i.tr.side]} ${i.tr.action} - ` + 46 | `${i.tr.file}${i.tr.fileDst ? ` -> ${i.tr.fileDst}` : ''}` + 47 | `${i.tr.error ? `: ${i.tr.error}` : ''}` 48 | } 49 | return info 50 | }) 51 | 52 | function formatInfo(info: any) { 53 | return `${info.timestamp} [${info.level}] [${info.module}] ${info.sync ? `[${info.sync.server.name}:${info.sync.path.name}]` : ''} ${info.message}` 54 | } 55 | 56 | const consoleFormat = format.combine( 57 | checkTransfer(), 58 | format.timestamp(dateFormat), 59 | format.colorize({ all: true, colors: levels.colors }), 60 | format.splat(), 61 | format.align(), 62 | format.printf((info) => formatInfo(info)) 63 | ) 64 | 65 | const cmdFileFormat = format.combine( 66 | checkTransfer(), 67 | format.timestamp(dateFormat), 68 | format.splat(), 69 | format.printf((info) => formatInfo(info)) 70 | ) 71 | 72 | const transfersFileFormat = format.combine( 73 | format.timestamp(dateFormat), 74 | format.splat(), 75 | format.printf((info: any) => JSON.stringify({ timestamp: info.timestamp, ...info.message.tr })) 76 | ) 77 | 78 | const handlers: any = { 79 | console: HAS_TTY ? new transports.Console({ level: 'report', format: consoleFormat }) : null, 80 | file: IS_PROD_ENV 81 | ? new transports.File({ level: 'info', filename: MAIN_LOGS_FILE, maxsize: 10485760, tailable: true, maxFiles: 1, format: cmdFileFormat }) 82 | : null 83 | } 84 | 85 | const logger = createLogger({ levels: levels.levels, transports: Object.values(handlers).filter((h) => h !== null) as any }) 86 | 87 | function getSyncLogger(sync: SyncTransferContext) { 88 | return createLogger({ 89 | defaultMeta: { module: LOG_MODULE_SYNC, sync: sync }, 90 | levels: levels.levels, 91 | transports: [ 92 | HAS_TTY ? new transports.Console({ level: 'info', format: consoleFormat }) : null, 93 | new transports.File({ 94 | level: LOG_LEVEL_SYNC, 95 | filename: `${path.join(SYNC_LOGS_PATH, `${sync.server.id}_${sync.path.id}.log`)}`, 96 | maxsize: 10485760, 97 | tailable: true, 98 | maxFiles: 1, 99 | format: transfersFileFormat 100 | }) 101 | ].filter((t) => t !== null) 102 | }) 103 | } 104 | 105 | export function getLogger(module: string, sync: SyncTransferContext = null, storeTransfersLogs = false) { 106 | if (storeTransfersLogs) { 107 | return getSyncLogger(sync) 108 | } 109 | return logger.child({ module, sync }) 110 | } 111 | 112 | export function setLevelLogger(level) { 113 | for (const handler of Object.values(handlers).filter((h) => h !== null) as any) { 114 | handler.level = level 115 | } 116 | } 117 | 118 | if (IS_DEV_ENV) { 119 | setLevelLogger('debug') 120 | } 121 | -------------------------------------------------------------------------------- /main/components/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | import mime from 'mime-types' 7 | 8 | import type { SyncTransfer } from '@sync-in-desktop/core/components/interfaces/sync-transfer.interface' 9 | 10 | const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 11 | 12 | export const throttleFunc = (context: any, func: (args?) => void, delay: number) => { 13 | let lastFunc 14 | let lastRan 15 | return (...args) => { 16 | if (!lastRan) { 17 | func.apply(context, args) 18 | lastRan = Date.now() 19 | } else { 20 | clearTimeout(lastFunc) 21 | lastFunc = setTimeout( 22 | function () { 23 | if (Date.now() - lastRan >= delay) { 24 | func.apply(context, args) 25 | lastRan = Date.now() 26 | } 27 | }, 28 | delay - (Date.now() - lastRan) 29 | ) 30 | } 31 | } 32 | } 33 | 34 | export function bytesToHuman(bytes: number, asDict: true, precision?: number, perSecond?: boolean): { value: number; unit: string } 35 | export function bytesToHuman(bytes: number, asDict: false, precision?: number, perSecond?: boolean): string 36 | export function bytesToHuman(bytes: number, asDict?: boolean, precision?: number, perSecond?: boolean): { value: number; unit: string } 37 | export function bytesToHuman( 38 | bytes: number, 39 | asDict: boolean = true, 40 | precision: number = 2, 41 | perSecond: boolean = false 42 | ): 43 | | { 44 | value: number 45 | unit: string 46 | } 47 | | string { 48 | let value: number 49 | let unit: string 50 | if (bytes === 0) { 51 | value = 0 52 | unit = 'B' 53 | } else { 54 | const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1) 55 | value = parseFloat((bytes / Math.pow(1024, Math.floor(exponent))).toFixed(precision)) 56 | unit = units[exponent] 57 | } 58 | if (perSecond) { 59 | unit = `${unit}/s` 60 | } 61 | if (asDict) { 62 | return { value, unit } 63 | } 64 | return `${value} ${unit}` 65 | } 66 | 67 | export function bytesToUnit(bytes: number, unit: string, precision = 2): number { 68 | const exponent = units.indexOf(unit) 69 | return parseFloat((bytes / Math.pow(1024, Math.floor(exponent))).toFixed(precision)) 70 | } 71 | 72 | export function setMimeType(tr: SyncTransfer): SyncTransfer { 73 | if (tr.isDir) { 74 | tr.mime = 'directory' 75 | } else { 76 | tr.mime = mime.lookup(tr.file) || 'file' 77 | if (tr.mime !== 'file') { 78 | tr.mime = tr.mime.replace('/', '-') 79 | } 80 | } 81 | return tr 82 | } 83 | 84 | export function sortObjsByDate(objs: any[], property: string, asc = false) { 85 | objs.sort((a, b) => { 86 | const dA = new Date(a[property]).getTime() 87 | const dB = new Date(b[property]).getTime() 88 | if (isNaN(dA)) { 89 | return 1 90 | } else if (isNaN(dB)) { 91 | return -1 92 | } else if (dA === dB) { 93 | return 0 94 | } 95 | if (asc) { 96 | return dA > dB ? 1 : -1 97 | } else { 98 | return dB > dA ? 1 : -1 99 | } 100 | }) 101 | } 102 | 103 | export function compareVersions(v1: string, v2: string): number { 104 | const v1Parts: number[] = v1.split('.').map(Number) 105 | const v2Parts: number[] = v2.split('.').map(Number) 106 | 107 | for (let i = 0; i < 3; i++) { 108 | if (v1Parts[i] < v2Parts[i]) return -1 109 | if (v1Parts[i] > v2Parts[i]) return 1 110 | } 111 | return 0 112 | } 113 | 114 | export function findOldestVersion(versions: string[]): string { 115 | return versions.reduce((oldest, current) => { 116 | return compareVersions(oldest, current) <= 0 ? oldest : current 117 | }) 118 | } 119 | 120 | export function getReleaseOS(): 'mac' | 'linux' | 'win' { 121 | switch (process.platform) { 122 | case 'darwin': 123 | return 'mac' 124 | case 'win32': 125 | return 'win' 126 | case 'linux': 127 | return 'linux' 128 | default: 129 | throw new Error(`Unable to determine release os : ${process.platform}`) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /renderer/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "renderer": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular/build:application", 22 | "options": { 23 | "outputPath": { 24 | "base": "../dist/renderer", 25 | "browser": "" 26 | }, 27 | "index": "src/index.html", 28 | "browser": "src/main.ts", 29 | "polyfills": [ 30 | "zone.js" 31 | ], 32 | "tsConfig": "tsconfig.app.json", 33 | "inlineStyleLanguage": "scss", 34 | "stylePreprocessorOptions": { 35 | "sass": { 36 | "silenceDeprecations": [ 37 | "mixed-decls", 38 | "color-functions", 39 | "global-builtin", 40 | "import" 41 | ] 42 | } 43 | }, 44 | "assets": [ 45 | "src/favicon.ico", 46 | "src/assets" 47 | ], 48 | "styles": [ 49 | "src/scss/styles.scss" 50 | ], 51 | "scripts": [] 52 | }, 53 | "configurations": { 54 | "production": { 55 | "budgets": [ 56 | { 57 | "type": "initial", 58 | "maximumWarning": "2mb", 59 | "maximumError": "5mb" 60 | }, 61 | { 62 | "type": "anyComponentStyle", 63 | "maximumWarning": "2kb", 64 | "maximumError": "4kb" 65 | } 66 | ], 67 | "outputHashing": "all" 68 | }, 69 | "development": { 70 | "watch": true, 71 | "optimization": false, 72 | "extractLicenses": false, 73 | "sourceMap": true, 74 | "namedChunks": true 75 | } 76 | }, 77 | "defaultConfiguration": "production" 78 | }, 79 | "serve": { 80 | "builder": "@angular/build:dev-server", 81 | "configurations": { 82 | "production": { 83 | "buildTarget": "renderer:build:production" 84 | }, 85 | "development": { 86 | "buildTarget": "renderer:build:development" 87 | } 88 | }, 89 | "defaultConfiguration": "development" 90 | }, 91 | "extract-i18n": { 92 | "builder": "@angular/build:extract-i18n", 93 | "options": { 94 | "buildTarget": "renderer:build" 95 | } 96 | }, 97 | "lint": { 98 | "builder": "@angular-eslint/builder:lint", 99 | "options": { 100 | "lintFilePatterns": [ 101 | "src/**/*.ts", 102 | "src/**/*.html" 103 | ] 104 | } 105 | } 106 | } 107 | } 108 | }, 109 | "cli": { 110 | "analytics": false, 111 | "schematicCollections": [ 112 | "@angular-eslint/schematics" 113 | ] 114 | }, 115 | "schematics": { 116 | "@schematics/angular:component": { 117 | "type": "component" 118 | }, 119 | "@schematics/angular:directive": { 120 | "type": "directive" 121 | }, 122 | "@schematics/angular:service": { 123 | "type": "service" 124 | }, 125 | "@schematics/angular:guard": { 126 | "typeSeparator": "." 127 | }, 128 | "@schematics/angular:interceptor": { 129 | "typeSeparator": "." 130 | }, 131 | "@schematics/angular:module": { 132 | "typeSeparator": "." 133 | }, 134 | "@schematics/angular:pipe": { 135 | "typeSeparator": "." 136 | }, 137 | "@schematics/angular:resolver": { 138 | "typeSeparator": "." 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [1.6.0](https://git.sync-in.org/sync-in/desktop/compare/v1.5.1...v1.6.0) (2025-12-07) 3 | 4 | 5 | ### Features 6 | 7 | * **settings:** add "Hide Dock Icon" option on macOS and improve tray accessibility ([59261c5](https://github.com/Sync-in/desktop/commit/59261c5e17ae4af1c258789eb82f7f65ece99178)) 8 | 9 | ## [1.5.1](https://git.sync-in.org/sync-in/desktop/compare/v1.5.0...v1.5.1) (2025-11-25) 10 | 11 | 12 | * **assets:** update app and tray icons, favicon, and related assets ([b302757](https://github.com/Sync-in/desktop/commit/b3027574c5aba3ee93819a6f84214331f80ef09e)) 13 | * **deps:** update dependencies in package-lock.json ([ed28649](https://github.com/Sync-in/desktop/commit/ed286497eadfde4df978c7a5287d893d789e93d8)) 14 | * **manifest:** add server release manifest generation ([1128087](https://github.com/Sync-in/desktop/commit/112808723d7afda9068f21348a2990b0c4d853f4)) 15 | * **updater:** extend user-agent header support and adjust update check interval to 8 hours ([a173c77](https://github.com/Sync-in/desktop/commit/a173c77bfd0e4f6718cc39585797197bb3283e9d)) 16 | 17 | ## [1.5.0](https://git.sync-in.org/sync-in/desktop/compare/v1.4.0...v1.5.0) (2025-10-26) 18 | 19 | ### Highlights 20 | 21 | * 🌍 **14 languages supported** — added 12 new ones: 🇩🇪 🇪🇸 🇵🇹 🇧🇷 🇮🇹 🇨🇳 🇮🇳 🇹🇷 🇯🇵 🇰🇷 🇵🇱 🇷🇺 22 | 23 | ### Features 24 | 25 | * **i18n:** add Japanese, Brazilian Portuguese, Polish, Korean, and Turkish translations ([443b898](https://github.com/Sync-in/desktop/commit/443b898515bd8d255fd7edc0379c8f7155242ffb)) 26 | * **i18n:** add Russian translations ([82b941d](https://github.com/Sync-in/desktop/commit/82b941d4ba0bfff5c7fabdee4575d7d627f5a250)) 27 | * **i18n:** add support for multiple languages and optimize unused translation keys detection ([85e527c](https://github.com/Sync-in/desktop/commit/85e527cdf2371d90a88527676981130ecbe72a73)) 28 | * **i18n:** enhance i18n setup with dynamic translation loading and locale resolution ([a07d8c6](https://github.com/Sync-in/desktop/commit/a07d8c6c68c74c5adfbd7dd2fda9b1321563f032)) 29 | * **i18n:** modularize localization setup and extend language support ([8d5eac6](https://github.com/Sync-in/desktop/commit/8d5eac64f8c30c56e83490e3eb64d53829b84e56)) 30 | * **i18n:** refactor i18n manager and add language normalization logic ([04fb872](https://github.com/Sync-in/desktop/commit/04fb872037da70a0a9eeafc71d0a0fcf6b24ae84)) 31 | 32 | ## [1.4.0](https://git.sync-in.org/sync-in/desktop/compare/v1.3.0...v1.4.0) (2025-09-26) 33 | 34 | 35 | ### Features 36 | 37 | * **auth:** add support for MFA authentication ([544ec08](https://github.com/Sync-in/desktop/commit/544ec08fc77a4577f81f9a49d2beed853d034a09)) 38 | * **main:** open external URLs in the user's browser instead of inside the app ([f3dda3d](https://github.com/Sync-in/desktop/commit/f3dda3db5e32fcd198208a12aa4454577b0e55ea)) 39 | 40 | ## [1.3.0](https://git.sync-in.org/sync-in/desktop/compare/v1.2.9...v1.3.0) (2025-09-06) 41 | 42 | 43 | ### Features 44 | 45 | * **main:** add options to launch app at login and to start hidden ([286a4e1](https://github.com/Sync-in/desktop/commit/286a4e1cf6ccbe32dfc0703a2925fbeca9aaa993)) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * **core:** prevent infinite loop when adding unreachable server ([baeca22](https://github.com/Sync-in/desktop/commit/baeca22ceeec42c6f09d5562e47e7b8189988443)) 51 | * **main:** apply zoomFactor to active BrowserView only ([228adb7](https://github.com/Sync-in/desktop/commit/228adb75ff92db6800ad8fa4333ca721d96d498c)) 52 | * **main:** avoid wrong notification on canceled downloads ([38df616](https://github.com/Sync-in/desktop/commit/38df6168bdf62eebd4b4bbe883254b81783b7825)) 53 | * **main:** correct webview overflow by snapping bounds to display scale ([7eac291](https://github.com/Sync-in/desktop/commit/7eac2914846db25684bd5e1aca9e467a4f3433fc)) 54 | * **main:** force webView.webContents.setZoomFactor(1) ([4db872d](https://github.com/Sync-in/desktop/commit/4db872d5f6090d9bf926e85f21f4606f33cb2f39)) 55 | * **main:** stop intercepting system Ctrl/Cmd+arrow keys globally ([7e01470](https://github.com/Sync-in/desktop/commit/7e01470baf5f4541c7c6b5506d5cccdfe64912e5)) 56 | * **main:** webView.webContents.setZoomFactor(1) only works after loading content ([4ae21ba](https://github.com/Sync-in/desktop/commit/4ae21ba8d36a2c595a04b5bee220e2fecbe76c0d)) 57 | * **renderer:** correct server dropdown position ([4aa1f82](https://github.com/Sync-in/desktop/commit/4aa1f821b9b09b84c295d579a0a119a0a30c632d)) 58 | -------------------------------------------------------------------------------- /webpack.config.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | import path from 'node:path' 7 | import { fileURLToPath } from 'url' 8 | import { dirname } from 'path' 9 | import TerserPlugin from 'terser-webpack-plugin' 10 | import ESLintWebpackPlugin from 'eslint-webpack-plugin' 11 | import CopyPlugin from 'copy-webpack-plugin' 12 | import ShebangPlugin from 'webpack-shebang-plugin' 13 | import pkg from './package.json' with { type: 'json' } 14 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin' 15 | 16 | const __filename = fileURLToPath(import.meta.url) 17 | const __dirname = dirname(__filename) 18 | const version = pkg.version 19 | const isProduction = process.env.NODE_ENV !== 'development' 20 | 21 | export default [ 22 | { 23 | mode: isProduction ? 'production' : 'development', 24 | devtool: isProduction ? undefined : 'inline-source-map', 25 | name: 'app', 26 | target: 'electron-main', 27 | entry: { 28 | main: path.join(__dirname, 'main', 'main.ts'), 29 | preload: path.join(__dirname, 'main', 'preload.ts') 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.ts$/, 35 | include: [path.resolve(__dirname, 'main'), path.resolve(__dirname, 'core'), path.resolve(__dirname, 'i18n')], 36 | exclude: /node_modules/, 37 | use: { 38 | loader: 'swc-loader', 39 | options: { 40 | jsc: { parser: { syntax: 'typescript' }, target: 'es2022' } 41 | } 42 | } 43 | } 44 | ] 45 | }, 46 | optimization: { 47 | minimize: true, 48 | minimizer: [ 49 | new TerserPlugin({ 50 | terserOptions: { 51 | format: { 52 | comments: false 53 | } 54 | }, 55 | extractComments: false 56 | }) 57 | ] 58 | }, 59 | plugins: [ 60 | new ForkTsCheckerWebpackPlugin({ async: !isProduction }), 61 | new CopyPlugin({ 62 | patterns: [ 63 | { 64 | from: './main/assets', 65 | to: 'assets' 66 | }, 67 | { 68 | from: './i18n/*.json', 69 | to: '' 70 | } 71 | ] 72 | }), 73 | new ESLintWebpackPlugin({ 74 | failOnError: isProduction, 75 | extensions: ['ts'], 76 | files: ['main/**', 'core/**'], 77 | exclude: ['node_modules', path.resolve(__dirname, 'renderer'), path.resolve(__dirname, 'cli')] 78 | }) 79 | ], 80 | resolve: { extensions: ['.ts', '.js'] }, 81 | output: { 82 | path: path.resolve(__dirname, 'dist', 'main') 83 | } 84 | }, 85 | { 86 | mode: isProduction ? 'production' : 'development', 87 | devtool: isProduction ? undefined : 'inline-source-map', 88 | name: 'cli', 89 | target: 'node', 90 | entry: path.join(__dirname, 'cli', 'main.ts'), 91 | module: { 92 | rules: [ 93 | { 94 | test: /\.ts$/, 95 | exclude: /node_modules/, 96 | use: { 97 | loader: 'swc-loader', 98 | options: { 99 | jsc: { parser: { syntax: 'typescript' }, target: 'es2022' } 100 | } 101 | } 102 | } 103 | ] 104 | }, 105 | optimization: { 106 | minimize: true, 107 | minimizer: [ 108 | new TerserPlugin({ 109 | terserOptions: { 110 | format: { 111 | comments: false 112 | } 113 | }, 114 | extractComments: false 115 | }) 116 | ] 117 | }, 118 | plugins: [ 119 | new ForkTsCheckerWebpackPlugin({ async: !isProduction }), 120 | new ESLintWebpackPlugin({ 121 | failOnError: isProduction, 122 | files: ['cli/**', 'core/**'], 123 | extensions: ['ts'], 124 | exclude: ['node_modules', path.resolve(__dirname, 'renderer'), path.resolve(__dirname, 'main')] 125 | }), 126 | new ShebangPlugin() 127 | ], 128 | resolve: { extensions: ['.ts', '.js'] }, 129 | output: { 130 | path: path.resolve(__dirname, 'releases', 'sync-in-cli'), 131 | filename: `sync-in-cli-${version}.js` 132 | }, 133 | ignoreWarnings: [{ module: /yargs/ }] 134 | } 135 | ] 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sync-in 5 | 6 | 7 | 8 | _Welcome to the official Sync-in desktop client repository!_ 9 | 10 | - 🌍 [Website](https://sync-in.com) 11 | - 📄 [Documentation](https://sync-in.com/docs) 12 | - 📥 [Download the desktop app](https://sync-in.com/downloads) 13 | 14 | License 15 | Discord 16 | 17 | The client for the **Sync-in** platform, bringing a complete local experience with advanced file sync and 18 | server management features. 19 | 20 | Available as a **desktop application** (Windows, macOS, Linux) and a **command line interface (CLI)**, Sync-in adapts to 21 | your workflow — whether on a personal workstation, a Linux server, or a headless environment... 22 | 23 | 24 | 25 | Sync-in 26 | 27 | 28 | --- 29 | 30 | ## 🚀 Features 31 | 32 | ### 💻 Desktop Application 33 | 34 | - ⚙️ **Full feature parity with the web version** 35 | - 🌐 **Multi-server support**: connect and manage multiple Sync-in servers 36 | - 🔁 **Local file synchronization** 37 | - One-way sync (push or pull) 38 | - Two-way sync 39 | - 🛡️ **Two sync modes**: 40 | - **Secure**: checksum-based for maximum integrity 41 | - **Fast**: metadata-based (date, size) for improved performance 42 | - 📆 **Plannable syncs**: schedule automatic synchronizations 43 | - 🎯 **Custom filters**: exclude specific files/folders from sync 44 | - 🧪 **Sync simulation**: preview what will happen before syncing 45 | - 📥 **Integrated download manager** offers a unified experience. 46 | - 👀 **Transfer visualization**: view real-time transfer logs 47 | - 🤖 **Smart assistant**: step-by-step sync configuration wizard 48 | - 🔄 **Automatic updates**: the application keeps itself up to date in the background 49 | 50 | ### 📟 Command Line Interface (CLI) 51 | 52 | - Works on **any Linux server** 53 | - Sync files to/from any Sync-in server 54 | - Perfect for automation, headless servers, cron jobs, or scripts 55 | 56 | --- 57 | 58 | ### 🧩 Project Resources 59 | 60 | - 🌍 **Official website**: [https://sync-in.com](https://sync-in.com) 61 | - 📖 **Documentation**: [https://sync-in.com/docs](https://sync-in.com/docs) 62 | - 🗄️ **Server** : [https://github.com/Sync-in/server](https://github.com/Sync-in/server) 63 | - 📟 **Desktop & CLI** - Cross-platform desktop app and command-line interface : [https://github.com/Sync-in/desktop](https://github.com/Sync-in/desktop) 64 | - 🐳 **Docker** - Setup & Deployment Guide : [https://sync-in.com/docs/setup-guide/docker](https://sync-in.com/docs/setup-guide/docker) 65 | 66 | --- 67 | 68 | ## 💛 Support 69 | 70 | Sync-in is an independent open source project. 71 | If you find it useful, you can: 72 | 73 | - ⭐ Star the repositories 74 | - 🐛 Report issues and suggest improvements 75 | - 🤝 Contribute code, translations, or documentation 76 | - 💬 Join the community on : 77 | - [GitHub Discussions](https://github.com/Sync-in/desktop/discussions) 78 | - [Discord](https://discord.gg/qhJyzwaymT) 79 | - 💖 Support the project ! 80 | - [GitHub Sponsors](https://github.com/sponsors/Sync-in) 81 | - [Other ways to support](https://sync-in.com/support) 82 | 83 | --- 84 | 85 | ## 🤝 Contributing 86 | Before submitting your pull request, please confirm the following: 87 | 88 | - ✅ I have read and followed the [contribution guide](CONTRIBUTING.md). 89 | - ✅ I am submitting this pull request in good faith and to help improve Sync-in. 90 | 91 | --- 92 | 93 | ## 📜 License 94 | This project is licensed under the **GNU Affero General Public License (AGPL-3.0-or-later)**. 95 | See [LICENSE](LICENSE) for the full text. 96 | 97 | Sync-in® is a registered trademark, see our [Trademark Policy](https://sync-in.com/trademark). 98 | 99 | --- 100 | 101 | _Thank you for using **Sync-in** ! 🚀_ -------------------------------------------------------------------------------- /renderer/src/i18n/l10n.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | import { inject, Injectable } from '@angular/core' 7 | import { 8 | getBrowserLanguage, 9 | getSchema, 10 | L10N_CONFIG, 11 | L10nConfig, 12 | L10nFormat, 13 | L10nLocale, 14 | L10nLocaleResolver, 15 | L10nStorage, 16 | L10nTranslationLoader 17 | } from 'angular-l10n' 18 | import { i18nLocaleSupported, LANG_SUPPORTED, normalizeLanguage } from '../../../i18n' 19 | import { catchError, from, map, Observable, of } from 'rxjs' 20 | import { BsLocaleService } from 'ngx-bootstrap/datepicker' 21 | import { loadDayjsLocale } from './lib/dayjs.i18n' 22 | import { loadBootstrapLocale } from './lib/bs.i18n' 23 | 24 | export const STORAGE_SESSION_KEY = 'locale' as const 25 | export const LANG_FORMAT: L10nFormat = 'language-region' as const 26 | 27 | export const l10nConfig: L10nConfig & { 28 | schema: { locale: { language: i18nLocaleSupported }; dir: 'ltr' | 'rtl' }[] 29 | } = { 30 | format: LANG_FORMAT, 31 | providers: [{ name: 'app', asset: 'app' }], 32 | fallback: false, 33 | cache: true, 34 | keySeparator: '|', 35 | defaultLocale: { language: 'en' }, 36 | schema: [ 37 | { locale: { language: 'de' }, dir: 'ltr' }, 38 | { locale: { language: 'en' }, dir: 'ltr' }, 39 | { locale: { language: 'es' }, dir: 'ltr' }, 40 | { locale: { language: 'fr' }, dir: 'ltr' }, 41 | { locale: { language: 'hi' }, dir: 'ltr' }, 42 | { locale: { language: 'it' }, dir: 'ltr' }, 43 | { locale: { language: 'ja' }, dir: 'ltr' }, 44 | { locale: { language: 'ko' }, dir: 'ltr' }, 45 | { locale: { language: 'pl' }, dir: 'ltr' }, 46 | { locale: { language: 'pt' }, dir: 'ltr' }, 47 | { locale: { language: 'pt-BR' }, dir: 'ltr' }, 48 | { locale: { language: 'ru' }, dir: 'ltr' }, 49 | { locale: { language: 'tr' }, dir: 'ltr' }, 50 | { locale: { language: 'zh' }, dir: 'ltr' } 51 | ] 52 | } 53 | 54 | @Injectable({ providedIn: 'root' }) 55 | export class TranslationStorage implements L10nStorage { 56 | private readonly hasStorage = typeof Storage !== 'undefined' 57 | 58 | async read(): Promise { 59 | if (!this.hasStorage) return null 60 | let stored: L10nLocale | null = null 61 | const raw = sessionStorage.getItem(STORAGE_SESSION_KEY) 62 | if (raw) { 63 | try { 64 | stored = JSON.parse(raw) 65 | } catch (e) { 66 | console.warn('Invalid locale in sessionStorage, resetting.', e) 67 | sessionStorage.removeItem(STORAGE_SESSION_KEY) 68 | } 69 | } 70 | const lang = stored?.language 71 | const isSupported = !!lang && LANG_SUPPORTED.has(lang as i18nLocaleSupported) 72 | if (!isSupported) { 73 | sessionStorage.removeItem(STORAGE_SESSION_KEY) 74 | return null 75 | } 76 | return stored 77 | } 78 | 79 | async write(locale: L10nLocale): Promise { 80 | if (!this.hasStorage) return 81 | try { 82 | const value = JSON.stringify(locale) 83 | sessionStorage.setItem(STORAGE_SESSION_KEY, value) 84 | } catch (e) { 85 | console.warn('Failed to write locale to sessionStorage:', e) 86 | } 87 | } 88 | } 89 | 90 | @Injectable() 91 | export class TranslationLoader implements L10nTranslationLoader { 92 | private readonly bsLocale = inject(BsLocaleService) 93 | 94 | get(language: string): Observable> { 95 | if (language) { 96 | loadBootstrapLocale(language) 97 | loadDayjsLocale(language).catch(console.error) 98 | this.bsLocale.use(language.toLowerCase()) 99 | } else { 100 | return of({}) 101 | } 102 | // Dynamically load the JSON file for the requested language 103 | return from(import(`../../../i18n/${language}.json`)).pipe( 104 | map((module: any) => module?.default ?? module ?? {}), 105 | catchError(() => of({})) 106 | ) 107 | } 108 | } 109 | 110 | @Injectable() 111 | export class TranslateLocaleResolver implements L10nLocaleResolver { 112 | private readonly config = inject(L10N_CONFIG) 113 | 114 | public async get(): Promise { 115 | const browserLanguage = normalizeLanguage(getBrowserLanguage(LANG_FORMAT)) 116 | if (browserLanguage) { 117 | const schema = getSchema(this.config.schema, browserLanguage, LANG_FORMAT) 118 | if (schema) { 119 | return Promise.resolve(schema.locale) 120 | } 121 | } 122 | return Promise.resolve(null) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /cli/cmds/cmd-servers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { Argv, CommandModule } from 'yargs' 8 | import { ServersManager } from '../../core/components/handlers/servers' 9 | import { Server } from '../../core/components/models/server' 10 | 11 | const serverLS: CommandModule = { 12 | command: 'list', 13 | aliases: ['ls'], 14 | describe: 'List all servers', 15 | handler: () => { 16 | for (const server of ServersManager.list) { 17 | console.log(server.toString()) 18 | } 19 | } 20 | } 21 | const serverADD: CommandModule = { 22 | command: 'add', 23 | aliases: ['mk'], 24 | describe: 'Add and register a server', 25 | builder: { 26 | name: { 27 | alias: 'n', 28 | describe: 'Given name to identify the server', 29 | demandOption: true, 30 | type: 'string' 31 | }, 32 | url: { 33 | alias: 'w', 34 | describe: 'Server URL', 35 | demandOption: true, 36 | type: 'string' 37 | }, 38 | login: { 39 | alias: 'u', 40 | describe: 'Username or email', 41 | demandOption: true, 42 | type: 'string' 43 | }, 44 | password: { 45 | alias: 'p', 46 | describe: 'User password', 47 | demandOption: true, 48 | type: 'string' 49 | }, 50 | code: { 51 | alias: 'c', 52 | describe: 'Two-Fa Authentication Code', 53 | demandOption: false, 54 | type: 'string' 55 | } 56 | }, 57 | handler: async (argv: any) => { 58 | const server = new Server({ name: argv.name, url: argv.url }) 59 | console.log('Adding the server') 60 | const manager = new ServersManager(server, false) 61 | await manager.checkUpdatedProperties(server) 62 | const [ok, msg] = await manager.add(argv.login, argv.password, argv.code) 63 | if (ok) { 64 | console.log('Server authentication & registration OK') 65 | console.log(server.toString()) 66 | } else { 67 | console.error(`The server was not registered: ${msg}`) 68 | } 69 | } 70 | } 71 | const serverAUTH: CommandModule = { 72 | command: 'auth', 73 | aliases: ['touch'], 74 | describe: 'Re-authenticate on a server', 75 | builder: { 76 | server: { 77 | alias: 's', 78 | describe: 'Given id or name to identify the server', 79 | demandOption: true, 80 | type: 'string' 81 | }, 82 | login: { 83 | alias: 'u', 84 | describe: 'Username or email', 85 | demandOption: true, 86 | type: 'string' 87 | }, 88 | password: { 89 | alias: 'p', 90 | describe: 'User password', 91 | demandOption: true, 92 | type: 'string' 93 | }, 94 | code: { 95 | alias: 'c', 96 | describe: 'Two-Fa Authentication Code', 97 | demandOption: false, 98 | type: 'string' 99 | } 100 | }, 101 | handler: async (argv: any) => { 102 | const server = ServersManager.find(argv.server) 103 | console.log(server.toString()) 104 | const manager = new ServersManager(server, false) 105 | try { 106 | await manager.register(argv.login, argv.password, argv.code) 107 | ServersManager.saveSettings() 108 | console.log('The token has been updated') 109 | } catch (e) { 110 | console.log(`The token has not been updated: ${e}`) 111 | } 112 | } 113 | } 114 | const serverRM: CommandModule = { 115 | command: 'remove', 116 | aliases: ['rm'], 117 | describe: 'Remove a server', 118 | builder: { 119 | server: { 120 | alias: 's', 121 | describe: 'Given id or name to identify the server', 122 | demandOption: true 123 | } 124 | }, 125 | handler: async (argv: any) => { 126 | const server = ServersManager.find(argv.server) 127 | console.log(server.toString()) 128 | const status = await ServersManager.unregister(server) 129 | if (status.ok) { 130 | console.log('The server has been removed') 131 | } else { 132 | console.log(`The server has not been deleted : ${status.msg}`) 133 | } 134 | } 135 | } 136 | 137 | export function getServerCMD(yargs) { 138 | return { 139 | command: 'servers', 140 | describe: 'Manage servers', 141 | builder: (yargs): Argv[] => [ 142 | yargs.command(serverLS).help(false).version(false), 143 | yargs.command(serverADD).help(false).version(false), 144 | yargs.command(serverAUTH).help(false).version(false), 145 | yargs.command(serverRM).help(false).version(false) 146 | ], 147 | handler: () => yargs.showHelp() 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /renderer/src/app/components/bottom-bar-downloads.component.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | @if (downloads.length) { 8 |
9 | @if (dropdownView.state === DOWNLOAD_STATE.PROGRESSING) { 10 | 11 | } 12 | 36 | 88 |
89 | } 90 | 91 | -------------------------------------------------------------------------------- /renderer/src/app/components/modal-server.component.html: -------------------------------------------------------------------------------- 1 | 6 | 18 | 110 | 122 | -------------------------------------------------------------------------------- /core/components/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2025 Johan Legrand 3 | * This file is part of Sync-in | The open source file sync and share solution 4 | * See the LICENSE file for licensing details 5 | */ 6 | 7 | import { RequestsManager } from './handlers/requests' 8 | import { SyncPath } from './models/syncpath' 9 | import { Server } from './models/server' 10 | import { FilesParser } from './handlers/parser' 11 | import { DiffParser } from './handlers/diff' 12 | import { Sync } from './handlers/sync' 13 | import { Report } from './handlers/report' 14 | import { EventEmitter } from 'events' 15 | import { CORE, coreEvents } from './handlers/events' 16 | import { getLogger } from './handlers/loggers' 17 | import { SyncInstance } from './models/sync' 18 | import { SyncStatus } from './interfaces/sync-status.interface' 19 | 20 | export class SyncManager { 21 | private readonly server: Server 22 | private readonly appEvents: EventEmitter 23 | private syncs: SyncInstance[] = [] 24 | private mustExit = false 25 | 26 | constructor(server: Server, appEvents: EventEmitter = null) { 27 | this.server = server 28 | this.appEvents = appEvents 29 | this.checkBeforeStop = this.checkBeforeStop.bind(this) 30 | this.listeners() 31 | } 32 | 33 | private listeners() { 34 | coreEvents.once(CORE.EXIT, this.checkBeforeStop) 35 | coreEvents.on(CORE.SYNC_STOP, this.checkBeforeStop) 36 | } 37 | 38 | async run(syncPaths: SyncPath[], reportOnly = false, async = false) { 39 | if (async) { 40 | await Promise.all(syncPaths.map((syncPath: SyncPath) => this.runSync(syncPath, reportOnly))) 41 | } else { 42 | for (const syncPath of syncPaths) { 43 | if (this.mustExit) { 44 | break 45 | } 46 | await this.runSync(syncPath, reportOnly) 47 | } 48 | } 49 | this.removeListeners() 50 | } 51 | 52 | async runSync(syncPath: SyncPath, reportOnly = false) { 53 | const logger = getLogger(reportOnly ? 'Report' : 'Sync', { server: this.server.identity(), path: syncPath.identity() }) 54 | logger.info(`${syncPath.localPath} ${syncPath.symbol} ${syncPath.remotePath}`) 55 | logger.info( 56 | `{FirstSync: ${syncPath.firstSync}, DiffMode: ${syncPath.diffMode}, ConflictMode: ${syncPath.conflictMode}, ReportOnly: ${reportOnly}}` 57 | ) 58 | const req = new RequestsManager(this.server) 59 | const syncInstance = new SyncInstance(syncPath.id, req, reportOnly) 60 | this.syncs.push(syncInstance) 61 | this.sendSyncEvent({ serverId: this.server.id, syncPathId: syncPath.id, state: true, reportOnly: reportOnly }) 62 | try { 63 | await syncPath.checks() 64 | const parser = new FilesParser(syncPath, req, reportOnly, this.appEvents) 65 | syncInstance.instance = parser 66 | await parser.run() 67 | const diffParser = new DiffParser(syncPath, parser) 68 | if (reportOnly) { 69 | const report = new Report(diffParser, logger, this.appEvents) 70 | syncInstance.instance = report 71 | await report.run() 72 | } else { 73 | const sync = new Sync(req, diffParser, logger, this.appEvents) 74 | syncInstance.instance = sync 75 | await sync.run() 76 | await parser.saveSnapshots(sync.wasAborted) 77 | } 78 | } catch (e) { 79 | logger.error(e) 80 | syncPath.mainError = `${e}` 81 | if (typeof e !== 'string') { 82 | logger.error(e.stack) 83 | } 84 | } finally { 85 | req.cleanOnExit() 86 | this.syncs.splice(this.syncs.indexOf(syncInstance), 1) 87 | this.sendSyncEvent({ 88 | serverId: this.server.id, 89 | syncPathId: syncPath.id, 90 | state: false, 91 | reportOnly: reportOnly, 92 | mainError: syncPath.mainError, 93 | lastErrors: syncPath.lastErrors 94 | }) 95 | } 96 | } 97 | 98 | checkBeforeStop(sync: { server: number; paths: number[] } = null, reportOnly: boolean) { 99 | if (sync && sync.server === this.server.id) { 100 | // stop sync for specific server/path (or all paths) 101 | this.mustExit = !sync.paths.length 102 | for (const s of sync.paths.length 103 | ? this.syncs.filter((s: SyncInstance) => sync.paths.indexOf(s.syncPathId) > -1 && s.reportOnly === reportOnly) 104 | : this.syncs) { 105 | s.instance.logger.warn('Stop sync requested : %O', sync) 106 | s.instance.stop() 107 | s.req.cleanOnExit(true) 108 | } 109 | } else { 110 | // stop all syncs 111 | this.mustExit = true 112 | for (const s of this.syncs) { 113 | s.instance.logger.warn('Stop sync requested : %O', sync) 114 | s.instance.stop() 115 | s.req.cleanOnExit(true) 116 | } 117 | this.removeListeners() 118 | } 119 | } 120 | 121 | private sendSyncEvent(params: SyncStatus) { 122 | coreEvents.emit(CORE.SYNC_STATUS, params) 123 | } 124 | 125 | private removeListeners() { 126 | coreEvents.removeListener(CORE.SYNC_STOP, this.checkBeforeStop) 127 | coreEvents.removeListener(CORE.EXIT, this.checkBeforeStop) 128 | } 129 | } 130 | --------------------------------------------------------------------------------