仕事気分でツイッターがしたい
Twitterは楽しい.ただ一つ難点があるとするのなら 時間の浪費を感じる .
TwitterはTwitterであるが故,時間の浪費をしている子尾を感じてしまう.
せっかく楽しいTwitterを負い目なく楽しみたい.
まずその原因はTwitterの見た目がTwitterっぽすぎるのだ.
つまりあの画面を見て「仕事してるな」と思うことはできない.
なので自分をだますために仕事で見る画面のデザインに変えてしまおう!!
ということでChrome拡張を作りました
最終的にはこんな感じ
罪悪感なくツイッターするために仕事の気分でできるツイッターを作りました pic.twitter.com/DiXHzxdbJz
— 電電猫猫@CoeFont (@nya3_neko2) February 1, 2023
お試しはChrome拡張のWebstoreに
使うときにはTwitterの方の表示デザインをブラックにしてください
GitHubはこっちね
Chrome拡張を作ろう
今回のmanifest.jsonを作ります.ざっくりchromeに読み込むためにChrome拡張の権限やらなにやらの宣言ファイルです.
manifest.json
{
"name": "FakeLookTwitterChromeExtension",
"version": "1.1.1",
"description": "Not Twitte Like Look Twitter Design Custom",
"manifest_version": 3,
"icons": {
"16": "asset/logo16.png",
"48": "asset/logo48.png",
"128": "asset/logo128.png"
},
"content_scripts": [
{
"matches": ["https://twitter.com/*"],
"js": ["js/jquery-3.6.3.min.js", "js/content.ts"],
"exclude_matches": ["https://twitter.com/messages"],
"css": ["css/content.css", "css/injectedFile.css"],
"run_at": "document_end"
}
],
"web_accessible_resources": [
{
"resources": ["html/*.html"],
"matches": ["https://twitter.com/*"]
}
]
}
今はmanifest version3が推奨なので3でいきます.詳細はGoogle公式ドキュメントを読もう.
Chrome拡張の動作するURLは"matches": ["https://twitter.com/*"]
で決めます.
DMのデザインはめんどくさかったので, DMは除外します. "exclude_matches": ["https://twitter.com/messages"],
今回はローカルのHTMLファイルをJSで読んでサイトに無理やりぶちこみます
メインとなるcontent.js
content.js
最初っからTSにしとけばよかったな
var searchForm;
var saveChannel;
var isChannelShow = true;
var workspaceTitle = "Slacker";
const channelTitleMaxLength = 19;
const defaultChannel = [
["home", "/home"],
["notification", "/notifications"],
["explore", "/explore"],
["bookmark", "/i/bookmarks"],
["profile", "/"],
];
if (localStorage.getItem("saveChannel") != null) {
saveChannel = JSON.parse(localStorage.getItem("saveChannel"));
}
if (localStorage.getItem("workspaceTitle") != null) {
workspaceTitle = localStorage.getItem("workspaceTitle");
}
function isDefaultChannelURL(checkURL) {
defaultChannel.forEach((element) => {
if (checkURL == element[1]) {
return true;
}
});
return false;
}
function addHtmlToBody(htmlPath) {
// jquery load text file
path = chrome.runtime.getURL(htmlPath);
// dataが読み込まれるまで待つ
var htmlText = $.ajax({
url: path,
async: false,
}).responseText;
// get the body element
var body = document.getElementsByTagName("body")[0];
var div = document.createElement("div");
// change background color
div.innerHTML = htmlText;
body.appendChild(div);
return div;
}
function twitterSearch(e) {
if (e.keyCode === 13) {
window.location.href = "https://twitter.com/search?q=" + searchForm.value;
}
return false;
}
function makeChannelTitle(url) {
var a = url.replace("https://twitter.com/", "");
if (a == "") {
a = "home";
} else if (a == "home") {
a = "home";
} else if (a == "notifications") {
a = "notification";
} else if (a == "explore") {
a = "explore";
} else if (a == "i/bookmarks") {
a = "bookmark";
} else if (url.includes("search")) {
a = url.replace("https://twitter.com/search?q=", "");
} else if (url.includes("hashtag")) {
a = url.replace("https://twitter.com/hashtag/", "");
} else if (url.includes("lists")) {
a = "list";
} else if (url.includes("moments")) {
a = "moment";
} else if (!a.includes("/")) {
a = "times_" + url.replace("https://twitter.com/", "");
}
if (a.includes("?")) {
a = a.split("?")[0];
} else if (a.includes("&")) {
a = a.split("&")[0];
} else if (a.includes("/status")) {
a = a.split("/status")[0];
}
return decodeUrlEncodedString(a);
}
// add channel
function addChannelUI(elementArray) {
console.log(elementArray);
var channel = document.createElement("li");
var channelLink = document.createElement("a");
var deleteButton = document.createElement("button");
channelLink.href = elementArray[1];
// set channel name
if (elementArray[0].length > channelTitleMaxLength) {
channelLink.innerText =
"# " + elementArray[0].slice(0, channelTitleMaxLength) + "...";
} else {
channelLink.innerText = "# " + elementArray[0];
}
// set event lister
document.getElementById("channelList").appendChild(channel);
if (elementArray[0] == "profile") {
console.log("profile")
channelLink.id = "profile";
channel.addEventListener("click", function () {
document
.querySelectorAll('[data-testid="AppTabBar_Profile_Link"]')[0]
.click();
});
} else {
document.getElementById("channelList").appendChild(channel);
// li 全体にリンクを追加
channel.addEventListener("click", function () {
window.location.href = elementArray[1];
});
}
channel.appendChild(channelLink);
// add delete button
deleteButton.innerText = "×";
deleteButton.className = "deleteButton";
deleteButton.addEventListener("click", function () {
// delete channel
for (let i = 0; i < saveChannel.length; i++) {
if (saveChannel[i][0] == elementArray[0]) {
// confirm
if (!confirm("delete " + elementArray[0] + "channel?")) {
return;
}
saveChannel.splice(i, 1);
localStorage.setItem("saveChannel", JSON.stringify(saveChannel));
break;
}
}
// delete channel UI
channel.remove();
});
channel.appendChild(deleteButton);
// delete button display check
defaultChannel.forEach((element) => {
if (elementArray[1] == element[1]) {
deleteButton.style.display = "none";
return;
}
});
}
function backButton() {
window.history.back();
}
function addchannel() {
const nowURL = window.location.href;
// 既に追加されているかチェック:デフォルトチャンネル
if (isDefaultChannelURL(nowURL)) {
alert("already added");
return;
}
// 既に追加されているかチェック:追加分
for (let i = 0; i < saveChannel.length; i++) {
if (
saveChannel[i][1] == nowURL ||
saveChannel[i][0] == makeChannelTitle(nowURL)
) {
alert("already added");
return;
}
}
saveChannel.push([makeChannelTitle(nowURL), nowURL]);
localStorage.setItem("saveChannel", JSON.stringify(saveChannel));
addChannelUI(saveChannel[saveChannel.length - 1]);
alert("channel added");
}
function setChannelTitle() {
// チャンネルタイトルを表示
document.getElementById("channelTitle").innerText =
"# " + makeChannelTitle(window.location.href);
}
window.onload = function () {
addHtmlToBody("html/sidebar.html").style.height =
window.innerHeight - 44 + "px";
addHtmlToBody("html/searchBar.html");
addHtmlToBody("html/channelHeader.html");
document.getElementById("backButton").addEventListener("click", backButton);
// 検索バーのイベントリスナーを追加
searchForm = document.getElementById("searchInputForm");
searchForm.addEventListener("keypress", twitterSearch);
// channel 追加
if (saveChannel == null || saveChannel == undefined) {
saveChannel = defaultChannel;
}
saveChannel.forEach((element) => {
addChannelUI(element);
});
// addchannel event listener
var addchannelButton = document.getElementById("addChannel");
addchannelButton.addEventListener("click", addchannel);
// チャンネルタイトルを表示
setChannelTitle();
// workspace title
workspaceTitleElement = document.getElementById("workspaceTitle");
workspaceTitleElement.value = workspaceTitle;
workspaceTitleElement.placeholder = workspaceTitle;
workspaceTitleElement.addEventListener("keypress", function () {
workspaceTitle = workspaceTitleElement.value;
localStorage.setItem("workspaceTitle", workspaceTitle);
workspaceTitleElement.placeholder = workspaceTitle;
});
// set toggle
document
.getElementById("channelToggle")
.addEventListener("click", switchChannels);
// get ol id funcList
var funcList = document.getElementById("funcList");
// set event listener each li
for (let i = 0; i < funcList.children.length; i++) {
funcList.children[i].addEventListener("click", function () {
window.location.href = funcList.children[i].children[0].href;
});
}
};
window.onhashchange = function () {
document.getElementById("channelTitle").innerText = makeChannelTitle(
window.location.href
);
};
// decode URL encoded string
function decodeUrlEncodedString(str) {
return decodeURIComponent(str.replace(/\+/g, " "));
}
window.addEventListener(
"click",
function () {
setChannelTitle();
},
false
);
window.addEventListener("popstate", (e) => {
setChannelTitle();
});
function switchChannels() {
// display none
isChannelShow = !isChannelShow;
if (isChannelShow) {
document.getElementById("channelList").style.display = "block";
document.getElementById("channelToggle").innerText = "▼ Channels";
} else {
document.getElementById("channelList").style.display = "none";
document.getElementById("channelToggle").innerText = "▶ Channels";
}
}
大事なのはここくらいなもん
function addHtmlToBody(htmlPath) {
path = chrome.runtime.getURL(htmlPath);
var htmlText = $.ajax({
url: path,
async: false,
}).responseText;
var body = document.getElementsByTagName("body")[0];
var div = document.createElement("div");
div.innerHTML = htmlText;
body.appendChild(div);
return div;
}
path = chrome.runtime.getURL(htmlPath);
これでchrome拡張においたHTMLを読み込める.
でもこれだけだとChrome拡張が権限なくてHTML読んでくれないっぽいので,manifest.jsonでアクセスできるようにします
"web_accessible_resources": [
{
"resources": ["html/*.html"],
"matches": ["https://twitter.com/*"]
}
あとはHTMLとcss(sass)は諸々とasset
Chrome拡張を読み込もう
拡張機能を管理のところからパッケージ化されてないの読み込みます
manifest.jsonミスるとここで文句言われて読み込まれません
完成
機能紹介
- 見た目が某チャットアプリ風
- 検索バーから検索が可能
- サイドバーから下書き・予約済み・通知・メンションなど細かくアクセスできる
- 任意のTwitterのページをaddchannelからサイドバーに追加できる
- 個人のページだとtimes_{user id}になる
- Workspace名を任意に変更できる
つくって意外と便利だったのがtimesのchannel追加機能.よく見るエゴサとかの検索とか保存しとくと便利
おわり!
だいたい満足した.js書くの数年ぶり過ぎてなんも分からんかった.あとmanifest_versionが3でちょくちょく変わってて知らないのも多かった
ひとまずのお試ししてみて.バグってたらごめん