Memos 的那年今日和随机笔记
Memos 最初定位是一款笔记软件。对于笔记来说,怎么复现曾经有价值的笔记,激活沉淀的知识?在方法层面,除了标签、双链,就是「那年今日」和「随机笔记」。
最早用到「那年今日」功能是在 Day One 里自带的,Obsidian 里通过 DataView 也能实现。「随机笔记」则是 Obsidian 自带的,我还在这个 idea 的基础上实现了 随机读书笔记。
Memos 同样支持这两项功能。
那年今日
Memos 有一个捷径菜单,只要在捷径中加一行语句,就可以实现那年今日功能。
created_ts.getMonth() == now.getMonth() && created_ts.getDate() == now.getDate()
效果如下:

随机笔记
随机笔记功能,则需要在「设置-系统-自定义代码」中添加 JS 脚本实现,不同的 Memos 版本脚本不一样,我目前用的是脚本支持 V0.25 以上。
/*
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);
}
})();
效果如下:




评论