first commit

This commit is contained in:
codeonquer 2024-03-26 15:25:29 +08:00
commit 8c488bdcc9
22 changed files with 5720 additions and 0 deletions

14
.editorconfig Normal file
View file

@ -0,0 +1,14 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
tab_width = 4
# 自动生成的 json 文件存在换行 diff
[*.json]
insert_final_newline = false

3
.eslintignore Normal file
View file

@ -0,0 +1,3 @@
node_modules/
main.js

27
.eslintrc Normal file
View file

@ -0,0 +1,27 @@
{
"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"
}
}

34
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,34 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "16.x"
- name: Build plugin
run: |
npm install
npm run build
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
gh release create "$tag" \
--title="$tag" \
--draft \
main.js manifest.json styles.css

22
.gitignore vendored Normal file
View 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
View file

@ -0,0 +1 @@
tag-version-prefix=""

56
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,56 @@
{
"javascript.format.enable": false,
"typescript.format.enable": false,
"vetur.format.defaultFormatter.html": "none",
"vetur.format.defaultFormatter.js": "none",
"vetur.format.defaultFormatter.ts": "none",
"prettier.enable": false,
"editor.formatOnSave": false,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue"
],
"stylelint.validate": [
"css",
"scss"
],
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[javascriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[typescriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[css]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint",
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": "explicit"
}
},
"[scss]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint",
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": "explicit"
}
}
}

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 codeonquer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

19
README.md Normal file
View file

@ -0,0 +1,19 @@
# Obsidian Sync ConfigFolder to CommonFolder
The development of this plugin was motivated by the following two reasons:
- The config folder for Obsidian must start with a dot (`.`);
- Some synchronization tools used do not support syncing folders that start with a dot (`.`).
Therefore, the plugin was developed to synchronize the contents of the config folder to a common folder with the command `Sync ConfigFolder to CommonFolder`, and it also supports restoring the config folder from the common folder with the command `Restore ConfigFolder from CommonFolder`.
The settings for the common folder support both relative and absolute paths, with relative paths based on the root folder of the current Vault.
开发本插件的原因是基于以下两点:
- obsidian 的配置路径必须以 `.` 开头;
- 使用的部分同步工具不支持同步以 `.` 开头的目录;
所以开发该插件将配置目录的内容同步到普通目录之中(`Sync ConfigFolder to CommonFolder` 命令),也支持将配置目录复原到配置目录之中(`Restore ConfigFolder from CommonFolder` 命令)。
设置的普通目录支持相对路径和绝对路径,相对路径基于当前 Vault 的根目录。

48
esbuild.config.mjs Normal file
View file

@ -0,0 +1,48 @@
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: ["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",
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

3
main.ts Normal file
View file

@ -0,0 +1,3 @@
import { SyncPlugin } from './modules/SyncPlugin';
export default SyncPlugin;

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "sync-config-folder-to-common-folder",
"name": "Sync ConfigFolder to CommonFolder",
"description": "Sync ConfigFolder to CommonFolder",
"author": "Codeonquer",
"authorUrl": "https://github.com/codeonquer",
"isDesktopOnly": true,
"version": "1.0.0",
"minAppVersion": "0.15.0"
}

137
modules/SyncModal.ts Normal file
View file

@ -0,0 +1,137 @@
import { App, Modal } from 'obsidian';
import { SyncPlugin } from './SyncPlugin';
export enum SyncModel {
None,
Sync,
Restore
}
export class SyncModal extends Modal {
syncModel: SyncModel;
plugin: SyncPlugin;
constructor(app: App, syncModel: SyncModel, plugin: SyncPlugin) {
super(app);
this.syncModel = syncModel;
this.plugin = plugin;
}
get fs() {
// @ts-ignore
return this.app.vault.adapter.fs;
}
get path() {
// @ts-ignore
return this.app.vault.adapter.path;
}
get basePath() {
// @ts-ignore
return this.app.vault.adapter.basePath;
}
get configFolder() {
const configDir = this.plugin.settings.configFolderPath || this.app.vault.configDir;
return this.path.join(this.basePath, configDir);
}
get commonFolderPath() {
return this.plugin.settings.commonFolderPath;
}
get commonFolder() {
return this.path.isAbsolute(this.commonFolderPath)
? this.commonFolderPath
: this.path.join(this.basePath, this.commonFolderPath)
}
logNormal (text: string, contentEl?: HTMLElement, ) {
if (contentEl) {
contentEl.createDiv({ text })
}
}
logSuccess (text: string, contentEl?: HTMLElement) {
if (contentEl) {
contentEl.createDiv({ text, attr: { style: 'color: green;' } })
}
}
logFail (text: string, contentEl?: HTMLElement) {
if (contentEl) {
contentEl.createDiv({ text, attr: { style: 'color: red;' } })
}
}
syncConfig(contentEl?: HTMLElement) {
try {
this.logNormal('Check if ConfigFolder exists (检查 ConfigFolder 是否存在) ...', contentEl);
const isConfigFolderExisted = this.fs.existsSync(this.configFolder);
if (!isConfigFolderExisted) {
this.logSuccess('ConfigFolder does not exist, no need to back up (ConfigFolder 不存在,无需备份) .', contentEl);
return;
}
this.logNormal('Reset CommonFolder (重置 CommonFolder 中) ...', contentEl);
const isCommonFolderExisted = this.fs.existsSync(this.commonFolder);
if (isCommonFolderExisted) {
this.fs.rmSync(this.commonFolder, { force: true, recursive: true });
}
this.fs.mkdirSync(this.commonFolder, { recursive: true });
this.logSuccess('Reset CommonFolder successfully (重置 CommonFolder 成功) .', contentEl);
this.logNormal('Back up ConfigFolder (备份 ConfigFolder 中) ...', contentEl);
this.fs.cpSync(this.configFolder, this.commonFolder, { recursive: true });
this.logSuccess('Backup ConfigFolder successful (备份 ConfigFolder 成功) .', contentEl);
} catch (e) {
this.logFail(e.message, contentEl);
}
}
restoreConfig(contentEl: HTMLElement) {
try {
this.logNormal('Check if CommonFolder exists (检查 CommonFolder 是否存在) ...', contentEl);
const isCommonFolderExisted = this.fs.existsSync(this.commonFolder);
if (!isCommonFolderExisted) {
this.logSuccess('CommonFolder does not exist and cannot be restored (CommonFolder 不存在,无法恢复) .', contentEl);
return;
}
this.logNormal('Reset ConfigFolder (重置 ConfigFolder 中) ...', contentEl);
const isConfigFolderExisted = this.fs.existsSync(this.configFolder);
if (isConfigFolderExisted) {
this.fs.rmSync(this.configFolder, { force: true, recursive: true });
}
this.fs.mkdirSync(this.configFolder, { recursive: true });
this.logSuccess('Reset ConfigFolder successfully (重置 ConfigFolder 成功) .', contentEl);
this.logNormal('Restore ConfigFolder (恢复 ConfigFolder 中) ...', contentEl);
this.fs.cpSync(this.commonFolder, this.configFolder, { recursive: true });
this.logSuccess('Restore ConfigFolder successfully (恢复 ConfigFolder 成功) .', contentEl);
} catch (e) {
this.logFail(e.message, contentEl);
}
}
onOpen() {
window.setTimeout(() => {
const { contentEl } = this;
if (this.syncModel === SyncModel.Restore) {
this.restoreConfig(contentEl);
return;
}
this.syncConfig(contentEl);
}, 300);
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

52
modules/SyncPlugin.ts Normal file
View file

@ -0,0 +1,52 @@
import { Plugin } from 'obsidian';
import { ISyncPluginSettings, DEFAULT_SETTINGS } from './settings';
import { SyncModal, SyncModel } from './SyncModal';
import { SyncSettingTab } from './SyncSettingTab';
export class SyncPlugin extends Plugin {
settings: ISyncPluginSettings;
async onload() {
await this.loadSettings();
this.addCommand({
id: 'sync-config-folder-to-common-folder',
name: 'Sync ConfigFolder to CommonFolder',
callback: () => {
new SyncModal(this.app, SyncModel.Sync, this).open();
}
});
this.addCommand({
id: 'restore-config-folder-from-common-folder',
name: 'Restore ConfigFolder from CommonFolder',
callback: () => {
new SyncModal(this.app, SyncModel.Restore, this).open();
}
});
this.addSettingTab(new SyncSettingTab(this.app, this));
if (this.settings.autoSaveInterval) {
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window.setInterval(() => console.log('setInterval'), this.settings.autoSaveInterval * 1000));
}
}
onunload() {
}
async loadSettings() {
const originData = await this.loadData();
console.log('sync-config-folder-to-common-folder loadData ', originData);
this.settings = Object.assign({}, DEFAULT_SETTINGS, originData);
console.log('sync-config-folder-to-common-folder loadSettings ', this.settings);
}
async saveSettings() {
console.log('sync-config-folder-to-common-folder saveData ', this.settings);
await this.saveData(this.settings);
}
}

54
modules/SyncSettingTab.ts Normal file
View file

@ -0,0 +1,54 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import { DEFAULT_CONFIG_FOLDER_PATH, DEFAULT_COMMON_FOLDER_PATH, DEFAULT_INTERVAL } from './settings';
import { SyncPlugin } from './SyncPlugin';
export class SyncSettingTab extends PluginSettingTab {
plugin: SyncPlugin;
constructor(app: App, plugin: SyncPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h1', { text: 'The following changes need to be restarted to take effect (以下修改都需要重启生效)' });
new Setting(containerEl)
.setName('Config Folder Path')
.setDesc('Customize the path filled in ConfigFolder, which needs to be consistent with the one in Files and links->Override config folder (自定义填入 ConfigFolder 的路径,需要和 文件与链接->切换配置文件夹 中的保持一致)')
.addText(text => text
.setPlaceholder(DEFAULT_CONFIG_FOLDER_PATH)
.setValue(this.plugin.settings.configFolderPath)
.onChange(async (value) => {
this.plugin.settings.configFolderPath = value || DEFAULT_CONFIG_FOLDER_PATH;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Common Folder Path')
.setDesc('Customize the path to be filled in ConfigFolder for synchronization. The relative path needs to start with ./ or ../ (自定义填入 ConfigFolder 同步的路径,相对路径需要以 ./ 或者 ../ 开头)')
.addText(text => text
.setPlaceholder(DEFAULT_COMMON_FOLDER_PATH)
.setValue(this.plugin.settings.commonFolderPath)
.onChange(async (value) => {
this.plugin.settings.commonFolderPath = value || DEFAULT_COMMON_FOLDER_PATH;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Auto Save Interval')
.setDesc('Customize the auto save time interval in seconds, 0 means off (自定义自动保存的时间间隔单位是秒0 代表关闭)')
.addText(text => text
.setPlaceholder(String(DEFAULT_INTERVAL))
.setValue(String(this.plugin.settings.autoSaveInterval))
.onChange(async (value) => {
this.plugin.settings.autoSaveInterval = Number(value) || DEFAULT_INTERVAL;
await this.plugin.saveSettings();
}));
}
}

15
modules/settings.ts Normal file
View file

@ -0,0 +1,15 @@
export interface ISyncPluginSettings {
configFolderPath: string;
commonFolderPath: string;
autoSaveInterval: number;
}
export const DEFAULT_CONFIG_FOLDER_PATH = '.obsidian';
export const DEFAULT_COMMON_FOLDER_PATH = './configBackupFolder';
export const DEFAULT_INTERVAL = 0;
export const DEFAULT_SETTINGS: ISyncPluginSettings = {
configFolderPath: DEFAULT_CONFIG_FOLDER_PATH,
commonFolderPath: DEFAULT_COMMON_FOLDER_PATH,
autoSaveInterval: DEFAULT_INTERVAL
};

5125
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

30
package.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "obsidian-sync-config-folder-to-common-folder",
"version": "1.0.0",
"description": "Obsidian Sync ConfigFolder to CommonFolder",
"keywords": [],
"author": "codeonquer",
"repository": {
"url": "https://github.com/codeonquer/obsidian-sync-config-folder-to-common-folder"
},
"license": "MIT",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"editorconfig": "2.0.0",
"esbuild": "0.17.3",
"eslint": "8.57.0",
"obsidian": "latest",
"stylelint": "16.3.0",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
}

8
styles.css Normal file
View file

@ -0,0 +1,8 @@
/*
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/

24
tsconfig.json Normal file
View 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"
]
}

14
version-bump.mjs Normal file
View file

@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
let 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
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}