夜航船夫

Memos 的那年今日和随机笔记

Memos 最初定位是一款笔记软件。对于笔记来说,怎么复现曾经有价值的笔记,激活沉淀的知识?在方法层面,除了标签、双链,就是「那年今日」和「随机笔记」。

最早用到「那年今日」功能是在 Day One 里自带的,Obsidian 里通过 DataView 也能实现。「随机笔记」则是 Obsidian 自带的,我还在这个 idea 的基础上实现了 随机读书笔记。

Memos 同样支持这两项功能。

那年今日

Memos 有一个捷径菜单,只要在捷径中加一行语句,就可以实现那年今日功能。

created_ts.getMonth() == now.getMonth() && created_ts.getDate() == now.getDate()

效果如下:

memos-on-this-day.png
Memos 那年今日

随机笔记

随机笔记功能,则需要在「设置-系统-自定义代码」中添加 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);
  }
})();

效果如下:

memos-random-note.png
Memos 随机笔记

相关文章

国庆出行感受

国庆假期,带家人自驾去了一趟山东烟台、青岛,四大一小,主要是看海景,听演唱会。分享一下感受: 1 、高速:担心路上会堵车,9.30 请假一天,早上 8 点出发,江浙沪一路自驾到山东烟台,1060 公里。除了长江大桥堵了一段,其他路段畅通。过…

读书笔记管理体系

平时主要使用微信读书,看书过程中会进行划线和评论。完成后,习惯性导出笔记整理,不定期重看。否则,过不了多久,看书过程中产生的灵感、看到的金句在脑海里都会丢失,有时候甚至为了找一本书里写过的一句话,费老鼻子劲,有点像找房间里的东西,你知道它在…

Apple Watch Setup

现在有一只 Apple Watch SE 3 (GPS),星光色,配的米兰尼斯表带,44 寸手腕大小刚好,主要用于健康追踪。目前戴了 9 个月,分享一下 Apple Watch 的 Setup 。 #表盘表盘我选用的是模块化表盘,表盘上有更…

评论

评论加载中…