Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save magma-chili/e58ae420f196e0407304691e5c24cf3f to your computer and use it in GitHub Desktop.
Save magma-chili/e58ae420f196e0407304691e5c24cf3f to your computer and use it in GitHub Desktop.
πŸͺ‘ Table of Obsidian community plugins updates πŸ”Œ
/* ---
* #######################################################
* Dataview table of Obsidian community plugins updates.js
* #######################################################
*
* Author: dp0z (depose#2272)
* Updated: 2023-04-21T02:30:18
* Link: https://gist.github.com/magma-chili/e58ae420f196e0407304691e5c24cf3f
* Description: A DataviewJS script to display Obsidian community plugin updates in a table with a convenient 'Update' button.
* 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: Fixed a bug caused by an unused variable and updated instructions to suggest `dv.view(...)` rather than a `dataviewjs` codeblock due
* to a report received from Krakor#0447. Thanks Krakor!
*
* TODO:
* - [x] 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?
* - [ ] Track the note where the table was rendered and only refresh Dataview views if it is still open.
* - [-] `SetTimeout(...)` before refresh to prevent duplicate table Dataview rendering bug?
* - Resolved by rendering table first.
* - [ ] Only remove button on success (not promise fulfillment, which returns `undefined`).
* - [ ] Sort / search function?
* - [x] Refresh only the parent element rather than the entirety of Dataview?
* - [ ] Compare with the published version and show a `Notice` when the published version does not match the local version.
* - [ ] Unfold heading if table is hidden?
* - [ ] Remove all unnecessary `await` calls and/or make `async` where possible.
* - [ ] Instead of calling `app.plugins.checkForUpdates()`, I could just get the community plugins manifest from the Obsidian repo
* and display an updates button when versions don't match... 🀯
* - Look at `app.plugins.checkForUpdates()` in `app.js`.
* - What about `app.plugins.checkForDepreciations()`?
* - [ ] Each column heading should be a `<span>` with a unique `[id=...]` attribute for CSS styling
* - [ ] 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:
* - [ ] 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,
{ updates = {} } = plugins;
plugins.updates = updates;
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', 'Update', { 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;
},
updatePluginButtonClickEvent = (repo, version, manifest, id) => {
app.plugins.installPlugin(repo, version, manifest).then(() => {
delete DataviewCustom.app.plugins.updates[manifest.id];
document.getElementById(id).hide();
});
},
createFolderShortcut = (input) => {
const folderIcon = dv.el('span', 'πŸ“', {
cls: 'internal-link',
attr: { 'aria-label': `Open ${input}`, style: 'text-decoration: none' },
});
folderIcon.addEventListener('click', async (event) => {
event.preventDefault();
app.openWithDefaultApp(input);
});
return folderIcon;
},
renderTable = () =>
dv.table(
[
'Name',
'Author',
'Description',
'Installed version',
spanAriaLabel("Open the plugin's directory", 'πŸ“‚'),
'New version',
spanAriaLabel('Changelog', 'πŸ“œ'),
'Install',
],
dv
.array(Object.values(app.plugins.updates))
.sort((p) => p.manifest.name, 'asc')
.map((p) => {
const pluginUpdateManifest = p.manifest,
pluginId = pluginUpdateManifest.id,
pluginName = pluginUpdateManifest.name,
pluginChangelog = convertToEntities(DataviewCustom.app.plugins.updates[pluginId]?.changelog),
pluginRepo = p.repo,
GitHubRepoUrl = `https://www.github.com/${pluginRepo}`,
GitHubRepoTagsUrl = `${GitHubRepoUrl}/releases/tag`,
pluginAuthorUrl = pluginUpdateManifest.authorUrl,
pluginFundingUrl = pluginUpdateManifest.fundingUrl,
pluginAuthor = pluginUpdateManifest.author,
pluginManifest = app.plugins.manifests?.[pluginId],
pluginDescription = pluginUpdateManifest.description,
pluginInstalledVersion = pluginManifest.version,
pluginDesktopOnly = pluginManifest.isDesktopOnly,
pluginUpdateVersion = p.version,
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)
)}`,
pluginFolderCell = createFolderShortcut(pluginManifest.dir),
installedVersionCell = `[${pluginInstalledVersion}](${GitHubRepoTagsUrl}/${pluginInstalledVersion})`,
newVersionCell = `[${pluginUpdateVersion}](${GitHubRepoTagsUrl}/${pluginUpdateVersion})`,
changelogCell = pluginChangelog ? spanAriaLabel(pluginChangelog, 'πŸ“œ') : '',
updateButtonId = `update-button-${pluginId}`,
updateButtonCell = createButton({
id: updateButtonId,
clickOverride: {
click: updatePluginButtonClickEvent,
params: [pluginRepo, pluginUpdateVersion, pluginUpdateManifest, updateButtonId],
},
});
return [
nameCell,
authorCell,
descriptionCell,
installedVersionCell,
pluginFolderCell,
newVersionCell,
changelogCell,
updateButtonCell,
];
})
),
checkForPluginUpdates = () => {
// Trigger `plugin.checkForUpdates()` only once and refresh the table only once after the notice is displayed.
if (!DataviewCustom.app.plugins.updates._requested) {
DataviewCustom.app.plugins.updates = {};
DataviewCustom.app.plugins.updates._requested = true;
// Alert the user that an update check has been triggered.
const pleaseWaitNotice = new Notice('Checking for updates, please wait', 0),
refreshDataviewHideNotice = function (force = false) {
// Refresh the table with Dataview.
dv.component.render();
// Remove the 'please wait' `Notice`.
pleaseWaitNotice.hide();
},
getChangeLogs = async function () {
if (Object.values(app.plugins.updates).length > 0) {
// Updates found.
await Promise.all(
Object.values(app.plugins.updates).map(async (p) => {
const gitHubUrl = `https://github.com/${p.repo}/releases/tag/${p.version}`,
pluginId = p.manifest.id;
let pluginReleaseTagMessage;
try {
const pluginReleaseTagPageHtml = await requestUrl(gitHubUrl).text,
parsedPluginReleaseTagPageHtml = new DOMParser().parseFromString(
pluginReleaseTagPageHtml,
'text/html'
);
pluginReleaseTagMessage = parsedPluginReleaseTagPageHtml.querySelector(
'html body div.Box-body > *:last-child:not(.border-md-bottom)'
)?.innerText;
} catch (error) {
outputError(`Could not fetch the contents of '${gitHubUrl}': ${error}`, false);
}
if (!DataviewCustom.app.plugins.updates[pluginId]) {
DataviewCustom.app.plugins.updates[pluginId] = {};
}
DataviewCustom.app.plugins.updates[pluginId].changelog = pluginReleaseTagMessage ?? '';
})
);
// Refresh the table because we are done.
refreshDataviewHideNotice();
} else {
// No updates available.
// Remove the 'please wait' `Notice`.
pleaseWaitNotice.hide();
}
};
// Ask Obsidian to check for updates, please.
app.plugins.checkForUpdates().then(async () => await getChangeLogs());
}
};
// #endregion Elements
// #region Main
checkCustomDataStructure();
renderTable();
checkForPluginUpdates();
// #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 21, 2023

You can now open a given plugin's folder with one click:

image

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