1330 lines
62 KiB
PHP
1330 lines
62 KiB
PHP
<?php
|
||
/*
|
||
Plugin Name: MC Player History
|
||
Plugin URI: https://git.viper.ipv64.net/M_Viper/MC-Player-History---WordPress-Plugin
|
||
Description: Spielerverlauf deines Minecraft Servers.
|
||
Version: 1.3.0
|
||
Author: M_Viper
|
||
Author URI: https://m-viper.de
|
||
Requires at least: 6.8
|
||
Tested up to: 6.8
|
||
PHP Version: 7.4
|
||
License: GPL2
|
||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||
Text Domain: MC-Player-History---WordPress-Plugin
|
||
Requires PHP: 7.4
|
||
Support: [Discord Support](https://discord.com/invite/FdRs4BRd8D)
|
||
Support: [Telegram Support](https://t.me/M_Viper04)
|
||
*/
|
||
|
||
|
||
if ( ! defined( 'ABSPATH' ) ) {
|
||
exit;
|
||
}
|
||
|
||
// ===============================
|
||
// MC PLAYER HISTORY - UPDATE SYSTEM
|
||
// ===============================
|
||
|
||
// Aktuelle Plugin-Version aus Plugin-Header lesen
|
||
function mcph_get_plugin_version() {
|
||
if ( ! function_exists( 'get_plugin_data' ) ) {
|
||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||
}
|
||
$plugin_data = get_plugin_data( __FILE__ );
|
||
return $plugin_data['Version'] ?? '0.0.0';
|
||
}
|
||
|
||
// Cache manuell leeren
|
||
function mcph_clear_update_cache() {
|
||
if ( isset($_GET['mcph_clear_cache']) && current_user_can('manage_options') ) {
|
||
check_admin_referer('mcph_clear_cache_action');
|
||
delete_transient('mcph_latest_release');
|
||
wp_redirect( admin_url('plugins.php') );
|
||
exit;
|
||
}
|
||
}
|
||
add_action('admin_init', 'mcph_clear_update_cache');
|
||
|
||
// Neueste Release-Infos von Gitea holen
|
||
function mcph_get_latest_release_info( $force_refresh = false ) {
|
||
$transient_key = 'mcph_latest_release';
|
||
|
||
if ( $force_refresh ) {
|
||
delete_transient( $transient_key );
|
||
}
|
||
|
||
$release_info = get_transient( $transient_key );
|
||
|
||
if ( false === $release_info ) {
|
||
$response = wp_remote_get(
|
||
'https://git.viper.ipv64.net/api/v1/repos/M_Viper/MC-Player-History---WordPress-Plugin/releases/latest',
|
||
['timeout' => 10]
|
||
);
|
||
|
||
if ( ! is_wp_error($response) && 200 === wp_remote_retrieve_response_code($response) ) {
|
||
$body = wp_remote_retrieve_body($response);
|
||
$data = json_decode($body, true);
|
||
|
||
if ( $data && isset($data['tag_name']) ) {
|
||
$tag = ltrim( $data['tag_name'], 'v' );
|
||
$release_info = [
|
||
'version' => $tag,
|
||
'download_url' => $data['zipball_url'] ?? '',
|
||
'notes' => $data['body'] ?? '',
|
||
'published_at' => $data['published_at'] ?? '',
|
||
];
|
||
set_transient( $transient_key, $release_info, 6 * HOUR_IN_SECONDS );
|
||
} else {
|
||
set_transient( $transient_key, [], HOUR_IN_SECONDS );
|
||
}
|
||
} else {
|
||
set_transient( $transient_key, [], HOUR_IN_SECONDS );
|
||
}
|
||
}
|
||
return $release_info;
|
||
}
|
||
|
||
// Admin-Update-Hinweis
|
||
function mcph_show_update_notice() {
|
||
if ( ! current_user_can('manage_options') ) return;
|
||
|
||
$current_version = mcph_get_plugin_version();
|
||
$latest_release = mcph_get_latest_release_info();
|
||
|
||
if ( ! empty($latest_release['version']) && version_compare($current_version, $latest_release['version'], '<') ) {
|
||
$refresh_url = wp_nonce_url( admin_url('plugins.php?mcph_clear_cache=1'), 'mcph_clear_cache_action' );
|
||
?>
|
||
<div class="notice notice-warning is-dismissible">
|
||
<h3>MC Player History – Update verfügbar</h3>
|
||
<p>
|
||
Aktuelle Version: <strong><?php echo esc_html($current_version); ?></strong><br>
|
||
Neueste Version: <strong><?php echo esc_html($latest_release['version']); ?></strong>
|
||
</p>
|
||
<p>
|
||
<a href="<?php echo esc_url($latest_release['download_url']); ?>" class="button button-primary" target="_blank">Update herunterladen</a>
|
||
<a href="https://git.viper.ipv64.net/M_Viper/MC-Player-History---WordPress-Plugin/releases" class="button" target="_blank">Release Notes</a>
|
||
<a href="<?php echo esc_url($refresh_url); ?>" class="button">Jetzt neu prüfen</a>
|
||
</p>
|
||
</div>
|
||
<?php
|
||
}
|
||
}
|
||
add_action('admin_notices', 'mcph_show_update_notice');
|
||
|
||
|
||
global $mc_player_history_db_version;
|
||
$mc_player_history_db_version = '1.17.0'; // DB Update für Joins, Bans, Mutes, Bedrock
|
||
|
||
function mcph_log( $message ) {
|
||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||
error_log( '[MC Player History] ' . $message );
|
||
}
|
||
}
|
||
|
||
/**
|
||
* LiteBans-Verbindung aufbauen (falls LiteBans Manager konfiguriert ist).
|
||
*/
|
||
function mcph_get_litebans_connection() {
|
||
static $connection = null;
|
||
static $initialized = false;
|
||
|
||
if ( $initialized ) {
|
||
return $connection;
|
||
}
|
||
$initialized = true;
|
||
|
||
$settings = get_option( 'wp_litebans_pro_settings', array() );
|
||
if ( ! is_array( $settings ) || empty( $settings['db_name'] ) || empty( $settings['db_user'] ) ) {
|
||
$connection = null;
|
||
return $connection;
|
||
}
|
||
|
||
$host = ! empty( $settings['db_host'] ) ? sanitize_text_field( $settings['db_host'] ) : 'localhost';
|
||
if ( ! empty( $settings['db_port'] ) ) {
|
||
$host .= ':' . intval( $settings['db_port'] );
|
||
}
|
||
|
||
$prefix = isset( $settings['table_prefix'] ) ? preg_replace( '/[^a-zA-Z0-9_]/', '', $settings['table_prefix'] ) : 'litebans_';
|
||
if ( empty( $prefix ) ) {
|
||
$prefix = 'litebans_';
|
||
}
|
||
|
||
$lbdb = new wpdb(
|
||
sanitize_text_field( $settings['db_user'] ),
|
||
isset( $settings['db_pass'] ) ? $settings['db_pass'] : '',
|
||
sanitize_text_field( $settings['db_name'] ),
|
||
$host
|
||
);
|
||
|
||
if ( ! empty( $lbdb->last_error ) ) {
|
||
mcph_log( 'LiteBans Verbindung fehlgeschlagen: ' . $lbdb->last_error );
|
||
$connection = null;
|
||
return $connection;
|
||
}
|
||
|
||
$connection = array(
|
||
'db' => $lbdb,
|
||
'prefix' => $prefix,
|
||
);
|
||
|
||
return $connection;
|
||
}
|
||
|
||
/**
|
||
* Holt Bans/Mutes/Warns aus LiteBans für einen Spieler.
|
||
*/
|
||
function mcph_get_litebans_punishments( $uuid, $username ) {
|
||
static $count_cache = array();
|
||
|
||
$cache_key = (string) $uuid . '|' . strtolower( (string) $username );
|
||
if ( isset( $count_cache[ $cache_key ] ) ) {
|
||
return $count_cache[ $cache_key ];
|
||
}
|
||
|
||
$conn = mcph_get_litebans_connection();
|
||
if ( empty( $conn['db'] ) || empty( $conn['prefix'] ) ) {
|
||
$count_cache[ $cache_key ] = null;
|
||
return null;
|
||
}
|
||
|
||
$lbdb = $conn['db'];
|
||
$prefix = $conn['prefix'];
|
||
|
||
$search_uuid = '';
|
||
if ( ! empty( $uuid ) && strpos( $uuid, 'legacy-' ) !== 0 ) {
|
||
$search_uuid = $uuid;
|
||
}
|
||
|
||
if ( empty( $search_uuid ) && ! empty( $username ) ) {
|
||
$history_table = esc_sql( $prefix . 'history' );
|
||
$search_uuid = $lbdb->get_var(
|
||
$lbdb->prepare(
|
||
"SELECT uuid FROM {$history_table} WHERE name = %s ORDER BY date DESC LIMIT 1",
|
||
$username
|
||
)
|
||
);
|
||
}
|
||
|
||
if ( empty( $search_uuid ) ) {
|
||
$count_cache[ $cache_key ] = null;
|
||
return null;
|
||
}
|
||
|
||
$bans_table = esc_sql( $prefix . 'bans' );
|
||
$mutes_table = esc_sql( $prefix . 'mutes' );
|
||
$warns_table = esc_sql( $prefix . 'warnings' );
|
||
|
||
$bans = intval( $lbdb->get_var( $lbdb->prepare( "SELECT COUNT(*) FROM {$bans_table} WHERE uuid = %s", $search_uuid ) ) );
|
||
$mutes = intval( $lbdb->get_var( $lbdb->prepare( "SELECT COUNT(*) FROM {$mutes_table} WHERE uuid = %s", $search_uuid ) ) );
|
||
$warns = intval( $lbdb->get_var( $lbdb->prepare( "SELECT COUNT(*) FROM {$warns_table} WHERE uuid = %s", $search_uuid ) ) );
|
||
|
||
if ( ! empty( $lbdb->last_error ) ) {
|
||
mcph_log( 'LiteBans Query Fehler: ' . $lbdb->last_error );
|
||
$count_cache[ $cache_key ] = null;
|
||
return null;
|
||
}
|
||
|
||
$count_cache[ $cache_key ] = array(
|
||
'bans' => $bans,
|
||
'mutes' => $mutes,
|
||
'warns' => $warns,
|
||
);
|
||
|
||
return $count_cache[ $cache_key ];
|
||
}
|
||
|
||
/**
|
||
* Hilfsfunktion: Minecraft Color Codes umwandeln
|
||
*/
|
||
function mcph_parse_minecraft_colors( $text ) {
|
||
if ( empty( $text ) ) return '';
|
||
$map = array(
|
||
'&0' => '<span style="color: #000000">', '&1' => '<span style="color: #0000AA">',
|
||
'&2' => '<span style="color: #00AA00">', '&3' => '<span style="color: #00AAAA">',
|
||
'&4' => '<span style="color: #AA0000">', '&5' => '<span style="color: #AA00AA">',
|
||
'&6' => '<span style="color: #FFAA00">', '&7' => '<span style="color: #AAAAAA">',
|
||
'&8' => '<span style="color: #555555">', '&9' => '<span style="color: #5555FF">',
|
||
'&a' => '<span style="color: #55FF55">', '&b' => '<span style="color: #55FFFF">',
|
||
'&c' => '<span style="color: #FF5555">', '&d' => '<span style="color: #FF55FF">',
|
||
'&e' => '<span style="color: #FFFF55">', '&f' => '<span style="color: #FFFFFF">',
|
||
'&l' => '<span style="font-weight: bold">', '&m' => '<span style="text-decoration: line-through">',
|
||
'&n' => '<span style="text-decoration: underline">', '&o' => '<span style="font-style: italic">',
|
||
'&r' => '</span>',
|
||
);
|
||
foreach ( $map as $code => $html ) {
|
||
if ( $code === '&r' ) {
|
||
$text = str_replace( $code, $html . '</span>', $text );
|
||
} else {
|
||
$text = str_replace( $code, $html, $text );
|
||
}
|
||
}
|
||
return $text;
|
||
}
|
||
|
||
/**
|
||
* Erkennt Bedrock-Spieler robust aus API-Objekt, UUID/XUID und Namen.
|
||
*/
|
||
function mcph_detect_bedrock_player( $player, $uuid = '', $name = '' ) {
|
||
if ( is_object( $player ) ) {
|
||
$flag_keys = array( 'isBedrock', 'is_bedrock', 'bedrock', 'floodgate' );
|
||
foreach ( $flag_keys as $key ) {
|
||
if ( isset( $player->$key ) ) {
|
||
$value = $player->$key;
|
||
if ( is_bool( $value ) ) {
|
||
return $value ? 1 : 0;
|
||
}
|
||
if ( is_numeric( $value ) ) {
|
||
return intval( $value ) > 0 ? 1 : 0;
|
||
}
|
||
if ( is_string( $value ) ) {
|
||
$normalized = strtolower( trim( $value ) );
|
||
if ( in_array( $normalized, array( '1', 'true', 'yes', 'y', 'on' ), true ) ) {
|
||
return 1;
|
||
}
|
||
if ( in_array( $normalized, array( '0', 'false', 'no', 'n', 'off' ), true ) ) {
|
||
return 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if ( isset( $player->xuid ) && ! empty( $player->xuid ) ) {
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
$uuid = strtolower( (string) $uuid );
|
||
if ( strpos( $uuid, 'xuid' ) === 0 || strpos( $uuid, 'floodgate' ) === 0 ) {
|
||
return 1;
|
||
}
|
||
|
||
// Java-Namen enthalten keinen Punkt; viele Bedrock-Setups nutzen Punkt-Präfix.
|
||
$name = trim( (string) $name );
|
||
if ( strpos( $name, '.' ) !== false ) {
|
||
return 1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* 1. Tabelle beim Aktivieren anlegen + NEUE FELDER
|
||
*/
|
||
register_activation_hook( __FILE__, 'mcph_install' );
|
||
function mcph_install() {
|
||
global $wpdb, $mc_player_history_db_version;
|
||
$table_name = $wpdb->prefix . 'mc_players';
|
||
$charset_collate = $wpdb->get_charset_collate();
|
||
|
||
$sql = "CREATE TABLE $table_name (
|
||
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||
uuid varchar(64) NOT NULL,
|
||
username varchar(64) NOT NULL,
|
||
prefix varchar(255) DEFAULT NULL,
|
||
first_seen datetime NOT NULL,
|
||
last_seen datetime NOT NULL,
|
||
is_online tinyint(1) NOT NULL DEFAULT 0,
|
||
playtime_seconds INT NOT NULL DEFAULT 0,
|
||
kills INT NOT NULL DEFAULT 0,
|
||
deaths INT NOT NULL DEFAULT 0,
|
||
balance DOUBLE NOT NULL DEFAULT 0,
|
||
is_bedrock tinyint(1) NOT NULL DEFAULT 0,
|
||
joins INT NOT NULL DEFAULT 0,
|
||
bans INT NOT NULL DEFAULT 0,
|
||
mutes INT NOT NULL DEFAULT 0,
|
||
warns INT NOT NULL DEFAULT 0,
|
||
PRIMARY KEY (id),
|
||
UNIQUE KEY uuid (uuid),
|
||
KEY username (username),
|
||
KEY is_online (is_online),
|
||
KEY playtime_seconds (playtime_seconds)
|
||
) $charset_collate;";
|
||
|
||
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
|
||
dbDelta( $sql );
|
||
update_option( 'mc_player_history_db_version', $mc_player_history_db_version );
|
||
}
|
||
|
||
register_deactivation_hook( __FILE__, 'mcph_deactivate' );
|
||
function mcph_deactivate() {
|
||
wp_clear_scheduled_hook( 'mcph_sync_event' );
|
||
}
|
||
|
||
/**
|
||
* Erweiterung der Datenbank beim Laden (falls Update)
|
||
*/
|
||
add_action( 'init', 'mcph_add_columns_v130' );
|
||
function mcph_add_columns_v130() {
|
||
global $wpdb;
|
||
$table_name = $wpdb->prefix . 'mc_players';
|
||
|
||
// Playtime
|
||
if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'playtime_seconds'" ) ) {
|
||
$wpdb->query( "ALTER TABLE $table_name ADD COLUMN playtime_seconds INT NOT NULL DEFAULT 0 AFTER last_seen" );
|
||
}
|
||
// Kills
|
||
if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'kills'" ) ) {
|
||
$wpdb->query( "ALTER TABLE $table_name ADD COLUMN kills INT NOT NULL DEFAULT 0 AFTER playtime_seconds" );
|
||
}
|
||
// Deaths
|
||
if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'deaths'" ) ) {
|
||
$wpdb->query( "ALTER TABLE $table_name ADD COLUMN deaths INT NOT NULL DEFAULT 0 AFTER kills" );
|
||
}
|
||
// Balance
|
||
if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'balance'" ) ) {
|
||
$wpdb->query( "ALTER TABLE $table_name ADD COLUMN balance DOUBLE NOT NULL DEFAULT 0 AFTER deaths" );
|
||
}
|
||
// Is Bedrock
|
||
if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'is_bedrock'" ) ) {
|
||
$wpdb->query( "ALTER TABLE $table_name ADD COLUMN is_bedrock tinyint(1) NOT NULL DEFAULT 0 AFTER balance" );
|
||
}
|
||
// Joins
|
||
if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'joins'" ) ) {
|
||
$wpdb->query( "ALTER TABLE $table_name ADD COLUMN joins INT NOT NULL DEFAULT 0 AFTER is_bedrock" );
|
||
}
|
||
// Bans
|
||
if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'bans'" ) ) {
|
||
$wpdb->query( "ALTER TABLE $table_name ADD COLUMN bans INT NOT NULL DEFAULT 0 AFTER joins" );
|
||
}
|
||
// Mutes
|
||
if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'mutes'" ) ) {
|
||
$wpdb->query( "ALTER TABLE $table_name ADD COLUMN mutes INT NOT NULL DEFAULT 0 AFTER bans" );
|
||
}
|
||
// Warns
|
||
if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'warns'" ) ) {
|
||
$wpdb->query( "ALTER TABLE $table_name ADD COLUMN warns INT NOT NULL DEFAULT 0 AFTER mutes" );
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 2. Cron-Job
|
||
*/
|
||
add_action( 'init', 'mcph_setup_schedule' );
|
||
function mcph_setup_schedule() {
|
||
if ( ! wp_next_scheduled( 'mcph_sync_event' ) ) {
|
||
wp_schedule_event( time(), 'mcph_2min', 'mcph_sync_event' );
|
||
}
|
||
}
|
||
add_filter( 'cron_schedules', 'mcph_add_cron_interval' );
|
||
function mcph_add_cron_interval( $schedules ) {
|
||
$schedules['mcph_2min'] = array( 'interval' => 2 * MINUTE_IN_SECONDS, 'display' => 'Alle 2 Minuten' );
|
||
return $schedules;
|
||
}
|
||
|
||
add_action( 'mcph_sync_event', 'mcph_sync_from_statusapi' );
|
||
function mcph_sync_from_statusapi() {
|
||
$api_url = get_option( 'mcph_statusapi_url' );
|
||
if ( ! empty( $api_url ) && strpos( $api_url, 'http' ) !== 0 ) { $api_url = 'http://' . $api_url; }
|
||
if ( empty( $api_url ) ) { $api_url = 'http://localhost:9191'; }
|
||
|
||
$response = wp_remote_get( $api_url, array( 'timeout' => 5 ) );
|
||
if ( is_wp_error( $response ) ) { return; }
|
||
$json = json_decode( wp_remote_retrieve_body( $response ) );
|
||
if ( ! $json ) { return; }
|
||
|
||
global $wpdb;
|
||
$table_name = $wpdb->prefix . 'mc_players';
|
||
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_name'" ) != $table_name ) { return; }
|
||
|
||
// Alle Offline setzen
|
||
$wpdb->query( "UPDATE $table_name SET is_online = 0" );
|
||
$players = isset( $json->players ) && is_array( $json->players ) ? $json->players : array();
|
||
|
||
foreach ( $players as $p ) {
|
||
$name = isset( $p->name ) ? sanitize_text_field( $p->name ) : '';
|
||
$prefix = isset( $p->prefix ) ? sanitize_text_field( $p->prefix ) : '';
|
||
|
||
// Basis Stats
|
||
$playtime = isset( $p->playtime ) ? intval( $p->playtime ) : 0;
|
||
|
||
// Economy
|
||
$balance = 0;
|
||
if ( isset( $p->economy ) && is_object( $p->economy ) && isset( $p->economy->balance ) ) {
|
||
$balance = floatval( $p->economy->balance );
|
||
}
|
||
|
||
// Kills/Deaths (Fallback wenn nicht in API)
|
||
$kills = isset( $p->kills ) ? intval( $p->kills ) : 0;
|
||
$deaths = isset( $p->deaths ) ? intval( $p->deaths ) : 0;
|
||
|
||
// NEU: Weitere Stats
|
||
$joins = isset( $p->joins ) ? intval( $p->joins ) : 0;
|
||
|
||
// UUID
|
||
if ( isset( $p->uuid ) && !empty( $p->uuid ) ) {
|
||
$uuid = sanitize_text_field( $p->uuid );
|
||
} else {
|
||
$uuid = 'legacy-' . md5( strtolower( trim( $name ) ) );
|
||
}
|
||
|
||
$is_bedrock = mcph_detect_bedrock_player( $p, $uuid, $name );
|
||
|
||
$bans = 0;
|
||
$mutes = 0;
|
||
$warns = 0;
|
||
|
||
// Primär aus StatusAPI lesen (falls geliefert).
|
||
if ( isset( $p->punishments ) && is_object( $p->punishments ) ) {
|
||
$bans = isset( $p->punishments->bans ) ? intval( $p->punishments->bans ) : 0;
|
||
$mutes = isset( $p->punishments->mutes ) ? intval( $p->punishments->mutes ) : 0;
|
||
$warns = isset( $p->punishments->warns ) ? intval( $p->punishments->warns ) : 0;
|
||
}
|
||
|
||
// Falls LiteBans konfiguriert ist, dortige Werte bevorzugen.
|
||
$litebans_punishments = mcph_get_litebans_punishments( $uuid, $name );
|
||
if ( is_array( $litebans_punishments ) ) {
|
||
$bans = intval( $litebans_punishments['bans'] ?? 0 );
|
||
$mutes = intval( $litebans_punishments['mutes'] ?? 0 );
|
||
$warns = intval( $litebans_punishments['warns'] ?? 0 );
|
||
}
|
||
|
||
if ( empty( $name ) ) continue;
|
||
|
||
$exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table_name WHERE uuid = %s", $uuid ) );
|
||
|
||
// Fallback alte Einträge
|
||
if ( ! $exists ) {
|
||
$old_entry = $wpdb->get_row( $wpdb->prepare( "SELECT id, uuid FROM $table_name WHERE username = %s", $name ) );
|
||
if ( $old_entry ) {
|
||
$update_data = array(
|
||
'uuid' => $uuid,
|
||
'last_seen' => current_time( 'mysql' ),
|
||
'is_online' => 1,
|
||
'playtime_seconds' => $playtime,
|
||
'balance' => $balance,
|
||
'is_bedrock' => $is_bedrock,
|
||
'joins' => $joins,
|
||
'bans' => $bans,
|
||
'mutes' => $mutes,
|
||
'warns' => $warns
|
||
);
|
||
$update_format = array( '%s', '%s', '%d', '%d', '%s', '%d', '%d', '%d', '%d', '%d' );
|
||
|
||
if ( ! empty( $prefix ) ) {
|
||
$update_data['prefix'] = $prefix;
|
||
$update_format[] = '%s';
|
||
}
|
||
$wpdb->update( $table_name, $update_data, array( 'id' => $old_entry->id ), $update_format, array( '%d' ) );
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Zeitkonvertierung
|
||
$last_seen_time = isset( $p->last_seen ) ? intval( $p->last_seen ) : time();
|
||
$last_seen_mysql = date( 'Y-m-d H:i:s', $last_seen_time );
|
||
|
||
$first_seen_mysql = current_time('mysql');
|
||
if ( isset( $p->first_seen ) && ! $exists ) {
|
||
$first_seen_mysql = date( 'Y-m-d H:i:s', intval( $p->first_seen ) );
|
||
}
|
||
|
||
if ( $exists ) {
|
||
$update_data = array(
|
||
'username' => $name,
|
||
'last_seen' => $last_seen_mysql,
|
||
'is_online' => 1,
|
||
'playtime_seconds' => $playtime,
|
||
'balance' => $balance,
|
||
'is_bedrock' => $is_bedrock,
|
||
'joins' => $joins,
|
||
'bans' => $bans,
|
||
'mutes' => $mutes,
|
||
'warns' => $warns
|
||
);
|
||
$update_format = array( '%s', '%s', '%d', '%d', '%s', '%d', '%d', '%d', '%d', '%d' );
|
||
|
||
if ( ! empty( $prefix ) ) {
|
||
$update_data['prefix'] = $prefix;
|
||
$update_format[] = '%s';
|
||
}
|
||
$wpdb->update( $table_name, $update_data, array( 'uuid' => $uuid ), $update_format, array( '%s' ) );
|
||
} else {
|
||
$wpdb->insert( $table_name, array(
|
||
'uuid' => $uuid,
|
||
'username' => $name,
|
||
'prefix' => $prefix,
|
||
'first_seen' => $first_seen_mysql,
|
||
'last_seen' => $last_seen_mysql,
|
||
'is_online' => 1,
|
||
'playtime_seconds' => $playtime,
|
||
'balance' => $balance,
|
||
'is_bedrock' => $is_bedrock,
|
||
'joins' => $joins,
|
||
'bans' => $bans,
|
||
'mutes' => $mutes,
|
||
'warns' => $warns
|
||
), array( '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%d', '%d', '%d', '%d', '%d' ) );
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 3. AJAX Handler
|
||
*/
|
||
add_action( 'wp_ajax_mcph_refresh', 'mcph_ajax_refresh' );
|
||
add_action( 'wp_ajax_nopriv_mcph_refresh', 'mcph_ajax_refresh' );
|
||
function mcph_ajax_refresh() {
|
||
$limit = isset( $_POST['limit'] ) ? intval( $_POST['limit'] ) : 500;
|
||
$only_online = isset( $_POST['only_online'] ) && $_POST['only_online'] === 'true';
|
||
$html = mcph_generate_player_html( $limit, $only_online, true );
|
||
wp_send_json_success( array( 'html' => $html ) );
|
||
}
|
||
|
||
/**
|
||
* Holt StatusAPI-Spieler (optional mit Refresh).
|
||
*/
|
||
function mcph_fetch_statusapi_players( $force_refresh = false ) {
|
||
$cache_key = 'mcph_api_cache_data';
|
||
if ( $force_refresh ) {
|
||
delete_transient( $cache_key );
|
||
}
|
||
|
||
$api_response = get_transient( $cache_key );
|
||
if ( false !== $api_response ) {
|
||
return $api_response;
|
||
}
|
||
|
||
$api_url = get_option( 'mcph_statusapi_url' );
|
||
if ( ! empty( $api_url ) && strpos( $api_url, 'http' ) !== 0 ) {
|
||
$api_url = 'http://' . $api_url;
|
||
}
|
||
if ( empty( $api_url ) ) {
|
||
$api_url = 'http://localhost:9191';
|
||
}
|
||
|
||
$response = wp_remote_get( $api_url, array( 'timeout' => 2 ) );
|
||
if ( is_wp_error( $response ) ) {
|
||
return false;
|
||
}
|
||
|
||
$body = wp_remote_retrieve_body( $response );
|
||
$json = json_decode( $body );
|
||
if ( $json && isset( $json->players ) && is_array( $json->players ) ) {
|
||
set_transient( $cache_key, $json->players, 2 );
|
||
return $json->players;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 3b. AJAX Handler: Spielerprofil (Erweitert)
|
||
*/
|
||
/**
|
||
* Forum-Banner: Holt Forumsdaten anhand der MC-UUID.
|
||
* Erfordert WP Business Forum Plugin (WBF_DB, WBF_Levels).
|
||
*/
|
||
function mcph_get_forum_banner( $uuid ) {
|
||
if ( ! class_exists( 'WBF_DB' ) || ! class_exists( 'WBF_Levels' ) ) return null;
|
||
global $wpdb;
|
||
$user_id = $wpdb->get_var( $wpdb->prepare(
|
||
"SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = 'mc_uuid' AND meta_value = %s LIMIT 1",
|
||
$uuid
|
||
) );
|
||
if ( ! $user_id ) return null;
|
||
$forum_user = WBF_DB::get_user( (int) $user_id );
|
||
if ( ! $forum_user ) return null;
|
||
$post_count = intval( $forum_user->post_count );
|
||
return array(
|
||
'display_name' => $forum_user->display_name,
|
||
'avatar_url' => $forum_user->avatar_url,
|
||
'banner_url' => $forum_user->banner_url ?? '',
|
||
'role' => $forum_user->role,
|
||
'post_count' => $post_count,
|
||
'registered' => $forum_user->registered,
|
||
'last_active' => $forum_user->last_active,
|
||
'bio' => $forum_user->bio,
|
||
'level_badge_html' => WBF_Levels::is_enabled() ? WBF_Levels::badge( $post_count ) : '',
|
||
'level_progress_html' => WBF_Levels::is_enabled() ? WBF_Levels::progress_bar( $post_count ) : '',
|
||
);
|
||
}
|
||
|
||
add_action( 'wp_ajax_mcph_player_profile', 'mcph_ajax_player_profile' );
|
||
add_action( 'wp_ajax_nopriv_mcph_player_profile', 'mcph_ajax_player_profile' );
|
||
function mcph_ajax_player_profile() {
|
||
$uuid = isset( $_POST['uuid'] ) ? sanitize_text_field( $_POST['uuid'] ) : '';
|
||
if ( empty( $uuid ) ) { wp_send_json_error( 'Kein UUID angegeben.' ); return; }
|
||
|
||
global $wpdb;
|
||
$table_name = $wpdb->prefix . 'mc_players';
|
||
$player = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE uuid = %s", $uuid ) );
|
||
|
||
if ( ! $player ) { wp_send_json_error( 'Spieler nicht gefunden.' ); return; }
|
||
|
||
// Live-Status prüfen
|
||
$is_online = (bool) $player->is_online;
|
||
$is_bedrock = (bool) $player->is_bedrock;
|
||
$balance_value = floatval( $player->balance );
|
||
$api_response = mcph_fetch_statusapi_players( true );
|
||
if ( is_array( $api_response ) ) {
|
||
$live_uuids = array();
|
||
$matched_live_player = null;
|
||
foreach ( $api_response as $p ) {
|
||
$candidate_uuid = '';
|
||
if ( isset( $p->uuid ) && ! empty( $p->uuid ) ) {
|
||
$candidate_uuid = sanitize_text_field( $p->uuid );
|
||
} elseif ( isset( $p->name ) ) {
|
||
$candidate_uuid = 'legacy-' . md5( strtolower( trim( $p->name ) ) );
|
||
}
|
||
|
||
if ( ! empty( $candidate_uuid ) ) {
|
||
$live_uuids[ $candidate_uuid ] = true;
|
||
if ( $candidate_uuid === $player->uuid ) {
|
||
$matched_live_player = $p;
|
||
}
|
||
}
|
||
|
||
if ( ! $matched_live_player && isset( $p->name ) && strtolower( trim( (string) $p->name ) ) === strtolower( trim( (string) $player->username ) ) ) {
|
||
$matched_live_player = $p;
|
||
}
|
||
}
|
||
$is_online = isset( $live_uuids[ $player->uuid ] );
|
||
|
||
// Live-Daten bevorzugen, falls verfügbar.
|
||
if ( $matched_live_player ) {
|
||
$is_bedrock = (bool) mcph_detect_bedrock_player( $matched_live_player, $player->uuid, $player->username );
|
||
if ( isset( $matched_live_player->economy ) && is_object( $matched_live_player->economy ) && isset( $matched_live_player->economy->balance ) ) {
|
||
$balance_value = floatval( $matched_live_player->economy->balance );
|
||
}
|
||
}
|
||
}
|
||
|
||
$kd = ( $player->deaths > 0 ) ? round( $player->kills / $player->deaths, 2 ) : ( $player->kills > 0 ? $player->kills : 0 );
|
||
|
||
wp_send_json_success( array(
|
||
'uuid' => $player->uuid,
|
||
'username' => $player->username,
|
||
'prefix' => $player->prefix ?? '',
|
||
'first_seen' => $player->first_seen,
|
||
'last_seen' => $player->last_seen,
|
||
'is_online' => $is_online,
|
||
'playtime_seconds' => intval( $player->playtime_seconds ),
|
||
'kills' => intval( $player->kills ),
|
||
'deaths' => intval( $player->deaths ),
|
||
'kd' => $kd,
|
||
'balance' => $balance_value,
|
||
'is_bedrock' => $is_bedrock,
|
||
'joins' => intval( $player->joins ),
|
||
'bans' => intval( $player->bans ),
|
||
'mutes' => intval( $player->mutes ),
|
||
'warns' => intval( $player->warns ),
|
||
'forum_banner' => mcph_get_forum_banner( $player->uuid ),
|
||
) );
|
||
}
|
||
|
||
/**
|
||
* 4. HTML Generator (Karten-Ansicht)
|
||
*/
|
||
function mcph_generate_player_html( $limit = 500, $only_online = false, $is_ajax = false ) {
|
||
global $wpdb;
|
||
$table_name = $wpdb->prefix . 'mc_players';
|
||
|
||
// Live Cache
|
||
$api_response = mcph_fetch_statusapi_players( $is_ajax );
|
||
|
||
$live_online_uuids = array();
|
||
$has_live_data = false;
|
||
if ( is_array( $api_response ) && ! empty( $api_response ) ) {
|
||
$has_live_data = true;
|
||
foreach ( $api_response as $p ) {
|
||
if ( isset( $p->name ) ) {
|
||
$uuid = ( isset( $p->uuid ) && !empty( $p->uuid ) ) ? sanitize_text_field( $p->uuid ) : 'legacy-' . md5( strtolower( trim( $p->name ) ) );
|
||
$live_online_uuids[ $uuid ] = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
$sql_limit = $only_online ? ( $limit * 2 ) : $limit;
|
||
$rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_name ORDER BY last_seen DESC LIMIT %d", $sql_limit ) );
|
||
|
||
if ( empty( $rows ) ) {
|
||
return '<p style="text-align:center; color:#777;">Keine Spieler gefunden.</p>';
|
||
}
|
||
|
||
ob_start();
|
||
echo '<div class="mc-grid">';
|
||
$displayed_count = 0;
|
||
|
||
foreach ( $rows as $row ) {
|
||
$is_online = $has_live_data ? isset( $live_online_uuids[ $row->uuid ] ) : ( $row->is_online == 1 );
|
||
if ( $only_online && ! $is_online ) continue;
|
||
if ( $displayed_count >= $limit ) break;
|
||
|
||
$displayed_count++;
|
||
$username = esc_html( $row->username );
|
||
$prefix = mcph_parse_minecraft_colors( $row->prefix );
|
||
|
||
// Avatar
|
||
if ( strpos( $username, '.' ) !== false || ( isset($row->uuid) && strpos($row->uuid, 'xuid') === 0 ) ) {
|
||
$avatar = isset($row->uuid) && !empty($row->uuid) ? 'https://mc-heads.net/head/' . esc_attr($row->uuid) . '/100' : 'https://mc-heads.net/head/' . $username . '/100';
|
||
} else {
|
||
$avatar = 'https://mc-heads.net/head/' . $username . '/100';
|
||
}
|
||
|
||
$anim_style = ! $is_ajax ? 'animation: fadeInUp 0.5s ease forwards; opacity: 0; animation-delay: ' . ( $displayed_count * 0.05 ) . 's;' : '';
|
||
|
||
if ( $is_online ) {
|
||
$status_html = '<span class="mc-status mc-online">Online</span>';
|
||
} else {
|
||
$time = strtotime( $row->last_seen );
|
||
$date_str = ( date('d.m.Y', $time) == date('d.m.Y') ) ? date('H:i', $time) : date('d.m.Y H:i', $time);
|
||
$status_html = '<span class="mc-status mc-offline">Zuletzt: ' . $date_str . '</span>';
|
||
}
|
||
|
||
echo '<div class="mc-player-card" data-uuid="' . esc_attr( $row->uuid ) . '" data-username="' . esc_attr( $row->username ) . '" style="' . $anim_style . '">';
|
||
echo '<img src="' . $avatar . '" class="mc-avatar" alt="' . $username . '" loading="lazy">';
|
||
echo '<div class="mc-info-stack">';
|
||
if ( ! empty( $row->prefix ) ) echo '<span class="mc-prefix">' . $prefix . '</span>';
|
||
echo '<span class="mc-name">' . $username . '</span>';
|
||
echo '<div class="mc-spacer"></div>';
|
||
echo '<div class="mc-status-line">' . $status_html . '</div>';
|
||
echo '</div></div>';
|
||
}
|
||
echo '</div>';
|
||
echo '<div class="mc-update-time">Aktualisiert: ' . current_time('H:i:s') . '</div>';
|
||
return ob_get_clean();
|
||
}
|
||
|
||
/**
|
||
* 5. Admin Menü
|
||
*/
|
||
add_action( 'admin_menu', 'mcph_admin_menu' );
|
||
function mcph_admin_menu() {
|
||
add_options_page( 'MC Player History', 'MC Player History', 'manage_options', 'mc_player_history', 'mcph_options_page' );
|
||
}
|
||
add_action( 'admin_init', 'mcph_settings_init' );
|
||
function mcph_settings_init() {
|
||
register_setting( 'mcph_plugin_options', 'mcph_statusapi_url' );
|
||
}
|
||
function mcph_options_page() {
|
||
if ( isset( $_POST['mcph_cleanup_duplicates'] ) && check_admin_referer( 'mcph_cleanup_action' ) ) {
|
||
global $wpdb; $table_name = $wpdb->prefix . 'mc_players';
|
||
$duplicates = $wpdb->get_results( "SELECT username, COUNT(*) as count, GROUP_CONCAT(id ORDER BY last_seen DESC) as ids FROM $table_name GROUP BY username HAVING count > 1" );
|
||
$cleaned = 0;
|
||
foreach ( $duplicates as $dup ) {
|
||
$ids = explode( ',', $dup->ids ); array_shift( $ids );
|
||
foreach ( $ids as $id ) { $wpdb->delete( $table_name, array( 'id' => $id ), array( '%d' ) ); $cleaned++; }
|
||
}
|
||
echo '<div class="notice notice-success"><p>' . $cleaned . ' Duplikate wurden entfernt.</p></div>';
|
||
}
|
||
if ( isset( $_POST['mcph_manual_sync'] ) && check_admin_referer( 'mcph_manual_sync_action' ) ) {
|
||
mcph_sync_from_statusapi();
|
||
delete_transient( 'mcph_api_cache_data' );
|
||
echo '<div class="notice notice-success"><p>Manueller Sync ausgeführt.</p></div>';
|
||
}
|
||
?>
|
||
<div class="wrap">
|
||
<h1>MC Player History Einstellungen</h1>
|
||
<form method="post" action="options.php">
|
||
<?php settings_fields( 'mcph_plugin_options' ); do_settings_sections( 'mcph_plugin_options' ); ?>
|
||
<table class="form-table">
|
||
<tr valign="top">
|
||
<th scope="row">StatusAPI URL</th>
|
||
<td>
|
||
<input type="text" name="mcph_statusapi_url" value="<?php echo esc_attr( get_option( 'mcph_statusapi_url' ) ); ?>" class="regular-text" placeholder="http://localhost:9191" />
|
||
<p class="description">Bitte unbedingt mit http:// angeben.</p>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<?php submit_button(); ?>
|
||
</form>
|
||
<h2>Duplikate bereinigen</h2>
|
||
<form method="post"><?php wp_nonce_field( 'mcph_cleanup_action' ); ?>
|
||
<p class="description">Entfernt doppelte Spieler-Einträge aus der Datenbank.</p>
|
||
<input type="submit" name="mcph_cleanup_duplicates" class="button button-secondary" value="Duplikate jetzt entfernen" />
|
||
</form>
|
||
<h2>Manueller Sync</h2>
|
||
<form method="post"><?php wp_nonce_field( 'mcph_manual_sync_action' ); ?>
|
||
<input type="submit" name="mcph_manual_sync" class="button" value="Jetzt synchronisieren" />
|
||
</form>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
/**
|
||
* 6. Shortcode + CSS + JS (Profil erweitert)
|
||
*/
|
||
add_shortcode( 'mc_player_history', 'mcph_shortcode' );
|
||
function mcph_shortcode( $atts ) {
|
||
$atts = shortcode_atts( array( 'limit' => 500, 'interval' => 2, 'only_online' => 'false' ), $atts );
|
||
$container_id = 'mc-player-wrapper-' . uniqid();
|
||
ob_start();
|
||
echo '<div id="' . $container_id . '" class="mc-player-list">';
|
||
echo mcph_generate_player_html( $atts['limit'], $atts['only_online'] === 'true', false );
|
||
echo '</div>';
|
||
$profile_id = 'mc-profile-' . substr( $container_id, -6 );
|
||
echo '<div id="' . $profile_id . '" class="mc-profile-panel" style="display:none;"></div>';
|
||
?>
|
||
<script>
|
||
(function() {
|
||
const intervalSeconds = Math.max(2, <?php echo intval( $atts['interval'] ); ?>);
|
||
const wrapperId = "<?php echo $container_id; ?>";
|
||
const profileId = "<?php echo $profile_id; ?>";
|
||
const limit = <?php echo intval( $atts['limit'] ); ?>;
|
||
const onlyOnline = "<?php echo ( $atts['only_online'] === 'true' ) ? 'true' : 'false'; ?>";
|
||
const ajaxUrl = '<?php echo admin_url('admin-ajax.php'); ?>';
|
||
|
||
function formatPlaytime(seconds) {
|
||
seconds = parseInt(seconds) || 0;
|
||
if (seconds < 60) return seconds + 's';
|
||
if (seconds < 3600) return Math.floor(seconds/60) + 'm';
|
||
if (seconds < 86400) {
|
||
const h = Math.floor(seconds/3600);
|
||
const m = Math.floor((seconds%3600)/60);
|
||
return h + 'h ' + m + 'm';
|
||
}
|
||
const d = Math.floor(seconds/86400);
|
||
const h = Math.floor((seconds%86400)/3600);
|
||
return d + 'd ' + h + 'h';
|
||
}
|
||
|
||
function formatDate(dateStr) {
|
||
if (!dateStr) return '—';
|
||
const d = new Date(dateStr.replace(' ', 'T'));
|
||
const now = new Date();
|
||
const today = now.toDateString() === d.toDateString();
|
||
if (today) return d.toLocaleTimeString('de-DE', {hour:'2-digit', minute:'2-digit'});
|
||
return d.toLocaleDateString('de-DE', {day:'2-digit', month:'2-digit', year:'numeric'})
|
||
+ ' ' + d.toLocaleTimeString('de-DE', {hour:'2-digit', minute:'2-digit'});
|
||
}
|
||
|
||
function parseMinecraftColors(text) {
|
||
if (!text) return '';
|
||
const colorMap = {
|
||
'0':'#000000','1':'#0000AA','2':'#00AA00','3':'#00AAAA',
|
||
'4':'#AA0000','5':'#AA00AA','6':'#FFAA00','7':'#AAAAAA',
|
||
'8':'#555555','9':'#5555FF','a':'#55FF55','b':'#55FFFF',
|
||
'c':'#FF5555','d':'#FF55FF','e':'#FFFF55','f':'#FFFFFF'
|
||
};
|
||
text = text.replace(/§/g, '&');
|
||
let out = '', open = false;
|
||
const parts = text.split(/(&[0-9a-frk-orA-FK-OR])/);
|
||
for (const part of parts) {
|
||
const m = part.match(/^&([0-9a-frk-orA-FK-OR])$/i);
|
||
if (m) {
|
||
const code = m[1].toLowerCase();
|
||
if (code === 'r') {
|
||
if (open) { out += '</span>'; open = false; }
|
||
} else if (colorMap[code]) {
|
||
if (open) { out += '</span>'; open = false; }
|
||
out += `<span style="color:${colorMap[code]}">`;
|
||
open = true;
|
||
}
|
||
} else {
|
||
out += part.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
}
|
||
if (open) out += '</span>';
|
||
return out;
|
||
}
|
||
|
||
function statCard(icon, label, value, highlight) {
|
||
const cls = highlight ? 'mc-stat-card mc-stat-highlight' : 'mc-stat-card';
|
||
return `<div class="${cls}">
|
||
<div class="mc-stat-icon">${icon}</div>
|
||
<div class="mc-stat-value">${value}</div>
|
||
<div class="mc-stat-label">${label}</div>
|
||
</div>`;
|
||
}
|
||
|
||
let activeProfileUuid = null;
|
||
let activeProfileUsername = null;
|
||
|
||
function showProfile(uuid, username, options) {
|
||
const opts = options || {};
|
||
const isSilent = opts.silent === true;
|
||
const wrapper = document.getElementById(wrapperId);
|
||
const panel = document.getElementById(profileId);
|
||
if (!wrapper || !panel) return;
|
||
|
||
if (isSilent) {
|
||
if (panel.style.display === 'none' || panel.style.display === '') return;
|
||
} else {
|
||
panel.innerHTML = '<div class="mc-profile-loading"><div class="mc-spinner"></div><span>Lade Spielerprofil…</span></div>';
|
||
wrapper.style.display = 'none';
|
||
panel.style.display = 'block';
|
||
panel.style.opacity = '0';
|
||
requestAnimationFrame(() => { panel.style.transition = 'opacity 0.3s'; panel.style.opacity = '1'; });
|
||
}
|
||
|
||
activeProfileUuid = uuid;
|
||
activeProfileUsername = username || '';
|
||
|
||
const formData = new FormData();
|
||
formData.append('action', 'mcph_player_profile');
|
||
formData.append('uuid', uuid);
|
||
|
||
fetch(ajaxUrl, { method: 'POST', body: formData })
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (!data.success) { panel.innerHTML = '<p style="color:red;text-align:center">Fehler beim Laden.</p>'; return; }
|
||
const p = data.data;
|
||
|
||
const avatarHead = `https://mc-heads.net/head/${encodeURIComponent(p.uuid)}/120`;
|
||
const avatarBody = `https://mc-heads.net/body/${encodeURIComponent(p.uuid)}/100`;
|
||
const prefixHtml = parseMinecraftColors(p.prefix);
|
||
const onlineHtml = p.is_online
|
||
? '<span class="mc-status mc-online">🟢 Online</span>'
|
||
: '<span class="mc-status mc-offline">🔴 Offline</span>';
|
||
|
||
const kd = (p.deaths > 0) ? (p.kills / p.deaths).toFixed(2) : (p.kills > 0 ? p.kills : '—');
|
||
|
||
const balanceFormatted = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'USD' }).format(p.balance);
|
||
|
||
// NEU: Icons und Text für neue Stats
|
||
const platformIcon = p.is_bedrock ? '🪨' : '☕'; // Stein für Bedrock, Tasse für Java
|
||
const platformLabel = p.is_bedrock ? 'Bedrock' : 'Java';
|
||
|
||
// Forum-Banner (nur wenn Forum-Plugin aktiv + Spieler verknüpft)
|
||
let headerBannerClass = '';
|
||
let headerBannerStyle = '';
|
||
let headerBannerOverlay = '';
|
||
if (p.forum_banner && p.forum_banner.banner_url) {
|
||
headerBannerClass = ' mc-profile-header-banner';
|
||
headerBannerStyle = ` style="background-image:url('${p.forum_banner.banner_url}')"`;
|
||
headerBannerOverlay = '<div class="mc-profile-header-overlay"></div>';
|
||
}
|
||
|
||
panel.innerHTML = `
|
||
<div class="mc-profile-inner">
|
||
<button class="mc-back-btn" id="mc-back-${profileId}">← Zurück</button>
|
||
|
||
<div class="mc-profile-header${headerBannerClass}"${headerBannerStyle}>
|
||
${headerBannerOverlay}
|
||
<div class="mc-profile-header-left">
|
||
${prefixHtml ? `<div class="mc-profile-prefix">${prefixHtml}</div>` : ''}
|
||
<div class="mc-profile-name">${p.username}</div>
|
||
<div class="mc-profile-uuid">${p.uuid}</div>
|
||
<div class="mc-profile-status-row">
|
||
${onlineHtml}
|
||
<span class="mc-platform-badge">${platformIcon} ${platformLabel}</span>
|
||
</div>
|
||
</div>
|
||
<div class="mc-profile-header-right">
|
||
<img src="${avatarBody}" class="mc-profile-avatar" alt="${p.username}" onerror="this.src='${avatarHead}'">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mc-profile-divider"></div>
|
||
|
||
<div class="mc-stats-grid">
|
||
${statCard('⏱️', 'Spielzeit', formatPlaytime(p.playtime_seconds), true)}
|
||
${statCard('💰', 'Kontostand', balanceFormatted, true)}
|
||
${statCard('🚀', 'Joins', p.joins.toLocaleString('de-DE'), false)}
|
||
${statCard('⚔️', 'Kills', p.kills.toLocaleString('de-DE'), false)}
|
||
${statCard('💀', 'Tode', p.deaths.toLocaleString('de-DE'), false)}
|
||
${statCard('📊', 'K/D Ratio', kd, false)}
|
||
|
||
${statCard('🚫', 'Bans', p.bans, false)}
|
||
${statCard('🔇', 'Mutes', p.mutes, false)}
|
||
${statCard('⚠️', 'Warns', p.warns, false)}
|
||
|
||
${statCard('📅', 'Zuerst gesehen', formatDate(p.first_seen), false)}
|
||
${statCard('🕐', 'Zuletzt online', formatDate(p.last_seen), false)}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('mc-back-' + profileId).addEventListener('click', hideProfile);
|
||
})
|
||
.catch(() => { panel.innerHTML = '<p style="color:red;text-align:center">Verbindungsfehler.</p>'; });
|
||
}
|
||
|
||
function hideProfile() {
|
||
const wrapper = document.getElementById(wrapperId);
|
||
const panel = document.getElementById(profileId);
|
||
if (!wrapper || !panel) return;
|
||
activeProfileUuid = null;
|
||
activeProfileUsername = null;
|
||
panel.style.opacity = '0';
|
||
setTimeout(() => {
|
||
panel.style.display = 'none';
|
||
wrapper.style.display = '';
|
||
}, 280);
|
||
}
|
||
|
||
function refreshProfile() {
|
||
if (!activeProfileUuid) return;
|
||
showProfile(activeProfileUuid, activeProfileUsername, { silent: true });
|
||
}
|
||
|
||
function attachCardClicks(container) {
|
||
container.querySelectorAll('.mc-player-card').forEach(card => {
|
||
card.addEventListener('click', function() {
|
||
const uuid = this.dataset.uuid;
|
||
const username = this.dataset.username;
|
||
if (uuid) showProfile(uuid, username);
|
||
});
|
||
});
|
||
}
|
||
|
||
const wrapper = document.getElementById(wrapperId);
|
||
if (wrapper) attachCardClicks(wrapper);
|
||
|
||
function refreshList() {
|
||
fetch(ajaxUrl, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||
body: 'action=mcph_refresh&limit=' + limit + '&only_online=' + onlyOnline
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const w = document.getElementById(wrapperId);
|
||
if (w) {
|
||
w.innerHTML = data.data.html;
|
||
attachCardClicks(w);
|
||
}
|
||
}
|
||
})
|
||
.catch(error => console.error('MC Player History Error:', error));
|
||
}
|
||
setInterval(refreshList, intervalSeconds * 1000);
|
||
setInterval(refreshProfile, intervalSeconds * 1000);
|
||
})();
|
||
</script>
|
||
<?php
|
||
// CSS Erweitert für neues Grid
|
||
echo '<style>
|
||
@import url("https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;600;700&family=Barlow:wght@400;500;600&display=swap");
|
||
|
||
.mc-player-list, .mc-profile-panel {
|
||
--mc-bg: #1f232a;
|
||
--mc-surface: #2a2f37;
|
||
--mc-surface-2: #242a33;
|
||
--mc-ink: #e6f1ff;
|
||
--mc-muted: #9aa7b2;
|
||
--mc-border: #3a4450;
|
||
--mc-accent: #18c7ff;
|
||
--mc-accent-2: #00e5ff;
|
||
--mc-glow: 0 0 24px rgba(24, 199, 255, 0.28);
|
||
--mc-radius: 16px;
|
||
}
|
||
|
||
.mc-player-list { width: 100%; font-family: "Barlow", "Segoe UI", Arial, sans-serif; }
|
||
.mc-grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); justify-content: center; align-items: stretch; }
|
||
@keyframes fadeInUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
||
|
||
/* KARTE */
|
||
.mc-player-card {
|
||
position: relative;
|
||
display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 18px 12px;
|
||
background: linear-gradient(180deg, #2a2f37 0%, #242a33 100%);
|
||
border: 1px solid var(--mc-border);
|
||
border-radius: var(--mc-radius);
|
||
box-shadow: 0 10px 24px rgba(12, 16, 22, 0.35);
|
||
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
|
||
cursor: pointer; overflow: hidden;
|
||
}
|
||
.mc-player-card::before {
|
||
content: "";
|
||
position: absolute; inset: 0;
|
||
background: radial-gradient(220px 80px at 20% 0%, rgba(24,199,255,0.15), transparent 70%);
|
||
opacity: 0; transition: opacity 0.25s ease;
|
||
}
|
||
.mc-player-card:hover { transform: translateY(-6px); border-color: rgba(24,199,255,0.45); box-shadow: 0 16px 34px rgba(12,16,22,0.45); }
|
||
.mc-player-card:hover::before { opacity: 1; }
|
||
.mc-avatar {
|
||
width: 82px; height: 82px; border-radius: 12px; background: #11151b; padding: 4px;
|
||
border: 1px solid rgba(24,199,255,0.4);
|
||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.35);
|
||
transition: transform 0.25s ease; object-fit: contain;
|
||
}
|
||
.mc-player-card:hover .mc-avatar { transform: translateY(-4px) scale(1.05); }
|
||
.mc-info-stack { display: flex; flex-direction: column; align-items: center; width: 100%; flex-grow: 1; position: relative; padding-bottom: 34px; justify-content: flex-start; z-index: 1; }
|
||
.mc-prefix { font-size: 0.88em; display: block; width: 100%; text-align: center; margin-bottom: 4px; line-height: 1.2; }
|
||
.mc-name { font-weight: 700; font-size: 1.05em; color: var(--mc-ink); word-break: break-word; display: block; width: 100%; text-align: center; }
|
||
.mc-spacer { flex-grow: 1; }
|
||
.mc-status-line { position: absolute; bottom: 0; left: 0; width: 100%; text-align: center; margin: 0; padding-bottom: 5px; }
|
||
.mc-status { display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border-radius: 999px; font-size: 0.78em; font-weight: 700; letter-spacing: 0.2px; background: #1f2937; color: #e2e8f0; border: 1px solid #334155; }
|
||
.mc-online { background: rgba(24,199,255,0.15); color: #baf1ff; border: 1px solid rgba(24,199,255,0.5); box-shadow: var(--mc-glow); }
|
||
.mc-offline { background: rgba(248,113,113,0.14); color: #fecaca; border: 1px solid rgba(248,113,113,0.4); }
|
||
.mc-update-time { font-size: 0.82em; color: var(--mc-muted); text-align: center; margin-top: 20px; font-style: italic; padding: 8px; border: 1px dashed #3a4450; border-radius: 12px; }
|
||
|
||
@media (min-width: 1200px) { .mc-grid { grid-template-columns: repeat(6, 1fr); } }
|
||
@media (max-width: 600px) { .mc-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; } .mc-avatar { width: 70px; height: 70px; } .mc-name { font-size: 0.98em; } .mc-player-card { padding: 14px 8px; } }
|
||
|
||
/* ===== SPIELERPROFIL ===== */
|
||
.mc-profile-panel { width: 100%; font-family: "Barlow", "Segoe UI", Arial, sans-serif; }
|
||
.mc-profile-inner {
|
||
background: #2a2f37;
|
||
border: 1px solid #3a4450;
|
||
border-radius: 18px; padding: 26px;
|
||
box-shadow: 0 18px 40px rgba(10, 12, 16, 0.55);
|
||
}
|
||
.mc-back-btn {
|
||
display: inline-flex; align-items: center; gap: 6px; background: #1f232a;
|
||
border: 1.5px solid var(--mc-accent); color: #baf1ff; font-size: 0.9em; font-weight: 700;
|
||
padding: 6px 14px; border-radius: 999px; cursor: pointer; margin-bottom: 20px; transition: all 0.2s ease;
|
||
box-shadow: var(--mc-glow);
|
||
}
|
||
.mc-back-btn:hover { background: rgba(24,199,255,0.18); color: #e6f9ff; transform: translateX(-2px); }
|
||
.mc-profile-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; position: relative; overflow: hidden; border-radius: 14px; }
|
||
.mc-profile-header > * { position: relative; z-index: 1; }
|
||
.mc-profile-header::before {
|
||
content: ""; position: absolute; inset: 0;
|
||
background: linear-gradient(120deg, rgba(24,199,255,0.15), rgba(0,0,0,0.25));
|
||
opacity: 0.7; z-index: 0;
|
||
}
|
||
.mc-profile-header-banner { background-size: cover; background-position: center; padding: 20px 22px; border-radius: 14px; border: 1px solid rgba(24,199,255,0.3); }
|
||
.mc-profile-header-overlay { position: absolute; inset: 0; background: linear-gradient(90deg, rgba(8,12,18,0.45), rgba(8,12,18,0.7)); z-index: 0; }
|
||
.mc-profile-header-left { flex: 1; display: flex; flex-direction: column; gap: 6px; }
|
||
.mc-profile-prefix { font-size: 1em; font-weight: 600; line-height: 1.2; }
|
||
.mc-profile-name { font-size: 2.05em; font-weight: 700; color: #e6f1ff; line-height: 1.05; letter-spacing: 0.2px; font-family: "Rajdhani", "Segoe UI", Arial, sans-serif; }
|
||
.mc-profile-uuid { font-size: 0.72em; color: var(--mc-muted); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; word-break: break-all; margin-top: 2px; }
|
||
.mc-profile-status-row { margin-top: 8px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||
.mc-platform-badge { font-size: 0.85em; background: rgba(24,199,255,0.12); color: #baf1ff; padding: 4px 10px; border-radius: 999px; font-weight: 700; border: 1px solid rgba(24,199,255,0.3); display: inline-flex; align-items: center; gap: 4px; }
|
||
.mc-profile-header-right { flex-shrink: 0; }
|
||
.mc-profile-avatar { width: 110px; height: auto; display: block; filter: drop-shadow(0 12px 22px rgba(0, 0, 0, 0.5)); transition: transform 0.3s ease; transform: scaleX(-1); }
|
||
.mc-profile-avatar:hover { transform: scale(1.04); }
|
||
.mc-profile-divider { height: 1px; background: linear-gradient(90deg, transparent, rgba(24,199,255,0.35), transparent); margin: 22px 0; }
|
||
|
||
/* Grid: 4 Spalten fuer Desktop, 2 fuer Mobile */
|
||
.mc-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
||
.mc-stat-card {
|
||
background: #232832; border: 1px solid #34404c; border-radius: 12px; padding: 14px 10px; text-align: center;
|
||
box-shadow: 0 10px 22px rgba(8, 10, 14, 0.4); transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||
}
|
||
.mc-stat-card:hover { transform: translateY(-3px); border-color: rgba(24,199,255,0.45); box-shadow: 0 16px 28px rgba(10, 12, 16, 0.55); }
|
||
.mc-stat-highlight {
|
||
background: linear-gradient(135deg, rgba(24,199,255,0.35) 0%, rgba(0,229,255,0.12) 100%);
|
||
border-color: rgba(24,199,255,0.55);
|
||
}
|
||
.mc-stat-highlight .mc-stat-icon, .mc-stat-highlight .mc-stat-value, .mc-stat-highlight .mc-stat-label { color: #e6f9ff !important; }
|
||
.mc-stat-icon { font-size: 1.4em; line-height: 1; margin-bottom: 8px; }
|
||
.mc-stat-value { font-size: 1.12em; font-weight: 700; color: var(--mc-ink); line-height: 1.2; word-break: break-word; }
|
||
.mc-stat-label { font-size: 0.7em; font-weight: 700; color: var(--mc-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.6px; }
|
||
|
||
.mc-profile-loading { display: flex; align-items: center; justify-content: center; gap: 12px; padding: 60px 20px; color: var(--mc-muted); font-size: 0.95em; }
|
||
.mc-spinner { width: 22px; height: 22px; border: 3px solid #2e3642; border-top-color: var(--mc-accent); border-radius: 50%; animation: mc-spin 0.7s linear infinite; }
|
||
@keyframes mc-spin { to { transform: rotate(360deg); } }
|
||
|
||
@media (max-width: 600px) {
|
||
.mc-profile-inner { padding: 18px 14px; }
|
||
.mc-profile-name { font-size: 1.6em; }
|
||
.mc-profile-avatar { width: 82px; }
|
||
.mc-stats-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
|
||
}
|
||
|
||
@media (max-width: 600px) { .mc-profile-header-banner { padding: 12px; } }
|
||
</style>';
|
||
return ob_get_clean();
|
||
}
|
||
|
||
/* ================= SIDEBAR WIDGET ================= */
|
||
class MC_Top_Playtime_Widget extends WP_Widget {
|
||
public function __construct() {
|
||
parent::__construct( 'mc_top_playtime_widget', 'MC Top Spieler', array( 'description' => 'Zeigt die 6 Spieler mit der meisten Spielzeit an.' ) );
|
||
}
|
||
public static function format_duration( $seconds ) {
|
||
if ( $seconds < 60 ) return $seconds . 's';
|
||
elseif ( $seconds < 3600 ) return floor( $seconds / 60 ) . 'm';
|
||
elseif ( $seconds < 86400 ) { $h = floor( $seconds / 3600 ); $m = floor( ( $seconds % 3600 ) / 60 ); return $h . 'h ' . $m . 'm'; }
|
||
else { $d = floor( $seconds / 86400 ); $h = floor( ( $seconds % 86400 ) / 3600 ); return $d . 'd ' . $h . 'h'; }
|
||
}
|
||
private static function parse_minecraft_color_codes_inline( $text ) {
|
||
if ( $text === null || $text === '' ) return '';
|
||
$map = array('0'=>'#000000','1'=>'#0000AA','2'=>'#00AA00','3'=>'#00AAAA','4'=>'#AA0000','5'=>'#AA00AA','6'=>'#FFAA00','7'=>'#AAAAAA','8'=>'#555555','9'=>'#5555FF','a'=>'#55FF55','b'=>'#55FFFF','c'=>'#FF5555','d'=>'#FF55FF','e'=>'#FFFF55','f'=>'#FFFFFF');
|
||
$text = str_replace('§', '&', $text);
|
||
$parts = preg_split('/(&[0-9a-frk-or])/i', $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
|
||
$out = ''; $open = false;
|
||
foreach ( $parts as $part ) {
|
||
if ( preg_match('/^&([0-9a-frk-orA-FK-OR])$/', $part, $m) ) {
|
||
$code = strtolower( $m[1] );
|
||
if ( $code === 'r' ) { if ( $open ) { $out .= '</span>'; $open = false; } continue; }
|
||
if ( isset( $map[$code] ) ) { if ( $open ) { $out .= '</span>'; $open = false; } $out .= '<span style="color: ' . esc_attr( $map[$code] ) . ';">'; $open = true; continue; }
|
||
if ( $code === 'l' ) { $out .= '<strong>'; continue; }
|
||
}
|
||
$out .= esc_html( $part );
|
||
}
|
||
if ( $open ) $out .= '</span>';
|
||
return $out;
|
||
}
|
||
public function form( $instance ) {
|
||
$title = isset( $instance['title'] ) ? $instance['title'] : 'Top 6 Spielzeit';
|
||
$columns = isset( $instance['columns'] ) ? intval( $instance['columns'] ) : 1;
|
||
?>
|
||
<p>
|
||
<label for="<?php echo $this->get_field_id( 'title' ); ?>">Titel:</label>
|
||
<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
|
||
</p>
|
||
<p>
|
||
<label for="<?php echo $this->get_field_id( 'columns' ); ?>">Spieler pro Zeile:</label>
|
||
<select id="<?php echo $this->get_field_id( 'columns' ); ?>" name="<?php echo $this->get_field_name( 'columns' ); ?>">
|
||
<option value="1" <?php selected( $columns, 1 ); ?>>1 Spieler (mit Rang + Zeit)</option>
|
||
<option value="2" <?php selected( $columns, 2 ); ?>>2 Spieler (kompakt, kein Rang, keine Zeit)</option>
|
||
</select>
|
||
</p>
|
||
<?php
|
||
}
|
||
public function update( $new_instance, $old_instance ) {
|
||
$instance = array();
|
||
$instance['title'] = ( ! empty( $new_instance['title'] ) ) ? sanitize_text_field( $new_instance['title'] ) : 'Top 6 Spielzeit';
|
||
$instance['columns'] = isset( $new_instance['columns'] ) && in_array( intval( $new_instance['columns'] ), array(1,2), true ) ? intval( $new_instance['columns'] ) : 1;
|
||
return $instance;
|
||
}
|
||
public function widget( $args, $instance ) {
|
||
echo $args['before_widget'];
|
||
$title = ! empty( $instance['title'] ) ? $instance['title'] : 'Top 6 Spielzeit';
|
||
$columns = isset( $instance['columns'] ) ? intval( $instance['columns'] ) : 1;
|
||
if ( ! empty( $title ) ) { echo $args['before_title'] . apply_filters( 'widget_title', $title ) . $args['after_title']; }
|
||
global $wpdb; $table_name = $wpdb->prefix . 'mc_players';
|
||
$top_players = $wpdb->get_results( "SELECT * FROM $table_name WHERE playtime_seconds > 0 ORDER BY playtime_seconds DESC LIMIT 6" );
|
||
$container_class = 'mc-top-list' . ( $columns === 2 ? ' mc-two-columns' : ' mc-one-column' );
|
||
?>
|
||
<div class="<?php echo esc_attr( $container_class ); ?>">
|
||
<?php if ( empty( $top_players ) ): ?>
|
||
<p style="text-align:center; color:#888; font-size:13px; padding:10px;">Noch keine Daten.</p>
|
||
<?php else: ?>
|
||
<?php $rank = 1; foreach ( $top_players as $row ):
|
||
$username = esc_html( $row->username );
|
||
$time_string = self::format_duration( $row->playtime_seconds );
|
||
$raw_prefix = isset( $row->prefix ) ? $row->prefix : '';
|
||
$prefix_html = self::parse_minecraft_color_codes_inline( $raw_prefix );
|
||
$avatar = isset( $row->uuid ) && ! empty( $row->uuid ) ? 'https://mc-heads.net/head/' . esc_attr( $row->uuid ) . '/50' : 'https://mc-heads.net/head/' . rawurlencode( $username ) . '/50';
|
||
?>
|
||
<?php if ( $columns === 1 ): ?>
|
||
<div class="mc-top-item">
|
||
<div class="mc-top-rank"><?php echo esc_html( $rank ); ?></div>
|
||
<img src="<?php echo esc_url( $avatar ); ?>" class="mc-top-avatar" alt="<?php echo $username; ?>">
|
||
<div class="mc-top-info">
|
||
<?php if ( ! empty( $prefix_html ) ): ?><div class="mc-top-prefix"><?php echo wp_kses_post( $prefix_html ); ?></div><?php endif; ?>
|
||
<div class="mc-top-name"><?php echo $username; ?></div>
|
||
</div>
|
||
<div class="mc-top-time"><?php echo esc_html( $time_string ); ?></div>
|
||
</div>
|
||
<?php $rank++; ?>
|
||
<?php else: ?>
|
||
<div class="mc-top-item mc-compact">
|
||
<img src="<?php echo esc_url( $avatar ); ?>" class="mc-top-avatar" alt="<?php echo $username; ?>">
|
||
<?php if ( ! empty( $prefix_html ) ): ?><div class="mc-top-prefix"><?php echo wp_kses_post( $prefix_html ); ?></div><?php endif; ?>
|
||
<div class="mc-top-name"><?php echo $username; ?></div>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
<style>
|
||
.mc-top-list { width:100%; box-sizing:border-box; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; }
|
||
.mc-one-column { display:flex; flex-direction:column; gap:8px; }
|
||
.mc-one-column .mc-top-item { display:flex; align-items:center; background:#f8f9fa; border:1px solid #e9ecef; border-radius:8px; padding:8px 10px; transition:all .18s ease; }
|
||
.mc-one-column .mc-top-item:hover { background:#e9ecef; transform:translateX(3px); border-color:#ced4da; }
|
||
.mc-top-rank { font-weight:800; color:#adb5bd; font-size:1.2rem; width:28px; text-align:center; margin-right:8px; }
|
||
.mc-one-column .mc-top-avatar { width:34px; height:34px; margin-right:10px; background:#fff; border-radius:4px; box-shadow:0 2px 4px rgba(0,0,0,0.06); object-fit:cover; }
|
||
.mc-top-info { flex-grow:1; display:flex; flex-direction:column; line-height:1.2; }
|
||
.mc-top-name { font-weight:600; color:#333; font-size:0.95rem; }
|
||
.mc-top-time { font-size:0.8rem; font-weight:700; color:#6f42c1; background:rgba(111,66,193,0.08); padding:4px 8px; border-radius:10px; margin-left:8px; white-space:nowrap; }
|
||
.mc-two-columns { display:grid; grid-template-columns:repeat(2,1fr); gap:10px; }
|
||
.mc-two-columns .mc-top-item.mc-compact { display:flex; flex-direction:column; align-items:center; background:#f8f9fa; border:1px solid #e9ecef; border-radius:8px; padding:10px 6px; text-align:center; transition:all .18s ease; }
|
||
.mc-two-columns .mc-top-item.mc-compact:hover { background:#e9ecef; transform:translateY(-2px); border-color:#ced4da; }
|
||
.mc-two-columns .mc-top-avatar { width:44px; height:44px; margin-bottom:6px; background:#fff; border-radius:6px; box-shadow:0 2px 6px rgba(0,0,0,0.06); object-fit:cover; }
|
||
.mc-two-columns .mc-top-name { font-weight:700; color:#222; font-size:0.9rem; }
|
||
.mc-top-prefix { font-size:0.75rem; font-weight:600; line-height:1.1; text-shadow:0 1px 1px rgba(0,0,0,0.18); display:inline-block; }
|
||
@media (max-width:360px) { .mc-two-columns { grid-template-columns:1fr; } }
|
||
</style>
|
||
<?php
|
||
echo $args['after_widget'];
|
||
}
|
||
}
|
||
add_action( 'widgets_init', function(){ register_widget( 'MC_Top_Playtime_Widget' ); });
|