mirror of
https://github.com/xheldon/git-folder-sync.git
synced 2026-07-22 06:59:00 +00:00
953 lines
No EOL
28 KiB
TypeScript
953 lines
No EOL
28 KiB
TypeScript
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, Menu, TFolder } from 'obsidian';
|
|
import { GitSyncSettings, DEFAULT_SETTINGS, SyncResult } from './types';
|
|
import { GitHubService } from './github-service';
|
|
import { setLanguage, t, getSupportedLanguages, getActualLanguage } from './i18n-simple';
|
|
import { FileCacheService } from './file-cache';
|
|
|
|
export default class GitSyncPlugin extends Plugin {
|
|
settings: GitSyncSettings;
|
|
githubService: GitHubService;
|
|
fileCacheService: FileCacheService;
|
|
ribbonIconEl: HTMLElement | null = null;
|
|
statusBarEl: HTMLElement | null = null;
|
|
currentFile: TFile | null = null;
|
|
private fileModifyTimeout: NodeJS.Timeout | null = null;
|
|
|
|
async onload() {
|
|
await this.loadSettings();
|
|
|
|
// Initialize language settings
|
|
setLanguage(this.settings.language);
|
|
|
|
this.githubService = new GitHubService(this.settings.githubToken);
|
|
this.fileCacheService = new FileCacheService();
|
|
|
|
// Add ribbon icon based on settings
|
|
this.updateRibbonIcon();
|
|
|
|
// Add status bar
|
|
this.statusBarEl = this.addStatusBarItem();
|
|
this.statusBarEl.addClass('git-sync-status-bar');
|
|
|
|
// Listen for file switching events
|
|
this.registerEvent(
|
|
this.app.workspace.on('active-leaf-change', () => {
|
|
this.updateStatusBar();
|
|
})
|
|
);
|
|
|
|
// Listen for file open events
|
|
this.registerEvent(
|
|
this.app.workspace.on('file-open', (file) => {
|
|
this.currentFile = file;
|
|
this.updateStatusBar();
|
|
})
|
|
);
|
|
|
|
// Listen for file content modification events
|
|
this.registerEvent(
|
|
this.app.vault.on('modify', (file) => {
|
|
// Only handle markdown files that are currently open
|
|
if (file.path.endsWith('.md') && this.currentFile && file.path === this.currentFile.path) {
|
|
console.log(`Detected file modification: ${file.path}`);
|
|
// Type conversion to TFile
|
|
if (file instanceof TFile) {
|
|
this.onFileContentModified(file);
|
|
}
|
|
}
|
|
})
|
|
);
|
|
|
|
// Add sync button to note editing interface
|
|
this.addCommand({
|
|
id: 'show-sync-menu',
|
|
name: t('command.show.sync.menu'),
|
|
editorCallback: (editor: Editor, view: MarkdownView) => {
|
|
this.showSyncMenu(editor, view);
|
|
}
|
|
});
|
|
|
|
// Add editor right-click menu
|
|
this.registerEvent(
|
|
this.app.workspace.on('editor-menu', (menu: Menu, editor: Editor, view: MarkdownView) => {
|
|
menu.addItem((item) => {
|
|
item
|
|
.setTitle(t('plugin.name'))
|
|
.setIcon('sync')
|
|
.onClick(async () => {
|
|
this.showSyncMenu(editor, view);
|
|
});
|
|
});
|
|
})
|
|
);
|
|
|
|
// Add settings page
|
|
this.addSettingTab(new GitSyncSettingTab(this.app, this));
|
|
|
|
// Initial status bar update
|
|
this.updateStatusBar();
|
|
}
|
|
|
|
onunload() {
|
|
// Clean up timers
|
|
if (this.fileModifyTimeout) {
|
|
clearTimeout(this.fileModifyTimeout);
|
|
this.fileModifyTimeout = null;
|
|
}
|
|
}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
this.githubService.updateToken(this.settings.githubToken);
|
|
// Update language settings
|
|
setLanguage(this.settings.language);
|
|
// Update ribbon icon display state
|
|
this.updateRibbonIcon();
|
|
}
|
|
|
|
private updateRibbonIcon() {
|
|
// Remove existing button
|
|
if (this.ribbonIconEl) {
|
|
this.ribbonIconEl.remove();
|
|
this.ribbonIconEl = null;
|
|
}
|
|
|
|
// Add button based on settings
|
|
if (this.settings.showRibbonIcon) {
|
|
this.ribbonIconEl = this.addRibbonIcon('settings', t('settings.title'), (evt: MouseEvent) => {
|
|
// Directly open plugin settings interface
|
|
(this.app as any).setting.open();
|
|
(this.app as any).setting.openTabById(this.manifest.id);
|
|
});
|
|
}
|
|
}
|
|
|
|
private showSyncMenu(editor: Editor, view: MarkdownView) {
|
|
const menu = new Menu();
|
|
|
|
menu.addItem((item) => {
|
|
item
|
|
.setTitle(t('actions.sync.current.to.remote'))
|
|
.setIcon('upload')
|
|
.onClick(async () => {
|
|
await this.syncCurrentFileToRemote(view.file);
|
|
});
|
|
});
|
|
|
|
menu.addItem((item) => {
|
|
item
|
|
.setTitle(t('actions.pull.remote.to.current'))
|
|
.setIcon('download')
|
|
.onClick(async () => {
|
|
await this.pullRemoteToCurrentFile(view.file);
|
|
});
|
|
});
|
|
|
|
const rect = (editor as any).containerEl?.getBoundingClientRect();
|
|
if (rect) {
|
|
menu.showAtPosition({ x: rect.right - 100, y: rect.top + 50 });
|
|
} else {
|
|
// If unable to get editor position, show at mouse position
|
|
menu.showAtMouseEvent(new MouseEvent('click'));
|
|
}
|
|
}
|
|
|
|
async syncCurrentFileToRemote(file: TFile): Promise<void> {
|
|
if (!this.settings.githubToken || !this.settings.repositoryUrl) {
|
|
new Notice(t('settings.github.token.or.repo.not.configured'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const content = await this.app.vault.read(file);
|
|
const result = await this.githubService.uploadFile(
|
|
this.settings.repositoryUrl,
|
|
file.path,
|
|
content
|
|
);
|
|
|
|
if (result.success) {
|
|
new Notice(t('sync.success', { filename: file.name }));
|
|
|
|
// Update cache - file synced to remote
|
|
await this.fileCacheService.updateFileCache(
|
|
file.path,
|
|
file.path,
|
|
new Date().toISOString(), // Use current time as last modified time
|
|
'', // SHA value can be obtained from upload result later
|
|
true, // Published
|
|
this.app.vault
|
|
);
|
|
} else {
|
|
new Notice(t('sync.failed', { message: result.message }));
|
|
}
|
|
} catch (error) {
|
|
console.error('File sync failed:', error);
|
|
new Notice(t('sync.error'));
|
|
}
|
|
}
|
|
|
|
async pullRemoteToCurrentFile(file: TFile): Promise<void> {
|
|
if (!this.settings.githubToken || !this.settings.repositoryUrl) {
|
|
new Notice(t('settings.github.token.or.repo.not.configured'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await this.githubService.downloadFile(
|
|
this.settings.repositoryUrl,
|
|
file.path
|
|
);
|
|
|
|
if (result.success && result.content) {
|
|
await this.app.vault.modify(file, result.content);
|
|
new Notice(t('pull.success', { filename: file.name }));
|
|
|
|
// Update cache - file synced from remote
|
|
await this.fileCacheService.updateFileCache(
|
|
file.path,
|
|
file.path,
|
|
new Date().toISOString(), // Use current time as last modified time
|
|
'', // SHA value can be obtained from download result later
|
|
true, // Published
|
|
this.app.vault
|
|
);
|
|
} else {
|
|
new Notice(t('pull.failed', { message: result.message }));
|
|
}
|
|
} catch (error) {
|
|
console.error('File pull failed:', error);
|
|
new Notice(t('pull.error'));
|
|
}
|
|
}
|
|
|
|
async initializeRepository(): Promise<SyncResult> {
|
|
if (!this.isVaultEmpty()) {
|
|
return { success: false, message: t('vault.not.empty') };
|
|
}
|
|
|
|
try {
|
|
const result = await this.githubService.downloadAllFiles(this.settings.repositoryUrl);
|
|
if (result.success && result.files) {
|
|
for (const file of result.files) {
|
|
await this.createFileInVault(file.path, file.content || '');
|
|
|
|
// Update cache - file initialized from remote
|
|
await this.fileCacheService.updateFileCache(
|
|
file.path,
|
|
file.path,
|
|
new Date().toISOString(), // Use current time as last modified time
|
|
'', // SHA value can be obtained from download result later
|
|
true, // Published
|
|
this.app.vault
|
|
);
|
|
}
|
|
|
|
// Refresh status bar display
|
|
setTimeout(() => this.updateStatusBar(), 2000);
|
|
|
|
return { success: true, message: t('repo.init.success'), filesProcessed: result.files.length };
|
|
}
|
|
return { success: false, message: result.message };
|
|
} catch (error) {
|
|
return { success: false, message: t('repo.init.failed', { error: error.message }) };
|
|
}
|
|
}
|
|
|
|
async forceSyncRemoteToLocal(): Promise<SyncResult> {
|
|
try {
|
|
const result = await this.githubService.downloadAllFiles(this.settings.repositoryUrl);
|
|
if (result.success && result.files) {
|
|
for (const file of result.files) {
|
|
await this.createFileInVault(file.path, file.content || '');
|
|
|
|
// Update cache - file synced from remote
|
|
await this.fileCacheService.updateFileCache(
|
|
file.path,
|
|
file.path,
|
|
new Date().toISOString(), // Use current time as last modified time
|
|
'', // SHA value can be obtained from download result later
|
|
true, // Published
|
|
this.app.vault
|
|
);
|
|
}
|
|
|
|
// Refresh status bar display
|
|
setTimeout(() => this.updateStatusBar(), 2000);
|
|
|
|
return { success: true, message: t('force.sync.remote.to.local.success'), filesProcessed: result.files.length };
|
|
}
|
|
return { success: false, message: result.message };
|
|
} catch (error) {
|
|
return { success: false, message: t('force.sync.remote.to.local.failed', { error: error.message }) };
|
|
}
|
|
}
|
|
|
|
async forceSyncLocalToRemote(): Promise<SyncResult> {
|
|
try {
|
|
const files = this.getAllVaultFiles();
|
|
console.log('Found files:', files.map(f => f.path));
|
|
|
|
if (files.length === 0) {
|
|
return { success: false, message: t('no.syncable.files.in.vault') };
|
|
}
|
|
|
|
let processed = 0;
|
|
let failed = 0;
|
|
|
|
for (const file of files) {
|
|
try {
|
|
const content = await this.app.vault.read(file);
|
|
const result = await this.githubService.uploadFile(
|
|
this.settings.repositoryUrl,
|
|
file.path,
|
|
content
|
|
);
|
|
|
|
if (result.success) {
|
|
processed++;
|
|
console.log(`Successfully synced file: ${file.path}`);
|
|
|
|
// Update cache immediately - file synced to remote
|
|
await this.fileCacheService.updateFileCache(
|
|
file.path,
|
|
file.path,
|
|
new Date().toISOString(), // Use current time as last modified time
|
|
'', // SHA value can be obtained from upload result later
|
|
true, // Published
|
|
this.app.vault
|
|
);
|
|
} else {
|
|
failed++;
|
|
console.error(`File sync failed: ${file.path}`, result.message);
|
|
}
|
|
} catch (error) {
|
|
failed++;
|
|
console.error(`Failed to read or sync file: ${file.path}`, error);
|
|
}
|
|
}
|
|
|
|
const message = failed > 0
|
|
? t('force.sync.local.to.remote.success', { processed, failed })
|
|
: t('force.sync.local.to.remote.success.no.failures');
|
|
|
|
// Refresh status bar display
|
|
setTimeout(() => this.updateStatusBar(), 2000);
|
|
|
|
return { success: true, message, filesProcessed: processed };
|
|
} catch (error) {
|
|
console.error('Force sync failed:', error);
|
|
return { success: false, message: t('force.sync.local.to.remote.failed', { error: error.message }) };
|
|
}
|
|
}
|
|
|
|
private isVaultEmpty(): boolean {
|
|
const files = this.app.vault.getFiles();
|
|
return files.filter(file => !file.path.startsWith('.obsidian')).length === 0;
|
|
}
|
|
|
|
private getAllVaultFiles(): TFile[] {
|
|
const allFiles = this.app.vault.getFiles();
|
|
console.log('All files in vault:', allFiles.map(f => f.path));
|
|
|
|
// Filter out files in .obsidian folder
|
|
const filteredFiles = allFiles.filter(file => !file.path.startsWith('.obsidian'));
|
|
console.log('Filtered files:', filteredFiles.map(f => f.path));
|
|
|
|
return filteredFiles;
|
|
}
|
|
|
|
private async createFileInVault(path: string, content: string): Promise<void> {
|
|
const folderPath = path.substring(0, path.lastIndexOf('/'));
|
|
if (folderPath && !this.app.vault.getAbstractFileByPath(folderPath)) {
|
|
await this.app.vault.createFolder(folderPath);
|
|
}
|
|
|
|
const existingFile = this.app.vault.getAbstractFileByPath(path);
|
|
if (existingFile instanceof TFile) {
|
|
await this.app.vault.modify(existingFile, content);
|
|
} else {
|
|
await this.app.vault.create(path, content);
|
|
}
|
|
}
|
|
|
|
private async updateStatusBar() {
|
|
if (!this.statusBarEl) return;
|
|
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (!activeFile || !activeFile.path.endsWith('.md')) {
|
|
this.statusBarEl.empty();
|
|
this.statusBarEl.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
this.currentFile = activeFile;
|
|
this.statusBarEl.style.display = 'flex';
|
|
this.statusBarEl.empty();
|
|
|
|
// Create status text
|
|
const statusText = this.statusBarEl.createEl('span', {
|
|
cls: 'git-sync-status-text',
|
|
text: t('status.bar.checking')
|
|
});
|
|
|
|
// Create sync button
|
|
const syncButton = this.statusBarEl.createEl('button', {
|
|
cls: 'git-sync-status-button',
|
|
text: t('status.bar.sync')
|
|
});
|
|
|
|
// Check file remote status
|
|
if (this.settings.githubToken && this.settings.repositoryUrl) {
|
|
try {
|
|
// First check cache
|
|
const cache = this.fileCacheService.getFileCache(activeFile.path);
|
|
|
|
if (cache) {
|
|
// Use cached data
|
|
console.log(`Using cached data to display file status: ${activeFile.path}`);
|
|
|
|
if (cache.isPublished) {
|
|
const date = new Date(cache.lastModified);
|
|
let statusMsg = t('status.bar.last.modified', { date: this.formatDate(date) });
|
|
|
|
// Use isSynced status from cache directly, no need to recalculate hash
|
|
if (!cache.isSynced) {
|
|
statusMsg += ` (${t('status.bar.local.modified')})`;
|
|
statusText.style.color = 'var(--color-orange)';
|
|
} else {
|
|
statusMsg += ` (${t('status.bar.synced')})`;
|
|
statusText.style.color = 'var(--color-green)';
|
|
}
|
|
|
|
statusText.textContent = statusMsg;
|
|
} else {
|
|
statusText.textContent = t('status.bar.not.published');
|
|
statusText.style.color = 'var(--text-muted)';
|
|
}
|
|
|
|
// Asynchronously update cache if it's about to expire
|
|
if (!this.fileCacheService.isCacheValid(cache, 4 * 60 * 1000)) { // Start async refresh after 4 minutes
|
|
this.refreshFileCache(activeFile.path).catch(console.error);
|
|
}
|
|
} else {
|
|
// No cache, need to fetch from GitHub
|
|
console.log(`No cache, fetching file status from GitHub: ${activeFile.path}`);
|
|
await this.fetchAndCacheFileStatus(activeFile.path, statusText);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to update status bar:', error);
|
|
statusText.textContent = t('status.bar.check.failed');
|
|
}
|
|
} else {
|
|
statusText.textContent = t('status.bar.not.configured');
|
|
}
|
|
|
|
// Add button click event
|
|
syncButton.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
this.showSyncMenuForStatusBar(activeFile);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle file content modification events (with debounce)
|
|
*/
|
|
private async onFileContentModified(file: TFile) {
|
|
// Clear previous timer
|
|
if (this.fileModifyTimeout) {
|
|
clearTimeout(this.fileModifyTimeout);
|
|
}
|
|
|
|
// Set debounce, execute after 2000ms
|
|
this.fileModifyTimeout = setTimeout(async () => {
|
|
try {
|
|
// Get current file cache
|
|
const cache = this.fileCacheService.getFileCache(file.path);
|
|
if (!cache) {
|
|
// No cache, update status bar directly
|
|
this.updateStatusBar();
|
|
return;
|
|
}
|
|
|
|
// Check if file was actually modified
|
|
const content = await this.app.vault.read(file);
|
|
const currentHash = FileCacheService.calculateContentHash(content);
|
|
|
|
if (currentHash !== cache.contentHash) {
|
|
console.log(`File content modified: ${file.path}`);
|
|
|
|
// Update content hash and sync status in cache
|
|
const updatedCache = {
|
|
...cache,
|
|
contentHash: currentHash,
|
|
isSynced: false, // Mark as not synced
|
|
cacheTime: Date.now()
|
|
};
|
|
|
|
this.fileCacheService.setFileCache(file.path, updatedCache);
|
|
|
|
// Update status bar display immediately
|
|
this.updateStatusBar();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to handle file modification event:', error);
|
|
}
|
|
}, 2000);
|
|
}
|
|
|
|
/**
|
|
* Fetch file status from GitHub and cache it
|
|
*/
|
|
private async fetchAndCacheFileStatus(filePath: string, statusText: HTMLElement) {
|
|
try {
|
|
// Check rate limit status first
|
|
const rateLimitCheck = await this.githubService.checkRateLimit();
|
|
|
|
if (!rateLimitCheck.canProceed) {
|
|
statusText.textContent = t('status.bar.rate.limit', { minutes: rateLimitCheck.waitMinutes });
|
|
return;
|
|
}
|
|
|
|
const result = await this.githubService.getFileLastModified(
|
|
this.settings.repositoryUrl,
|
|
filePath
|
|
);
|
|
|
|
if (result.success) {
|
|
if (result.exists && result.lastModified) {
|
|
const date = new Date(result.lastModified);
|
|
statusText.textContent = t('status.bar.last.modified', { date: this.formatDate(date) });
|
|
statusText.style.color = 'var(--text-normal)';
|
|
|
|
// Update cache
|
|
await this.fileCacheService.updateFileCache(
|
|
filePath,
|
|
filePath, // GitHub path same as local path
|
|
result.lastModified,
|
|
'', // SHA value temporarily empty, can be obtained from other APIs later
|
|
true,
|
|
this.app.vault
|
|
);
|
|
} else {
|
|
statusText.textContent = t('status.bar.not.published');
|
|
statusText.style.color = 'var(--text-muted)';
|
|
|
|
// Cache unpublished status
|
|
await this.fileCacheService.updateFileCache(
|
|
filePath,
|
|
filePath,
|
|
'',
|
|
'',
|
|
false,
|
|
this.app.vault
|
|
);
|
|
}
|
|
} else {
|
|
statusText.textContent = t('status.bar.check.failed');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch file status:', error);
|
|
statusText.textContent = t('status.bar.check.failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Asynchronously refresh file cache
|
|
*/
|
|
private async refreshFileCache(filePath: string) {
|
|
try {
|
|
console.log(`Asynchronously refreshing file cache: ${filePath}`);
|
|
|
|
// Lightweight rate limit check - temporarily skip, let GitHub API handle rate limits itself
|
|
|
|
const result = await this.githubService.getFileLastModified(
|
|
this.settings.repositoryUrl,
|
|
filePath
|
|
);
|
|
|
|
if (result.success) {
|
|
await this.fileCacheService.updateFileCache(
|
|
filePath,
|
|
filePath,
|
|
result.lastModified || '',
|
|
'', // SHA value temporarily empty
|
|
result.exists || false,
|
|
this.app.vault
|
|
);
|
|
console.log(`File cache refreshed: ${filePath}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to refresh file cache:', error);
|
|
}
|
|
}
|
|
|
|
private showSyncMenuForStatusBar(file: TFile) {
|
|
const menu = new Menu();
|
|
|
|
menu.addItem((item) => {
|
|
item
|
|
.setTitle(t('actions.sync.current.to.remote'))
|
|
.setIcon('upload')
|
|
.onClick(async () => {
|
|
await this.syncCurrentFileToRemote(file);
|
|
// Update status bar
|
|
setTimeout(() => this.updateStatusBar(), 1000);
|
|
});
|
|
});
|
|
|
|
menu.addItem((item) => {
|
|
item
|
|
.setTitle(t('actions.pull.remote.to.current'))
|
|
.setIcon('download')
|
|
.onClick(async () => {
|
|
await this.pullRemoteToCurrentFile(file);
|
|
// Update status bar
|
|
setTimeout(() => this.updateStatusBar(), 1000);
|
|
});
|
|
});
|
|
|
|
// Show menu near status bar button
|
|
const rect = this.statusBarEl?.getBoundingClientRect();
|
|
if (rect) {
|
|
menu.showAtPosition({ x: rect.left, y: rect.top - 10 });
|
|
}
|
|
}
|
|
|
|
private formatDate(date: Date): string {
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
|
|
if (diffDays === 0) {
|
|
return t('date.today');
|
|
} else if (diffDays === 1) {
|
|
return t('date.yesterday');
|
|
} else if (diffDays < 7) {
|
|
return t('date.days.ago', { days: diffDays });
|
|
} else {
|
|
return date.toLocaleDateString('zh-CN');
|
|
}
|
|
}
|
|
}
|
|
|
|
class GitSyncSettingTab extends PluginSettingTab {
|
|
plugin: GitSyncPlugin;
|
|
|
|
constructor(app: App, plugin: GitSyncPlugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
display(): void {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
containerEl.createEl('h2', { text: t('settings.title') });
|
|
|
|
// Language setting - put at the beginning
|
|
new Setting(containerEl)
|
|
.setName(t('settings.language.name'))
|
|
.setDesc(t('settings.language.desc'))
|
|
.addDropdown(dropdown => {
|
|
const languages = getSupportedLanguages();
|
|
languages.forEach(lang => {
|
|
dropdown.addOption(lang.value, lang.label);
|
|
});
|
|
dropdown.setValue(this.plugin.settings.language);
|
|
dropdown.onChange(async (value) => {
|
|
this.plugin.settings.language = value as 'zh' | 'en' | 'auto';
|
|
await this.plugin.saveSettings();
|
|
// Re-render settings interface
|
|
this.display();
|
|
});
|
|
});
|
|
|
|
new Setting(containerEl)
|
|
.setName(t('settings.github.token.name'))
|
|
.setDesc(t('settings.github.token.desc'))
|
|
.addText(text => text
|
|
.setPlaceholder(t('settings.github.token.placeholder'))
|
|
.setValue(this.plugin.settings.githubToken)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.githubToken = value;
|
|
}));
|
|
|
|
let pathInfoEl: HTMLElement;
|
|
new Setting(containerEl)
|
|
.setName(t('settings.github.repo.name'))
|
|
.setDesc(t('settings.github.repo.desc'))
|
|
.addText(text => text
|
|
.setPlaceholder(t('settings.github.repo.placeholder'))
|
|
.setValue(this.plugin.settings.repositoryUrl)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.repositoryUrl = value;
|
|
this.updatePathInfo(pathInfoEl, value);
|
|
}));
|
|
|
|
// Add path description display area
|
|
pathInfoEl = containerEl.createEl('div', {
|
|
cls: 'setting-item-description git-sync-path-info',
|
|
text: this.getPathInfoText(this.plugin.settings.repositoryUrl)
|
|
});
|
|
|
|
// Test repository URL button - put below repository path
|
|
new Setting(containerEl)
|
|
.setName(t('settings.test.url.name'))
|
|
.setDesc(t('settings.test.url.desc'))
|
|
.addButton(button => button
|
|
.setButtonText(t('settings.test.url.button'))
|
|
.onClick(async () => {
|
|
if (!this.plugin.settings.repositoryUrl) {
|
|
new Notice(t('notice.url.required'));
|
|
return;
|
|
}
|
|
|
|
console.log('=== Testing Repository URL ===');
|
|
console.log('Input URL:', this.plugin.settings.repositoryUrl);
|
|
|
|
// Test URL parsing
|
|
const testResult = this.plugin.githubService.parseRepositoryUrl(this.plugin.settings.repositoryUrl);
|
|
|
|
if (testResult) {
|
|
console.log('URL parsing successful:', testResult);
|
|
new Notice(`${t('notice.url.test.success')}\n${t('test.url.user')}: ${testResult.owner}\n${t('test.url.repo')}: ${testResult.repo}\n${t('test.url.path')}: ${testResult.path || t('test.url.root')}`, 6000);
|
|
} else {
|
|
console.log('URL parsing failed');
|
|
new Notice(t('notice.url.test.failed'), 6000);
|
|
}
|
|
}));
|
|
|
|
// Sidebar button display toggle
|
|
new Setting(containerEl)
|
|
.setName(t('settings.ribbon.name'))
|
|
.setDesc(t('settings.ribbon.desc'))
|
|
.addToggle(toggle => toggle
|
|
.setValue(this.plugin.settings.showRibbonIcon)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.showRibbonIcon = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
|
|
// Save settings button - put below basic settings area
|
|
const saveButtonContainer = containerEl.createEl('div', {
|
|
cls: 'git-sync-save-button-container'
|
|
});
|
|
const saveButton = saveButtonContainer.createEl('button', {
|
|
cls: 'mod-cta',
|
|
text: t('settings.save.button')
|
|
});
|
|
saveButton.addEventListener('click', async () => {
|
|
await this.plugin.saveSettings();
|
|
new Notice(t('notice.settings.saved'));
|
|
});
|
|
|
|
// Danger zone title
|
|
containerEl.createEl('h2', {
|
|
text: t('settings.danger.zone.title'),
|
|
cls: 'danger-zone'
|
|
});
|
|
|
|
// Initialize repository
|
|
new Setting(containerEl)
|
|
.setName(t('settings.init.name'))
|
|
.setDesc(t('settings.init.desc'))
|
|
.addButton(button => {
|
|
const isVaultEmpty = this.isVaultEmpty();
|
|
button
|
|
.setButtonText(t('settings.init.button'))
|
|
.setDisabled(!isVaultEmpty)
|
|
.onClick(async () => {
|
|
if (!this.plugin.settings.githubToken || !this.plugin.settings.repositoryUrl) {
|
|
new Notice(t('notice.config.required'));
|
|
return;
|
|
}
|
|
|
|
button.setButtonText(t('settings.init.loading'));
|
|
button.setDisabled(true);
|
|
|
|
try {
|
|
const result = await this.plugin.initializeRepository();
|
|
if (result.success) {
|
|
new Notice(t('notice.result.with.count', { message: result.message, count: result.filesProcessed }));
|
|
} else {
|
|
new Notice(result.message);
|
|
}
|
|
} catch (error) {
|
|
new Notice(t('notice.init.error'));
|
|
} finally {
|
|
button.setButtonText(t('settings.init.button'));
|
|
button.setDisabled(!this.isVaultEmpty());
|
|
}
|
|
});
|
|
|
|
if (isVaultEmpty) {
|
|
button.setCta();
|
|
}
|
|
});
|
|
|
|
// Force sync remote to local
|
|
new Setting(containerEl)
|
|
.setName(t('settings.sync.remote.to.local.name'))
|
|
.setDesc(t('settings.sync.remote.to.local.desc'))
|
|
.addButton(button => button
|
|
.setButtonText(t('settings.sync.remote.to.local.button'))
|
|
.setWarning()
|
|
.onClick(async () => {
|
|
if (!this.plugin.settings.githubToken || !this.plugin.settings.repositoryUrl) {
|
|
new Notice(t('notice.config.required'));
|
|
return;
|
|
}
|
|
|
|
button.setButtonText(t('settings.sync.remote.to.local.loading'));
|
|
button.setDisabled(true);
|
|
|
|
try {
|
|
const result = await this.plugin.forceSyncRemoteToLocal();
|
|
if (result.success) {
|
|
new Notice(t('notice.result.with.count', { message: result.message, count: result.filesProcessed }));
|
|
} else {
|
|
new Notice(result.message);
|
|
}
|
|
} catch (error) {
|
|
new Notice(t('notice.sync.error'));
|
|
} finally {
|
|
button.setButtonText(t('settings.sync.remote.to.local.button'));
|
|
button.setDisabled(false);
|
|
}
|
|
}));
|
|
|
|
// Force sync local to remote
|
|
new Setting(containerEl)
|
|
.setName(t('settings.sync.local.to.remote.name'))
|
|
.setDesc(t('settings.sync.local.to.remote.desc'))
|
|
.addButton(button => button
|
|
.setButtonText(t('settings.sync.local.to.remote.button'))
|
|
.setWarning()
|
|
.onClick(async () => {
|
|
if (!this.plugin.settings.githubToken || !this.plugin.settings.repositoryUrl) {
|
|
new Notice(t('notice.config.required'));
|
|
return;
|
|
}
|
|
|
|
button.setButtonText(t('settings.sync.local.to.remote.loading'));
|
|
button.setDisabled(true);
|
|
|
|
try {
|
|
const result = await this.plugin.forceSyncLocalToRemote();
|
|
if (result.success) {
|
|
new Notice(t('notice.result.with.count', { message: result.message, count: result.filesProcessed }));
|
|
} else {
|
|
new Notice(result.message);
|
|
}
|
|
} catch (error) {
|
|
new Notice(t('notice.sync.error'));
|
|
} finally {
|
|
button.setButtonText(t('settings.sync.local.to.remote.button'));
|
|
button.setDisabled(false);
|
|
}
|
|
}));
|
|
|
|
// Clear file cache
|
|
new Setting(containerEl)
|
|
.setName(t('settings.clear.cache.name'))
|
|
.setDesc(t('settings.clear.cache.desc'))
|
|
.addButton(button => button
|
|
.setButtonText(t('settings.clear.cache.button'))
|
|
.onClick(async () => {
|
|
try {
|
|
const stats = this.plugin.fileCacheService.getCacheStats();
|
|
this.plugin.fileCacheService.clearCache();
|
|
new Notice(t('notice.cache.cleared', { count: stats.totalFiles }));
|
|
} catch (error) {
|
|
new Notice(t('notice.cache.clear.error'));
|
|
}
|
|
}));
|
|
|
|
// Sponsor title
|
|
containerEl.createEl('h2', {
|
|
text: t('settings.sponsor.section.title'),
|
|
cls: 'sponsor'
|
|
});
|
|
|
|
// Sponsor section
|
|
const sponsorSection = containerEl.createEl('div', { cls: 'setting-item' });
|
|
const sponsorInfo = sponsorSection.createEl('div', { cls: 'setting-item-info' });
|
|
sponsorInfo.createEl('div', {
|
|
cls: 'setting-item-name',
|
|
text: t('settings.sponsor.name')
|
|
});
|
|
sponsorInfo.createEl('div', {
|
|
cls: 'setting-item-description',
|
|
text: t('settings.sponsor.desc')
|
|
});
|
|
|
|
const sponsorControl = sponsorSection.createEl('div', {
|
|
cls: getActualLanguage() === 'zh' ? 'setting-item-control git-sync-sponsor-control' : 'setting-item-control'
|
|
});
|
|
|
|
// PayPal sponsor button
|
|
const sponsorButton = sponsorControl.createEl('a', {
|
|
cls: 'mod-cta',
|
|
text: t('settings.sponsor.button'),
|
|
href: 'https://paypal.me/xheldoncao'
|
|
});
|
|
sponsorButton.style.textDecoration = 'none';
|
|
sponsorButton.style.padding = '6px 12px';
|
|
sponsorButton.style.borderRadius = '3px';
|
|
sponsorButton.style.display = 'inline-block';
|
|
|
|
// Show China mainland donation channel if Chinese interface
|
|
if (getActualLanguage() === 'zh') {
|
|
// Display China mainland donation channel on new line
|
|
const chinaSponsorContainer = sponsorControl.createEl('div', {
|
|
cls: 'git-sync-china-sponsor'
|
|
});
|
|
chinaSponsorContainer.createEl('span', {
|
|
text: t('settings.sponsor.china.label') + ': '
|
|
});
|
|
const chinaLink = chinaSponsorContainer.createEl('a', {
|
|
text: 'https://www.xheldon.com/donate/',
|
|
href: 'https://www.xheldon.com/donate/'
|
|
});
|
|
chinaLink.style.color = 'var(--text-accent)';
|
|
chinaLink.style.textDecoration = 'none';
|
|
}
|
|
}
|
|
|
|
private isVaultEmpty(): boolean {
|
|
const files = this.app.vault.getFiles();
|
|
return files.filter(file => !file.path.startsWith('.obsidian')).length === 0;
|
|
}
|
|
|
|
private updatePathInfo(el: HTMLElement, value: string) {
|
|
if (el) {
|
|
el.empty();
|
|
el.createEl('span', { text: this.getPathInfoText(value) });
|
|
}
|
|
}
|
|
|
|
private getPathInfoText(value: string): string {
|
|
if (!value) {
|
|
return t('path.info.empty');
|
|
}
|
|
|
|
// Use GitHub service parsing method
|
|
const parseResult = this.plugin.githubService.parseRepositoryUrl(value);
|
|
|
|
if (parseResult) {
|
|
let pathInfo = t('path.info.sync.to', { owner: parseResult.owner, repo: parseResult.repo });
|
|
if (parseResult.path) {
|
|
pathInfo += t('path.info.folder', { path: parseResult.path });
|
|
} else {
|
|
pathInfo += t('path.info.root');
|
|
}
|
|
return pathInfo;
|
|
}
|
|
|
|
return t('path.info.error');
|
|
}
|
|
} |