// ==UserScript== // @name FriendsKit // @namespace https://github.com/yuzulabo // @version 1.2.1 // @description friends.nico の独自機能を再現するユーザスクリプト // @author nzws // @match https://knzk.me/* // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect friendskit.nzws.me // @connect media.knzk.me // @require https://unpkg.com/blob-util/dist/blob-util.min.js // @downloadURL none // ==/UserScript== const s = localStorage.friendskit; const F = { conf: s ? JSON.parse(s) : { keyword: [] }, imgcache: {}, iconcache: {}, regExps: [], }; const api = F.conf.api_server ? F.conf.api_server : 'https://friendskit.nzws.me/api/'; const user_emoji_regexp = new RegExp(':(_|@)([A-Za-z0-9_@.]+):', 'gm'); const shorten_regexp = new RegExp('(sm|nm|im|sg|mg|bk|lv|co|ch|ar|ap|jk|nw|l\/|dic\/|user\/|mylist\/)([0-9]+)', 'gm'); F.conf.keyword.forEach(word => { F.regExps.push({ word: word, regexp: new RegExp(word, 'gim') }); }); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach(node => runner(node)); }); }); function watcher() { const p = location.pathname; if (F.path !== p) { runner(document.querySelector('.column:last-child')); if (p === '/web/getting-started' && !document.querySelector('.friendskit-cp-btn')) { const settingLi = document.createElement('li'); settingLi.innerHTML = ` · FriendsKit CP`; document.querySelector('.getting-started__footer ul').appendChild(settingLi); settingLi.addEventListener('click', openCP); } } F.path = p; } function runner(node) { if (!node.tagName) return; const statusAll = node.querySelectorAll('.status__content'); if (!statusAll[0]) return; for (let status of statusAll) { const ue_found = status.innerHTML.match(user_emoji_regexp); if (ue_found) { const display_name_account = status.parentNode.querySelector('.display-name__account'); const status_display_name = status.parentNode.querySelector('.status__display-name'); const origin_acct = display_name_account ? display_name_account.textContent.slice(1) : status_display_name.title; const domain = '@' + (origin_acct.indexOf('@') !== -1 ? origin_acct.split('@')[1] : location.hostname); replaceTool(status, ue_found, domain); } else { replaceTool(status); } } } function replaceTool(status, ue_found, domain) { if (status.hasChildNodes()) { for (let node of status.childNodes) { replaceTool(node, ue_found, domain); } } else { if (status.nodeName === '#text') { const html = document.createElement('span'); html.innerHTML = status.data; if (ue_found) { ue_found.forEach(async data => { let acct = data.slice(2).slice(0, -1); if (acct.indexOf('@') === -1) acct += domain; const image = await getIconUrl(acct); html.innerHTML = html.innerHTML.replace(new RegExp(data, 'gm'), ``); }); } F.regExps.forEach(regexp => { html.innerHTML = html.innerHTML.replace(regexp.regexp, `${regexp.word}`); }); html.innerHTML = html.innerHTML.replace(shorten_regexp, `$1$2`); if (html.innerHTML !== status.data || ue_found) status.parentNode.replaceChild(html, status); } } } function openCP() { const div = document.createElement('div'); div.className = 'friendskit-cp'; div.innerHTML = `
FriendsKit CP
キーワードハイライト設定
カンマ(,)区切りで指定
お気に入りアイコン設定

* アイコン設定で複数の設定がされている時、この設定で一番上の物が優先されます。

文字にする (1~2文字程度):
画像にする (画像URLを指定):
お気に入りアイコン拡大
細かなやつ
FriendsKit v1.2.0

GitHub: yuzulabo/FriendsKit
Greasy Fork: 381132-friendskit

`; document.body.appendChild(div); document.querySelector('.app-holder').classList.add('friendskit-disable'); const btns = document.querySelectorAll(".fcp-clickable"); for (let btn of btns) { btn.addEventListener('click', (e) => CPOpr(e)); } } function CPOpr(e) { const mode = e.target.dataset.fcp; if (!mode) return; if (mode === 'save') { const newConf = { keyword: document.getElementById('friendskit-keyword').value.split(','), fav_icon_default_force: document.getElementById('fav_icon_default_force').checked, fav_icon_char: document.getElementById('fav_icon_char').value, fav_icon: document.getElementById('fav_icon').value, fav_icon_gray: document.getElementById('fav_icon_gray').value, no_fav_icon_big: document.getElementById('no_fav_icon_big').checked, }; Object.assign(F.conf, newConf); save(); alert('保存しました。再読み込みします...'); location.reload(); } else if (mode === 'close') { if (!confirm('変更は破棄されますがよろしいですか?')) return; const element = document.querySelector('.friendskit-cp'); element.parentNode.removeChild(element); document.querySelector('.app-holder').classList.remove('friendskit-disable'); } else if (mode === 'import-settings') { const code = prompt('エクスポート時に出力したコードを入力してください:'); if (!code) return; if (friendskit.importSettings(code)) { alert('インポートしました。再読み込みします...'); location.reload(); } else { alert('このコードは壊れています。インポートできません。'); } } else if (mode === 'export-settings') { friendskit.exportSettings('CP'); alert('設定のエクスポートをクリップボードにコピーしました。'); } else if (mode === 'reset-settings') { if (!confirm('設定をリセットします!よろしいですか?')) return; if (!confirm('まじで?')) return; if (!confirm('まじか...')) return; if (friendskit.resetSettings()) { location.reload(); } } } const friendskit = { keyword: { add: (word) => { const key = F.conf.keyword.indexOf(word); if (key !== -1) { console.warn('[FriendsKit]', 'このワードは追加済みです'); return; } F.conf.keyword.push(word); save(); console.log('[FriendsKit]', 'Done✨'); }, remove: (word) => { const key = F.conf.keyword.indexOf(word); if (key === -1) { console.warn('[FriendsKit]', 'このワードは存在しません'); return; } delete F.conf.keyword[key]; save(); console.log('[FriendsKit]', 'Done✨'); }, list: () => { console.log('[FriendsKit]', F.conf.keyword); }, reset: () => { F.conf.keyword = []; save(); console.log('[FriendsKit]', 'Done✨'); } }, changeSettings: (name, value) => { F.conf[name] = value ? value : null; save(); console.log('[FriendsKit]', 'Done✨'); }, exportSettings: (type) => { if (type === 'CP') { GM_setClipboard(localStorage.friendskit); } else { GM_setClipboard('friendskit.importSettings(`' + localStorage.friendskit + '`)'); console.log('[FriendsKit]', 'Done✨\nクリップボードにコピーしたコードをインポートしたいページの Console にそのまま打ち込んでください。'); } }, resetSettings: () => { delete localStorage.friendskit; return !localStorage.friendskit; }, importSettings: (data) => { try { JSON.parse(data); } catch(e) { console.warn('[FriendsKit]', 'このデータは壊れています', e); return false; } localStorage.friendskit = data; console.log('[FriendsKit]', 'Done✨'); return true; } }; exportFunction(friendskit, unsafeWindow, {defineAs: 'friendskit' }); async function getImage(url) { return new Promise(resolve => { if (F.imgcache[url]) { resolve(F.imgcache[url]); return; } blobUtil.imgSrcToDataURL(url, 'image/png', 'Anonymous').then(function (dataurl) { F.imgcache[url] = dataurl; resolve(F.imgcache[url]); }).catch(function (err) { console.warn('[FriendsKit]', '画像取得に失敗', url); }); }); } async function getIconUrl(acct) { return new Promise(resolve => { if (F.iconcache[acct]) { resolve(F.iconcache[acct]); return; } GM_xmlhttpRequest({ method: 'POST', responseType: 'json', url: api + 'get_icon.php?acct=' + acct, onerror: () => { console.warn('[FriendsKit]', 'json取得に失敗', acct); return; }, onload: (response) => { if (response.status !== 200) { console.warn('[FriendsKit]', `json取得に失敗 ${response.status}`, acct); return; } if (response.response.error) { console.warn('[FriendsKit]', response.response.error, acct); return; } F.iconcache[acct] = response.response.url; resolve(F.iconcache[acct]); } }); }); } function save() { const data = JSON.stringify(F.conf); localStorage.friendskit = data; } function at_pizza() { const textarea = document.querySelector('.autosuggest-textarea__textarea'); if (!textarea) return; if (textarea.value.match(/[@|@]ピザ/)) { window.open('https://www.google.com/search?q=近くのピザ屋さん'); } if (textarea.value.match(/[@|@]ハローワーク/)) { window.open('https://www.hellowork.go.jp/'); } } const mainElem = document.getElementById('mastodon'); if (!mainElem) return; observer.observe(mainElem, { childList: true, subtree: true }); let css = ` .friendskit-cp { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #e6ebf0; color: #000; padding: 20px; border-radius: 5px; min-width: 55%; max-width: 95%; min-height: 50%; max-height: 65%; overflow-y: scroll; } .friendskit-disable { filter: blur(4px); pointer-events: none; } .friendskit-cp textarea, .friendskit-cp .input-text, .friendskit-cp label { display: block; width: 100%; margin: 10px 0; } .fcp-clickable { cursor: pointer; } .h1 { font-size: 2.4rem; } .h2 { font-size: 1.7rem; } .h3 { font-size: 1.3rem; } .h1, .h2, .h3 { margin: 1rem 0; padding-bottom: 0.3rem; font-weight: 500; line-height: 1.2; border-bottom: 1px solid #c0cdd9; } .close { float: right; color: #000; text-shadow: 0 1px 0 #fff; opacity: .5; } .button.danger { background: #df405a; } `; window.onload = async () => { setInterval(watcher, 1000); document.querySelector('.compose-form__publish-button-wrapper button').addEventListener('click', at_pizza, false); document.querySelector('.autosuggest-textarea__textarea').onkeydown = (e) => { if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) at_pizza(); }; if (!F.conf.fav_icon_default_force) { const i = F.conf.fav_icon ? await getImage(F.conf.fav_icon) : 'https://media.knzk.me/media_attachments/files/004/510/885/original/caf4ce0b3e7bd6d3.png'; const ig = F.conf.fav_icon_gray ? await getImage(F.conf.fav_icon_gray) : 'https://media.knzk.me/media_attachments/files/004/510/887/original/bfb5e4222a3423fd.png'; const char = F.conf.fav_icon_char ? F.conf.fav_icon_char : null; css += ` .fa-star { background-image: ${char || !ig ? `none` : `url('${ig}')`}; width: 16px; height: 16px; background-size: cover; background-repeat: no-repeat; background-position: center center; } .fa-star:before { content: '${char ? char : (ig ? '' : '\\f005')}'; } .active .fa-star, .notification__message .fa-star { background-image: ${char || !i ? `none` : `url('${i}')`}; } .active .fa-star:before, .notification__message .fa-star:before { content: '${char ? char : (i ? '' : '\\f005')}'; } `; if (i && !char) { css += ` .active .fa-star, .notification__message .fa-star { background-image: url('${i}'); } `; } } if (!F.conf.no_fav_icon_big) { css += ` .status__info, .status__content { margin-right: 40px; } .status button.star-icon { position: absolute; top: 20px; right: 10px; z-index: 999999; } .status .fa-star { width: 40px; height: 40px; font-size: 2em; } `; } GM_addStyle(css); };