2 Commits
1.1.2 ... main

Author SHA1 Message Date
7d70d3ddff mc-player-history.php aktualisiert 2026-02-11 19:07:31 +00:00
984ca0a367 mc-player-history.php aktualisiert 2026-02-10 22:35:46 +00:00

View File

@@ -2,7 +2,7 @@
/* /*
Plugin Name: MC Player History Plugin Name: MC Player History
Description: Spielerverlauf deines Minecraft Servers. Description: Spielerverlauf deines Minecraft Servers.
Version: 1.1.1 Version: 1.1.3
Author: M_Viper Author: M_Viper
*/ */
@@ -10,6 +10,113 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; 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; global $mc_player_history_db_version;
$mc_player_history_db_version = '1.15.0'; $mc_player_history_db_version = '1.15.0';
@@ -53,11 +160,11 @@ function mcph_parse_minecraft_colors( $text ) {
} }
/** /**
* 1. Tabelle beim Aktivieren anlegen * 1. Tabelle beim Aktivieren anlegen + Playtime Spalte
*/ */
register_activation_hook( __FILE__, 'mcph_install' ); register_activation_hook( __FILE__, 'mcph_install' );
function mcph_install() { function mcph_install() {
global $wpdb; global $wpdb, $mc_player_history_db_version; // FIX: Variable global verfügbar machen
$table_name = $wpdb->prefix . 'mc_players'; $table_name = $wpdb->prefix . 'mc_players';
$charset_collate = $wpdb->get_charset_collate(); $charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table_name ( $sql = "CREATE TABLE $table_name (
@@ -68,10 +175,12 @@ function mcph_install() {
first_seen datetime NOT NULL, first_seen datetime NOT NULL,
last_seen datetime NOT NULL, last_seen datetime NOT NULL,
is_online tinyint(1) NOT NULL DEFAULT 0, is_online tinyint(1) NOT NULL DEFAULT 0,
playtime_seconds INT NOT NULL DEFAULT 0,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE KEY uuid (uuid), UNIQUE KEY uuid (uuid),
KEY username (username), KEY username (username),
KEY is_online (is_online) KEY is_online (is_online),
KEY playtime_seconds (playtime_seconds)
) $charset_collate;"; ) $charset_collate;";
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta( $sql ); dbDelta( $sql );
@@ -83,6 +192,22 @@ function mcph_deactivate() {
wp_clear_scheduled_hook( 'mcph_sync_event' ); 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 * 2. Cron-Job
*/ */
@@ -113,12 +238,15 @@ function mcph_sync_from_statusapi() {
$table_name = $wpdb->prefix . 'mc_players'; $table_name = $wpdb->prefix . 'mc_players';
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_name'" ) != $table_name ) { return; } 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" ); $wpdb->query( "UPDATE $table_name SET is_online = 0" );
$players = isset( $json->players ) && is_array( $json->players ) ? $json->players : array(); $players = isset( $json->players ) && is_array( $json->players ) ? $json->players : array();
// 2. Spieler durchgehen
foreach ( $players as $p ) { foreach ( $players as $p ) {
$name = isset( $p->name ) ? sanitize_text_field( $p->name ) : ''; $name = isset( $p->name ) ? sanitize_text_field( $p->name ) : '';
$prefix = isset( $p->prefix ) ? sanitize_text_field( $p->prefix ) : ''; $prefix = isset( $p->prefix ) ? sanitize_text_field( $p->prefix ) : '';
// UUID aus API übernehmen, falls vorhanden, sonst legacy-Hash // UUID aus API übernehmen, falls vorhanden, sonst legacy-Hash
if ( isset( $p->uuid ) && !empty( $p->uuid ) ) { if ( isset( $p->uuid ) && !empty( $p->uuid ) ) {
$uuid = sanitize_text_field( $p->uuid ); $uuid = sanitize_text_field( $p->uuid );
@@ -127,31 +255,62 @@ function mcph_sync_from_statusapi() {
} }
if ( empty( $name ) ) continue; if ( empty( $name ) ) continue;
// Prüfe ob Spieler bereits existiert (nach UUID ODER Username) // Prüfe ob Spieler bereits existiert (nach UUID)
$exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table_name WHERE uuid = %s", $uuid ) ); $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table_name WHERE uuid = %s", $uuid ) );
// Falls nicht per UUID gefunden, prüfe nach Username (für Migration alter Einträge) // Fallback: Falls nicht per UUID gefunden, prüfe nach Username
if ( ! $exists ) { if ( ! $exists ) {
$old_entry = $wpdb->get_row( $wpdb->prepare( "SELECT id, uuid FROM $table_name WHERE username = %s", $name ) ); $old_entry = $wpdb->get_row( $wpdb->prepare( "SELECT id, uuid FROM $table_name WHERE username = %s", $name ) );
if ( $old_entry ) { if ( $old_entry ) {
// Alter Eintrag gefunden - UUID aktualisieren statt neuen Eintrag // 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( $wpdb->update(
$table_name, $table_name,
array( 'uuid' => $uuid, 'prefix' => $prefix, 'last_seen' => current_time( 'mysql' ), 'is_online' => 1 ), $update_data,
array( 'id' => $old_entry->id ), array( 'id' => $old_entry->id ),
array( '%s', '%s', '%s', '%d' ), $update_format,
array( '%d' ) array( '%d' )
); );
continue; 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' ); $now = current_time( 'mysql' );
if ( $exists ) { if ( $exists ) {
$wpdb->update( $table_name, array( 'username' => $name, 'prefix' => $prefix, 'last_seen' => $now, 'is_online' => 1 ), array( 'uuid' => $uuid ), array( '%s', '%s', '%s', '%d' ), array( '%s' ) ); // 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 { } else {
$wpdb->insert( $table_name, array( 'uuid' => $uuid, 'username' => $name, 'prefix' => $prefix, 'first_seen' => $now, 'last_seen' => $now, 'is_online' => 1 ), array( '%s', '%s', '%s', '%s', '%s', '%d' ) ); // 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' ) );
} }
} }
} }
@@ -215,7 +374,7 @@ function mcph_generate_player_html( $limit = 500, $only_online = false, $is_ajax
} }
} }
$sql_limit = $only_online ? ($limit * 2) : $limit; $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 ) ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_name ORDER BY last_seen DESC LIMIT %d", $sql_limit ) );
if ( empty( $rows ) ) { if ( empty( $rows ) ) {
@@ -261,7 +420,7 @@ function mcph_generate_player_html( $limit = 500, $only_online = false, $is_ajax
$anim_style = ''; $anim_style = '';
if ( ! $is_ajax ) { if ( ! $is_ajax ) {
$anim_style = 'animation: fadeInUp 0.5s ease forwards; opacity: 0; animation-delay: ' . ($displayed_count * 0.05) . 's;'; $anim_style = 'animation: fadeInUp 0.5s ease forwards; opacity: 0; animation-delay: ' . ( $displayed_count * 0.05 ) . 's;';
} }
if ( $is_online ) { if ( $is_online ) {
@@ -402,10 +561,10 @@ function mcph_shortcode( $atts ) {
?> ?>
<script> <script>
(function() { (function() {
const intervalSeconds = <?php echo intval($atts['interval']); ?>; const intervalSeconds = <?php echo intval( $atts['interval'] ); ?>;
const wrapperId = "<?php echo $container_id; ?>"; const wrapperId = "<?php echo $container_id; ?>";
const limit = <?php echo intval($atts['limit']); ?>; const limit = <?php echo intval( $atts['limit'] ); ?>;
const onlyOnline = "<?php echo ($atts['only_online'] === 'true') ? 'true' : 'false'; ?>"; const onlyOnline = "<?php echo ( $atts['only_online'] === 'true' ) ? 'true' : 'false'; ?>";
function refreshList() { function refreshList() {
fetch('<?php echo admin_url('admin-ajax.php'); ?>', { fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
@@ -605,3 +764,271 @@ function mcph_shortcode( $atts ) {
return ob_get_clean(); 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' );
});