From 3c2c699df198c295b252941bf1d79b043208e694 Mon Sep 17 00:00:00 2001 From: Acylation <532117255@qq.com> Date: Thu, 4 Apr 2024 17:05:10 +0800 Subject: [PATCH] Eliminate global `app` and stop using node `path` --- src/SettingTab.ts | 21 +++++--- src/fetchHelper.ts | 122 +++++++++++++++++++++++++++++++++++++++++++ src/fetchHelpers.ts | 92 -------------------------------- src/main.ts | 12 +++-- src/schemeHelper.ts | 118 +++++++++++++++++++++++++++++++++++++++++ src/schemeHelpers.ts | 95 --------------------------------- 6 files changed, 263 insertions(+), 197 deletions(-) create mode 100644 src/fetchHelper.ts delete mode 100644 src/fetchHelpers.ts create mode 100644 src/schemeHelper.ts delete mode 100644 src/schemeHelpers.ts diff --git a/src/SettingTab.ts b/src/SettingTab.ts index 5486c91..7cdffad 100644 --- a/src/SettingTab.ts +++ b/src/SettingTab.ts @@ -1,16 +1,17 @@ import ClickClackPlugin, { Sounds } from './main'; import { PluginSettingTab, Setting, App, DropdownComponent } from 'obsidian'; -import { getScheme, getInstalledSchemes, loadScheme } from './schemeHelpers'; import { defaultScheme } from './defaultSound'; -import { checkOrDownload } from './fetchHelpers'; import { i18n } from './libs/i18n'; +import { FetchHelper } from './fetchHelper'; export class ClickClackSettingTab extends PluginSettingTab { plugin: ClickClackPlugin; + fetchHelper: FetchHelper; constructor(app: App, plugin: ClickClackPlugin) { super(app, plugin); this.plugin = plugin; + this.fetchHelper = new FetchHelper(app); } async display(): Promise { @@ -61,7 +62,9 @@ export class ClickClackSettingTab extends PluginSettingTab { .addOptions({ default: i18n.t('settings.scheme.default'), }) - .addOptions(await getInstalledSchemes()) + .addOptions( + await this.plugin.schemeHelper.getInstalledSchemes() + ) .setValue(this.plugin.settings.activeScheme.id); }); }); @@ -69,12 +72,16 @@ export class ClickClackSettingTab extends PluginSettingTab { const dropdown = new DropdownComponent(schemeSetting.controlEl); dropdown .addOptions({ default: i18n.t('settings.scheme.default') }) - .addOptions(await getInstalledSchemes()) + .addOptions(await this.plugin.schemeHelper.getInstalledSchemes()) .setValue(this.plugin.settings.activeScheme.id) .onChange(async (value) => { - const scheme = (await getScheme(value)) ?? defaultScheme; + const scheme = + (await this.plugin.schemeHelper.getScheme(value)) ?? + defaultScheme; this.plugin.settings.activeScheme = scheme; - this.plugin.sounds = await loadScheme(scheme); + this.plugin.sounds = await this.plugin.schemeHelper.loadScheme( + scheme + ); await this.plugin.saveSettings(); }); @@ -87,7 +94,7 @@ export class ClickClackSettingTab extends PluginSettingTab { .setButtonText(i18n.t('settings.download.button')) .setIcon('download') .onClick(async () => { - await checkOrDownload(); + await this.fetchHelper.checkOrDownload(); }) ); } diff --git a/src/fetchHelper.ts b/src/fetchHelper.ts new file mode 100644 index 0000000..4ae72c5 --- /dev/null +++ b/src/fetchHelper.ts @@ -0,0 +1,122 @@ +import { normalizePath, requestUrl, Notice, App } from 'obsidian'; +import { githubAsset } from 'typings/githubAsset'; +import * as zip from '@zip.js/zip.js'; + +export class FetchHelper { + app: App; + + constructor(app: App) { + this.app = app; + } + + unzip = async (zipFile: string) => { + const blob = new Blob( + [await this.app.vault.adapter.readBinary(zipFile)], + { + type: 'octet/stream', + } + ); + const entries = await new zip.ZipReader( + new zip.BlobReader(blob) + ).getEntries(); + + const basePath = normalizePath( + [ + this.app.vault.configDir, + 'plugins', + 'click-clack', + 'resources', + ].join('/') + ); + this.checkOrCreateFolder(basePath); + + const folders = entries.filter((e) => e.directory); + folders.forEach(async (e) => + this.checkOrCreateFolder([basePath, e.filename].join('/')) + ); + const files = entries.filter((e) => !e.directory); + files.forEach(async (e) => { + if ( + await this.app.vault.adapter.exists( + [basePath, e.filename].join('/') + ) + ) + return; + if (!e || !e.getData) return; + await this.app.vault.adapter.writeBinary( + [basePath, e.filename].join('/'), + await ( + await e.getData(new zip.BlobWriter('audio/wav'), {}) + ).arrayBuffer() + ); + }); + }; + + checkOrCreateFolder = async (name: string) => { + if (await this.app.vault.adapter.exists(name)) return; + await this.app.vault.adapter.mkdir(name); + }; + + checkOrDownload = async () => { + const resourcePath = normalizePath( + [ + this.app.vault.configDir, + 'plugins', + 'click-clack', + 'resources', + ].join('/') + ); + const zipPath = normalizePath( + [ + this.app.vault.configDir, + 'plugins', + 'click-clack', + 'resources.zip', + ].join('/') + ); + + this.checkOrCreateFolder(resourcePath); + if (await this.app.vault.adapter.exists(zipPath)) { + this.unzip(zipPath); + } else { + await this.download('resources.zip', zipPath); + this.unzip(zipPath); + } + }; + + download = async (name: string, localPath: string) => { + new Notice(`Click Clack: Downloading ${name}!`, 5000); + try { + await this.fetchAsset(name, localPath); + new Notice( + `Click Clack: Resource ${name} successfully downloaded! ✔️`, + 5000 + ); + } catch (error) { + new Notice(`Click Clack: Failed to fetch ${name}: ${error} ❌`); + throw Error( + `Failed to fetch resource ${name} from GitHub release.` + ); + } + }; + + fetchAsset = async (target: string, localPath: string) => { + const assetInfo = await requestUrl( + `https://api.github.com/repos/acylation/obsidian-click-clack/releases/tags/${ + this.app.plugins.getPlugin('click-clack')?.manifest.version ?? + '0.1.0' + }` + ).json; + const asset = assetInfo.assets.find( + (a: githubAsset) => a.name == target + ); + if (asset === undefined) + throw Error('Could not find the online asset!'); + + const data = await requestUrl({ + url: asset.url, + headers: { Accept: 'application/octet-stream' }, + }).arrayBuffer; + await this.app.vault.adapter.writeBinary(localPath, data); + }; +} diff --git a/src/fetchHelpers.ts b/src/fetchHelpers.ts deleted file mode 100644 index cb27422..0000000 --- a/src/fetchHelpers.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { normalizePath, requestUrl, Notice } from 'obsidian'; -import * as path from 'path'; -import { githubAsset } from 'typings/githubAsset'; -import * as zip from '@zip.js/zip.js'; - -const unzip = async (zipFile: string) => { - const blob = new Blob([await app.vault.adapter.readBinary(zipFile)], { - type: 'octet/stream', - }); - const entries = await new zip.ZipReader( - new zip.BlobReader(blob) - ).getEntries(); - - const basePath = normalizePath( - path.join(app.vault.configDir, 'plugins', 'click-clack', 'resources') - ); - checkOrCreateFolder(basePath); - - const folders = entries.filter((e) => e.directory); - folders.forEach(async (e) => - checkOrCreateFolder(path.join(basePath, e.filename)) - ); - const files = entries.filter((e) => !e.directory); - files.forEach(async (e) => { - if (await app.vault.adapter.exists(path.join(basePath, e.filename))) - return; - if (!e || !e.getData) return; - await app.vault.adapter.writeBinary( - path.join(basePath, e.filename), - await ( - await e.getData(new zip.BlobWriter('audio/wav'), {}) - ).arrayBuffer() - ); - }); -}; - -const checkOrCreateFolder = async (name: string) => { - if (await app.vault.adapter.exists(name)) return; - await app.vault.adapter.mkdir(name); -}; - -export const checkOrDownload = async () => { - const resourcePath = normalizePath( - path.join(app.vault.configDir, 'plugins', 'click-clack', 'resources') - ); - const zipPath = normalizePath( - path.join( - app.vault.configDir, - 'plugins', - 'click-clack', - 'resources.zip' - ) - ); - - checkOrCreateFolder(resourcePath); - if (await app.vault.adapter.exists(zipPath)) { - unzip(zipPath); - } else { - await download('resources.zip', zipPath); - unzip(zipPath); - } -}; - -const download = async (name: string, localPath: string) => { - new Notice(`Click Clack: Downloading ${name}!`, 5000); - try { - await fetchAsset(name, localPath); - new Notice( - `Click Clack: Resource ${name} successfully downloaded! ✔️`, - 5000 - ); - } catch (error) { - new Notice(`Click Clack: Failed to fetch ${name}: ${error} ❌`); - throw Error(`Failed to fetch resource ${name} from GitHub release.`); - } -}; - -const fetchAsset = async (target: string, localPath: string) => { - const assetInfo = await requestUrl( - `https://api.github.com/repos/acylation/obsidian-click-clack/releases/tags/${ - app.plugins.getPlugin('click-clack')?.manifest.version ?? '0.1.0' - }` - ).json; - const asset = assetInfo.assets.find((a: githubAsset) => a.name == target); - if (asset === undefined) throw Error('Could not find the online asset!'); - - const data = await requestUrl({ - url: asset.url, - headers: { Accept: 'application/octet-stream' }, - }).arrayBuffer; - await app.vault.adapter.writeBinary(localPath, data); -}; diff --git a/src/main.ts b/src/main.ts index 7d1e6d7..f6884a6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,6 @@ import { Plugin, App, PluginManifest } from 'obsidian'; import { DEFAULT_MAP, keySoundMap } from './keySoundMap'; import { ClickClackSettings, DEFAULT_SETTINGS_V1 } from './settings'; import { ClickClackSettingTab } from './SettingTab'; -import { loadScheme } from './schemeHelpers'; export interface Sounds { key: Howl; @@ -13,20 +12,25 @@ export interface Sounds { delete: Howl; } import { defaultSounds } from './defaultSound'; +import { SchemeHelper } from './schemeHelper'; export default class ClickClackPlugin extends Plugin { settings: ClickClackSettings; sounds: Sounds; + schemeHelper: SchemeHelper; constructor(app: App, manifest: PluginManifest) { super(app, manifest); this.sounds = defaultSounds; + this.schemeHelper = new SchemeHelper(app); } async onload() { await this.loadSettings(); this.addSettingTab(new ClickClackSettingTab(this.app, this)); - this.sounds = await loadScheme(this.settings.activeScheme); + this.sounds = await this.schemeHelper.loadScheme( + this.settings.activeScheme + ); this.registerDomEvent( this.app.workspace.containerEl, 'keydown', @@ -85,7 +89,9 @@ export default class ClickClackPlugin extends Plugin { } async refreshSounds() { - this.sounds = await loadScheme(this.settings.activeScheme); + this.sounds = await this.schemeHelper.loadScheme( + this.settings.activeScheme + ); } stopSounds() { diff --git a/src/schemeHelper.ts b/src/schemeHelper.ts new file mode 100644 index 0000000..29cd190 --- /dev/null +++ b/src/schemeHelper.ts @@ -0,0 +1,118 @@ +import { Scheme } from './settings'; +import { App, normalizePath } from 'obsidian'; +import { defaultSounds } from './defaultSound'; +import { Howl } from 'howler'; + +export class SchemeHelper { + app: App; + + constructor(app: App) { + this.app = app; + } + + async getBase64URL(path: string) { + const data = await new Blob( + [await this.app.vault.adapter.readBinary(path)], + { + type: 'audio/wav', + } + ).arrayBuffer(); + const b64 = btoa(String.fromCharCode(...new Uint8Array(data))); + return 'data:audio/wav;base64,' + b64; + } + + async loadScheme(scheme: Scheme) { + if (scheme.id === 'default') return defaultSounds; + + //TODO: catch error and set sound package fallback + const keyStr = await this.getBase64URL(scheme.sounds.key); + const key2Str = await this.getBase64URL(scheme.sounds.key2); + const enterStr = await this.getBase64URL(scheme.sounds.enter); + const spaceStr = await this.getBase64URL(scheme.sounds.space); + const deleteStr = await this.getBase64URL(scheme.sounds.delete); + + const sounds = { + key: new Howl({ src: keyStr, preload: true }), + key2: new Howl({ src: key2Str, preload: true }), + enter: new Howl({ src: enterStr, preload: true }), + space: new Howl({ src: spaceStr, preload: true }), + delete: new Howl({ src: deleteStr, preload: true }), + }; + + return sounds; + } + + async getScheme(name: string) { + const schemePath = normalizePath( + [ + this.app.vault.configDir, + 'plugins', + 'click-clack', + 'resources', + name, + ].join('/') + ); + const manifestPath = normalizePath( + [schemePath, 'manifest.json'].join('/') + ); + + if (await this.app.vault.adapter.exists(manifestPath)) { + const manifest = JSON.parse( + await this.app.vault.adapter.read(manifestPath) + ); + return { + id: manifest['id'] as string, + caption: manifest['caption'] as string, + sounds: { + key: [schemePath, manifest['key'] as string].join('/'), + key2: [schemePath, manifest['key2'] as string].join('/'), + space: [schemePath, manifest['space'] as string].join('/'), + enter: [schemePath, manifest['enter'] as string].join('/'), + delete: [schemePath, manifest['delete'] as string].join( + '/' + ), + }, + } as Scheme; + } + } + + async getInstalledSchemes() { + const resourcePath = normalizePath( + [ + this.app.vault.configDir, + 'plugins', + 'click-clack', + 'resources', + ].join('/') + ); + if (!(await this.app.vault.adapter.exists(resourcePath))) return; + + const paths = await this.app.vault.adapter.list(resourcePath); + + const schemes = await Promise.all( + paths.folders.map(async (f) => { + const manifestPath = normalizePath( + [f, 'manifest.json'].join('/') + ); + if (await this.app.vault.adapter.exists(manifestPath)) { + const manifest = JSON.parse( + await this.app.vault.adapter.read(manifestPath) + ); + return [ + manifest['id'] as string, + manifest['caption'] as string, + ]; + } + return [ + f, + f + .split('-') + .map((s) => s[0].toUpperCase() + s.substring(1)) + .join(' '), + ]; + }) + ); + + return Object.fromEntries(schemes); + } +} diff --git a/src/schemeHelpers.ts b/src/schemeHelpers.ts deleted file mode 100644 index 5d7c60b..0000000 --- a/src/schemeHelpers.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Scheme } from './settings'; -import { normalizePath } from 'obsidian'; -import * as path from 'path'; -import { defaultSounds } from './defaultSound'; -import { Howl } from 'howler'; - -async function getBase64URL(path: string) { - const data = await new Blob([await app.vault.adapter.readBinary(path)], { - type: 'audio/wav', - }).arrayBuffer(); - const b64 = btoa(String.fromCharCode(...new Uint8Array(data))); - return 'data:audio/wav;base64,' + b64; -} - -export async function loadScheme(scheme: Scheme) { - if (scheme.id === 'default') return defaultSounds; - - //TODO: catch error and set sound package fallback - const keyStr = await getBase64URL(scheme.sounds.key); - const key2Str = await getBase64URL(scheme.sounds.key2); - const enterStr = await getBase64URL(scheme.sounds.enter); - const spaceStr = await getBase64URL(scheme.sounds.space); - const deleteStr = await getBase64URL(scheme.sounds.delete); - - const sounds = { - key: new Howl({ src: keyStr, preload: true }), - key2: new Howl({ src: key2Str, preload: true }), - enter: new Howl({ src: enterStr, preload: true }), - space: new Howl({ src: spaceStr, preload: true }), - delete: new Howl({ src: deleteStr, preload: true }), - }; - - return sounds; -} - -export async function getScheme(name: string) { - const schemePath = normalizePath( - path.join( - app.vault.configDir, - 'plugins', - 'click-clack', - 'resources', - name - ) - ); - const manifestPath = normalizePath(path.join(schemePath, 'manifest.json')); - - if (await app.vault.adapter.exists(manifestPath)) { - const manifest = JSON.parse(await app.vault.adapter.read(manifestPath)); - return { - id: manifest['id'] as string, - caption: manifest['caption'] as string, - sounds: { - key: path.join(schemePath, manifest['key'] as string), - key2: path.join(schemePath, manifest['key2'] as string), - space: path.join(schemePath, manifest['space'] as string), - enter: path.join(schemePath, manifest['enter'] as string), - delete: path.join(schemePath, manifest['delete'] as string), - }, - } as Scheme; - } -} - -export async function getInstalledSchemes() { - const resourcePath = normalizePath( - path.join(app.vault.configDir, 'plugins', 'click-clack', 'resources') - ); - if (!(await app.vault.adapter.exists(resourcePath))) return; - - const paths = await app.vault.adapter.list(resourcePath); - - const schemes = await Promise.all( - paths.folders.map(async (f) => { - const manifestPath = normalizePath(path.join(f, 'manifest.json')); - if (await app.vault.adapter.exists(manifestPath)) { - const manifest = JSON.parse( - await app.vault.adapter.read(manifestPath) - ); - return [ - manifest['id'] as string, - manifest['caption'] as string, - ]; - } - return [ - f, - f - .split('-') - .map((s) => s[0].toUpperCase() + s.substring(1)) - .join(' '), - ]; - }) - ); - - return Object.fromEntries(schemes); -}