iPhone/iPad用 コンソールエミュレータ。既存のconsole.logをすべて画面にミラーリングします。
Version vom
Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/577436/1822383/Screen%20Display%20Log.js
// ==UserScript==
// @name Screen Display Log
// @namespace http://tampermonkey.net/
// @version 2.0
// @description iPhone/iPad用 コンソールエミュレータ。既存のconsole.logをすべて画面にミラーリングします。
// @author Professional Debugger
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
class ScreenLogger {
constructor() {
this.id = 'tm-pro-console';
this.maxLogs = 100; // 最大ログ保持数(メモリ対策)
this.container = null;
this.logArea = null;
this.isMinimized = false;
this._init();
}
_init() {
if (document.getElementById(this.id)) return;
this._injectStyles();
this._createUI();
this._hookConsole();
this._hookGlobalErrors();
}
_injectStyles() {
const style = document.createElement('style');
style.textContent = `
#${this.id} {
position: fixed; bottom: 0; right: 0; width: 100%; max-height: 40%;
color: #ddd; font-size: 10px;
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
z-index: 2147483647; pointer-events: none; border-top: 1px solid #444;
display: flex; flex-direction: column; transition: height 0.2s; overflow-y: hidden;
}
#${this.id}.minimized { height: 30px !important; overflow: hidden; }
#${this.id} * { pointer-events: auto; box-sizing: border-box; }
.tm-header {
background: #222; padding: 5px 10px; display: flex;
justify-content: space-between; align-items: center; cursor: pointer;
}
.tm-controls button {
background: #444; color: white; border: none; padding: 2px 8px;
margin-left: 5px; border-radius: 3px; font-size: 10px;
}
#${this.id} .tm-log-area { display: block; flex: 1; overflow-y: auto; padding: 5px; -webkit-overflow-scrolling: touch; background: #1e1e1e; }
.tm-line {
border-bottom: 1px solid #333; padding: 3px 0;
white-space: pre-wrap; word-break: break-all; line-height: 1.4;
}
.type-log { color: #00ff00; }
.type-warn { color: #ffaa00; background: rgba(255,170,0,0.2); }
.type-error { color: #ff5555; background: rgba(255,85,85,0.2); }
.type-info { color: #55aaff; }
.log-time { color: #888; font-size: 8px; margin-right: 5px; }
/* ツリー構造のスタイル */
.tm-tree { margin-left: 14px; border-left: 1px dotted #444; }
.tm-summary { list-style: none; cursor: pointer; outline: none; display: flex; align-items: center; color: #55aaff; }
.tm-summary::-webkit-details-marker { display: none; } /* デフォルトの矢印を消す */
.tm-summary::before {
content: '▶'; display: inline-block; width: 12px; font-size: 8px;
transition: transform 0.1s; margin-right: 2px; color: #888;
}
details[open] > .tm-summary::before { transform: rotate(90deg); }
.tm-key { color: #9cdcfe; margin-right: 5px; } /* プロパティ名 */
.tm-val { color: #ce9178; } /* 値(文字列など) */
.tm-tag { color: #569cd6; } /* HTMLタグ名 */
.tm-attr { color: #9cdcfe; } /* HTML属性名 */
.tm-attr-val { color: #ce9178; } /* HTML属性値 */
`;
document.head.appendChild(style);
}
_createUI() {
this.container = document.createElement('div');
this.container.id = this.id;
// ヘッダーの作成
const header = document.createElement('div');
header.className = 'tm-header';
header.id = 'tm-header';
const title = document.createElement('span');
const b = document.createElement('b');
b.textContent = 'DEBUG CONSOLE';
title.appendChild(b);
header.appendChild(title);
// コントロールボタンの作成
const controls = document.createElement('div');
controls.className = 'tm-controls';
const clearBtn = document.createElement('button');
clearBtn.id = 'tm-clear';
clearBtn.textContent = 'Clear';
const toggleBtn = document.createElement('button');
toggleBtn.id = 'tm-toggle';
toggleBtn.textContent = 'Min/Max';
controls.appendChild(clearBtn);
controls.appendChild(toggleBtn);
header.appendChild(controls);
// ログエリアの作成
this.logArea = document.createElement('div');
this.logArea.className = 'tm-log-area';
this.logArea.id = 'tm-log-area';
this.container.appendChild(header);
this.container.appendChild(this.logArea);
document.documentElement.appendChild(this.container);
// イベントリスナー
clearBtn.onclick = () => this.clear();
toggleBtn.onclick = () => this.toggle();
header.onclick = (e) => {
if (e.target.tagName !== 'BUTTON') this.toggle();
};
}
toggle() {
this.isMinimized = !this.isMinimized;
this.container.classList.toggle('minimized', this.isMinimized);
}
clear() {
this.logArea.innerHTML = '';
}
/**
* 値を再帰的にDOM要素(ツリー構造)として描画
*/
_renderTree(arg, isRoot = false) {
if (arg === null) return this._createSpan('null', 'tm-val');
if (arg === undefined) return this._createSpan('undefined', 'tm-val');
if (typeof arg !== 'object') return this._createSpan(String(arg), 'tm-val');
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.className = 'tm-summary';
const content = document.createElement('div');
content.className = 'tm-tree';
// --- HTML要素の処理 ---
if (arg instanceof HTMLElement) {
summary.appendChild(this._createSpan('<', ''));
summary.appendChild(this._createSpan(arg.tagName.toLowerCase(), 'tm-tag'));
Array.from(arg.attributes).forEach(a => {
summary.appendChild(this._createSpan(' ', ''));
summary.appendChild(this._createSpan(a.name, 'tm-attr'));
summary.appendChild(this._createSpan('="', ''));
summary.appendChild(this._createSpan(a.value, 'tm-attr-val'));
summary.appendChild(this._createSpan('"', ''));
});
summary.appendChild(this._createSpan('>', ''));
if (arg.childNodes.length > 0) {
arg.childNodes.forEach(child => {
const item = document.createElement('div');
if (child.nodeType === Node.TEXT_NODE) {
const text = child.textContent.trim();
if (text) {
item.textContent = text;
content.appendChild(item);
}
} else {
item.appendChild(this._renderTree(child));
content.appendChild(item);
}
});
const closing = document.createElement('div');
closing.appendChild(this._createSpan('</', ''));
closing.appendChild(this._createSpan(arg.tagName.toLowerCase(), 'tm-tag'));
closing.appendChild(this._createSpan('>', ''));
content.appendChild(closing);
} else {
// 子なし要素は展開させない
const singleTag = summary.cloneNode(true);
return singleTag;
}
}
// --- 配列・オブジェクトの処理 ---
else {
const isArray = Array.isArray(arg);
const keys = Object.keys(arg);
const label = isArray ? `Array(${arg.length})` : `Object { ${keys.slice(0, 2).join(', ')}${keys.length > 2 ? '...' : ''} }`;
summary.textContent = label;
if (keys.length === 0) return this._createSpan(isArray ? '[]' : '{}', 'tm-val');
keys.forEach(key => {
const row = document.createElement('div');
row.style.display = 'flex';
row.appendChild(this._createSpan(`${key}: `, 'tm-key'));
row.appendChild(this._renderTree(arg[key]));
content.appendChild(row);
});
}
details.appendChild(summary);
details.appendChild(content);
return details;
}
_createSpan(text, className) {
const span = document.createElement('span');
span.className = className;
span.textContent = text;
return span;
}
print(type, args) {
const line = document.createElement('div');
line.className = `tm-line type-${type}`;
const threshold = 30;
const isAtBottom = (this.logArea.scrollHeight - this.logArea.scrollTop - this.logArea.clientHeight) <= threshold;
const time = new Date().toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
const timeSpan = this._createSpan(time, 'log-time');
line.appendChild(timeSpan);
Array.from(args).forEach(arg => {
const itemContainer = document.createElement('div');
itemContainer.style.display = 'inline-block';
itemContainer.style.verticalAlign = 'top';
itemContainer.style.marginRight = '10px';
itemContainer.appendChild(this._renderTree(arg, true));
line.appendChild(itemContainer);
});
this.logArea.appendChild(line);
if (this.logArea.children.length > this.maxLogs) {
this.logArea.removeChild(this.logArea.firstChild);
}
// ユーザーが一番下を見ていた場合のみ、新しいログを追ってスクロールする
if (isAtBottom && !this.isMinimized) {
this.logArea.scrollTop = this.logArea.scrollHeight;
}
}
_hookConsole() {
const methods = ['log', 'warn', 'error', 'info'];
methods.forEach(method => {
const original = console[method];
console[method] = (...args) => {
this.print(method, args);
original.apply(console, args);
};
});
}
_hookGlobalErrors() {
window.onerror = (msg, url, line, col, error) => {
this.print('error', [`Runtime Error: ${msg}\nAt: ${url}:${line}:${col}`]);
};
window.onunhandledrejection = (event) => {
this.print('error', [`Unhandled Promise Rejection: ${event.reason}`]);
};
}
}
// 実行
if (document.readyState === 'complete' || document.readyState === 'interactive') {
new ScreenLogger();
} else {
window.addEventListener('DOMContentLoaded', () => new ScreenLogger());
}
})();