1034 lines
38 KiB
PHP
1034 lines
38 KiB
PHP
<?php
|
||
/*
|
||
Plugin Name: MC Player History
|
||
Description: Spielerverlauf deines Minecraft Servers.
|
||
Version: 1.1.3
|
||
Author: M_Viper
|
||
*/
|
||
|
||
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.15.0';
|
||
|
||
// Hilfsfunktion für Logging
|
||
function mcph_log( $message ) {
|
||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||
if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
|
||
error_log( '[MC Player History] ' . $message );
|
||
} else {
|
||
error_log( '[MC Player History] ' . $message );
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
}
|
||
|
||
/**
|
||
* 1. Tabelle beim Aktivieren anlegen + Playtime Spalte
|
||
*/
|
||
register_activation_hook( __FILE__, 'mcph_install' );
|
||
function mcph_install() {
|
||
global $wpdb, $mc_player_history_db_version; // FIX: Variable global verfügbar machen
|
||
$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,
|
||
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' );
|
||
}
|
||
|
||
/**
|
||
* NEU: Playtime Spalte hinzufügen (wird beim Laden geprüft)
|
||
*/
|
||
add_action( 'init', 'mcph_add_playtime_column' );
|
||
function mcph_add_playtime_column() {
|
||
global $wpdb;
|
||
$table_name = $wpdb->prefix . 'mc_players';
|
||
|
||
// Prüfen, ob Spalte existiert
|
||
$column_exists = $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'playtime_seconds'" );
|
||
|
||
if ( empty( $column_exists ) ) {
|
||
$wpdb->query( "ALTER TABLE $table_name ADD COLUMN playtime_seconds INT NOT NULL DEFAULT 0 AFTER last_seen" );
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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; }
|
||
|
||
// 1. Alle Spieler auf Offline setzen (Reset)
|
||
$wpdb->query( "UPDATE $table_name SET is_online = 0" );
|
||
$players = isset( $json->players ) && is_array( $json->players ) ? $json->players : array();
|
||
|
||
// 2. Spieler durchgehen
|
||
foreach ( $players as $p ) {
|
||
$name = isset( $p->name ) ? sanitize_text_field( $p->name ) : '';
|
||
$prefix = isset( $p->prefix ) ? sanitize_text_field( $p->prefix ) : '';
|
||
|
||
// UUID aus API übernehmen, falls vorhanden, sonst legacy-Hash
|
||
if ( isset( $p->uuid ) && !empty( $p->uuid ) ) {
|
||
$uuid = sanitize_text_field( $p->uuid );
|
||
} else {
|
||
$uuid = 'legacy-' . md5( strtolower( trim( $name ) ) );
|
||
}
|
||
if ( empty( $name ) ) continue;
|
||
|
||
// Prüfe ob Spieler bereits existiert (nach UUID)
|
||
$exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table_name WHERE uuid = %s", $uuid ) );
|
||
|
||
// Fallback: Falls nicht per UUID gefunden, prüfe nach Username
|
||
if ( ! $exists ) {
|
||
$old_entry = $wpdb->get_row( $wpdb->prepare( "SELECT id, uuid FROM $table_name WHERE username = %s", $name ) );
|
||
if ( $old_entry ) {
|
||
// FIX: Basis-Daten für Update (ohne Prefix)
|
||
$update_data = array(
|
||
'uuid' => $uuid,
|
||
'last_seen' => current_time( 'mysql' ),
|
||
'is_online' => 1
|
||
);
|
||
$update_format = array( '%s', '%s', '%d' );
|
||
|
||
// FIX: Prefix nur aktualisieren, wenn API einen Wert liefert (nicht leer)
|
||
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;
|
||
}
|
||
}
|
||
|
||
// NEU: Playtime erhöhen (2 Minuten = 120 Sekunden pro Sync)
|
||
$wpdb->query( $wpdb->prepare( "UPDATE $table_name SET playtime_seconds = playtime_seconds + 120 WHERE uuid = %s", $uuid ) );
|
||
|
||
$now = current_time( 'mysql' );
|
||
|
||
if ( $exists ) {
|
||
// FIX: Basis-Daten für Update (ohne Prefix)
|
||
$update_data = array(
|
||
'username' => $name,
|
||
'last_seen' => $now,
|
||
'is_online' => 1
|
||
);
|
||
$update_format = array( '%s', '%s', '%d' );
|
||
|
||
// FIX: Prefix nur aktualisieren, wenn API einen Wert liefert (nicht leer)
|
||
if ( ! empty( $prefix ) ) {
|
||
$update_data['prefix'] = $prefix;
|
||
$update_format[] = '%s';
|
||
}
|
||
|
||
$wpdb->update( $table_name, $update_data, array( 'uuid' => $uuid ), $update_format, array( '%s' ) );
|
||
} else {
|
||
// Bei neuen Spielern fügen wir das Prefix ein (auch wenn es leer ist)
|
||
$wpdb->insert( $table_name, array( 'uuid' => $uuid, 'username' => $name, 'prefix' => $prefix, 'first_seen' => $now, 'last_seen' => $now, 'is_online' => 1, 'playtime_seconds' => 0 ), array( '%s', '%s', '%s', '%s', '%s', '%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 ) );
|
||
}
|
||
|
||
/**
|
||
* 4. HTML Generator (Strikte Reihenfolge)
|
||
*/
|
||
function mcph_generate_player_html( $limit = 500, $only_online = false, $is_ajax = false ) {
|
||
global $wpdb;
|
||
$table_name = $wpdb->prefix . 'mc_players';
|
||
|
||
// --- LIVE STATUS CHECK MIT CACHE ---
|
||
$cache_key = 'mcph_api_cache_data';
|
||
$api_response = get_transient( $cache_key );
|
||
|
||
if ( false === $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 ) ) {
|
||
$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, 5 );
|
||
$api_response = $json->players;
|
||
}
|
||
}
|
||
}
|
||
|
||
$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 aus API übernehmen, falls vorhanden, sonst legacy-Hash
|
||
if ( isset( $p->uuid ) && !empty( $p->uuid ) ) {
|
||
$uuid = sanitize_text_field( $p->uuid );
|
||
} else {
|
||
$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 = false;
|
||
if ( $has_live_data ) {
|
||
$is_online = isset( $live_online_uuids[ $row->uuid ] );
|
||
} else {
|
||
$is_online = ( $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-Logik: Moderne 3D-Köpfe (alle in gleiche Richtung)
|
||
$avatar = '';
|
||
if ( strpos( $username, '.' ) !== false || ( isset($row->uuid) && strpos($row->uuid, 'xuid') === 0 ) ) {
|
||
// Bedrock: mc-heads.net mit 3D Head
|
||
$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 {
|
||
// Java: mc-heads.net mit 3D Head (gleiche Richtung wie Bedrock)
|
||
$avatar = 'https://mc-heads.net/head/' . $username . '/100';
|
||
}
|
||
|
||
$anim_style = '';
|
||
if ( ! $is_ajax ) {
|
||
$anim_style = '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>';
|
||
}
|
||
|
||
// STRUKTUR: Bild -> Prefix -> Name -> Spacer -> Status
|
||
echo '<div class="mc-player-card" style="' . $anim_style . '">';
|
||
echo '<img src="' . $avatar . '" class="mc-avatar" alt="' . $username . '" loading="lazy">';
|
||
|
||
echo '<div class="mc-info-stack">';
|
||
|
||
// 1. PREFIX
|
||
if ( ! empty( $row->prefix ) ) {
|
||
echo '<span class="mc-prefix">' . $prefix . '</span>';
|
||
}
|
||
|
||
// 2. NAME
|
||
echo '<span class="mc-name">' . $username . '</span>';
|
||
|
||
// 3. SPACER (Nimmt den verbleibenden Platz ein)
|
||
echo '<div class="mc-spacer"></div>';
|
||
|
||
// 4. STATUS
|
||
echo '<div class="mc-status-line">' . $status_html . '</div>';
|
||
|
||
echo '</div>';
|
||
echo '</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() {
|
||
// Duplikate bereinigen
|
||
if ( isset( $_POST['mcph_cleanup_duplicates'] ) && check_admin_referer( 'mcph_cleanup_action' ) ) {
|
||
global $wpdb;
|
||
$table_name = $wpdb->prefix . 'mc_players';
|
||
|
||
// Finde Duplikate (gleicher Username, verschiedene UUIDs)
|
||
$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 );
|
||
// Behalte den neuesten (ersten), lösche die anderen
|
||
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
|
||
*/
|
||
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>';
|
||
|
||
?>
|
||
<script>
|
||
(function() {
|
||
const intervalSeconds = <?php echo intval( $atts['interval'] ); ?>;
|
||
const wrapperId = "<?php echo $container_id; ?>";
|
||
const limit = <?php echo intval( $atts['limit'] ); ?>;
|
||
const onlyOnline = "<?php echo ( $atts['only_online'] === 'true' ) ? 'true' : 'false'; ?>";
|
||
|
||
function refreshList() {
|
||
fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||
},
|
||
body: 'action=mcph_refresh&limit=' + limit + '&only_online=' + onlyOnline
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const wrapper = document.getElementById(wrapperId);
|
||
if (wrapper) {
|
||
wrapper.innerHTML = data.data.html;
|
||
}
|
||
}
|
||
})
|
||
.catch(error => console.error('MC Player History Error:', error));
|
||
}
|
||
|
||
setInterval(refreshList, intervalSeconds * 1000);
|
||
})();
|
||
</script>
|
||
<?php
|
||
|
||
echo '<style>
|
||
.mc-player-list { width: 100%; }
|
||
|
||
.mc-grid {
|
||
display: grid;
|
||
gap: 20px;
|
||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||
justify-content: center;
|
||
align-items: stretch;
|
||
}
|
||
|
||
@keyframes fadeInUp {
|
||
from { opacity: 0; transform: translateY(10px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
/* KARTE */
|
||
.mc-player-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 20px 10px;
|
||
background: linear-gradient(145deg, #ffffff 0%, #f8f9fa 100%);
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 12px;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
cursor: default;
|
||
position: relative;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||
}
|
||
|
||
.mc-player-card:hover {
|
||
transform: translateY(-8px) scale(1.02);
|
||
box-shadow: 0 12px 24px rgba(0,0,0,0.15);
|
||
border-color: #667eea;
|
||
background: linear-gradient(145deg, #ffffff 0%, #f0f2ff 100%);
|
||
}
|
||
|
||
.mc-avatar {
|
||
border-radius: 8px;
|
||
border: 3px solid #e0e0e0;
|
||
background: transparent;
|
||
width: 80px;
|
||
height: 80px;
|
||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||
flex-shrink: 0;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
object-fit: contain;
|
||
padding: 0;
|
||
}
|
||
|
||
.mc-player-card:hover .mc-avatar {
|
||
border-color: #667eea;
|
||
transform: scale(1.15) rotateY(10deg);
|
||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
/* INFOS BEREICH */
|
||
.mc-info-stack {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 100%;
|
||
flex-grow: 1;
|
||
position: relative;
|
||
padding-bottom: 35px;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
/* 1. PREFIX */
|
||
.mc-prefix {
|
||
font-size: 0.9em;
|
||
display: block;
|
||
width: 100%;
|
||
text-align: center;
|
||
margin-bottom: 4px;
|
||
line-height: 1.2;
|
||
text-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
/* 2. NAME */
|
||
.mc-name {
|
||
font-weight: bold;
|
||
font-size: 1.1em;
|
||
color: #2d3748;
|
||
word-break: break-word;
|
||
display: block;
|
||
width: 100%;
|
||
text-align: center;
|
||
text-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
/* 3. SPACER */
|
||
.mc-spacer {
|
||
flex-grow: 1;
|
||
}
|
||
|
||
/* 4. STATUS (UNTEN) */
|
||
.mc-status-line {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
text-align: center;
|
||
margin: 0;
|
||
padding-bottom: 5px;
|
||
}
|
||
|
||
.mc-status {
|
||
display: inline-block;
|
||
padding: 5px 14px;
|
||
border-radius: 20px;
|
||
font-size: 0.8em;
|
||
font-weight: 600;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.mc-online {
|
||
background: linear-gradient(135deg, #d4f8e8 0%, #b8f2d9 100%);
|
||
color: #0a7340;
|
||
border: 1px solid #81e6b8;
|
||
box-shadow: 0 2px 4px rgba(10, 115, 64, 0.1);
|
||
}
|
||
|
||
.mc-offline {
|
||
background: linear-gradient(135deg, #fff5f5 0%, #ffe5e5 100%);
|
||
color: #c53030;
|
||
border: 1px solid #fbb6b6;
|
||
box-shadow: 0 2px 4px rgba(197, 48, 48, 0.1);
|
||
}
|
||
|
||
.mc-player-card:hover .mc-status {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.mc-update-time {
|
||
font-size: 0.85em;
|
||
color: #718096;
|
||
text-align: center;
|
||
margin-top: 20px;
|
||
font-style: italic;
|
||
padding: 8px;
|
||
background: rgba(102, 126, 234, 0.05);
|
||
border-radius: 6px;
|
||
display: inline-block;
|
||
width: 100%;
|
||
}
|
||
|
||
@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: 1em;
|
||
}
|
||
.mc-player-card {
|
||
padding: 15px 8px;
|
||
}
|
||
}
|
||
</style>';
|
||
|
||
return ob_get_clean();
|
||
}
|
||
|
||
/* ================= SIDEBAR WIDGET: TOP SPIELZEIT (1/2 Spalten + sichere Inline-Farben) ================= */
|
||
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. Wahl: 1 oder 2 Spieler pro Zeile.' )
|
||
);
|
||
}
|
||
|
||
// Zeitformatierung
|
||
public static function format_duration( $seconds ) {
|
||
if ( $seconds < 60 ) {
|
||
return $seconds . 's';
|
||
} elseif ( $seconds < 3600 ) {
|
||
$minutes = floor( $seconds / 60 );
|
||
return $minutes . 'm';
|
||
} elseif ( $seconds < 86400 ) {
|
||
$hours = floor( $seconds / 3600 );
|
||
$minutes = floor( ( $seconds % 3600 ) / 60 );
|
||
return $hours . 'h ' . $minutes . 'm';
|
||
} else {
|
||
$days = floor( $seconds / 86400 );
|
||
$hours = floor( ( $seconds % 86400 ) / 3600 );
|
||
return $days . 'd ' . $hours . 'h';
|
||
}
|
||
}
|
||
|
||
// Konvertiert Minecraft-Farbcodes (&0-&f, &r) in inline <span style="color:#...">Text</span>
|
||
private static function parse_minecraft_color_codes_inline( $text ) {
|
||
if ( $text === null || $text === '' ) {
|
||
return '';
|
||
}
|
||
|
||
// Mapping Minecraft color codes -> HEX
|
||
$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',
|
||
);
|
||
|
||
// Ersetze alternative §-Codes durch & falls nötig
|
||
$text = str_replace('§', '&', $text);
|
||
|
||
// Teile so, dass die Codes erhalten bleiben
|
||
$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 ) {
|
||
// Ist es ein Code?
|
||
if ( preg_match('/^&([0-9a-frk-orA-FK-OR])$/', $part, $m) ) {
|
||
$code = strtolower( $m[1] );
|
||
|
||
// Reset
|
||
if ( $code === 'r' ) {
|
||
if ( $open ) {
|
||
$out .= '</span>';
|
||
$open = false;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Farbe
|
||
if ( isset( $map[$code] ) ) {
|
||
if ( $open ) {
|
||
$out .= '</span>';
|
||
$open = false;
|
||
}
|
||
$hex = $map[$code];
|
||
$out .= '<span style="color: ' . esc_attr( $hex ) . ';">';
|
||
$open = true;
|
||
continue;
|
||
}
|
||
|
||
// Formatcodes: &l &o &n &m &k -> wir implementieren nur fett (&l) &o kursiv optional
|
||
if ( $code === 'l' ) { // fett
|
||
$out .= '<strong>';
|
||
continue;
|
||
}
|
||
if ( $code === 'o' ) { // italic
|
||
$out .= '<em>';
|
||
continue;
|
||
}
|
||
if ( in_array( $code, array('m','n','k'), true) ) {
|
||
// durchgestrichen / underline / obfuscated - ignorieren oder erweitern falls gewünscht
|
||
continue;
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
// Normaler Text: escapen
|
||
$escaped = esc_html( $part );
|
||
$out .= $escaped;
|
||
}
|
||
|
||
// Schließe eventuell offene Tags: zuerst format, dann color spans
|
||
// (Wir schlossen format tags nicht oben -> vereinfachung: entfernen ungeschlossene format-tags)
|
||
// Schließe color span falls offen
|
||
if ( $open ) {
|
||
$out .= '</span>';
|
||
$open = false;
|
||
}
|
||
|
||
// Hinweis: Wir benutzten esc_html pro Textteil; das ist sicher.
|
||
return $out;
|
||
}
|
||
|
||
// Backend-Form
|
||
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
|
||
}
|
||
|
||
// Update
|
||
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;
|
||
}
|
||
|
||
// Frontend-Ausgabe
|
||
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 6 Spieler abfragen
|
||
$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 );
|
||
|
||
// Rohes Prefix aus DB (z.B. "&7Member")
|
||
$raw_prefix = isset( $row->prefix ) ? $row->prefix : '';
|
||
$prefix_html = self::parse_minecraft_color_codes_inline( $raw_prefix );
|
||
|
||
// Avatar URL (UUID bevorzugt)
|
||
if ( isset( $row->uuid ) && ! empty( $row->uuid ) ) {
|
||
$avatar = 'https://mc-heads.net/head/' . esc_attr( $row->uuid ) . '/50';
|
||
} else {
|
||
$avatar = 'https://mc-heads.net/head/' . rawurlencode( $username ) . '/50';
|
||
}
|
||
?>
|
||
<?php if ( $columns === 1 ): ?>
|
||
<!-- Einzelne Zeile: mit Rang und Zeit -->
|
||
<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: ?>
|
||
<!-- Kompaktes Grid-Item: vertikal (kein Rang, keine Zeit) -->
|
||
<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>
|
||
/* Basis */
|
||
.mc-top-list { width:100%; box-sizing:border-box; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; }
|
||
|
||
/* 1 Spalte */
|
||
.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; }
|
||
|
||
/* 2 Spalten kompakt */
|
||
.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; }
|
||
|
||
/* WICHTIG: KEINE feste color; Inline-styles haben Vorrang.
|
||
Leichter Schatten verbessert Lesbarkeit heller Farben. */
|
||
.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'];
|
||
}
|
||
}
|
||
|
||
// Registrierung
|
||
add_action( 'widgets_init', function(){
|
||
register_widget( 'MC_Top_Playtime_Widget' );
|
||
});
|