Files
MC-Player-History---WordPre…/mc-player-history.php
2026-04-07 19:59:16 +02:00

1330 lines
62 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
}
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' ); });