iPhone/iPad用 コンソールエミュレータ。既存のconsole.logをすべて画面にミラーリングします。
À partir de
Ce script ne devrait pas être installé directement. C'est une librairie créée pour d'autres scripts. Elle doit être inclus avec la commande // @require https://update.greasyfork.org/scripts/577436/1822323/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%;
background: rgba(0, 0, 0, 0.9); 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;
}
#${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;
}
.tm-log-area {
flex: 1; overflow-y: auto; padding: 5px; -webkit-overflow-scrolling: touch;
}
.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.1); }
.type-error { color: #ff5555; background: rgba(255,85,85,0.1); }
.type-info { color: #55aaff; }
.log-time { color: #888; font-size: 8px; margin-right: 5px; }
`;
document.head.appendChild(style);
}
_createUI() {
this.container = document.createElement('div');
this.container.id = this.id;
this.container.innerHTML = `
<div class="tm-header" id="tm-header">
<span><b>DEBUG CONSOLE</b></span>
<div class="tm-controls">
<button id="tm-clear">Clear</button>
<button id="tm-toggle">Min/Max</button>
</div>
</div>
<div class="tm-log-area" id="tm-log-area"></div>
`;
document.documentElement.appendChild(this.container);
this.logArea = this.container.querySelector('#tm-log-area');
this.container.querySelector('#tm-clear').onclick = () => this.clear();
this.container.querySelector('#tm-toggle').onclick = () => this.toggle();
this.container.querySelector('#tm-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 = '';
}
/**
* オブジェクトを詳細に文字列化(循環参照対応)
*/
_serialize(obj, depth = 0) {
if (depth > 3) return "[Object (Too Deep)]"; // 無限再帰防止
if (obj === null) return "null";
if (obj === undefined) return "undefined";
if (typeof obj === "function") return `[Function: ${obj.name || 'anonymous'}]`;
if (typeof obj === "symbol") return obj.toString();
if (obj instanceof Error) return `${obj.name}: ${obj.message}\n${obj.stack}`;
if (obj instanceof HTMLElement) return obj.outerHTML;
if (typeof obj === "object") {
if (Array.isArray(obj)) {
return "[" + obj.map(item => this._serialize(item, depth + 1)).join(", ") + "]";
}
try {
// 循環参照チェックを伴うJSON化
const cache = new Set();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (cache.has(value)) return "[Circular]";
cache.add(value);
}
if (typeof value === 'function') return `[Function]`;
return value;
}, 2);
} catch (e) {
return "[Unserializable Object]";
}
}
return String(obj);
}
/**
* 画面にログを描画
*/
print(type, args) {
const msg = Array.from(args).map(arg => this._serialize(arg)).join(' ');
const time = new Date().toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
const line = document.createElement('div');
line.className = `tm-line type-${type}`;
line.innerHTML = `<span class="log-time">${time}</span>${this._escapeHtml(msg)}`;
this.logArea.appendChild(line);
// ログが増えすぎたら古いものを削除
if (this.logArea.children.length > this.maxLogs) {
this.logArea.removeChild(this.logArea.firstChild);
}
// 自動スクロール
if (!this.isMinimized) {
this.container.scrollTop = this.container.scrollHeight;
this.logArea.scrollTop = this.logArea.scrollHeight;
}
}
_escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
_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());
}
})();