Last active
April 22, 2023 20:08
-
-
Save magma-chili/97b86f5b612369340af9b572c05c9357 to your computer and use it in GitHub Desktop.
πͺ Table of available Obsidian community plugins π
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* --- | |
* ######################################################### | |
* Dataview table of available Obsidian community plugins.js | |
* ######################################################### | |
* | |
* Author: dp0z (depose#2272) | |
* Updated: 2023-04-21T02:30:18 | |
* Link: https://gist.github.com/magma-chili/97b86f5b612369340af9b572c05c9357 | |
* Description: Efficiency enhancements and buttons conform to row height. | |
* Instructions: Save as a `.js` file to use with `dv.view(...)`. | |
* - `dv.view(...)` method parameters: https://blacksmithgu.github.io/obsidian-dataview/api/code-reference/#dvviewpath-input | |
* NOTE: It is possible to place this code inside a `dataviewjs` code block and render it, but it may lead to unexpected/unwanted behavior. | |
* - See: https://discord.com/channels/686053708261228577/1014259487445622855/1098963307521654868 | |
* | |
* Changelog: Display if the plugin is only usable on desktop in the `Description` column and add a funding url link to the `Author` column. | |
* | |
* TODO: | |
* - [-] Add a 'please wait' notice using a `Promise`. | |
* - [x] Refresh table after. | |
* - [ ] Add a refresh button. | |
* - [ ] Add pageination and search. | |
* - [ ] Optimize. | |
* - [ ] How does Obsidian do it so quickly natively? | |
* - [ ] Only remove button on success (not promise fulfillment, which returns `undefined`). | |
* - [ ] Sort / search function? | |
* - [ ] Compare with the published version and show a `Notice` when the published version does not match the local version. | |
* - [ ] Remove all unnecessary `await` calls and/or make `async` where possible. | |
* - [ ] Remove excessive destructuring. | |
* - [ ] Each column heading should be a `<span>` with a unique `[id=...]` attribute for CSS styling/`querySelector(...)`ing. | |
* - [ ] Shared module for reusable code between views. | |
* - [ ] Or transpile from TypeScript source. | |
* - [ ] Simplify button parameters with destructing. | |
* - [ ] Guess author from repo if missing from manifest. | |
* | |
* BUG: | |
* - [ ] Isn't aware of a plugin that has been uninstalled. | |
* - [ ] FIXME: Button is hidden even if plugin install fails. | |
* - [ ] Some authors use markdown link format - don't try to hyperlink this. | |
* | |
* --- */ | |
// #region Utility | |
const outputError = (message, showNotice = true, timeout = 3000) => { | |
let errorMessage = `${file.name}: ${message}`; | |
console.error(errorMessage); | |
if (showNotice) { | |
new Notice(errorMessage, timeout); | |
} | |
}, | |
checkCustomDataStructure = ({ DataviewCustom = {} } = window) => { | |
/** | |
* | |
* Persistent global object for use with Dataview. | |
* | |
* This data structure is for the output of code that is expensive to run. | |
* We don't want to re-run it every time Dataview refreshes. | |
* We store this data in the global scope so it is persistent. | |
* | |
*/ | |
const { app = {} } = DataviewCustom, | |
{ plugins = {} } = app, | |
{ available = {} } = plugins; | |
plugins.available = available; | |
app.plugins = plugins; | |
DataviewCustom.app = app; | |
window.DataviewCustom = DataviewCustom; | |
}; | |
// #endregion Utility | |
// #region Elements | |
const convertToEntities = (s) => s?.replace(/./gs, (s) => (/["'`\n\r]+/g.exec(s) ? `&#${s.charCodeAt(0)};` : s)), | |
spanAriaLabel = (ariaLabel, displayText = ariaLabel) => `<span aria-label="${ariaLabel}">${displayText}</span>`, | |
createButton = ({ id, clickOverride } = input) => { | |
const button = dv.el('button', 'Install', { cls: 'mod-cta', attr: { id: id, style: 'height: 100%' } }); | |
button.addEventListener('click', async (event) => { | |
event.preventDefault(); | |
clickOverride.click( | |
clickOverride.params[0], | |
clickOverride.params[1], | |
clickOverride.params[2], | |
clickOverride.params[3] | |
); | |
}); | |
return button; | |
}, | |
installPluginButtonClickEvent = (repo, version, manifest, id) => { | |
app.plugins.installPlugin(repo, version, manifest).then(() => { | |
delete DataviewCustom.app.plugins.available[manifest.id]; | |
document.getElementById(id).hide(); | |
}); | |
}, | |
renderTable = () => | |
dv.table( | |
['Name', 'Author', 'Description', 'Version', 'Install'], | |
dv | |
.array(Object.values(DataviewCustom.app.plugins.available).filter((p) => p !== true)) | |
.sort((p) => p?.manifest.name, 'asc') | |
.map((p) => { | |
const pluginInstallManifest = p.manifest, | |
pluginRepo = p.repo, | |
pluginId = pluginInstallManifest.id, | |
pluginName = pluginInstallManifest.name, | |
GitHubRepoUrl = `https://www.github.com/${pluginRepo}`, | |
GitHubRepoTagsUrl = `${GitHubRepoUrl}/releases/tag`, | |
pluginAuthorUrl = pluginInstallManifest?.authorUrl, | |
pluginFundingUrl = pluginInstallManifest.fundingUrl, | |
pluginAuthor = pluginInstallManifest.author, | |
pluginDescription = pluginInstallManifest.description, | |
pluginInstallVersion = pluginInstallManifest.version, | |
pluginDesktopOnly = pluginInstallManifest.isDesktopOnly, | |
nameCell = `${spanAriaLabel(pluginId, 'π')} [${pluginName}](${GitHubRepoUrl})`, | |
fundingUrlCell = pluginFundingUrl | |
? spanAriaLabel( | |
'Support the author if this plugin', | |
// FIXME: some fundingUrl`s are objects with multiple values. Can I show a modal like Obsidian does? | |
`[π](${pluginFundingUrl instanceof Object ? Object.values(pluginFundingUrl)[0] : pluginFundingUrl})` | |
) | |
: '', | |
authorCell = pluginAuthorUrl ? `${fundingUrlCell} [${pluginAuthor}](${pluginAuthorUrl})` : pluginAuthor, | |
desktopOnlyCell = pluginDesktopOnly ? spanAriaLabel('This plugin works on desktop only', 'π₯οΈ') : '', | |
// Some people have `<span>` elements in the description property of their manifests, I guess. | |
descriptionCell = `${desktopOnlyCell} ${spanAriaLabel( | |
convertToEntities(pluginDescription?.innerText ? pluginDescription.innerText : pluginDescription) | |
)}`, | |
versionCell = `[${pluginInstallVersion}](${GitHubRepoTagsUrl}/${pluginInstallVersion})`, | |
installButtonId = `install-button-${pluginId}`, | |
updateButtonCell = createButton({ | |
id: installButtonId, | |
clickOverride: { | |
click: installPluginButtonClickEvent, | |
params: [pluginRepo, pluginInstallVersion, pluginInstallManifest, installButtonId], | |
}, | |
}); | |
return [nameCell, authorCell, descriptionCell, versionCell, updateButtonCell]; | |
}) | |
), | |
checkForAvailablePlugins = async () => { | |
// Run expensive function and save data without running again until `hasRun` is cleared. | |
if (!DataviewCustom.app.plugins.available._requested) { | |
DataviewCustom.app.plugins.available = {}; | |
DataviewCustom.app.plugins.available._requested = true; | |
// Request the last list of installable community plugins | |
const availableCommunityPluginsJson = await requestUrl( | |
'https://raw.githubusercontent.com/obsidianmd/obsidian-releases/HEAD/community-plugins.json' | |
).json, | |
installedPluginsIds = Object.values(app.plugins.manifests).map((p) => p.id), | |
availableCommunityPlugins = availableCommunityPluginsJson | |
.filter((p) => !installedPluginsIds.includes(p.id)) | |
.sort((a, b) => a.repo.localeCompare(b.repo, 'en', { sensitivity: 'base' })), | |
getPluginOnlineManifest = async function (repo) { | |
// Request the latest plugin manifest from the Plugin's repo. | |
const requestUri = `https://raw.githubusercontent.com/${repo}/HEAD/manifest.json`; | |
let requestResult; | |
try { | |
requestResult = await requestUrl(requestUri).json; | |
} catch (error) { | |
catchError(`Could not fetch the contents of '${requestUri}': ${error}`, false); | |
} | |
const plugin = { | |
repo: repo, | |
manifest: requestResult, | |
}; | |
DataviewCustom.app.plugins.available[plugin.manifest.id] = plugin; | |
}; | |
if (availableCommunityPlugins.length > 0) { | |
// Request the latest plugin manifest from the repo. | |
if (app.isMobile) { | |
// Run synchronously on mobile or we run into `SocketError Connection Reset` on Android. | |
for (const p of availableCommunityPlugins) { | |
await getPluginOnlineManifest(p.repo); | |
} | |
} else { | |
// Run asynchronously on desktop. | |
await Promise.all( | |
availableCommunityPlugins.map(async (p) => { | |
getPluginOnlineManifest(p.repo); | |
}) | |
); | |
} | |
dv.component.render(); | |
} | |
} | |
}; | |
// #endregion Elements | |
// #region Main | |
checkCustomDataStructure(); | |
renderTable(); | |
checkForAvailablePlugins(); | |
// #endregion Main | |
/** | |
* | |
* BUG: If this file ends with a comment it breaks Dataview's eval because the anonymous function's | |
* terminator is placed at the end of the trimmed code so it ends like like: // comment}() | |
* | |
*/ | |
(() => {})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Now with more efficient calls, a working mobile version, and a custom data structure.