Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f5efe273d | |||
| d6c3faa42c |
43
README.md
43
README.md
@@ -11,12 +11,11 @@ Die Daten werden live über eine externe **StatusAPI** aus deinem Minecraft-Netz
|
||||
|
||||
### Pflichtvoraussetzung:
|
||||
- Ein **BungeeCord-Server**
|
||||
- Das Plugin **StatusAPI**
|
||||
- Das Plugin **[StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)**
|
||||
- **StatusAPI MUSS im BungeeCord-Ordner `/plugins` liegen**
|
||||
|
||||
❗ **NICHT** auf Spigot / Paper / Velocity
|
||||
❗ **NUR** im **BungeeCord-Server unter `/plugins`**
|
||||
|
||||
```text
|
||||
BungeeCord/
|
||||
├─ plugins/
|
||||
@@ -24,8 +23,12 @@ BungeeCord/
|
||||
└─ config.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 StatusAPI
|
||||
|
||||
**Download:** [https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)
|
||||
|
||||
Die **StatusAPI** stellt einen HTTP-Endpunkt bereit (z. B. `http://localhost:9191`),
|
||||
der von diesem WordPress-Plugin **regelmäßig abgefragt** wird, um Spieler- und Online-Daten zu synchronisieren.
|
||||
|
||||
@@ -54,30 +57,30 @@ Die StatusAPI **MUSS** auf dem **BungeeCord-Server** installiert sein und sich i
|
||||
## 📦 Installation (WordPress)
|
||||
|
||||
1. Plugin-Dateien hochladen nach:
|
||||
```text
|
||||
```text
|
||||
wp-content/plugins/mc-player-history/
|
||||
```
|
||||
```
|
||||
|
||||
4. Plugin im WordPress-Admin aktivieren
|
||||
2. Plugin im WordPress-Admin aktivieren
|
||||
|
||||
5. Unter **Einstellungen → MC Player History** die **StatusAPI URL** eintragen
|
||||
3. Unter **Einstellungen → MC Player History** die **StatusAPI URL** eintragen
|
||||
Beispiel:
|
||||
```text
|
||||
```text
|
||||
http://localhost:9191
|
||||
```
|
||||
```
|
||||
|
||||
6. Einstellungen speichern
|
||||
4. Einstellungen speichern
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Verbindung zur StatusAPI
|
||||
|
||||
- Die **StatusAPI** läuft auf dem **BungeeCord-Server**
|
||||
- Die **[StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)** läuft auf dem **BungeeCord-Server**
|
||||
- WordPress ruft diese URL per **HTTP** ab
|
||||
- Standard-Fallback:
|
||||
```text
|
||||
```text
|
||||
http://localhost:9191
|
||||
```
|
||||
```
|
||||
|
||||
❗ Stelle sicher, dass:
|
||||
|
||||
@@ -88,7 +91,6 @@ Die StatusAPI **MUSS** auf dem **BungeeCord-Server** installiert sein und sich i
|
||||
---
|
||||
|
||||
## 🧩 Shortcode
|
||||
|
||||
```shortcode
|
||||
[mc_player_history]
|
||||
```
|
||||
@@ -102,15 +104,15 @@ Die StatusAPI **MUSS** auf dem **BungeeCord-Server** installiert sein und sich i
|
||||
| only_online | Nur Online-Spieler anzeigen | false |
|
||||
|
||||
### Beispiel
|
||||
|
||||
```shortcode
|
||||
[mc_player_history limit="50" interval="5" only_online="true"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Anzeige-Reihenfolge (fix)
|
||||
|
||||
Die Struktur ist **fest definiert** und kann **nicht verändert** werden:
|
||||
|
||||
```text
|
||||
Avatar
|
||||
Prefix
|
||||
@@ -119,6 +121,8 @@ Name
|
||||
Status (unten)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Cron & Caching
|
||||
|
||||
- WordPress-Cron läuft alle **2 Minuten**
|
||||
@@ -131,7 +135,7 @@ Status (unten)
|
||||
|
||||
### ❌ Keine Spieler sichtbar?
|
||||
|
||||
- StatusAPI nicht erreichbar
|
||||
- **[StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)** nicht erreichbar
|
||||
- StatusAPI **nicht im `BungeeCord/plugins/`**
|
||||
- Falsche URL (z. B. `http://` vergessen)
|
||||
- Firewall blockiert den API-Port
|
||||
@@ -141,3 +145,10 @@ Status (unten)
|
||||
- StatusAPI liefert keine `players`
|
||||
- BungeeCord-Server nicht gestartet
|
||||
- Falscher API-Port konfiguriert
|
||||
|
||||
---
|
||||
|
||||
## 📚 Weitere Informationen
|
||||
|
||||
Für detaillierte Informationen zur Installation und Konfiguration der StatusAPI besuche das Repository:
|
||||
**[https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)**
|
||||
@@ -1,492 +1,492 @@
|
||||
<?php
|
||||
/*
|
||||
Plugin Name: MC Player History
|
||||
Description: Strikte Reihenfolge: Bild -> Prefix -> Name -> Status (unten).
|
||||
Version: 1.1.0
|
||||
Author: Dein Name
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
register_activation_hook( __FILE__, 'mcph_install' );
|
||||
function mcph_install() {
|
||||
global $wpdb;
|
||||
$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,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uuid (uuid),
|
||||
KEY username (username),
|
||||
KEY is_online (is_online)
|
||||
) $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' );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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; }
|
||||
|
||||
$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 ) : '';
|
||||
$uuid = 'legacy-' . md5( strtolower( trim( $name ) ) );
|
||||
if ( empty( $name ) ) continue;
|
||||
|
||||
$exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table_name WHERE uuid = %s", $uuid ) );
|
||||
$now = current_time( 'mysql' );
|
||||
|
||||
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' ) );
|
||||
} 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' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = '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 = 'https://minotar.net/avatar/' . $username . '/80';
|
||||
|
||||
$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 . '">';
|
||||
|
||||
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() {
|
||||
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>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; /* Weniger Abstand dazwischen */
|
||||
padding: 20px 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: default;
|
||||
position: relative; /* Fallback */
|
||||
}
|
||||
|
||||
.mc-player-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.mc-avatar {
|
||||
border-radius: 4px;
|
||||
border: 3px solid #ddd;
|
||||
background: #fff;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
transition: border-color 0.3s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mc-player-card:hover .mc-avatar {
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
/* INFOS BEREICH */
|
||||
.mc-info-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
flex-grow: 1; /* Füllt den Rest der Karte */
|
||||
position: relative; /* Referenz für absoluten Status */
|
||||
padding-bottom: 35px; /* Platz für Status unten */
|
||||
justify-content: flex-start; /* Oben ausrichten */
|
||||
}
|
||||
|
||||
/* 1. PREFIX */
|
||||
.mc-prefix {
|
||||
font-size: 0.9em;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 2. NAME */
|
||||
.mc-name {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
color: #333;
|
||||
word-break: break-word;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 3. SPACER */
|
||||
.mc-spacer {
|
||||
flex-grow: 1; /* Nimmt ALLEN leeren Platz zwischen Name und Status ein */
|
||||
}
|
||||
|
||||
/* 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: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.mc-online { background-color: #e6fffa; color: #28a745; border: 1px solid #b2f5ea; }
|
||||
.mc-offline { background-color: #fff5f5; color: #e53e3e; border: 1px solid #feb2b2; }
|
||||
|
||||
.mc-update-time {
|
||||
font-size: 0.8em;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@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: 10px; }
|
||||
.mc-avatar { width: 50px; height: 50px; }
|
||||
.mc-name { font-size: 1em; }
|
||||
.mc-player-card { padding: 10px 5px; }
|
||||
}
|
||||
</style>';
|
||||
|
||||
return ob_get_clean();
|
||||
<?php
|
||||
/*
|
||||
Plugin Name: MC Player History
|
||||
Description: Spielerverlauf deines Minecraft Servers.
|
||||
Version: 1.1.0
|
||||
Author: Dein Name
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
register_activation_hook( __FILE__, 'mcph_install' );
|
||||
function mcph_install() {
|
||||
global $wpdb;
|
||||
$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,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uuid (uuid),
|
||||
KEY username (username),
|
||||
KEY is_online (is_online)
|
||||
) $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' );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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; }
|
||||
|
||||
$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 ) : '';
|
||||
$uuid = 'legacy-' . md5( strtolower( trim( $name ) ) );
|
||||
if ( empty( $name ) ) continue;
|
||||
|
||||
$exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table_name WHERE uuid = %s", $uuid ) );
|
||||
$now = current_time( 'mysql' );
|
||||
|
||||
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' ) );
|
||||
} 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' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = '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 = 'https://minotar.net/avatar/' . $username . '/80';
|
||||
|
||||
$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 . '">';
|
||||
|
||||
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() {
|
||||
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>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; /* Weniger Abstand dazwischen */
|
||||
padding: 20px 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: default;
|
||||
position: relative; /* Fallback */
|
||||
}
|
||||
|
||||
.mc-player-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.mc-avatar {
|
||||
border-radius: 4px;
|
||||
border: 3px solid #ddd;
|
||||
background: #fff;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
transition: border-color 0.3s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mc-player-card:hover .mc-avatar {
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
/* INFOS BEREICH */
|
||||
.mc-info-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
flex-grow: 1; /* Füllt den Rest der Karte */
|
||||
position: relative; /* Referenz für absoluten Status */
|
||||
padding-bottom: 35px; /* Platz für Status unten */
|
||||
justify-content: flex-start; /* Oben ausrichten */
|
||||
}
|
||||
|
||||
/* 1. PREFIX */
|
||||
.mc-prefix {
|
||||
font-size: 0.9em;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 2. NAME */
|
||||
.mc-name {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
color: #333;
|
||||
word-break: break-word;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 3. SPACER */
|
||||
.mc-spacer {
|
||||
flex-grow: 1; /* Nimmt ALLEN leeren Platz zwischen Name und Status ein */
|
||||
}
|
||||
|
||||
/* 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: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.mc-online { background-color: #e6fffa; color: #28a745; border: 1px solid #b2f5ea; }
|
||||
.mc-offline { background-color: #fff5f5; color: #e53e3e; border: 1px solid #feb2b2; }
|
||||
|
||||
.mc-update-time {
|
||||
font-size: 0.8em;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@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: 10px; }
|
||||
.mc-avatar { width: 50px; height: 50px; }
|
||||
.mc-name { font-size: 1em; }
|
||||
.mc-player-card { padding: 10px 5px; }
|
||||
}
|
||||
</style>';
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
Reference in New Issue
Block a user