tj-deck

TweetDeckをスマホで使いやすくするスクリプト

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.gf.qytechs.cn/scripts/383989/703472/tj-deck.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

class TJScrollTask {
	constructor(tjDeck, targetL, duration) {
		this.tjDeck = tjDeck;
		this.$t = tjDeck.$wrap;
		this.x = targetL;
		this.d = duration;
		this.sl = tjDeck.wrapL;
		this.sTime = Date.now();
		this.ended = false;

		this._bindAnim = this._anim.bind(this);


		// 目標が画面外なら処理をしない
		var $clms = tjDeck.getClms();
		if (targetL < 0 || targetL > $clms[0].offsetWidth * ($clms.length-1)) {
			this.ended = true;
		} else {
			requestAnimationFrame(this._bindAnim);
		}
	}

	stop() {
		if (this.ended) return;
		this.ended = true;
		cancelAnimationFrame(this._bindAnim);
	}

	_anim() {
		if (this.ended) return;
		var t = (Date.now()-this.sTime)/this.d,
			b = this.sl,
			c = this.x - this.sl,
			d = 1;
		if (t > 1 && !this.ended) {
			this.stop();
			t = 1;
		}
		this.tjDeck.scrollWrap(this._easeOut(t, b, c, d));
		if (t < 1) requestAnimationFrame(this._bindAnim);
	}
	_easeOut(t, b, c, d) {
		t /= d;
		t = t-1;
		return c*(t*t*t + 1) + b;
	}
}

class TJDeck {
	constructor() {
		this.version = "0.0.9";
		this.$wrap = document.querySelector(".js-app-columns");
		this.wrapL = 0;
		this.scrollTask = null;
		this.options = this.getOptionObj();
		this.setOptionFromObj(this.options);

		this.$options = this.createOptionPanel();
		document.body.appendChild(this.$options);

		this.updateBlur();
		this.updateLight();
	}
	getOption(name, def) {
		var val = localStorage.getItem("tj_deck_"+name);
		return !val? def:val=="true";
	}
	getOptionObj() {
		return {
			light: this.getOption("light", true),
			light_clm: this.getOption("light_clm", false),
			blur: this.getOption("blur", false)
		}
	}
	setOption(name, value) {
		localStorage.setItem("tj_deck_"+name, value);
	}
	setOptionFromObj(obj) {
		var keys = Object.keys(obj);
		for (var i=0; i < keys.length; i++) {
			this.setOption(keys[i], obj[keys[i]]);
		}
	}
	getClms() {
		return this.$wrap.querySelectorAll("section.column");
	}
	back() {

		// TJDeck 設定画面が表示中なら消して終了
		if (this.$options.style.display != "none") {
			this.updateOption();
			this.hideOptionPanel();
			return;
		}

		// モーダルが表示中なら消して終了
		var $mdlDismiss = document.querySelector(".mdl-dismiss");
		if ($mdlDismiss) {
			$mdlDismiss.click();
			return;
		}

		// ツイートパネルが表示中なら消して終了
		if (this.isShownDrawer()) {
			this.hideDrawer();
			return;
		}

		// カラムに戻るボタンがあれば押して終了
		var $clm = this.getClosestColumn(this.wrapL);
		var $backToHome = $clm.querySelector(".js-column-back");
		if ($backToHome) {
			$backToHome.click();
			return;
		}

	}
	// 何か表示中ならtrue
	isShownItem() {
		return !!document.querySelector(".mdl-dismiss") || this.isShownDrawer();
	}
	// ドロワーが表示中ならtrue
	isShownDrawer() {
		return !!document.querySelector(".hide-detail-view-inline");
	}
	// ドロワーを非表示にする
	hideDrawer() {
		var $btn = document.querySelector(".js-hide-drawer");
		if ($btn) $btn.click();
	}
	// ドロワーを表示する
	showDrawer() {
		var $btn = document.querySelector(".js-show-drawer");
		if ($btn) $btn.click();
	}

	// 戻るボタンを管理する
	manageBack() {
		history.pushState(null, null, "");
		window.addEventListener("popstate", function (event) {
			this.back();
			history.pushState(null, null, "");
			
		}.bind(this));
	}

	observeModals() {
		var observer = new MutationObserver(function (records) {
			var record, $modal;
			for (var i=0; i < records.length; i++) {
				record = records[i];
				for (var n=0; n < record.addedNodes.length; n++) {
					$modal = record.addedNodes[i];
					this.stopAnkerFromModal($modal);
				}
			}
		}.bind(this));
		var options = {
			attributes: false,
			characterData: true,
			childList: true
		};
		
		var $targets = document.querySelectorAll(".js-modals-container, .js-modal");

		for (var i=0; i < $targets.length; i++) {
			observer.observe($targets[i], options);
		}

	}

	stopAnkerFromModal($modal) {
		var $ankers = $modal.querySelectorAll("a"),
			$a;
		var cb = function (event) {
			event.preventDefault();
			event.target.removeEventListener("click", cb);
			return false;
		} 
		for (var i=0; i < $ankers.length; i++) {
			$a = $ankers[i];
			if ($a.href && $a.href.match(/#$/)) {
				$a.addEventListener("click", cb);
			}
		}
	}

	// カラムの増減を監視する
	observeClms() {
		var observer = new MutationObserver(function (records) {
			var $targetClm;

			// レコードの数だけ繰り返す
			var record;
			for (var i=0; i < records.length; i++) {
				record = records[i];

				// 追加されたカラムがあればターゲットにする
				if (record.addedNodes[0]) {
					$targetClm = record.addedNodes[0];
				}

				// 削除されたカラムがあれば前後のカラムをターゲットにする
				// なければ最初のカラム
				if (record.removedNodes[0]) {
					if (record.nextSibling instanceof Element) {
						$targetClm = record.nextSibling;
					}
					else if (record.previousSibling instanceof Element) {
						$targetClm = record.previousSibling;
					}
					else {
						$targetClm = this.getClms()[0];
					}
				}
			}

			// ターゲットがあればスクロール処理
			if ($targetClm && $targetClm instanceof Element) {
				this.scrollWrapAnim($targetClm.offsetLeft);
			}
		}.bind(this));

		var options = {
			attributes: false,
			characterData: false,
			childList: true
		};

		observer.observe(this.$wrap, options);
	}

	// 横スクロールを管理する
	manageScroll() {
		var sPos;
		var sTime = Date.now();
		var prevPos;
		var $prevClm;
		var flag = null;// -1:開始前, 0:縦方向, 1:横方向


		// デフォルトのスクロールを止める
		document.querySelector(".js-app-columns-container").addEventListener("scroll", function (event) {
			event.target.scrollLeft = 0;
		}.bind(this));


		// タッチスタート
		document.querySelector(".js-app-columns").addEventListener("touchstart", function (event) {
			if (event.touches.length > 1 || this.isShownItem()) return;
			sPos = this._getPosObj(event);
			prevPos = sPos;
			flag = -1;
			sTime = Date.now();
			$prevClm = this.getClosestColumn(this.wrapL);
		}.bind(this));

		window.addEventListener("touchmove", function (event) {
			if (!flag) return;
			if (flag < 0) {
				var pos = this._getPosObj(event);
				if (Math.abs(pos.x - sPos.x) < Math.abs(pos.y - sPos.y)) {
					flag = 0;
					return;
				} else {
					flag = 1;
				}
			}
			if (flag == 1) {
				if (this.scrollTask) this.scrollTask.stop();
				var pos = this._getPosObj(event);
				prevPos = pos;
				if (!this.options.light_clm) {// 軽量版じゃなければ動かす
					this.scrollWrap(this.wrapL + prevPos.x - pos.x);
				}
			}
		}.bind(this));
		window.addEventListener("touchend", function (event) {
			if (flag < 1) return;
			flag = null;
			var time = Date.now(),
				pos = prevPos,
				distance = sPos.x - pos.x;
			
			var $targetClm;
			// スワイプ時
			if (Math.abs(distance) / (time-sTime) >= 0.5) {
				if (distance > 0) {
					$targetClm = $prevClm.nextElementSibling;
					this.hideMenu();
				} else {
					$targetClm = $prevClm.previousElementSibling;
					if (!$targetClm) this.showMenu();
				}
			}
			else {
				$targetClm = this.getClosestColumn(this.wrapL);
			}
			if ($targetClm && $targetClm instanceof Element) {
				this.scrollWrapAnim($targetClm.offsetLeft);
			}
		}.bind(this));
	}

	scrollWrapAnim(left) {
		if (this.scrollTask) this.scrollTask.stop();

		this.scrollTask = new TJScrollTask(this, left, this.options.light_clm?0:500);
	}

	// 指定位置までスクロール
	scrollWrap(left) {
		var $clms = this.getClms();
		// 画面外は処理しない
		if (left < 0 || left > $clms[0].offsetWidth * ($clms.length-1) || !isFinite(left)) return;
		this.$wrap.style.transform = `translateX(${-left}px)`;
		this.wrapL = left;
	}

	getClosestColumn(left) {
		var $clms = this.getClms();
		for (var i=0; i < $clms.length; i++) {
			var distance =  Math.abs(left - $clms[i].offsetLeft);
			if (distance <= $clms[i].offsetWidth/2) {
				return $clms[i];
			}
		}
		return $clms[$clms.length-1];
	}
	
	_getPosObj(event) {
		return {
			x: event.touches[0].pageX,
			y: event.touches[0].pageY
		}
	}

	hideMenu() {
		document.body.classList.add("tj_hide_menu");
	}
	showMenu() {
		document.body.classList.remove("tj_hide_menu");
	}

	showTJSetting() {
		
	}

	addTJNav() {
		var $nav = document.createElement("nav");
		$nav.classList.add("tj_nav");

		$nav.appendChild(this.createTweetBtn());
		$nav.appendChild(this.createSettingBtn());

		document.querySelector(".js-app-content").appendChild($nav);
	}

	createTweetBtn() {
		var $btn = document.createElement("button");
		$btn.classList.add("tj_tweet_btn", "Button", "Button--primary", "tweet-button");
		$btn.innerHTML = `<i class="Icon icon-compose icon-medium"></i>`;
		$btn.addEventListener("click", this.showDrawer.bind(this));
		return $btn;
	}

	createSettingBtn() {
		var $btn = document.createElement("a");
		$btn.classList.add("tj_setting_btn");
		$btn.href = "javascript:void(0)";
		$btn.innerHTML = `<i class="Icon icon-settings"></i>`;
		$btn.addEventListener("click", this.showOptionPanel.bind(this));
		return $btn;
	}

	createOptionPanel() {
		var $panel = document.createElement("div");
		$panel.classList.add("tj_options");
		$panel.style.display = "none";
		$panel.innerHTML =
`
<p class="title">TJDeck 設定</p>
<div>
	<label for="tj_ops_light">基本アニメーションをなくす:</label>
	<input type="checkbox" name="tj_ops_light" id="tj_ops_light">
</div>
<div>
	<label for="tj_ops_light_clm">カラム切り替えアニメーションをなくす:</label>
	<input type="checkbox" name="tj_ops_light_clm" id="tj_ops_light_clm">
</div>
<div>
	<label for="tj_ops_blur">カラムをぼかす(撮影用):</label>
	<input type="checkbox" name="tj_ops_blur" id="tj_ops_blur">
</div>
<div>
	<p>Script Version: ${this.version}</p>
</div>
<div>
	<a href="javascript:void(0)" class="tj_ops_close">閉じる</a>
</div>
`;
		$panel.querySelector(".tj_ops_close").addEventListener("click", function () {
			this.updateOption();
			this.hideOptionPanel();
		}.bind(this));
		return $panel;
	}

	hideOptionPanel() {
		var $panel = this.$options;
		$panel.style.display = "none";
	}
	showOptionPanel() {
		var $panel = this.$options;
		this.updateOptionPanel($panel);
		$panel.style.display = "";
	}

	updateOptionPanel() {
		var $panel = this.$options;
		["light", "light_clm", "blur"].forEach(function(key) {
			var $input = $panel.querySelector("#tj_ops_"+key);
			$input.checked = this.options[key];
		}.bind(this));
	}

	updateOption() {
		var $panel = this.$options;
		["light", "light_clm", "blur"].forEach(function(key) {
			var $input = $panel.querySelector("#tj_ops_"+key);
			this.options[key] = $input? $input.checked:false;
		}.bind(this));
		this.setOptionFromObj(this.options);

		this.updateBlur();
		this.updateLight();
	}

	updateBlur() {
		if (this.options.blur) {
			this.$wrap.classList.add("tj_blur");
		} else {
			this.$wrap.classList.remove("tj_blur");
		}
	}

	updateLight() {
		if (this.options.light) {
			document.body.classList.add("tj_light");
		} else {
			document.body.classList.remove("tj_light");
		}
	}
	
	manageStyle() {
		this.addStyle();
		var prevWidth = window.innerWidth;
		window.addEventListener("resize", function () {
			// 同じなら処理しない
			if (prevWidth == window.innerWidth) return;
			var $style = document.querySelector("#tj_deck_css");
			if ($style) $style.remove();
			this.addStyle();
			this.scrollWrap(this.wrapL * (window.innerWidth / prevWidth));
			prevWidth = window.innerWidth;
		}.bind(this));
	}

	refreshStyle() {
	}

	addStyle() {
		var $head = document.querySelector("head"),
			$style = document.createElement("style");
		$style.type = "text/css";
		$style.id = "tj_deck_css";
		$style.innerHTML =
`
html {
	/*overscroll-behavior: none; プルダウンでリロードさせない */
}

body.tj_light,
body.tj_light * {
	transition-duration: 0ms!important;
}
body.tj_light .inline-reply {
	/* 0にするとアニメーションイベントが発生せずに動作がおかしくなるので1ms */
	transition-duration: 1ms!important;
}

.js-column-options {
	display: none!important;
}
.is-options-open .js-column-options {
	display: block!important;
}

/* TJDeck オプションパネル */
.tj_options {
	position: fixed;
	width: 100%;
	height: 100%;
	top: 0;
	left: 0;
	padding: 1em;
	background: #fff;
	color: #222;
	z-index: 300;
}
.tj_options .title {
	margin-bottom: 1em;
	font-size: 1.1em;
	font-weight: bold;
	text-align: center;
}
.tj_options > div {
	margin: 1em 0;
}
.tj_options label,
.tj_options input {
	display: inline-block!important;
	margin: 0!important;
	vertical-align: middle!important;
}


/* サイドメニューの表示切替 */
.js-app-header {
	position: fixed!important;
}
.tj_hide_menu .js-app-header {
	transform: translateX(-50px);
}

/* メインの位置を左端に */
.js-app-content {
	left: 0!important;
}


/* サイドバーが出たらナビを隠す */
.hide-detail-view-inline .tj_nav {
	display: none;
}

.tj_tweet_btn {
	position: fixed!important;
	width: 60px!important;
	height: 60px!important;
	bottom: 1em!important;
	right: 1em!important;
	padding: 0;
	background-color: #1da1f2;
	color: #fff;
	border-radius: 36px;
	font-size: 16px;
	line-height: 1em;
	text-align: center;
	box-shadow: 1px 1px 5px rgba(0, 0, 0, .5);
	z-index: 200;
}
.tj_tweet_btn .icon-compose,
.tj_setting_btn .icon-settings {
	display: inline-block;
	margin-top: 0;
	font-size: 20px!important;
}
.tj_setting_btn {
	position: fixed;
	width: 50px;
	height: 50px;
	top: 0!important;
	right: 40px!important;
	background-color: transparent;
	color: #333;
	text-align: center;
	box-shadow: none;
	z-index: 200;
}
.tj_setting_btn > i.icon-settings {
	margin-top: -2px;
	line-height: 50px;
}

.application {
	z-index: auto;
}

/* カラムの余白をなくす */
.app-columns {
	padding: 0!important;
}


/* カラムを幅いっぱいに表示 */
.column {
	width: ${document.body.clientWidth}px!important;
	height: ${document.body.clientHeight}px!important;
	max-width: 600px!important;
	margin: 0!important;
}

/* カラムの設定をabsoluteに */
.js-column-options-container {
	position: absolute!important;
	width: 100%;
}

/* サイドパネルを表示したときにメインを動かなくする */
.application > .app-content {
	margin-right: 0!important;
	transform: translateX(0px)!important;
}

/* メインエリアのスクロールを禁止 */
#container {
	overflow: hidden!important;
}

/* サイドパネルを幅いっぱいに表示 */
.js-drawer {
	width: ${document.body.clientWidth}px!important;
	max-width: 600px!important;
	/*left: -${document.body.clientWidth}px!important;*/
	left: 0!important;
	transform: translateX(-${document.body.clientWidth}px);
}
.hide-detail-view-inline .js-drawer {/* 表示中 */
	width: ${document.body.clientWidth}px!important;
	max-width: 600px!important;
	/*left: 0!important;*/
	transform: translateX(0);
	z-index: 201!important;
}
.hide-detail-view-inline .js-drawer:after {
	display: none!important;
}

/* サイドパネルのタイトルを消す */
.js-docked-compose .compose-text-title {
	display: none!important;
}
/* アカウント選択アイコン位置を上にずらす */
.js-docked-compose .compose-accounts {
	width: 200px!important;
	margin-top: -50px;
}

/* ツイート入力エリアをすこし小さくする */
.js-docked-compose .compose-text-container {
	padding: 5px!important;
}
.js-docked-compose .js-compose-text {
	height: 90px!important;
}

/* ツイートボタンを大きく */
.js-docked-compose .js-send-button {
	width: 100px!important;
	text-align: center;
}

/* 各種ボタンを小さくして横並びにする */
.js-docked-compose .compose-content button.js-add-image-button,
.js-docked-compose .compose-content .js-schedule-button,
.js-docked-compose .compose-content .js-tweet-button,
.js-docked-compose .compose-content .js-dm-button {
	display: inline-block!important;
	width: auto!important;
}
.js-docked-compose .compose-content .js-tweet-button.is-hidden,
.js-docked-compose .compose-content .js-dm-button.is-hidden {
	display: none!important;
}
.js-add-image-button > .label,
.js-schedule-button > .label,
.js-tweet-button > .label,
.js-dm-button > .label {
	display: none!important;
}
.js-add-image-button,
.js-scheduler,
.js-tweet-type-button {
	display: inline-block;
	transform: translateY(-65px);
}


/* サイドパネルのフッターを消す */
.js-docked-compose > footer {
	display: none!important;
}
.js-docked-compose .compose-content {
	bottom: 0!important;
}

/* サイドパネルのヘッダーを消す */
.js-compose-header {
	position: absolute!important;
	right: 20px!important;
	border: 0!important;
}
header.js-compose-header div.compose-title {
	display: none!important;
}
.js-account-selector-grid-toggle {
	margin-right: 50px!important;
}

/* モーダルの位置調整 */
.overlay:before,
.ovl-plain:before,
.ovl:before {
	display: none!important;
}

/* リツイートモーダルの幅設定 */
#actions-modal > .mdl {
	max-width: 100%!important;
}

/* モーダルのメディア表示調整 */
.js-modal-panel .js-embeditem {/* 画面いっぱいに表示 */
	height: 100%!important;
	top: 0!important;
	bottom: 0!important;
}
.js-modal-panel .js-embeditem iframe {
	max-width: 100%!important;
	max-height: 100%!important;
}
.js-modal-panel .js-med-tweet {/* ツイートを非表示 */
	display: none!important;
}

/* 閉じるボタン */
.js-modal-panel .mdl-dismiss {
	z-index: 2;
}

/* 画像表示を調整する */
.js-modal-panel .js-embeditem {
	display: flex!important;
	flex-direction: column;
	z-index: 1;
}
/* 画像表示部分 */
.js-modal-panel .js-embeditem .l-table {
	position: relative!important;
	display: block!important;
	height: auto!important;
	flex: auto;
}

.js-modal-panel .js-embeditem .l-table div,
.js-modal-panel .js-embeditem .l-table a {
	position: static!important;
}
.js-modal-panel .js-embeditem .l-table .js-media-image-link {
	pointer-events: none;
}

/* 画像サイズ指定 */
.js-modal-panel .js-embeditem .l-table img,
.js-modal-panel .js-embeditem .l-table iframe {
	position: absolute;
	max-width: 100%!important;
	max-height: 100%!important;
	width: auto!important;
	height: auto!important;
	top: 0!important;
	bottom: 0!important;
	left: 0!important;
	right: 0!important;
	margin: auto!important;
}
.js-modal-panel .js-embeditem .l-table iframe {
	width: 100%!important;
	height: 100%!important;
}

/* 画像検索ボタンの位置調整 */
.js-modal-panel .js-embeditem .l-table .reverse-image-search {
	position: fixed!important;
	display: block!important;
	left: 10px!important;
}

/* 画像移動ボタンの表示位置を調整する */
.js-modal-panel .js-embeditem .js-media-gallery-prev,
.js-modal-panel .js-embeditem .js-media-gallery-next {
	position: relative!important;
	top: auto!important;
	width: 50%!important;
	height: 60px!important;
}
.js-modal-panel .js-embeditem .js-media-gallery-next {
	margin-top: -60px;
	align-self: flex-end;
}

/* 画像下部のリンクを非表示 */
.med-origlink,
.med-flaglink {
	display: none!important;
}


/* デバッグ用モザイク */
.tj_blur .js-stream-item-content {
	filter: blur(5px);
}
.tj_blur section.column:nth-child(1) .js-stream-item-content {
	filter: none;
}
`;
		$head.appendChild($style);
	}
}


window.tj_deck = null;
function tjDeckStart() {
	console.log("TJDeckスタート!!!");
	window.tj_deck = new TJDeck();
	window.tj_deck.manageStyle();
	window.tj_deck.manageScroll();
	window.tj_deck.manageBack();
	window.tj_deck.observeClms();
	window.tj_deck.observeModals();
	window.tj_deck.hideMenu();
	window.tj_deck.addTJNav();
	document.querySelector("textarea.js-compose-text").spellcheck = false;
}



if (document.querySelector(".js-app-columns")) {
	tjDeckStart();
} else {
	var timer = setInterval(function () {
		if (document.querySelector(".js-app-columns")) {
			tjDeckStart();
			clearInterval(timer);
		} else {
			console.log("まだロード中");
		}
	}, 500);
}