Files
MC-Player-History---WordPre…/mc-player-history.php

1034 lines
38 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
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' );
});