mirror of
https://github.com/gustjose/obsidian-syncthing-manager.git
synced 2026-07-22 06:40:35 +00:00
Inicial Commit
This commit is contained in:
commit
ddab451dc8
27 changed files with 4130 additions and 0 deletions
10
.editorconfig
Normal file
10
.editorconfig
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
|
||||
main.js
|
||||
23
.eslintrc
Normal file
23
.eslintrc
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": { "node": true },
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off"
|
||||
}
|
||||
}
|
||||
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# vscode
|
||||
.vscode
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
# Don't include the compiled main.js file in the repo.
|
||||
# They should be uploaded to GitHub releases instead.
|
||||
main.js
|
||||
|
||||
# Exclude sourcemaps
|
||||
*.map
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
tag-version-prefix=""
|
||||
5
LICENSE
Normal file
5
LICENSE
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Copyright (C) 2020-2025 by Dynalist Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
94
README.md
Normal file
94
README.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Obsidian Sample Plugin
|
||||
|
||||
This is a sample plugin for Obsidian (https://obsidian.md).
|
||||
|
||||
This project uses TypeScript to provide type checking and documentation.
|
||||
The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definition format, which contains TSDoc comments describing what it does.
|
||||
|
||||
This sample plugin demonstrates some of the basic functionality the plugin API can do.
|
||||
- Adds a ribbon icon, which shows a Notice when clicked.
|
||||
- Adds a command "Open Sample Modal" which opens a Modal.
|
||||
- Adds a plugin setting tab to the settings page.
|
||||
- Registers a global click event and output 'click' to the console.
|
||||
- Registers a global interval which logs 'setInterval' to the console.
|
||||
|
||||
## First time developing plugins?
|
||||
|
||||
Quick starting guide for new plugin devs:
|
||||
|
||||
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
|
||||
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
|
||||
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
|
||||
- Install NodeJS, then run `npm i` in the command line under your repo folder.
|
||||
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
|
||||
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
|
||||
- Reload Obsidian to load the new version of your plugin.
|
||||
- Enable plugin in settings window.
|
||||
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
|
||||
|
||||
## Releasing new releases
|
||||
|
||||
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
|
||||
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
|
||||
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
|
||||
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
|
||||
- Publish the release.
|
||||
|
||||
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
|
||||
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
|
||||
|
||||
## Adding your plugin to the community plugin list
|
||||
|
||||
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
|
||||
- Publish an initial version.
|
||||
- Make sure you have a `README.md` file in the root of your repo.
|
||||
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
|
||||
|
||||
## How to use
|
||||
|
||||
- Clone this repo.
|
||||
- Make sure your NodeJS is at least v16 (`node --version`).
|
||||
- `npm i` or `yarn` to install dependencies.
|
||||
- `npm run dev` to start compilation in watch mode.
|
||||
|
||||
## Manually installing the plugin
|
||||
|
||||
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
|
||||
|
||||
## Improve code quality with eslint (optional)
|
||||
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
|
||||
- To use eslint with this project, make sure to install eslint from terminal:
|
||||
- `npm install -g eslint`
|
||||
- To use eslint to analyze this project use this command:
|
||||
- `eslint main.ts`
|
||||
- eslint will then create a report with suggestions for code improvement by file and line number.
|
||||
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
|
||||
- `eslint ./src/`
|
||||
|
||||
## Funding URL
|
||||
|
||||
You can include funding URLs where people who use your plugin can financially support it.
|
||||
|
||||
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": "https://buymeacoffee.com"
|
||||
}
|
||||
```
|
||||
|
||||
If you have multiple URLs, you can also do:
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": {
|
||||
"Buy Me a Coffee": "https://buymeacoffee.com",
|
||||
"GitHub Sponsor": "https://github.com/sponsors",
|
||||
"Patreon": "https://www.patreon.com/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
See https://github.com/obsidianmd/obsidian-api
|
||||
10
manifest.json
Normal file
10
manifest.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "obsidian-syncthing-manager",
|
||||
"name": "Syncthing Manager",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Painel de controle para Syncthing. Monitore status e force a sincronização no Desktop e Mobile.",
|
||||
"author": "Gustavo Carreiro",
|
||||
"authorUrl": "https://github.com/gustjose",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
2320
package-lock.json
generated
Normal file
2320
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
package.json
Normal file
26
package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "obsidian-sample-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node scripts/esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node scripts/esbuild.config.mjs production",
|
||||
"version": "node scripts/version-bump.mjs && git add manifest.json versions.json",
|
||||
"test-cofre": "node scripts/install-vault.mjs",
|
||||
"sync-conflict": "node scripts/create-conflict.mjs"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
43
scripts/create-conflict.mjs
Normal file
43
scripts/create-conflict.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// --- CONFIGURAÇÃO ---
|
||||
const VAULT_PATH = 'C:/Users/Gustavo/Android/dev-obsidian';
|
||||
|
||||
const BASE_FILENAME = 'TesteConflito.md';
|
||||
|
||||
function generateTimestamp() {
|
||||
const now = new Date();
|
||||
// Formata para YYYYMMDD-HHMMSS (Padrão do Syncthing)
|
||||
const date = now.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
const time = now.toTimeString().slice(0, 8).replace(/:/g, '');
|
||||
return `${date}-${time}`;
|
||||
}
|
||||
|
||||
function createConflict() {
|
||||
if (!fs.existsSync(VAULT_PATH)) {
|
||||
console.error(`❌ Erro: O caminho do cofre não existe: ${VAULT_PATH}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 1. Cria o arquivo Original (se não existir ou sobrescreve)
|
||||
const originalPath = path.join(VAULT_PATH, BASE_FILENAME);
|
||||
const originalContent = `# Nota Original\n\nEsta é a versão que estava no disco.\n\n- Item A\n- Item B\n- Item C`;
|
||||
|
||||
fs.writeFileSync(originalPath, originalContent);
|
||||
console.log(`✅ Arquivo original criado: ${BASE_FILENAME}`);
|
||||
|
||||
// 2. Cria o arquivo de Conflito
|
||||
const timestamp = generateTimestamp();
|
||||
const conflictName = `TesteConflito.sync-conflict-${timestamp}-ScriptDev.md`;
|
||||
const conflictPath = path.join(VAULT_PATH, conflictName);
|
||||
|
||||
const conflictContent = `# Nota em Conflito\n\nEsta é a versão que veio de outro dispositivo (Conflito).\n\n- Item A\n- Item B (Modificado)\n- Item D (Novo)`;
|
||||
|
||||
fs.writeFileSync(conflictPath, conflictContent);
|
||||
console.log(`⚠️ Arquivo de conflito criado: ${conflictName}`);
|
||||
|
||||
console.log('\n👉 Agora abra o Obsidian e verifique o painel lateral do plugin!');
|
||||
}
|
||||
|
||||
createConflict();
|
||||
49
scripts/esbuild.config.mjs
Normal file
49
scripts/esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
44
scripts/install-vault.mjs
Normal file
44
scripts/install-vault.mjs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const VAULT_PLUGINS_PATH = 'C:/Users/Gustavo/Android/dev-obsidian/.obsidian/plugins';
|
||||
|
||||
console.log('🚀 Iniciando processo de Build e Instalação...');
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf-8'));
|
||||
const pluginId = manifest.id;
|
||||
const targetDir = path.join(VAULT_PLUGINS_PATH, pluginId);
|
||||
|
||||
console.log(`📦 Plugin ID detectado: ${pluginId}`);
|
||||
console.log(`📂 Destino: ${targetDir}`);
|
||||
|
||||
try {
|
||||
console.log('🔨 Compilando (npm run build)...');
|
||||
execSync('npm run build', { stdio: 'inherit' });
|
||||
} catch (e) {
|
||||
console.error('❌ Erro na compilação. Processo abortado.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
console.log('📁 Criando pasta do plugin no cofre...');
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
const filesToCopy = ['main.js', 'manifest.json', 'styles.css'];
|
||||
|
||||
filesToCopy.forEach(file => {
|
||||
const source = path.resolve(file);
|
||||
const destination = path.join(targetDir, file);
|
||||
|
||||
if (fs.existsSync(source)) {
|
||||
fs.copyFileSync(source, destination);
|
||||
console.log(`✅ Copiado: ${file}`);
|
||||
} else {
|
||||
console.warn(`⚠️ Aviso: Arquivo ${file} não encontrado na raiz.`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🎉 Sucesso! Plugin atualizado no cofre de testes.');
|
||||
console.log('👉 Lembre-se de recarregar o plugin no Obsidian ou usar o Hot Reload.');
|
||||
17
scripts/version-bump.mjs
Normal file
17
scripts/version-bump.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
const targetVersion = process.env.npm_package_version;
|
||||
|
||||
// read minAppVersion from manifest.json and bump version to target version
|
||||
const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const { minAppVersion } = manifest;
|
||||
manifest.version = targetVersion;
|
||||
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||
|
||||
// update versions.json with target version and minAppVersion from manifest.json
|
||||
// but only if the target version is not already in versions.json
|
||||
const versions = JSON.parse(readFileSync('versions.json', 'utf8'));
|
||||
if (!Object.values(versions).includes(minAppVersion)) {
|
||||
versions[targetVersion] = minAppVersion;
|
||||
writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));
|
||||
}
|
||||
65
src/api/syncthing-api.ts
Normal file
65
src/api/syncthing-api.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { requestUrl, RequestUrlParam } from 'obsidian';
|
||||
|
||||
export class SyncthingAPI {
|
||||
|
||||
// Checa se o servidor está online (Ping)
|
||||
static async getStatus(url: string, apiKey: string): Promise<any> {
|
||||
return this.request(url, apiKey, '/rest/system/status');
|
||||
}
|
||||
|
||||
// Busca a lista de todas as pastas
|
||||
static async getFolders(url: string, apiKey: string): Promise<any[]> {
|
||||
const config = await this.request(url, apiKey, '/rest/config');
|
||||
return config.folders;
|
||||
}
|
||||
|
||||
// Busca o status de uma pasta
|
||||
static async getFolderStats(url: string, apiKey: string, folderId: string): Promise<any> {
|
||||
return this.request(url, apiKey, `/rest/db/status?folder=${folderId}`);
|
||||
}
|
||||
|
||||
static async getConnections(url: string, apiKey: string): Promise<any> {
|
||||
return this.request(url, apiKey, '/rest/system/connections');
|
||||
}
|
||||
|
||||
// --- NOVO: Força o Scan (Sincronização) ---
|
||||
static async forceScan(url: string, apiKey: string, folderId?: string): Promise<any> {
|
||||
let endpoint = '/rest/db/scan';
|
||||
// Se tiver um ID de pasta, forçamos scan SÓ nela (mais rápido).
|
||||
// Se não tiver, o Syncthing faz scan em tudo.
|
||||
if (folderId) {
|
||||
endpoint += `?folder=${folderId}`;
|
||||
}
|
||||
// Nota: Scan exige método POST
|
||||
return this.request(url, apiKey, endpoint, 'POST');
|
||||
}
|
||||
|
||||
// Refatorei para aceitar o método (GET/POST)
|
||||
private static async request(url: string, apiKey: string, endpointPath: string, method: string = 'GET'): Promise<any> {
|
||||
const baseUrl = url.replace(/\/$/, '');
|
||||
const endpoint = `${baseUrl}${endpointPath}`;
|
||||
|
||||
const params: RequestUrlParam = {
|
||||
url: endpoint,
|
||||
method: method, // Agora é dinâmico
|
||||
headers: { 'X-API-Key': apiKey }
|
||||
};
|
||||
|
||||
const response = await requestUrl(params);
|
||||
|
||||
// O Syncthing retorna 200 (OK) para a maioria das coisas
|
||||
if (response.status === 200) {
|
||||
// O Endpoint de Scan retorna vazio ou texto simples, nem sempre JSON
|
||||
if (response.text && response.text.length > 0) {
|
||||
try {
|
||||
return response.json;
|
||||
} catch {
|
||||
return response.text;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
} else {
|
||||
throw new Error(`Erro HTTP ${response.status}: ${endpointPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/lang/lang.ts
Normal file
30
src/lang/lang.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { moment } from 'obsidian';
|
||||
import en from './locales/en';
|
||||
import pt_br from './locales/pt-br';
|
||||
|
||||
// Estado global do idioma
|
||||
let userLanguage = 'auto';
|
||||
|
||||
// Função para o Main.ts definir o idioma escolhido
|
||||
export function setLanguage(lang: string) {
|
||||
userLanguage = lang;
|
||||
}
|
||||
|
||||
export function t(key: keyof typeof en): string {
|
||||
let current = userLanguage;
|
||||
|
||||
// Se for 'auto', perguntamos ao Obsidian qual a língua
|
||||
if (current === 'auto') {
|
||||
// @ts-ignore
|
||||
current = moment.locale();
|
||||
}
|
||||
|
||||
// Detecção de Português
|
||||
if (current && current.toLowerCase().startsWith('pt_br')) {
|
||||
// @ts-ignore
|
||||
return pt_br[key] || en[key];
|
||||
}
|
||||
|
||||
// Padrão: Inglês
|
||||
return en[key];
|
||||
}
|
||||
83
src/lang/locales/en.ts
Normal file
83
src/lang/locales/en.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
export default {
|
||||
// Comandos
|
||||
cmd_open_panel: 'Open Side Panel',
|
||||
cmd_force_sync: 'Force Sync Now',
|
||||
cmd_debug_connect: 'Debug: Test Connection',
|
||||
|
||||
// Ribbon
|
||||
ribbon_tooltip: 'Open Syncthing Controller',
|
||||
|
||||
// Status / View
|
||||
status_synced: 'Synced',
|
||||
status_syncing: 'Syncing...',
|
||||
status_offline: 'Offline',
|
||||
status_error: 'Error',
|
||||
status_unknown: 'Unknown',
|
||||
|
||||
info_last_sync: 'Last Sync',
|
||||
info_devices: 'Online Devices',
|
||||
info_folder: 'Folder',
|
||||
btn_sync_now: 'Sync Now',
|
||||
btn_requesting: 'Requesting...',
|
||||
|
||||
// Settings - Headers
|
||||
setting_header_conn: 'Connection',
|
||||
setting_header_folder: 'Folder Monitoring',
|
||||
setting_header_interface: 'Interface',
|
||||
setting_header_general: 'General',
|
||||
|
||||
// Settings - General
|
||||
setting_lang_name: 'Language',
|
||||
setting_lang_desc: 'Force a specific language or use Auto.',
|
||||
|
||||
// Settings - Connection
|
||||
setting_https_name: 'Use HTTPS',
|
||||
setting_https_desc: 'Enable if your Syncthing GUI uses secure connection (TLS). Usually disabled for localhost.',
|
||||
setting_host_name: 'IP Address / Host',
|
||||
setting_host_desc: 'Default: 127.0.0.1 (localhost).',
|
||||
setting_port_name: 'Port',
|
||||
setting_port_desc: 'Default: 8384.',
|
||||
setting_api_name: 'API Key',
|
||||
setting_api_desc: 'Find it in Syncthing > Actions > Settings > General.',
|
||||
btn_test_conn: 'Test Connection',
|
||||
|
||||
// Settings - Folder
|
||||
setting_folder_name: 'Vault Folder',
|
||||
setting_folder_desc: 'Select which Syncthing folder corresponds to this Obsidian Vault.',
|
||||
dropdown_default: 'Select a folder',
|
||||
dropdown_none: 'None selected',
|
||||
btn_search_folders: 'Search folders (uses connection settings above)',
|
||||
|
||||
// Settings - Interface
|
||||
setting_status_bar_name: 'Show in Status Bar',
|
||||
setting_status_bar_desc: 'Desktop only. Reload to apply.',
|
||||
setting_ribbon_name: 'Show Ribbon Icon',
|
||||
setting_ribbon_desc: 'Left sidebar icon (Desktop/Mobile). Reload to apply.',
|
||||
|
||||
// Notices / Errors
|
||||
notice_syncing: 'Syncing...',
|
||||
notice_success_conn: 'Success! Connected to ID: ',
|
||||
notice_fail_conn: 'Connection failed. Check IP, Port and HTTPS.',
|
||||
notice_error_auth: 'Authentication Error.',
|
||||
notice_offline: 'Syncthing Offline.',
|
||||
notice_folders_found: 'folders found!',
|
||||
notice_config_first: 'Configure URL and API Key first.',
|
||||
notice_searching: 'Searching folders...',
|
||||
|
||||
// Conflict Modal
|
||||
modal_conflict_title: 'Resolve Conflicts',
|
||||
modal_conflict_empty: 'No conflict files found. All clean!',
|
||||
modal_conflict_desc: 'Choose which version you want to keep.',
|
||||
btn_compare: 'Compare Content',
|
||||
btn_keep_original: 'Keep Original',
|
||||
tooltip_keep_original: 'Deletes the conflict file (right side) and keeps the current one.',
|
||||
btn_keep_conflict: 'Use This Version',
|
||||
tooltip_keep_conflict: 'Replaces the original file with this conflict version.',
|
||||
|
||||
// Diff View
|
||||
diff_original_header: 'Original File (Current)',
|
||||
diff_conflict_header: 'Conflict Version',
|
||||
diff_loading: 'Loading...',
|
||||
diff_original_missing: '(Original file not found or deleted)',
|
||||
diff_read_error: 'Error reading file.'
|
||||
};
|
||||
83
src/lang/locales/pt-br.ts
Normal file
83
src/lang/locales/pt-br.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
export default {
|
||||
// Comandos
|
||||
cmd_open_panel: 'Abrir Painel Lateral',
|
||||
cmd_force_sync: 'Forçar Sincronização Agora',
|
||||
cmd_debug_connect: 'Debug: Testar Conexão',
|
||||
|
||||
// Ribbon
|
||||
ribbon_tooltip: 'Abrir Syncthing Controller',
|
||||
|
||||
// Status / View
|
||||
status_synced: 'Sincronizado',
|
||||
status_syncing: 'Sincronizando...',
|
||||
status_offline: 'Desconectado',
|
||||
status_error: 'Erro',
|
||||
status_unknown: 'Desconhecido',
|
||||
|
||||
info_last_sync: 'Último Sync',
|
||||
info_devices: 'Dispositivos Online',
|
||||
info_folder: 'Pasta',
|
||||
btn_sync_now: 'Sincronizar Agora',
|
||||
btn_requesting: 'Solicitando...',
|
||||
|
||||
// Settings - Headers
|
||||
setting_header_conn: 'Conexão',
|
||||
setting_header_folder: 'Monitoramento de Pasta',
|
||||
setting_header_interface: 'Interface',
|
||||
setting_header_general: 'Geral',
|
||||
|
||||
// Settings - General
|
||||
setting_lang_name: 'Idioma',
|
||||
setting_lang_desc: 'Force um idioma específico ou use Automático.',
|
||||
|
||||
// Settings - Connection
|
||||
setting_https_name: 'Usar HTTPS',
|
||||
setting_https_desc: 'Ative se o Syncthing usa conexão segura (TLS). Geralmente desligado para localhost.',
|
||||
setting_host_name: 'Endereço IP / Host',
|
||||
setting_host_desc: 'Padrão: 127.0.0.1 (localhost).',
|
||||
setting_port_name: 'Porta',
|
||||
setting_port_desc: 'Padrão: 8384.',
|
||||
setting_api_name: 'Chave da API (API Key)',
|
||||
setting_api_desc: 'Encontre em Syncthing > Ações > Configurações > Geral.',
|
||||
btn_test_conn: 'Testar Conexão',
|
||||
|
||||
// Settings - Folder
|
||||
setting_folder_name: 'Pasta do Cofre',
|
||||
setting_folder_desc: 'Selecione qual pasta do Syncthing corresponde a este cofre.',
|
||||
dropdown_default: 'Selecione uma pasta',
|
||||
dropdown_none: 'Nenhuma selecionada',
|
||||
btn_search_folders: 'Buscar pastas (usa configurações acima)',
|
||||
|
||||
// Settings - Interface
|
||||
setting_status_bar_name: 'Mostrar na Barra de Status',
|
||||
setting_status_bar_desc: 'Apenas Desktop. Recarregue para aplicar.',
|
||||
setting_ribbon_name: 'Mostrar Ícone no Ribbon',
|
||||
setting_ribbon_desc: 'Barra lateral esquerda (Desktop/Mobile). Recarregue para aplicar.',
|
||||
|
||||
// Notices / Errors
|
||||
notice_syncing: 'Sincronizando...',
|
||||
notice_success_conn: 'Sucesso! Conectado ao ID: ',
|
||||
notice_fail_conn: 'Falha. Verifique IP, Porta e HTTPS.',
|
||||
notice_error_auth: 'Erro de Autenticação.',
|
||||
notice_offline: 'Syncthing Offline.',
|
||||
notice_folders_found: 'pastas encontradas!',
|
||||
notice_config_first: 'Configure a API Key primeiro.',
|
||||
notice_searching: 'Buscando pastas...',
|
||||
|
||||
// Modal de Conflitos
|
||||
modal_conflict_title: 'Resolver Conflitos',
|
||||
modal_conflict_empty: 'Nenhum arquivo de conflito encontrado. Tudo limpo!',
|
||||
modal_conflict_desc: 'Escolha qual versão você deseja manter.',
|
||||
btn_compare: 'Comparar Conteúdo',
|
||||
btn_keep_original: 'Manter Original',
|
||||
tooltip_keep_original: 'Apaga o arquivo de conflito (o da direita) e mantém o atual.',
|
||||
btn_keep_conflict: 'Usar Esta Versão',
|
||||
tooltip_keep_conflict: 'Substitui o original pelo arquivo de conflito.',
|
||||
|
||||
// Visualização de Diferença
|
||||
diff_original_header: 'Arquivo Original (Atual)',
|
||||
diff_conflict_header: 'Versão em Conflito',
|
||||
diff_loading: 'Carregando...',
|
||||
diff_original_missing: '(Arquivo original não encontrado ou foi deletado)',
|
||||
diff_read_error: 'Erro ao ler arquivo.'
|
||||
};
|
||||
360
src/main.ts
Normal file
360
src/main.ts
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
import { Plugin, Notice, WorkspaceLeaf, Platform } from 'obsidian';
|
||||
import { SyncthingSettingTab } from './ui/settings';
|
||||
import { t, setLanguage } from './lang/lang';
|
||||
import { SyncthingAPI } from './api/syncthing-api';
|
||||
import { SyncthingView, VIEW_TYPE_SYNCTHING } from './ui/view';
|
||||
import { SyncthingEventMonitor } from './services/event-monitor';
|
||||
|
||||
// Chaves do LocalStorage (Apenas para dados SENSÍVEIS ou ESPECÍFICOS do hardware)
|
||||
const LS_KEY_HOST = 'syncthing-controller-host';
|
||||
const LS_KEY_PORT = 'syncthing-controller-port';
|
||||
const LS_KEY_HTTPS = 'syncthing-controller-https';
|
||||
const LS_KEY_API = 'syncthing-controller-api-key';
|
||||
const LS_KEY_FOLDER = 'syncthing-controller-folder-id';
|
||||
const LS_KEY_FOLDER_LABEL = 'syncthing-controller-folder-label';
|
||||
|
||||
export interface SyncthingPluginSettings {
|
||||
// Dados locais (não salvos no data.json real)
|
||||
syncthingHost: string;
|
||||
syncthingPort: string;
|
||||
useHttps: boolean;
|
||||
syncthingApiKey: string;
|
||||
syncthingFolderId: string;
|
||||
syncthingFolderLabel: string;
|
||||
|
||||
// Dados globais (sincronizados via data.json)
|
||||
updateInterval: number;
|
||||
showStatusBar: boolean;
|
||||
showRibbonIcon: boolean;
|
||||
language: string; // <--- Agora é global
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: SyncthingPluginSettings = {
|
||||
syncthingHost: '127.0.0.1',
|
||||
syncthingPort: '8384',
|
||||
useHttps: false,
|
||||
syncthingApiKey: '',
|
||||
syncthingFolderId: '',
|
||||
syncthingFolderLabel: '',
|
||||
updateInterval: 30,
|
||||
showStatusBar: true,
|
||||
showRibbonIcon: true,
|
||||
language: 'auto'
|
||||
}
|
||||
|
||||
type SyncStatus = 'conectado' | 'sincronizando' | 'escutando' | 'desconectado' | 'erro' | 'desconhecido';
|
||||
|
||||
export default class SyncthingController extends Plugin {
|
||||
settings: SyncthingPluginSettings;
|
||||
statusBarItem: HTMLElement | null = null;
|
||||
ribbonIconEl: HTMLElement | null = null;
|
||||
monitor: SyncthingEventMonitor;
|
||||
|
||||
public lastSyncTime: string = '--:--';
|
||||
public connectedDevices: number = 0;
|
||||
public currentStatus: SyncStatus = 'desconhecido';
|
||||
|
||||
get apiUrl(): string {
|
||||
const protocol = this.settings.useHttps ? 'https://' : 'http://';
|
||||
let host = this.settings.syncthingHost.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
|
||||
// Correção para Android:
|
||||
if (host === 'localhost' && Platform.isMobileApp) {
|
||||
host = '127.0.0.1';
|
||||
}
|
||||
|
||||
return `${protocol}${host}:${this.settings.syncthingPort}`;
|
||||
}
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// Aplica o idioma salvo no data.json
|
||||
setLanguage(this.settings.language);
|
||||
|
||||
this.registerView(
|
||||
VIEW_TYPE_SYNCTHING,
|
||||
(leaf) => new SyncthingView(leaf, this)
|
||||
);
|
||||
|
||||
if (this.settings.showStatusBar) {
|
||||
this.statusBarItem = this.addStatusBarItem();
|
||||
this.statusBarItem.addClass('mod-clickable');
|
||||
this.statusBarItem.setAttribute('aria-label', 'Syncthing Controller');
|
||||
this.statusBarItem.addEventListener('click', async () => {
|
||||
await this.forcarSincronizacao();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.settings.showRibbonIcon) {
|
||||
this.ribbonIconEl = this.addRibbonIcon('refresh-cw', t('ribbon_tooltip'), () => {
|
||||
this.activateView();
|
||||
});
|
||||
}
|
||||
|
||||
this.addCommand({
|
||||
id: 'open-syncthing-view',
|
||||
name: t('cmd_open_panel'),
|
||||
callback: () => { this.activateView(); }
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'syncthing-force-scan',
|
||||
name: t('cmd_force_sync'),
|
||||
icon: 'refresh-cw',
|
||||
callback: async () => { await this.forcarSincronizacao(); }
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'test-syncthing-connection',
|
||||
name: t('cmd_debug_connect'),
|
||||
callback: async () => { await this.verificarConexao(true); }
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'debug-syncthing-connection',
|
||||
name: 'Debug: Testar Conexão (Detalhado)',
|
||||
callback: async () => {
|
||||
// 1. Pega a URL calculada (já com a correção de IP se você aplicou)
|
||||
const urlAlvo = this.apiUrl;
|
||||
|
||||
// 2. Mostra na tela qual URL está sendo usada
|
||||
// Isso vai te confirmar se o "localhost" virou "127.0.0.1"
|
||||
new Notice(`Tentando conectar em:\n${urlAlvo}`, 5000); // Fica 5s na tela
|
||||
console.log(`[DEBUG] URL Alvo: ${urlAlvo}`);
|
||||
|
||||
try {
|
||||
// 3. Tenta fazer o request básico de status
|
||||
const status = await SyncthingAPI.getStatus(urlAlvo, this.settings.syncthingApiKey);
|
||||
|
||||
// 4. Se der certo, mostra o ID do dispositivo
|
||||
new Notice(`✅ SUCESSO!\nConectado ao Device ID:\n${status.myID.substring(0, 10)}...`, 5000);
|
||||
console.log('[DEBUG] Sucesso:', status);
|
||||
|
||||
} catch (error) {
|
||||
// 5. Se der erro, mostra a mensagem exata do erro na tela
|
||||
let msgErro = 'Erro desconhecido';
|
||||
if (error instanceof Error) msgErro = error.message;
|
||||
|
||||
new Notice(`❌ FALHA:\n${msgErro}`, 10000); // Fica 10s na tela
|
||||
console.error('[DEBUG] Erro detalhado:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.monitor = new SyncthingEventMonitor(this);
|
||||
this.addSettingTab(new SyncthingSettingTab(this.app, this));
|
||||
|
||||
this.atualizarTodosVisuais();
|
||||
await this.verificarConexao(false);
|
||||
await this.atualizarContagemDispositivos();
|
||||
|
||||
this.monitor.start();
|
||||
}
|
||||
|
||||
onunload() {
|
||||
if (this.monitor) this.monitor.stop();
|
||||
}
|
||||
|
||||
async activateView() {
|
||||
const { workspace } = this.app;
|
||||
let leaf: WorkspaceLeaf | null = null;
|
||||
const leaves = workspace.getLeavesOfType(VIEW_TYPE_SYNCTHING);
|
||||
|
||||
if (leaves.length > 0) {
|
||||
leaf = leaves[0];
|
||||
} else {
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
if (leaf) await leaf.setViewState({ type: VIEW_TYPE_SYNCTHING, active: true });
|
||||
}
|
||||
if (leaf) workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
atualizarTodosVisuais() {
|
||||
this.atualizarStatusBar(this.currentStatus);
|
||||
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_SYNCTHING);
|
||||
leaves.forEach((leaf) => {
|
||||
if (leaf.view instanceof SyncthingView) leaf.view.updateView();
|
||||
});
|
||||
}
|
||||
|
||||
async testarApiApenas() {
|
||||
try {
|
||||
new Notice('Ping...');
|
||||
const status = await SyncthingAPI.getStatus(this.apiUrl, this.settings.syncthingApiKey);
|
||||
new Notice(`${t('notice_success_conn')} ${status.myID.substring(0, 5)}...`);
|
||||
} catch (error) {
|
||||
new Notice(t('notice_fail_conn'));
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async atualizarContagemDispositivos() {
|
||||
try {
|
||||
const connections = await SyncthingAPI.getConnections(this.apiUrl, this.settings.syncthingApiKey);
|
||||
const devices = connections.connections || {};
|
||||
const count = Object.values(devices).filter((d: any) => d.connected).length;
|
||||
this.connectedDevices = count;
|
||||
this.atualizarTodosVisuais();
|
||||
} catch (e) {
|
||||
console.error("Erro ao buscar dispositivos:", e);
|
||||
}
|
||||
}
|
||||
|
||||
async forcarSincronizacao() {
|
||||
if (!this.settings.syncthingApiKey) {
|
||||
new Notice(t('notice_config_first'));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((this.app as any).commands) {
|
||||
(this.app as any).commands.executeCommandById('editor:save-file');
|
||||
}
|
||||
|
||||
new Notice(t('notice_syncing'));
|
||||
this.currentStatus = 'sincronizando';
|
||||
this.atualizarTodosVisuais();
|
||||
|
||||
try {
|
||||
await SyncthingAPI.forceScan(
|
||||
this.apiUrl,
|
||||
this.settings.syncthingApiKey,
|
||||
this.settings.syncthingFolderId
|
||||
);
|
||||
await this.atualizarContagemDispositivos();
|
||||
} catch (error) {
|
||||
console.error('Erro ao forçar scan:', error);
|
||||
new Notice('Erro ao solicitar Sync.');
|
||||
this.currentStatus = 'erro';
|
||||
this.atualizarTodosVisuais();
|
||||
}
|
||||
}
|
||||
|
||||
async verificarConexao(showNotice: boolean = false) {
|
||||
try {
|
||||
const systemStatus = await SyncthingAPI.getStatus(this.apiUrl, this.settings.syncthingApiKey);
|
||||
|
||||
if (this.settings.syncthingFolderId) {
|
||||
const folderStats = await SyncthingAPI.getFolderStats(this.apiUrl, this.settings.syncthingApiKey, this.settings.syncthingFolderId);
|
||||
const state = folderStats.state;
|
||||
const needBytes = folderStats.needBytes;
|
||||
|
||||
if (state === 'scanning' || state === 'syncing' || needBytes > 0) {
|
||||
this.currentStatus = 'sincronizando';
|
||||
if (showNotice) new Notice(t('notice_syncing'));
|
||||
} else if (state === 'idle' && needBytes === 0) {
|
||||
this.lastSyncTime = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
this.currentStatus = 'conectado';
|
||||
if (showNotice) new Notice(t('status_synced'));
|
||||
} else {
|
||||
this.currentStatus = 'erro';
|
||||
if (showNotice) new Notice(`Status: ${state}`);
|
||||
}
|
||||
} else {
|
||||
this.currentStatus = 'conectado';
|
||||
if (showNotice) new Notice(`${t('notice_success_conn')} ${systemStatus.myID.substring(0, 5)}...`);
|
||||
}
|
||||
this.atualizarTodosVisuais();
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error && error.message.includes('403')) {
|
||||
this.currentStatus = 'erro';
|
||||
if (showNotice) new Notice(t('notice_error_auth'));
|
||||
} else {
|
||||
this.currentStatus = 'desconectado';
|
||||
if (showNotice) new Notice(t('notice_offline'));
|
||||
}
|
||||
this.atualizarTodosVisuais();
|
||||
}
|
||||
}
|
||||
|
||||
atualizarStatusBar(status: SyncStatus) {
|
||||
this.currentStatus = status;
|
||||
let text = t('status_unknown');
|
||||
let color = 'var(--text-muted)';
|
||||
let color_bg = 'currentColor';
|
||||
|
||||
switch (status) {
|
||||
case 'conectado':
|
||||
text = t('status_synced'); color = 'var(--text-success)'; break;
|
||||
case 'sincronizando':
|
||||
text = t('status_syncing'); color = 'var(--text-warning)'; break;
|
||||
case 'desconectado':
|
||||
text = t('status_offline'); color = 'var(--text-muted)'; break;
|
||||
case 'erro':
|
||||
text = t('status_error'); color = 'var(--text-error)'; break;
|
||||
}
|
||||
|
||||
const tooltipInfo = `${text}\n\n${t('info_last_sync')}: ${this.lastSyncTime}\n${t('info_devices')}: ${this.connectedDevices}`;
|
||||
|
||||
const customSvg = `<svg viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg" fill="none" style="width: 100%; height: 100%;"><path d="M161.785 101.327a66 66 0 0 1-4.462 19.076m-49.314 40.495A66 66 0 0 1 96 162a66 66 0 0 1-45.033-17.75M31.188 83.531A66 66 0 0 1 96 30a66 66 0 0 1 39.522 13.141" stroke="${color_bg}" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M146.887 147.005a9 9 0 0 1-9 9 9 9 0 0 1-9-9 9 9 0 0 1 9-9 9 9 0 0 1 9-9 9 9 0 0 1 9 9zm18.25-78.199a9 9 0 0 1-9 9 9 9 0 0 1-9-9 9 9 0 0 1 9-9 9 9 0 0 1 9 9zM118.5 105a9 9 0 0 1-9 9 9 9 0 0 1-9-9 9 9 0 0 1 9-9 9 9 0 0 1 9 9zm-76.248 11.463a9 9 0 0 1-9 9 9 9 0 0 1-9-9 9 9 0 0 1 9-9 9 9 0 0 1 9 9zm113.885-68.656a21 21 0 0 0-21 21 21 21 0 0 0 1.467 7.564l-14.89 11.555A21 21 0 0 0 109.5 84a21 21 0 0 0-20.791 18.057l-36.45 5.48a21 21 0 0 0-19.007-12.074 21 21 0 0 0-21 21 21 21 0 0 0 21 21 21 21 0 0 0 20.791-18.059l36.463-5.48A21 21 0 0 0 109.5 126a21 21 0 0 0 6.283-.988l5.885 8.707a21 21 0 0 0-4.781 13.287 21 21 0 0 0 21 21 21 21 0 0 0 21-21 21 21 0 0 0-21-21 21 21 0 0 0-6.283.986l-5.883-8.707A21 21 0 0 0 130.5 105a21 21 0 0 0-1.428-7.594l14.885-11.552a21 21 0 0 0 12.18 3.953 21 21 0 0 0 21-21 21 21 0 0 0-21-21z" fill="currentColor" fill-rule="evenodd"/></svg>`;
|
||||
|
||||
if (this.statusBarItem) {
|
||||
this.statusBarItem.empty();
|
||||
const iconSpan = this.statusBarItem.createSpan({ cls: 'status-bar-item-icon' });
|
||||
iconSpan.innerHTML = customSvg.replace('style="width: 100%; height: 100%;"', 'width="20" height="20"');
|
||||
this.statusBarItem.style.color = color;
|
||||
this.statusBarItem.setAttribute('aria-label', tooltipInfo);
|
||||
}
|
||||
|
||||
if (this.ribbonIconEl) {
|
||||
this.ribbonIconEl.empty();
|
||||
const iconContainer = this.ribbonIconEl.createDiv({ cls: 'ribbon-icon-svg' });
|
||||
iconContainer.innerHTML = customSvg;
|
||||
this.ribbonIconEl.style.color = color;
|
||||
this.ribbonIconEl.setAttribute('aria-label', tooltipInfo);
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
// 1. Carrega tudo do data.json (incluindo o Idioma)
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
|
||||
// Sanitização: Limpa placeholders antigos se ainda existirem no data.json
|
||||
if ((this.settings.syncthingHost as string) === 'device-specific') this.settings.syncthingHost = '';
|
||||
if ((this.settings.syncthingPort as string) === 'device-specific') this.settings.syncthingPort = '';
|
||||
|
||||
// 2. Sobrescreve APENAS os dados de conexão com o LocalStorage
|
||||
const localHost = window.localStorage.getItem(LS_KEY_HOST);
|
||||
const localPort = window.localStorage.getItem(LS_KEY_PORT);
|
||||
const localHttps = window.localStorage.getItem(LS_KEY_HTTPS);
|
||||
const localKey = window.localStorage.getItem(LS_KEY_API);
|
||||
const localFolder = window.localStorage.getItem(LS_KEY_FOLDER);
|
||||
const localFolderLabel = window.localStorage.getItem(LS_KEY_FOLDER_LABEL);
|
||||
|
||||
if (localHost) this.settings.syncthingHost = localHost;
|
||||
if (localPort) this.settings.syncthingPort = localPort;
|
||||
if (localHttps !== null) this.settings.useHttps = (localHttps === 'true');
|
||||
if (localKey) this.settings.syncthingApiKey = localKey;
|
||||
if (localFolder) this.settings.syncthingFolderId = localFolder;
|
||||
if (localFolderLabel) this.settings.syncthingFolderLabel = localFolderLabel;
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
// 1. Salva no data.json: Configurações GERAIS e IDIOMA
|
||||
const sharedSettings = {
|
||||
updateInterval: this.settings.updateInterval,
|
||||
showStatusBar: this.settings.showStatusBar,
|
||||
showRibbonIcon: this.settings.showRibbonIcon,
|
||||
language: this.settings.language, // <--- SALVO NO JSON COMPARTILHADO
|
||||
|
||||
// Placeholders para não vazar dados locais
|
||||
syncthingHost: 'device-specific',
|
||||
syncthingPort: 'device-specific',
|
||||
useHttps: false,
|
||||
syncthingApiKey: 'device-specific',
|
||||
syncthingFolderId: 'device-specific',
|
||||
syncthingFolderLabel: 'device-specific'
|
||||
};
|
||||
await this.saveData(sharedSettings);
|
||||
|
||||
// 2. Salva no LocalStorage: Apenas dados locais e sensíveis
|
||||
window.localStorage.setItem(LS_KEY_HOST, this.settings.syncthingHost);
|
||||
window.localStorage.setItem(LS_KEY_PORT, this.settings.syncthingPort);
|
||||
window.localStorage.setItem(LS_KEY_HTTPS, String(this.settings.useHttps));
|
||||
window.localStorage.setItem(LS_KEY_API, this.settings.syncthingApiKey);
|
||||
window.localStorage.setItem(LS_KEY_FOLDER, this.settings.syncthingFolderId);
|
||||
window.localStorage.setItem(LS_KEY_FOLDER_LABEL, this.settings.syncthingFolderLabel);
|
||||
}
|
||||
}
|
||||
64
src/services/conflict-manager.ts
Normal file
64
src/services/conflict-manager.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { App, TFile, Notice } from 'obsidian';
|
||||
|
||||
export interface ConflictFile {
|
||||
file: TFile;
|
||||
baseName: string;
|
||||
date: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export class ConflictManager {
|
||||
app: App;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
getConflicts(): ConflictFile[] {
|
||||
const allFiles = this.app.vault.getFiles();
|
||||
const conflicts: ConflictFile[] = [];
|
||||
|
||||
const regex = /(.+)\.sync-conflict-(\d{8}-\d{6})-(.+)(\.[^.]+)$/;
|
||||
|
||||
allFiles.forEach(file => {
|
||||
if (file.name.includes('.sync-conflict-')) {
|
||||
const match = file.name.match(regex);
|
||||
conflicts.push({
|
||||
file: file,
|
||||
baseName: match ? match[1] + match[4] : file.name,
|
||||
date: match ? match[2] : 'Unknown',
|
||||
path: file.path
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
async deleteConflict(conflict: ConflictFile) {
|
||||
try {
|
||||
await this.app.vault.delete(conflict.file);
|
||||
new Notice(`Conflito removido: ${conflict.file.name}`);
|
||||
} catch (e) {
|
||||
new Notice('Erro ao apagar arquivo.');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async acceptConflict(conflict: ConflictFile) {
|
||||
const originalPath = conflict.path.replace(conflict.file.name, conflict.baseName);
|
||||
const originalFile = this.app.vault.getAbstractFileByPath(originalPath);
|
||||
|
||||
try {
|
||||
if (originalFile && originalFile instanceof TFile) {
|
||||
await this.app.vault.delete(originalFile);
|
||||
}
|
||||
|
||||
await this.app.vault.rename(conflict.file, originalPath);
|
||||
new Notice(`Versão de conflito restaurada: ${conflict.baseName}`);
|
||||
} catch (e) {
|
||||
new Notice('Erro ao restaurar conflito.');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/services/event-monitor.ts
Normal file
131
src/services/event-monitor.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import { requestUrl, Notice } from 'obsidian';
|
||||
import SyncthingController from '../main';
|
||||
|
||||
export class SyncthingEventMonitor {
|
||||
plugin: SyncthingController;
|
||||
running: boolean = false;
|
||||
lastEventId: number = 0;
|
||||
|
||||
constructor(plugin: SyncthingController) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.running) return;
|
||||
|
||||
try {
|
||||
// MUDANÇA AQUI: Usa o getter this.plugin.apiUrl
|
||||
const url = `${this.plugin.apiUrl}/rest/events?limit=1`;
|
||||
const response = await requestUrl({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: { 'X-API-Key': this.plugin.settings.syncthingApiKey }
|
||||
});
|
||||
|
||||
if (response.status === 200 && Array.isArray(response.json) && response.json.length > 0) {
|
||||
const lastEvent = response.json[response.json.length - 1];
|
||||
this.lastEventId = lastEvent.id;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Erro ao buscar ID inicial:", e);
|
||||
}
|
||||
|
||||
this.running = true;
|
||||
this.loop();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
private async loop() {
|
||||
while (this.running) {
|
||||
try {
|
||||
if (!this.plugin.settings.syncthingApiKey) {
|
||||
await this.sleep(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
// MUDANÇA AQUI: Usa o getter this.plugin.apiUrl
|
||||
const url = `${this.plugin.apiUrl}/rest/events?since=${this.lastEventId}&timeout=60`;
|
||||
|
||||
const response = await requestUrl({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: { 'X-API-Key': this.plugin.settings.syncthingApiKey }
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const events = response.json;
|
||||
if (Array.isArray(events) && events.length > 0) {
|
||||
for (const event of events) {
|
||||
this.lastEventId = event.id;
|
||||
this.processEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
await this.sleep(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ... MANTENHA O RESTO DO ARQUIVO (processEvent, updateStatusFromState, sleep) IGUAL ...
|
||||
// Copie do arquivo anterior, a lógica de processamento de eventos não mudou.
|
||||
|
||||
private processEvent(event: any) {
|
||||
const targetFolder = this.plugin.settings.syncthingFolderId;
|
||||
if (!targetFolder) return;
|
||||
|
||||
// 1. Dispositivos
|
||||
if (event.type === 'DeviceConnected' || event.type === 'DeviceDisconnected') {
|
||||
this.plugin.atualizarContagemDispositivos();
|
||||
}
|
||||
|
||||
// 2. FolderCompletion
|
||||
if (event.type === 'FolderCompletion') {
|
||||
const data = event.data;
|
||||
if (data.folder === targetFolder) {
|
||||
if (data.completion < 100 || data.needBytes > 0) {
|
||||
this.plugin.atualizarStatusBar('sincronizando');
|
||||
} else {
|
||||
this.plugin.lastSyncTime = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
this.plugin.atualizarStatusBar('conectado');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. StateChanged
|
||||
if (event.type === 'StateChanged') {
|
||||
const data = event.data;
|
||||
if (data.folder === targetFolder) {
|
||||
if (data.to === 'scanning' || data.to === 'syncing') {
|
||||
this.plugin.atualizarStatusBar('sincronizando');
|
||||
} else if (data.to === 'idle') {
|
||||
this.plugin.lastSyncTime = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
this.plugin.atualizarStatusBar('conectado');
|
||||
} else if (data.to === 'error') {
|
||||
this.plugin.atualizarStatusBar('erro');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. FolderSummary
|
||||
if (event.type === 'FolderSummary') {
|
||||
const data = event.data;
|
||||
if (data.folder === targetFolder) {
|
||||
if (data.summary.needBytes > 0) {
|
||||
this.plugin.atualizarStatusBar('sincronizando');
|
||||
} else {
|
||||
this.plugin.lastSyncTime = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
this.plugin.atualizarStatusBar('conectado');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
131
src/ui/conflict-modal.ts
Normal file
131
src/ui/conflict-modal.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import { App, Modal, Setting, TFile, ButtonComponent, Notice } from 'obsidian';
|
||||
import { ConflictManager, ConflictFile } from '../services/conflict-manager';
|
||||
import { t } from '../lang/lang';
|
||||
|
||||
export class ConflictModal extends Modal {
|
||||
manager: ConflictManager;
|
||||
conflicts: ConflictFile[];
|
||||
onCloseCallback: () => void;
|
||||
|
||||
constructor(app: App, manager: ConflictManager, onCloseCallback: () => void) {
|
||||
super(app);
|
||||
this.manager = manager;
|
||||
this.onCloseCallback = onCloseCallback;
|
||||
this.conflicts = manager.getConflicts();
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
this.modalEl.style.width = '80vw';
|
||||
this.modalEl.style.maxWidth = '1000px';
|
||||
|
||||
contentEl.createEl('h2', { text: `${t('modal_conflict_title')} (${this.conflicts.length})` });
|
||||
|
||||
if (this.conflicts.length === 0) {
|
||||
contentEl.createEl('p', { text: t('modal_conflict_empty') });
|
||||
return;
|
||||
}
|
||||
|
||||
contentEl.createEl('p', { text: t('modal_conflict_desc') });
|
||||
|
||||
this.conflicts.forEach(conflict => {
|
||||
const container = contentEl.createDiv({ cls: 'conflict-item-box' });
|
||||
|
||||
container.createEl('h4', {
|
||||
text: conflict.baseName,
|
||||
attr: { style: 'margin-bottom: 5px;' }
|
||||
});
|
||||
|
||||
container.createEl('div', { text: `Data: ${conflict.date}`, cls: 'conflict-meta' });
|
||||
container.createEl('div', { text: conflict.path, cls: 'conflict-meta-path' });
|
||||
|
||||
const previewContainer = container.createDiv();
|
||||
const actionsContainer = container.createDiv({ cls: 'conflict-actions' });
|
||||
|
||||
// Botão de Comparar
|
||||
new ButtonComponent(actionsContainer)
|
||||
.setButtonText(t('btn_compare'))
|
||||
.setIcon('eye')
|
||||
.onClick(async () => {
|
||||
if (previewContainer.hasChildNodes()) {
|
||||
previewContainer.empty();
|
||||
return;
|
||||
}
|
||||
await this.renderContentPreview(previewContainer, conflict);
|
||||
});
|
||||
|
||||
actionsContainer.createSpan({ attr: { style: 'flex-grow: 1;' } });
|
||||
|
||||
// Botão Manter Original
|
||||
new ButtonComponent(actionsContainer)
|
||||
.setButtonText(t('btn_keep_original'))
|
||||
.setTooltip(t('tooltip_keep_original'))
|
||||
.onClick(async () => {
|
||||
await this.manager.deleteConflict(conflict);
|
||||
this.refresh();
|
||||
});
|
||||
|
||||
// Botão Aceitar Conflito
|
||||
new ButtonComponent(actionsContainer)
|
||||
.setButtonText(t('btn_keep_conflict'))
|
||||
.setCta()
|
||||
.setTooltip(t('tooltip_keep_conflict'))
|
||||
.onClick(async () => {
|
||||
await this.manager.acceptConflict(conflict);
|
||||
this.refresh();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async renderContentPreview(container: HTMLElement, conflict: ConflictFile) {
|
||||
container.empty();
|
||||
const diffWrapper = container.createDiv({ cls: 'st-diff-container' });
|
||||
|
||||
// Lado Esquerdo (Original)
|
||||
const leftBox = diffWrapper.createDiv({ cls: 'st-diff-box' });
|
||||
leftBox.createDiv({ cls: 'st-diff-header', text: t('diff_original_header') });
|
||||
const leftContent = leftBox.createDiv({ cls: 'st-diff-content st-diff-original' });
|
||||
leftContent.setText(t('diff_loading'));
|
||||
|
||||
// Lado Direito (Conflito)
|
||||
const rightBox = diffWrapper.createDiv({ cls: 'st-diff-box' });
|
||||
rightBox.createDiv({ cls: 'st-diff-header', text: `${t('diff_conflict_header')} (${conflict.date})` });
|
||||
const rightContent = rightBox.createDiv({ cls: 'st-diff-content st-diff-conflict' });
|
||||
rightContent.setText(t('diff_loading'));
|
||||
|
||||
try {
|
||||
// Lê o arquivo de conflito
|
||||
const conflictText = await this.app.vault.read(conflict.file);
|
||||
rightContent.setText(conflictText);
|
||||
|
||||
// Tenta achar e ler o original
|
||||
const originalPath = conflict.path.replace(conflict.file.name, conflict.baseName);
|
||||
const originalFile = this.app.vault.getAbstractFileByPath(originalPath);
|
||||
|
||||
if (originalFile instanceof TFile) {
|
||||
const originalText = await this.app.vault.read(originalFile);
|
||||
leftContent.setText(originalText);
|
||||
} else {
|
||||
leftContent.setText(t('diff_original_missing'));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
leftContent.setText(t('diff_read_error'));
|
||||
rightContent.setText(t('diff_read_error'));
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.conflicts = this.manager.getConflicts();
|
||||
this.onOpen();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
this.onCloseCallback();
|
||||
}
|
||||
}
|
||||
181
src/ui/settings.ts
Normal file
181
src/ui/settings.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import { App, PluginSettingTab, Setting, Notice } from 'obsidian';
|
||||
import SyncthingController from '../main';
|
||||
import { SyncthingAPI } from '../api/syncthing-api';
|
||||
import { t, setLanguage } from '../lang/lang';
|
||||
|
||||
export class SyncthingSettingTab extends PluginSettingTab {
|
||||
plugin: SyncthingController;
|
||||
|
||||
constructor(app: App, plugin: SyncthingController) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
// --- CONEXÃO ---
|
||||
containerEl.createEl('h4', { text: t('setting_header_conn') });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t('setting_host_name'))
|
||||
.setDesc(t('setting_host_desc'))
|
||||
.addText(text => {
|
||||
text
|
||||
.setPlaceholder('127.0.0.1')
|
||||
.setValue(this.plugin.settings.syncthingHost)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.syncthingHost = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.style.width = '100%';
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t('setting_port_name'))
|
||||
.setDesc(t('setting_port_desc'))
|
||||
.addText(text => {
|
||||
text
|
||||
.setPlaceholder('8384')
|
||||
.setValue(this.plugin.settings.syncthingPort)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.syncthingPort = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t('setting_https_name'))
|
||||
.setDesc(t('setting_https_desc'))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.useHttps)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.useHttps = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
const api_key_Setting = new Setting(containerEl)
|
||||
.setName(t('setting_api_name'))
|
||||
.setDesc(t('setting_api_desc'))
|
||||
.addText(text => {
|
||||
text
|
||||
.setPlaceholder('...')
|
||||
.setValue(this.plugin.settings.syncthingApiKey)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.syncthingApiKey = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.type = 'password';
|
||||
text.inputEl.style.width = '100%';
|
||||
});
|
||||
|
||||
api_key_Setting.addButton(button => button
|
||||
.setButtonText(t('btn_test_conn'))
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
await this.plugin.testarApiApenas();
|
||||
}));
|
||||
|
||||
// --- PASTA ---
|
||||
containerEl.createEl('br');
|
||||
containerEl.createEl('h4', { text: t('setting_header_folder') });
|
||||
|
||||
const folderSetting = new Setting(containerEl)
|
||||
.setName(t('setting_folder_name'))
|
||||
.setDesc(t('setting_folder_desc'));
|
||||
|
||||
folderSetting.addDropdown(async (dropdown) => {
|
||||
const currentId = this.plugin.settings.syncthingFolderId;
|
||||
const currentLabel = this.plugin.settings.syncthingFolderLabel;
|
||||
const display = currentLabel || currentId || t('dropdown_none');
|
||||
dropdown.addOption(currentId, display);
|
||||
dropdown.setValue(currentId);
|
||||
|
||||
dropdown.onChange(async (value) => {
|
||||
this.plugin.settings.syncthingFolderId = value;
|
||||
const index = dropdown.selectEl.selectedIndex;
|
||||
if (index >= 0) {
|
||||
const label = dropdown.selectEl.options[index].text;
|
||||
this.plugin.settings.syncthingFolderLabel = label;
|
||||
} else {
|
||||
this.plugin.settings.syncthingFolderLabel = '';
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.verificarConexao(false);
|
||||
});
|
||||
});
|
||||
|
||||
folderSetting.addExtraButton(btn => btn
|
||||
.setIcon('search')
|
||||
.setTooltip(t('btn_search_folders'))
|
||||
.onClick(async () => {
|
||||
try {
|
||||
new Notice(t('notice_searching'));
|
||||
const folders = await SyncthingAPI.getFolders(this.plugin.apiUrl, this.plugin.settings.syncthingApiKey);
|
||||
|
||||
const selectEl = folderSetting.controlEl.querySelector('select');
|
||||
if (selectEl) selectEl.innerHTML = '';
|
||||
|
||||
const optionDefault = document.createElement('option');
|
||||
optionDefault.value = '';
|
||||
optionDefault.text = t('dropdown_default');
|
||||
selectEl?.appendChild(optionDefault);
|
||||
|
||||
folders.forEach((folder: any) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = folder.id;
|
||||
option.text = folder.label || folder.id;
|
||||
option.selected = (folder.id === this.plugin.settings.syncthingFolderId);
|
||||
selectEl?.appendChild(option);
|
||||
});
|
||||
|
||||
new Notice(`${folders.length} ${t('notice_folders_found')}`);
|
||||
|
||||
} catch (error) {
|
||||
new Notice(t('notice_fail_conn'));
|
||||
console.error(error);
|
||||
}
|
||||
}));
|
||||
|
||||
// --- INTERFACE ---
|
||||
containerEl.createEl('br');
|
||||
containerEl.createEl('h4', { text: t('setting_header_interface') });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t('setting_lang_name'))
|
||||
.setDesc(t('setting_lang_desc'))
|
||||
.addDropdown(dropDown => {
|
||||
dropDown.addOption('auto', 'Auto');
|
||||
dropDown.addOption('en', 'English');
|
||||
dropDown.addOption('pt', 'Português');
|
||||
dropDown.setValue(this.plugin.settings.language);
|
||||
dropDown.onChange(async (value) => {
|
||||
this.plugin.settings.language = value;
|
||||
await this.plugin.saveSettings();
|
||||
setLanguage(value); // Atualiza imediatamente
|
||||
this.display(); // Recarrega a interface para aplicar a tradução
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t('setting_status_bar_name'))
|
||||
.setDesc(t('setting_status_bar_desc'))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.showStatusBar)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showStatusBar = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t('setting_ribbon_name'))
|
||||
.setDesc(t('setting_ribbon_desc'))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.showRibbonIcon)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showRibbonIcon = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
133
src/ui/view.ts
Normal file
133
src/ui/view.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { ItemView, WorkspaceLeaf, setIcon } from 'obsidian';
|
||||
import SyncthingController from '../main';
|
||||
import { t } from '../lang/lang';
|
||||
import { ConflictManager } from '../services/conflict-manager';
|
||||
import { ConflictModal } from '../ui/conflict-modal';
|
||||
|
||||
export const VIEW_TYPE_SYNCTHING = 'syncthing-view';
|
||||
|
||||
export class SyncthingView extends ItemView {
|
||||
plugin: SyncthingController;
|
||||
conflictManager: ConflictManager;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: SyncthingController) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
this.conflictManager = new ConflictManager(plugin.app);
|
||||
}
|
||||
|
||||
getViewType() { return VIEW_TYPE_SYNCTHING; }
|
||||
getDisplayText() { return 'Syncthing Controller'; }
|
||||
getIcon() { return 'refresh-cw'; }
|
||||
|
||||
async onOpen() { this.render(); }
|
||||
async onClose() {}
|
||||
|
||||
render() {
|
||||
const container = this.containerEl.children[1];
|
||||
container.empty();
|
||||
container.addClass('syncthing-view-container');
|
||||
|
||||
// 1. Conflitos
|
||||
const conflicts = this.conflictManager.getConflicts();
|
||||
if (conflicts.length > 0) {
|
||||
const alertBox = container.createDiv({ cls: 'st-conflict-alert' });
|
||||
|
||||
// Estilos diretos no elemento são permitidos (o erro era passar no objeto {})
|
||||
alertBox.style.backgroundColor = 'var(--background-modifier-error)';
|
||||
alertBox.style.color = 'var(--text-on-accent)';
|
||||
alertBox.style.padding = '12px';
|
||||
alertBox.style.borderRadius = '6px';
|
||||
alertBox.style.marginBottom = '20px';
|
||||
alertBox.style.textAlign = 'center';
|
||||
alertBox.style.fontWeight = 'bold';
|
||||
alertBox.style.cursor = 'pointer';
|
||||
alertBox.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
|
||||
|
||||
const iconSpan = alertBox.createSpan();
|
||||
setIcon(iconSpan, 'alert-octagon');
|
||||
alertBox.createSpan({ text: ` ${conflicts.length} Conflito(s) Detectado(s)!` });
|
||||
|
||||
// Aqui usamos a CLASSE do CSS que você já copiou
|
||||
alertBox.createDiv({
|
||||
text: 'Clique aqui para resolver',
|
||||
cls: 'st-conflict-subtext'
|
||||
});
|
||||
|
||||
alertBox.addEventListener('click', () => {
|
||||
new ConflictModal(this.app, this.conflictManager, () => {
|
||||
this.render();
|
||||
}).open();
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Status
|
||||
const statusBox = container.createDiv({ cls: 'st-status-box' });
|
||||
const iconDiv = statusBox.createDiv({ cls: 'st-big-icon' });
|
||||
|
||||
const currentStatus = this.plugin.currentStatus;
|
||||
let color = 'var(--text-muted)';
|
||||
let statusText = t('status_unknown');
|
||||
|
||||
switch (currentStatus) {
|
||||
case 'conectado':
|
||||
color = 'var(--text-success)';
|
||||
statusText = t('status_synced');
|
||||
setIcon(iconDiv, 'check-circle');
|
||||
break;
|
||||
case 'sincronizando':
|
||||
color = 'var(--text-warning)';
|
||||
statusText = t('status_syncing');
|
||||
setIcon(iconDiv, 'loader');
|
||||
break;
|
||||
case 'desconectado':
|
||||
color = 'var(--text-muted)';
|
||||
statusText = t('status_offline');
|
||||
setIcon(iconDiv, 'wifi-off');
|
||||
break;
|
||||
case 'erro':
|
||||
color = 'var(--text-error)';
|
||||
statusText = t('status_error');
|
||||
setIcon(iconDiv, 'alert-triangle');
|
||||
break;
|
||||
}
|
||||
|
||||
iconDiv.style.color = color;
|
||||
statusBox.createDiv({ cls: 'st-status-text', text: statusText }).style.color = color;
|
||||
|
||||
// 3. Tabela
|
||||
const infoContainer = container.createDiv({ cls: 'st-info-container' });
|
||||
this.createRow(infoContainer, 'clock', t('info_last_sync'), this.plugin.lastSyncTime);
|
||||
this.createRow(infoContainer, 'monitor', t('info_devices'), this.plugin.connectedDevices.toString());
|
||||
|
||||
const folderDisplay = this.plugin.settings.syncthingFolderLabel || 'Default';
|
||||
this.createRow(infoContainer, 'folder', t('info_folder'), folderDisplay);
|
||||
|
||||
// 4. Botão
|
||||
const btnContainer = container.createDiv({ cls: 'st-btn-container' });
|
||||
const btn = btnContainer.createEl('button', { cls: 'mod-cta', text: t('btn_sync_now') });
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
btn.setText(t('btn_requesting'));
|
||||
btn.disabled = true;
|
||||
await this.plugin.forcarSincronizacao();
|
||||
});
|
||||
}
|
||||
|
||||
createRow(container: HTMLElement, icon: string, label: string, value: string) {
|
||||
const row = container.createDiv({ cls: 'st-info-row' });
|
||||
const left = row.createDiv({ cls: 'st-info-left' });
|
||||
setIcon(left.createSpan({ cls: 'st-info-icon' }), icon);
|
||||
left.createSpan({ text: label });
|
||||
|
||||
const valueDiv = row.createDiv({ cls: 'st-info-value', text: value });
|
||||
valueDiv.style.maxWidth = '150px';
|
||||
valueDiv.style.whiteSpace = 'nowrap';
|
||||
valueDiv.style.overflow = 'hidden';
|
||||
valueDiv.style.textOverflow = 'ellipsis';
|
||||
}
|
||||
|
||||
updateView() {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
175
styles.css
Normal file
175
styles.css
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
/* --- PAINEL LATERAL (VIEW) --- */
|
||||
|
||||
.syncthing-view-container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* Caixa de Status */
|
||||
.st-status-box {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.st-big-icon svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.st-status-text {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tabela de Informações */
|
||||
.st-info-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.st-info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.st-info-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.st-info-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.st-info-value {
|
||||
font-weight: bold;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
/* Botões */
|
||||
.st-btn-container button {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* --- ALERTA DE CONFLITO (VIEW) --- */
|
||||
|
||||
.st-conflict-alert {
|
||||
background-color: var(--background-modifier-error);
|
||||
color: var(--text-on-accent);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.st-conflict-alert:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Classe que resolve o erro do VS Code */
|
||||
.st-conflict-subtext {
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
margin-top: 4px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* --- MODAL DE RESOLUÇÃO DE CONFLITOS --- */
|
||||
|
||||
.conflict-item-box {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--background-primary);
|
||||
}
|
||||
|
||||
.conflict-meta {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.conflict-meta-path {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-faint);
|
||||
margin-bottom: 10px;
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
.conflict-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
/* --- COMPARAÇÃO DE CONTEÚDO (DIFF) --- */
|
||||
|
||||
.st-diff-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.st-diff-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.st-diff-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.st-diff-content {
|
||||
background-color: var(--background-primary-alt);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 0.8em;
|
||||
white-space: pre-wrap;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.st-diff-original {
|
||||
border-left: 3px solid var(--text-success);
|
||||
}
|
||||
|
||||
.st-diff-conflict {
|
||||
border-left: 3px solid var(--text-warning);
|
||||
}
|
||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in a new issue