Screen Display Log

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

Устаревшая версия за 12.05.2026. Перейдите к последней версии.

Этот скрипт недоступен для установки пользователем. Он является библиотекой, которая подключается к другим скриптам мета-ключом // @require https://update.greasyfork.org/scripts/577436/1822384/Screen%20Display%20Log.js

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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() {
            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());
    }
})();