v1.0.5: Fix Obsidian community plugin feedback issues

- Fix hardcoded .obsidian paths to use app.vault.configDir for better compatibility
- Move inline JavaScript styles to CSS files for better theme adaptability
- Reduce console.log pollution by implementing debug-only logging
- Improve type safety by removing any type casting where possible
- Fix GitHub settings not saving to data.json
- Enhance test URL button to validate both URL format and GitHub token
- Fix debugLog recursive call issue
- Add comprehensive GitHub connection testing with user info and permissions
- Improve user experience with loading states and detailed feedback
- Enhanced code quality to meet Obsidian community plugin standards
This commit is contained in:
Xheldon 2025-08-01 15:20:29 +08:00
parent 1850f3b895
commit c0948fe13b
11 changed files with 376 additions and 127 deletions

View file

@ -5,6 +5,50 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.5] - 2024-12-19
### Fixed
- Fixed hardcoded `.obsidian` paths to use `app.vault.configDir` for better compatibility
- Moved inline JavaScript styles to CSS files for better theme adaptability
- Reduced console.log pollution by implementing debug-only logging
- Improved type safety by removing `any` type casting where possible
- Fixed GitHub settings not saving to data.json
- Fixed debugLog recursive call issue causing test button to be unresponsive
### Added
- Enhanced test URL button to validate both URL format and GitHub token
- Comprehensive GitHub connection testing with user info and permissions
- Loading states and detailed feedback for better user experience
### Changed
- Enhanced code quality to meet Obsidian community plugin standards
- Better CSS class management for status indicators and UI elements
## [1.0.4] - 2024-12-19
### Fixed
- Fixed hardcoded `.obsidian` paths to use `app.vault.configDir` for better compatibility
- Moved inline JavaScript styles to CSS files for better theme adaptability
- Reduced console.log pollution by implementing debug-only logging
- Improved type safety by removing `any` type casting where possible
### Changed
- Enhanced code quality to meet Obsidian community plugin standards
- Better CSS class management for status indicators and UI elements
## [1.0.3] - 2024-12-19
### Fixed
- Fixed GitHub Actions permissions for automatic release creation
### Changed
- Bumped version for testing the fixed workflow
## [1.0.2] - 2024-12-19
### Changed
- Clean release test version
## [1.0.1] - 2024-12-19
### Changed

View file

@ -147,13 +147,7 @@ export class CosService {
// Generate signature for Tencent COS
const signature = await this.generateTencentSignature('PUT', remotePath, date, file.type);
console.log('Tencent COS upload debug:', {
url,
remotePath,
signature: signature.substring(0, 50) + '...',
fileSize: file.size,
fileType: file.type
});
// Debug logging removed for production
// Convert File to ArrayBuffer for requestUrl
const fileBuffer = await file.arrayBuffer();

View file

@ -86,7 +86,7 @@ export class FileCacheService implements FileCacheManager {
clearCache(): void {
this.cache.clear();
localStorage.removeItem(this.CACHE_KEY);
console.log('All file caches cleared');
// console.log('All file caches cleared');
}
/**

View file

@ -172,7 +172,7 @@ export class GitHubService {
}
public parseRepositoryUrl(url: string): { owner: string; repo: string; path: string } | null {
console.log('Parsing repository URL:', url);
// console.log('Parsing repository URL:', url);
if (!url) {
console.error('URL is empty');
@ -181,7 +181,7 @@ export class GitHubService {
// Remove leading and trailing spaces
const cleanUrl = url.trim();
console.log('Cleaned URL:', cleanUrl);
// console.log('Cleaned URL:', cleanUrl);
// Support two formats:
// 1. https://github.com/username/repo/path (standard GitHub URL)
@ -197,7 +197,7 @@ export class GitHubService {
repo: match[2],
path: match[3] || '',
};
console.log('Parse result (standard GitHub URL):', result);
// console.log('Parse result (standard GitHub URL):', result);
return result;
}
@ -209,7 +209,7 @@ export class GitHubService {
repo: match[2],
path: match[3] || '',
};
console.log('Parse result (short format):', result);
// console.log('Parse result (short format):', result);
return result;
}
@ -221,6 +221,63 @@ export class GitHubService {
return null;
}
/**
* Test GitHub token and repository access
*/
async testConnection(repositoryUrl: string): Promise<SyncResult & { repoInfo?: { owner: string; repo: string; path: string } }> {
// First test URL parsing
const repoInfo = this.parseRepositoryUrl(repositoryUrl);
if (!repoInfo) {
return { success: false, message: t('github.api.invalid.url.format') };
}
// Check rate limit
if (!(await this.beforeApiCall())) {
return { success: false, message: t('github.api.rate.limit.exceeded.short') };
}
try {
// Test token validity by getting user info
const userResponse = await this.octokit.rest.users.getAuthenticated();
// Test repository access
const repoResponse = await this.octokit.rest.repos.get({
owner: repoInfo.owner,
repo: repoInfo.repo,
});
// Test if we can access the specific path (if provided)
if (repoInfo.path) {
try {
await this.octokit.rest.repos.getContent({
owner: repoInfo.owner,
repo: repoInfo.repo,
path: repoInfo.path,
});
} catch (error) {
// Path doesn't exist, but that's okay - we can create it
if (error.status !== 404) {
throw error;
}
}
}
return {
success: true,
message: t('github.api.connection.test.success', {
user: userResponse.data.login,
repo: `${repoInfo.owner}/${repoInfo.repo}`,
permissions: repoResponse.data.permissions?.push ? 'read/write' : 'read-only'
}),
repoInfo
};
} catch (error) {
console.error('GitHub connection test failed:', error);
const errorInfo = this.handleGitHubError(error);
return { success: false, message: errorInfo.message };
}
}
async uploadFile(repositoryUrl: string, filePath: string, content: string): Promise<SyncResult> {
const repoInfo = this.parseRepositoryUrl(repositoryUrl);
if (!repoInfo) {

View file

@ -26,6 +26,7 @@ const translations = {
'settings.test.url.name': '测试仓库URL',
'settings.test.url.desc': '验证GitHub仓库路径格式是否正确',
'settings.test.url.button': '测试URL',
'settings.test.url.loading': '测试中...',
'settings.sponsor.name': '💖 支持开发者',
'settings.sponsor.desc': '如果这个插件对你有帮助,欢迎请我喝杯咖啡!你的支持是我继续开发的动力。',
'settings.sponsor.button': '💝 PayPal 赞助',
@ -99,6 +100,7 @@ const translations = {
'notice.settings.saved': '设置已保存',
'notice.config.required': '请先配置GitHub Token和仓库地址',
'notice.url.required': '请先输入GitHub仓库路径',
'notice.token.required': '请先填写GitHub Token',
'notice.url.test.success': 'URL格式正确',
'notice.url.test.failed': 'URL格式不正确请检查格式。详情请查看控制台。',
'notice.init.error': '初始化仓库时发生错误',
@ -162,6 +164,7 @@ const translations = {
'github.api.rate.limit.exceeded.short': 'GitHub API调用次数已达上限请稍后重试',
'github.api.file.upload.success': '文件上传成功',
'github.api.all.files.download.success': '所有文件下载成功',
'github.api.connection.test.success': '✅ 连接测试成功!\n👤 用户: {user}\n📁 仓库: {repo}\n🔑 权限: {permissions}',
// Status bar
'status.bar.checking': '检查中...',
@ -230,6 +233,7 @@ const translations = {
'settings.test.url.name': 'Test Repository URL',
'settings.test.url.desc': 'Verify if GitHub repository path format is correct',
'settings.test.url.button': 'Test URL',
'settings.test.url.loading': 'Testing...',
'settings.sponsor.name': '💖 Support Developer',
'settings.sponsor.desc': 'If this plugin helps you, consider buying me a coffee! Your support motivates me to continue development.',
'settings.sponsor.button': '💝 PayPal Sponsor',
@ -302,6 +306,7 @@ const translations = {
'notice.settings.saved': 'Settings saved',
'notice.config.required': 'Please configure GitHub Token and repository address first',
'notice.url.required': 'Please enter GitHub repository path first',
'notice.token.required': 'Please enter GitHub Token first',
'notice.url.test.success': 'URL format is correct!',
'notice.url.test.failed': 'URL format is incorrect, please check format. See console for details.',
'notice.init.error': 'Error occurred during repository initialization',
@ -365,6 +370,7 @@ const translations = {
'github.api.rate.limit.exceeded.short': 'GitHub API rate limit exceeded, please retry later',
'github.api.file.upload.success': 'File uploaded successfully',
'github.api.all.files.download.success': 'All files downloaded successfully',
'github.api.connection.test.success': '✅ Connection test successful!\n👤 User: {user}\n📁 Repository: {repo}\n🔑 Permissions: {permissions}',
// Status bar
'status.bar.checking': 'Checking...',
@ -415,12 +421,28 @@ const translations = {
// Current language
let currentLanguage: Language = 'auto';
// Extended window interface for type safety
interface ExtendedWindow extends Window {
app?: {
vault?: {
adapter?: {
path?: {
locale?: string;
};
};
};
};
moment?: {
locale(): string;
};
}
// Detect Obsidian's language settings
function detectObsidianLanguage(): 'zh' | 'en' {
// Check Obsidian's language settings - more accurate method
const obsidianLang = localStorage.getItem('language') ||
(window as any).app?.vault?.adapter?.path?.locale ||
(window as any).moment?.locale();
(window as ExtendedWindow).app?.vault?.adapter?.path?.locale ||
(window as ExtendedWindow).moment?.locale();
// Check browser language
const browserLang = navigator.language || navigator.languages?.[0] || 'en';

164
main.js
View file

@ -355,6 +355,7 @@ var init_i18n_simple = __esm({
"settings.test.url.name": "\u6D4B\u8BD5\u4ED3\u5E93URL",
"settings.test.url.desc": "\u9A8C\u8BC1GitHub\u4ED3\u5E93\u8DEF\u5F84\u683C\u5F0F\u662F\u5426\u6B63\u786E",
"settings.test.url.button": "\u6D4B\u8BD5URL",
"settings.test.url.loading": "\u6D4B\u8BD5\u4E2D...",
"settings.sponsor.name": "\u{1F496} \u652F\u6301\u5F00\u53D1\u8005",
"settings.sponsor.desc": "\u5982\u679C\u8FD9\u4E2A\u63D2\u4EF6\u5BF9\u4F60\u6709\u5E2E\u52A9\uFF0C\u6B22\u8FCE\u8BF7\u6211\u559D\u676F\u5496\u5561\uFF01\u4F60\u7684\u652F\u6301\u662F\u6211\u7EE7\u7EED\u5F00\u53D1\u7684\u52A8\u529B\u3002",
"settings.sponsor.button": "\u{1F49D} PayPal \u8D5E\u52A9",
@ -426,6 +427,7 @@ var init_i18n_simple = __esm({
"notice.settings.saved": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58",
"notice.config.required": "\u8BF7\u5148\u914D\u7F6EGitHub Token\u548C\u4ED3\u5E93\u5730\u5740",
"notice.url.required": "\u8BF7\u5148\u8F93\u5165GitHub\u4ED3\u5E93\u8DEF\u5F84",
"notice.token.required": "\u8BF7\u5148\u586B\u5199GitHub Token",
"notice.url.test.success": "URL\u683C\u5F0F\u6B63\u786E\uFF01",
"notice.url.test.failed": "URL\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5\u683C\u5F0F\u3002\u8BE6\u60C5\u8BF7\u67E5\u770B\u63A7\u5236\u53F0\u3002",
"notice.init.error": "\u521D\u59CB\u5316\u4ED3\u5E93\u65F6\u53D1\u751F\u9519\u8BEF",
@ -484,6 +486,7 @@ var init_i18n_simple = __esm({
"github.api.rate.limit.exceeded.short": "GitHub API\u8C03\u7528\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
"github.api.file.upload.success": "\u6587\u4EF6\u4E0A\u4F20\u6210\u529F",
"github.api.all.files.download.success": "\u6240\u6709\u6587\u4EF6\u4E0B\u8F7D\u6210\u529F",
"github.api.connection.test.success": "\u2705 \u8FDE\u63A5\u6D4B\u8BD5\u6210\u529F\uFF01\n\u{1F464} \u7528\u6237: {user}\n\u{1F4C1} \u4ED3\u5E93: {repo}\n\u{1F511} \u6743\u9650: {permissions}",
// Status bar
"status.bar.checking": "\u68C0\u67E5\u4E2D...",
"status.bar.sync": "\u540C\u6B65",
@ -547,6 +550,7 @@ var init_i18n_simple = __esm({
"settings.test.url.name": "Test Repository URL",
"settings.test.url.desc": "Verify if GitHub repository path format is correct",
"settings.test.url.button": "Test URL",
"settings.test.url.loading": "Testing...",
"settings.sponsor.name": "\u{1F496} Support Developer",
"settings.sponsor.desc": "If this plugin helps you, consider buying me a coffee! Your support motivates me to continue development.",
"settings.sponsor.button": "\u{1F49D} PayPal Sponsor",
@ -617,6 +621,7 @@ var init_i18n_simple = __esm({
"notice.settings.saved": "Settings saved",
"notice.config.required": "Please configure GitHub Token and repository address first",
"notice.url.required": "Please enter GitHub repository path first",
"notice.token.required": "Please enter GitHub Token first",
"notice.url.test.success": "URL format is correct!",
"notice.url.test.failed": "URL format is incorrect, please check format. See console for details.",
"notice.init.error": "Error occurred during repository initialization",
@ -675,6 +680,7 @@ var init_i18n_simple = __esm({
"github.api.rate.limit.exceeded.short": "GitHub API rate limit exceeded, please retry later",
"github.api.file.upload.success": "File uploaded successfully",
"github.api.all.files.download.success": "All files downloaded successfully",
"github.api.connection.test.success": "\u2705 Connection test successful!\n\u{1F464} User: {user}\n\u{1F4C1} Repository: {repo}\n\u{1F511} Permissions: {permissions}",
// Status bar
"status.bar.checking": "Checking...",
"status.bar.sync": "Sync",
@ -870,13 +876,6 @@ var init_cos_service = __esm({
const url = `https://${this.config.bucket}.cos.${this.config.region}.myqcloud.com/${remotePath}`;
const date = new Date().toUTCString();
const signature = await this.generateTencentSignature("PUT", remotePath, date, file.type);
console.log("Tencent COS upload debug:", {
url,
remotePath,
signature: signature.substring(0, 50) + "...",
fileSize: file.size,
fileType: file.type
});
const fileBuffer = await file.arrayBuffer();
const response = await (0, import_obsidian2.requestUrl)({
url,
@ -4395,13 +4394,11 @@ var GitHubService = class {
}
}
parseRepositoryUrl(url) {
console.log("Parsing repository URL:", url);
if (!url) {
console.error("URL is empty");
return null;
}
const cleanUrl = url.trim();
console.log("Cleaned URL:", cleanUrl);
let match;
match = cleanUrl.match(/^https:\/\/github\.com\/([^\/]+)\/([^\/]+)(?:\/(.*))?$/);
if (match) {
@ -4410,7 +4407,6 @@ var GitHubService = class {
repo: match[2],
path: match[3] || ""
};
console.log("Parse result (standard GitHub URL):", result);
return result;
}
match = cleanUrl.match(/^([^\/]+)\/([^\/]+)(?:\/(.*))?$/);
@ -4420,7 +4416,6 @@ var GitHubService = class {
repo: match[2],
path: match[3] || ""
};
console.log("Parse result (short format):", result);
return result;
}
console.error("Unable to parse URL format:", cleanUrl);
@ -4429,6 +4424,52 @@ var GitHubService = class {
console.error("2. username/repo/path (short format)");
return null;
}
/**
* Test GitHub token and repository access
*/
async testConnection(repositoryUrl) {
var _a;
const repoInfo = this.parseRepositoryUrl(repositoryUrl);
if (!repoInfo) {
return { success: false, message: t("github.api.invalid.url.format") };
}
if (!await this.beforeApiCall()) {
return { success: false, message: t("github.api.rate.limit.exceeded.short") };
}
try {
const userResponse = await this.octokit.rest.users.getAuthenticated();
const repoResponse = await this.octokit.rest.repos.get({
owner: repoInfo.owner,
repo: repoInfo.repo
});
if (repoInfo.path) {
try {
await this.octokit.rest.repos.getContent({
owner: repoInfo.owner,
repo: repoInfo.repo,
path: repoInfo.path
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
}
return {
success: true,
message: t("github.api.connection.test.success", {
user: userResponse.data.login,
repo: `${repoInfo.owner}/${repoInfo.repo}`,
permissions: ((_a = repoResponse.data.permissions) == null ? void 0 : _a.push) ? "read/write" : "read-only"
}),
repoInfo
};
} catch (error) {
console.error("GitHub connection test failed:", error);
const errorInfo = this.handleGitHubError(error);
return { success: false, message: errorInfo.message };
}
}
async uploadFile(repositoryUrl, filePath, content) {
const repoInfo = this.parseRepositoryUrl(repositoryUrl);
if (!repoInfo) {
@ -4672,7 +4713,6 @@ var FileCacheService = class {
clearCache() {
this.cache.clear();
localStorage.removeItem(this.CACHE_KEY);
console.log("All file caches cleared");
}
/**
* Check if cache is valid
@ -4762,6 +4802,11 @@ var FileCacheService = class {
// main.ts
init_cos_service();
function debugLog(...args) {
if (true) {
console.log("[Git Folder Sync]", ...args);
}
}
var GitSyncPlugin = class extends import_obsidian3.Plugin {
constructor() {
super(...arguments);
@ -4793,7 +4838,7 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
this.registerEvent(
this.app.vault.on("modify", (file) => {
if (file.path.endsWith(".md") && this.currentFile && file.path === this.currentFile.path) {
console.log(`Detected file modification: ${file.path}`);
debugLog(`Detected file modification: ${file.path}`);
if (file instanceof import_obsidian3.TFile) {
this.onFileContentModified(file);
}
@ -4991,7 +5036,7 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
async forceSyncLocalToRemote() {
try {
const files = this.getAllVaultFiles();
console.log("Found files:", files.map((f) => f.path));
debugLog("Found files:", files.map((f) => f.path));
if (files.length === 0) {
return { success: false, message: t("no.syncable.files.in.vault") };
}
@ -5007,7 +5052,7 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
);
if (result.success) {
processed++;
console.log(`Successfully synced file: ${file.path}`);
debugLog(`Successfully synced file: ${file.path}`);
await this.fileCacheService.updateFileCache(
file.path,
file.path,
@ -5038,25 +5083,25 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
}
isVaultEmpty() {
const files = this.app.vault.getFiles();
return files.filter((file) => !file.path.startsWith(".obsidian")).length === 0;
return files.filter((file) => !file.path.startsWith(this.app.vault.configDir)).length === 0;
}
getAllVaultFiles() {
const allFiles = this.app.vault.getFiles();
console.log("All files in vault:", allFiles.map((f) => f.path));
debugLog("All files in vault:", allFiles.map((f) => f.path));
const filteredFiles = allFiles.filter((file) => {
if (file.path.startsWith(".obsidian")) {
if (file.path.startsWith(this.app.vault.configDir)) {
return false;
}
if (this.settings.keepLocalImages && this.settings.localImagePath) {
const normalizedImagePath = this.settings.localImagePath.replace(/^\/+|\/+$/g, "");
if (normalizedImagePath && file.path.startsWith(normalizedImagePath + "/")) {
console.log(`Excluding file from sync (in image directory): ${file.path}`);
debugLog(`Excluding file from sync (in image directory): ${file.path}`);
return false;
}
}
return true;
});
console.log("Filtered files:", filteredFiles.map((f) => f.path));
debugLog("Filtered files:", filteredFiles.map((f) => f.path));
return filteredFiles;
}
async createFileInVault(path, content) {
@ -5077,11 +5122,13 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile || !activeFile.path.endsWith(".md")) {
this.statusBarEl.empty();
this.statusBarEl.style.display = "none";
this.statusBarEl.removeClass("visible");
this.statusBarEl.addClass("hidden");
return;
}
this.currentFile = activeFile;
this.statusBarEl.style.display = "flex";
this.statusBarEl.removeClass("hidden");
this.statusBarEl.addClass("visible");
this.statusBarEl.empty();
const statusText = this.statusBarEl.createEl("span", {
cls: "git-sync-status-text",
@ -5095,27 +5142,27 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
try {
const cache = this.fileCacheService.getFileCache(activeFile.path);
if (cache) {
console.log(`Using cached data to display file status: ${activeFile.path}`);
debugLog(`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) });
if (!cache.isSynced) {
statusMsg += ` (${t("status.bar.local.modified")})`;
statusText.style.color = "var(--color-orange)";
statusText.addClass("modified");
} else {
statusMsg += ` (${t("status.bar.synced")})`;
statusText.style.color = "var(--color-green)";
statusText.addClass("synced");
}
statusText.textContent = statusMsg;
} else {
statusText.textContent = t("status.bar.not.published");
statusText.style.color = "var(--text-muted)";
statusText.addClass("not-published");
}
if (!this.fileCacheService.isCacheValid(cache, 4 * 60 * 1e3)) {
this.refreshFileCache(activeFile.path).catch(console.error);
}
} else {
console.log(`No cache, fetching file status from GitHub: ${activeFile.path}`);
debugLog(`No cache, fetching file status from GitHub: ${activeFile.path}`);
await this.fetchAndCacheFileStatus(activeFile.path, statusText);
}
} catch (error) {
@ -5147,7 +5194,7 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
const content = await this.app.vault.read(file);
const currentHash = FileCacheService.calculateContentHash(content);
if (currentHash !== cache.contentHash) {
console.log(`File content modified: ${file.path}`);
debugLog(`File content modified: ${file.path}`);
const updatedCache = {
...cache,
contentHash: currentHash,
@ -5181,7 +5228,7 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
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)";
statusText.addClass("normal");
await this.fileCacheService.updateFileCache(
filePath,
filePath,
@ -5194,7 +5241,7 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
);
} else {
statusText.textContent = t("status.bar.not.published");
statusText.style.color = "var(--text-muted)";
statusText.addClass("not-published");
await this.fileCacheService.updateFileCache(
filePath,
filePath,
@ -5217,7 +5264,7 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
*/
async refreshFileCache(filePath) {
try {
console.log(`Asynchronously refreshing file cache: ${filePath}`);
debugLog(`Asynchronously refreshing file cache: ${filePath}`);
const result = await this.githubService.getFileLastModified(
this.settings.repositoryUrl,
filePath
@ -5232,7 +5279,7 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
result.exists || false,
this.app.vault
);
console.log(`File cache refreshed: ${filePath}`);
debugLog(`File cache refreshed: ${filePath}`);
}
} catch (error) {
console.error("Failed to refresh file cache:", error);
@ -5400,7 +5447,7 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
}
uploadNotice.hide();
new import_obsidian3.Notice(t("cos.upload.success"));
console.log("Image uploaded successfully:", uploadResult.url);
debugLog("Image uploaded successfully:", uploadResult.url);
} else {
throw new Error(uploadResult.message || "Upload failed");
}
@ -5439,7 +5486,7 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
const cursorPos = editor.getCursor();
editor.replaceRange(imageMarkdown, cursorPos);
new import_obsidian3.Notice(t("cos.upload.success"));
console.log("Image saved locally:", localPath);
debugLog("Image saved locally:", localPath);
} catch (error) {
new import_obsidian3.Notice(t("cos.upload.failed", { error: error.message }));
console.error("Local image save failed:", error);
@ -5463,7 +5510,7 @@ var GitSyncPlugin = class extends import_obsidian3.Plugin {
}
const arrayBuffer = await file.arrayBuffer();
await this.app.vault.createBinary(localPath, arrayBuffer);
console.log("Local image copy saved:", localPath);
debugLog("Local image copy saved:", localPath);
} catch (error) {
console.warn("Failed to save local image copy:", error);
}
@ -5492,11 +5539,13 @@ var GitSyncSettingTab = class extends import_obsidian3.PluginSettingTab {
});
new import_obsidian3.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;
await this.plugin.saveSettings();
}));
let pathInfoEl;
new import_obsidian3.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);
await this.plugin.saveSettings();
}));
pathInfoEl = containerEl.createEl("div", {
cls: "setting-item-description git-sync-path-info",
@ -5507,18 +5556,29 @@ var GitSyncSettingTab = class extends import_obsidian3.PluginSettingTab {
new import_obsidian3.Notice(t("notice.url.required"));
return;
}
console.log("=== Testing Repository URL ===");
console.log("Input URL:", this.plugin.settings.repositoryUrl);
const testResult = this.plugin.githubService.parseRepositoryUrl(this.plugin.settings.repositoryUrl);
if (testResult) {
console.log("URL parsing successful:", testResult);
new import_obsidian3.Notice(`${t("notice.url.test.success")}
${t("test.url.user")}: ${testResult.owner}
${t("test.url.repo")}: ${testResult.repo}
${t("test.url.path")}: ${testResult.path || t("test.url.root")}`, 6e3);
} else {
console.log("URL parsing failed");
new import_obsidian3.Notice(t("notice.url.test.failed"), 6e3);
if (!this.plugin.settings.githubToken) {
new import_obsidian3.Notice(t("notice.token.required"));
return;
}
button.setButtonText(t("settings.test.url.loading"));
button.setDisabled(true);
try {
debugLog("=== Testing GitHub Connection ===");
debugLog("Input URL:", this.plugin.settings.repositoryUrl);
const testResult = await this.plugin.githubService.testConnection(this.plugin.settings.repositoryUrl);
if (testResult.success) {
debugLog("Connection test successful:", testResult);
new import_obsidian3.Notice(testResult.message, 8e3);
} else {
debugLog("Connection test failed:", testResult.message);
new import_obsidian3.Notice(testResult.message, 6e3);
}
} catch (error) {
debugLog("Connection test error:", error);
new import_obsidian3.Notice(t("github.api.operation.failed", { message: error.message }), 6e3);
} finally {
button.setButtonText(t("settings.test.url.button"));
button.setDisabled(false);
}
}));
new import_obsidian3.Setting(containerEl).setName(t("settings.ribbon.name")).setDesc(t("settings.ribbon.desc")).addToggle((toggle) => toggle.setValue(this.plugin.settings.showRibbonIcon).onChange(async (value) => {
@ -5746,14 +5806,10 @@ ${t("test.url.path")}: ${testResult.path || t("test.url.root")}`, 6e3);
cls: getActualLanguage() === "zh" ? "setting-item-control git-sync-sponsor-control" : "setting-item-control"
});
const sponsorButton = sponsorControl.createEl("a", {
cls: "mod-cta",
cls: "mod-cta git-sync-sponsor-button",
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";
if (getActualLanguage() === "zh") {
const chinaSponsorContainer = sponsorControl.createEl("div", {
cls: "git-sync-china-sponsor"
@ -5765,13 +5821,11 @@ ${t("test.url.path")}: ${testResult.path || t("test.url.root")}`, 6e3);
text: "https://www.xheldon.com/donate/",
href: "https://www.xheldon.com/donate/"
});
chinaLink.style.color = "var(--text-accent)";
chinaLink.style.textDecoration = "none";
}
}
isVaultEmpty() {
const files = this.app.vault.getFiles();
return files.filter((file) => !file.path.startsWith(".obsidian")).length === 0;
return files.filter((file) => !file.path.startsWith(this.app.vault.configDir)).length === 0;
}
updatePathInfo(el, value) {
if (el) {

146
main.ts
View file

@ -1,10 +1,30 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, Menu, TFolder } from 'obsidian';
// Extended types for better type safety
interface ExtendedApp extends App {
setting: {
open(): void;
openTabById(id: string): void;
};
}
interface ExtendedEditor extends Editor {
containerEl?: HTMLElement;
}
import { GitSyncSettings, DEFAULT_SETTINGS, SyncResult, CosConfig, ImagePasteContext, CosProviderConfig } from './types';
import { GitHubService } from './github-service';
import { setLanguage, t, getSupportedLanguages, getActualLanguage } from './i18n-simple';
import { FileCacheService } from './file-cache';
import { CosService, parseImagePath } from './cos-service';
// Debug logging function - only logs in development mode
function debugLog(...args: any[]): void {
// Only log in development mode (when plugin is loaded from file system)
if (process.env.NODE_ENV === 'development' || window.location.hostname === 'localhost') {
console.log('[Git Folder Sync]', ...args);
}
}
export default class GitSyncPlugin extends Plugin {
settings: GitSyncSettings;
githubService: GitHubService;
@ -51,7 +71,7 @@ export default class GitSyncPlugin extends Plugin {
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}`);
debugLog(`Detected file modification: ${file.path}`);
// Type conversion to TFile
if (file instanceof TFile) {
this.onFileContentModified(file);
@ -127,10 +147,10 @@ export default class GitSyncPlugin extends Plugin {
// Add button based on settings
if (this.settings.showRibbonIcon) {
this.ribbonIconEl = this.addRibbonIcon('settings', t('settings.title'), (evt: MouseEvent) => {
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);
(this.app as ExtendedApp).setting.open();
(this.app as ExtendedApp).setting.openTabById(this.manifest.id);
});
}
}
@ -156,7 +176,7 @@ export default class GitSyncPlugin extends Plugin {
});
});
const rect = (editor as any).containerEl?.getBoundingClientRect();
const rect = (editor as ExtendedEditor).containerEl?.getBoundingClientRect();
if (rect) {
menu.showAtPosition({ x: rect.right - 100, y: rect.top + 50 });
} else {
@ -299,7 +319,7 @@ export default class GitSyncPlugin extends Plugin {
async forceSyncLocalToRemote(): Promise<SyncResult> {
try {
const files = this.getAllVaultFiles();
console.log('Found files:', files.map(f => f.path));
debugLog('Found files:', files.map(f => f.path));
if (files.length === 0) {
return { success: false, message: t('no.syncable.files.in.vault') };
@ -319,7 +339,7 @@ export default class GitSyncPlugin extends Plugin {
if (result.success) {
processed++;
console.log(`Successfully synced file: ${file.path}`);
debugLog(`Successfully synced file: ${file.path}`);
// Update cache immediately - file synced to remote
await this.fileCacheService.updateFileCache(
@ -356,17 +376,17 @@ export default class GitSyncPlugin extends Plugin {
private isVaultEmpty(): boolean {
const files = this.app.vault.getFiles();
return files.filter(file => !file.path.startsWith('.obsidian')).length === 0;
return files.filter(file => !file.path.startsWith(this.app.vault.configDir)).length === 0;
}
private getAllVaultFiles(): TFile[] {
const allFiles = this.app.vault.getFiles();
console.log('All files in vault:', allFiles.map(f => f.path));
debugLog('All files in vault:', allFiles.map(f => f.path));
// Filter out files in .obsidian folder and local image path (if configured)
// Filter out files in config folder and local image path (if configured)
const filteredFiles = allFiles.filter(file => {
// Always exclude .obsidian folder
if (file.path.startsWith('.obsidian')) {
// Always exclude Obsidian config folder
if (file.path.startsWith(this.app.vault.configDir)) {
return false;
}
@ -374,7 +394,7 @@ export default class GitSyncPlugin extends Plugin {
if (this.settings.keepLocalImages && this.settings.localImagePath) {
const normalizedImagePath = this.settings.localImagePath.replace(/^\/+|\/+$/g, ''); // Remove leading/trailing slashes
if (normalizedImagePath && file.path.startsWith(normalizedImagePath + '/')) {
console.log(`Excluding file from sync (in image directory): ${file.path}`);
debugLog(`Excluding file from sync (in image directory): ${file.path}`);
return false;
}
}
@ -382,7 +402,7 @@ export default class GitSyncPlugin extends Plugin {
return true;
});
console.log('Filtered files:', filteredFiles.map(f => f.path));
debugLog('Filtered files:', filteredFiles.map(f => f.path));
return filteredFiles;
}
@ -407,12 +427,14 @@ export default class GitSyncPlugin extends Plugin {
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile || !activeFile.path.endsWith('.md')) {
this.statusBarEl.empty();
this.statusBarEl.style.display = 'none';
this.statusBarEl.removeClass('visible');
this.statusBarEl.addClass('hidden');
return;
}
this.currentFile = activeFile;
this.statusBarEl.style.display = 'flex';
this.statusBarEl.removeClass('hidden');
this.statusBarEl.addClass('visible');
this.statusBarEl.empty();
// Create status text
@ -435,25 +457,25 @@ export default class GitSyncPlugin extends Plugin {
if (cache) {
// Use cached data
console.log(`Using cached data to display file status: ${activeFile.path}`);
debugLog(`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)';
}
// Use isSynced status from cache directly, no need to recalculate hash
if (!cache.isSynced) {
statusMsg += ` (${t('status.bar.local.modified')})`;
statusText.addClass('modified');
} else {
statusMsg += ` (${t('status.bar.synced')})`;
statusText.addClass('synced');
}
statusText.textContent = statusMsg;
} else {
statusText.textContent = t('status.bar.not.published');
statusText.style.color = 'var(--text-muted)';
statusText.addClass('not-published');
}
// Asynchronously update cache if it's about to expire
@ -462,7 +484,7 @@ export default class GitSyncPlugin extends Plugin {
}
} else {
// No cache, need to fetch from GitHub
console.log(`No cache, fetching file status from GitHub: ${activeFile.path}`);
debugLog(`No cache, fetching file status from GitHub: ${activeFile.path}`);
await this.fetchAndCacheFileStatus(activeFile.path, statusText);
}
} catch (error) {
@ -505,7 +527,7 @@ export default class GitSyncPlugin extends Plugin {
const currentHash = FileCacheService.calculateContentHash(content);
if (currentHash !== cache.contentHash) {
console.log(`File content modified: ${file.path}`);
debugLog(`File content modified: ${file.path}`);
// Update content hash and sync status in cache
const updatedCache = {
@ -548,7 +570,7 @@ export default class GitSyncPlugin extends Plugin {
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)';
statusText.addClass('normal');
// Update cache
await this.fileCacheService.updateFileCache(
@ -561,7 +583,7 @@ export default class GitSyncPlugin extends Plugin {
);
} else {
statusText.textContent = t('status.bar.not.published');
statusText.style.color = 'var(--text-muted)';
statusText.addClass('not-published');
// Cache unpublished status
await this.fileCacheService.updateFileCache(
@ -587,7 +609,7 @@ export default class GitSyncPlugin extends Plugin {
*/
private async refreshFileCache(filePath: string) {
try {
console.log(`Asynchronously refreshing file cache: ${filePath}`);
debugLog(`Asynchronously refreshing file cache: ${filePath}`);
// Lightweight rate limit check - temporarily skip, let GitHub API handle rate limits itself
@ -605,7 +627,7 @@ export default class GitSyncPlugin extends Plugin {
result.exists || false,
this.app.vault
);
console.log(`File cache refreshed: ${filePath}`);
debugLog(`File cache refreshed: ${filePath}`);
}
} catch (error) {
console.error('Failed to refresh file cache:', error);
@ -838,7 +860,7 @@ export default class GitSyncPlugin extends Plugin {
uploadNotice.hide();
new Notice(t('cos.upload.success'));
console.log('Image uploaded successfully:', uploadResult.url);
debugLog('Image uploaded successfully:', uploadResult.url);
} else {
throw new Error(uploadResult.message || 'Upload failed');
}
@ -895,7 +917,7 @@ export default class GitSyncPlugin extends Plugin {
editor.replaceRange(imageMarkdown, cursorPos);
new Notice(t('cos.upload.success'));
console.log('Image saved locally:', localPath);
debugLog('Image saved locally:', localPath);
} catch (error) {
new Notice(t('cos.upload.failed', { error: error.message }));
console.error('Local image save failed:', error);
@ -928,7 +950,7 @@ export default class GitSyncPlugin extends Plugin {
// Save file to vault
await this.app.vault.createBinary(localPath, arrayBuffer);
console.log('Local image copy saved:', localPath);
debugLog('Local image copy saved:', localPath);
} catch (error) {
console.warn('Failed to save local image copy:', error);
}
@ -974,6 +996,7 @@ class GitSyncSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.githubToken)
.onChange(async (value) => {
this.plugin.settings.githubToken = value;
await this.plugin.saveSettings();
}));
let pathInfoEl: HTMLElement;
@ -986,6 +1009,7 @@ class GitSyncSettingTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.repositoryUrl = value;
this.updatePathInfo(pathInfoEl, value);
await this.plugin.saveSettings();
}));
// Add path description display area
@ -1006,18 +1030,36 @@ class GitSyncSettingTab extends PluginSettingTab {
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);
if (!this.plugin.settings.githubToken) {
new Notice(t('notice.token.required'));
return;
}
// Set loading state
button.setButtonText(t('settings.test.url.loading'));
button.setDisabled(true);
try {
debugLog('=== Testing GitHub Connection ===');
debugLog('Input URL:', this.plugin.settings.repositoryUrl);
// Test connection (includes URL parsing, token validation, and repository access)
const testResult = await this.plugin.githubService.testConnection(this.plugin.settings.repositoryUrl);
if (testResult.success) {
debugLog('Connection test successful:', testResult);
new Notice(testResult.message, 8000);
} else {
debugLog('Connection test failed:', testResult.message);
new Notice(testResult.message, 6000);
}
} catch (error) {
debugLog('Connection test error:', error);
new Notice(t('github.api.operation.failed', { message: error.message }), 6000);
} finally {
// Reset button state
button.setButtonText(t('settings.test.url.button'));
button.setDisabled(false);
}
}));
@ -1421,14 +1463,10 @@ class GitSyncSettingTab extends PluginSettingTab {
// PayPal sponsor button
const sponsorButton = sponsorControl.createEl('a', {
cls: 'mod-cta',
cls: 'mod-cta git-sync-sponsor-button',
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') {
@ -1443,14 +1481,12 @@ class GitSyncSettingTab extends PluginSettingTab {
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;
return files.filter(file => !file.path.startsWith(this.app.vault.configDir)).length === 0;
}
private updatePathInfo(el: HTMLElement, value: string) {

View file

@ -1,7 +1,7 @@
{
"id": "git-folder-sync",
"name": "Git Folder Sync",
"version": "1.0.3",
"version": "1.0.5",
"minAppVersion": "0.15.0",
"description": "Synchronize files in your Obsidian Vault with a specified directory in the designated GitHub repository (rather than the entire repository); send your images to cloud storage services.",
"author": "Xheldon",

View file

@ -1,6 +1,6 @@
{
"name": "git-folder-sync",
"version": "1.0.3",
"version": "1.0.5",
"description": "Synchronize files in your Obsidian Vault with a specified directory in the designated GitHub repository (rather than the entire repository); send your images to cloud storage services.",
"main": "main.js",
"scripts": {

View file

@ -334,3 +334,43 @@
padding-left: 8px;
border-left: 3px solid var(--text-accent);
}
/* Status bar display states */
.git-sync-status-bar.hidden {
display: none;
}
.git-sync-status-bar.visible {
display: flex;
}
/* Status text color states */
.git-sync-status-text.modified {
color: var(--color-orange);
}
.git-sync-status-text.synced {
color: var(--color-green);
}
.git-sync-status-text.not-published {
color: var(--text-muted);
}
.git-sync-status-text.normal {
color: var(--text-normal);
}
/* Sponsor button styling */
.git-sync-sponsor-button {
text-decoration: none;
padding: 6px 12px;
border-radius: 3px;
display: inline-block;
}
/* China sponsor link styling */
.git-sync-china-sponsor a {
color: var(--text-accent);
text-decoration: none;
}

View file

@ -2,5 +2,7 @@
"1.0.0": "0.15.0",
"1.0.1": "0.15.0",
"1.0.2": "0.15.0",
"1.0.3": "0.15.0"
"1.0.3": "0.15.0",
"1.0.4": "0.15.0",
"1.0.5": "0.15.0"
}