Screen Display Log

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

Verzia zo dňa 12.05.2026. Pozri najnovšiu verziu.

Tento skript by nemal byť nainštalovaný priamo. Je to knižnica pre ďalšie skripty, ktorú by mali používať cez meta príkaz // @require https://update.greasyfork.org/scripts/577436/1822383/Screen%20Display%20Log.js

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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