Last active
September 17, 2024 10:55
-
-
Save magma-chili/e58ae420f196e0407304691e5c24cf3f to your computer and use it in GitHub Desktop.
πͺ Table of Obsidian community plugins updates π
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 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}() | |
* | |
*/ | |
(() => {})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You can now open a given plugin's folder with one click: