tj-deck

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

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/383989/703472/tj-deck.js

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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);
}