Screen Display Log

iPhone/iPad用 コンソールエミュレータ。既存のconsole.logをすべて画面にミラーリングします。

ეს სკრიპტი არ უნდა იყოს პირდაპირ დაინსტალირებული. ეს ბიბლიოთეკაა, სხვა სკრიპტებისთვის უნდა ჩართეთ მეტა-დირექტივაში // @require https://update.greatest.deepsurf.us/scripts/577436/1822402/Screen%20Display%20Log.js.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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 = `
                #tm-pro-console {
                    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; 
                }
                #tm-pro-console.minimized { height: 30px !important; overflow: hidden; }
                #tm-pro-console * { 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-pro-console .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() {
            if (this.logArea) {
                this.logArea.replaceChildren();
            }
        }

        /**
         * 値を再帰的に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());
    }
})();