Last active
January 23, 2026 13:55
-
-
Save mimonelu/f0e35ce16631c13179df0124baff14eb to your computer and use it in GitHub Desktop.
Nicosky - Blueskyのポストをニコニコ風に表示するブックマークレット
This file contains hidden or 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
| (() => { | |
| const JETSTREAM_URL = "wss://jetstream1.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post" | |
| const NUMBER_OF_SLOTS = 15 | |
| const TARGET_LANG = "ja" | |
| const FONT_SIZE = 5 | |
| const SLOT_HEIGHT = 6.25 | |
| const TRANSITION_DURATIONS = [100, 2500] | |
| const CONTAINER_CLASS_NAME = "nicosky" | |
| const slots = Array(NUMBER_OF_SLOTS).fill(false) | |
| let numberOfMessages = 0 | |
| let numberOfLangPosts = 0 | |
| let numberOfDisplayPosts = 0 | |
| let container = document.querySelector(`.${CONTAINER_CLASS_NAME}`) | |
| if (container) { | |
| if (container.__nicoskySocket) { | |
| try { | |
| container.__nicoskySocket.close() | |
| } catch {} | |
| } | |
| container.parentNode.removeChild(container) | |
| return | |
| } else { | |
| container = document.createElement("div") | |
| container.style.cssText = ` | |
| all: initial; | |
| contain: layout paint style; | |
| overflow: hidden; | |
| pointer-events: none; | |
| position: fixed; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100vh; | |
| z-index: 65535; | |
| ` | |
| container.classList.add(CONTAINER_CLASS_NAME) | |
| document.body.appendChild(container) | |
| } | |
| const infoContainer = document.createElement("div") | |
| infoContainer.style.cssText = ` | |
| ${makeTextStyles()} | |
| font-size: ${FONT_SIZE / 2}vh; | |
| color: #c0c0c0; | |
| position: absolute; | |
| right: 0.5em; | |
| bottom: 0.5em; | |
| ` | |
| container.appendChild(infoContainer) | |
| const socket = new WebSocket(JETSTREAM_URL) | |
| container.__nicoskySocket = socket | |
| socket.addEventListener("message", event => { | |
| numberOfMessages ++ | |
| updateInfo() | |
| if (event.data == null) { | |
| return | |
| } | |
| const record = JSON.parse(event.data)?.commit?.record | |
| if ( | |
| !record || | |
| !record.langs?.length || | |
| !record.text | |
| ) { | |
| return | |
| } | |
| const hasTargetLang = record.langs.includes(TARGET_LANG) | |
| if (!hasTargetLang) { | |
| return | |
| } | |
| numberOfLangPosts ++ | |
| const hasFacets = record.facets != null | |
| if (hasFacets) { | |
| return | |
| } | |
| const hasEmbed = record.embed != null | |
| if (hasEmbed) { | |
| return | |
| } | |
| const slotIndex = slots.findIndex(slot => slot === false) | |
| if (slotIndex === - 1) { | |
| return | |
| } | |
| updateInfo() | |
| slots[slotIndex] = true | |
| const textContainer = document.createElement("div") | |
| textContainer.addEventListener("transitionend", () => { | |
| if (container && textContainer) { | |
| container.removeChild(textContainer) | |
| slots[slotIndex] = false | |
| } | |
| }) | |
| const textNode = document.createTextNode(record.text) | |
| textContainer.appendChild(textNode) | |
| container.appendChild(textContainer) | |
| const isReply = record.reply != null | |
| const duration = record.text.length * TRANSITION_DURATIONS[0] + TRANSITION_DURATIONS[1] | |
| textContainer.style.cssText = ` | |
| ${makeTextStyles()} | |
| color: ${isReply ? "#c0e0ff" : "#ffffff" }; | |
| font-size: ${FONT_SIZE}vh; | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| transform: translate3d(100vw, 0, 0); | |
| transition: transform ${duration}ms linear; | |
| ` | |
| requestAnimationFrame(() => { | |
| const width = textContainer.clientWidth | |
| const y = slotIndex * SLOT_HEIGHT | |
| textContainer.style.cssText += ` | |
| top: ${y}vh; | |
| transform: translate3d(-${width}px, 0, 0); | |
| ` | |
| }) | |
| if (++ numberOfDisplayPosts >= 1000) { | |
| socket.close() | |
| } | |
| }) | |
| let lastInfo = 0 | |
| function updateInfo () { | |
| const now = performance.now() | |
| if (now - lastInfo < 125) { | |
| return | |
| } | |
| lastInfo = now | |
| infoContainer.innerText = `${numberOfDisplayPosts} shown (${numberOfLangPosts} "${TARGET_LANG}" / ${numberOfMessages} total)` | |
| } | |
| function makeTextStyles () { | |
| return ` | |
| font-family: monospace; | |
| -webkit-font-smoothing: none; | |
| font-weight: bold; | |
| text-shadow: -1px -1px 0 #000000, 1px -1px 0 #000000, -1px 1px 0 #000000, 1px 1px 0 #000000; | |
| user-select: none; | |
| white-space: nowrap; | |
| ` | |
| } | |
| })() |
Author
Author
説明
Blueskyのポストを某動画サイト風に表示するブックマークレットです。
- 「ブックマークレット登録用圧縮版」をブックマークとして登録し、任意のWebページで実行します。再実行で終了
- Webページによってはセキュリティの都合上、実行できない場合があります
- サーバ負荷軽減のため、1000件表示で自動終了します
- 表示するポストの条件
- ポスト言語に
jaを含むポスト facetsのないポストembedのないポスト- かつ、表示する場所があれば表示します
- ポスト言語に
- 薄い青色のテキストはリプライです。リポストや引用RPは対象外
- 右下に簡易統計情報を表示します
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
ブックマークレット登録用圧縮版