Last active
December 31, 2025 23:20
-
-
Save stingray82/8d60d8ee553befca92dc119fcd45e4b4 to your computer and use it in GitHub Desktop.
Devkit - Addons & Mods
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Hook into the admin bar menu with a high priority so it appears toward the right. | |
| add_action('admin_bar_menu', 'add_db_name_admin_bar_item', 100); | |
| update_option( 'dpdevkit_adminer', 'yes' ); | |
| /** | |
| * Adds a new node to the WordPress admin bar that displays the database name. | |
| * | |
| * If the option "dpdevkit_adminer" is set to "yes" and the plugin | |
| * devkit/devkit.php is active, the badge links to /devkit-adminer. | |
| * | |
| * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance, passed by reference. | |
| */ | |
| function add_db_name_admin_bar_item( $wp_admin_bar ) { | |
| // Get the database name. | |
| $db_name = defined( 'DB_NAME' ) ? DB_NAME : ''; | |
| // Option to control whether Adminer link should be used; default to "yes". | |
| $adminer_enabled = get_option( 'dpdevkit_adminer', 'yes' ); | |
| // Default href (no link). | |
| $href = '#'; | |
| if ( 'yes' === $adminer_enabled ) { | |
| if ( ! function_exists( 'is_plugin_active' ) ) { | |
| include_once ABSPATH . 'wp-admin/includes/plugin.php'; | |
| } | |
| if ( function_exists( 'is_plugin_active' ) && is_plugin_active( 'devkit/devkit.php' ) ) { | |
| $href = trailingslashit( site_url() ) . 'devkit-adminer'; | |
| } | |
| } | |
| // Style the DB badge. | |
| $label = sprintf( | |
| '<span style="background-color: #0073aa; color: #fff; padding: 3px 8px; border-radius: 3px; font-weight: bold;">DB: %s</span>', | |
| esc_html( $db_name ) | |
| ); | |
| // Admin bar node. | |
| $args = array( | |
| 'id' => 'db-name-display', | |
| 'title' => $label, | |
| 'href' => $href, | |
| 'meta' => array( | |
| 'title' => 'Current Database Name', | |
| 'target' => '_blank', // ← Open in new tab | |
| 'class' => 'db-adminer-link' // (optional class) | |
| ), | |
| ); | |
| $wp_admin_bar->add_node( $args ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Plugin Name: Admin Bar Debug Toggle | |
| * Description: Toggle WordPress debug ON/OFF from the admin bar, Safely edits wp-config.php with a backup and add a devkit sub menu. | |
| */ | |
| if ( ! defined( 'ABSPATH' ) ) exit; | |
| class TD_AdminBar_Debug_Toggle { | |
| const OPTION = 'td_debug_toggle_last'; | |
| const ACTION = 'td_toggle_debug'; | |
| public function __construct() { | |
| add_action( 'admin_bar_menu', [ $this, 'add_bar_item' ], 1000 ); | |
| add_action( 'admin_post_' . self::ACTION, [ $this, 'handle_toggle' ] ); | |
| add_action( 'admin_head', [ $this, 'styles' ] ); | |
| add_action( 'wp_head', [ $this, 'styles' ] ); | |
| } | |
| private function current_state(): bool { | |
| if ( ! defined( 'WP_DEBUG' ) ) return false; | |
| $v = constant( 'WP_DEBUG' ); | |
| // Handle booleans, integers, and stringy values like 'true'/'false' | |
| if ( is_bool( $v ) ) return $v; | |
| if ( is_int( $v ) ) return (bool) $v; | |
| if ( is_string( $v ) ) { | |
| $lv = strtolower( trim( $v ) ); | |
| return in_array( $lv, [ '1', 'true', 'on', 'yes' ], true ); | |
| } | |
| return (bool) $v; | |
| } | |
| public function add_bar_item( WP_Admin_Bar $bar ) { | |
| if ( ! is_user_logged_in() || ! current_user_can('manage_options') || ! is_admin_bar_showing() ) { | |
| return; | |
| } | |
| $on = $this->current_state(); | |
| $url = wp_nonce_url( | |
| admin_url( 'admin-post.php?action=' . self::ACTION . '&state=' . ( $on ? 'off' : 'on' ) ), | |
| self::ACTION | |
| ); | |
| $title = sprintf( | |
| '<span class="ab-icon dashicons dashicons-bug"></span>' . | |
| '<span class="td-dot" aria-hidden="true"></span>' . | |
| '<span class="ab-label td-debug-label">%s</span>', | |
| $on ? 'Debug: ON' : 'Debug: OFF' | |
| ); | |
| // Add main toggle | |
| $bar->add_node( [ | |
| 'id' => 'td-debug-toggle', | |
| 'title' => $title, | |
| 'href' => $url, | |
| 'meta' => [ | |
| 'class' => $on ? 'td-debug-on' : 'td-debug-off', | |
| 'title' => $on ? 'Click to turn OFF debug' : 'Click to turn ON debug', | |
| ], | |
| ] ); | |
| /** | |
| * Add Error Log submenu only if DevKit is installed & active | |
| */ | |
| if ( ! function_exists( 'is_plugin_active' ) ) { | |
| include_once ABSPATH . 'wp-admin/includes/plugin.php'; | |
| } | |
| if ( function_exists( 'is_plugin_active' ) && is_plugin_active( 'devkit/devkit.php' ) ) { | |
| $bar->add_node( [ | |
| 'id' => 'td-debug-error-log', | |
| 'parent' => 'td-debug-toggle', | |
| 'title' => 'Error Log', | |
| 'href' => admin_url( 'admin.php?page=devkit&tab=error-log' ), | |
| 'meta' => [ | |
| 'class' => 'td-error-log-link', | |
| ], | |
| ] ); | |
| } | |
| } | |
| public function styles() { | |
| if ( ! is_admin_bar_showing() ) return; ?> | |
| <style> | |
| #wpadminbar #wp-admin-bar-td-debug-toggle .ab-icon:before { top:2px; } | |
| #wpadminbar #wp-admin-bar-td-debug-toggle .td-dot{ | |
| display:inline-block; width:8px; height:8px; border-radius:999px; | |
| margin:0 6px 0 2px; vertical-align:middle; box-shadow:0 0 0 1px rgba(0,0,0,.15) inset; | |
| } | |
| #wpadminbar #wp-admin-bar-td-debug-toggle.td-debug-on .td-dot{ background:#00a32a; } | |
| #wpadminbar #wp-admin-bar-td-debug-toggle.td-debug-off .td-dot{ background:#d63638; } | |
| /* Label colors (optional) */ | |
| #wpadminbar #wp-admin-bar-td-debug-toggle.td-debug-on .td-debug-label { color:#00a32a; font-weight:600; } | |
| #wpadminbar #wp-admin-bar-td-debug-toggle.td-debug-off .td-debug-label { color:#d63638; font-weight:600; } | |
| /* Icon-only on narrow screens (optional) */ | |
| @media (max-width: 782px){ | |
| #wpadminbar #wp-admin-bar-td-debug-toggle .td-debug-label{ display:none; } | |
| } | |
| </style> | |
| <?php } | |
| public function handle_toggle() { | |
| if ( ! current_user_can('manage_options') ) wp_die('Unauthorized'); | |
| check_admin_referer( self::ACTION ); | |
| $state = ( isset($_GET['state']) && $_GET['state'] === 'on' ) ? true : false; | |
| $ok = $this->set_wp_debug( $state ); | |
| if ( $ok ) { | |
| update_option( self::OPTION, $state ? 'on' : 'off', false ); | |
| wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url() ); | |
| exit; | |
| } else { | |
| wp_die( 'Could not update wp-config.php. Check file permissions.' ); | |
| } | |
| } | |
| /** | |
| * Set WP_DEBUG/LOG/DISPLAY in wp-config.php with a one-time backup. | |
| */ | |
| /** | |
| * Toggle debug by NORMALIZING wp-config.php: | |
| * - remove all existing WP_DEBUG* defines (even inside if(!defined()) guards, any value like '0','1', true, false) | |
| * - insert a single, clean block BEFORE wp-settings.php is required. | |
| */ | |
| private function set_wp_debug( bool $enable ): bool { | |
| $config = $this->locate_wp_config(); | |
| if ( ! $config || ! is_writable( $config ) ) return false; | |
| $contents = file_get_contents( $config ); | |
| if ( $contents === false ) return false; | |
| // Backup & rotate | |
| $backup = $config . '.' . date('Ymd-His') . '.bak'; | |
| @copy( $config, $backup ); | |
| $this->cleanup_backups( $config, 3 ); | |
| // 1) Remove ANY prior defines/guards for these constants | |
| $contents = $this->strip_debug_defines( $contents, [ | |
| 'WP_DEBUG', | |
| 'WP_DEBUG_LOG', | |
| 'WP_DEBUG_DISPLAY', | |
| 'SCRIPT_DEBUG', | |
| ] ); | |
| // 2) Build the new single source of truth | |
| $block = "define( 'WP_DEBUG', " . ( $enable ? 'true' : 'false' ) . " );\n"; | |
| $block .= "define( 'WP_DEBUG_LOG', " . ( $enable ? 'true' : 'false' ) . " );\n"; | |
| // keep display off by default; flip to true if you want notices on-screen | |
| $block .= "define( 'WP_DEBUG_DISPLAY', " . ( /* display? */ false ? 'true' : 'false' ) . " );\n"; | |
| $block .= "define( 'SCRIPT_DEBUG', " . ( $enable ? 'true' : 'false' ) . " );\n"; | |
| // 3) Insert the block in a reliable early spot | |
| $contents = $this->insert_before_wp_settings( $contents, $block ); | |
| // 4) Write atomically | |
| $tmp = $config . '.tmp'; | |
| if ( file_put_contents( $tmp, $contents, LOCK_EX ) === false ) return false; | |
| $ok = @rename( $tmp, $config ); | |
| if ( ! $ok ) { @unlink( $tmp ); return false; } | |
| if ( function_exists('opcache_invalidate') ) @opcache_invalidate( $config, true ); | |
| error_log( 'Admin Bar Debug Toggle: set debug to ' . ( $enable ? 'ON' : 'OFF' ) . ' at ' . date('Y-m-d H:i:s') ); | |
| return true; | |
| } | |
| /** | |
| * Remove ALL occurrences of define('CONST', ...); including wrapped in: | |
| * if ( ! defined('CONST') ) { define('CONST', ...); } | |
| */ | |
| private function strip_debug_defines( string $contents, array $consts ): string { | |
| foreach ( $consts as $const ) { | |
| // Strip guarded blocks | |
| $guard = '/^\s*if\s*\(\s*!\s*defined\s*\(\s*[\'"]' . preg_quote($const,'/') . '[\'"]\s*\)\s*\)\s*\{\s*' . | |
| 'define\s*\(\s*[\'"]' . preg_quote($const,'/') . '[\'"]\s*,\s*.+?\)\s*;\s*\}\s*$/ims'; | |
| $contents = preg_replace( $guard, '', $contents ); | |
| // Strip plain defines (any value, quoted or not) | |
| $plain = '/^\s*define\s*\(\s*[\'"]' . preg_quote($const,'/') . '[\'"]\s*,\s*.+?\)\s*;\s*$/im'; | |
| $contents = preg_replace( $plain, '', $contents ); | |
| } | |
| return $contents; | |
| } | |
| /** | |
| * Insert $block BEFORE the require of wp-settings.php, else before the stop-edit marker, | |
| * else right after the opening <?php tag, else prepend. | |
| */ | |
| private function insert_before_wp_settings( string $contents, string $block ): string { | |
| // before wp-settings.php | |
| $requirePattern = '/(?:require|require_once)\s*\(?\s*ABSPATH\s*\.\s*[\'"]wp-settings\.php[\'"]\s*\)?\s*;\s*/i'; | |
| if ( preg_match( $requirePattern, $contents, $m, PREG_OFFSET_CAPTURE ) ) { | |
| $pos = $m[0][1]; | |
| return substr($contents, 0, $pos) . $block . substr($contents, $pos); | |
| } | |
| // before stop-editing marker | |
| $marker = "/* That's all, stop editing! Happy publishing. */"; | |
| $pos = strpos( $contents, $marker ); | |
| if ( $pos !== false ) { | |
| return substr($contents, 0, $pos) . $block . substr($contents, $pos); | |
| } | |
| // after opening tag | |
| if ( preg_match( '/^\s*<\?php\b/i', $contents, $m, PREG_OFFSET_CAPTURE ) ) { | |
| $len = strlen( $m[0][0] ); | |
| return substr($contents, 0, $len) . "\n" . $block . substr($contents, $len); | |
| } | |
| // prepend | |
| return $block . $contents; | |
| } | |
| /** | |
| * Delete older wp-config backups, keeping the N most recent. | |
| */ | |
| private function cleanup_backups( string $config_path, int $keep = 3 ): void { | |
| $pattern = $config_path . '.*.bak'; | |
| $files = glob( $pattern ); | |
| if ( ! $files || count( $files ) <= $keep ) return; | |
| // Sort newest → oldest by filemtime (fallback to name) | |
| usort( $files, function ( $a, $b ) { | |
| $ma = @filemtime( $a ) ?: 0; | |
| $mb = @filemtime( $b ) ?: 0; | |
| if ( $ma === $mb ) return strcmp( $b, $a ); // name tiebreaker, newest first | |
| return $mb <=> $ma; // newest first | |
| }); | |
| // Remove everything after the first $keep | |
| for ( $i = $keep; $i < count( $files ); $i++ ) { | |
| if ( is_file( $files[$i] ) ) @unlink( $files[$i] ); | |
| } | |
| } | |
| private function locate_wp_config() { | |
| // Standard location | |
| $p = ABSPATH . 'wp-config.php'; | |
| if ( file_exists( $p ) ) return $p; | |
| // Moved one level up (common) | |
| $up = dirname( ABSPATH ) . '/wp-config.php'; | |
| if ( file_exists( $up ) ) return $up; | |
| return false; | |
| } | |
| private function upsert_define( string $contents, string $const, string $boolLiteral ): string { | |
| $pattern = '/define\s*\(\s*[\'"]' . preg_quote($const, '/') . '[\'"]\s*,\s*(true|false)\s*\)\s*;\s*/i'; | |
| $replacement = "define( '$const', $boolLiteral );\n"; | |
| // 1) Replace if already present | |
| if ( preg_match( $pattern, $contents ) ) { | |
| return preg_replace( $pattern, $replacement, $contents, 1 ); | |
| } | |
| // 2) Insert before the standard marker if present | |
| $marker = "/* That's all, stop editing! Happy publishing. */"; | |
| $pos = strpos( $contents, $marker ); | |
| if ( $pos !== false ) { | |
| return substr($contents, 0, $pos) . $replacement . substr($contents, $pos); | |
| } | |
| // 3) Insert just BEFORE WordPress is loaded | |
| $requirePattern = '/(?:require|require_once)\s*\(?\s*ABSPATH\s*\.\s*[\'"]wp-settings\.php[\'"]\s*\)?\s*;\s*/i'; | |
| if ( preg_match( $requirePattern, $contents, $m, PREG_OFFSET_CAPTURE ) ) { | |
| $reqPos = $m[0][1]; | |
| return substr($contents, 0, $reqPos) . $replacement . substr($contents, $reqPos); | |
| } | |
| // 4) Fallback: insert right after the opening PHP tag so it runs early | |
| if ( preg_match( '/^\s*<\?php\b/i', $contents, $m, PREG_OFFSET_CAPTURE ) ) { | |
| $tagLen = strlen( $m[0][0] ); | |
| return substr($contents, 0, $tagLen) . "\n" . $replacement . substr($contents, $tagLen); | |
| } | |
| // Last resort: prepend | |
| return $replacement . $contents; | |
| } | |
| } | |
| new TD_AdminBar_Debug_Toggle(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Deletes all transients (including site-wide transients) from the database. | |
| * This technically didn't ever use DevKit as it was written before the option was added to devkit. | |
| */ | |
| add_action( 'init', function () { | |
| if ( get_option( 'enable_transients_admin_bar', null ) === null ) { | |
| add_option( 'enable_transients_admin_bar', 1 ); | |
| } | |
| }); | |
| // Delete all transients | |
| function delete_all_transients() { | |
| global $wpdb; | |
| $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_%'"); | |
| $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_site_transient_%'"); | |
| if (function_exists('wp_cache_flush')) { | |
| wp_cache_flush(); | |
| } | |
| } | |
| // Add Tools > Delete All Transients submenu | |
| function add_delete_transients_menu_item() { | |
| add_submenu_page( | |
| 'tools.php', | |
| 'Delete All Transients', | |
| 'Delete All Transients', | |
| 'manage_options', | |
| 'delete-all-transients', | |
| 'delete_all_transients_page' | |
| ); | |
| } | |
| add_action('admin_menu', 'add_delete_transients_menu_item'); | |
| // Settings registration | |
| function register_delete_transients_settings() { | |
| register_setting('delete_transients_settings_group', 'enable_transients_admin_bar'); | |
| } | |
| add_action('admin_init', 'register_delete_transients_settings'); | |
| // Settings page content | |
| function delete_all_transients_page() { | |
| ?> | |
| <div class="wrap"> | |
| <h1>Delete All Transients</h1> | |
| <form method="post" action=""> | |
| <?php wp_nonce_field('delete_all_transients_action', 'delete_all_transients_nonce'); ?> | |
| <input type="hidden" name="delete_all_transients" value="1" /> | |
| <p>Click the button below to delete all transients from the database.</p> | |
| <input type="submit" class="button button-primary" value="Delete All Transients" /> | |
| </form> | |
| <hr> | |
| <form method="post" action="options.php"> | |
| <?php | |
| settings_fields('delete_transients_settings_group'); | |
| do_settings_sections('delete_transients_settings_group'); | |
| ?> | |
| <label> | |
| <input type="checkbox" name="enable_transients_admin_bar" value="1" <?php checked( get_option( 'enable_transients_admin_bar', 1 ), 1 ); ?> /> | |
| Enable Admin Bar Button to Delete Transients | |
| </label><br><br> | |
| <?php submit_button('Save Settings'); ?> | |
| </form> | |
| </div> | |
| <?php | |
| } | |
| // Handle form submission on settings page | |
| function handle_delete_all_transients() { | |
| if (isset($_POST['delete_all_transients']) && check_admin_referer('delete_all_transients_action', 'delete_all_transients_nonce')) { | |
| delete_all_transients(); | |
| wp_redirect(admin_url('tools.php?transients-deleted=1')); | |
| exit; | |
| } | |
| } | |
| add_action('admin_init', 'handle_delete_all_transients'); | |
| // Admin notice | |
| function display_delete_transients_success_message() { | |
| if (isset($_GET['transients-deleted']) && $_GET['transients-deleted'] == 1) { | |
| echo '<div class="notice notice-success is-dismissible"><p>All transients have been successfully deleted.</p></div>'; | |
| } | |
| } | |
| add_action('admin_notices', 'display_delete_transients_success_message'); | |
| // Add Admin Bar Button | |
| function add_transient_clear_button_to_admin_bar($wp_admin_bar) { | |
| if (!current_user_can('manage_options')) return; | |
| if (!get_option('enable_transients_admin_bar', 1)) return; | |
| $wp_admin_bar->add_node([ | |
| 'id' => 'delete_transients_button', | |
| 'title' => 'Delete Transients', | |
| 'href' => wp_nonce_url(admin_url('tools.php?delete-transients-from-bar=1'), 'delete_transients_admin_bar', 'transient_bar_nonce'), | |
| 'meta' => ['title' => 'Delete all transients now'] | |
| ]); | |
| } | |
| add_action('admin_bar_menu', 'add_transient_clear_button_to_admin_bar', 100); | |
| // Handle Admin Bar Button Action | |
| function handle_admin_bar_transient_deletion() { | |
| if (isset($_GET['delete-transients-from-bar']) && current_user_can('manage_options')) { | |
| if (check_admin_referer('delete_transients_admin_bar', 'transient_bar_nonce')) { | |
| delete_all_transients(); | |
| $redirect_url = wp_get_referer() ? add_query_arg('transients-deleted', '1', wp_get_referer()) : admin_url('tools.php?transients-deleted=1'); | |
| wp_redirect($redirect_url); | |
| exit; | |
| } | |
| } | |
| } | |
| add_action('admin_init', 'handle_admin_bar_transient_deletion'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name WP Devkit Error Log Helper | |
| // @namespace http://tampermonkey.net/ | |
| // @version 0.3 | |
| // @description Click-to-copy error log entries + auto reload button | |
| // @author stingray82 | |
| // @match *://*/wp-admin/admin.php* | |
| // @run-at document-end | |
| // @grant GM_addStyle | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // Only run on the Devkit error log tab | |
| if (!/page=devkit/.test(location.search) || !/tab=error-log/.test(location.search)) { | |
| return; | |
| } | |
| // --- Styles for visual feedback on copy --- | |
| GM_addStyle(` | |
| .error-log code.tm-clickable-row { | |
| cursor: pointer; | |
| display: block; /* each <code> becomes its own row */ | |
| width: 100%; | |
| padding: 0 4px; | |
| margin: 1px 0; /* small gap between rows */ | |
| transition: background-color 0.15s ease, box-shadow 0.15s ease; | |
| } | |
| .error-log code.tm-copied { | |
| box-shadow: 0 0 0 2px #4ade80; | |
| border-radius: 4px; | |
| background-color: rgba(74, 222, 128, 0.15); | |
| } | |
| `); | |
| // Check if a <code> line is the *start* of a log entry | |
| function isEntryHeader(codeEl) { | |
| if (!codeEl) return false; | |
| const text = codeEl.innerText.replace(/^\s+/, ''); // trim left | |
| // [28-Nov-2025 ...] | |
| return /^\[\d{2}-[A-Za-z]{3}-\d{4}/.test(text); | |
| } | |
| // --- Click-to-copy using event delegation --- | |
| function initClickToCopy() { | |
| const container = document.querySelector('.error-log'); | |
| if (!container) return; | |
| // Give lines pointer + title | |
| container.querySelectorAll('code').forEach(code => { | |
| code.classList.add('tm-clickable-row'); | |
| if (!code.title) { | |
| code.title = 'Click to copy this log entry'; | |
| } | |
| }); | |
| // Only attach the event listener once | |
| if (container.dataset.tmClickHandler === '1') return; | |
| container.dataset.tmClickHandler = '1'; | |
| container.addEventListener('click', (e) => { | |
| const clicked = e.target.closest('code'); | |
| if (!clicked || !container.contains(clicked)) return; | |
| const allCodes = Array.from(container.querySelectorAll('code')); | |
| const clickedIndex = allCodes.indexOf(clicked); | |
| if (clickedIndex === -1) return; | |
| // 1) Walk backwards from clicked to find the header of this entry | |
| let startIdx = clickedIndex; | |
| while (startIdx > 0 && !isEntryHeader(allCodes[startIdx])) { | |
| startIdx--; | |
| } | |
| // If we never hit a header, just consider the clicked line as start | |
| if (!isEntryHeader(allCodes[startIdx])) { | |
| startIdx = clickedIndex; | |
| } | |
| // 2) From startIdx, collect lines until the *next* header (not included) | |
| const blockLines = []; | |
| const blockEls = []; | |
| for (let i = startIdx; i < allCodes.length; i++) { | |
| const el = allCodes[i]; | |
| // If this is a new header and it's not the first line, stop *before* it | |
| if (i > startIdx && isEntryHeader(el)) { | |
| break; | |
| } | |
| const lineText = el.innerText.replace(/\s+$/, ''); // trim right | |
| blockLines.push(lineText); | |
| blockEls.push(el); | |
| } | |
| const fullText = blockLines.join('\n'); | |
| copyText(fullText); | |
| flashCopied(blockEls); | |
| }); | |
| } | |
| function copyText(text) { | |
| if (navigator.clipboard && navigator.clipboard.writeText) { | |
| navigator.clipboard.writeText(text).catch(() => fallbackCopy(text)); | |
| } else { | |
| fallbackCopy(text); | |
| } | |
| } | |
| function fallbackCopy(text) { | |
| const ta = document.createElement('textarea'); | |
| ta.value = text; | |
| ta.style.position = 'fixed'; | |
| ta.style.top = '-9999px'; | |
| document.body.appendChild(ta); | |
| ta.select(); | |
| try { | |
| document.execCommand('copy'); | |
| } catch (e) { | |
| console.warn('Copy failed:', e); | |
| } | |
| document.body.removeChild(ta); | |
| } | |
| function flashCopied(els) { | |
| els.forEach(el => el.classList.add('tm-copied')); | |
| setTimeout(() => { | |
| els.forEach(el => el.classList.remove('tm-copied')); | |
| }, 300); | |
| } | |
| // Run once on load | |
| initClickToCopy(); | |
| // Re-run whenever the log area changes (e.g. reload button updates it) | |
| if ('MutationObserver' in window) { | |
| const observer = new MutationObserver(() => { | |
| initClickToCopy(); | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| } else { | |
| // Fallback: poll occasionally | |
| setInterval(initClickToCopy, 2000); | |
| } | |
| // --- Auto reload button (same as previous working version) --- | |
| let autoReloadInterval = null; | |
| function createAutoReloadButton() { | |
| const reloadBtn = document.querySelector('#reload-log'); | |
| if (!reloadBtn) return; | |
| // Don’t add twice | |
| if (document.querySelector('#tm-auto-reload-log')) return; | |
| const autoBtn = document.createElement('button'); | |
| autoBtn.id = 'tm-auto-reload-log'; | |
| autoBtn.className = 'button-secondary'; | |
| autoBtn.textContent = 'Auto reload (off)'; | |
| autoBtn.style.marginLeft = '8px'; | |
| autoBtn.addEventListener('click', () => { | |
| if (autoReloadInterval) { | |
| // Turn OFF | |
| clearInterval(autoReloadInterval); | |
| autoReloadInterval = null; | |
| autoBtn.textContent = 'Auto reload (off)'; | |
| } else { | |
| // Turn ON – click once immediately, then every 10 seconds | |
| reloadBtn.click(); | |
| autoReloadInterval = setInterval(() => { | |
| reloadBtn.click(); | |
| }, 10000); // 10,000 ms = 10 seconds | |
| autoBtn.textContent = 'Auto reload (10s)'; | |
| } | |
| }); | |
| // Insert right after the existing reload button | |
| reloadBtn.parentNode.insertBefore(autoBtn, reloadBtn.nextSibling); | |
| } | |
| createAutoReloadButton(); | |
| })(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment