-
-
Save lincolnthalles/957ee1b48634d4c41f5054526b17b5b0 to your computer and use it in GitHub Desktop.
| /* | |
| Select the appropriate code based on the Memos version you are currently running. | |
| */ |
| /* | |
| For Memos v0.22.2-v0.24.4. | |
| */ | |
| const RANDOM_MEMO_SETTINGS = { | |
| // Kinds of memos to cache: PUBLIC = visible to everyone, PROTECTED = logged in users, PRIVATE = only the creator | |
| memoKinds: ["PUBLIC", "PROTECTED", "PRIVATE"], | |
| // Username of the memo creator to filter the memos | |
| memoCreatorUsername: "", | |
| // Button text | |
| buttonText: "Random", | |
| // Button tooltip | |
| buttonTooltip: "Show a random memo", | |
| }; | |
| async function getRandomMemoUid(settings) { | |
| const memoKinds = settings.memoKinds || ["PUBLIC", "PROTECTED", "PRIVATE"]; | |
| const filters = [ | |
| `row_status=="NORMAL"`, | |
| `visibilities==${JSON.stringify(memoKinds)}`, | |
| "random==true", | |
| ]; | |
| if (settings.memoCreatorUsername) { | |
| filters.push(`creator=="users/${settings.memoCreatorUsername}"`); | |
| } | |
| const apiEndpoint = | |
| `${window.location.origin}/api/v1/memos?pageSize=1&filter=${encodeURIComponent(filters.join("&&"))}`; | |
| return await fetch(apiEndpoint) | |
| .then((response) => response.json()) | |
| .then((json) => { | |
| if (json.memos.length === 0) { | |
| return null; | |
| } | |
| return json.memos[0].uid; | |
| }) | |
| .catch((error) => { | |
| throw new Error(`Error fetching memos: ${error}`); | |
| }); | |
| } | |
| async function insertRandomMemoButton(settings) { | |
| if (document.getElementById("header-random") !== null) { | |
| return; | |
| } | |
| let insertionTries = 0; | |
| while ( | |
| document.getElementById("header-setting") === null && | |
| insertionTries < 20 | |
| ) { | |
| await new Promise((r) => setTimeout(r, 100)); | |
| insertionTries++; | |
| } | |
| const headerSetting = document.getElementById("header-setting"); | |
| if (headerSetting === null) { | |
| return; | |
| } | |
| headerSetting.insertAdjacentHTML( | |
| "afterend", | |
| `<a id="header-random" | |
| class="px-2 py-2 rounded-2xl border flex flex-row items-center text-lg text-gray-800 dark:text-gray-400 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-700 dark:hover:bg-zinc-800 w-full px-4 border-transparent" | |
| href="/" title="${settings.buttonTooltip}"> | |
| <div aria-label="Random"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-dices w-6 h-auto opacity-70 shrink-0"><rect width="12" height="12" x="2" y="10" rx="2" ry="2"/><path d="m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6"/><path d="M6 18h.01"/><path d="M10 14h.01"/><path d="M15 6h.01"/><path d="M18 9h.01"/></svg> | |
| </div> | |
| <span class="ml-3 truncate">${settings.buttonText}</span> | |
| </a>`, | |
| ); | |
| const headerRandom = document.getElementById("header-random"); | |
| if (headerRandom === null) { | |
| return; | |
| } | |
| const randomMemoUid = (await getRandomMemoUid(settings)) || ""; | |
| headerRandom.href = `/m/${randomMemoUid}`; | |
| headerRandom.onclick = async () => { | |
| window.location.href = `/m/${randomMemoUid}`; | |
| }; | |
| headerRandom.onauxclick = async () => { | |
| window.open(`/m/${randomMemoUid}`, "_blank"); | |
| }; | |
| } | |
| (async () => { | |
| try { | |
| await insertRandomMemoButton(RANDOM_MEMO_SETTINGS); | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| })(); |
| /* | |
| For Memos v0.25.0 and above. | |
| Due to the limitations of the current Memos API, this script will fetch | |
| ALL memos from the server and cache their IDs to reduce further requests. | |
| This may create a lot of network traffic. If this is an issue, | |
| tweak the settings below to narrow down what is fetched. | |
| */ | |
| const RANDOM_MEMO_SETTINGS = { | |
| // Amount of memos to cache. 0 = all | |
| cacheAmount: 0, | |
| // Time in minutes to expire the UUIDs cache | |
| cacheTimeMinutes: 60, | |
| // Amount of memos to fetch per request. Internal default is 10, limit is 1000. | |
| // Use a smaller page size to process memos in smaller batches, lowering the server and browser load. | |
| // Note that lower values will increase the amount of network requests. | |
| pageSize: 1000, | |
| // Memo state. E.g. "NORMAL", or "ARCHIVED". | |
| state: "", | |
| // Kinds of memos to cache: PUBLIC = visible to everyone, PROTECTED = logged in users, PRIVATE = only the creator. | |
| // E.g. ["PUBLIC", "PROTECTED", "PRIVATE"] | |
| visibility: [], | |
| // Tags to filter memos by. E.g. ["tag1", "tag2"] | |
| tags: [], | |
| // User ID of the memo creator. 0=everyone | |
| creatorID: 0, | |
| // Order by display_time, create_time, update_time or name. Asc or desc. E.g. "update_time desc". | |
| // This filter may make sense if you are restricting the randomness to a limited amount of memos. | |
| orderBy: "", | |
| // Button text | |
| buttonText: "Random", | |
| // Button tooltip | |
| buttonTooltip: "Show a random memo", | |
| // SVG Icon | |
| icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-dices w-6 h-auto opacity-70 shrink-0"><rect width="12" height="12" x="2" y="10" rx="2" ry="2"/><path d="m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6"/><path d="M6 18h.01"/><path d="M10 14h.01"/><path d="M15 6h.01"/><path d="M18 9h.01"/></svg>`, | |
| // Reference element ID. May be set to "header-memos" or "header-explore" to place the button above in the list. | |
| refElementId: "header-attachments", | |
| // Generated element ID | |
| genElementId: "header-random", | |
| // Local storage key to store the memo uuids | |
| cacheKey: "randomMemoCache", | |
| // Local storage key to store the last update timestamp | |
| cacheKeyLastUpdate: "randomMemoCacheLastUpdate", | |
| }; | |
| function getRandomMemo(settings) { | |
| const cachedUuids = localStorage.getItem(settings.cacheKey); | |
| if (!cachedUuids) { | |
| console.error("[random memo button]: found no cached UUIDs"); | |
| return ""; | |
| } | |
| const cache = JSON.parse(cachedUuids); | |
| const randomIndex = Math.floor(Math.random() * cache.length); | |
| return cache[randomIndex]; | |
| } | |
| async function fetchMemos(settings, pageToken = null, collected = 0) { | |
| const query = []; | |
| if (pageToken) { | |
| query.push(`pageToken=${pageToken}`); | |
| } | |
| if (settings.pageSize) { | |
| query.push(`pageSize=${settings.pageSize}`); | |
| } | |
| if (settings.state) { | |
| query.push(`state=${settings.state}`); | |
| } | |
| if (settings.orderBy) { | |
| query.push(`orderBy=${settings.orderBy}`); | |
| } | |
| const filters = []; | |
| if (settings.visibility.length) { | |
| filters.push(`visibility in ${JSON.stringify(settings.visibility)}`); | |
| } | |
| if (settings.creatorID) { | |
| filters.push(`creator_id==${settings.creatorID}`); | |
| } | |
| if (settings.tags.length) { | |
| filters.push(`tag in ${JSON.stringify(settings.tags)}`); | |
| } | |
| if (filters.length) { | |
| query.push(`filter=${encodeURIComponent(filters.join("&&"))}`); | |
| } | |
| const apiEndpoint = `${window.location.origin}/api/v1/memos?${query.join("&")}`; | |
| const memoUuids = []; | |
| await fetch(apiEndpoint) | |
| .then((response) => response.json()) | |
| .then(async (json) => { | |
| if (!json?.memos?.length) { | |
| return null; | |
| } | |
| for (let i = 0; i < json.memos.length; i++) { | |
| if (settings.cacheAmount > 0 && i >= settings.cacheAmount) break; | |
| memoUuids.push(json.memos[i].name); | |
| } | |
| if (json.nextPageToken) { | |
| const totalCollected = collected + memoUuids.length; | |
| if (settings.cacheAmount > 0 && totalCollected >= settings.cacheAmount) return; | |
| await fetchMemos(settings, json.nextPageToken, totalCollected).then((nextPage) => { | |
| const remainingLimit = settings.cacheAmount - totalCollected; | |
| if (settings.cacheAmount > 0 && nextPage.length > remainingLimit) { | |
| nextPage = nextPage.slice(0, remainingLimit); | |
| } | |
| memoUuids.push(...nextPage); | |
| }); | |
| } | |
| }) | |
| .catch((error) => { | |
| throw new Error(`[random memo button]: error fetching memos: ${error}`); | |
| }); | |
| return memoUuids; | |
| } | |
| async function cacheMemosUuids(settings) { | |
| const lastUpdate = localStorage.getItem(settings.cacheKeyLastUpdate); | |
| const cacheTimeMillis = (settings.cacheTimeMinutes || 60) * 60 * 1000; | |
| const mustUpdate = lastUpdate === null || Date.now() - lastUpdate > cacheTimeMillis; | |
| if (localStorage.getItem(settings.cacheKey) !== null && !mustUpdate) { | |
| return; | |
| } | |
| return await fetchMemos(settings) | |
| .then((memoUuids) => { | |
| localStorage.setItem(settings.cacheKey, JSON.stringify(memoUuids)); | |
| localStorage.setItem(settings.cacheKeyLastUpdate, Date.now()); | |
| }) | |
| .catch((error) => { | |
| throw new Error(`[random memo button]: error fetching memos: ${error}`); | |
| }); | |
| } | |
| async function insertRandomMemoButton(settings) { | |
| if (document.getElementById(settings.genElementId) !== null) { | |
| return; | |
| } | |
| let refElement = null; | |
| for (let i = 0; i < 20; i++) { | |
| refElement = document.getElementById(settings.refElementId); | |
| if (refElement) break; | |
| await new Promise((r) => setTimeout(r, 50)); | |
| } | |
| if (!refElement) { | |
| return; | |
| } | |
| const genElement = refElement.cloneNode(true); | |
| genElement.id = settings.genElementId; | |
| genElement.title = settings.buttonTooltip; | |
| genElement.removeAttribute("href"); | |
| genElement.addEventListener("mouseenter", () => { | |
| genElement.style.cursor = "pointer"; | |
| }); | |
| genElement.addEventListener("mouseleave", () => { | |
| genElement.style.cursor = ""; | |
| }); | |
| genElement.removeAttribute("data-discover"); | |
| const div = genElement.querySelector("div"); | |
| if (div) { | |
| div.removeAttribute("data-state"); | |
| div.removeAttribute("data-slot"); | |
| } | |
| const svg = genElement.querySelector("svg"); | |
| if (svg) { | |
| svg.innerHTML = settings.icon; | |
| } | |
| const span = genElement.querySelector("span"); | |
| if (span) { | |
| span.innerHTML = settings.buttonText; | |
| } | |
| if (refElement.parentNode) { | |
| refElement.parentNode.insertBefore(genElement, refElement.nextSibling); | |
| } | |
| const setHref = () => { | |
| const randomMemo = getRandomMemo(settings); | |
| if (!randomMemo) return; | |
| genElement.href = `/${randomMemo}`; | |
| }; | |
| genElement.onclick = setHref; | |
| genElement.onauxclick = setHref; | |
| } | |
| (async () => { | |
| try { | |
| await insertRandomMemoButton(RANDOM_MEMO_SETTINGS); | |
| await cacheMemosUuids(RANDOM_MEMO_SETTINGS); | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| })(); |
@lincolnthalles Thanks for this! I notice that random search has been implemented for the API in v0.21 (usememos/memos@bb10bb2); maybe this script can be updated to use that?
@lincolnthalles Thanks for this! I notice that random search has been implemented for the API in v0.21 (usememos/memos@bb10bb2); maybe this script can be updated to use that?
I've just added random_memo_v0.21+.js.
While this works and has less code, it has the drawback of doing one API query every time the index is reloaded.
The earlier code does a big API query once and caches all memo IDs for the specified amount of time.
Thank you for adding that, I appreciate it. The extra API queries are a trade-off, but it's nice to have another option.
I just tried it and there were no errors; however, it appeared to always show the latest memo when clicking the Random button. There's probably some mistake in the API's implementation.
Thank you for adding that, I appreciate it. The extra API queries are a trade-off, but it's nice to have another option.
I just tried it and there were no errors; however, it appeared to always show the latest memo when clicking the Random button. There's probably some mistake in the API's implementation.
The random API is only working on the latest v0.21.0. If you have a preliminary v0.21.0 built over a week ago, it won't work until you upgrade the server. Memos uses a rolling release model, so there can be different v0.21.0 in the wild.
You can test the API at the demo website.
Right, the demo website's API works—thanks for pointing that out. Now I'm not sure what's wrong with my instance (with MariaDB as storage) because I just double-checked and it was already using the latest stable Docker image updated just 2 days ago. I also tried switching to the 0.21 tag and the latest one (which actually is identical to stable, same hash) but still got the same behavior.
Anyway, for now I'll be using your original version and I'll try the v0.21+ one later when there's a new Docker image. Sorry for wasting your time on this issue!
After upgrading to version v0.22.2, clicking the button does not respond.
After upgrading to version v0.22.2, clicking the button does not respond.
API was renamed from v2 to v1. Fixed.
This user script is meant for usememos/memos.
How to add this script
Click
Settings->Systemand paste the code in theAdditional scriptfield, clickSaveand reload the page.Edit history
namefield was renamed touid).randomfilter was removed. Added a lot of filtering options. The random button is now replicated on the fly from the attachments, replacing the icon and action, preventing any styling issues.