// ==UserScript==
// @name AbemaTV, ニコニコ風コメントスクロール
// @description AbemaTVのコメントをスクリーン上に表示、またはニコニコ動画風にスクロールするスクリプト
// @namespace https://greasyfork.org/users/1242
// @include https://abema.tv/now-on-air/*
// @version 0.3.2
// @run-at document-end
// @grant GM_xmlhttpRequest
// @grant exportFunction
// @icon https://greasyfork.org/system/screenshots/screenshots/000/004/030/original/amebatv.png?1462005933
// ==/UserScript==
(function () {
var window = typeof unsafeWindow == "undefined" ? window : unsafeWindow,
document = window.document,
GM_TAG = "[AbemaTV, ニコニコ風コメントスクロール]",
DEBUG = false;
var settings = new Settings(),
loader = new CommentLoader(),
floater = new CommentFloat(),
scroller = new CommentScroll(),
poster = new CommentPost(),
schedule = new Schedule(),
zapping = new Zapping();
/**************************************
* 汎用メソッド
**************************************/
// 関数をunsafeWindowにエクスポートする
// Greasemonkey2.0から要素にイベントリスナ登録する際は必要
function exportGMFunc(fn, name) {
var fnName = name || fn.name;
if (exportFunction) {
exportFunction(fn, window, { defineAs: fnName });
} else {
window[fnName] = fn;
}
return window[fnName];
}
// ログ出力
function log(output) {
if (DEBUG) {
window.console && console.log(GM_TAG, output);
}
}
// イベントリスナ登録
function bind(events, callback, useCapture) {
events = events.split(" ");
for (var i = 0; i < events.length; i++) {
if (this.addEventListener) {
this.addEventListener(events[i], callback, useCapture);
} else if (this.attachEvent) {
this.attachEvent("on" + events[i], callback, useCapture);
}
}
}
window.Element.prototype.bind = bind;
window.Document.prototype.bind = bind;
window.Window.prototype.bind = bind;
// クラスがあるか否かを取得する
window.Element.prototype.hasClass = function (className) {
var tagClass = " " + this.className + " ";
return tagClass.indexOf(" " + className + " ") >= 0;
}
// クラスを追加する
window.Element.prototype.addClass = function (className) {
this.className = (this.className + " " + className).trim();
}
// クラスを削除する
window.Element.prototype.removeClass = function (className) {
var tagClass = " " + this.className + " ";
this.className = tagClass.replace(new RegExp(" " + className + " ", "gm"), "");
}
// HTMLエスケープ
var escapeHtml = (function () {
var escapeMap = {
'&': '&',
"'": ''',
'`': '`',
'"': '"',
'<': '<',
'>': '>'
};
var escapeReg = '[';
var reg;
for (var p in escapeMap) {
if (escapeMap.hasOwnProperty(p)) {
escapeReg += p;
}
}
escapeReg += ']';
reg = new RegExp(escapeReg, 'g');
return function (str) {
str = (str === null || str === void 0) ? '' : '' + str;
return str.replace(reg, function (match) {
return escapeMap[match];
});
};
}());
// 単数の要素を取得する
function $(selector, element) {
if (element) {
return element.querySelector(selector);
} else {
return document.querySelector(selector);
}
};
// 複数の要素を取得する
function $$(selector, element) {
if (element) {
return element.querySelectorAll(selector);
} else {
return document.querySelectorAll(selector);
}
};
// 要素を作成する
// @html html
// @container 指定した要素に追加
// @returns 作成した要素の配列
function $e(html, container) {
var holder = document.createElement("div");
holder.innerHTML = html;
var children = [];
for (var i = 0; i < holder.childNodes.length; i++) {
children.push(holder.childNodes[i]);
if (container) {
container.appendChild(holder.childNodes[i]);
}
}
return children;
}
// 文字列マッピング
function stringMap(target, maps) {
var output = target;
for (var k in maps) {
output = output.replace("{" + k + "}", maps[k]);
}
return output;
}
// CSSを追加
function addStyle(style) {
var tag = document.createElement("style");
tag.innerHTML = style;
document.getElementsByTagName("head")[0].appendChild(tag);
return tag;
}
// 乱数を生成
function getRandom(min, max) {
return Math.random() * (max - min) + min;
}
/**************************************
* スクリプトの実装
**************************************/
// localStorage に保存した設定のラッパー
function Settings() {
var self = this;
var Default = function () { }
Default.prototype = {
float: true,
scroll: true,
scrollfontsize: 28,
scrollspeed: 5000,
scrollmaxline: 8,
twitter: false,
wheelzapping: false,
keepaspect: true,
}
var data;
var handlers = {};
// 登録したリスナへ設定変更された時に通知する
this.addListener = function (key, callback) {
if (!handlers[key]) {
handlers[key] = [callback];
} else {
handlers[key].push(callback);
}
}
// 設定を保存する
this.save = function () {
if (localStorage && localStorage.setItem) {
localStorage.setItem(GM_TAG, JSON.stringify(data));
}
}
// 設定を取得する
this.get = function (key) {
try {
if (!data) {
data = new Default();
if (localStorage && localStorage.getItem) {
var d = JSON.parse(localStorage.getItem(GM_TAG));
for (var k in d) {
if (typeof d[k] != "undfined") {
data[k] = d[k];
}
}
}
}
return data[key];
} catch (ex) {
}
}
// 設定を変更する
this.set = function (key, value) {
try{
data[key] = value;
self.save();
var h = handlers[key];
if (h) {
for (var i = 0; i < h.length; i++) {
try {
h[i] && h[i](key, value);
} catch (ex) { }
}
}
} catch (ex) {
log(ex);
return false;
}
return true;
}
// コマンドで設定を変更する
// @returns 更新に成功したか否か
this.setByCommand = function (command) {
var args = command.split(" ");
var key = args[0].substr(1).toLowerCase();
switch (key) {
case "scroll":
case "float":
case "twitter":
case "wheelzapping":
if (args[1] == "on") {
return settings.set(key, true);
} else if (args[1] == "off") {
return settings.set(key, false);
} else {
return settings.set(key, !settings.get(key));
}
break;
case "scrollspeed":
var speed = parseInt(args[1]);
if (2000 <= speed && speed <= 10000) {
return settings.set(key, speed);
} else {
throw new RangeError("2000から10000までの間でなければなりません");
}
break;
case "scrollfontsize":
var size = parseInt(args[1]);
if (16 <= size && size <= 40) {
return settings.set(key, size);
} else {
throw new RangeError("16から40までの間でなければなりません");
}
break;
case "scrollmaxline":
var line = parseInt(args[1]);
if (5 <= line && line <= 20) {
return settings.set(key, line);
} else {
throw new RangeError("5から20までの間でなければなりません");
}
break;
case "font":
return settings.set(cmd0, escapeHtml(args.slice(1).join(" ").replace(/:;\n\r/g, " ")));
break;
default:
throw new Error(stringMap("{key} は無効なコマンドです", { key: key }));
break;
}
}
// 初期化する
this.init = function () {
log("Settings.init()");
log(data);
}
log("Settings()");
}
// コメント読み込みを実装
function CommentLoader() {
var self = this;
var counter;
var lastPostedTime = 0;
var lastCount = 0;
var lastLoadedTime = 0;
var observeTimer = 0;
var KMGP_REGEXP = /[kmgp]/;
this.COMMENT_API = "https://api.abema.io/v1/slots/{slotId}/comments?limit={limit}";
this.interval = 500;
this.slotId = null;
this.channel = null;
this.enable = false;
// 新着コメントの監視を開始する
this.start = function () {
self.enable = true;
observeTimer = setInterval(self._observe, self.interval);
}
// 新着コメントの監視を終了する
this.stop = function () {
self.enable = false;
clearInterval(observeTimer);
}
// コメント取得の前にOPSTIONSメソッドでリクエストを送信する
this._loadCommentOptionsRequest = function (callback) {
GM_xmlhttpRequest({
url: stringMap(self.COMMENT_API, {
slotId: self.slotId,
limit: 20
}),
method: "OPTIONS",
headers: {
"Access-Control-Request-Method": "GET",
"Access-Control-Request-Headers": "authorization,content-type",
},
onload: callback,
});
}
// コメント取得のリクエストを送信する
this._loadCommentRequest = function (callback) {
GM_xmlhttpRequest({
url: stringMap(self.COMMENT_API, {
slotId: self.slotId,
limit: 20
}),
method: "GET",
headers: {
"Authorization": "bearer " + getAbmToken(),
},
onload: callback,
})
}
// コメントを取得する
this._loadComment = function () {
lastLoadedTime = new Date().getTime();
var channel = self.channel;
var slotId = self.slotId;
// コメント読み込み
self._loadCommentRequest(function (e) {
// コメントを読み込む前にOPTIONSメソッドで一回叩く必要がある
// 404が返ってきたら、初期化読み込み後に再度コメント読み込み
if (e.status == 404) {
self._loadCommentOptionsRequest(function () {
self._loadCommentRequest(function (e) {
self._onCommentLoaded(e, channel, slotId);
});
})
} else {
self._onCommentLoaded(e, channel, slotId);
}
});
}
// コメントを取得した時に呼び出される
this._onCommentLoaded = function (e, channel, slotId) {
// log(e);
try {
if (!e || !e.responseText) return;
var data = JSON.parse(e.responseText.trim());
if (!data || !data.comments) return;
// 新しいコメントのインデックスが小さいので後ろから探索する
for (var i = data.comments.length - 1; 0 <= i; i--) {
var c = data.comments[i];
if (lastPostedTime < c.createdAtMs) {
lastPostedTime = c.createdAtMs;
log(c.message);
c.channel = channel;
c.slotId = slotId;
self.onLoaded(c);
}
}
} catch (ex) {
log(ex);
}
}
// 新着コメントを監視する
this._observe = function () {
if (!self.enable) return;
// log("_observe()");
try {
// スロットIDを取得
var slotId = getSlotId();
if (slotId) {
self.slotId = slotId;
} else if (!self.slotId) {
return;
}
// チャンネルを移動したら、コメントを取得
var channel = getChannel();
if (self.channel && self.channel == channel) {
// コメント数
if (!counter) self._getCountElement();
var count = counter && parseInt(counter.innerHTML);
var isThousandOver = counter && counter.innerHTML.match(KMGP_REGEXP);
// コメント数要素を取得できなかったら、コメントを取得しない
if (typeof count == "undefined") {
log("count is undefined");
return;
}
// CM中
if (count == NaN || count == 0) {
// 最後の読み込みから3秒以下なら、取得しない
if (new Date().getTime() - lastLoadedTime <= 3010) {
return;
}
}
// TODO: 現在は1000件以上のコメント数の取得が機能していない
// コメント数が増えてなかったら、コメントを取得しない
else if (lastCount >= count) {
// コメントが1000件以上で最後の読み込みが1.2秒以下なら取得しない
if (isThousandOver && new Date().getTime() - lastLoadedTime <= 1210) {
return;
}
} else {
lastCount = count;
}
} else {
self.channel = channel;
}
self._loadComment();
} catch (e) {
log(e);
}
}
// コメント数要素を取得する
this._getCountElement = function () {
return counter = $('span[data-reactid=".0.$main.0.1.1.0.2.1.1"]');
}
// 初期化する
this.init = function () {
log("CommentLoader.init()")
self._getCountElement();
}
this.onLoaded = null;
log("CommentLoader()");
}
// コメントフロート表示を実装
function CommentFloat() {
var self = this;
var lastSlotId = null;
this.container;
this.limits = 50;
// コメントを追加する
this.push = function (comment) {
if (!self.enable) return;
var isSlotIdChanged = lastSlotId != comment.slotId;
lastSlotId = comment.slotId;
// 除去
if (self.limits <= self.container.childNodes.length) {
self.container.removeChild(self.container.lastChild);
}
// 追加
self.container.insertBefore($e(stringMap('<span class="comment{isFirst}" channel="{channel}" uid="{uid}">{message}</span>', {
message: escapeHtml(comment.message),
channel: escapeHtml(comment.channel),
uid: escapeHtml(comment.id),
isFirst: isSlotIdChanged ? " isFirst" : ""
}))[0], self.container.firstChild);
}
// コメントを全て除去する
this.clear = function () {
while (self.container.firstChild) {
self.container.removeChild(self.container.firstChild);
}
}
// 初期化する
this.init = function (options) {
log("CommentFloat.init()")
addStyle("\
#floatComments { position: fixed; top: 44px; right: -20px; bottom: 115px; width: 340px; padding: 5px 50px 5px 5px;\
color: #fff; background: rgba(0,0,0,0.25); z-index: 8; overflow: auto; }\
#floatComments .comment { display: block; padding: 3px; overflow: hidden;\
border-bottom: 1px solid rgba(255,255,255,0.05); word-break: break-all; }\
#floatComments .comment:last-child { border: 0; }\
#floatComments .comment.isFirst { border-color: rgba(255,255,255,0.5); }\
#floatComments.disable { visibility: hidden; }\
");
self.container = $e('<div id="floatComments"></div>', document.body)[0];
if (!options.enable) {
self.container.addClass("disable");
}
self.enable = options.enable;
}
log("CommentFloat()");
}
// コメントスクロール表示を実装
function CommentScroll() {
var self = this;
var comments = new Array(); // 最大20行
for (var i = 0; i < 20; i++) {
comments.push({ count: -1 });
}
this.container;
this.maxLine = 8;
this.speed = 5000;
this.fontSize = 28;
// コメントをスクロールする
this.scroll = function (comment) {
if (!self.enable) return;
// 古いコメントは流さない
// 本当は10秒くらいがいいが取りこぼしが出るので大きめに設定
if (new Date().getTime() - comment.createdAtMs > 30000) {
log("old comment donot scroll");
log(comment);
return;
}
// コメントのハッシュタグを除去する
comment.message = comment.message.replace(/[##♯].*$/g, "");
// 要素の作成
var c = $e(stringMap('<span class="comment" uid="{uid}">{message}</span>', {
message: escapeHtml(comment.message),
uid: escapeHtml(comment.id),
}), self.container)[0];
c.style.fontSize = self.fontSize + "px";
c.style.lineHeight = self.fontSize * 1.1 + "px";
c.addClass("scroll");
var duration = self.speed; // スクロールに要する時間
var maxLine = self.maxLine; // コメントの最大行
var timeDelay = 300; // 同時に取得したコメントが縦一列にならないようにするディレイ
var marginWidth = 50; // 最後までスクロール仕切らないので余白を追加
var bodyWidth = document.body.clientWidth; // BODYの横幅
var commentWidth = c.clientWidth; // コメントの横幅
var totalWidth = bodyWidth + commentWidth; // スクロールする総距離
var pxpms = totalWidth / duration; // 1ms間にスクロールする距離
var commentScrollDuration = commentWidth / pxpms; // コメントのスクロールに要する時間
var bodyScrollDuration = bodyWidth / pxpms; // BODYのスクロールに要する時間
var marginScrollDuration = marginWidth / pxpms; // マージンのスクロールに要する時間
var now = new Date().getTime(); // 現在時間
var renderDelay = 0; // スクロール開始までのディレイ
var renderLine = 0; // スクロールする行
for (var i = 0; i < maxLine; i++) {
// 初回
if (comments[i].count < 0) {
comments[i].count = 0;
renderLine = i;
renderDelay = 0;
break;
}
// 後のコメントの方が長い(速い)
else if (comments[i].commentWidth < commentWidth) {
// 場面左端でコメントが被らない
if (comments[i].scrollEndTime < now + bodyScrollDuration - marginScrollDuration) {
renderLine = i;
renderDelay = i * timeDelay;
break;
}
}
// 後ろのコメントの方が短い(遅い)、画面右端でコメントが被らない
else if (comments[i].commentFullViewTime < now - marginScrollDuration) {
renderLine = i;
renderDelay = i * timeDelay;
break;
}
// すべての行でコメントが被る
else if (maxLine - 1 == i) {
renderLine = getRandom(0, maxLine - 1);
renderDelay = 0;
break;
}
}
comments[renderLine].count++;
comments[renderLine].commentWidth = commentWidth;
comments[renderLine].scrollEndTime = now + renderDelay + duration;
comments[renderLine].commentFullViewTime = now + renderDelay + commentScrollDuration;
setTimeout(function () {
c.style.transition = stringMap("left {0}ms linear", { 0: duration + marginScrollDuration });
c.style.top = (renderLine * self.fontSize * 1.1 + 45) + "px";
c.style.left = "-" + (commentWidth + marginWidth) + "px";
setTimeout(function () {
comments[renderLine].count--;
self.container.removeChild(c);
}, duration);
}, renderDelay);
}
// 初期化する
this.init = function (options) {
log("CommentScroll.init()")
addStyle("\
#scrollComments { position: absolute; top: 0; right: 0; left: 0; bottom: 0; overflow: hidden;\
font-size: 28px; font-weight: bold; color: #fff; }\
#scrollComments .comment { position: fixed; opacity: 0; left: 0; top: 0; z-index: 8;\
text-shadow: #000 1px 1px 1px; white-space: nowrap; }\
#scrollComments .comment.scroll { opacity: 1; left: 100%; }\
#scrollComments.disable { visibility: hidden; }\
");
self.container = $e('<div id="scrollComments"></div>', document.body)[0];
if (!options.enable) {
self.container.addClass("disable");
}
self.enable = options.enable;
self.speed = options.speed;
self.fontSize = options.fontSize;
self.maxLine = options.maxLine;
}
log("CommentScroll()");
}
// トーストを実装する
function Toast() {
var self = this;
var toast;
var showTimer;
// トーストを開く(クラスのみ付加、実装はCSS)
this.show = function (message, timeout) {
toast.innerHTML = escapeHtml(message);
toast.className = "toast shown";
clearTimeout(showTimer);
showTimer = setTimeout(function () {
self.close();
}, typeof timeout == undefined ? 5000 : timeout);
}
// トーストを閉じる(クラスのみ付加、実装はCSS)
this.close = function () {
clearTimeout(showTimer);
toast.className = "toast";
}
// 初期化する
this.init = function (container) {
toast = $e('<span class="toast"></span>', container)[0];
}
}
// コメント投稿を実装
function CommentPost() {
var self = this;
var toast = new Toast();
var form, commentField;
var deactiveTimer = 0;
this.COMMENT_LIMIT = 50;
this.POST_API = "https://api.abema.io/v1/slots/{slotId}/comments";
this.slotId;
this.isShare;
// コメント投稿する
this.post = function (message) {
log(message);
message = message.trim();
if (message.indexOf("/") == 0) {
try{
if (settings.setByCommand(message)) {
toast.show("設定しました", 3000);
} else {
toast.show("設定に失敗しました", 5000);
}
} catch (ex) {
toast.show(ex.message, 5000);
}
} else if (message.length > self.COMMENT_LIMIT) {
toast.show(stringMap("コメントが文字数制限{limit}文字を超えています", {
limit: self.COMMENT_LIMIT
}), 5000);
} else {
self.slotId = getSlotId() || lastSlotId;
self._postComment(message, function (e) {
log(e);
});
}
}
// コメントを投稿する
this._postComment = function (message, callback) {
var shareData = null;
if (self.isShare) {
shareData = {
twitter: getTwitterObject(),
elapsed: new Date().getTime() % 10000,
};
if (!shareData.twitter) {
shareData = null;
}
log(shareData);
}
GM_xmlhttpRequest({
url: stringMap(self.POST_API, {
slotId: self.slotId,
}),
data: JSON.stringify({
message: message,
share: shareData,
}),
method: "POST",
headers: {
"Authorization": "bearer " + getAbmToken(),
"Content-Type": "application/json",
},
onload: callback,
onerror: callback,
})
}
// フォームがSubmitされたときに呼び出される
this.onSubmit = function (e) {
log("onSubmit()");
self.post(e.target.message.value);
e.target.message.value = "";
return false;
}
// アクティブ化する
this.onActive = function () {
form.className = "active";
clearTimeout(deactiveTimer);
deactiveTimer = setTimeout(self.onDeactive, 5000);
}
// 非アクティブ化する
this.onDeactive = function () {
if (commentField.value.length == 0) {
form.className = "";
}
}
// フォームがKeyDownされたときに呼び出される
this.onFormKeydown = function (e) {
// 上下キーを無効
if (e.keyCode == 38 || e.keyCode == 40) {
e.stopImmediatePropagation();
e.preventDefault();
e.stopPropagation();
}
}
// コメントが入力されたとき呼び出される
this.onInput = function () {
var remaing = self.COMMENT_LIMIT - commentField.value.length;
if (remaing < 0) {
commentField.value = commentField.value.substr(0, self.COMMENT_LIMIT);
toast.show(stringMap("コメント文字数制限は50文字です", {
limit: self.COMMENT_LIMIT
}), 3000);
} else if (remaing < 20) {
toast.show(stringMap("残り{0}文字入力可能です", { 0: remaing }), 3000);
}
}
// 初期化する
this.init = function (options) {
log("CommentPost.init()")
try {
self.isShare = options.isShare;
addStyle("\
#CommentPost {\
position: absolute; bottom: 78px; right: 120px;\
z-index: 9; transition: opacity 0.1s ease-in-out; opacity: 0.01;\
}\
#CommentPost.active { opacity: 1; }\
#CommentPost input {\
min-width: 300px; max-width: 500px; height: 25px; line-height: 25px; padding: 1px 25px 1px 5px;\
border: 2px solid #fff; background: rgba(255,255,255,0.2); color: #fff; font-size: 15px;\
}\
#CommentPost input:focus, #CommentPost input:active { background: rgba(0,0,0,0.4); }\
#CommentPost .toast { position: absolute; left: 0; right: 0; bottom: 100%;\
height: 24px; border-radius: 12px; margin-bottom: 5px; padding: 2px 12px;\
background: rgba(22,22,22,0.95); color: #fff; opacity: 0; transition: opacity 0.3s ease-in-out; }\
#CommentPost .toast.shown { opacity: 1; }\
");
form = $e('<form id="CommentPost" onSubmit="return false;">\
<input type="text" name="message" autocomplete="off" placeholder="コメント&コマンド入力" />\
<span class="count"></span>\
</form>', document.body)[0];
commentField = $("input", form);
var sideForm = $('[class^="TVContainer__right-comment-area"] form');
$("textarea", sideForm).setAttribute("placeholder", "コメントを入力、設定コマンドは右下コメント欄へ");
self.onSubmit = exportGMFunc(self.onSubmit, "GM_ANLCS_CommentPost_onSubmit");
self.onFormKeydown = exportGMFunc(self.onFormKeydown, "GM_ANLCS_CommentPost_onFormKeydown");
self.onActive = exportGMFunc(self.onActive, "GM_ANLCS_CommentPost_onActive");
self.onDeactive = exportGMFunc(self.onDeactive, "GM_ANLCS_CommentPost_onDeactive");
self.onInput = exportGMFunc(self.onInput, "GM_ANLCS_CommentPost_onInput");
// コメント投稿
form.bind("submit", self.onSubmit);
// アクティブ化
document.body.bind("mousemove", self.onActive);
form.bind("keydown focus mousemove", self.onActive);
// キーダウン
form.bind("keydown", self.onFormKeydown);
// コメント入力
commentField.bind("keyup change", self.onInput);
// トースト初期化
toast.init(form);
} catch (ex) {
log(ex);
}
}
log("CommentPost()");
}
// スケジュール表示を実装
function Schedule() {
var self = this;
var closeTimer;
var DAY_TIMES = 24 * 60 * 60 * 1000;
var httpLoading = false;
var lastViewChannel = null;
this.LOGO_URL = "https://hayabusa.io/abema/channels/logo/{channel}.w100.png";
this.MEDIA_API = "https://api.abema.io/v1/media?dateFrom={from}&dateTo={to}";
this.sendOptions = false;
this.schedule = {}
// コメント取得の前にOPSTIONSメソッドでリクエストを送信する
this._options = function (from, to, callback) {
GM_xmlhttpRequest({
url: stringMap(self.MEDIA_API, {
from: from,
to: to
}),
method: "OPTIONS",
headers: {
"Access-Control-Request-Method": "GET",
"Access-Control-Request-Headers": "authorization,content-type",
},
onload: callback,
});
}
// コメント取得のリクエストを送信する
this._get = function (from, to, callback) {
GM_xmlhttpRequest({
url: stringMap(self.MEDIA_API, {
from: from,
to: to,
}),
method: "GET",
headers: {
"Authorization": "bearer " + getAbmToken(),
},
onload: callback,
});
}
// Dateオブジェクトから年月日の文字列を返す
this.getDateKey = function (dateObj) {
try {
var dateKey = "";
dateKey += dateObj.getFullYear();
dateKey += ("0" + (dateObj.getMonth() + 1)).slice(-2);
dateKey += ("0" + dateObj.getDate()).slice(-2);
return dateKey;
} catch (ex) { log(ex) }
}
// 指定された日のチャンネルの番組一覧を生成する
// @returns 生成したか否か
this._create = function (channelId, date) {
var dateKey = self.getDateKey(date);
var schedule = self.schedule[dateKey];
if (!schedule) return false;
// スロットを捜索
var slots;
for (var i = 0 ; i < schedule.channels.length; i++) {
if (schedule.channels[i].id == channelId) {
slots = schedule.channelSchedules[i].slots;
break;
}
}
if (!slots) return false;
// 日付ヘッダーを挿入する
$e(stringMap('<div class="header">{month}月{date}日</div>', {
month: date.getMonth() + 1,
date: date.getDate(),
}), self.container);
// 番組一覧を生成する
for (var j = 0; j < slots.length; j++) {
var now = new Date();
var startAt = new Date(slots[j].startAt * 1000);
var endAt = new Date(slots[j].endAt * 1000);
$e(stringMap('<div class="item {onair}"><span class="date">{startAt}-{endAt}</span><span class="title">{title}</span></div>', {
title: slots[j].title,
startAt: stringMap("{HH}:{MM}", {
HH: ("0" + startAt.getHours()).slice(-2),
MM: ("0" + startAt.getMinutes()).slice(-2)
}),
endAt: stringMap("{HH}:{MM}", {
HH: ("0" + endAt.getHours()).slice(-2),
MM: ("0" + endAt.getMinutes()).slice(-2)
}),
onair: startAt <= now && now < endAt ? "onair" : ""
}), self.container);
}
return true;
}
// チャンネルのプログラムリストを開く
this.show = function (channelId, date, isAppend) {
log("Schedule.show(): " + channelId + ", " + date);
// スクロール位置を保存
var scrollTop = self.container.scrollTop;
if (isAppend) {
// 次の日を読み込むボタンを除去する
var nextElement = $(".next", self.container);
if (nextElement) {
self.container.removeChild(nextElement);
}
} else {
// 要素の中身を一掃する
self.container.innerHTML = "";
}
var hasSlots = false;
// 取得済みの番組一覧を生成する
while (self._create(channelId, date)) {
// 次の日
date = new Date(date.getTime() + DAY_TIMES);
hasSlots = true;
}
if (!hasSlots) return;
// 次の日の番組があれば、読み込むボタンを生成する
var dateKey = self.getDateKey(date);
// 有効な番組日時リスト
var availableDates;
for (var key in self.schedule) {
availableDates = self.schedule[key].availableDates;
if (availableDates) break;
}
for (var i = 0 ; i < availableDates.length; i++) {
if (availableDates[i] == dateKey) {
// 番組表があればボタンを生成する
var next = $e(stringMap('<div class="next" next="{next}" channel="{channel}">次の日を表示</div>', {
next: date.getTime(),
channel: channelId,
}), self.container)[0];
next.bind("click", self.onNext);
break;
}
}
// スクロール位置を復元
self.container.scrollTop = scrollTop;
// チャンネルロゴの表示
self.container.style.backgroundImage = stringMap("url('" + self.LOGO_URL + "')", {
channel: channelId
});
self.container.className = "shown";
// サイドバーの開閉を監視
clearInterval(closeTimer);
closeTimer = setInterval(function () {
var list = $('[class*="right-v-channel-list"]')
if (list.parentNode.className.indexOf("shown") < 0) {
self.onClose();
}
}, 250);
}
// 次の日を表示する
this.onNext = function (e) {
e.stopImmediatePropagation();
e.preventDefault();
e.stopPropagation();
if (httpLoading) return;
var next = $(".next", self.container);
next.className = "next loading";
var channelId = next.getAttribute("channel");
var nextTimes = parseInt(next.getAttribute("next"));
var nextDate = new Date(nextTimes);
var nextDateKey = self.getDateKey(nextDate);
var schedule = self.schedule[nextDateKey];
if (schedule) {
self.show(channelId, nextDate, true);
} else {
httpLoading = true;
self._get(nextDateKey, nextDateKey, function (e) {
log(e)
httpLoading = false;
if (e.status == 200) {
try {
self.schedule[nextDateKey] = JSON.parse(e.responseText);
self.show(channelId, nextDate, true);
} catch (ex) {
log(ex);
}
}
});
}
}
// 番組
this.onHoverProgram = function (e) {
var logo = $("img[class*='logo']", e.target);
if (logo) {
var channelId = logo.getAttribute("alt");
var today = new Date();
var todayKey = self.getDateKey(today);
if (self.schedule[todayKey]) {
self.show(channelId, today);
}
lastViewChannel = channelId;
}
}
// 番組表を閉じる
this.onClose = function () {
clearInterval(closeTimer);
self.container.className = "";
}
// 放送中番組一覧ボタンをクリックされたとき
// 今日の番組表を取得する
this.onClick = function () {
var todayKey = self.getDateKey(new Date());
if (!self.schedule[todayKey]) {
httpLoading = true;
self._options(todayKey, todayKey, function (e) {
// log(e)
self._get(todayKey, todayKey, function (e) {
// log(e)
if (e.status == 200) {
try {
self.schedule[todayKey] = JSON.parse(e.responseText);
log(self.schedule);
if (lastViewChannel) {
self.show(lastViewChannel, todayKey);
}
} catch (ex) {
log(ex);
}
}
httpLoading = false;
});
});
}
}
// 初期化する
this.init = function () {
log("Schedule.init()");
addStyle("\
#gmSchedule { display: none; position: fixed; top: 0; right: -852px; bottom: 0; width: 852px; z-index: 9; overflow-y: scroll;\
background: #101010 no-repeat 280px 20px; opacity: 0; transition: opacity 0.2s ease-in, right 1s ease-in; color: #fff; }\
#gmSchedule.shown { display: block; opacity: 1; right: 0; }\
#gmSchedule.shown:after { display: block; content: ''; position: absolute; top: 0; left: 400px; bottom: 0; right: 0;\
background: #000; box-shadow: 0 0 8px #000; }\
#gmSchedule .item, #gmSchedule .next, #gmSchedule .header { padding: 1px 16px; width: 400px; }\
#gmSchedule .item:first-child, #gmSchedule .header { margin-top: 15px; }\
#gmSchedule .item:last-child, #gmSchedule .next { margin-bottom: 15px; }\
#gmSchedule .item.onair { background: #606060; }\
#gmSchedule .item .date { display: inline-block; color: #aaa; font-size: 11px; width: 70px; }\
#gmSchedule .item .title { font-size: 13px; }\
#gmSchedule .next { cursor: pointer; text-align: center; }\
#gmSchedule .header { text-align: center; }");
self.onHoverProgram = exportGMFunc(self.onHoverProgram, "GM_ANLCS_Schedule_onHoverProgram");
self.onClick = exportGMFunc(self.onClick, "GM_ANLCS_Schedule_onClick");
self.onClose = exportGMFunc(self.onClose, "GM_ANLCS_Schedule_onClose");
self.onNext = exportGMFunc(self.onNext, "GM_ANLCS_Schedule_onClose");
self.container = $e('<div id="gmSchedule"></div>', document.body)[0];
self.container.bind("click", self.onClose);
var openerButton = $$('[class^="TVContainer__side"] [class*="box"] [class*="button"]')[1];
openerButton && openerButton.bind("click", self.onClick);
var programList =$('[class*="right-slide-base"]');
programList && programList.bind("mouseover", self.onHoverProgram);
}
log("Schedule()")
}
// マウスホイールでチャンネル変更を有効化/無効化
function Zapping() {
var self = this;
this.enable = false;
this.init = function (options) {
var overrap = $("[class*='style__overlap']");
if (overrap) {
overrap.bind("wheel mousewheel DOMMouseScroll", exportGMFunc(function (e) {
if (self.enable) {
e.stopImmediatePropagation();
e.preventDefault();
e.stopPropagation();
}
}, "GM_ANLCS_overrap_onMousewheel"), true);
}
self.enable = options.enable;
}
}
// 映像のアスペクト比を維持して全体を映す
function screenFix() {
var screen = $("object").parentNode,
height = window.innerHeight,
width = window.innerWidth,
resizedWidth, resizedHeight;
if (height < width * 9 / 16) {
resizedHeight = height;
resizedWidth = resizedHeight * 16 / 9;
} else {
resizedWidth = width;
resizedHeight = resizedWidth * 9 / 16;
}
screen.style.height = resizedHeight + "px";
screen.style.width = resizedWidth + "px";
screen.style.left = (width - resizedWidth) / 2 + "px";
}
/**************************************
* AbemaTV用のの汎用メソッド
**************************************/
// 最新のデータを取得する
function getDataLayer(eventName) {
var data = window.dataLayer;
if (data) {
for (var i = data.length - 1; i >= 0; i--) {
if (data[i].event == eventName) {
return data[i];
}
}
}
}
var lastSlotId;
// スロットIDを取得する
function getSlotId() {
var data = getDataLayer("metaData-slotId");
if (data) {
if (data.slotId) {
return lastSlotId = data.slotId;
}
}
}
// チャンネルIDを取得する
function getChannel() {
var data = getDataLayer("pageview");
if (data) {
return data.channelId;
}
}
// AbemaTVのOAuthトークンを取得する
function getAbmToken() {
if (localStorage){
if(localStorage.getItem){
return localStorage.getItem("abm_token");
} else {
return localStorage["abm_token"];
}
}
}
// Twitter連携のOAuthトークンを取得する
function getTwitterObject () {
var token, tokenSecret;
if (localStorage) {
if (localStorage.getItem) {
token = localStorage.getItem("abm_twitterToken");
tokenSecret = localStorage.getItem("abm_twitterTokenSecret");
} else {
token = localStorage["abm_twitterToken"];
tokenSecret = localStorage["abm_twitterTokenSecret"]
}
return {
accessToken: token,
accessTokenSecret: tokenSecret,
}
}
}
/****************************
* 初期化
****************************/
function init() {
log("init()");
settings.init();
loader.init();
poster.init({
isShare: settings.get("twitter")
});
scroller.init({
enable: settings.get("scroll"),
speed: settings.get("scrollspeed"),
fontSize: settings.get("scrollfontsize"),
maxLine: settings.get("scrollmaxline"),
});
floater.init({
enable: settings.get("float"),
});
schedule.init();
zapping.init({
enable: settings.get("wheelzapping")
});
settings.addListener("scroll", function (key, value) {
scroller.enable = value;
if (value) {
scroller.container.removeClass("disable");
} else {
scroller.container.addClass("disable");
}
});
settings.addListener("float", function (key, value) {
floater.enable = value;
if (value) {
floater.container.removeClass("disable");
} else {
floater.container.addClass("disable");
}
});
settings.addListener("scrollfontsize", function (key, value) {
scroller.fontSize = value;
});
settings.addListener("scrollspeed", function (key, value) {
scroller.speed = value;
});
settings.addListener("twitter", function (key, value) {
poster.isShare = value;
});
settings.addListener("font", function (key, value) {
scroller.container.style.fontFamily = escapeHtml(value);
floater.container.style.fontFamily = escapeHtml(value);
});
settings.addListener("scrollmaxline", function (key, value) {
scroller.maxLine = value;
});
settings.addListener("wheelzapping", function (key, value) {
zapping.enable = value;
});
loader.onLoaded = function (c) {
floater && floater.push(c);
scroller && scroller.scroll(c);
}
loader.start();
window.bind("resize", exportGMFunc(screenFix, "GM_screenFix"));
setTimeout(screenFix, 1000);
log("end init()")
}
// ページの初期化を待つ
var readyTimer = setInterval(function () {
if (document.readyState == "complete") {
var data = getDataLayer("pageview");
if (data) {
clearInterval(readyTimer);
init();
}
}
}, 500);
log("loaded");
})()