Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save magma-chili/97b86f5b612369340af9b572c05c9357 to your computer and use it in GitHub Desktop.
Save magma-chili/97b86f5b612369340af9b572c05c9357 to your computer and use it in GitHub Desktop.
πŸͺ‘ Table of available Obsidian community plugins πŸ”Œ
/* ---
* #########################################################
* 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}()
*
*/
(() => {})();
@magma-chili
Copy link
Author

magma-chili commented Apr 7, 2023

Now with more efficient calls, a working mobile version, and a custom data structure.

image
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment