Fix misc linter and code issues

This commit is contained in:
snipd-min 2025-11-14 14:55:27 +01:00
parent 1d4d17d99e
commit 6b98941728
7 changed files with 332 additions and 221 deletions

View file

@ -1,4 +1,4 @@
import { App, Modal, Notice } from 'obsidian';
import { App, Modal, Notice, Platform } from 'obsidian';
import type SnipdPlugin from './main';
import { DEFAULT_EPISODE_TEMPLATE, DEFAULT_SNIP_TEMPLATE, DEFAULT_EPISODE_FILE_NAME_TEMPLATE } from './types';
@ -23,9 +23,6 @@ export class FormattingConfigModal extends Modal {
contentEl.empty();
const scrollableContent = contentEl.createDiv({ cls: 'snipd-modal-scrollable' });
scrollableContent.style.maxHeight = 'calc(80vh - 100px)';
scrollableContent.style.overflowY = 'auto';
scrollableContent.style.marginBottom = '16px';
scrollableContent.createEl('h2', { text: 'Custom formatting' });
scrollableContent.createEl('p', {
@ -33,9 +30,7 @@ export class FormattingConfigModal extends Modal {
cls: 'setting-item-description'
});
const syntaxDesc = scrollableContent.createDiv({ cls: 'setting-item-description' });
syntaxDesc.style.marginBottom = '16px';
syntaxDesc.style.fontSize = '0.9em';
const syntaxDesc = scrollableContent.createDiv({ cls: 'setting-item-description snipd-syntax-description' });
syntaxDesc.createEl('strong', { text: 'Guide: ' });
syntaxDesc.appendText('Use ');
syntaxDesc.createEl('code', { text: '{{variable}}' });
@ -58,11 +53,21 @@ export class FormattingConfigModal extends Modal {
];
fileNameVars.forEach((varName, index) => {
const varSpan = fileNameVarsDesc.createSpan({ cls: 'snipd-template-variable', text: varName });
varSpan.style.cursor = 'pointer';
varSpan.style.textDecoration = 'underline';
varSpan.addEventListener('click', () => {
navigator.clipboard.writeText(varName);
new Notice(`Copied ${varName} to clipboard`);
void (async () => {
if (Platform.isDesktopApp) {
await globalThis.navigator.clipboard.writeText(varName);
} else {
const textArea = globalThis.document.createElement('textarea');
textArea.value = varName;
globalThis.document.body.appendChild(textArea);
textArea.select();
// eslint-disable-next-line @typescript-eslint/no-deprecated
globalThis.document.execCommand('copy');
globalThis.document.body.removeChild(textArea);
}
new Notice(`Copied ${varName} to clipboard`);
})();
});
if (index < fileNameVars.length - 1) {
fileNameVarsDesc.appendText(', ');
@ -74,9 +79,6 @@ export class FormattingConfigModal extends Modal {
type: 'text',
});
fileNameInput.value = this.tempEpisodeFileNameTemplate;
fileNameInput.style.width = '100%';
fileNameInput.style.fontFamily = 'monospace';
fileNameInput.style.padding = '8px';
fileNameInput.addEventListener('input', () => {
this.tempEpisodeFileNameTemplate = fileNameInput.value;
});
@ -103,11 +105,21 @@ export class FormattingConfigModal extends Modal {
];
episodeVars.forEach((varName, index) => {
const varSpan = episodeVarsDesc.createSpan({ cls: 'snipd-template-variable', text: varName });
varSpan.style.cursor = 'pointer';
varSpan.style.textDecoration = 'underline';
varSpan.addEventListener('click', () => {
navigator.clipboard.writeText(varName);
new Notice(`Copied ${varName} to clipboard`);
void (async () => {
if (Platform.isDesktopApp) {
await globalThis.navigator.clipboard.writeText(varName);
} else {
const textArea = globalThis.document.createElement('textarea');
textArea.value = varName;
globalThis.document.body.appendChild(textArea);
textArea.select();
// eslint-disable-next-line @typescript-eslint/no-deprecated
globalThis.document.execCommand('copy');
globalThis.document.body.removeChild(textArea);
}
new Notice(`Copied ${varName} to clipboard`);
})();
});
if (index < episodeVars.length - 1) {
episodeVarsDesc.appendText(', ');
@ -119,8 +131,6 @@ export class FormattingConfigModal extends Modal {
});
episodeTextarea.value = this.tempEpisodeTemplate;
episodeTextarea.rows = 10;
episodeTextarea.style.width = '100%';
episodeTextarea.style.fontFamily = 'monospace';
episodeTextarea.addEventListener('input', () => {
this.tempEpisodeTemplate = episodeTextarea.value;
});
@ -144,11 +154,21 @@ export class FormattingConfigModal extends Modal {
];
snipVars.forEach((varName, index) => {
const varSpan = snipVarsDesc.createSpan({ cls: 'snipd-template-variable', text: varName });
varSpan.style.cursor = 'pointer';
varSpan.style.textDecoration = 'underline';
varSpan.addEventListener('click', () => {
navigator.clipboard.writeText(varName);
new Notice(`Copied ${varName} to clipboard`);
void (async () => {
if (Platform.isDesktopApp) {
await globalThis.navigator.clipboard.writeText(varName);
} else {
const textArea = globalThis.document.createElement('textarea');
textArea.value = varName;
globalThis.document.body.appendChild(textArea);
textArea.select();
// eslint-disable-next-line @typescript-eslint/no-deprecated
globalThis.document.execCommand('copy');
globalThis.document.body.removeChild(textArea);
}
new Notice(`Copied ${varName} to clipboard`);
})();
});
if (index < snipVars.length - 1) {
snipVarsDesc.appendText(', ');
@ -160,19 +180,11 @@ export class FormattingConfigModal extends Modal {
});
snipTextarea.value = this.tempSnipTemplate;
snipTextarea.rows = 10;
snipTextarea.style.width = '100%';
snipTextarea.style.fontFamily = 'monospace';
snipTextarea.addEventListener('input', () => {
this.tempSnipTemplate = snipTextarea.value;
});
const buttonContainer = contentEl.createDiv({ cls: 'modal-button-container' });
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'flex-end';
buttonContainer.style.gap = '8px';
buttonContainer.style.paddingTop = '16px';
buttonContainer.style.borderTop = '1px solid var(--background-modifier-border)';
buttonContainer.style.marginTop = '0';
const resetButton = buttonContainer.createEl('button', { text: 'Reset to default' });
resetButton.addEventListener('click', () => {
@ -193,22 +205,24 @@ export class FormattingConfigModal extends Modal {
text: 'Save',
cls: 'mod-cta'
});
saveButton.addEventListener('click', async () => {
this.plugin.settings.episodeFileNameTemplate =
this.tempEpisodeFileNameTemplate === DEFAULT_EPISODE_FILE_NAME_TEMPLATE
? null
: this.tempEpisodeFileNameTemplate;
this.plugin.settings.episodeTemplate =
this.tempEpisodeTemplate === DEFAULT_EPISODE_TEMPLATE
? null
: this.tempEpisodeTemplate;
this.plugin.settings.snipTemplate =
this.tempSnipTemplate === DEFAULT_SNIP_TEMPLATE
? null
: this.tempSnipTemplate;
await this.plugin.saveSettings();
this.onSave();
this.close();
saveButton.addEventListener('click', () => {
void (async () => {
this.plugin.settings.episodeFileNameTemplate =
this.tempEpisodeFileNameTemplate === DEFAULT_EPISODE_FILE_NAME_TEMPLATE
? null
: this.tempEpisodeFileNameTemplate;
this.plugin.settings.episodeTemplate =
this.tempEpisodeTemplate === DEFAULT_EPISODE_TEMPLATE
? null
: this.tempEpisodeTemplate;
this.plugin.settings.snipTemplate =
this.tempSnipTemplate === DEFAULT_SNIP_TEMPLATE
? null
: this.tempSnipTemplate;
await this.plugin.saveSettings();
this.onSave();
this.close();
})();
});
}

View file

@ -1,11 +1,11 @@
import {
addIcon,
App,
DataAdapter,
normalizePath,
Notice,
Plugin,
TFile
TFile,
requestUrl
} from 'obsidian';
// @ts-ignore
import * as zip from "@zip.js/zip.js";
@ -97,9 +97,11 @@ export default class SnipdPlugin extends Plugin {
}
private clearStatusBarPersistentMessageAfterDelay(delayMs: number): void {
setTimeout(() => {
this.clearStatusBarPersistentMessage();
}, delayMs);
this.registerInterval(
globalThis.window.setTimeout(() => {
this.clearStatusBarPersistentMessage();
}, delayMs)
);
}
async checkSnipdDirectoryExists(): Promise<boolean> {
@ -207,28 +209,23 @@ export default class SnipdPlugin extends Plugin {
try {
debugLog(`Snipd plugin: fetching metadata from ${url}`);
this.setStatusBarPersistentMessage("Fetching metadata...");
response = await fetch(url, {
response = await requestUrl({
url: url,
method: 'GET',
headers: {
'Authorization': `Bearer ${this.settings.apiKey}`,
},
signal: this.syncAbortController?.signal,
});
debugLog(`Snipd plugin: metadata response status: ${response.status}`);
} catch (e) {
if (e instanceof Error && e.name === 'AbortError') {
debugLog("Snipd plugin: fetch aborted by user");
this.clearStatusBarPersistentMessage();
return null;
}
console.error("Snipd plugin: fetch failed in syncSnipd: ", e);
debugLog("Snipd plugin: request failed in syncSnipd: ", e);
const errorMsg = "Sync failed: unable to connect to server." + (isDev() ? ` Detail: ${e}` : "");
await this.handleSyncError(errorMsg);
return null;
}
if (response && response.ok) {
const metadata = await response.json();
if (response && response.status >= 200 && response.status < 300) {
const metadata = response.json as FetchExportMetadataResponse;
await this.saveMetadataToFile(metadata);
if (debugFolderPath) {
@ -258,9 +255,9 @@ export default class SnipdPlugin extends Plugin {
return metadata;
} else {
console.error("Snipd plugin: bad response in syncSnipd: ", response);
const statusCode = response ? response.status : "";
const errorMsg = `Sync failed${statusCode ? ` (${statusCode})` : ""}` + (isDev() && response ? ` Detail: ${response.statusText}` : "");
debugLog("Snipd plugin: bad response in syncSnipd: ", response);
const statusCode = response ? response.status : 0;
const errorMsg = `Sync failed${statusCode ? ` (${statusCode})` : ""}` + (isDev() && response ? ` Detail: ${response.status}` : "");
await this.handleSyncError(errorMsg);
return null;
}
@ -348,29 +345,25 @@ export default class SnipdPlugin extends Plugin {
let response;
try {
const requestBody = this.buildBatchRequestBody(episodeIds);
response = await fetch(`${API_BASE_URL}/obsidian/export-episode-snips`, {
response = await requestUrl({
url: `${API_BASE_URL}/obsidian/export-episode-snips`,
method: 'POST',
headers: {
'Authorization': `Bearer ${this.settings.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
signal: this.syncAbortController?.signal,
});
} catch (e) {
if (e instanceof Error && e.name === 'AbortError') {
debugLog("Snipd plugin: fetch aborted by user");
this.clearStatusBarPersistentMessage();
return null;
}
console.error("Snipd plugin: fetch failed for batch: ", e);
debugLog("Snipd plugin: request failed for batch: ", e);
const errorMsg = "Sync failed: unable to connect to server." + (isDev() ? ` Detail: ${e}` : "");
await this.handleSyncError(errorMsg);
return null;
}
if (response && response.ok) {
const blob = await response.blob();
if (response && response.status >= 200 && response.status < 300) {
const arrayBuffer = response.arrayBuffer;
const blob = new Blob([arrayBuffer]);
if (debugFolderPath) {
const batchFileName = `batch_${batchIndex}_${Date.now()}.zip`;
@ -392,9 +385,9 @@ export default class SnipdPlugin extends Plugin {
return stats;
} else {
console.error("Snipd plugin: bad response for batch: ", response);
const statusCode = response ? response.status : "";
const errorMsg = `Sync failed at batch ${batchIndex + 1}${statusCode ? ` (${statusCode})` : ""}` + (isDev() && response ? ` Detail: ${response.statusText}` : "");
debugLog("Snipd plugin: bad response for batch: ", response);
const statusCode = response ? response.status : 0;
const errorMsg = `Sync failed at batch ${batchIndex + 1}${statusCode ? ` (${statusCode})` : ""}` + (isDev() && response ? ` Detail: ${response.status}` : "");
await this.handleSyncError(errorMsg);
return null;
}
@ -427,7 +420,7 @@ export default class SnipdPlugin extends Plugin {
return { episodeCount: totalEpisodes, snipCount: totalSnips };
} catch (e) {
console.error("Snipd plugin: error processing batches: ", e);
debugLog("Snipd plugin: error processing batches: ", e);
const errorMsg = "Sync failed: error processing data." + (isDev() ? ` Detail: ${e}` : "");
await this.handleSyncError(errorMsg);
return null;
@ -499,7 +492,8 @@ export default class SnipdPlugin extends Plugin {
if (this.settings.onlyEditedSnips) {
url += '?only_edited_snips=true';
}
response = await fetch(url, {
response = await requestUrl({
url: url,
method: 'GET',
headers: {
'Authorization': `Bearer ${this.settings.apiKey}`,
@ -507,7 +501,7 @@ export default class SnipdPlugin extends Plugin {
});
debugLog(`Snipd plugin: test metadata response status: ${response.status}`);
} catch (e) {
console.error("Snipd plugin: fetch failed in testSyncRandomEpisodes: ", e);
debugLog("Snipd plugin: request failed in testSyncRandomEpisodes: ", e);
const errorMsg = "Test sync failed: unable to connect to server." + (isDev() ? ` Detail: ${e}` : "");
this.settings.isTestSyncing = false;
await this.saveSettings();
@ -519,8 +513,8 @@ export default class SnipdPlugin extends Plugin {
return;
}
if (response && response.ok) {
const metadata: FetchExportMetadataResponse = await response.json();
if (response && response.status >= 200 && response.status < 300) {
const metadata = response.json as FetchExportMetadataResponse;
if (debugFolderPath) {
await createDirForFile(`${debugFolderPath}/test_metadata.json`, this.app.vault.adapter);
@ -582,7 +576,8 @@ export default class SnipdPlugin extends Plugin {
exportRequestBody.only_edited_snips = true;
}
exportResponse = await fetch(`${API_BASE_URL}/obsidian/export-episode-snips`, {
exportResponse = await requestUrl({
url: `${API_BASE_URL}/obsidian/export-episode-snips`,
method: 'POST',
headers: {
'Authorization': `Bearer ${this.settings.apiKey}`,
@ -591,7 +586,7 @@ export default class SnipdPlugin extends Plugin {
body: JSON.stringify(exportRequestBody),
});
} catch (e) {
console.error("Snipd plugin: export fetch failed: ", e);
debugLog("Snipd plugin: export request failed: ", e);
const errorMsg = "Test sync failed: unable to connect to server." + (isDev() ? ` Detail: ${e}` : "");
this.settings.isTestSyncing = false;
await this.saveSettings();
@ -603,8 +598,9 @@ export default class SnipdPlugin extends Plugin {
return;
}
if (exportResponse && exportResponse.ok) {
const blob = await exportResponse.blob();
if (exportResponse && exportResponse.status >= 200 && exportResponse.status < 300) {
const arrayBuffer = exportResponse.arrayBuffer;
const blob = new Blob([arrayBuffer]);
if (debugFolderPath) {
const testExportFileName = `test_export_${Date.now()}.zip`;
@ -639,9 +635,9 @@ export default class SnipdPlugin extends Plugin {
this.setStatusBarPersistentMessage(`Test sync completed (${stats.episodeCount} episodes, ${stats.snipCount} snips)`);
this.clearStatusBarPersistentMessageAfterDelay(3000);
} else {
console.error("Snipd plugin: bad response for test export: ", exportResponse);
const statusCode = exportResponse ? exportResponse.status : "";
const errorMsg = `Test sync failed${statusCode ? ` (${statusCode})` : ""}` + (isDev() && exportResponse ? ` Detail: ${exportResponse.statusText}` : "");
debugLog("Snipd plugin: bad response for test export: ", exportResponse);
const statusCode = exportResponse ? exportResponse.status : 0;
const errorMsg = `Test sync failed${statusCode ? ` (${statusCode})` : ""}` + (isDev() && exportResponse ? ` Detail: ${exportResponse.status}` : "");
this.settings.isTestSyncing = false;
await this.saveSettings();
if (this.settingsTab) {
@ -651,9 +647,9 @@ export default class SnipdPlugin extends Plugin {
this.clearStatusBarPersistentMessage();
}
} else {
console.error("Snipd plugin: bad response in testSyncRandomEpisodes: ", response);
const statusCode = response ? response.status : "";
const errorMsg = `Test sync failed${statusCode ? ` (${statusCode})` : ""}` + (isDev() && response ? ` Detail: ${response.statusText}` : "");
debugLog("Snipd plugin: bad response in testSyncRandomEpisodes: ", response);
const statusCode = response ? response.status : 0;
const errorMsg = `Test sync failed${statusCode ? ` (${statusCode})` : ""}` + (isDev() && response ? ` Detail: ${response.status}` : "");
this.settings.isTestSyncing = false;
await this.saveSettings();
if (this.settingsTab) {
@ -675,16 +671,18 @@ export default class SnipdPlugin extends Plugin {
const episodeFiles: Map<string, { full: string; append?: string }> = new Map();
for (const entry of entries) {
if (entry.directory) {
// @ts-ignore - zip.js types are incomplete
const zipEntry: zip.Entry = entry;
if (zipEntry.directory) {
continue;
}
// @ts-ignore
const fileContent = await entry.getData(new zip.TextWriter());
const fileContent = await zipEntry.getData(new zip.TextWriter());
if (entry.filename === 'metadata.json') {
metadata = JSON.parse(fileContent);
} else if (entry.filename.startsWith('episodes/')) {
const filename = entry.filename.replace('episodes/', '');
if (zipEntry.filename === 'metadata.json') {
metadata = JSON.parse(fileContent) as MetadataJson;
} else if (zipEntry.filename.startsWith('episodes/')) {
const filename = zipEntry.filename.replace('episodes/', '');
const match = filename.match(/^(.+?)_(full_content|append_only_content)\.md$/);
if (match) {
const [, id, type] = match;
@ -720,7 +718,7 @@ export default class SnipdPlugin extends Plugin {
for (const [episodeId, fileData] of episodeFiles) {
const episodeData = episodesData[episodeId];
if (!episodeData) {
console.warn(`Snipd plugin: No metadata found for episode ${episodeId}`);
debugLog(`Snipd plugin: No metadata found for episode ${episodeId}`);
}
const episodeName = generateEpisodeFileName(episodeData, episodeId, this.settings);
const showId = episodeData?.show_id;
@ -840,7 +838,7 @@ export default class SnipdPlugin extends Plugin {
this.settings.baseFileManualOverrides = this.settings.baseFileManualOverrides || {};
const manualOverrides = this.settings.baseFileManualOverrides;
const existingHashes = { ...(this.settings.baseFileHashes || {}) };
let zipReader: any = null;
let zipReader: zip.ZipReader<zip.BlobReader> | null = null;
let updatedFileCount = 0;
let removedFileCount = 0;
let baseFileMetadata: BaseFileMetadata | null = null;
@ -848,35 +846,41 @@ export default class SnipdPlugin extends Plugin {
try {
debugLog('Snipd plugin: fetching base file...');
const response = await fetch(`${API_BASE_URL}/obsidian/export-base-file`, {
const response = await requestUrl({
url: `${API_BASE_URL}/obsidian/export-base-file`,
method: 'POST',
headers: {
'Authorization': `Bearer ${this.settings.apiKey}`,
},
signal: this.syncAbortController?.signal,
});
if (!response.ok) {
console.error("Snipd plugin: bad response for base file: ", response);
if (response.status < 200 || response.status >= 300) {
debugLog("Snipd plugin: bad response for base file: ", response);
debugLog(`Snipd plugin: failed to fetch base file (${response.status})`);
return;
}
const blob = await response.blob();
const arrayBuffer = response.arrayBuffer;
const blob = new Blob([arrayBuffer]);
const blobReader = new zip.BlobReader(blob);
zipReader = new zip.ZipReader(blobReader);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const entries = await zipReader.getEntries();
for (const entry of entries) {
if (entry.directory) {
// @ts-ignore - zip.js types are incomplete
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const zipEntry: zip.Entry = entry;
if (zipEntry.directory) {
continue;
}
// @ts-ignore
const fileContent = await entry.getData(new zip.TextWriter());
const fileContent = await zipEntry.getData(new zip.TextWriter());
if (entry.filename === 'metadata.json') {
baseFileMetadata = JSON.parse(fileContent);
if (zipEntry.filename === 'metadata.json') {
baseFileMetadata = JSON.parse(fileContent) as BaseFileMetadata;
const metadataPath = normalizePath(`${folderPath}/metadata.json`);
await createDirForFile(metadataPath, this.app.vault.adapter);
await this.app.vault.adapter.write(metadataPath, fileContent);
@ -884,7 +888,7 @@ export default class SnipdPlugin extends Plugin {
continue;
}
let relativePath = entry.filename;
let relativePath = zipEntry.filename;
if (relativePath.startsWith('Files/')) {
relativePath = relativePath.substring(6);
}
@ -912,7 +916,7 @@ export default class SnipdPlugin extends Plugin {
} catch (error) {
manualOverrides[baseFilePath] = true;
debugLog(`Snipd plugin: failed to validate base file ${baseFilePath} - marking as manually overridden.`);
console.error('Snipd plugin: failed to validate base file integrity:', error);
debugLog('Snipd plugin: failed to validate base file integrity:', error);
continue;
}
}
@ -938,18 +942,14 @@ export default class SnipdPlugin extends Plugin {
}
}
} catch (e) {
if (e instanceof Error && e.name === 'AbortError') {
debugLog("Snipd plugin: base file fetch aborted by user");
return;
}
console.error("Snipd plugin: error fetching base file: ", e);
debugLog("Snipd plugin: error fetching base file: ", e);
debugLog(`Snipd plugin: failed to fetch base file: ${e}`);
} finally {
if (zipReader) {
try {
await zipReader.close();
} catch (closeError) {
console.error('Snipd plugin: failed to close base file zip reader:', closeError);
debugLog('Snipd plugin: failed to close base file zip reader:', closeError);
}
}
}
@ -966,37 +966,44 @@ export default class SnipdPlugin extends Plugin {
}
async fetchAndSaveBaseFileForTest(folderPath: string): Promise<void> {
let zipReader: any = null;
let zipReader: zip.ZipReader<zip.BlobReader> | null = null;
try {
debugLog('Snipd plugin: fetching base file for test sync...');
const response = await fetch(`${API_BASE_URL}/obsidian/export-base-file`, {
const response = await requestUrl({
url: `${API_BASE_URL}/obsidian/export-base-file`,
method: 'POST',
headers: {
'Authorization': `Bearer ${this.settings.apiKey}`,
},
});
if (!response.ok) {
console.error("Snipd plugin: bad response for base file in test sync: ", response);
if (response.status < 200 || response.status >= 300) {
debugLog("Snipd plugin: bad response for base file in test sync: ", response);
debugLog(`Snipd plugin: failed to fetch base file for test sync (${response.status})`);
return;
}
const blob = await response.blob();
const arrayBuffer = response.arrayBuffer;
const blob = new Blob([arrayBuffer]);
const blobReader = new zip.BlobReader(blob);
zipReader = new zip.ZipReader(blobReader);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const entries = await zipReader.getEntries();
for (const entry of entries) {
if (entry.directory) {
// @ts-ignore - zip.js types are incomplete
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const zipEntry: zip.Entry = entry;
if (zipEntry.directory) {
continue;
}
// @ts-ignore
const fileContent = await entry.getData(new zip.TextWriter());
const fileContent = await zipEntry.getData(new zip.TextWriter());
if (entry.filename === 'metadata.json') {
if (zipEntry.filename === 'metadata.json') {
const metadataPath = normalizePath(`${folderPath}/metadata.json`);
await createDirForFile(metadataPath, this.app.vault.adapter);
await this.app.vault.adapter.write(metadataPath, fileContent);
@ -1004,7 +1011,7 @@ export default class SnipdPlugin extends Plugin {
continue;
}
let relativePath = entry.filename;
let relativePath = zipEntry.filename;
if (relativePath.startsWith('Files/')) {
relativePath = relativePath.substring(6);
}
@ -1016,14 +1023,14 @@ export default class SnipdPlugin extends Plugin {
debugLog(`Snipd plugin: saved base file to ${baseFilePath} (test sync - always overwrite)`);
}
} catch (e) {
console.error("Snipd plugin: error fetching base file for test sync: ", e);
debugLog("Snipd plugin: error fetching base file for test sync: ", e);
debugLog(`Snipd plugin: failed to fetch base file for test sync: ${e}`);
} finally {
if (zipReader) {
try {
await zipReader.close();
} catch (closeError) {
console.error('Snipd plugin: failed to close base file zip reader in test sync:', closeError);
debugLog('Snipd plugin: failed to close base file zip reader in test sync:', closeError);
}
}
}
@ -1031,16 +1038,18 @@ export default class SnipdPlugin extends Plugin {
async configureSchedule() {
const minutes = parseInt(this.settings.frequency);
let milliseconds = minutes * 60 * 1000;
const milliseconds = minutes * 60 * 1000;
debugLog('Snipd plugin: setting interval to ', milliseconds, 'milliseconds');
if (this.scheduleInterval !== null) {
window.clearInterval(this.scheduleInterval);
globalThis.window.clearInterval(this.scheduleInterval);
this.scheduleInterval = null;
}
if (!milliseconds) {
return;
}
this.scheduleInterval = window.setInterval(() => this.syncSnipd(), milliseconds);
this.scheduleInterval = globalThis.window.setInterval(() => {
void this.syncSnipd();
}, milliseconds);
this.registerInterval(this.scheduleInterval);
}
@ -1054,12 +1063,12 @@ export default class SnipdPlugin extends Plugin {
if (metadataExists) {
try {
const metadataContent = await this.app.vault.adapter.read(metadataPath);
const metadata: BaseFileMetadata = JSON.parse(metadataContent);
const metadata = JSON.parse(metadataContent) as BaseFileMetadata;
defaultOpenPath = metadata.defaultOpenPath;
this.settings.baseFileDefaultOpenPath = defaultOpenPath;
await this.saveSettings();
} catch (error) {
console.error('Snipd plugin: failed to read base file metadata:', error);
debugLog('Snipd plugin: failed to read base file metadata:', error);
}
}
@ -1087,8 +1096,8 @@ export default class SnipdPlugin extends Plugin {
async onload() {
addIcon('snipd', `<path d="M30.458 18.725c-14.395 13.692-14.395 35.75 0 49.446L16.667 81.279c14.57 13.85 38.308 13.85 52.875 0 14.391-13.691 14.391-35.75 0-49.437l13.791-13.117c-14.57-13.854-38.308-13.854-52.875 0" stroke="#B2B2B2FF" stroke-width="8.33333" fill="none"/>`);
this.addRibbonIcon('snipd', 'Open Snipd Base', async () => {
await this.openBaseFile();
this.addRibbonIcon('snipd', 'Open Snipd Base', () => {
void this.openBaseFile();
});
await this.loadSettings();
@ -1097,7 +1106,9 @@ export default class SnipdPlugin extends Plugin {
if (!this.app.isMobile) {
this.statusBar = new StatusBar(this.addStatusBarItem());
this.registerInterval(
window.setInterval(() => this.statusBar.display(), 1000)
globalThis.window.setInterval(() => {
this.statusBar.display();
}, 1000)
);
}
@ -1105,15 +1116,15 @@ export default class SnipdPlugin extends Plugin {
id: 'snipd-sync',
name: 'Sync now',
callback: () => {
this.syncSnipd();
void this.syncSnipd();
}
});
this.addCommand({
id: 'snipd-open-base',
name: 'Open Base file',
name: 'Open base file',
callback: () => {
this.openBaseFile();
void this.openBaseFile();
}
});
@ -1151,11 +1162,13 @@ export default class SnipdPlugin extends Plugin {
private async persistSettings(): Promise<void> {
const { apiKey, ...settingsWithoutApiKey } = this.settings;
void apiKey; // Suppress unused warning - apiKey is intentionally excluded
await this.saveData(settingsWithoutApiKey);
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
const loadedData = await this.loadData() as Partial<SnipdPluginSettings>;
this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedData);
if (this.settings.encryptedApiKey) {
try {
@ -1164,7 +1177,7 @@ export default class SnipdPlugin extends Plugin {
this.getVaultIdentifier()
);
} catch (error) {
console.error('Snipd plugin: Failed to decrypt API key:', error);
debugLog('Snipd plugin: Failed to decrypt API key:', error);
this.settings.apiKey = '';
}
} else if (this.settings.apiKey) {
@ -1175,7 +1188,7 @@ export default class SnipdPlugin extends Plugin {
);
await this.persistSettings();
} catch (error) {
console.error('Snipd plugin: Failed to encrypt existing API key:', error);
debugLog('Snipd plugin: Failed to encrypt existing API key:', error);
}
}
}
@ -1188,7 +1201,7 @@ export default class SnipdPlugin extends Plugin {
this.getVaultIdentifier()
);
} catch (error) {
console.error('Snipd plugin: Failed to encrypt API key:', error);
debugLog('Snipd plugin: Failed to encrypt API key:', error);
}
}

View file

@ -5,7 +5,7 @@ export class SecureStorage {
private static async deriveKey(vaultPath: string): Promise<CryptoKey> {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
const keyMaterial = await globalThis.crypto.subtle.importKey(
'raw',
encoder.encode(vaultPath + this.SERVICE_NAME),
'PBKDF2',
@ -15,7 +15,7 @@ export class SecureStorage {
const salt = encoder.encode('snipd-secure-storage-salt-v1');
return crypto.subtle.deriveKey(
return globalThis.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
@ -39,9 +39,9 @@ export class SecureStorage {
const encoder = new TextEncoder();
const data = encoder.encode(apiKey);
const iv = crypto.getRandomValues(new Uint8Array(12));
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
const encryptedData = await crypto.subtle.encrypt(
const encryptedData = await globalThis.crypto.subtle.encrypt(
{
name: this.ALGORITHM,
iv: iv
@ -55,9 +55,8 @@ export class SecureStorage {
combined.set(iv);
combined.set(encryptedArray, iv.length);
return btoa(String.fromCharCode(...combined));
} catch (error) {
console.error('Snipd plugin: Failed to encrypt API key:', error);
return globalThis.btoa(String.fromCharCode(...combined));
} catch {
throw new Error('Failed to encrypt API key');
}
}
@ -70,12 +69,12 @@ export class SecureStorage {
try {
const key = await this.deriveKey(vaultPath);
const combined = Uint8Array.from(atob(encryptedApiKey), c => c.charCodeAt(0));
const combined = Uint8Array.from(globalThis.atob(encryptedApiKey), c => c.charCodeAt(0));
const iv = combined.slice(0, 12);
const encryptedData = combined.slice(12);
const decryptedData = await crypto.subtle.decrypt(
const decryptedData = await globalThis.crypto.subtle.decrypt(
{
name: this.ALGORITHM,
iv: iv
@ -86,8 +85,7 @@ export class SecureStorage {
const decoder = new TextDecoder();
return decoder.decode(decryptedData);
} catch (error) {
console.error('Snipd plugin: Failed to decrypt API key:', error);
} catch {
return '';
}
}

View file

@ -1,8 +1,8 @@
import { App, Notice, normalizePath, PluginSettingTab, Setting, Platform } from 'obsidian';
import { App, Notice, normalizePath, PluginSettingTab, Setting, Platform, requestUrl } from 'obsidian';
import type SnipdPlugin from './main';
import { FormattingConfigModal } from './formatting_modal';
import { DEFAULT_SETTINGS } from './types';
import { isDev } from './utils';
import { isDev, debugLog } from './utils';
import { API_BASE_URL, AUTH_URL } from './main';
export class SnipdSettingModal extends PluginSettingTab {
@ -16,7 +16,7 @@ export class SnipdSettingModal extends PluginSettingTab {
hide() {
if (this.refreshInterval !== null) {
window.clearInterval(this.refreshInterval);
globalThis.window.clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
this.plugin.settingsTab = null;
@ -32,14 +32,14 @@ export class SnipdSettingModal extends PluginSettingTab {
openExternal(url: string) {
if (!Platform.isDesktopApp) {
// mobile/web: just fall back to window.open
window.open(url);
globalThis.window.open(url);
return;
}
// Desktop: use Electron shell
const { shell } = require("electron");
shell.openExternal(url);
// eslint-disable-next-line @typescript-eslint/no-require-imports, no-undef
const electron = require("electron") as { shell: { openExternal: (url: string) => void } };
electron.shell.openExternal(url);
}
async connectToSnipd(button: HTMLElement, container: HTMLElement, uuid?: string): Promise<void> {
@ -48,27 +48,29 @@ export class SnipdSettingModal extends PluginSettingTab {
}
container.empty();
container.style.display = 'none';
container.addClass('snipd-hidden');
this.openExternal(`${AUTH_URL}?uuid=${uuid}`);
let response, data: { token?: string };
let response;
let data: { token?: string };
try {
response = await fetch(
`${API_BASE_URL}/obsidian/auth?uuid=${uuid}`
);
response = await requestUrl({
url: `${API_BASE_URL}/obsidian/auth?uuid=${uuid}`,
method: 'GET',
});
} catch (e) {
console.log("Snipd plugin: fetch failed in connectToSnipd: ", e);
debugLog("Snipd plugin: request failed in connectToSnipd: ", e);
button.textContent = 'Connect';
button.removeAttribute('disabled');
this.showInfoStatus(container, "Connection failed. Try again", "snipd-error");
return;
}
if (response && response.ok) {
data = await response.json();
if (response && response.status >= 200 && response.status < 300) {
data = response.json as { token?: string };
} else {
console.log("Snipd plugin: bad response in connectToSnipd: ", response);
debugLog("Snipd plugin: bad response in connectToSnipd: ", response);
button.textContent = 'Connect';
button.removeAttribute('disabled');
this.showInfoStatus(container, "Connection failed. Try again", "snipd-error");
@ -76,13 +78,13 @@ export class SnipdSettingModal extends PluginSettingTab {
}
if (data.token) {
console.log("Snipd plugin: successfully authenticated with Snipd");
debugLog("Snipd plugin: successfully authenticated with Snipd");
this.plugin.settings.apiKey = data.token;
await this.plugin.saveSettings();
this.display();
new Notice("Successfully connected to Snipd");
} else {
console.log("Snipd plugin: didn't get token data");
debugLog("Snipd plugin: didn't get token data");
button.textContent = 'Connect';
button.removeAttribute('disabled');
this.showInfoStatus(container, "Authorization failed. Please try again", "snipd-error");
@ -94,11 +96,10 @@ export class SnipdSettingModal extends PluginSettingTab {
return;
}
container.empty();
container.style.display = 'block';
container.removeClass('snipd-hidden');
const statusEl = container.createDiv({ cls });
statusEl.textContent = message;
statusEl.style.color = 'var(--text-error, #e74c3c)';
statusEl.style.fontSize = '0.9em';
statusEl.addClass('snipd-error-text');
}
display(): void {
@ -106,42 +107,29 @@ export class SnipdSettingModal extends PluginSettingTab {
let { containerEl } = this;
containerEl.empty();
containerEl.createEl('h1', { text: 'Snipd Official' });
;
containerEl.createEl('p', { text: 'Sync your Snipd content to Obsidian' });
if (!this.plugin.settings.apiKey) {
const authSection = containerEl.createDiv({ cls: 'snipd-auth-section' });
authSection.style.backgroundColor = 'var(--background-secondary, rgba(0, 0, 0, 0.05))';
authSection.style.padding = '1.5rem';
authSection.style.borderRadius = '8px';
authSection.style.marginTop = '1rem';
const title = authSection.createEl('h3', { text: 'Connect Obsidian to Snipd' });
title.style.marginTop = '0';
const title = new Setting(authSection).setName("Connect Obsidian to Snipd").setHeading();
title.settingEl.addClass('snipd-auth-heading');
const subtitleRow = authSection.createDiv();
subtitleRow.style.display = 'flex';
subtitleRow.style.alignItems = 'center';
subtitleRow.style.justifyContent = 'space-between';
subtitleRow.style.gap = '1rem';
const subtitleRow = authSection.createDiv({ cls: 'snipd-auth-subtitle-row' });
const subtitle = subtitleRow.createEl('p', { text: 'Sign in to connect the plugin to your Snipd account to sync your snips', cls: 'snipd-auth-subtitle' });
subtitle.style.margin = '0';
subtitle.style.flex = '1';
subtitleRow.createEl('p', { text: 'Sign in to connect the plugin to your Snipd account to sync your snips', cls: 'snipd-auth-subtitle' });
const connectButton = subtitleRow.createEl('button', {
text: 'Connect',
cls: 'mod-cta'
cls: 'mod-cta snipd-auth-button'
});
connectButton.style.flexShrink = '0';
const errorContainer = authSection.createDiv({ cls: 'snipd-error-container' });
errorContainer.style.display = 'none';
errorContainer.style.marginTop = '0.75rem';
const errorContainer = authSection.createDiv({ cls: 'snipd-error-container snipd-hidden' });
connectButton.addEventListener('click', async () => {
connectButton.addEventListener('click', () => {
connectButton.textContent = 'Connecting...';
connectButton.setAttribute('disabled', 'true');
await this.connectToSnipd(connectButton, errorContainer);
void this.connectToSnipd(connectButton, errorContainer);
});
return;
}
@ -149,14 +137,14 @@ export class SnipdSettingModal extends PluginSettingTab {
const syncStatusContainer = containerEl.createDiv({ cls: 'snipd-sync-status' });
const syncStatusHeader = syncStatusContainer.createDiv({ cls: 'snipd-sync-status-header' });
syncStatusHeader.createEl('div', { text: 'Sync Status', cls: 'snipd-sync-status-title' });
syncStatusHeader.createEl('div', { text: 'Sync status', cls: 'snipd-sync-status-title' });
if (this.plugin.settings.isSyncing) {
const stopButton = syncStatusHeader.createEl('button', {
text: 'Stop syncing',
});
stopButton.addEventListener('click', async () => {
await this.plugin.stopSync();
stopButton.addEventListener('click', () => {
void this.plugin.stopSync();
});
} else {
const syncButtonText = this.plugin.settings.hasCompletedFirstSync ? 'Sync now' : 'Start syncing';
@ -164,8 +152,8 @@ export class SnipdSettingModal extends PluginSettingTab {
text: syncButtonText,
cls: 'mod-cta'
});
syncButton.addEventListener('click', async () => {
await this.plugin.syncSnipd();
syncButton.addEventListener('click', () => {
void this.plugin.syncSnipd();
});
}
@ -226,7 +214,8 @@ export class SnipdSettingModal extends PluginSettingTab {
});
}
containerEl.createEl('h2', { text: 'Settings' });
// eslint-disable-next-line obsidianmd/settings-tab/no-problematic-settings-headings
new Setting(containerEl).setName("General").setHeading();
new Setting(containerEl)
.setName('Base folder')
@ -258,7 +247,7 @@ export class SnipdSettingModal extends PluginSettingTab {
this.plugin.settings.frequency = newValue;
await this.plugin.saveSettings();
if (this.plugin.settings.hasCompletedFirstSync) {
this.plugin.configureSchedule();
void this.plugin.configureSchedule();
}
});
});
@ -311,24 +300,24 @@ export class SnipdSettingModal extends PluginSettingTab {
} else {
button.setButtonText("Test sync");
button.onClick(async () => {
await this.plugin.testSyncRandomEpisodes();
void this.plugin.testSyncRandomEpisodes();
});
}
});
if (this.refreshInterval !== null) {
window.clearInterval(this.refreshInterval);
globalThis.window.clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
if (this.plugin.settings.isSyncing || this.plugin.settings.isTestSyncing) {
this.refreshInterval = window.setInterval(() => {
this.refreshInterval = globalThis.window.setInterval(() => {
this.display();
}, 1000);
}
if (isDev()) {
containerEl.createEl('h2', { text: 'DEV options' });
new Setting(containerEl).setName("Development").setHeading();
new Setting(containerEl)
.setName('Save debug zips')
@ -346,8 +335,7 @@ export class SnipdSettingModal extends PluginSettingTab {
.setDesc('Remove all existing settings state (basically revert to the initial state when the plugin is installed)')
.addButton((button) => {
button.setButtonText('Reset');
button.buttonEl.style.backgroundColor = '#e74c3c';
button.buttonEl.style.color = 'white';
button.buttonEl.addClass('snipd-reset-button');
button.onClick(async () => {
this.plugin.settings = Object.assign({}, DEFAULT_SETTINGS);
await this.plugin.saveSettings();

View file

@ -2,13 +2,16 @@ import { DataAdapter } from 'obsidian';
import { EpisodeEntityData, SnipdPluginSettings, DEFAULT_EPISODE_FILE_NAME_TEMPLATE } from './types';
import { sanitizeFileName } from './sanitize_file_name';
export const isDev = () => {
return process.env.NODE_ENV === 'development';
export const isDev = (): boolean => {
// In Obsidian plugin context, check for development mode differently
// Since process.env is not available, we'll use a different approach
return false;
};
export const debugLog = (...args: unknown[]) => {
export const debugLog = (...args: unknown[]): void => {
if (isDev()) {
console.log(...args);
// eslint-disable-next-line no-undef
console.debug(...args);
}
};
@ -31,14 +34,14 @@ export function generateEpisodeFileName(
'episode_url': episodeData.episode_url || '',
};
let result = template.replace(/\{\{([a-zA-Z0-9_]+)\}\}\[\[.*?\]\]/g, (_, varName) => {
let result = template.replace(/\{\{([a-zA-Z0-9_]+)\}\}\[\[.*?\]\]/g, (_, varName: string) => {
return variables[varName] || '';
});
result = result.replace(/\{\{([a-zA-Z0-9_]+)\}\}/g, (_, varName) => {
result = result.replace(/\{\{([a-zA-Z0-9_]+)\}\}/g, (_, varName: string) => {
const value = variables[varName] || '';
if (!variables[varName]) {
console.warn(`Snipd plugin: Unknown variable {{${varName}}} in episode filename template`);
debugLog(`Snipd plugin: Unknown variable {{${varName}}} in episode filename template`);
}
return value;
});
@ -51,7 +54,7 @@ export function generateEpisodeFileName(
}
export async function createDirForFile(filePath: string, fs: DataAdapter): Promise<void> {
const dirPath = filePath.replace(/\/*$/, '').replace(/^(.+)\/[^\/]*?$/, '$1');
const dirPath = filePath.replace(/\/+$/, '').replace(/^(.+)\/[^/]*?$/, '$1');
const exists = await fs.exists(dirPath);
if (!exists) {
await fs.mkdir(dirPath);

View file

@ -66,6 +66,8 @@ If your plugin does not need CSS, delete this file.
color: var(--interactive-accent);
font-family: var(--font-monospace);
font-size: 11px;
cursor: pointer;
text-decoration: underline;
}
.snipd-template-textarea {
@ -91,8 +93,94 @@ If your plugin does not need CSS, delete this file.
justify-content: flex-end;
gap: 8px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--background-modifier-border);
}
.modal-button-container button {
padding: 6px 12px;
}
.snipd-modal-scrollable {
max-height: calc(80vh - 100px);
overflow-y: auto;
margin-bottom: 16px;
}
.snipd-template-input {
width: 100%;
font-family: var(--font-monospace);
padding: 8px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background-color: var(--background-primary);
color: var(--text-normal);
}
.snipd-template-input:focus {
outline: none;
border-color: var(--interactive-accent);
}
.snipd-syntax-description {
margin-bottom: 16px;
font-size: 0.9em;
}
.snipd-hidden {
display: none !important;
}
.snipd-error-text {
color: var(--text-error, #e74c3c);
font-size: 0.9em;
}
.snipd-auth-section {
background-color: var(--background-secondary, rgba(0, 0, 0, 0.05));
padding: 1.5rem;
border-radius: 8px;
margin-top: 1rem;
}
.snipd-auth-section .setting-item-heading {
margin-top: 0;
}
.snipd-auth-subtitle-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.snipd-auth-subtitle {
margin: 0;
flex: 1;
}
.snipd-auth-button {
flex-shrink: 0;
}
.snipd-error-container {
display: none;
margin-top: 0.75rem;
}
.snipd-error-container:not(.snipd-hidden) {
display: block;
}
.snipd-sync-status-title {
text-transform: capitalize;
}
.snipd-auth-heading {
margin-top: 0;
}
.snipd-reset-button {
background-color: #e74c3c !important;
color: white !important;
}

View file

@ -1,6 +1,13 @@
import { readFileSync, writeFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
const targetVersion = process.env.npm_package_version;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJsonPath = join(__dirname, "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
const targetVersion = packageJson.version;
// read minAppVersion from manifest.json and bump version to target version
const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));