diff --git a/Minecraft-Modern-Theme/fonts/roboto/font.css b/Minecraft-Modern-Theme/fonts/roboto/font.css new file mode 100644 index 0000000..587a3f6 --- /dev/null +++ b/Minecraft-Modern-Theme/fonts/roboto/font.css @@ -0,0 +1,49 @@ +/* Roboto – lokal gehostet (DSGVO). Generiert, nicht manuell bearbeiten. */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('roboto-latin-ext-0fb06be4.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('roboto-latin-cb43c055.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('roboto-latin-ext-0fb06be4.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('roboto-latin-cb43c055.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('roboto-latin-ext-0fb06be4.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('roboto-latin-cb43c055.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/Minecraft-Modern-Theme/fonts/roboto/roboto-latin-cb43c055.woff2 b/Minecraft-Modern-Theme/fonts/roboto/roboto-latin-cb43c055.woff2 new file mode 100644 index 0000000..9b0f141 Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/roboto/roboto-latin-cb43c055.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/roboto/roboto-latin-ext-0fb06be4.woff2 b/Minecraft-Modern-Theme/fonts/roboto/roboto-latin-ext-0fb06be4.woff2 new file mode 100644 index 0000000..1ee3769 Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/roboto/roboto-latin-ext-0fb06be4.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/rubik/font.css b/Minecraft-Modern-Theme/fonts/rubik/font.css new file mode 100644 index 0000000..454dc79 --- /dev/null +++ b/Minecraft-Modern-Theme/fonts/rubik/font.css @@ -0,0 +1,49 @@ +/* Rubik – lokal gehostet (DSGVO). Generiert, nicht manuell bearbeiten. */ +@font-face { + font-family: 'Rubik'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('rubik-latin-ext-b7b482e1.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Rubik'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('rubik-latin-f673d854.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Rubik'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('rubik-latin-ext-b7b482e1.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Rubik'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('rubik-latin-f673d854.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Rubik'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('rubik-latin-ext-b7b482e1.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Rubik'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('rubik-latin-f673d854.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/Minecraft-Modern-Theme/fonts/rubik/rubik-latin-ext-b7b482e1.woff2 b/Minecraft-Modern-Theme/fonts/rubik/rubik-latin-ext-b7b482e1.woff2 new file mode 100644 index 0000000..dea13dc Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/rubik/rubik-latin-ext-b7b482e1.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/rubik/rubik-latin-f673d854.woff2 b/Minecraft-Modern-Theme/fonts/rubik/rubik-latin-f673d854.woff2 new file mode 100644 index 0000000..42fc52a Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/rubik/rubik-latin-f673d854.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/satisfy/font.css b/Minecraft-Modern-Theme/fonts/satisfy/font.css new file mode 100644 index 0000000..d8b78e4 --- /dev/null +++ b/Minecraft-Modern-Theme/fonts/satisfy/font.css @@ -0,0 +1,9 @@ +/* Satisfy – lokal gehostet (DSGVO). Generiert, nicht manuell bearbeiten. */ +@font-face { + font-family: 'Satisfy'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('satisfy-latin-b4c9305d.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/Minecraft-Modern-Theme/fonts/satisfy/satisfy-latin-b4c9305d.woff2 b/Minecraft-Modern-Theme/fonts/satisfy/satisfy-latin-b4c9305d.woff2 new file mode 100644 index 0000000..e369816 Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/satisfy/satisfy-latin-b4c9305d.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/source-sans-pro/font.css b/Minecraft-Modern-Theme/fonts/source-sans-pro/font.css new file mode 100644 index 0000000..43ae34c --- /dev/null +++ b/Minecraft-Modern-Theme/fonts/source-sans-pro/font.css @@ -0,0 +1,49 @@ +/* Source Sans Pro – lokal gehostet (DSGVO). Generiert, nicht manuell bearbeiten. */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('source-sans-pro-latin-ext-c12498bb.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('source-sans-pro-latin-ea667083.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('source-sans-pro-latin-ext-c008dc5c.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('source-sans-pro-latin-49902363.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('source-sans-pro-latin-ext-17c620e3.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('source-sans-pro-latin-c7df1611.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-49902363.woff2 b/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-49902363.woff2 new file mode 100644 index 0000000..d376442 Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-49902363.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-c7df1611.woff2 b/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-c7df1611.woff2 new file mode 100644 index 0000000..2d10d5b Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-c7df1611.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-ea667083.woff2 b/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-ea667083.woff2 new file mode 100644 index 0000000..3f0f70a Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-ea667083.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-ext-17c620e3.woff2 b/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-ext-17c620e3.woff2 new file mode 100644 index 0000000..6c07b9d Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-ext-17c620e3.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-ext-c008dc5c.woff2 b/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-ext-c008dc5c.woff2 new file mode 100644 index 0000000..0ec07f1 Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-ext-c008dc5c.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-ext-c12498bb.woff2 b/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-ext-c12498bb.woff2 new file mode 100644 index 0000000..edaf39a Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/source-sans-pro/source-sans-pro-latin-ext-c12498bb.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/ubuntu/font.css b/Minecraft-Modern-Theme/fonts/ubuntu/font.css new file mode 100644 index 0000000..babfbb0 --- /dev/null +++ b/Minecraft-Modern-Theme/fonts/ubuntu/font.css @@ -0,0 +1,33 @@ +/* Ubuntu – lokal gehostet (DSGVO). Generiert, nicht manuell bearbeiten. */ +@font-face { + font-family: 'Ubuntu'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('ubuntu-latin-ext-b2700a09.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Ubuntu'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('ubuntu-latin-62d0e61f.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Ubuntu'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('ubuntu-latin-ext-4f2e4a8f.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Ubuntu'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('ubuntu-latin-7b2f7147.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/Minecraft-Modern-Theme/fonts/ubuntu/ubuntu-latin-62d0e61f.woff2 b/Minecraft-Modern-Theme/fonts/ubuntu/ubuntu-latin-62d0e61f.woff2 new file mode 100644 index 0000000..8cb3d32 Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/ubuntu/ubuntu-latin-62d0e61f.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/ubuntu/ubuntu-latin-7b2f7147.woff2 b/Minecraft-Modern-Theme/fonts/ubuntu/ubuntu-latin-7b2f7147.woff2 new file mode 100644 index 0000000..8eaed1f Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/ubuntu/ubuntu-latin-7b2f7147.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/ubuntu/ubuntu-latin-ext-4f2e4a8f.woff2 b/Minecraft-Modern-Theme/fonts/ubuntu/ubuntu-latin-ext-4f2e4a8f.woff2 new file mode 100644 index 0000000..a0d4e55 Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/ubuntu/ubuntu-latin-ext-4f2e4a8f.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/ubuntu/ubuntu-latin-ext-b2700a09.woff2 b/Minecraft-Modern-Theme/fonts/ubuntu/ubuntu-latin-ext-b2700a09.woff2 new file mode 100644 index 0000000..16afeec Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/ubuntu/ubuntu-latin-ext-b2700a09.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/work-sans/font.css b/Minecraft-Modern-Theme/fonts/work-sans/font.css new file mode 100644 index 0000000..f8e45dc --- /dev/null +++ b/Minecraft-Modern-Theme/fonts/work-sans/font.css @@ -0,0 +1,49 @@ +/* Work Sans – lokal gehostet (DSGVO). Generiert, nicht manuell bearbeiten. */ +@font-face { + font-family: 'Work Sans'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('work-sans-latin-ext-94b95f7b.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Work Sans'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('work-sans-latin-f7b83de3.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Work Sans'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('work-sans-latin-ext-94b95f7b.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Work Sans'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('work-sans-latin-f7b83de3.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Work Sans'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('work-sans-latin-ext-94b95f7b.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Work Sans'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('work-sans-latin-f7b83de3.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/Minecraft-Modern-Theme/fonts/work-sans/work-sans-latin-ext-94b95f7b.woff2 b/Minecraft-Modern-Theme/fonts/work-sans/work-sans-latin-ext-94b95f7b.woff2 new file mode 100644 index 0000000..6206841 Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/work-sans/work-sans-latin-ext-94b95f7b.woff2 differ diff --git a/Minecraft-Modern-Theme/fonts/work-sans/work-sans-latin-f7b83de3.woff2 b/Minecraft-Modern-Theme/fonts/work-sans/work-sans-latin-f7b83de3.woff2 new file mode 100644 index 0000000..3a70b25 Binary files /dev/null and b/Minecraft-Modern-Theme/fonts/work-sans/work-sans-latin-f7b83de3.woff2 differ diff --git a/Minecraft-Modern-Theme/inc/announcement.php b/Minecraft-Modern-Theme/inc/announcement.php new file mode 100644 index 0000000..b88592f --- /dev/null +++ b/Minecraft-Modern-Theme/inc/announcement.php @@ -0,0 +1,327 @@ + array( 'label' => 'Theme-Standard', 'css' => 'inherit', 'google' => false, 'google_name' => '' ), + 'Arial' => array( 'label' => 'Arial', 'css' => 'Arial, Helvetica, sans-serif', 'google' => false, 'google_name' => '' ), + 'Roboto' => array( 'label' => 'Roboto', 'css' => "'Roboto', sans-serif", 'google' => true, 'google_name' => 'Roboto' ), + 'Montserrat' => array( 'label' => 'Montserrat', 'css' => "'Montserrat', sans-serif", 'google' => true, 'google_name' => 'Montserrat' ), + 'Open Sans' => array( 'label' => 'Open Sans', 'css' => "'Open Sans', sans-serif", 'google' => true, 'google_name' => 'Open+Sans' ), + 'Lato' => array( 'label' => 'Lato', 'css' => "'Lato', sans-serif", 'google' => true, 'google_name' => 'Lato' ), + 'Poppins' => array( 'label' => 'Poppins', 'css' => "'Poppins', sans-serif", 'google' => true, 'google_name' => 'Poppins' ), + 'Source Sans Pro' => array( 'label' => 'Source Sans Pro', 'css' => "'Source Sans Pro', sans-serif", 'google' => true, 'google_name' => 'Source+Sans+Pro' ), + 'Noto Sans' => array( 'label' => 'Noto Sans', 'css' => "'Noto Sans', sans-serif", 'google' => true, 'google_name' => 'Noto+Sans' ), + 'Raleway' => array( 'label' => 'Raleway', 'css' => "'Raleway', sans-serif", 'google' => true, 'google_name' => 'Raleway' ), + 'Merriweather' => array( 'label' => 'Merriweather', 'css' => "'Merriweather', serif", 'google' => true, 'google_name' => 'Merriweather' ), + 'Playfair Display' => array( 'label' => 'Playfair Display', 'css' => "'Playfair Display', serif", 'google' => true, 'google_name' => 'Playfair+Display' ), + 'Oswald' => array( 'label' => 'Oswald', 'css' => "'Oswald', sans-serif", 'google' => true, 'google_name' => 'Oswald' ), + 'Rubik' => array( 'label' => 'Rubik', 'css' => "'Rubik', sans-serif", 'google' => true, 'google_name' => 'Rubik' ), + 'Inter' => array( 'label' => 'Inter', 'css' => "'Inter', sans-serif", 'google' => true, 'google_name' => 'Inter' ), + 'Nunito' => array( 'label' => 'Nunito', 'css' => "'Nunito', sans-serif", 'google' => true, 'google_name' => 'Nunito' ), + 'Ubuntu' => array( 'label' => 'Ubuntu', 'css' => "'Ubuntu', sans-serif", 'google' => true, 'google_name' => 'Ubuntu' ), + 'PT Sans' => array( 'label' => 'PT Sans', 'css' => "'PT Sans', sans-serif", 'google' => true, 'google_name' => 'PT+Sans' ), + 'Archivo' => array( 'label' => 'Archivo', 'css' => "'Archivo', sans-serif", 'google' => true, 'google_name' => 'Archivo' ), + 'Fira Sans' => array( 'label' => 'Fira Sans', 'css' => "'Fira Sans', sans-serif", 'google' => true, 'google_name' => 'Fira+Sans' ), + 'Work Sans' => array( 'label' => 'Work Sans', 'css' => "'Work Sans', sans-serif", 'google' => true, 'google_name' => 'Work+Sans' ), + 'Quicksand' => array( 'label' => 'Quicksand', 'css' => "'Quicksand', sans-serif", 'google' => true, 'google_name' => 'Quicksand' ), + 'Karla' => array( 'label' => 'Karla', 'css' => "'Karla', sans-serif", 'google' => true, 'google_name' => 'Karla' ), + 'Dancing Script' => array( 'label' => 'Dancing Script', 'css' => "'Dancing Script', cursive", 'google' => true, 'google_name' => 'Dancing+Script' ), + 'Pacifico' => array( 'label' => 'Pacifico', 'css' => "'Pacifico', cursive", 'google' => true, 'google_name' => 'Pacifico' ), + 'Great Vibes' => array( 'label' => 'Great Vibes', 'css' => "'Great Vibes', cursive", 'google' => true, 'google_name' => 'Great+Vibes' ), + 'Satisfy' => array( 'label' => 'Satisfy', 'css' => "'Satisfy', cursive", 'google' => true, 'google_name' => 'Satisfy' ), + 'Allura' => array( 'label' => 'Allura', 'css' => "'Allura', cursive", 'google' => true, 'google_name' => 'Allura' ), + 'Alex Brush' => array( 'label' => 'Alex Brush', 'css' => "'Alex Brush', cursive", 'google' => true, 'google_name' => 'Alex+Brush' ), + 'Cookie' => array( 'label' => 'Cookie', 'css' => "'Cookie', cursive", 'google' => true, 'google_name' => 'Cookie' ), + ); +} + + +function mm_announcement_admin_init() { + add_menu_page( 'Ankündigung', 'Ankündigung', 'manage_options', 'mm-announcement', 'mm_announcement_admin_page', 'dashicons-megaphone', 61 ); + + register_setting( 'mm_announcement_group', 'mm_announcement_enabled' ); + register_setting( 'mm_announcement_group', 'mm_announcement_text' ); + register_setting( 'mm_announcement_group', 'mm_announcement_bg' ); + register_setting( 'mm_announcement_group', 'mm_announcement_color' ); + register_setting( 'mm_announcement_group', 'mm_announcement_font_size' ); + register_setting( 'mm_announcement_group', 'mm_announcement_font_family' ); + register_setting( 'mm_announcement_group', 'mm_announcement_position' ); + register_setting( 'mm_announcement_group', 'mm_announcement_countdown_enabled' ); + register_setting( 'mm_announcement_group', 'mm_announcement_countdown_label' ); + register_setting( 'mm_announcement_group', 'mm_announcement_countdown_date' ); + register_setting( 'mm_announcement_group', 'mm_announcement_countdown_expired_msg' ); +} +add_action( 'admin_menu', 'mm_announcement_admin_init' ); + + +function mm_announcement_admin_page() { + if ( ! current_user_can( 'manage_options' ) ) return; + + $fonts = mm_announcement_get_font_list(); + $selected_font = get_option( 'mm_announcement_font_family', 'inherit' ); + $selected_size = (int) get_option( 'mm_announcement_font_size', 16 ); + $bg = esc_attr( get_option( 'mm_announcement_bg', '#1e1e1e' ) ); + $color = esc_attr( get_option( 'mm_announcement_color', '#ffffff' ) ); + $text_sample = wp_strip_all_tags( get_option( 'mm_announcement_text' ) ) ?: 'Das ist eine Vorschau: Wie sieht die Schrift aus?'; + ?> +
+

Header-Ankündigung

+

Diese Leiste wird auf allen Seiten angezeigt. Die Vorschau unten zeigt sofort, wie die Ankündigung aussieht — Änderungen wirken direkt in der Vorschau, erst nach Änderungen speichern werden sie im Frontend übernommen.

+ +
+ + +

Allgemein

+ + + + + +
Aktivieren + +
+ +

Inhalt

+ + + + + +
Text + 6, 'media_buttons' => false, 'tinymce' => true, 'quicktags' => true ) ); ?> + +

Verfügbare Icons

+

Klicke auf ein Icon, um es in den Editor einzufügen.

+
+ ' . $icon . ''; + } + ?> +
+ + +

HTML erlaubt (z. B. <a>-Links).

+ +
+
+ +
+

Live Vorschau

+
+
+ +

Position

+ + + + + +
Anzeigeort + +
+ +

Design

+ + + + + + + + + + + + + + + + + +
Hintergrundfarbe
Textfarbe
Schriftgröße (px)
Schriftfamilie + +
+ +

Countdown Timer

+ + + + + + + + + + + + + + + + + +
Countdown aktivieren
Label (Text vor Timer)
Zieldatum & Uhrzeit
Nachricht nach Ablauf
+ + +
+
+ + ' . $countdown_label . ' + Laden... + '; + } + ?> +
+
+
+ + +
+ +
+
+ /fonts/open-sans/. + $fonts = mm_announcement_get_font_list(); + $font_key = get_option( 'mm_announcement_font_family', 'inherit' ); + if ( isset( $fonts[ $font_key ] ) && ! empty( $fonts[ $font_key ]['google'] ) ) { + mm_enqueue_local_font( $font_key ); + } +} +add_action( 'wp_enqueue_scripts', 'mm_announcement_enqueue_assets' ); + + +function mm_announcement_admin_assets( $hook ) { + if ( $hook !== 'toplevel_page_mm-announcement' ) return; + + wp_add_inline_style( 'wp-admin', ' + .mm-announcement-admin .form-table th { width: 180px; vertical-align: top; } + .mm-announcement-admin .description { margin-top: 6px; color: #666; max-width: 720px; } + .mm-announcement-admin h2 { margin-top: 24px; } + #mm-announcement-preview { transition: all 140ms ease; } + #mm-announcement-preview-text { transition: font-family 160ms ease, font-size 120ms ease; white-space: normal; word-break: break-word; } + ' ); + + wp_enqueue_script( 'mm-announcement-admin-script', get_template_directory_uri() . '/js/mm-announcement-admin.js', array( 'jquery' ), filemtime( get_template_directory() . '/js/mm-announcement-admin.js' ), true ); + wp_localize_script( 'mm-announcement-admin-script', 'MM_Announcement_Fonts', mm_announcement_get_font_list() ); + wp_localize_script( 'mm-announcement-admin-script', 'MM_Announcement_Current', array( + 'font' => get_option( 'mm_announcement_font_family', 'inherit' ), + 'size' => (int) get_option( 'mm_announcement_font_size', 16 ), + 'bg' => get_option( 'mm_announcement_bg', '#1e1e1e' ), + 'color' => get_option( 'mm_announcement_color', '#ffffff' ), + 'text' => wp_kses_post( get_option( 'mm_announcement_text' ) ), + // Basis-URL der lokalen Schriften für die Live-Vorschau (kein Google-Aufruf) + 'fontsBase' => get_template_directory_uri() . '/fonts/', + ) ); +} +add_action( 'admin_enqueue_scripts', 'mm_announcement_admin_assets' ); +// === Announcement-Bar Höhe als CSS-Variable + Body-Klasse === +function minecraft_modern_announcement_offset_script() { + if ( ! get_option( 'mm_announcement_enabled' ) ) return; + ?> + + 'Bitte gib eine Frage ein.', - 'parts' => [], - ] ); - } - - // "regeln:Kategorie" → direkt zu Rules-Intent, kein Scoring nötig - if ( preg_match( '/^regeln?:/iu', $q ) ) { - $bot = get_option( 'mm_bot_data', [] ); - $result = mm_intent_rules( $q, $bot ); - wp_send_json_success( mm_build_response( [ $result ] ) ); - } - - // "ban:Steve" → direkt zu Ban-Intent mit Spielername - if ( preg_match( '/^ban:(.+)$/iu', $q, $m ) ) { - $bot = get_option( 'mm_bot_data', [] ); - $name = trim( $m[1] ); - $result = mm_intent_ban_check( $q, $name, $bot ); - wp_send_json_success( mm_build_response( [ $result ] ) ); - } - - try { - $bot = get_option( 'mm_bot_data', [] ); - $parts = []; - - // ---- 1. Manuelle Q&A (höchste Priorität) -------------------- - if ( ! empty( $bot['qa'] ) ) { - foreach ( $bot['qa'] as $item ) { - if ( empty( $item['keys'] ) ) continue; - $keys = array_map( 'trim', explode( ',', mb_strtolower( $item['keys'] ) ) ); - foreach ( $keys as $k ) { - if ( $k !== '' && strpos( $q_lc, $k ) !== false ) { - $parts[] = [ - 'source' => 'qa', - 'content' => wp_kses_post( $item['val'] ), - ]; - break 2; - } - } - } - } - - if ( ! empty( $parts ) ) { - wp_send_json_success( mm_build_response( $parts ) ); - } - - // ---- Intent-Scores berechnen -------------------------------- - $intents = mm_score_intents( $q_lc, $q ); - arsort( $intents ); - $active = array_filter( $intents, function( $s ) { return $s > 0; } ); - - $player_name = mm_extract_player_name( $q ); - - // Wenn Discord-Intent erkannt wurde, NUR diesen ausführen - if (isset($active['discord'])) { - $result = mm_handle_intent('discord', $q, $q_lc, $player_name, $bot); - if ($result) { - $parts[] = $result; - } - } elseif (isset($active['team'])) { - $result = mm_handle_intent('team', $q, $q_lc, $player_name, $bot); - if ($result) { - $parts[] = $result; - } - } else { - foreach ( $active as $intent => $score ) { - // Ban-Intent dominant → Regeln nicht zusätzlich auslösen - if ( $intent === 'rules' && isset( $intents['ban_check'] ) && $intents['ban_check'] >= 25 ) { - continue; - } - // Ban-Intent dominant → Shop/Ticket/Forum nicht zusätzlich auslösen - if ( in_array( $intent, array( 'shop', 'ticket', 'forum' ), true ) - && isset( $intents['ban_check'] ) && $intents['ban_check'] >= 40 ) { - continue; - } - // Player-History dominant → Server-Status nicht zusätzlich auslösen - if ( $intent === 'server_status' - && isset( $intents['player_history'] ) && $intents['player_history'] >= 15 ) { - continue; - } - $result = mm_handle_intent( $intent, $q, $q_lc, $player_name, $bot ); - if ( $result ) { - $parts[] = $result; - } - } - } - - if ( empty( $parts ) ) { - $parts[] = mm_fallback_response( $bot ); - } - - wp_send_json_success( mm_build_response( $parts ) ); - - } catch ( \Throwable $e ) { - wp_send_json_success( [ - 'reply' => '⚠️ Interner Fehler: ' . esc_html( $e->getMessage() ), - 'parts' => [], - ] ); - } -} - -// ============================================================ -// INTENT SCORING -// Jeder Treffer erhöht den Score; der Intent mit dem höchsten -// Score wird zuerst beantwortet. Mehrere Intents möglich. -// ============================================================ - -function mm_score_intents( $q_lc, $q_raw ) { - $scores = [ - 'server_status' => 0, - 'ban_check' => 0, - 'player_history' => 0, - 'rules' => 0, - 'wiki' => 0, - 'shop' => 0, - 'ticket' => 0, - 'forum' => 0, - 'gallery' => 0, - 'faq' => 0, - 'discord' => 0, - 'team' => 0, - ]; - // Team - foreach ([ - 'team', 'teammitglieder', 'admins', 'moderatoren', 'wer ist im team', 'wer sind die admins', 'wer sind die moderatoren', 'serverteam', 'staff', 'supporter', 'admin', 'mod', 'leitung', 'server leitung', 'wer ist staff', 'wer ist admin', 'wer ist mod' - ] as $kw) { - if (strpos($q_lc, $kw) !== false) $scores['team'] += 20; - } - // Discord - foreach (['discord', 'dc', 'discord link', 'discord einladung', 'discord server', 'discord invite'] as $kw) { - if (strpos($q_lc, $kw) !== false) $scores['discord'] += 20; - } - - // Server-Status - foreach ( ['ip', 'adresse', 'join', 'einloggen', 'verbinden', 'server ip', - 'wie komme', 'online spieler', 'wieviele spieler', 'server status', - 'serveradresse', 'spieler online'] as $kw ) { - if ( strpos( $q_lc, $kw ) !== false ) $scores['server_status'] += 10; - } - - // Ban / Mute / Kick - foreach ( ['ban', 'gebannt', 'gesperrt', 'mute', 'gemutet', 'stumm', - 'kick', 'gekickt', 'warn', 'verwarnung', 'entban', 'unban', - 'antrag', 'sperre', 'strafe', 'strafen', 'bestraft', - 'meine strafen', 'mein ban', 'sanktion'] as $kw ) { - if ( strpos( $q_lc, $kw ) !== false ) $scores['ban_check'] += 15; - } - // Stärker gewichten bei Ich-Bezug - if ( preg_match( '/\b(bin ich|wurde ich|meine|mein)\b.*(gebannt|gesperrt|gemutet|strafe|ban|mute)/i', $q_raw ) ) { - $scores['ban_check'] += 20; - } - if ( preg_match( '/\b(meine strafen|strafen prüfen|mein status)\b/i', $q_raw ) ) { - $scores['ban_check'] += 25; - } - - // Player History - foreach ( ['spielzeit', 'playtime', 'zuletzt online', 'last seen', - 'letzte login', 'wie lang', 'wie lange', 'war online', - 'online status', 'ist online', 'stunden gespielt'] as $kw ) { - if ( strpos( $q_lc, $kw ) !== false ) $scores['player_history'] += 15; - } - - // Regeln - foreach ( array( 'regel', 'regeln', 'regelwerk', 'darf ich', 'ist erlaubt', - 'verboten', 'richtlinie', 'grief', 'stehlen', 'pvp', - 'chat regel', 'rule', 'verbote', - 'cheaten', 'cheat', 'hacks', 'hacken', 'modifikation', - 'beleidigung', 'schimpfen', 'respekt' ) as $kw ) { - if ( strpos( $q_lc, $kw ) !== false ) $scores['rules'] += 12; - } - - // Wiki - foreach ( ['wiki', 'dokumentation', 'guide', 'anleitung', 'howto', - 'how to', 'wie funktioniert', 'erkläre', 'erklärt'] as $kw ) { - if ( strpos( $q_lc, $kw ) !== false ) $scores['wiki'] += 12; - } - - // Shop - foreach ( [ - 'shop', 'kaufen', 'item', 'items', 'preis', 'kosten', - 'coins', 'tagesaktion', 'daily deal', 'angebot', 'kategorie', - 'warenkorb', 'bestellen', 'rang kaufen', - 'was kostet', 'wie teuer', 'preis von', 'kosten von', 'preis für', 'wie viel kostet' - ] as $kw ) { - if ( strpos( $q_lc, $kw ) !== false ) $scores['shop'] += 12; - } - - // Ticket / Support - foreach ( ['ticket', 'support', 'hilfe', 'problem melden', - 'bug melden', 'melde', 'anfrage', 'beschwerde', - 'anliegen', 'kontaktier'] as $kw ) { - if ( strpos( $q_lc, $kw ) !== false ) $scores['ticket'] += 12; - } - - // Forum - foreach ( ['forum', 'diskussion', 'thread', - 'neuen thread', 'thread erstellen', 'im forum posten', 'im forum schreiben', - 'forum login', 'passwort vergessen', 'forum kategorie', - 'forum nachricht', 'direktnachricht', 'forum profil', - 'forum rang', 'forum level', 'forum mitglieder'] as $kw ) { - if ( strpos( $q_lc, $kw ) !== false ) $scores['forum'] += 10; - } - - // Gallery - foreach ( [ - 'galerie', 'gallery', 'bild', 'bilder', 'screenshot', 'screenshots', - 'foto', 'fotos', 'tagesbild', 'bild des tages', - 'hochladen', 'upload', 'uploaden', 'bild teilen', 'bilder teilen', - 'verifizier', 'verify', '/verify', 'token galerie', - 'meine galerie', 'spieler galerie', 'galerie upload', - ] as $kw ) { - if ( strpos( $q_lc, $kw ) !== false ) $scores['gallery'] += 10; - } - // Upload-Kontext stärker gewichten - if ( preg_match( '/\b(wie|kann ich|wie kann|anleitung|schritt)\b.*(bild|foto|screenshot|hochlad|upload)/i', $q_lc ) ) { - $scores['gallery'] += 20; - } - - // FAQ: Mehr Keywords und höheres Scoring, damit typische Fragen wie "Wie ...?", "Was ...?", "Wieso ...?", "Erklärung ..." erkannt werden - foreach ([ - 'faq', 'häufige frage', 'frequently', 'oft gefragt', - 'wie ', 'was ', 'wieso ', 'wo ', 'wer ', 'hilfe', 'erklärung', 'funktioniert', 'bedeutet', 'nutzen', 'benutzen', 'erkläre', 'erklärt', 'erfahre', 'erfahren', 'frage', 'fragen', 'hilfe', 'problem', 'tipp', 'tipps', 'anleitung', 'erläutere', 'erläutert', 'erläutern', 'erläutere', 'erläutert', 'erläutern', 'beschreibe', 'beschreibt', 'beschreiben', 'beschreibe', 'beschreibt', 'beschreiben', 'wie kann', 'wie mache', 'wie mache ich', 'wie funktioniert', 'wie nutze', 'wie benutze', 'wie setze', 'wie entferne', 'wie öffne', 'wie melde', 'wie erreiche', 'wie akzeptiere', 'wie fordere', 'wie wechsle', 'wie sehe', 'wie teleportiere', 'wie sende', 'wie setze ich', 'wie entferne ich', 'wie öffne ich', 'wie melde ich', 'wie erreiche ich', 'wie akzeptiere ich', 'wie fordere ich', 'wie wechsle ich', 'wie sehe ich', 'wie teleportiere ich', 'wie sende ich' - ] as $kw) { - if ( strpos( $q_lc, $kw ) !== false ) $scores['faq'] += 18; - } - - return $scores; -} - -// ============================================================ -// INTENT HANDLER DISPATCHER -// ============================================================ - -function mm_handle_intent( $intent, $q, $q_lc, $player_name, $bot ) { - switch ( $intent ) { - case 'server_status': return mm_intent_server_status( $bot ); - case 'ban_check': return mm_intent_ban_check( $q, $player_name, $bot ); - case 'player_history': return mm_intent_player_history( $q, $player_name ); - case 'rules': return mm_intent_rules( $q, $bot ); - case 'wiki': return mm_intent_wiki( $q ); - case 'shop': return mm_intent_shop( $q_lc, $bot ); - case 'ticket': return mm_intent_ticket( $q, $bot ); - case 'forum': return mm_intent_forum( $q ); - case 'gallery': return mm_intent_gallery( $bot, $q_lc ); - case 'faq': return mm_intent_faq( $q ); - case 'discord': return mm_intent_discord( $bot ); - case 'team': return mm_intent_team(); - } -} - -// ============================================================ -// INTENT: TEAM -// ============================================================ -function mm_intent_team() { - $query = new WP_Query( array( - 'post_type' => 'team_member', - 'posts_per_page' => -1, - 'orderby' => 'menu_order', - 'order' => 'ASC', - ) ); - if ( ! $query->have_posts() ) { - return [ - 'source' => 'team', - 'title' => '👥 Team', - 'content' => 'Es wurden noch keine Teammitglieder eingetragen.', - ]; - } - $html = '
'; - $html .= '
👥 Team
'; - while ( $query->have_posts() ) { - $query->the_post(); - $name = get_the_title(); - $rank = get_post_meta( get_the_ID(), '_team_member_rank', true ); - $uuid = get_post_meta( get_the_ID(), '_team_member_uuid', true ); - $img = get_the_post_thumbnail_url( get_the_ID(), 'medium' ); - $desc = get_the_excerpt(); - $avatar = ''; - if ( $uuid ) { - $avatar = 'https://visage.surgeplay.com/bust/' . esc_attr( $uuid ) . '.png'; - } elseif ( $img ) { - $avatar = $img; - } - $html .= '
'; - if ( $avatar ) { - $html .= '' . esc_attr( $name ) . ''; - } else { - $html .= '
' . esc_html( mb_substr( $name, 0, 1 ) ) . '
'; - } - $html .= '
'; - $html .= '
' . esc_html( $name ) . '
'; - if ( $rank ) { - $html .= '
' . esc_html( $rank ) . '
'; - } - if ( $desc ) { - $html .= '
' . esc_html( $desc ) . '
'; - } - $html .= '
'; - $html .= '
'; - } - $html .= '
'; - wp_reset_postdata(); - return [ - 'source' => 'team', - 'title' => '👥 Team', - 'content' => $html, - ]; -} -// ============================================================ -// INTENT: DISCORD -// ============================================================ - -function mm_intent_discord( $bot ) { - $discord = $bot['link_discord'] ?? ''; - if ( ! empty( $discord ) ) { - return [ - 'source' => 'discord', - 'title' => '💬 Discord', - 'content' => 'Hier ist der Discord-Einladungslink:
' . esc_html( $discord ) . '', - ]; - } else { - return [ - 'source' => 'discord', - 'title' => '💬 Discord', - 'content' => 'Es wurde kein Discord-Link im Backend hinterlegt.', - ]; - } -} - -// ============================================================ -// INTENT: SERVER STATUS (BungeeCord Status Plugin) -// ============================================================ - -function mm_intent_server_status( $bot ) { - $lines = array(); - - // ── 1. Statische Infos aus Bot-Setup ────────────────────── - $ip = $bot['server_ip'] ?? ''; - $ver = $bot['server_ver'] ?? ''; - $specs = $bot['server_specs'] ?? ''; - $discord = $bot['link_discord'] ?? ''; - - if ( $ip ) $lines[] = '🌐 Adresse: ' . esc_html( $ip ) . ''; - if ( $ver ) $lines[] = '🎮 Version: ' . esc_html( $ver ); - if ( $specs ) $lines[] = '🖥️ Server: ' . esc_html( $specs ); - - // ── 2. Live-Daten via BungeeCord-Status-Plugin ───────────── - $servers = get_option( 'mcss_servers', array() ); - - if ( ! empty( $servers ) && function_exists( 'mcss_fetch_server_with_ranks' ) ) { - $srv = $servers[0]; - $cache_key = 'mm_srv_' . md5( ( $srv['host'] ?? '' ) . ( $srv['player_port'] ?? '9191' ) ); - $data = get_transient( $cache_key ); - - // Caching-Zeit erhöht auf 60 Sekunden - if ( false === $data ) { - try { - $data = mcss_fetch_server_with_ranks( $srv ); - } catch ( \Throwable $e ) { - $data = null; - } - set_transient( $cache_key, $data, 60 ); - } - - if ( ! empty( $data ) && isset( $data['online'] ) ) { - $is_online = (bool) $data['online']; - $icon = $is_online ? '🟢' : '🔴'; - $status = $is_online ? 'Online' : 'Offline'; - - // Spieler-Anzahl (players = Array von Objekten) - $player_list = isset( $data['players'] ) && is_array( $data['players'] ) ? $data['players'] : array(); - $player_count = count( $player_list ); - $player_text = $is_online ? ' · ' . $player_count . ' Spieler online' : ''; - - $srv_label = ! empty( $srv['name'] ) ? esc_html( $srv['name'] ) . ': ' : ''; - $lines[] = $icon . ' ' . $srv_label . $status . $player_text; - - // Version vom Server - if ( $is_online && ! empty( $data['version'] ) && empty( $ver ) ) { - $lines[] = '🎮 Version: ' . esc_html( $data['version'] ); - } - - // MOTD - if ( $is_online && ! empty( $data['motd'] ) ) { - $motd = is_array( $data['motd'] ) - ? implode( ' ', $data['motd'] ) - : (string) $data['motd']; - $motd = trim( $motd ); - if ( $motd ) { - $lines[] = '💬 ' . mm_format_motd( $motd ); - } - } - - // Spieler auflisten (bis 8) - if ( $is_online && ! empty( $player_list ) ) { - $names = array(); - foreach ( array_slice( $player_list, 0, 8 ) as $p ) { - if ( is_array( $p ) && ! empty( $p['name'] ) ) { - $names[] = esc_html( $p['name'] ); - } elseif ( is_string( $p ) ) { - $names[] = esc_html( $p ); - } - } - if ( ! empty( $names ) ) { - $lines[] = '👥 Spieler: ' . implode( ', ', $names ) - . ( $player_count > 8 ? ' +' . ( $player_count - 8 ) . ' weitere' : '' ); - } - } - } - - // Weitere konfigurierte Server kurz anzeigen - if ( count( $servers ) > 1 ) { - $extra = array(); - foreach ( array_slice( $servers, 1 ) as $s ) { - if ( empty( $s['name'] ) ) continue; - $ck = 'mm_srv_' . md5( ( $s['host'] ?? '' ) . ( $s['player_port'] ?? '9191' ) ); - $d = get_transient( $ck ); - if ( false === $d && function_exists( 'mcss_fetch_server_with_ranks' ) ) { - try { - $d = mcss_fetch_server_with_ranks( $s ); - } catch ( \Throwable $e ) { - $d = null; - } - set_transient( $ck, $d, 60 ); - } - $on = ! empty( $d['online'] ); - $count = ( $on && ! empty( $d['players'] ) ) ? count( $d['players'] ) : 0; - $extra[] = ( $on ? '🟢' : '🔴' ) . ' ' . esc_html( $s['name'] ) - . ( $on ? ' (' . $count . ')' : '' ); - } - if ( ! empty( $extra ) ) { - $lines[] = '🗺️ Weitere Server: ' . implode( ' · ', $extra ); - } - } - } - - // ── 3. Kein Plugin, keine Daten ─────────────────────────── - if ( empty( $lines ) ) { - return array( - 'source' => 'server_status', - 'title' => '🖥️ Server-Status', - 'content' => 'Server-Infos wurden noch nicht konfiguriert. Bitte im Bot-Setup eintragen.', - ); - } - - // ── 4. Links (nur Discord, kein Wiki-Link mehr) ───────────── - if ( $discord ) { - $lines[] = '💬 Discord'; - } - - return array( - 'source' => 'server_status', - 'title' => '🖥️ Server-Status', - 'content' => implode( '
', $lines ), - ); -} - -// ============================================================ -// HILFSFUNKTION: Minecraft §-Farbcodes → HTML -// Unterstützt §0-§f (Farben) und §l/§o/§n/§m/§r (Format) -// ============================================================ - -function mm_format_motd( $text ) { - if ( empty( $text ) ) return ''; - - $colors = 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', - ); - - // Normalisieren: § und & beide unterstützen - $section = "\xC2\xA7"; // UTF-8 für § - $text = str_replace( '&', $section, $text ); - - // Aufteilen nach §X Segmenten - $parts = preg_split( '/\xC2\xA7([0-9a-fklmnorA-FKLMNOR])/', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); - $output = ''; - $open = 0; - $bold = false; - $italic = false; - - foreach ( $parts as $i => $part ) { - if ( $i % 2 === 0 ) { - // Text-Segment - if ( $part !== '' ) { - $output .= esc_html( $part ); - } - } else { - // Code-Segment - $code = strtolower( $part ); - - // Offene Spans schließen - if ( $open > 0 ) { - $output .= str_repeat( '', $open ); - $open = 0; - } - if ( $bold ) { $output .= ''; $bold = false; } - if ( $italic ) { $output .= ''; $italic = false; } - - if ( isset( $colors[ $code ] ) ) { - $output .= ''; - $open++; - } elseif ( $code === 'l' ) { - $output .= ''; - $bold = true; - } elseif ( $code === 'o' ) { - $output .= ''; - $italic = true; - } elseif ( $code === 'r' ) { - // Reset — alles schon geschlossen oben - } - // §k (obfuscated), §m (strike), §n (underline) ignorieren wir - } - } - - // Offene Tags schließen - if ( $open > 0 ) $output .= str_repeat( '', $open ); - if ( $bold ) $output .= ''; - if ( $italic ) $output .= ''; - - return $output; -} - -// ============================================================ -// INTENT: BAN / MUTE / KICK / WARN (LiteBans Manager) -// ============================================================ - -function mm_intent_ban_check( $q, $player_name, $bot ) { - $dashboard_url = isset( $bot["litebans_dashboard_url"] ) ? $bot["litebans_dashboard_url"] : ""; - - if ( empty( $player_name ) ) { - $html = '🔍 Welchen Spielernamen soll ich prüfen?

'; - $html .= 'Tippe deinen Minecraft-Namen direkt ein:

'; - $html .= '
'; - $html .= 'Format:'; - $html .= ''; - $html .= '
'; - $html .= '
Ersetze DeinName mit deinem echten Minecraft-Namen.
'; - $html .= 'Oder schreibe z.B.: ist Steve gebannt
'; - if ( $dashboard_url ) { - $html .= '

LiteBans Dashboard öffnen'; - } - return array( "source" => "ban_check", "title" => "🔨 Strafen prüfen", "content" => $html ); - } - - $info = mm_litebans_query( $player_name ); - - if ( is_null( $info ) ) { - return array( "source" => "ban_check", "title" => "🔨 Ban-Status", - "content" => "⚠️ LiteBans ist nicht konfiguriert oder die Datenbank ist nicht erreichbar." ); - } - - if ( isset( $info["not_found"] ) ) { - return array( "source" => "ban_check", "title" => "🔨 Ban-Status: " . esc_html( $player_name ), - "content" => "❓ Spieler " . esc_html( $player_name ) . " wurde in der LiteBans-Datenbank nicht gefunden." ); - } - - $lines = array(); - - // Avatar + Name - $lines[] = "
" - . "Avatar" - . "" . esc_html( $info["player"] ) . "" - . "
"; - - // Aktiver Ban - if ( $info["active_ban"] ) { - $b = $info["active_ban"]; - $lines[] = "
"; - $lines[] = "🚫 Aktiver Ban"; - $lines[] = "📋 Grund: " . esc_html( $b["reason"] ); - $lines[] = "👮 Von: " . esc_html( $b["by"] ); - $lines[] = "⏰ Bis: " . esc_html( $b["until"] ) . ""; - $lines[] = "
"; - } else { - $lines[] = "✅ Kein aktiver Ban"; - } - - // Aktiver Mute - if ( $info["active_mute"] ) { - $m = $info["active_mute"]; - $lines[] = "
"; - $lines[] = "🔇 Aktiver Mute"; - $lines[] = "📋 Grund: " . esc_html( $m["reason"] ); - $lines[] = "⏰ Bis: " . esc_html( $m["until"] ) . ""; - $lines[] = "
"; - } else { - $lines[] = "✅ Kein aktiver Mute"; - } - - // Kicks & Warns - $stats = array(); - if ( $info["kicks"] > 0 ) $stats[] = "👢 " . $info["kicks"] . " Kick(s)"; - if ( $info["warnings"] > 0 ) $stats[] = "⚠️ " . $info["warnings"] . " Verwarnung(en)"; - if ( ! empty( $stats ) ) $lines[] = implode( " · ", $stats ); - - // Ban-History - if ( ! empty( $info["ban_history"] ) ) { - $lines[] = "
🕐 Letzte Bestrafungen:"; - foreach ( $info["ban_history"] as $h ) { - $icon = $h["type"] === "ban" ? "🚫" : ( $h["type"] === "mute" ? "🔇" : ( $h["type"] === "kick" ? "👢" : "⚠️" ) ); - $lines[] = $icon . " " . esc_html( $h["date"] ) - . " – " . esc_html( $h["reason"] ) - . " (" . esc_html( $h["by"] ) . ")"; - } - } - - // Entbannungsanträge - if ( ! empty( $info["unban_requests"] ) ) { - $lines[] = "
📋 Entbannungsanträge:"; - foreach ( $info["unban_requests"] as $r ) { - $lines[] = $r["status"] . " – " . esc_html( $r["date"] ); - } - } - - // Links - $link_parts = array(); - if ( $dashboard_url ) { - $link_parts[] = "Dashboard"; - } - if ( $info["active_ban"] && $dashboard_url ) { - $unban_url = add_query_arg( array( - "lb_player" => $info["player"], - "lb_reason" => $info["active_ban"]["reason"], - ), $dashboard_url ); - $link_parts[] = "Entbannungsantrag stellen →"; - } - if ( ! empty( $link_parts ) ) $lines[] = "
" . implode( " · ", $link_parts ); - - $status_icon = $info["active_ban"] ? "🚫" : "✅"; - - return array( - "source" => "ban_check", - "title" => $status_icon . " Ban-Status: " . esc_html( $info["player"] ), - "content" => implode( "
", $lines ), - ); -} - -function mm_litebans_query( $player_name ) { - $settings = get_option( "wp_litebans_pro_settings", array() ); - if ( empty( $settings["db_name"] ) || empty( $settings["db_user"] ) ) return null; - - $prefix = isset( $settings["table_prefix"] ) ? $settings["table_prefix"] : "litebans_"; - $db_host = isset( $settings["db_host"] ) ? $settings["db_host"] : "localhost"; - if ( ! empty( $settings["db_port"] ) ) $db_host .= ":" . intval( $settings["db_port"] ); - - try { - $lbdb = new wpdb( $settings["db_user"], $settings["db_pass"], $settings["db_name"], $db_host ); - $lbdb->suppress_errors( true ); - $lbdb->prefix = $prefix; - } catch ( Exception $e ) { - return null; - } - - $name = sanitize_text_field( $player_name ); - - // UUID über history-Tabelle (wie das Plugin selbst) - $uuid = $lbdb->get_var( $lbdb->prepare( - "SELECT uuid FROM {$prefix}history WHERE name = %s ORDER BY id DESC LIMIT 1", $name - ) ); - - if ( empty( $uuid ) ) { - // Fuzzy-Suche - $row = $lbdb->get_row( $lbdb->prepare( - "SELECT uuid, name FROM {$prefix}history WHERE name LIKE %s ORDER BY id DESC LIMIT 1", - "%" . $lbdb->esc_like( $name ) . "%" - ) ); - if ( ! $row ) return array( "not_found" => true ); - $uuid = $row->uuid; - $name = $row->name; - } - - $info = array( "player" => $name, "active_ban" => null, "active_mute" => null, - "kicks" => 0, "warnings" => 0, "ban_history" => array(), "unban_requests" => array() ); - - // Aktiver Ban (via UUID) - $ban = $lbdb->get_row( $lbdb->prepare( - "SELECT reason, banned_by_name, until, active, removed_by_name FROM {$prefix}bans WHERE uuid = %s AND active = 1 ORDER BY time DESC LIMIT 1", $uuid - ) ); - if ( $ban && intval( $ban->active ) === 1 && empty( $ban->removed_by_name ) ) { - $until = (int) $ban->until; - $info["active_ban"] = array( - "reason" => mm_lb_clean( $ban->reason ), - "by" => $ban->banned_by_name, - "until" => ( $until <= 0 ) ? "Permanent" : date_i18n( "d.m.Y H:i", $until / 1000 ), - ); - } - - // Aktiver Mute (via UUID) - $mute = $lbdb->get_row( $lbdb->prepare( - "SELECT reason, banned_by_name, until, active, removed_by_name FROM {$prefix}mutes WHERE uuid = %s AND active = 1 ORDER BY time DESC LIMIT 1", $uuid - ) ); - if ( $mute && intval( $mute->active ) === 1 && empty( $mute->removed_by_name ) ) { - $until = (int) $mute->until; - $info["active_mute"] = array( - "reason" => mm_lb_clean( $mute->reason ), - "by" => $mute->banned_by_name, - "until" => ( $until <= 0 ) ? "Permanent" : date_i18n( "d.m.Y H:i", $until / 1000 ), - ); - } - - // Kicks & Warnings (UUID) - $info["kicks"] = (int) $lbdb->get_var( $lbdb->prepare( "SELECT COUNT(*) FROM {$prefix}kicks WHERE uuid = %s", $uuid ) ); - if ( $lbdb->get_var( "SHOW TABLES LIKE '{$prefix}warnings'" ) ) { - $info["warnings"] = (int) $lbdb->get_var( $lbdb->prepare( "SELECT COUNT(*) FROM {$prefix}warnings WHERE uuid = %s", $uuid ) ); - } - - // Ban-History (letzte 5 aller Typen) - $history = array(); - foreach ( array( "bans", "mutes", "kicks" ) as $t ) { - $rows = $lbdb->get_results( $lbdb->prepare( - "SELECT reason, banned_by_name, time FROM {$prefix}{$t} WHERE uuid = %s ORDER BY time DESC LIMIT 3", $uuid - ) ); - $type = rtrim( $t, "s" ); // bans→ban, mutes→mute, kicks→kick - foreach ( (array) $rows as $r ) { - $history[] = array( "type" => $type, "date" => date_i18n( "d.m.Y", $r->time / 1000 ), - "reason" => mm_lb_clean( $r->reason ), "by" => $r->banned_by_name, "ts" => $r->time ); - } - } - usort( $history, function( $a, $b ) { return $b["ts"] - $a["ts"]; } ); - $info["ban_history"] = array_slice( $history, 0, 5 ); - - // Entbannungsanträge (WP CPT) - $requests = get_posts( array( "post_type" => "unban_request", "posts_per_page" => 5, - "post_status" => "any", "meta_query" => array( array( "key" => "_lb_player", "value" => $name, "compare" => "=" ) ) ) ); - foreach ( $requests as $r ) { - $status = get_post_meta( $r->ID, "_lb_status", true ) ?: "pending"; - $label = $status === "approved" ? "✅ Angenommen" : ( $status === "rejected" ? "❌ Abgelehnt" : "⏳ Wartend" ); - $info["unban_requests"][] = array( "date" => get_the_date( "d.m.Y", $r->ID ), "status" => $label ); - } - - return $info; -} - -function mm_lb_clean( $text ) { - if ( ! $text ) return "–"; - return preg_replace( "/(?:§|&)[0-9a-fk-orA-FK-OR]/u", "", $text ); -} - -// ============================================================ -// INTENT: PLAYER HISTORY (MC Player History Plugin) -// ============================================================ -// INTENT: PLAYER HISTORY (MC Player History Plugin) -// ============================================================ - -function mm_intent_player_history( $q, $player_name ) { - $bot = get_option( 'mm_bot_data', [] ); - - // Nur aktiv wenn URL im Backend gesetzt - if ( empty( $bot['url_player_history'] ) ) return null; - - $history_url = $bot['url_player_history']; - - // Kein Spielername → Top-Liste + Hinweis - if ( empty( $player_name ) ) { - global $wpdb; - $table = $wpdb->prefix . 'mc_players'; - if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) { - return [ - 'source' => 'player_history', - 'title' => '👤 Spieler-History', - 'content' => 'Das MC Player History Plugin ist nicht installiert oder die Tabelle fehlt.', - ]; - } - - // Top 5 nach Spielzeit - $top = $wpdb->get_results( - "SELECT username, prefix, playtime_seconds, is_online - FROM $table WHERE playtime_seconds > 0 - ORDER BY playtime_seconds DESC LIMIT 5" - ); - - $html = 'Nenne mir einen Minecraft-Namen, z.B.: Spielzeit von Steve

'; - - if ( ! empty( $top ) ) { - $html .= '🏆 Top-Spieler:
'; - $i = 1; - foreach ( $top as $p ) { - $prefix_html = mm_ph_format_prefix( $p->prefix ); - $time_str = mm_format_playtime( (int) $p->playtime_seconds ); - $online_dot = $p->is_online ? '🟢' : '⚫'; - $html .= $online_dot . ' ' . ( $prefix_html ? $prefix_html . ' ' : '' ) - . '' . esc_html( $p->username ) . '' - . ' – ' . esc_html( $time_str ) . '
'; - $i++; - } - $html .= '
'; - } - - $html .= "→ Alle Spieler ansehen"; - - return [ - 'source' => 'player_history', - 'title' => '👤 Spieler-History', - 'content' => $html, - ]; - } - - // Spieler suchen - global $wpdb; - $table = $wpdb->prefix . 'mc_players'; - - if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) { - return [ - 'source' => 'player_history', - 'title' => '👤 Spieler-Info', - 'content' => 'MC Player History Plugin ist nicht aktiv oder Tabelle fehlt.', - ]; - } - - // Exakter Treffer - $row = $wpdb->get_row( $wpdb->prepare( - "SELECT username, prefix, is_online, first_seen, last_seen, playtime_seconds - FROM $table WHERE username = %s LIMIT 1", - $player_name - ) ); - - // Fuzzy-Suche als Fallback - if ( ! $row ) { - $row = $wpdb->get_row( $wpdb->prepare( - "SELECT username, prefix, is_online, first_seen, last_seen, playtime_seconds - FROM $table WHERE username LIKE %s LIMIT 1", - '%' . $wpdb->esc_like( $player_name ) . '%' - ) ); - } - - if ( ! $row ) { - return [ - 'source' => 'player_history', - 'title' => '👤 Spieler: ' . esc_html( $player_name ), - 'content' => '❓ Spieler ' . esc_html( $player_name ) . ' wurde noch nicht auf dem Server gesehen.' - . '

Spieler-Liste öffnen', - ]; - } - - $online_icon = $row->is_online - ? '🟢 Gerade online' - : '⚫ Offline'; - - $playtime = mm_format_playtime( (int) $row->playtime_seconds ); - $last_seen = $row->last_seen - ? wp_date( 'd.m.Y \u\m H:i \U\h\r', strtotime( $row->last_seen ) ) - : 'Unbekannt'; - $first_seen = $row->first_seen - ? wp_date( 'd.m.Y', strtotime( $row->first_seen ) ) - : 'Unbekannt'; - - $lines = [ - '📶 Status: ' . $online_icon, - '⏱️ Spielzeit: ' . esc_html( $playtime ) . '', - '🕐 Zuletzt gesehen: ' . esc_html( $last_seen ) . '', - '📅 Erstmals gesehen: ' . esc_html( $first_seen ) . '', - ]; - - // Rang / Prefix (mit MC-Farben) - if ( ! empty( $row->prefix ) ) { - $prefix_html = mm_ph_format_prefix( $row->prefix ); - if ( $prefix_html !== '' ) { - $lines[] = '🏷️ Rang: ' . $prefix_html; - } - } - - // Avatar - $lines[] = 'Avatar ' - . 'Spieler-Liste öffnen'; - - return [ - 'source' => 'player_history', - 'title' => '👤 Spieler: ' . esc_html( $row->username ), - 'content' => implode( '
', $lines ), - ]; -} - -/** - * Minecraft-Farbcodes in HTML umwandeln. - * Nutzt mcph_parse_minecraft_colors() vom Plugin wenn verfügbar, - * ansonsten eigene Implementierung als Fallback. - */ -function mm_ph_format_prefix( $prefix ) { - if ( empty( $prefix ) ) return ''; - - // Plugin-eigene Funktion bevorzugen - if ( function_exists( 'mcph_parse_minecraft_colors' ) ) { - return mcph_parse_minecraft_colors( $prefix ); - } - - // Fallback: eigene Implementierung - $map = [ - '&0' => '', - '&1' => '', - '&2' => '', - '&3' => '', - '&4' => '', - '&5' => '', - '&6' => '', - '&7' => '', - '&8' => '', - '&9' => '', - '&a' => '', - '&b' => '', - '&c' => '', - '&d' => '', - '&e' => '', - '&f' => '', - '&l' => '', - '&o' => '', - '&n' => '', - '&m' => '', - ]; - - // Anzahl geöffneter Spans zählen für sauberes Schließen - $open = 0; - $text = str_ireplace( array_keys( $map ), array_values( $map ), esc_html( $prefix ) ); - - // &r = alle schließen - $text = preg_replace_callback( '/&r/i', function() use ( &$open ) { - $close = str_repeat( '', $open ); - $open = 0; - return $close; - }, $text ); - - // Spans mitzählen - preg_match_all( '//', $text, $closes ); - $remaining = count( $opens[0] ) - count( $closes[0] ); - if ( $remaining > 0 ) { - $text .= str_repeat( '', $remaining ); - } - - return $text; -} - -/** Sekunden in lesbare Spielzeit umrechnen */ -function mm_format_playtime( $seconds ) { - if ( $seconds <= 0 ) return '0 Minuten'; - $h = floor( $seconds / 3600 ); - $m = floor( ( $seconds % 3600 ) / 60 ); - if ( $h > 0 ) return "{$h}h {$m}min"; - return "{$m} Minuten"; -} - -// ============================================================ -// INTENT: REGELN (Multi Rules Plugin) -// ============================================================ - -function mm_intent_rules( $q, $bot ) { - $rules_url = ! empty( $bot['url_rules'] ) ? $bot['url_rules'] : home_url( '/regeln' ); - $options = get_option( 'mrp_settings' ); - $tabs = isset( $options['tabs'] ) && is_array( $options['tabs'] ) ? $options['tabs'] : array(); - - // ── Schritt 2: Kategorie wurde gewählt → "regeln:Minecraft" ── - if ( preg_match( '/^regeln?:(.+)$/iu', trim( $q ), $m ) ) { - $chosen = trim( $m[1] ); - return mm_rules_show_tab( $chosen, $tabs, $rules_url ); - } - - // ── Spezifische Suche: Suchbegriff vorhanden ────────────── - $q_clean = trim( preg_replace( - '/\b(regel|regeln|regelwerk|zeig|alle|zeige|liste|gibt es|was sind)\b/iu', - '', $q - ) ); - $q_clean = trim( preg_replace( '/\s+/', ' ', $q_clean ) ); - - if ( strlen( $q_clean ) >= 3 ) { - return mm_rules_search( $q_clean, $tabs, $rules_url ); - } - - // ── Schritt 1: Allgemein → Kategorie-Auswahl ───────────── - if ( empty( $tabs ) ) { - return array( - 'source' => 'rules', - 'title' => '📜 Regelwerk', - 'content' => "→ Zum Regelwerk", - ); - } - - $html = 'Wähle eine Kategorie:

'; - $html .= '
'; - - foreach ( $tabs as $tab ) { - $tab_title = $tab['title'] ?? 'Allgemein'; - $rule_count = ! empty( $tab['rules'] ) ? count( $tab['rules'] ) : 0; - // Trigger: "regeln:Minecraft" - $query = 'regeln:' . $tab_title; - $html .= ''; - } - - $html .= '
'; - $html .= '
Alle Regeln im Browser öffnen'; - - return array( - 'source' => 'rules', - 'title' => '📜 Regelwerk', - 'content' => $html, - ); -} - -/** - * Alle Regeln eines bestimmten Tabs anzeigen. - */ -function mm_rules_show_tab( $tab_name, $tabs, $rules_url ) { - $found_tab = null; - - foreach ( $tabs as $tab ) { - if ( mb_strtolower( $tab['title'] ?? '' ) === mb_strtolower( $tab_name ) ) { - $found_tab = $tab; - break; - } - } - - // Fuzzy-Fallback - if ( ! $found_tab ) { - foreach ( $tabs as $tab ) { - if ( strpos( mb_strtolower( $tab['title'] ?? '' ), mb_strtolower( $tab_name ) ) !== false ) { - $found_tab = $tab; - break; - } - } - } - - if ( ! $found_tab ) { - return array( - 'source' => 'rules', - 'title' => '📜 Regelwerk', - 'content' => 'Kategorie ' . esc_html( $tab_name ) . ' nicht gefunden.
' - . "→ Zum Regelwerk", - ); - } - - $rules = isset( $found_tab['rules'] ) && is_array( $found_tab['rules'] ) ? $found_tab['rules'] : array(); - - if ( empty( $rules ) ) { - return array( - 'source' => 'rules', - 'title' => '📜 ' . esc_html( $found_tab['title'] ), - 'content' => 'In dieser Kategorie sind noch keine Regeln hinterlegt.', - ); - } - - $html = ''; - foreach ( $rules as $rule ) { - $title = $rule['rule_title'] ?? ''; - $content = $rule['rule_content'] ?? ''; - - $content_plain = wp_strip_all_tags( $content ); - if ( mb_strlen( $content_plain ) > 300 ) { - $rendered = '

' - . esc_html( mb_substr( $content_plain, 0, 300 ) ) . '…

'; - } else { - $rendered = '
' - . wp_kses_post( $content ) - . '
'; - } - - $html .= '
'; - $html .= '📌 ' . esc_html( $title ) . ''; - $html .= $rendered; - $html .= '
'; - } - - // Zurück-Button zur Kategorieauswahl - $html .= '
'; - $html .= ''; - $html .= '' - . 'Im Browser öffnen'; - $html .= '
'; - - return array( - 'source' => 'rules', - 'title' => '📜 ' . esc_html( $found_tab['title'] ) . ' – ' . count( $rules ) . ' Regeln', - 'content' => $html, - ); -} - -/** - * Volltext-Suche über alle Tabs + Regeln. - */ -function mm_rules_search( $q_clean, $tabs, $rules_url ) { - $q_lc = mb_strtolower( $q_clean ); - $words = array_filter( preg_split( '/\s+/', $q_lc ), function( $w ) { - $stop = array( 'regel', 'regeln', 'ist', 'darf', 'ich', 'erlaubt', 'verboten', 'wie', 'was', 'kann' ); - return strlen( $w ) > 2 && ! in_array( $w, $stop, true ); - } ); - - $matches = array(); - - foreach ( $tabs as $tab ) { - $tab_title = $tab['title'] ?? 'Allgemein'; - if ( empty( $tab['rules'] ) || ! is_array( $tab['rules'] ) ) continue; - - foreach ( $tab['rules'] as $rule ) { - $r_title = mb_strtolower( $rule['rule_title'] ?? '' ); - $r_content = mb_strtolower( wp_strip_all_tags( $rule['rule_content'] ?? '' ) ); - $t_lc = mb_strtolower( $tab_title ); - $score = 0; - - if ( strpos( $r_title, $q_lc ) !== false ) $score += 30; - if ( strpos( $r_content, $q_lc ) !== false ) $score += 15; - if ( strpos( $t_lc, $q_lc ) !== false ) $score += 10; - - foreach ( $words as $w ) { - if ( strpos( $r_title, $w ) !== false ) $score += 12; - if ( strpos( $r_content, $w ) !== false ) $score += 5; - if ( strpos( $t_lc, $w ) !== false ) $score += 4; - } - - if ( $score > 0 ) { - $matches[] = array( - 'score' => $score, - 'tab' => $tab_title, - 'title' => $rule['rule_title'] ?? '', - 'content' => $rule['rule_content'] ?? '', - ); - } - } - } - - if ( empty( $matches ) ) { - // Keine Treffer → direkt zur Kategorieauswahl - $html = 'Keine Regel zu ' . esc_html( $q_clean ) . ' gefunden.

'; - $html .= 'Wähle eine Kategorie:

'; - $html .= '
'; - foreach ( $tabs as $tab ) { - $tab_title = $tab['title'] ?? 'Allgemein'; - $html .= ''; - } - $html .= '
'; - - return array( - 'source' => 'rules', - 'title' => '📜 Regelwerk', - 'content' => $html, - ); - } - - usort( $matches, function( $a, $b ) { return $b['score'] - $a['score']; } ); - $top = array_slice( $matches, 0, 3 ); - $html = ''; - - foreach ( $top as $r ) { - $content_plain = wp_strip_all_tags( $r['content'] ); - if ( mb_strlen( $content_plain ) > 300 ) { - $rendered = '

' - . esc_html( mb_substr( $content_plain, 0, 300 ) ) . '…

'; - } else { - $rendered = '
' - . wp_kses_post( $r['content'] ) - . '
'; - } - - $html .= '
'; - $html .= '📌 ' . esc_html( $r['title'] ) . ''; - $html .= ' (' . esc_html( $r['tab'] ) . ')'; - $html .= $rendered; - $html .= '
'; - } - - if ( count( $matches ) > 3 ) { - $html .= '' . ( count( $matches ) - 3 ) . ' weitere Treffer vorhanden.
'; - } - - $html .= '
'; - $html .= ''; - $html .= 'Im Browser öffnen'; - $html .= '
'; - - return array( - 'source' => 'rules', - 'title' => '📜 Regelwerk – ' . count( $matches ) . ' Treffer', - 'content' => $html, - ); -} - -// ============================================================ -// INTENT: WIKI (WP Multi Wiki Plugin) -// ============================================================ - -function mm_intent_wiki( $q ) { - if ( ! post_type_exists( 'wmw_article' ) ) return null; - - // Suchbegriff bereinigen - $search = preg_replace( '/\b(wiki|anleitung|guide|howto|how to|dokument|erkläre?|zeig mir)\b/i', '', $q ); - $search = trim( preg_replace( '/\s+/', ' ', $search ) ); - - if ( strlen( $search ) < 2 ) { - // Alle Wikis auflisten - if ( function_exists( 'wmw_get_wikis' ) ) { - $wikis = wmw_get_wikis(); - if ( ! empty( $wikis ) ) { - $links = array_map( function( $w ) { - return '→ ' - . esc_html( $w->post_title ) . ''; - }, $wikis ); - return [ - 'source' => 'wiki', - 'title' => '📖 Wiki', - 'content' => 'Verfügbare Wikis:
' . implode( '
', $links ), - ]; - } - } - return null; - } - - $args = [ - 'post_type' => 'wmw_article', - 'post_status' => 'publish', - 's' => $search, - 'posts_per_page' => 4, - 'orderby' => 'relevance', - ]; - $results = get_posts( $args ); - - if ( empty( $results ) ) { - // Fallback: alle Wiki-Artikel - $bot = get_option( 'mm_bot_data', [] ); - $url = ! empty( $bot['url_wiki'] ) ? $bot['url_wiki'] : ''; - $link = $url ? " → Wiki öffnen" : ''; - return [ - 'source' => 'wiki', - 'title' => '📖 Wiki', - 'content' => 'Kein Wiki-Artikel zu „' . esc_html( $search ) . '“ gefunden.' . $link, - ]; - } - - $html = []; - foreach ( $results as $post ) { - $excerpt = get_the_excerpt( $post->ID ); - $html[] = '📄 ' - . esc_html( $post->post_title ) . '' - . ( $excerpt ? '
' . wp_trim_words( $excerpt, 25, '…' ) . '' : '' ); - } - - return [ - 'source' => 'wiki', - 'title' => '📖 Wiki-Treffer', - 'content' => implode( '

', $html ), - ]; -} - -// ============================================================ -// INTENT: SHOP (WP Ingame Shop Pro) -// ============================================================ - -function mm_intent_shop( $q_lc, $bot ) { - global $wpdb; - $t_items = $wpdb->prefix . 'wis_items'; - $t_cats = $wpdb->prefix . 'wis_categories'; - - if ( $wpdb->get_var( "SHOW TABLES LIKE '$t_items'" ) !== $t_items ) return null; - - // DEBUG: Suchbegriff und Item-Anzahl loggen - if (!function_exists('mm_debug_log')) { - function mm_debug_log($msg) { - if (defined('WP_DEBUG') && WP_DEBUG) { - error_log('[MM SHOP] ' . $msg); - } - } - } - - mm_debug_log('Suchbegriff: ' . $q_lc); - - $currency = get_option( 'wis_currency_name', 'Coins' ); - $lines = []; - $img_base = get_option( 'wis_image_base_url', '' ); - $shop_url = $bot['url_shop'] ?? ''; - // Shop-Antwort-Box (grau, Card-Layout) - // $lines[] = '
'; - - // ── Tagesaktion nur anzeigen, wenn KEINE Preisabfrage ───────────────── - $is_price_query = false; - $search = $q_lc; - if (preg_match('/\b(was kostet|preis von|wie teuer|wie viel kostet|kosten von|preis für|preis)\b/i', $search)) { - $is_price_query = true; - } - if (!$is_price_query) { - $deal = $wpdb->get_row( "SELECT * FROM $t_items WHERE is_daily_deal = 1 AND status = 'publish' LIMIT 1" ); - if ( $deal ) { - $offer = isset( $deal->offer_price ) && $deal->offer_price > 0 ? $deal->offer_price : $deal->price; - $img = $img_base . str_replace(':', '_', $deal->item_id) . '.png'; - $item_link = $shop_url ? esc_url($shop_url) . '#item-' . urlencode($deal->item_id) : ''; - $lines[] = '
' - . '🔥 Tagesaktion:
' - . '' . esc_attr($deal->name) . '' - . '
' . esc_html( $deal->name ) . '
' - . '' - . '' . esc_html( (string) $offer ) . ' ' . esc_html( $currency ) . '' - . ( $deal->price != $offer ? ' ' . esc_html( (string) $deal->price ) . '' : '' ) - . '' - . ( $item_link ? '
Zum Item im Shop' : '' ) - . '
'; - } - } - - // ── Suchbegriff extrahieren (auch für Preisabfragen) ──────────────── - // (is_price_query wurde oben schon gesetzt) - $search = $q_lc; - if ($is_price_query) { - // Nur das Item extrahieren - $search = preg_replace('/\b(shop|kaufen|kosten|preis|items?|angebot|tagesaktion|daily deal|kategorie|rang|was gibt|zeig|liste|was kostet|preis von|wie teuer|wie viel kostet|kosten von|preis für)\b/i', '', $search); - } else { - $search = preg_replace('/\b(shop|kaufen|kosten|preis|items?|angebot|tagesaktion|daily deal|kategorie|rang|was gibt|zeig|liste)\b/i', '', $search); - } - $search = trim(preg_replace('/\s+/', ' ', $search)); - // Satzzeichen am Anfang/Ende entfernen - $search = preg_replace('/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/u', '', $search); - - if (strlen($search) >= 3) { - mm_debug_log('Suchbegriff nach Extraktion: ' . $search); - // Spezifische Suche (auch für Preisabfragen) - $items = $wpdb->get_results($wpdb->prepare( - "SELECT * FROM $t_items - WHERE status = 'publish' AND (item_title LIKE %s OR name LIKE %s OR item_id LIKE %s) - ORDER BY price ASC LIMIT 8", - '%' . $wpdb->esc_like($search) . '%', - '%' . $wpdb->esc_like($search) . '%', - '%' . $wpdb->esc_like($search) . '%' - )); - mm_debug_log('LIKE-Query: ' . $search . ' | Treffer: ' . count($items)); - - if (!empty($items)) { - mm_debug_log('LIKE-Query erstes Item: ' . (isset($items[0]) ? json_encode($items[0]) : 'n/a')); - // Preisabfrage: Nur das erste Item als Preisantwort - if ($is_price_query) { - $item = $items[0]; - $img = $img_base . str_replace(':', '_', $item->item_id) . '.png'; - $item_link = $shop_url ? esc_url($shop_url) . '#item-' . urlencode($item->item_id) : ''; - $item_display_name = $item->name ?: ($item->item_title ?: $item->item_id); - // Serverliste aufbereiten - $server_list = ''; - if (!empty($item->servers)) { - $servers = json_decode($item->servers, true); - if (is_array($servers) && count($servers) > 0) { - $server_list = '
Verfügbar auf: ' . esc_html(implode(', ', $servers)) . '
'; - } - } - $lines[] = '
' - . '
🛒 Shop-Item:
' - . '' . esc_attr($item_display_name) . '' - . '
' . esc_html($item_display_name) . '
' - . '
' . esc_html((string)$item->price) . ' ' . esc_html($currency) . '
' - . $server_list - . '
'; - } else { - $lines[] = '🛒 Gefundene Items:'; - foreach ($items as $item) { - $img = $img_base . str_replace(':', '_', $item->item_id) . '.png'; - $item_link = $shop_url ? esc_url($shop_url) . '#item-' . urlencode($item->item_id) : ''; - $lines[] = '' . esc_attr($item->item_title) . '' - . '' . esc_html($item->item_title) . '' - . ' – ' . esc_html((string)$item->price) . ' ' . esc_html($currency) - . ($item_link ? ' [Shop]' : ''); - } - } - } else { - // Exakte Suche, falls LIKE nichts findet - $item = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM $t_items WHERE status = 'publish' AND (name = %s OR item_id = %s OR item_title = %s) LIMIT 1", - $search, $search, $search - )); - mm_debug_log('Exakte Suche: ' . ($item ? json_encode($item) : 'kein Treffer')); - // Case-insensitive Fallback, falls nötig - if (!$item && !empty($search)) { - $item = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM $t_items WHERE status = 'publish' AND (name COLLATE utf8_general_ci = %s COLLATE utf8_general_ci OR item_id COLLATE utf8_general_ci = %s COLLATE utf8_general_ci OR item_title COLLATE utf8_general_ci = %s COLLATE utf8_general_ci) LIMIT 1", - $search, $search, $search - )); - mm_debug_log('Case-insensitive Suche: ' . ($item ? json_encode($item) : 'kein Treffer')); - } - // Fallback: Suchbegriff als Minecraft-ID mit Prefix testen - if (!$item && !empty($search) && strpos($search, 'minecraft:') === false) { - $mc_id = 'minecraft:' . strtolower(str_replace([' ', '_'], ['_', '_'], $search)); - $item = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM $t_items WHERE status = 'publish' AND (item_id = %s OR item_id COLLATE utf8_general_ci = %s COLLATE utf8_general_ci) LIMIT 1", - $mc_id, $mc_id - )); - mm_debug_log('MC-ID Fallback: ' . $mc_id . ' | ' . ($item ? json_encode($item) : 'kein Treffer')); - } - if ($item) { - mm_debug_log('Gefundenes Item (exakt/fallback): ' . json_encode($item)); - $img = $img_base . str_replace(':', '_', $item->item_id) . '.png'; - $item_link = $shop_url ? esc_url($shop_url) . '#item-' . urlencode($item->item_id) : ''; - $lines[] = '
' - . '' . esc_attr($item->name) . '' - . '
' . esc_html($item->name) . '
' - . 'Preis: ' . esc_html((string)$item->price) . ' ' . esc_html($currency) . '' - . ($item_link ? ' [Shop]' : '') - . '
'; - } else { - // Aggressive Fuzzy-Suche als letzter Versuch - $all_items = $wpdb->get_results("SELECT * FROM $t_items WHERE status = 'publish'"); - mm_debug_log('Fuzzy-Suche über alle Items (' . count($all_items) . '): ' . $search); - $search_lc = mb_strtolower($search); - $fuzzy = null; - foreach ($all_items as $it) { - if (strpos(mb_strtolower($it->name), $search_lc) !== false - || strpos(mb_strtolower($it->item_title), $search_lc) !== false - || strpos(mb_strtolower($it->item_id), $search_lc) !== false) { - $fuzzy = $it; - break; - } - } - mm_debug_log('Fuzzy-Suche Treffer: ' . ($fuzzy ? json_encode($fuzzy) : 'kein Treffer')); - if ($fuzzy) { - $img = $img_base . str_replace(':', '_', $fuzzy->item_id) . '.png'; - $item_link = $shop_url ? esc_url($shop_url) . '#item-' . urlencode($fuzzy->item_id) : ''; - $item_display_name = $fuzzy->name ?: ($fuzzy->item_title ?: $fuzzy->item_id); - // Serverliste aufbereiten - $server_list = ''; - if (!empty($fuzzy->servers)) { - $servers = json_decode($fuzzy->servers, true); - if (is_array($servers) && count($servers) > 0) { - $server_list = '
Verfügbar auf: ' . esc_html(implode(', ', $servers)) . '
'; - } - } - $lines[] = '
' - . '
🛒 Shop-Item:
' - . '' . esc_attr($item_display_name) . '' - . '
' . esc_html($item_display_name) . '
' - . '
' . esc_html((string)$fuzzy->price) . ' ' . esc_html($currency) . '
' - . $server_list - . '
'; - } else { - $lines[] = 'Kein Item mit „' . esc_html($search) . '“ gefunden.'; - } - } - } - } else { - // Übersicht: Kategorien + Item-Anzahl - $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $t_items WHERE status = 'publish'" ); - $lines[] = '🛒 Shop-Übersicht: ' . $total . ' Items verfügbar.'; - - $cats = $wpdb->get_results( - "SELECT c.name, COUNT(i.id) as cnt - FROM $t_cats c - LEFT JOIN $t_items i ON i.category_id = c.id AND i.status = 'publish' - GROUP BY c.id ORDER BY cnt DESC LIMIT 8" - ); - if ( ! empty( $cats ) ) { - $lines[] = '📂 Kategorien:'; - foreach ( $cats as $cat ) { - $lines[] = ' • ' . esc_html( $cat->name ) . ' (' . intval( $cat->cnt ) . ' Items)'; - } - } - - // Günstigste & teuerste Items - $cheapest = $wpdb->get_row( "SELECT item_title, price FROM $t_items WHERE status = 'publish' AND price > 0 ORDER BY price ASC LIMIT 1" ); - $priciest = $wpdb->get_row( "SELECT item_title, price FROM $t_items WHERE status = 'publish' ORDER BY price DESC LIMIT 1" ); - if ( $cheapest ) $lines[] = '💚 Günstigstes: ' . esc_html( $cheapest->item_title ) . ' – ' . esc_html( (string) $cheapest->price ) . ' ' . esc_html( $currency ); - if ( $priciest ) $lines[] = '💎 Teuerstes: ' . esc_html( $priciest->item_title ) . ' – ' . esc_html( (string) $priciest->price ) . ' ' . esc_html( $currency ); - } - - if ( $shop_url ) { - $lines[] = ""; - } - - return [ - 'source' => 'shop', - 'title' => '🛒 Shop', - 'content' => implode( '
', $lines ), - ]; -} - -// ============================================================ -// INTENT: TICKET (WP Multi Ticket Pro) -// ============================================================ - -function mm_intent_ticket( $q, $bot ) { - global $wpdb; - $table = $wpdb->prefix . 'wmt_tickets'; - $table_msg = $wpdb->prefix . 'wmt_messages'; - $exists = $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) === $table; - - // ── Ticket per Nummer nachschlagen ──────────────────────── - if ( preg_match( '/#?(\d{3,6})\b/', $q, $m ) && $exists ) { - $ticket_id = intval( $m[1] ); - $ticket = $wpdb->get_row( $wpdb->prepare( - "SELECT id, subject, status, category, department, created_at, guest_name, guest_email - FROM $table WHERE id = %d", - $ticket_id - ) ); - - if ( $ticket ) { - // Letzte Nachricht - $last_msg = ''; - if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_msg'" ) === $table_msg ) { - $msg = $wpdb->get_row( $wpdb->prepare( - "SELECT message, created_at FROM $table_msg WHERE ticket_id = %d ORDER BY created_at DESC LIMIT 1", - $ticket_id - ) ); - if ( $msg ) { - $last_msg = '
💬 Letzte Nachricht: ' - . esc_html( wp_trim_words( $msg->message, 20, '…' ) ) - . ' (' . wp_date( 'd.m.Y H:i', strtotime( $msg->created_at ) ) . ')'; - } - } - - $lines = [ - '🎫 Ticket #' . $ticket->id . '', - '📋 Betreff: ' . esc_html( $ticket->subject ) . '', - '📌 Status: ' . esc_html( $ticket->status ) . '', - '🏷️ Kategorie: ' . esc_html( $ticket->category ?: '–' ), - ]; - if ( ! empty( $ticket->department ) ) { - $lines[] = '🏢 Abteilung: ' . esc_html( $ticket->department ); - } - if ( ! empty( $ticket->guest_name ) ) { - $lines[] = '👤 Von: ' . esc_html( $ticket->guest_name ); - } - $lines[] = '📅 Erstellt: ' . wp_date( 'd.m.Y \u\m H:i', strtotime( $ticket->created_at ) ); - if ( $last_msg ) $lines[] = $last_msg; - - return [ - 'source' => 'ticket', - 'title' => '🎫 Ticket #' . $ticket->id, - 'content' => implode( '
', $lines ), - ]; - } else { - return [ - 'source' => 'ticket', - 'title' => '🎫 Ticket nicht gefunden', - 'content' => 'Ticket #' . $ticket_id . ' existiert nicht.', - ]; - } - } - - // ── Allgemeine Ticket-Übersicht ──────────────────────────── - $lines = []; - - if ( $exists ) { - $open = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $table WHERE status NOT LIKE %s", '%Geschlossen%' ) ); - $closed = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $table WHERE status LIKE %s", '%Geschlossen%' ) ); - $total = $open + $closed; - - $lines[] = '📊 Gesamt: ' . $total . ' Tickets · Offen: ' . $open . ' · Geschlossen: ' . $closed . ''; - - // Kategorien anzeigen - $cats = $wpdb->get_results( - "SELECT COALESCE(category, 'Keine Kategorie') as cat, COUNT(*) as cnt - FROM $table GROUP BY category ORDER BY cnt DESC LIMIT 5" - ); - if ( ! empty( $cats ) ) { - $cat_parts = []; - foreach ( $cats as $c ) { - $cat_parts[] = esc_html( $c->cat ) . ' (' . intval( $c->cnt ) . ')'; - } - $lines[] = '📂 Kategorien: ' . implode( ' · ', $cat_parts ); - } - } - - $ticket_url = $bot['url_tickets'] ?? ''; - if ( $ticket_url ) { - $lines[] = "→ Neues Ticket erstellen"; - $lines[] = 'Oder gib eine Ticket-Nummer ein, z.B.: #1234'; - } else { - $lines[] = 'Bitte nutze das Support-Formular auf unserer Website.'; - } - - return [ - 'source' => 'ticket', - 'title' => '🎫 Support-Ticket', - 'content' => implode( '
', $lines ), - ]; -} - -// ============================================================ -// INTENT: FORUM (WP Business Forum) -// ============================================================ - -function mm_intent_forum( $q ) { - // Nur aktiv wenn WBF installiert UND URL im Backend gesetzt - $bot = get_option( 'mm_bot_data', [] ); - if ( ! class_exists( 'WBF_DB' ) ) return null; - if ( empty( $bot['url_forum'] ) ) return null; - - global $wpdb; - - $forum_url = esc_url( $bot['url_forum'] ); - $q_lc = mb_strtolower( $q ); - - // Tabellen prüfen — fehlen sie, früh abbrechen statt hängen - $t_tbl = $wpdb->prefix . 'forum_threads'; - $p_tbl = $wpdb->prefix . 'forum_posts'; - $u_tbl = $wpdb->prefix . 'forum_users'; - $c_tbl = $wpdb->prefix . 'forum_categories'; - - $tables_ok = ( - $wpdb->get_var( "SHOW TABLES LIKE '$t_tbl'" ) === $t_tbl && - $wpdb->get_var( "SHOW TABLES LIKE '$u_tbl'" ) === $u_tbl && - $wpdb->get_var( "SHOW TABLES LIKE '$c_tbl'" ) === $c_tbl - ); - - // ── Sub-Intent: Registrierung ───────────────────────────── - if ( preg_match( '/\b(registrier(en|ung)?|konto erstellen|account erstellen|sign.?up|einschreiben)\b/i', $q ) ) { - $lines = []; - if ( $tables_ok ) { - try { - $reg_mode = get_option( 'wbf_settings', [] )['registration_mode'] ?? 'open'; - $member_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $u_tbl WHERE role != 'banned'" ); - - $lines[] = '📝 Registrierung:'; - if ( $reg_mode === 'disabled' ) { - $lines[] = '❌ Die Registrierung ist derzeit deaktiviert.'; - } elseif ( $reg_mode === 'invite' ) { - $lines[] = '🔒 Das Forum nutzt Einladungscodes. Du benötigst einen gültigen Code.'; - } else { - $lines[] = '✅ Die Registrierung ist offen — jederzeit möglich.'; - } - if ( $member_count > 0 ) { - $lines[] = '👥 Bereits ' . $member_count . ' Mitglieder dabei.'; - } - } catch ( \Throwable $e ) { - $lines[] = '📝 Registrierung verfügbar.'; - } - } else { - $lines[] = '📝 Registrierung:'; - $lines[] = 'Erstelle deinen Account direkt im Forum.'; - } - $lines[] = "→ Zum Forum & Registrierung"; - return [ - 'source' => 'forum', - 'title' => '💬 Forum-Registrierung', - 'content' => implode( '
', $lines ), - ]; - } - - // ── Sub-Intent: Login / Passwort ────────────────────────── - if ( preg_match( '/\b(login|einloggen|passwort vergessen|passwort.?reset|zugangsdaten)\b/i', $q ) ) { - $lines = []; - $lines[] = '🔑 Forum-Login:'; - $lines[] = "→ Zum Forum & Login"; - if ( preg_match( '/passwort/i', $q ) ) { - $lines[] = '
🔓 Passwort vergessen?'; - $lines[] = 'Klicke auf der Login-Seite auf „Passwort vergessen".'; - } - return [ - 'source' => 'forum', - 'title' => '💬 Forum-Login', - 'content' => implode( '
', $lines ), - ]; - } - - // ── Sub-Intent: Profil ──────────────────────────────────── - if ( preg_match( '/\b(profil|mein konto|mein account|avatar|signatur|forum rang|forum level)\b/i', $q ) ) { - $lines = []; - $lines[] = '👤 Dein Forum-Profil enthält:'; - $lines[] = ' • Anzeigename, Avatar & Signatur'; - if ( class_exists( 'WBF_Levels' ) && WBF_Levels::is_enabled() ) { - $lines[] = ' • Level-System (basiert auf Beitragsanzahl)'; - } - $lines[] = ' • Rollen-Badge, Beitragsanzahl & Registrierungsdatum'; - $lines[] = "→ Zum Forum → Profil öffnen"; - return [ - 'source' => 'forum', - 'title' => '💬 Forum-Profil', - 'content' => implode( '
', $lines ), - ]; - } - - // ── Sub-Intent: Nachrichten / DM ────────────────────────── - if ( preg_match( '/\b(direktnachricht|forum nachricht|forum inbox|pn schreiben|privat.?nachricht)\b/i', $q ) ) { - $lines = []; - $lines[] = '✉️ Direktnachrichten im Forum:'; - $lines[] = 'Du kannst eingeloggten Mitgliedern direkt Nachrichten schreiben.'; - $lines[] = "→ Forum öffnen → Postfach"; - return [ - 'source' => 'forum', - 'title' => '💬 Forum-Nachrichten', - 'content' => implode( '
', $lines ), - ]; - } - - // ── Sub-Intent: Thread erstellen ────────────────────────── - if ( preg_match( '/\b(neuen thread|thread erstellen|beitrag erstellen|topic erstellen|im forum schreiben|im forum posten)\b/i', $q ) ) { - $lines = []; - $lines[] = '✏️ Neuen Thread erstellen:'; - $lines[] = '① Melde dich im Forum an.'; - $lines[] = '② Wähle die passende Kategorie.'; - $lines[] = '③ Klicke auf „Neuen Thread" und fülle Titel & Inhalt aus.'; - $lines[] = "→ Forum öffnen"; - return [ - 'source' => 'forum', - 'title' => '💬 Thread erstellen', - 'content' => implode( '
', $lines ), - ]; - } - - // ── Suchbegriff extrahieren ──────────────────────────────── - $search = preg_replace( '/\b(forum|diskussion|thread|thema|beitrag|post|kategorie|suche?)\b/i', '', $q ); - $search = trim( preg_replace( '/\s+/', ' ', $search ) ); - - // ── Thread-Suche ────────────────────────────────────────── - if ( strlen( $search ) >= 3 && $tables_ok ) { - try { - $threads = $wpdb->get_results( $wpdb->prepare( - "SELECT t.id, t.title, t.slug, t.reply_count, t.view_count, t.is_pinned, - c.name as cat_name - FROM {$t_tbl} t - LEFT JOIN {$c_tbl} c ON t.category_id = c.id - WHERE t.title LIKE %s AND t.deleted_at IS NULL - ORDER BY t.is_pinned DESC, t.created_at DESC LIMIT 5", - '%' . $wpdb->esc_like( $search ) . '%' - ) ); - - if ( ! empty( $threads ) ) { - $html = ''; - foreach ( $threads as $t ) { - $t_url = $forum_url . '?thread=' . esc_attr( $t->slug ); - $pin = $t->is_pinned ? '📌 ' : ''; - $meta = []; - if ( $t->reply_count > 0 ) $meta[] = $t->reply_count . ' Antworten'; - if ( $t->view_count > 0 ) $meta[] = $t->view_count . ' Aufrufe'; - $title_short = mb_strlen( $t->title ) > 50 ? mb_substr( $t->title, 0, 50 ) . '…' : $t->title; - $html .= '
'; - $html .= $pin . '' . esc_html( $title_short ) . ''; - if ( $t->cat_name ) $html .= ' · ' . esc_html( $t->cat_name ) . ''; - if ( ! empty( $meta ) ) $html .= '
' . implode( ' · ', $meta ) . ''; - $html .= '
'; - } - $html .= "
Alle Threads im Forum"; - return [ - 'source' => 'forum', - 'title' => '💬 Forum – ' . count( $threads ) . ' Treffer', - 'content' => $html, - ]; - } - - return [ - 'source' => 'forum', - 'title' => '💬 Forum', - 'content' => 'Kein Thread zu ' . esc_html( $search ) . ' gefunden.
' - . "→ Forum öffnen & selbst suchen", - ]; - } catch ( \Throwable $e ) { - // Bei DB-Fehler: einfach Übersicht zeigen - } - } - - // ── Allgemeine Übersicht mit Live-Statistiken ────────────── - $lines = []; - - if ( $tables_ok ) { - try { - $total_threads = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $t_tbl WHERE deleted_at IS NULL" ); - $total_posts = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $p_tbl WHERE deleted_at IS NULL" ); - $total_members = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $u_tbl WHERE role != 'banned'" ); - $online_count = (int) $wpdb->get_var( - "SELECT COUNT(*) FROM $u_tbl WHERE last_active >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)" - ); - - $stat_parts = []; - if ( $total_threads > 0 ) $stat_parts[] = '' . $total_threads . ' Threads'; - if ( $total_posts > 0 ) $stat_parts[] = '' . $total_posts . ' Beiträge'; - if ( $total_members > 0 ) $stat_parts[] = '' . $total_members . ' Mitglieder'; - if ( ! empty( $stat_parts ) ) { - $lines[] = '📊 ' . implode( ' · ', $stat_parts ); - } - - $online_icon = $online_count > 0 ? '🟢' : '⚫'; - $lines[] = $online_icon . ' ' . ( $online_count > 0 - ? '' . $online_count . ' Mitglied' . ( $online_count > 1 ? 'er' : '' ) . ' gerade online' - : 'Aktuell niemand aktiv' ); - - // Kategorien - $cats = $wpdb->get_results( - "SELECT c.name, c.slug, COUNT(t.id) as thread_count - FROM {$c_tbl} c - LEFT JOIN {$t_tbl} t ON t.category_id = c.id AND t.deleted_at IS NULL - WHERE c.parent_id = 0 - GROUP BY c.id ORDER BY c.sort_order ASC LIMIT 6" - ); - if ( ! empty( $cats ) ) { - $lines[] = '
📂 Kategorien:'; - foreach ( $cats as $cat ) { - $c_url = $forum_url . '?cat=' . esc_attr( $cat->slug ); - $count = $cat->thread_count > 0 ? ' (' . (int) $cat->thread_count . ')' : ''; - $lines[] = ' → ' - . esc_html( $cat->name ) . '' . $count; - } - } - - // Neueste Threads - $recent = $wpdb->get_results( - "SELECT t.title, t.slug, t.reply_count - FROM {$t_tbl} t - WHERE t.deleted_at IS NULL AND t.is_pinned = 0 - ORDER BY t.created_at DESC LIMIT 3" - ); - if ( ! empty( $recent ) ) { - $lines[] = '
🆕 Neueste Threads:'; - foreach ( $recent as $r ) { - $t_url = $forum_url . '?thread=' . esc_attr( $r->slug ); - $title_short = mb_strlen( $r->title ) > 50 ? mb_substr( $r->title, 0, 50 ) . '…' : $r->title; - $replies = $r->reply_count > 0 ? ' (' . (int) $r->reply_count . ' Antw.)' : ''; - $lines[] = ' → ' - . esc_html( $title_short ) . '' . $replies; - } - } - } catch ( \Throwable $e ) { - $lines[] = '⚠️ Forum-Daten konnten nicht geladen werden.'; - } - } - - $lines[] = ( ! empty( $lines ) ? '
' : '' ) - . "→ Forum öffnen"; - - return [ - 'source' => 'forum', - 'title' => '💬 Forum', - 'content' => implode( '
', $lines ), - ]; -} - -// ============================================================ -// INTENT: GALERIE (MC MultiServer Gallery PRO) -// ============================================================ - -function mm_intent_gallery( $bot, $q_lc = '' ) { - $gallery_url = ! empty( $bot['url_gallery'] ) ? $bot['url_gallery'] : home_url( '/galerie' ); - - // ── Erkennen ob Upload-Frage ────────────────────────────── - $is_upload_query = (bool) preg_match( - '/hochlad|upload|uploaden|verifizier|verify|token|bild teilen|bilder teilen|wie.*bild|anleitung.*galerie/i', - $q_lc - ); - - $lines = []; - - // ── 1. Galerie-Link ────────────────────────────────────── - $lines[] = "📷 → Galerie öffnen"; - - // ── 2. Upload-Anleitung (bei Upload-Frage oder allgemein) ─ - if ( $is_upload_query ) { - $lines[] = '
'; - $lines[] = '📤 Bilder hochladen – so geht\'s:'; - $lines[] = '① Galerie öffnen und auf „Bilder hochladen" klicken.'; - $lines[] = '② Deinen Minecraft-Namen eingeben und den Server wählen → Session starten.'; - $lines[] = '③ Im Spiel den Befehl /verify [token] eingeben, um dich zu verifizieren.'; - $lines[] = '④ Screenshots auswählen (optional Album anlegen) → hochladen ✓'; - $lines[] = '💡 Der Verify-Token ist 5 Minuten gültig. Du musst dazu online auf dem Server sein.'; - } else { - // Kurz-Hinweis auf Upload auch bei allgemeiner Galerie-Frage - $lines[] = "🖼️ Du kannst eigene Screenshots direkt auf der Galerie-Seite hochladen."; - } - - // ── 3. Bild des Tages ──────────────────────────────────── - if ( post_type_exists( 'mc_gallery' ) ) { - $today_key = 'mc_daily_image_' . gmdate( 'Y-m-d' ); - $image_id = get_transient( $today_key ); - - if ( $image_id && wp_attachment_is_image( $image_id ) ) { - $img = wp_get_attachment_image_src( $image_id, 'medium' ); - $full = wp_get_attachment_image_src( $image_id, 'full' ); - $meta = get_post( $image_id ); - $uploader = ''; - - // Spielername aus Galerie-Post-Meta holen - if ( $meta && $meta->post_parent ) { - $player = get_post_meta( $meta->post_parent, 'mc_player', true ); - if ( $player ) { - $uploader = ' von ' . esc_html( $player ) . ''; - } - } - - if ( $img ) { - $lines[] = '
'; - $lines[] = '🌟 Bild des Tages' . $uploader . ':'; - $lines[] = "" - . "" - . ""; - } - } else { - // Kein Tagesbild gecacht → Hinweis - $lines[] = '📅 Heute noch kein Bild des Tages verfügbar.'; - } - - // Galerie-Statistik - $count = wp_count_posts( 'mc_gallery' ); - if ( $count && $count->publish > 0 ) { - $lines[] = '📁 ' . intval( $count->publish ) . ' Galerie(n) auf dem Server.'; - } - } - - return [ - 'source' => 'gallery', - 'title' => $is_upload_query ? '📤 Galerie & Upload' : '📷 Galerie', - 'content' => implode( '
', $lines ), - ]; -} - -// ============================================================ -// INTENT: FAQ (Custom Post Type 'faq') -// ============================================================ - -function mm_intent_faq( $q ) { - if ( ! post_type_exists( 'faq' ) ) return null; - - $search = preg_replace( '/\b(faq|häufig|frage|oft gefragt)\b/i', '', $q ); - $search = trim( $search ); - - $results = get_posts( [ - 'post_type' => 'faq', - 'post_status' => 'publish', - 's' => strlen( $search ) >= 3 ? $search : '', - 'posts_per_page' => 4, - 'orderby' => 'relevance', - ] ); - - if ( empty( $results ) ) return null; - - $html = '
'; - foreach ( $results as $post ) { - $title = esc_html( $post->post_title ); - $content = apply_filters( 'the_content', $post->post_content ); - $html .= '
'; - $html .= '
' . $title . '
'; - $html .= '
' . $content . '
'; - $html .= '
'; - } - $html .= '
'; - - return [ - 'source' => 'faq', - 'title' => 'FAQ', - 'content' => $html, - ]; -} - -// ============================================================ -// SPIELER-NAME EXTRAHIEREN -// ============================================================ - -function mm_extract_player_name( $q ) { - // Explizit: "von Steve", "spieler Steve", "für Steve" - if ( preg_match( '/(?:von|spieler|name|für|player|ban.*?von|bann.*?von)[:\s]+([A-Za-z0-9_]{3,16})/iu', $q, $m ) ) { - return $m[1]; - } - // "Steve ist gebannt", "ist Steve gebannt" - if ( preg_match( '/\b([A-Za-z0-9_]{3,16})\s+ist\s+(gebannt|gesperrt|online|offline)/i', $q, $m ) ) { - return $m[1]; - } - // "bin ich" → kein Name (eingeloggter User, kein Minecraft-Login hier) - if ( preg_match( '/\b(bin ich|ich bin|mein ban|mein status)\b/i', $q ) ) { - return ''; - } - // Allgemein: letztes kapitalisiertes Wort als Spielername (MC-Namen beginnen meist groß) - if ( preg_match_all( '/\b([A-Z][A-Za-z0-9_]{2,15})\b/', $q, $m ) ) { - $stop = [ 'Minecraft', 'Wiki', 'Forum', 'FAQ', 'Shop', 'Discord', 'Ban', 'Mute', - 'Kick', 'Warn', 'Spielzeit', 'Playtime', 'Galerie', 'Ticket', 'Support' ]; - foreach ( array_reverse( $m[1] ) as $candidate ) { - if ( ! in_array( $candidate, $stop, true ) ) { - return $candidate; - } - } - } - return ''; -} - -// ============================================================ -// FALLBACK -// ============================================================ - -function mm_fallback_response( $bot ) { - $hints = [ - '🖥️ Server – z.B. "Wie ist die Server-IP?"', - ]; - if ( ! empty( $bot['url_rules'] ) ) $hints[] = '📜 Regeln – z.B. "Ist PvP erlaubt?"'; - if ( ! empty( $bot['url_player_history'] ) ) $hints[] = '👤 Spieler-Info – z.B. "Spielzeit von Steve"'; - if ( ! empty( $bot['litebans_dashboard_url'] ) || ! empty( get_option( 'wp_litebans_pro_settings', [] )['db_name'] ) ) - $hints[] = '🔨 Ban-Status – z.B. "Ist Steve gebannt?"'; - if ( ! empty( $bot['url_wiki'] ) ) $hints[] = '📖 Wiki – z.B. "Wiki Befehle"'; - if ( ! empty( $bot['url_gallery'] ) ) $hints[] = '📷 Galerie – z.B. "Zeig die Galerie"'; - if ( ! empty( $bot['url_shop'] ) ) $hints[] = '🛒 Shop – z.B. "Was kostet ein Diamantschwert?"'; - if ( ! empty( $bot['url_tickets'] ) ) $hints[] = '🎫 Ticket – z.B. "Ticket erstellen"'; - - return [ - 'source' => 'fallback', - 'title' => '❓ Ich habe dazu keine Antwort', - 'content' => 'Versuche es mit einem dieser Themen:
' . implode( '
', $hints ), - ]; -} - -// ============================================================ -// RESPONSE BUILDER -// Alle Part-Arrays zu einem einzigen formatierten HTML-String -// zusammenführen. -// ============================================================ - -function mm_build_response( $parts ) { - if ( count( $parts ) === 1 ) { - $p = $parts[0]; - return [ - 'reply' => isset( $p['title'] ) - ? "{$p['title']}
{$p['content']}" - : $p['content'], - 'parts' => $parts, - ]; - } - - $blocks = []; - foreach ( $parts as $p ) { - $block = isset( $p['title'] ) ? "{$p['title']}
" : ''; - $block .= $p['content']; - $blocks[] = $block; - } - - return [ - 'reply' => implode( '
', $blocks ), - 'parts' => $parts, - ]; + 'Bitte gib eine Frage ein.', + 'parts' => [], + ] ); + } + + // "regeln:Kategorie" → direkt zu Rules-Intent, kein Scoring nötig + if ( preg_match( '/^regeln?:/iu', $q ) ) { + $bot = get_option( 'mm_bot_data', [] ); + $result = mm_intent_rules( $q, $bot ); + wp_send_json_success( mm_build_response( [ $result ] ) ); + } + + // "ban:Steve" → direkt zu Ban-Intent mit Spielername + if ( preg_match( '/^ban:(.+)$/iu', $q, $m ) ) { + $bot = get_option( 'mm_bot_data', [] ); + $name = trim( $m[1] ); + $result = mm_intent_ban_check( $q, $name, $bot ); + wp_send_json_success( mm_build_response( [ $result ] ) ); + } + + try { + $bot = get_option( 'mm_bot_data', [] ); + $parts = []; + + // ---- 1. Manuelle Q&A (höchste Priorität) -------------------- + if ( ! empty( $bot['qa'] ) ) { + foreach ( $bot['qa'] as $item ) { + if ( empty( $item['keys'] ) ) continue; + $keys = array_map( 'trim', explode( ',', mb_strtolower( $item['keys'] ) ) ); + foreach ( $keys as $k ) { + if ( $k !== '' && strpos( $q_lc, $k ) !== false ) { + $parts[] = [ + 'source' => 'qa', + 'content' => wp_kses_post( $item['val'] ), + ]; + break 2; + } + } + } + } + + if ( ! empty( $parts ) ) { + wp_send_json_success( mm_build_response( $parts ) ); + } + + // ---- Intent-Scores berechnen -------------------------------- + $intents = mm_score_intents( $q_lc, $q ); + arsort( $intents ); + $active = array_filter( $intents, function( $s ) { return $s > 0; } ); + + $player_name = mm_extract_player_name( $q ); + + // Wenn Discord-Intent erkannt wurde, NUR diesen ausführen + if (isset($active['discord'])) { + $result = mm_handle_intent('discord', $q, $q_lc, $player_name, $bot); + if ($result) { + $parts[] = $result; + } + } elseif (isset($active['team'])) { + $result = mm_handle_intent('team', $q, $q_lc, $player_name, $bot); + if ($result) { + $parts[] = $result; + } + } else { + foreach ( $active as $intent => $score ) { + // Ban-Intent dominant → Regeln nicht zusätzlich auslösen + if ( $intent === 'rules' && isset( $intents['ban_check'] ) && $intents['ban_check'] >= 25 ) { + continue; + } + // Ban-Intent dominant → Shop/Ticket/Forum nicht zusätzlich auslösen + if ( in_array( $intent, array( 'shop', 'ticket', 'forum' ), true ) + && isset( $intents['ban_check'] ) && $intents['ban_check'] >= 40 ) { + continue; + } + // Player-History dominant → Server-Status nicht zusätzlich auslösen + if ( $intent === 'server_status' + && isset( $intents['player_history'] ) && $intents['player_history'] >= 15 ) { + continue; + } + $result = mm_handle_intent( $intent, $q, $q_lc, $player_name, $bot ); + if ( $result ) { + $parts[] = $result; + } + } + } + + if ( empty( $parts ) ) { + $parts[] = mm_fallback_response( $bot ); + } + + wp_send_json_success( mm_build_response( $parts ) ); + + } catch ( \Throwable $e ) { + wp_send_json_success( [ + 'reply' => '⚠️ Interner Fehler: ' . esc_html( $e->getMessage() ), + 'parts' => [], + ] ); + } +} + +// ============================================================ +// INTENT SCORING +// Jeder Treffer erhöht den Score; der Intent mit dem höchsten +// Score wird zuerst beantwortet. Mehrere Intents möglich. +// ============================================================ + +function mm_score_intents( $q_lc, $q_raw ) { + $scores = [ + 'server_status' => 0, + 'ban_check' => 0, + 'player_history' => 0, + 'rules' => 0, + 'wiki' => 0, + 'shop' => 0, + 'ticket' => 0, + 'forum' => 0, + 'gallery' => 0, + 'faq' => 0, + 'discord' => 0, + 'team' => 0, + ]; + // Team + foreach ([ + 'team', 'teammitglieder', 'admins', 'moderatoren', 'wer ist im team', 'wer sind die admins', 'wer sind die moderatoren', 'serverteam', 'staff', 'supporter', 'admin', 'mod', 'leitung', 'server leitung', 'wer ist staff', 'wer ist admin', 'wer ist mod' + ] as $kw) { + if (strpos($q_lc, $kw) !== false) $scores['team'] += 20; + } + // Discord + foreach (['discord', 'dc', 'discord link', 'discord einladung', 'discord server', 'discord invite'] as $kw) { + if (strpos($q_lc, $kw) !== false) $scores['discord'] += 20; + } + + // Server-Status + foreach ( ['ip', 'adresse', 'join', 'einloggen', 'verbinden', 'server ip', + 'wie komme', 'online spieler', 'wieviele spieler', 'server status', + 'serveradresse', 'spieler online'] as $kw ) { + if ( strpos( $q_lc, $kw ) !== false ) $scores['server_status'] += 10; + } + + // Ban / Mute / Kick + foreach ( ['ban', 'gebannt', 'gesperrt', 'mute', 'gemutet', 'stumm', + 'kick', 'gekickt', 'warn', 'verwarnung', 'entban', 'unban', + 'antrag', 'sperre', 'strafe', 'strafen', 'bestraft', + 'meine strafen', 'mein ban', 'sanktion'] as $kw ) { + if ( strpos( $q_lc, $kw ) !== false ) $scores['ban_check'] += 15; + } + // Stärker gewichten bei Ich-Bezug + if ( preg_match( '/\b(bin ich|wurde ich|meine|mein)\b.*(gebannt|gesperrt|gemutet|strafe|ban|mute)/i', $q_raw ) ) { + $scores['ban_check'] += 20; + } + if ( preg_match( '/\b(meine strafen|strafen prüfen|mein status)\b/i', $q_raw ) ) { + $scores['ban_check'] += 25; + } + + // Player History + foreach ( ['spielzeit', 'playtime', 'zuletzt online', 'last seen', + 'letzte login', 'wie lang', 'wie lange', 'war online', + 'online status', 'ist online', 'stunden gespielt'] as $kw ) { + if ( strpos( $q_lc, $kw ) !== false ) $scores['player_history'] += 15; + } + + // Regeln + foreach ( array( 'regel', 'regeln', 'regelwerk', 'darf ich', 'ist erlaubt', + 'verboten', 'richtlinie', 'grief', 'stehlen', 'pvp', + 'chat regel', 'rule', 'verbote', + 'cheaten', 'cheat', 'hacks', 'hacken', 'modifikation', + 'beleidigung', 'schimpfen', 'respekt' ) as $kw ) { + if ( strpos( $q_lc, $kw ) !== false ) $scores['rules'] += 12; + } + + // Wiki + foreach ( ['wiki', 'dokumentation', 'guide', 'anleitung', 'howto', + 'how to', 'wie funktioniert', 'erkläre', 'erklärt'] as $kw ) { + if ( strpos( $q_lc, $kw ) !== false ) $scores['wiki'] += 12; + } + + // Shop + foreach ( [ + 'shop', 'kaufen', 'item', 'items', 'preis', 'kosten', + 'coins', 'tagesaktion', 'daily deal', 'angebot', 'kategorie', + 'warenkorb', 'bestellen', 'rang kaufen', + 'was kostet', 'wie teuer', 'preis von', 'kosten von', 'preis für', 'wie viel kostet' + ] as $kw ) { + if ( strpos( $q_lc, $kw ) !== false ) $scores['shop'] += 12; + } + + // Ticket / Support + foreach ( ['ticket', 'support', 'hilfe', 'problem melden', + 'bug melden', 'melde', 'anfrage', 'beschwerde', + 'anliegen', 'kontaktier'] as $kw ) { + if ( strpos( $q_lc, $kw ) !== false ) $scores['ticket'] += 12; + } + + // Forum + foreach ( ['forum', 'diskussion', 'thread', + 'neuen thread', 'thread erstellen', 'im forum posten', 'im forum schreiben', + 'forum login', 'passwort vergessen', 'forum kategorie', + 'forum nachricht', 'direktnachricht', 'forum profil', + 'forum rang', 'forum level', 'forum mitglieder'] as $kw ) { + if ( strpos( $q_lc, $kw ) !== false ) $scores['forum'] += 10; + } + + // Gallery + foreach ( [ + 'galerie', 'gallery', 'bild', 'bilder', 'screenshot', 'screenshots', + 'foto', 'fotos', 'tagesbild', 'bild des tages', + 'hochladen', 'upload', 'uploaden', 'bild teilen', 'bilder teilen', + 'verifizier', 'verify', '/verify', 'token galerie', + 'meine galerie', 'spieler galerie', 'galerie upload', + ] as $kw ) { + if ( strpos( $q_lc, $kw ) !== false ) $scores['gallery'] += 10; + } + // Upload-Kontext stärker gewichten + if ( preg_match( '/\b(wie|kann ich|wie kann|anleitung|schritt)\b.*(bild|foto|screenshot|hochlad|upload)/i', $q_lc ) ) { + $scores['gallery'] += 20; + } + + // FAQ: Mehr Keywords und höheres Scoring, damit typische Fragen wie "Wie ...?", "Was ...?", "Wieso ...?", "Erklärung ..." erkannt werden + foreach ([ + 'faq', 'häufige frage', 'frequently', 'oft gefragt', + 'wie ', 'was ', 'wieso ', 'wo ', 'wer ', 'hilfe', 'erklärung', 'funktioniert', 'bedeutet', 'nutzen', 'benutzen', 'erkläre', 'erklärt', 'erfahre', 'erfahren', 'frage', 'fragen', 'hilfe', 'problem', 'tipp', 'tipps', 'anleitung', 'erläutere', 'erläutert', 'erläutern', 'erläutere', 'erläutert', 'erläutern', 'beschreibe', 'beschreibt', 'beschreiben', 'beschreibe', 'beschreibt', 'beschreiben', 'wie kann', 'wie mache', 'wie mache ich', 'wie funktioniert', 'wie nutze', 'wie benutze', 'wie setze', 'wie entferne', 'wie öffne', 'wie melde', 'wie erreiche', 'wie akzeptiere', 'wie fordere', 'wie wechsle', 'wie sehe', 'wie teleportiere', 'wie sende', 'wie setze ich', 'wie entferne ich', 'wie öffne ich', 'wie melde ich', 'wie erreiche ich', 'wie akzeptiere ich', 'wie fordere ich', 'wie wechsle ich', 'wie sehe ich', 'wie teleportiere ich', 'wie sende ich' + ] as $kw) { + if ( strpos( $q_lc, $kw ) !== false ) $scores['faq'] += 18; + } + + return $scores; +} + +// ============================================================ +// INTENT HANDLER DISPATCHER +// ============================================================ + +function mm_handle_intent( $intent, $q, $q_lc, $player_name, $bot ) { + switch ( $intent ) { + case 'server_status': return mm_intent_server_status( $bot ); + case 'ban_check': return mm_intent_ban_check( $q, $player_name, $bot ); + case 'player_history': return mm_intent_player_history( $q, $player_name ); + case 'rules': return mm_intent_rules( $q, $bot ); + case 'wiki': return mm_intent_wiki( $q ); + case 'shop': return mm_intent_shop( $q_lc, $bot ); + case 'ticket': return mm_intent_ticket( $q, $bot ); + case 'forum': return mm_intent_forum( $q ); + case 'gallery': return mm_intent_gallery( $bot, $q_lc ); + case 'faq': return mm_intent_faq( $q ); + case 'discord': return mm_intent_discord( $bot ); + case 'team': return mm_intent_team(); + } +} + +// ============================================================ +// INTENT: TEAM +// ============================================================ +function mm_intent_team() { + $query = new WP_Query( array( + 'post_type' => 'team_member', + 'posts_per_page' => -1, + 'orderby' => 'menu_order', + 'order' => 'ASC', + ) ); + if ( ! $query->have_posts() ) { + return [ + 'source' => 'team', + 'title' => '👥 Team', + 'content' => 'Es wurden noch keine Teammitglieder eingetragen.', + ]; + } + $html = '
'; + $html .= '
👥 Team
'; + while ( $query->have_posts() ) { + $query->the_post(); + $name = get_the_title(); + $rank = get_post_meta( get_the_ID(), '_team_member_rank', true ); + $uuid = get_post_meta( get_the_ID(), '_team_member_uuid', true ); + $img = get_the_post_thumbnail_url( get_the_ID(), 'medium' ); + $desc = get_the_excerpt(); + $avatar = ''; + if ( $uuid ) { + $avatar = 'https://visage.surgeplay.com/bust/' . esc_attr( $uuid ) . '.png'; + } elseif ( $img ) { + $avatar = $img; + } + $html .= '
'; + if ( $avatar ) { + $html .= '' . esc_attr( $name ) . ''; + } else { + $html .= '
' . esc_html( mb_substr( $name, 0, 1 ) ) . '
'; + } + $html .= '
'; + $html .= '
' . esc_html( $name ) . '
'; + if ( $rank ) { + $html .= '
' . esc_html( $rank ) . '
'; + } + if ( $desc ) { + $html .= '
' . esc_html( $desc ) . '
'; + } + $html .= '
'; + $html .= '
'; + } + $html .= '
'; + wp_reset_postdata(); + return [ + 'source' => 'team', + 'title' => '👥 Team', + 'content' => $html, + ]; +} +// ============================================================ +// INTENT: DISCORD +// ============================================================ + +function mm_intent_discord( $bot ) { + $discord = $bot['link_discord'] ?? ''; + if ( ! empty( $discord ) ) { + return [ + 'source' => 'discord', + 'title' => '💬 Discord', + 'content' => 'Hier ist der Discord-Einladungslink:
' . esc_html( $discord ) . '', + ]; + } else { + return [ + 'source' => 'discord', + 'title' => '💬 Discord', + 'content' => 'Es wurde kein Discord-Link im Backend hinterlegt.', + ]; + } +} + +// ============================================================ +// INTENT: SERVER STATUS (BungeeCord Status Plugin) +// ============================================================ + +function mm_intent_server_status( $bot ) { + $lines = array(); + + // ── 1. Statische Infos aus Bot-Setup ────────────────────── + $ip = $bot['server_ip'] ?? ''; + $ver = $bot['server_ver'] ?? ''; + $specs = $bot['server_specs'] ?? ''; + $discord = $bot['link_discord'] ?? ''; + + if ( $ip ) $lines[] = '🌐 Adresse: ' . esc_html( $ip ) . ''; + if ( $ver ) $lines[] = '🎮 Version: ' . esc_html( $ver ); + if ( $specs ) $lines[] = '🖥️ Server: ' . esc_html( $specs ); + + // ── 2. Live-Daten via BungeeCord-Status-Plugin ───────────── + $servers = get_option( 'mcss_servers', array() ); + + if ( ! empty( $servers ) && function_exists( 'mcss_fetch_server_with_ranks' ) ) { + $srv = $servers[0]; + $cache_key = 'mm_srv_' . md5( ( $srv['host'] ?? '' ) . ( $srv['player_port'] ?? '9191' ) ); + $data = get_transient( $cache_key ); + + // Caching-Zeit erhöht auf 60 Sekunden + if ( false === $data ) { + try { + $data = mcss_fetch_server_with_ranks( $srv ); + } catch ( \Throwable $e ) { + $data = null; + } + set_transient( $cache_key, $data, 60 ); + } + + if ( ! empty( $data ) && isset( $data['online'] ) ) { + $is_online = (bool) $data['online']; + $icon = $is_online ? '🟢' : '🔴'; + $status = $is_online ? 'Online' : 'Offline'; + + // Spieler-Anzahl (players = Array von Objekten) + $player_list = isset( $data['players'] ) && is_array( $data['players'] ) ? $data['players'] : array(); + $player_count = count( $player_list ); + $player_text = $is_online ? ' · ' . $player_count . ' Spieler online' : ''; + + $srv_label = ! empty( $srv['name'] ) ? esc_html( $srv['name'] ) . ': ' : ''; + $lines[] = $icon . ' ' . $srv_label . $status . $player_text; + + // Version vom Server + if ( $is_online && ! empty( $data['version'] ) && empty( $ver ) ) { + $lines[] = '🎮 Version: ' . esc_html( $data['version'] ); + } + + // MOTD + if ( $is_online && ! empty( $data['motd'] ) ) { + $motd = is_array( $data['motd'] ) + ? implode( ' ', $data['motd'] ) + : (string) $data['motd']; + $motd = trim( $motd ); + if ( $motd ) { + $lines[] = '💬 ' . mm_format_motd( $motd ); + } + } + + // Spieler auflisten (bis 8) + if ( $is_online && ! empty( $player_list ) ) { + $names = array(); + foreach ( array_slice( $player_list, 0, 8 ) as $p ) { + if ( is_array( $p ) && ! empty( $p['name'] ) ) { + $names[] = esc_html( $p['name'] ); + } elseif ( is_string( $p ) ) { + $names[] = esc_html( $p ); + } + } + if ( ! empty( $names ) ) { + $lines[] = '👥 Spieler: ' . implode( ', ', $names ) + . ( $player_count > 8 ? ' +' . ( $player_count - 8 ) . ' weitere' : '' ); + } + } + } + + // Weitere konfigurierte Server kurz anzeigen + if ( count( $servers ) > 1 ) { + $extra = array(); + foreach ( array_slice( $servers, 1 ) as $s ) { + if ( empty( $s['name'] ) ) continue; + $ck = 'mm_srv_' . md5( ( $s['host'] ?? '' ) . ( $s['player_port'] ?? '9191' ) ); + $d = get_transient( $ck ); + if ( false === $d && function_exists( 'mcss_fetch_server_with_ranks' ) ) { + try { + $d = mcss_fetch_server_with_ranks( $s ); + } catch ( \Throwable $e ) { + $d = null; + } + set_transient( $ck, $d, 60 ); + } + $on = ! empty( $d['online'] ); + $count = ( $on && ! empty( $d['players'] ) ) ? count( $d['players'] ) : 0; + $extra[] = ( $on ? '🟢' : '🔴' ) . ' ' . esc_html( $s['name'] ) + . ( $on ? ' (' . $count . ')' : '' ); + } + if ( ! empty( $extra ) ) { + $lines[] = '🗺️ Weitere Server: ' . implode( ' · ', $extra ); + } + } + } + + // ── 3. Kein Plugin, keine Daten ─────────────────────────── + if ( empty( $lines ) ) { + return array( + 'source' => 'server_status', + 'title' => '🖥️ Server-Status', + 'content' => 'Server-Infos wurden noch nicht konfiguriert. Bitte im Bot-Setup eintragen.', + ); + } + + // ── 4. Links (nur Discord, kein Wiki-Link mehr) ───────────── + if ( $discord ) { + $lines[] = '💬 Discord'; + } + + return array( + 'source' => 'server_status', + 'title' => '🖥️ Server-Status', + 'content' => implode( '
', $lines ), + ); +} + +// ============================================================ +// HILFSFUNKTION: Minecraft §-Farbcodes → HTML +// Unterstützt §0-§f (Farben) und §l/§o/§n/§m/§r (Format) +// ============================================================ + +function mm_format_motd( $text ) { + if ( empty( $text ) ) return ''; + + $colors = 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', + ); + + // Normalisieren: § und & beide unterstützen + $section = "\xC2\xA7"; // UTF-8 für § + $text = str_replace( '&', $section, $text ); + + // Aufteilen nach §X Segmenten + $parts = preg_split( '/\xC2\xA7([0-9a-fklmnorA-FKLMNOR])/', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); + $output = ''; + $open = 0; + $bold = false; + $italic = false; + + foreach ( $parts as $i => $part ) { + if ( $i % 2 === 0 ) { + // Text-Segment + if ( $part !== '' ) { + $output .= esc_html( $part ); + } + } else { + // Code-Segment + $code = strtolower( $part ); + + // Offene Spans schließen + if ( $open > 0 ) { + $output .= str_repeat( '', $open ); + $open = 0; + } + if ( $bold ) { $output .= ''; $bold = false; } + if ( $italic ) { $output .= ''; $italic = false; } + + if ( isset( $colors[ $code ] ) ) { + $output .= ''; + $open++; + } elseif ( $code === 'l' ) { + $output .= ''; + $bold = true; + } elseif ( $code === 'o' ) { + $output .= ''; + $italic = true; + } elseif ( $code === 'r' ) { + // Reset — alles schon geschlossen oben + } + // §k (obfuscated), §m (strike), §n (underline) ignorieren wir + } + } + + // Offene Tags schließen + if ( $open > 0 ) $output .= str_repeat( '', $open ); + if ( $bold ) $output .= ''; + if ( $italic ) $output .= ''; + + return $output; +} + +// ============================================================ +// INTENT: BAN / MUTE / KICK / WARN (LiteBans Manager) +// ============================================================ + +function mm_intent_ban_check( $q, $player_name, $bot ) { + $dashboard_url = isset( $bot["litebans_dashboard_url"] ) ? $bot["litebans_dashboard_url"] : ""; + + if ( empty( $player_name ) ) { + $html = '🔍 Welchen Spielernamen soll ich prüfen?

'; + $html .= 'Tippe deinen Minecraft-Namen direkt ein:

'; + $html .= '
'; + $html .= 'Format:'; + $html .= ''; + $html .= '
'; + $html .= '
Ersetze DeinName mit deinem echten Minecraft-Namen.
'; + $html .= 'Oder schreibe z.B.: ist Steve gebannt
'; + if ( $dashboard_url ) { + $html .= '

LiteBans Dashboard öffnen'; + } + return array( "source" => "ban_check", "title" => "🔨 Strafen prüfen", "content" => $html ); + } + + $info = mm_litebans_query( $player_name ); + + if ( is_null( $info ) ) { + return array( "source" => "ban_check", "title" => "🔨 Ban-Status", + "content" => "⚠️ LiteBans ist nicht konfiguriert oder die Datenbank ist nicht erreichbar." ); + } + + if ( isset( $info["not_found"] ) ) { + return array( "source" => "ban_check", "title" => "🔨 Ban-Status: " . esc_html( $player_name ), + "content" => "❓ Spieler " . esc_html( $player_name ) . " wurde in der LiteBans-Datenbank nicht gefunden." ); + } + + $lines = array(); + + // Avatar + Name + $lines[] = "
" + . "Avatar" + . "" . esc_html( $info["player"] ) . "" + . "
"; + + // Aktiver Ban + if ( $info["active_ban"] ) { + $b = $info["active_ban"]; + $lines[] = "
"; + $lines[] = "🚫 Aktiver Ban"; + $lines[] = "📋 Grund: " . esc_html( $b["reason"] ); + $lines[] = "👮 Von: " . esc_html( $b["by"] ); + $lines[] = "⏰ Bis: " . esc_html( $b["until"] ) . ""; + $lines[] = "
"; + } else { + $lines[] = "✅ Kein aktiver Ban"; + } + + // Aktiver Mute + if ( $info["active_mute"] ) { + $m = $info["active_mute"]; + $lines[] = "
"; + $lines[] = "🔇 Aktiver Mute"; + $lines[] = "📋 Grund: " . esc_html( $m["reason"] ); + $lines[] = "⏰ Bis: " . esc_html( $m["until"] ) . ""; + $lines[] = "
"; + } else { + $lines[] = "✅ Kein aktiver Mute"; + } + + // Kicks & Warns + $stats = array(); + if ( $info["kicks"] > 0 ) $stats[] = "👢 " . $info["kicks"] . " Kick(s)"; + if ( $info["warnings"] > 0 ) $stats[] = "⚠️ " . $info["warnings"] . " Verwarnung(en)"; + if ( ! empty( $stats ) ) $lines[] = implode( " · ", $stats ); + + // Ban-History + if ( ! empty( $info["ban_history"] ) ) { + $lines[] = "
🕐 Letzte Bestrafungen:"; + foreach ( $info["ban_history"] as $h ) { + $icon = $h["type"] === "ban" ? "🚫" : ( $h["type"] === "mute" ? "🔇" : ( $h["type"] === "kick" ? "👢" : "⚠️" ) ); + $lines[] = $icon . " " . esc_html( $h["date"] ) + . " – " . esc_html( $h["reason"] ) + . " (" . esc_html( $h["by"] ) . ")"; + } + } + + // Entbannungsanträge + if ( ! empty( $info["unban_requests"] ) ) { + $lines[] = "
📋 Entbannungsanträge:"; + foreach ( $info["unban_requests"] as $r ) { + $lines[] = $r["status"] . " – " . esc_html( $r["date"] ); + } + } + + // Links + $link_parts = array(); + if ( $dashboard_url ) { + $link_parts[] = "Dashboard"; + } + if ( $info["active_ban"] && $dashboard_url ) { + $unban_url = add_query_arg( array( + "lb_player" => $info["player"], + "lb_reason" => $info["active_ban"]["reason"], + ), $dashboard_url ); + $link_parts[] = "Entbannungsantrag stellen →"; + } + if ( ! empty( $link_parts ) ) $lines[] = "
" . implode( " · ", $link_parts ); + + $status_icon = $info["active_ban"] ? "🚫" : "✅"; + + return array( + "source" => "ban_check", + "title" => $status_icon . " Ban-Status: " . esc_html( $info["player"] ), + "content" => implode( "
", $lines ), + ); +} + +function mm_litebans_query( $player_name ) { + $settings = get_option( "wp_litebans_pro_settings", array() ); + if ( empty( $settings["db_name"] ) || empty( $settings["db_user"] ) ) return null; + + $prefix = isset( $settings["table_prefix"] ) ? $settings["table_prefix"] : "litebans_"; + $db_host = isset( $settings["db_host"] ) ? $settings["db_host"] : "localhost"; + if ( ! empty( $settings["db_port"] ) ) $db_host .= ":" . intval( $settings["db_port"] ); + + try { + $lbdb = new wpdb( $settings["db_user"], $settings["db_pass"], $settings["db_name"], $db_host ); + $lbdb->suppress_errors( true ); + $lbdb->prefix = $prefix; + } catch ( Exception $e ) { + return null; + } + + $name = sanitize_text_field( $player_name ); + + // UUID über history-Tabelle (wie das Plugin selbst) + $uuid = $lbdb->get_var( $lbdb->prepare( + "SELECT uuid FROM {$prefix}history WHERE name = %s ORDER BY id DESC LIMIT 1", $name + ) ); + + if ( empty( $uuid ) ) { + // Fuzzy-Suche + $row = $lbdb->get_row( $lbdb->prepare( + "SELECT uuid, name FROM {$prefix}history WHERE name LIKE %s ORDER BY id DESC LIMIT 1", + "%" . $lbdb->esc_like( $name ) . "%" + ) ); + if ( ! $row ) return array( "not_found" => true ); + $uuid = $row->uuid; + $name = $row->name; + } + + $info = array( "player" => $name, "active_ban" => null, "active_mute" => null, + "kicks" => 0, "warnings" => 0, "ban_history" => array(), "unban_requests" => array() ); + + // Aktiver Ban (via UUID) + $ban = $lbdb->get_row( $lbdb->prepare( + "SELECT reason, banned_by_name, until, active, removed_by_name FROM {$prefix}bans WHERE uuid = %s AND active = 1 ORDER BY time DESC LIMIT 1", $uuid + ) ); + if ( $ban && intval( $ban->active ) === 1 && empty( $ban->removed_by_name ) ) { + $until = (int) $ban->until; + $info["active_ban"] = array( + "reason" => mm_lb_clean( $ban->reason ), + "by" => $ban->banned_by_name, + "until" => ( $until <= 0 ) ? "Permanent" : date_i18n( "d.m.Y H:i", $until / 1000 ), + ); + } + + // Aktiver Mute (via UUID) + $mute = $lbdb->get_row( $lbdb->prepare( + "SELECT reason, banned_by_name, until, active, removed_by_name FROM {$prefix}mutes WHERE uuid = %s AND active = 1 ORDER BY time DESC LIMIT 1", $uuid + ) ); + if ( $mute && intval( $mute->active ) === 1 && empty( $mute->removed_by_name ) ) { + $until = (int) $mute->until; + $info["active_mute"] = array( + "reason" => mm_lb_clean( $mute->reason ), + "by" => $mute->banned_by_name, + "until" => ( $until <= 0 ) ? "Permanent" : date_i18n( "d.m.Y H:i", $until / 1000 ), + ); + } + + // Kicks & Warnings (UUID) + $info["kicks"] = (int) $lbdb->get_var( $lbdb->prepare( "SELECT COUNT(*) FROM {$prefix}kicks WHERE uuid = %s", $uuid ) ); + if ( $lbdb->get_var( "SHOW TABLES LIKE '{$prefix}warnings'" ) ) { + $info["warnings"] = (int) $lbdb->get_var( $lbdb->prepare( "SELECT COUNT(*) FROM {$prefix}warnings WHERE uuid = %s", $uuid ) ); + } + + // Ban-History (letzte 5 aller Typen) + $history = array(); + foreach ( array( "bans", "mutes", "kicks" ) as $t ) { + $rows = $lbdb->get_results( $lbdb->prepare( + "SELECT reason, banned_by_name, time FROM {$prefix}{$t} WHERE uuid = %s ORDER BY time DESC LIMIT 3", $uuid + ) ); + $type = rtrim( $t, "s" ); // bans→ban, mutes→mute, kicks→kick + foreach ( (array) $rows as $r ) { + $history[] = array( "type" => $type, "date" => date_i18n( "d.m.Y", $r->time / 1000 ), + "reason" => mm_lb_clean( $r->reason ), "by" => $r->banned_by_name, "ts" => $r->time ); + } + } + usort( $history, function( $a, $b ) { return $b["ts"] - $a["ts"]; } ); + $info["ban_history"] = array_slice( $history, 0, 5 ); + + // Entbannungsanträge (WP CPT) + $requests = get_posts( array( "post_type" => "unban_request", "posts_per_page" => 5, + "post_status" => "any", "meta_query" => array( array( "key" => "_lb_player", "value" => $name, "compare" => "=" ) ) ) ); + foreach ( $requests as $r ) { + $status = get_post_meta( $r->ID, "_lb_status", true ) ?: "pending"; + $label = $status === "approved" ? "✅ Angenommen" : ( $status === "rejected" ? "❌ Abgelehnt" : "⏳ Wartend" ); + $info["unban_requests"][] = array( "date" => get_the_date( "d.m.Y", $r->ID ), "status" => $label ); + } + + return $info; +} + +function mm_lb_clean( $text ) { + if ( ! $text ) return "–"; + return preg_replace( "/(?:§|&)[0-9a-fk-orA-FK-OR]/u", "", $text ); +} + +// ============================================================ +// INTENT: PLAYER HISTORY (MC Player History Plugin) +// ============================================================ +// INTENT: PLAYER HISTORY (MC Player History Plugin) +// ============================================================ + +function mm_intent_player_history( $q, $player_name ) { + $bot = get_option( 'mm_bot_data', [] ); + + // Nur aktiv wenn URL im Backend gesetzt + if ( empty( $bot['url_player_history'] ) ) return null; + + $history_url = $bot['url_player_history']; + + // Kein Spielername → Top-Liste + Hinweis + if ( empty( $player_name ) ) { + global $wpdb; + $table = $wpdb->prefix . 'mc_players'; + if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) { + return [ + 'source' => 'player_history', + 'title' => '👤 Spieler-History', + 'content' => 'Das MC Player History Plugin ist nicht installiert oder die Tabelle fehlt.', + ]; + } + + // Top 5 nach Spielzeit + $top = $wpdb->get_results( + "SELECT username, prefix, playtime_seconds, is_online + FROM $table WHERE playtime_seconds > 0 + ORDER BY playtime_seconds DESC LIMIT 5" + ); + + $html = 'Nenne mir einen Minecraft-Namen, z.B.: Spielzeit von Steve

'; + + if ( ! empty( $top ) ) { + $html .= '🏆 Top-Spieler:
'; + $i = 1; + foreach ( $top as $p ) { + $prefix_html = mm_ph_format_prefix( $p->prefix ); + $time_str = mm_format_playtime( (int) $p->playtime_seconds ); + $online_dot = $p->is_online ? '🟢' : '⚫'; + $html .= $online_dot . ' ' . ( $prefix_html ? $prefix_html . ' ' : '' ) + . '' . esc_html( $p->username ) . '' + . ' – ' . esc_html( $time_str ) . '
'; + $i++; + } + $html .= '
'; + } + + $html .= "→ Alle Spieler ansehen"; + + return [ + 'source' => 'player_history', + 'title' => '👤 Spieler-History', + 'content' => $html, + ]; + } + + // Spieler suchen + global $wpdb; + $table = $wpdb->prefix . 'mc_players'; + + if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) { + return [ + 'source' => 'player_history', + 'title' => '👤 Spieler-Info', + 'content' => 'MC Player History Plugin ist nicht aktiv oder Tabelle fehlt.', + ]; + } + + // Exakter Treffer + $row = $wpdb->get_row( $wpdb->prepare( + "SELECT username, prefix, is_online, first_seen, last_seen, playtime_seconds + FROM $table WHERE username = %s LIMIT 1", + $player_name + ) ); + + // Fuzzy-Suche als Fallback + if ( ! $row ) { + $row = $wpdb->get_row( $wpdb->prepare( + "SELECT username, prefix, is_online, first_seen, last_seen, playtime_seconds + FROM $table WHERE username LIKE %s LIMIT 1", + '%' . $wpdb->esc_like( $player_name ) . '%' + ) ); + } + + if ( ! $row ) { + return [ + 'source' => 'player_history', + 'title' => '👤 Spieler: ' . esc_html( $player_name ), + 'content' => '❓ Spieler ' . esc_html( $player_name ) . ' wurde noch nicht auf dem Server gesehen.' + . '

Spieler-Liste öffnen', + ]; + } + + $online_icon = $row->is_online + ? '🟢 Gerade online' + : '⚫ Offline'; + + $playtime = mm_format_playtime( (int) $row->playtime_seconds ); + $last_seen = $row->last_seen + ? wp_date( 'd.m.Y \u\m H:i \U\h\r', strtotime( $row->last_seen ) ) + : 'Unbekannt'; + $first_seen = $row->first_seen + ? wp_date( 'd.m.Y', strtotime( $row->first_seen ) ) + : 'Unbekannt'; + + $lines = [ + '📶 Status: ' . $online_icon, + '⏱️ Spielzeit: ' . esc_html( $playtime ) . '', + '🕐 Zuletzt gesehen: ' . esc_html( $last_seen ) . '', + '📅 Erstmals gesehen: ' . esc_html( $first_seen ) . '', + ]; + + // Rang / Prefix (mit MC-Farben) + if ( ! empty( $row->prefix ) ) { + $prefix_html = mm_ph_format_prefix( $row->prefix ); + if ( $prefix_html !== '' ) { + $lines[] = '🏷️ Rang: ' . $prefix_html; + } + } + + // Avatar + $lines[] = 'Avatar ' + . 'Spieler-Liste öffnen'; + + return [ + 'source' => 'player_history', + 'title' => '👤 Spieler: ' . esc_html( $row->username ), + 'content' => implode( '
', $lines ), + ]; +} + +/** + * Minecraft-Farbcodes in HTML umwandeln. + * Nutzt mcph_parse_minecraft_colors() vom Plugin wenn verfügbar, + * ansonsten eigene Implementierung als Fallback. + */ +function mm_ph_format_prefix( $prefix ) { + if ( empty( $prefix ) ) return ''; + + // Plugin-eigene Funktion bevorzugen + if ( function_exists( 'mcph_parse_minecraft_colors' ) ) { + return mcph_parse_minecraft_colors( $prefix ); + } + + // Fallback: eigene Implementierung + $map = [ + '&0' => '', + '&1' => '', + '&2' => '', + '&3' => '', + '&4' => '', + '&5' => '', + '&6' => '', + '&7' => '', + '&8' => '', + '&9' => '', + '&a' => '', + '&b' => '', + '&c' => '', + '&d' => '', + '&e' => '', + '&f' => '', + '&l' => '', + '&o' => '', + '&n' => '', + '&m' => '', + ]; + + // Anzahl geöffneter Spans zählen für sauberes Schließen + $open = 0; + $text = str_ireplace( array_keys( $map ), array_values( $map ), esc_html( $prefix ) ); + + // &r = alle schließen + $text = preg_replace_callback( '/&r/i', function() use ( &$open ) { + $close = str_repeat( '', $open ); + $open = 0; + return $close; + }, $text ); + + // Spans mitzählen + preg_match_all( '//', $text, $closes ); + $remaining = count( $opens[0] ) - count( $closes[0] ); + if ( $remaining > 0 ) { + $text .= str_repeat( '', $remaining ); + } + + return $text; +} + +/** Sekunden in lesbare Spielzeit umrechnen */ +function mm_format_playtime( $seconds ) { + if ( $seconds <= 0 ) return '0 Minuten'; + $h = floor( $seconds / 3600 ); + $m = floor( ( $seconds % 3600 ) / 60 ); + if ( $h > 0 ) return "{$h}h {$m}min"; + return "{$m} Minuten"; +} + +// ============================================================ +// INTENT: REGELN (Multi Rules Plugin) +// ============================================================ + +function mm_intent_rules( $q, $bot ) { + $rules_url = ! empty( $bot['url_rules'] ) ? $bot['url_rules'] : home_url( '/regeln' ); + $options = get_option( 'mrp_settings' ); + $tabs = isset( $options['tabs'] ) && is_array( $options['tabs'] ) ? $options['tabs'] : array(); + + // ── Schritt 2: Kategorie wurde gewählt → "regeln:Minecraft" ── + if ( preg_match( '/^regeln?:(.+)$/iu', trim( $q ), $m ) ) { + $chosen = trim( $m[1] ); + return mm_rules_show_tab( $chosen, $tabs, $rules_url ); + } + + // ── Spezifische Suche: Suchbegriff vorhanden ────────────── + $q_clean = trim( preg_replace( + '/\b(regel|regeln|regelwerk|zeig|alle|zeige|liste|gibt es|was sind)\b/iu', + '', $q + ) ); + $q_clean = trim( preg_replace( '/\s+/', ' ', $q_clean ) ); + + if ( strlen( $q_clean ) >= 3 ) { + return mm_rules_search( $q_clean, $tabs, $rules_url ); + } + + // ── Schritt 1: Allgemein → Kategorie-Auswahl ───────────── + if ( empty( $tabs ) ) { + return array( + 'source' => 'rules', + 'title' => '📜 Regelwerk', + 'content' => "→ Zum Regelwerk", + ); + } + + $html = 'Wähle eine Kategorie:

'; + $html .= '
'; + + foreach ( $tabs as $tab ) { + $tab_title = $tab['title'] ?? 'Allgemein'; + $rule_count = ! empty( $tab['rules'] ) ? count( $tab['rules'] ) : 0; + // Trigger: "regeln:Minecraft" + $query = 'regeln:' . $tab_title; + $html .= ''; + } + + $html .= '
'; + $html .= '
Alle Regeln im Browser öffnen'; + + return array( + 'source' => 'rules', + 'title' => '📜 Regelwerk', + 'content' => $html, + ); +} + +/** + * Alle Regeln eines bestimmten Tabs anzeigen. + */ +function mm_rules_show_tab( $tab_name, $tabs, $rules_url ) { + $found_tab = null; + + foreach ( $tabs as $tab ) { + if ( mb_strtolower( $tab['title'] ?? '' ) === mb_strtolower( $tab_name ) ) { + $found_tab = $tab; + break; + } + } + + // Fuzzy-Fallback + if ( ! $found_tab ) { + foreach ( $tabs as $tab ) { + if ( strpos( mb_strtolower( $tab['title'] ?? '' ), mb_strtolower( $tab_name ) ) !== false ) { + $found_tab = $tab; + break; + } + } + } + + if ( ! $found_tab ) { + return array( + 'source' => 'rules', + 'title' => '📜 Regelwerk', + 'content' => 'Kategorie ' . esc_html( $tab_name ) . ' nicht gefunden.
' + . "→ Zum Regelwerk", + ); + } + + $rules = isset( $found_tab['rules'] ) && is_array( $found_tab['rules'] ) ? $found_tab['rules'] : array(); + + if ( empty( $rules ) ) { + return array( + 'source' => 'rules', + 'title' => '📜 ' . esc_html( $found_tab['title'] ), + 'content' => 'In dieser Kategorie sind noch keine Regeln hinterlegt.', + ); + } + + $html = ''; + foreach ( $rules as $rule ) { + $title = $rule['rule_title'] ?? ''; + $content = $rule['rule_content'] ?? ''; + + $content_plain = wp_strip_all_tags( $content ); + if ( mb_strlen( $content_plain ) > 300 ) { + $rendered = '

' + . esc_html( mb_substr( $content_plain, 0, 300 ) ) . '…

'; + } else { + $rendered = '
' + . wp_kses_post( $content ) + . '
'; + } + + $html .= '
'; + $html .= '📌 ' . esc_html( $title ) . ''; + $html .= $rendered; + $html .= '
'; + } + + // Zurück-Button zur Kategorieauswahl + $html .= '
'; + $html .= ''; + $html .= '' + . 'Im Browser öffnen'; + $html .= '
'; + + return array( + 'source' => 'rules', + 'title' => '📜 ' . esc_html( $found_tab['title'] ) . ' – ' . count( $rules ) . ' Regeln', + 'content' => $html, + ); +} + +/** + * Volltext-Suche über alle Tabs + Regeln. + */ +function mm_rules_search( $q_clean, $tabs, $rules_url ) { + $q_lc = mb_strtolower( $q_clean ); + $words = array_filter( preg_split( '/\s+/', $q_lc ), function( $w ) { + $stop = array( 'regel', 'regeln', 'ist', 'darf', 'ich', 'erlaubt', 'verboten', 'wie', 'was', 'kann' ); + return strlen( $w ) > 2 && ! in_array( $w, $stop, true ); + } ); + + $matches = array(); + + foreach ( $tabs as $tab ) { + $tab_title = $tab['title'] ?? 'Allgemein'; + if ( empty( $tab['rules'] ) || ! is_array( $tab['rules'] ) ) continue; + + foreach ( $tab['rules'] as $rule ) { + $r_title = mb_strtolower( $rule['rule_title'] ?? '' ); + $r_content = mb_strtolower( wp_strip_all_tags( $rule['rule_content'] ?? '' ) ); + $t_lc = mb_strtolower( $tab_title ); + $score = 0; + + if ( strpos( $r_title, $q_lc ) !== false ) $score += 30; + if ( strpos( $r_content, $q_lc ) !== false ) $score += 15; + if ( strpos( $t_lc, $q_lc ) !== false ) $score += 10; + + foreach ( $words as $w ) { + if ( strpos( $r_title, $w ) !== false ) $score += 12; + if ( strpos( $r_content, $w ) !== false ) $score += 5; + if ( strpos( $t_lc, $w ) !== false ) $score += 4; + } + + if ( $score > 0 ) { + $matches[] = array( + 'score' => $score, + 'tab' => $tab_title, + 'title' => $rule['rule_title'] ?? '', + 'content' => $rule['rule_content'] ?? '', + ); + } + } + } + + if ( empty( $matches ) ) { + // Keine Treffer → direkt zur Kategorieauswahl + $html = 'Keine Regel zu ' . esc_html( $q_clean ) . ' gefunden.

'; + $html .= 'Wähle eine Kategorie:

'; + $html .= '
'; + foreach ( $tabs as $tab ) { + $tab_title = $tab['title'] ?? 'Allgemein'; + $html .= ''; + } + $html .= '
'; + + return array( + 'source' => 'rules', + 'title' => '📜 Regelwerk', + 'content' => $html, + ); + } + + usort( $matches, function( $a, $b ) { return $b['score'] - $a['score']; } ); + $top = array_slice( $matches, 0, 3 ); + $html = ''; + + foreach ( $top as $r ) { + $content_plain = wp_strip_all_tags( $r['content'] ); + if ( mb_strlen( $content_plain ) > 300 ) { + $rendered = '

' + . esc_html( mb_substr( $content_plain, 0, 300 ) ) . '…

'; + } else { + $rendered = '
' + . wp_kses_post( $r['content'] ) + . '
'; + } + + $html .= '
'; + $html .= '📌 ' . esc_html( $r['title'] ) . ''; + $html .= ' (' . esc_html( $r['tab'] ) . ')'; + $html .= $rendered; + $html .= '
'; + } + + if ( count( $matches ) > 3 ) { + $html .= '' . ( count( $matches ) - 3 ) . ' weitere Treffer vorhanden.
'; + } + + $html .= '
'; + $html .= ''; + $html .= 'Im Browser öffnen'; + $html .= '
'; + + return array( + 'source' => 'rules', + 'title' => '📜 Regelwerk – ' . count( $matches ) . ' Treffer', + 'content' => $html, + ); +} + +// ============================================================ +// INTENT: WIKI (WP Multi Wiki Plugin) +// ============================================================ + +function mm_intent_wiki( $q ) { + if ( ! post_type_exists( 'wmw_article' ) ) return null; + + // Suchbegriff bereinigen + $search = preg_replace( '/\b(wiki|anleitung|guide|howto|how to|dokument|erkläre?|zeig mir)\b/i', '', $q ); + $search = trim( preg_replace( '/\s+/', ' ', $search ) ); + + if ( strlen( $search ) < 2 ) { + // Alle Wikis auflisten + if ( function_exists( 'wmw_get_wikis' ) ) { + $wikis = wmw_get_wikis(); + if ( ! empty( $wikis ) ) { + $links = array_map( function( $w ) { + return '→ ' + . esc_html( $w->post_title ) . ''; + }, $wikis ); + return [ + 'source' => 'wiki', + 'title' => '📖 Wiki', + 'content' => 'Verfügbare Wikis:
' . implode( '
', $links ), + ]; + } + } + return null; + } + + $args = [ + 'post_type' => 'wmw_article', + 'post_status' => 'publish', + 's' => $search, + 'posts_per_page' => 4, + 'orderby' => 'relevance', + ]; + $results = get_posts( $args ); + + if ( empty( $results ) ) { + // Fallback: alle Wiki-Artikel + $bot = get_option( 'mm_bot_data', [] ); + $url = ! empty( $bot['url_wiki'] ) ? $bot['url_wiki'] : ''; + $link = $url ? " → Wiki öffnen" : ''; + return [ + 'source' => 'wiki', + 'title' => '📖 Wiki', + 'content' => 'Kein Wiki-Artikel zu „' . esc_html( $search ) . '“ gefunden.' . $link, + ]; + } + + $html = []; + foreach ( $results as $post ) { + $excerpt = get_the_excerpt( $post->ID ); + $html[] = '📄 ' + . esc_html( $post->post_title ) . '' + . ( $excerpt ? '
' . wp_trim_words( $excerpt, 25, '…' ) . '' : '' ); + } + + return [ + 'source' => 'wiki', + 'title' => '📖 Wiki-Treffer', + 'content' => implode( '

', $html ), + ]; +} + +// ============================================================ +// INTENT: SHOP (WP Ingame Shop Pro) +// ============================================================ + +function mm_intent_shop( $q_lc, $bot ) { + global $wpdb; + $t_items = $wpdb->prefix . 'wis_items'; + $t_cats = $wpdb->prefix . 'wis_categories'; + + if ( $wpdb->get_var( "SHOW TABLES LIKE '$t_items'" ) !== $t_items ) return null; + + // DEBUG: Suchbegriff und Item-Anzahl loggen + if (!function_exists('mm_debug_log')) { + function mm_debug_log($msg) { + if (defined('WP_DEBUG') && WP_DEBUG) { + error_log('[MM SHOP] ' . $msg); + } + } + } + + mm_debug_log('Suchbegriff: ' . $q_lc); + + $currency = get_option( 'wis_currency_name', 'Coins' ); + $lines = []; + $img_base = get_option( 'wis_image_base_url', '' ); + $shop_url = $bot['url_shop'] ?? ''; + // Shop-Antwort-Box (grau, Card-Layout) + // $lines[] = '
'; + + // ── Tagesaktion nur anzeigen, wenn KEINE Preisabfrage ───────────────── + $is_price_query = false; + $search = $q_lc; + if (preg_match('/\b(was kostet|preis von|wie teuer|wie viel kostet|kosten von|preis für|preis)\b/i', $search)) { + $is_price_query = true; + } + if (!$is_price_query) { + $deal = $wpdb->get_row( "SELECT * FROM $t_items WHERE is_daily_deal = 1 AND status = 'publish' LIMIT 1" ); + if ( $deal ) { + $offer = isset( $deal->offer_price ) && $deal->offer_price > 0 ? $deal->offer_price : $deal->price; + $img = $img_base . str_replace(':', '_', $deal->item_id) . '.png'; + $item_link = $shop_url ? esc_url($shop_url) . '#item-' . urlencode($deal->item_id) : ''; + $lines[] = '
' + . '🔥 Tagesaktion:
' + . '' . esc_attr($deal->name) . '' + . '
' . esc_html( $deal->name ) . '
' + . '' + . '' . esc_html( (string) $offer ) . ' ' . esc_html( $currency ) . '' + . ( $deal->price != $offer ? ' ' . esc_html( (string) $deal->price ) . '' : '' ) + . '' + . ( $item_link ? '
Zum Item im Shop' : '' ) + . '
'; + } + } + + // ── Suchbegriff extrahieren (auch für Preisabfragen) ──────────────── + // (is_price_query wurde oben schon gesetzt) + $search = $q_lc; + if ($is_price_query) { + // Nur das Item extrahieren + $search = preg_replace('/\b(shop|kaufen|kosten|preis|items?|angebot|tagesaktion|daily deal|kategorie|rang|was gibt|zeig|liste|was kostet|preis von|wie teuer|wie viel kostet|kosten von|preis für)\b/i', '', $search); + } else { + $search = preg_replace('/\b(shop|kaufen|kosten|preis|items?|angebot|tagesaktion|daily deal|kategorie|rang|was gibt|zeig|liste)\b/i', '', $search); + } + $search = trim(preg_replace('/\s+/', ' ', $search)); + // Satzzeichen am Anfang/Ende entfernen + $search = preg_replace('/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/u', '', $search); + + if (strlen($search) >= 3) { + mm_debug_log('Suchbegriff nach Extraktion: ' . $search); + // Spezifische Suche (auch für Preisabfragen) + $items = $wpdb->get_results($wpdb->prepare( + "SELECT * FROM $t_items + WHERE status = 'publish' AND (item_title LIKE %s OR name LIKE %s OR item_id LIKE %s) + ORDER BY price ASC LIMIT 8", + '%' . $wpdb->esc_like($search) . '%', + '%' . $wpdb->esc_like($search) . '%', + '%' . $wpdb->esc_like($search) . '%' + )); + mm_debug_log('LIKE-Query: ' . $search . ' | Treffer: ' . count($items)); + + if (!empty($items)) { + mm_debug_log('LIKE-Query erstes Item: ' . (isset($items[0]) ? json_encode($items[0]) : 'n/a')); + // Preisabfrage: Nur das erste Item als Preisantwort + if ($is_price_query) { + $item = $items[0]; + $img = $img_base . str_replace(':', '_', $item->item_id) . '.png'; + $item_link = $shop_url ? esc_url($shop_url) . '#item-' . urlencode($item->item_id) : ''; + $item_display_name = $item->name ?: ($item->item_title ?: $item->item_id); + // Serverliste aufbereiten + $server_list = ''; + if (!empty($item->servers)) { + $servers = json_decode($item->servers, true); + if (is_array($servers) && count($servers) > 0) { + $server_list = '
Verfügbar auf: ' . esc_html(implode(', ', $servers)) . '
'; + } + } + $lines[] = '
' + . '
🛒 Shop-Item:
' + . '' . esc_attr($item_display_name) . '' + . '
' . esc_html($item_display_name) . '
' + . '
' . esc_html((string)$item->price) . ' ' . esc_html($currency) . '
' + . $server_list + . '
'; + } else { + $lines[] = '🛒 Gefundene Items:'; + foreach ($items as $item) { + $img = $img_base . str_replace(':', '_', $item->item_id) . '.png'; + $item_link = $shop_url ? esc_url($shop_url) . '#item-' . urlencode($item->item_id) : ''; + $lines[] = '' . esc_attr($item->item_title) . '' + . '' . esc_html($item->item_title) . '' + . ' – ' . esc_html((string)$item->price) . ' ' . esc_html($currency) + . ($item_link ? ' [Shop]' : ''); + } + } + } else { + // Exakte Suche, falls LIKE nichts findet + $item = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM $t_items WHERE status = 'publish' AND (name = %s OR item_id = %s OR item_title = %s) LIMIT 1", + $search, $search, $search + )); + mm_debug_log('Exakte Suche: ' . ($item ? json_encode($item) : 'kein Treffer')); + // Case-insensitive Fallback, falls nötig + if (!$item && !empty($search)) { + $item = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM $t_items WHERE status = 'publish' AND (name COLLATE utf8_general_ci = %s COLLATE utf8_general_ci OR item_id COLLATE utf8_general_ci = %s COLLATE utf8_general_ci OR item_title COLLATE utf8_general_ci = %s COLLATE utf8_general_ci) LIMIT 1", + $search, $search, $search + )); + mm_debug_log('Case-insensitive Suche: ' . ($item ? json_encode($item) : 'kein Treffer')); + } + // Fallback: Suchbegriff als Minecraft-ID mit Prefix testen + if (!$item && !empty($search) && strpos($search, 'minecraft:') === false) { + $mc_id = 'minecraft:' . strtolower(str_replace([' ', '_'], ['_', '_'], $search)); + $item = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM $t_items WHERE status = 'publish' AND (item_id = %s OR item_id COLLATE utf8_general_ci = %s COLLATE utf8_general_ci) LIMIT 1", + $mc_id, $mc_id + )); + mm_debug_log('MC-ID Fallback: ' . $mc_id . ' | ' . ($item ? json_encode($item) : 'kein Treffer')); + } + if ($item) { + mm_debug_log('Gefundenes Item (exakt/fallback): ' . json_encode($item)); + $img = $img_base . str_replace(':', '_', $item->item_id) . '.png'; + $item_link = $shop_url ? esc_url($shop_url) . '#item-' . urlencode($item->item_id) : ''; + $lines[] = '
' + . '' . esc_attr($item->name) . '' + . '
' . esc_html($item->name) . '
' + . 'Preis: ' . esc_html((string)$item->price) . ' ' . esc_html($currency) . '' + . ($item_link ? ' [Shop]' : '') + . '
'; + } else { + // Aggressive Fuzzy-Suche als letzter Versuch + $all_items = $wpdb->get_results("SELECT * FROM $t_items WHERE status = 'publish'"); + mm_debug_log('Fuzzy-Suche über alle Items (' . count($all_items) . '): ' . $search); + $search_lc = mb_strtolower($search); + $fuzzy = null; + foreach ($all_items as $it) { + if (strpos(mb_strtolower($it->name), $search_lc) !== false + || strpos(mb_strtolower($it->item_title), $search_lc) !== false + || strpos(mb_strtolower($it->item_id), $search_lc) !== false) { + $fuzzy = $it; + break; + } + } + mm_debug_log('Fuzzy-Suche Treffer: ' . ($fuzzy ? json_encode($fuzzy) : 'kein Treffer')); + if ($fuzzy) { + $img = $img_base . str_replace(':', '_', $fuzzy->item_id) . '.png'; + $item_link = $shop_url ? esc_url($shop_url) . '#item-' . urlencode($fuzzy->item_id) : ''; + $item_display_name = $fuzzy->name ?: ($fuzzy->item_title ?: $fuzzy->item_id); + // Serverliste aufbereiten + $server_list = ''; + if (!empty($fuzzy->servers)) { + $servers = json_decode($fuzzy->servers, true); + if (is_array($servers) && count($servers) > 0) { + $server_list = '
Verfügbar auf: ' . esc_html(implode(', ', $servers)) . '
'; + } + } + $lines[] = '
' + . '
🛒 Shop-Item:
' + . '' . esc_attr($item_display_name) . '' + . '
' . esc_html($item_display_name) . '
' + . '
' . esc_html((string)$fuzzy->price) . ' ' . esc_html($currency) . '
' + . $server_list + . '
'; + } else { + $lines[] = 'Kein Item mit „' . esc_html($search) . '“ gefunden.'; + } + } + } + } else { + // Übersicht: Kategorien + Item-Anzahl + $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $t_items WHERE status = 'publish'" ); + $lines[] = '🛒 Shop-Übersicht: ' . $total . ' Items verfügbar.'; + + $cats = $wpdb->get_results( + "SELECT c.name, COUNT(i.id) as cnt + FROM $t_cats c + LEFT JOIN $t_items i ON i.category_id = c.id AND i.status = 'publish' + GROUP BY c.id ORDER BY cnt DESC LIMIT 8" + ); + if ( ! empty( $cats ) ) { + $lines[] = '📂 Kategorien:'; + foreach ( $cats as $cat ) { + $lines[] = ' • ' . esc_html( $cat->name ) . ' (' . intval( $cat->cnt ) . ' Items)'; + } + } + + // Günstigste & teuerste Items + $cheapest = $wpdb->get_row( "SELECT item_title, price FROM $t_items WHERE status = 'publish' AND price > 0 ORDER BY price ASC LIMIT 1" ); + $priciest = $wpdb->get_row( "SELECT item_title, price FROM $t_items WHERE status = 'publish' ORDER BY price DESC LIMIT 1" ); + if ( $cheapest ) $lines[] = '💚 Günstigstes: ' . esc_html( $cheapest->item_title ) . ' – ' . esc_html( (string) $cheapest->price ) . ' ' . esc_html( $currency ); + if ( $priciest ) $lines[] = '💎 Teuerstes: ' . esc_html( $priciest->item_title ) . ' – ' . esc_html( (string) $priciest->price ) . ' ' . esc_html( $currency ); + } + + if ( $shop_url ) { + $lines[] = ""; + } + + return [ + 'source' => 'shop', + 'title' => '🛒 Shop', + 'content' => implode( '
', $lines ), + ]; +} + +// ============================================================ +// INTENT: TICKET (WP Multi Ticket Pro) +// ============================================================ + +function mm_intent_ticket( $q, $bot ) { + global $wpdb; + $table = $wpdb->prefix . 'wmt_tickets'; + $table_msg = $wpdb->prefix . 'wmt_messages'; + $exists = $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) === $table; + + // ── Ticket per Nummer nachschlagen ──────────────────────── + if ( preg_match( '/#?(\d{3,6})\b/', $q, $m ) && $exists ) { + $ticket_id = intval( $m[1] ); + $ticket = $wpdb->get_row( $wpdb->prepare( + "SELECT id, subject, status, category, department, created_at, guest_name, guest_email + FROM $table WHERE id = %d", + $ticket_id + ) ); + + if ( $ticket ) { + // Letzte Nachricht + $last_msg = ''; + if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_msg'" ) === $table_msg ) { + $msg = $wpdb->get_row( $wpdb->prepare( + "SELECT message, created_at FROM $table_msg WHERE ticket_id = %d ORDER BY created_at DESC LIMIT 1", + $ticket_id + ) ); + if ( $msg ) { + $last_msg = '
💬 Letzte Nachricht: ' + . esc_html( wp_trim_words( $msg->message, 20, '…' ) ) + . ' (' . wp_date( 'd.m.Y H:i', strtotime( $msg->created_at ) ) . ')'; + } + } + + $lines = [ + '🎫 Ticket #' . $ticket->id . '', + '📋 Betreff: ' . esc_html( $ticket->subject ) . '', + '📌 Status: ' . esc_html( $ticket->status ) . '', + '🏷️ Kategorie: ' . esc_html( $ticket->category ?: '–' ), + ]; + if ( ! empty( $ticket->department ) ) { + $lines[] = '🏢 Abteilung: ' . esc_html( $ticket->department ); + } + if ( ! empty( $ticket->guest_name ) ) { + $lines[] = '👤 Von: ' . esc_html( $ticket->guest_name ); + } + $lines[] = '📅 Erstellt: ' . wp_date( 'd.m.Y \u\m H:i', strtotime( $ticket->created_at ) ); + if ( $last_msg ) $lines[] = $last_msg; + + return [ + 'source' => 'ticket', + 'title' => '🎫 Ticket #' . $ticket->id, + 'content' => implode( '
', $lines ), + ]; + } else { + return [ + 'source' => 'ticket', + 'title' => '🎫 Ticket nicht gefunden', + 'content' => 'Ticket #' . $ticket_id . ' existiert nicht.', + ]; + } + } + + // ── Allgemeine Ticket-Übersicht ──────────────────────────── + $lines = []; + + if ( $exists ) { + $open = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $table WHERE status NOT LIKE %s", '%Geschlossen%' ) ); + $closed = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $table WHERE status LIKE %s", '%Geschlossen%' ) ); + $total = $open + $closed; + + $lines[] = '📊 Gesamt: ' . $total . ' Tickets · Offen: ' . $open . ' · Geschlossen: ' . $closed . ''; + + // Kategorien anzeigen + $cats = $wpdb->get_results( + "SELECT COALESCE(category, 'Keine Kategorie') as cat, COUNT(*) as cnt + FROM $table GROUP BY category ORDER BY cnt DESC LIMIT 5" + ); + if ( ! empty( $cats ) ) { + $cat_parts = []; + foreach ( $cats as $c ) { + $cat_parts[] = esc_html( $c->cat ) . ' (' . intval( $c->cnt ) . ')'; + } + $lines[] = '📂 Kategorien: ' . implode( ' · ', $cat_parts ); + } + } + + $ticket_url = $bot['url_tickets'] ?? ''; + if ( $ticket_url ) { + $lines[] = "→ Neues Ticket erstellen"; + $lines[] = 'Oder gib eine Ticket-Nummer ein, z.B.: #1234'; + } else { + $lines[] = 'Bitte nutze das Support-Formular auf unserer Website.'; + } + + return [ + 'source' => 'ticket', + 'title' => '🎫 Support-Ticket', + 'content' => implode( '
', $lines ), + ]; +} + +// ============================================================ +// INTENT: FORUM (WP Business Forum) +// ============================================================ + +function mm_intent_forum( $q ) { + // Nur aktiv wenn WBF installiert UND URL im Backend gesetzt + $bot = get_option( 'mm_bot_data', [] ); + if ( ! class_exists( 'WBF_DB' ) ) return null; + if ( empty( $bot['url_forum'] ) ) return null; + + global $wpdb; + + $forum_url = esc_url( $bot['url_forum'] ); + $q_lc = mb_strtolower( $q ); + + // Tabellen prüfen — fehlen sie, früh abbrechen statt hängen + $t_tbl = $wpdb->prefix . 'forum_threads'; + $p_tbl = $wpdb->prefix . 'forum_posts'; + $u_tbl = $wpdb->prefix . 'forum_users'; + $c_tbl = $wpdb->prefix . 'forum_categories'; + + $tables_ok = ( + $wpdb->get_var( "SHOW TABLES LIKE '$t_tbl'" ) === $t_tbl && + $wpdb->get_var( "SHOW TABLES LIKE '$u_tbl'" ) === $u_tbl && + $wpdb->get_var( "SHOW TABLES LIKE '$c_tbl'" ) === $c_tbl + ); + + // ── Sub-Intent: Registrierung ───────────────────────────── + if ( preg_match( '/\b(registrier(en|ung)?|konto erstellen|account erstellen|sign.?up|einschreiben)\b/i', $q ) ) { + $lines = []; + if ( $tables_ok ) { + try { + $reg_mode = get_option( 'wbf_settings', [] )['registration_mode'] ?? 'open'; + $member_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $u_tbl WHERE role != 'banned'" ); + + $lines[] = '📝 Registrierung:'; + if ( $reg_mode === 'disabled' ) { + $lines[] = '❌ Die Registrierung ist derzeit deaktiviert.'; + } elseif ( $reg_mode === 'invite' ) { + $lines[] = '🔒 Das Forum nutzt Einladungscodes. Du benötigst einen gültigen Code.'; + } else { + $lines[] = '✅ Die Registrierung ist offen — jederzeit möglich.'; + } + if ( $member_count > 0 ) { + $lines[] = '👥 Bereits ' . $member_count . ' Mitglieder dabei.'; + } + } catch ( \Throwable $e ) { + $lines[] = '📝 Registrierung verfügbar.'; + } + } else { + $lines[] = '📝 Registrierung:'; + $lines[] = 'Erstelle deinen Account direkt im Forum.'; + } + $lines[] = "→ Zum Forum & Registrierung"; + return [ + 'source' => 'forum', + 'title' => '💬 Forum-Registrierung', + 'content' => implode( '
', $lines ), + ]; + } + + // ── Sub-Intent: Login / Passwort ────────────────────────── + if ( preg_match( '/\b(login|einloggen|passwort vergessen|passwort.?reset|zugangsdaten)\b/i', $q ) ) { + $lines = []; + $lines[] = '🔑 Forum-Login:'; + $lines[] = "→ Zum Forum & Login"; + if ( preg_match( '/passwort/i', $q ) ) { + $lines[] = '
🔓 Passwort vergessen?'; + $lines[] = 'Klicke auf der Login-Seite auf „Passwort vergessen".'; + } + return [ + 'source' => 'forum', + 'title' => '💬 Forum-Login', + 'content' => implode( '
', $lines ), + ]; + } + + // ── Sub-Intent: Profil ──────────────────────────────────── + if ( preg_match( '/\b(profil|mein konto|mein account|avatar|signatur|forum rang|forum level)\b/i', $q ) ) { + $lines = []; + $lines[] = '👤 Dein Forum-Profil enthält:'; + $lines[] = ' • Anzeigename, Avatar & Signatur'; + if ( class_exists( 'WBF_Levels' ) && WBF_Levels::is_enabled() ) { + $lines[] = ' • Level-System (basiert auf Beitragsanzahl)'; + } + $lines[] = ' • Rollen-Badge, Beitragsanzahl & Registrierungsdatum'; + $lines[] = "→ Zum Forum → Profil öffnen"; + return [ + 'source' => 'forum', + 'title' => '💬 Forum-Profil', + 'content' => implode( '
', $lines ), + ]; + } + + // ── Sub-Intent: Nachrichten / DM ────────────────────────── + if ( preg_match( '/\b(direktnachricht|forum nachricht|forum inbox|pn schreiben|privat.?nachricht)\b/i', $q ) ) { + $lines = []; + $lines[] = '✉️ Direktnachrichten im Forum:'; + $lines[] = 'Du kannst eingeloggten Mitgliedern direkt Nachrichten schreiben.'; + $lines[] = "→ Forum öffnen → Postfach"; + return [ + 'source' => 'forum', + 'title' => '💬 Forum-Nachrichten', + 'content' => implode( '
', $lines ), + ]; + } + + // ── Sub-Intent: Thread erstellen ────────────────────────── + if ( preg_match( '/\b(neuen thread|thread erstellen|beitrag erstellen|topic erstellen|im forum schreiben|im forum posten)\b/i', $q ) ) { + $lines = []; + $lines[] = '✏️ Neuen Thread erstellen:'; + $lines[] = '① Melde dich im Forum an.'; + $lines[] = '② Wähle die passende Kategorie.'; + $lines[] = '③ Klicke auf „Neuen Thread" und fülle Titel & Inhalt aus.'; + $lines[] = "→ Forum öffnen"; + return [ + 'source' => 'forum', + 'title' => '💬 Thread erstellen', + 'content' => implode( '
', $lines ), + ]; + } + + // ── Suchbegriff extrahieren ──────────────────────────────── + $search = preg_replace( '/\b(forum|diskussion|thread|thema|beitrag|post|kategorie|suche?)\b/i', '', $q ); + $search = trim( preg_replace( '/\s+/', ' ', $search ) ); + + // ── Thread-Suche ────────────────────────────────────────── + if ( strlen( $search ) >= 3 && $tables_ok ) { + try { + $threads = $wpdb->get_results( $wpdb->prepare( + "SELECT t.id, t.title, t.slug, t.reply_count, t.view_count, t.is_pinned, + c.name as cat_name + FROM {$t_tbl} t + LEFT JOIN {$c_tbl} c ON t.category_id = c.id + WHERE t.title LIKE %s AND t.deleted_at IS NULL + ORDER BY t.is_pinned DESC, t.created_at DESC LIMIT 5", + '%' . $wpdb->esc_like( $search ) . '%' + ) ); + + if ( ! empty( $threads ) ) { + $html = ''; + foreach ( $threads as $t ) { + $t_url = $forum_url . '?thread=' . esc_attr( $t->slug ); + $pin = $t->is_pinned ? '📌 ' : ''; + $meta = []; + if ( $t->reply_count > 0 ) $meta[] = $t->reply_count . ' Antworten'; + if ( $t->view_count > 0 ) $meta[] = $t->view_count . ' Aufrufe'; + $title_short = mb_strlen( $t->title ) > 50 ? mb_substr( $t->title, 0, 50 ) . '…' : $t->title; + $html .= '
'; + $html .= $pin . '' . esc_html( $title_short ) . ''; + if ( $t->cat_name ) $html .= ' · ' . esc_html( $t->cat_name ) . ''; + if ( ! empty( $meta ) ) $html .= '
' . implode( ' · ', $meta ) . ''; + $html .= '
'; + } + $html .= "
Alle Threads im Forum"; + return [ + 'source' => 'forum', + 'title' => '💬 Forum – ' . count( $threads ) . ' Treffer', + 'content' => $html, + ]; + } + + return [ + 'source' => 'forum', + 'title' => '💬 Forum', + 'content' => 'Kein Thread zu ' . esc_html( $search ) . ' gefunden.
' + . "→ Forum öffnen & selbst suchen", + ]; + } catch ( \Throwable $e ) { + // Bei DB-Fehler: einfach Übersicht zeigen + } + } + + // ── Allgemeine Übersicht mit Live-Statistiken ────────────── + $lines = []; + + if ( $tables_ok ) { + try { + $total_threads = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $t_tbl WHERE deleted_at IS NULL" ); + $total_posts = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $p_tbl WHERE deleted_at IS NULL" ); + $total_members = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $u_tbl WHERE role != 'banned'" ); + $online_count = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM $u_tbl WHERE last_active >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)" + ); + + $stat_parts = []; + if ( $total_threads > 0 ) $stat_parts[] = '' . $total_threads . ' Threads'; + if ( $total_posts > 0 ) $stat_parts[] = '' . $total_posts . ' Beiträge'; + if ( $total_members > 0 ) $stat_parts[] = '' . $total_members . ' Mitglieder'; + if ( ! empty( $stat_parts ) ) { + $lines[] = '📊 ' . implode( ' · ', $stat_parts ); + } + + $online_icon = $online_count > 0 ? '🟢' : '⚫'; + $lines[] = $online_icon . ' ' . ( $online_count > 0 + ? '' . $online_count . ' Mitglied' . ( $online_count > 1 ? 'er' : '' ) . ' gerade online' + : 'Aktuell niemand aktiv' ); + + // Kategorien + $cats = $wpdb->get_results( + "SELECT c.name, c.slug, COUNT(t.id) as thread_count + FROM {$c_tbl} c + LEFT JOIN {$t_tbl} t ON t.category_id = c.id AND t.deleted_at IS NULL + WHERE c.parent_id = 0 + GROUP BY c.id ORDER BY c.sort_order ASC LIMIT 6" + ); + if ( ! empty( $cats ) ) { + $lines[] = '
📂 Kategorien:'; + foreach ( $cats as $cat ) { + $c_url = $forum_url . '?cat=' . esc_attr( $cat->slug ); + $count = $cat->thread_count > 0 ? ' (' . (int) $cat->thread_count . ')' : ''; + $lines[] = ' → ' + . esc_html( $cat->name ) . '' . $count; + } + } + + // Neueste Threads + $recent = $wpdb->get_results( + "SELECT t.title, t.slug, t.reply_count + FROM {$t_tbl} t + WHERE t.deleted_at IS NULL AND t.is_pinned = 0 + ORDER BY t.created_at DESC LIMIT 3" + ); + if ( ! empty( $recent ) ) { + $lines[] = '
🆕 Neueste Threads:'; + foreach ( $recent as $r ) { + $t_url = $forum_url . '?thread=' . esc_attr( $r->slug ); + $title_short = mb_strlen( $r->title ) > 50 ? mb_substr( $r->title, 0, 50 ) . '…' : $r->title; + $replies = $r->reply_count > 0 ? ' (' . (int) $r->reply_count . ' Antw.)' : ''; + $lines[] = ' → ' + . esc_html( $title_short ) . '' . $replies; + } + } + } catch ( \Throwable $e ) { + $lines[] = '⚠️ Forum-Daten konnten nicht geladen werden.'; + } + } + + $lines[] = ( ! empty( $lines ) ? '
' : '' ) + . "→ Forum öffnen"; + + return [ + 'source' => 'forum', + 'title' => '💬 Forum', + 'content' => implode( '
', $lines ), + ]; +} + +// ============================================================ +// INTENT: GALERIE (MC MultiServer Gallery PRO) +// ============================================================ + +function mm_intent_gallery( $bot, $q_lc = '' ) { + $gallery_url = ! empty( $bot['url_gallery'] ) ? $bot['url_gallery'] : home_url( '/galerie' ); + + // ── Erkennen ob Upload-Frage ────────────────────────────── + $is_upload_query = (bool) preg_match( + '/hochlad|upload|uploaden|verifizier|verify|token|bild teilen|bilder teilen|wie.*bild|anleitung.*galerie/i', + $q_lc + ); + + $lines = []; + + // ── 1. Galerie-Link ────────────────────────────────────── + $lines[] = "📷 → Galerie öffnen"; + + // ── 2. Upload-Anleitung (bei Upload-Frage oder allgemein) ─ + if ( $is_upload_query ) { + $lines[] = '
'; + $lines[] = '📤 Bilder hochladen – so geht\'s:'; + $lines[] = '① Galerie öffnen und auf „Bilder hochladen" klicken.'; + $lines[] = '② Deinen Minecraft-Namen eingeben und den Server wählen → Session starten.'; + $lines[] = '③ Im Spiel den Befehl /verify [token] eingeben, um dich zu verifizieren.'; + $lines[] = '④ Screenshots auswählen (optional Album anlegen) → hochladen ✓'; + $lines[] = '💡 Der Verify-Token ist 5 Minuten gültig. Du musst dazu online auf dem Server sein.'; + } else { + // Kurz-Hinweis auf Upload auch bei allgemeiner Galerie-Frage + $lines[] = "🖼️ Du kannst eigene Screenshots direkt auf der Galerie-Seite hochladen."; + } + + // ── 3. Bild des Tages ──────────────────────────────────── + if ( post_type_exists( 'mc_gallery' ) ) { + $today_key = 'mc_daily_image_' . gmdate( 'Y-m-d' ); + $image_id = get_transient( $today_key ); + + if ( $image_id && wp_attachment_is_image( $image_id ) ) { + $img = wp_get_attachment_image_src( $image_id, 'medium' ); + $full = wp_get_attachment_image_src( $image_id, 'full' ); + $meta = get_post( $image_id ); + $uploader = ''; + + // Spielername aus Galerie-Post-Meta holen + if ( $meta && $meta->post_parent ) { + $player = get_post_meta( $meta->post_parent, 'mc_player', true ); + if ( $player ) { + $uploader = ' von ' . esc_html( $player ) . ''; + } + } + + if ( $img ) { + $lines[] = '
'; + $lines[] = '🌟 Bild des Tages' . $uploader . ':'; + $lines[] = "" + . "" + . ""; + } + } else { + // Kein Tagesbild gecacht → Hinweis + $lines[] = '📅 Heute noch kein Bild des Tages verfügbar.'; + } + + // Galerie-Statistik + $count = wp_count_posts( 'mc_gallery' ); + if ( $count && $count->publish > 0 ) { + $lines[] = '📁 ' . intval( $count->publish ) . ' Galerie(n) auf dem Server.'; + } + } + + return [ + 'source' => 'gallery', + 'title' => $is_upload_query ? '📤 Galerie & Upload' : '📷 Galerie', + 'content' => implode( '
', $lines ), + ]; +} + +// ============================================================ +// INTENT: FAQ (Custom Post Type 'faq') +// ============================================================ + +function mm_intent_faq( $q ) { + if ( ! post_type_exists( 'faq' ) ) return null; + + $search = preg_replace( '/\b(faq|häufig|frage|oft gefragt)\b/i', '', $q ); + $search = trim( $search ); + + $results = get_posts( [ + 'post_type' => 'faq', + 'post_status' => 'publish', + 's' => strlen( $search ) >= 3 ? $search : '', + 'posts_per_page' => 4, + 'orderby' => 'relevance', + ] ); + + if ( empty( $results ) ) return null; + + $html = '
'; + foreach ( $results as $post ) { + $title = esc_html( $post->post_title ); + $content = apply_filters( 'the_content', $post->post_content ); + $html .= '
'; + $html .= '
' . $title . '
'; + $html .= '
' . $content . '
'; + $html .= '
'; + } + $html .= '
'; + + return [ + 'source' => 'faq', + 'title' => 'FAQ', + 'content' => $html, + ]; +} + +// ============================================================ +// SPIELER-NAME EXTRAHIEREN +// ============================================================ + +function mm_extract_player_name( $q ) { + // Explizit: "von Steve", "spieler Steve", "für Steve" + if ( preg_match( '/(?:von|spieler|name|für|player|ban.*?von|bann.*?von)[:\s]+([A-Za-z0-9_]{3,16})/iu', $q, $m ) ) { + return $m[1]; + } + // "Steve ist gebannt", "ist Steve gebannt" + if ( preg_match( '/\b([A-Za-z0-9_]{3,16})\s+ist\s+(gebannt|gesperrt|online|offline)/i', $q, $m ) ) { + return $m[1]; + } + // "bin ich" → kein Name (eingeloggter User, kein Minecraft-Login hier) + if ( preg_match( '/\b(bin ich|ich bin|mein ban|mein status)\b/i', $q ) ) { + return ''; + } + // Allgemein: letztes kapitalisiertes Wort als Spielername (MC-Namen beginnen meist groß) + if ( preg_match_all( '/\b([A-Z][A-Za-z0-9_]{2,15})\b/', $q, $m ) ) { + $stop = [ 'Minecraft', 'Wiki', 'Forum', 'FAQ', 'Shop', 'Discord', 'Ban', 'Mute', + 'Kick', 'Warn', 'Spielzeit', 'Playtime', 'Galerie', 'Ticket', 'Support' ]; + foreach ( array_reverse( $m[1] ) as $candidate ) { + if ( ! in_array( $candidate, $stop, true ) ) { + return $candidate; + } + } + } + return ''; +} + +// ============================================================ +// FALLBACK +// ============================================================ + +function mm_fallback_response( $bot ) { + $hints = [ + '🖥️ Server – z.B. "Wie ist die Server-IP?"', + ]; + if ( ! empty( $bot['url_rules'] ) ) $hints[] = '📜 Regeln – z.B. "Ist PvP erlaubt?"'; + if ( ! empty( $bot['url_player_history'] ) ) $hints[] = '👤 Spieler-Info – z.B. "Spielzeit von Steve"'; + if ( ! empty( $bot['litebans_dashboard_url'] ) || ! empty( get_option( 'wp_litebans_pro_settings', [] )['db_name'] ) ) + $hints[] = '🔨 Ban-Status – z.B. "Ist Steve gebannt?"'; + if ( ! empty( $bot['url_wiki'] ) ) $hints[] = '📖 Wiki – z.B. "Wiki Befehle"'; + if ( ! empty( $bot['url_gallery'] ) ) $hints[] = '📷 Galerie – z.B. "Zeig die Galerie"'; + if ( ! empty( $bot['url_shop'] ) ) $hints[] = '🛒 Shop – z.B. "Was kostet ein Diamantschwert?"'; + if ( ! empty( $bot['url_tickets'] ) ) $hints[] = '🎫 Ticket – z.B. "Ticket erstellen"'; + + return [ + 'source' => 'fallback', + 'title' => '❓ Ich habe dazu keine Antwort', + 'content' => 'Versuche es mit einem dieser Themen:
' . implode( '
', $hints ), + ]; +} + +// ============================================================ +// RESPONSE BUILDER +// Alle Part-Arrays zu einem einzigen formatierten HTML-String +// zusammenführen. +// ============================================================ + +function mm_build_response( $parts ) { + if ( count( $parts ) === 1 ) { + $p = $parts[0]; + return [ + 'reply' => isset( $p['title'] ) + ? "{$p['title']}
{$p['content']}" + : $p['content'], + 'parts' => $parts, + ]; + } + + $blocks = []; + foreach ( $parts as $p ) { + $block = isset( $p['title'] ) ? "{$p['title']}
" : ''; + $block .= $p['content']; + $blocks[] = $block; + } + + return [ + 'reply' => implode( '
', $blocks ), + 'parts' => $parts, + ]; } \ No newline at end of file diff --git a/Minecraft-Modern-Theme/inc/assistant-widget.php b/Minecraft-Modern-Theme/inc/assistant-widget.php index 5bdc670..f461f11 100644 --- a/Minecraft-Modern-Theme/inc/assistant-widget.php +++ b/Minecraft-Modern-Theme/inc/assistant-widget.php @@ -1,913 +1,913 @@ - 'mm_bot_sanitize_settings', - ] ); -} ); - -function mm_bot_sanitize_settings( $input ) { - $clean = []; - - $text_fields = [ 'server_ip', 'server_ver', 'server_specs', 'bot_name', 'welcome' ]; - $url_fields = [ - 'url_wiki', 'url_rules', 'url_tickets', 'url_faq', - 'url_team', 'url_shop', 'url_gallery', 'url_player_history', - 'url_forum', 'link_discord', 'litebans_dashboard_url', - ]; - - foreach ( $text_fields as $f ) { - $clean[ $f ] = isset( $input[ $f ] ) ? sanitize_text_field( $input[ $f ] ) : ''; - } - foreach ( $url_fields as $f ) { - $clean[ $f ] = isset( $input[ $f ] ) ? esc_url_raw( $input[ $f ] ) : ''; - } - - $clean['qa'] = []; - if ( ! empty( $input['qa'] ) && is_array( $input['qa'] ) ) { - foreach ( $input['qa'] as $item ) { - if ( empty( $item['keys'] ) ) continue; - $clean['qa'][] = [ - 'keys' => sanitize_text_field( $item['keys'] ), - 'val' => wp_kses_post( $item['val'] ), - ]; - } - } - - return $clean; -} - -// ========================================================================= -// 2. ADMIN-SEITE -// ========================================================================= - -// BUG-FIX: Funktion außerhalb von mm_render_bot_admin() definieren. -// Innerhalb einer Funktion definierte benannte Funktionen erzeugen einen -// Fatal Error "Cannot redeclare", wenn die äußere Funktion jemals ein -// zweites Mal ausgeführt wird (z.B. durch AJAX, REST, bestimmte Plugins). -if ( ! function_exists( 'mm_find_page_by_shortcode' ) ) : -function mm_find_page_by_shortcode( $shortcodes ) { - global $wpdb; - $conditions = []; - foreach ( (array) $shortcodes as $sc ) { - $conditions[] = $wpdb->prepare( 'post_content LIKE %s', '%[' . $wpdb->esc_like( $sc ) . '%' ); - } - if ( empty( $conditions ) ) return ''; - $where = implode( ' OR ', $conditions ); - $page = $wpdb->get_row( - "SELECT ID FROM {$wpdb->posts} - WHERE post_status = 'publish' - AND post_type IN ('page','post') - AND ({$where}) - LIMIT 1" - ); - return $page ? get_permalink( $page->ID ) : ''; -} -endif; - -function mm_render_bot_admin() { - $data = get_option( 'mm_bot_data', [] ); - ?> -
-

Bot-Zentrale

- - - -
- - - -
-

1. Server-Infos

- - - - - - - - - - - - - -
- -

Wird bei Fragen nach der Server-IP angezeigt.

-
- -

Wird bei Fragen nach der Server-Hardware angezeigt.

-
-
- - -
-

2. Bot-Einstellungen

- - - - - - - - - - - - - -
- - -

UUID / Name wird im Theme-Customizer unter "Assistent" gesetzt.

-
- -

Erster Text der beim Öffnen des Assistenten angezeigt wird.

-
-
- - -
-

3. Wichtige Links

-

- 💡 Wenn eine Seite mit dem passenden Shortcode gefunden wird, erscheint ein „Vorschlag übernehmen"-Button. -

- - mm_find_page_by_shortcode( ['wmw_wiki', 'wmw_search', 'wmw_article'] ), - 'url_rules' => mm_find_page_by_shortcode( ['mrp_rules', 'mc_rules', 'multi_rules'] ), - 'url_tickets' => mm_find_page_by_shortcode( ['wmtp_tickets', 'wm_tickets', 'multi_ticket'] ), - 'url_shop' => mm_find_page_by_shortcode( ['wis_shop', 'ingame_shop', 'wis_items'] ), - 'url_gallery' => mm_find_page_by_shortcode( ['mc_gallery', 'mc_gallery_overview', 'mc_gallery_upload', 'mc_gallery_all_albums'] ), - 'url_faq' => mm_find_page_by_shortcode( ['faq_list', 'faq', 'faq_page'] ), - 'url_player_history' => mm_find_page_by_shortcode( ['mc_player_history', 'mc_players', 'player_history'] ), - 'url_forum' => mm_find_page_by_shortcode( ['business_forum'] ), - 'litebans_dashboard_url' => mm_find_page_by_shortcode( ['litebans_dashboard', 'litebans', 'wp_litebans'] ), - ]; - - $links = [ - 'url_wiki' => [ 'label' => '📖 Wiki-URL', 'plugin' => 'WP Multi Wiki', 'active' => post_type_exists( 'wmw_article' ) ], - 'url_rules' => [ 'label' => '📜 Regelwerk-URL', 'plugin' => 'Multi Rules', 'active' => function_exists( 'mrp_get_plugin_version' ) ], - 'url_tickets' => [ 'label' => '🎫 Ticket/Support-URL', 'plugin' => 'WP Multi Ticket Pro', 'active' => class_exists( 'WP_Multi_Ticket_Pro' ) ], - 'url_shop' => [ 'label' => '🛒 Shop-URL', 'plugin' => 'WP Ingame Shop Pro', 'active' => class_exists( 'WIS_Activator' ) ], - 'url_gallery' => [ 'label' => '📷 Galerie-URL', 'plugin' => 'MC MultiServer Gallery PRO', 'active' => post_type_exists( 'mc_gallery' ) ], - 'url_faq' => [ 'label' => '❓ FAQ-URL', 'plugin' => 'FAQ Post Type', 'active' => post_type_exists( 'faq' ) ], - 'url_player_history' => [ 'label' => '👤 Spieler-History-URL', 'plugin' => 'MC Player History', 'active' => function_exists( 'mcph_get_plugin_version' ) ], - 'url_forum' => [ 'label' => '💬 Forum-URL', 'plugin' => 'WP Business Forum', 'active' => class_exists( 'WBF_DB' ) ], - 'url_team' => [ 'label' => '👥 Team-URL', 'plugin' => '', 'active' => true ], - 'link_discord' => [ 'label' => '💬 Discord-Einladung', 'plugin' => '', 'active' => true ], - 'litebans_dashboard_url' => [ 'label' => '🔨 LiteBans Dashboard-URL', 'plugin' => 'LiteBans Manager', 'active' => class_exists( 'WP_LiteBans_Pro' ) ], - ]; - ?> - - - - - - - - - $cfg ) : - $badge = ''; - if ( $cfg['plugin'] ) { - $cls = $cfg['active'] ? 'badge' : 'badge inactive'; - $txt = $cfg['active'] ? 'aktiv' : 'inaktiv'; - $badge = '' . esc_html( $cfg['plugin'] ) . ' ' . $txt . ''; - } - $saved = $data[ $key ] ?? ''; - $suggested = $autodetect[ $key ] ?? ''; - $show_suggest = $suggested && $suggested !== $saved; - ?> - - - - - -
-
- - - - - - - - ✔ Erkannt & gesetzt - -
-
-
- - -
-

4. Individuelle Q&A

-

- Schlüsselwörter (kommagetrennt) → Antwort. Hat höchste Priorität vor allen Plugin-Abfragen. -

- - - - - - -
SchlüsselwörterAntwort (HTML erlaubt)
-
- $item ) : - if ( empty( $item['keys'] ) ) continue; - ?> -
- - - -
- -
- -
- - -
-
- - - '🖥️ Server-Status', 'q' => 'server status' ]; - } - - // Regeln – nur wenn URL im Backend gesetzt - if ( ! empty( $data['url_rules'] ) ) { - $quick[] = [ 'label' => '📜 Regelwerk', 'q' => 'regeln' ]; - } - - // Wiki – nur wenn URL im Backend gesetzt - if ( ! empty( $data['url_wiki'] ) ) { - $quick[] = [ 'label' => '📖 Wiki', 'q' => 'wiki' ]; - } - - // Shop – nur wenn URL im Backend gesetzt - global $wpdb; - if ( ! empty( $data['url_shop'] ) ) { - $quick[] = [ 'label' => '🛒 Shop', 'q' => 'shop' ]; - } - - // Ticket / Support – nur wenn URL im Backend gesetzt - if ( ! empty( $data['url_tickets'] ) ) { - $quick[] = [ 'label' => '🎫 Support-Ticket', 'q' => 'ticket erstellen' ]; - } - - // Forum - if ( class_exists( 'WBF_DB' ) && ! empty( $data['url_forum'] ) ) { - $quick[] = [ 'label' => '💬 Forum', 'q' => 'forum' ]; - } - - // Galerie – nur wenn URL im Backend gesetzt - if ( ! empty( $data['url_gallery'] ) ) { - $quick[] = [ 'label' => '📷 Galerie', 'q' => 'galerie' ]; - } - - // Ban-Status - $lb = get_option( 'wp_litebans_pro_settings', [] ); - if ( ! empty( $lb['db_name'] ) ) { - $quick[] = [ 'label' => '🔨 Strafen prüfen', 'q' => 'meine strafen' ]; - } - - // Spieler-History – nur wenn URL im Backend gesetzt - if ( ! empty( $data['url_player_history'] ) ) { - $quick[] = [ 'label' => '⏱️ Spielzeit', 'q' => 'spielzeit' ]; - } - - // Discord - if ( ! empty( $data['link_discord'] ) ) { - $quick[] = [ 'label' => '💬 Discord', 'q' => 'discord' ]; - } - ?> - - -
- - - - - - -
- - - - - 'mm_bot_sanitize_settings', + ] ); +} ); + +function mm_bot_sanitize_settings( $input ) { + $clean = []; + + $text_fields = [ 'server_ip', 'server_ver', 'server_specs', 'bot_name', 'welcome' ]; + $url_fields = [ + 'url_wiki', 'url_rules', 'url_tickets', 'url_faq', + 'url_team', 'url_shop', 'url_gallery', 'url_player_history', + 'url_forum', 'link_discord', 'litebans_dashboard_url', + ]; + + foreach ( $text_fields as $f ) { + $clean[ $f ] = isset( $input[ $f ] ) ? sanitize_text_field( $input[ $f ] ) : ''; + } + foreach ( $url_fields as $f ) { + $clean[ $f ] = isset( $input[ $f ] ) ? esc_url_raw( $input[ $f ] ) : ''; + } + + $clean['qa'] = []; + if ( ! empty( $input['qa'] ) && is_array( $input['qa'] ) ) { + foreach ( $input['qa'] as $item ) { + if ( empty( $item['keys'] ) ) continue; + $clean['qa'][] = [ + 'keys' => sanitize_text_field( $item['keys'] ), + 'val' => wp_kses_post( $item['val'] ), + ]; + } + } + + return $clean; +} + +// ========================================================================= +// 2. ADMIN-SEITE +// ========================================================================= + +// BUG-FIX: Funktion außerhalb von mm_render_bot_admin() definieren. +// Innerhalb einer Funktion definierte benannte Funktionen erzeugen einen +// Fatal Error "Cannot redeclare", wenn die äußere Funktion jemals ein +// zweites Mal ausgeführt wird (z.B. durch AJAX, REST, bestimmte Plugins). +if ( ! function_exists( 'mm_find_page_by_shortcode' ) ) : +function mm_find_page_by_shortcode( $shortcodes ) { + global $wpdb; + $conditions = []; + foreach ( (array) $shortcodes as $sc ) { + $conditions[] = $wpdb->prepare( 'post_content LIKE %s', '%[' . $wpdb->esc_like( $sc ) . '%' ); + } + if ( empty( $conditions ) ) return ''; + $where = implode( ' OR ', $conditions ); + $page = $wpdb->get_row( + "SELECT ID FROM {$wpdb->posts} + WHERE post_status = 'publish' + AND post_type IN ('page','post') + AND ({$where}) + LIMIT 1" + ); + return $page ? get_permalink( $page->ID ) : ''; +} +endif; + +function mm_render_bot_admin() { + $data = get_option( 'mm_bot_data', [] ); + ?> +
+

Bot-Zentrale

+ + + +
+ + + +
+

1. Server-Infos

+ + + + + + + + + + + + + +
+ +

Wird bei Fragen nach der Server-IP angezeigt.

+
+ +

Wird bei Fragen nach der Server-Hardware angezeigt.

+
+
+ + +
+

2. Bot-Einstellungen

+ + + + + + + + + + + + + +
+ + +

UUID / Name wird im Theme-Customizer unter "Assistent" gesetzt.

+
+ +

Erster Text der beim Öffnen des Assistenten angezeigt wird.

+
+
+ + +
+

3. Wichtige Links

+

+ 💡 Wenn eine Seite mit dem passenden Shortcode gefunden wird, erscheint ein „Vorschlag übernehmen"-Button. +

+ + mm_find_page_by_shortcode( ['wmw_wiki', 'wmw_search', 'wmw_article'] ), + 'url_rules' => mm_find_page_by_shortcode( ['mrp_rules', 'mc_rules', 'multi_rules'] ), + 'url_tickets' => mm_find_page_by_shortcode( ['wmtp_tickets', 'wm_tickets', 'multi_ticket'] ), + 'url_shop' => mm_find_page_by_shortcode( ['wis_shop', 'ingame_shop', 'wis_items'] ), + 'url_gallery' => mm_find_page_by_shortcode( ['mc_gallery', 'mc_gallery_overview', 'mc_gallery_upload', 'mc_gallery_all_albums'] ), + 'url_faq' => mm_find_page_by_shortcode( ['faq_list', 'faq', 'faq_page'] ), + 'url_player_history' => mm_find_page_by_shortcode( ['mc_player_history', 'mc_players', 'player_history'] ), + 'url_forum' => mm_find_page_by_shortcode( ['business_forum'] ), + 'litebans_dashboard_url' => mm_find_page_by_shortcode( ['litebans_dashboard', 'litebans', 'wp_litebans'] ), + ]; + + $links = [ + 'url_wiki' => [ 'label' => '📖 Wiki-URL', 'plugin' => 'WP Multi Wiki', 'active' => post_type_exists( 'wmw_article' ) ], + 'url_rules' => [ 'label' => '📜 Regelwerk-URL', 'plugin' => 'Multi Rules', 'active' => function_exists( 'mrp_get_plugin_version' ) ], + 'url_tickets' => [ 'label' => '🎫 Ticket/Support-URL', 'plugin' => 'WP Multi Ticket Pro', 'active' => class_exists( 'WP_Multi_Ticket_Pro' ) ], + 'url_shop' => [ 'label' => '🛒 Shop-URL', 'plugin' => 'WP Ingame Shop Pro', 'active' => class_exists( 'WIS_Activator' ) ], + 'url_gallery' => [ 'label' => '📷 Galerie-URL', 'plugin' => 'MC MultiServer Gallery PRO', 'active' => post_type_exists( 'mc_gallery' ) ], + 'url_faq' => [ 'label' => '❓ FAQ-URL', 'plugin' => 'FAQ Post Type', 'active' => post_type_exists( 'faq' ) ], + 'url_player_history' => [ 'label' => '👤 Spieler-History-URL', 'plugin' => 'MC Player History', 'active' => function_exists( 'mcph_get_plugin_version' ) ], + 'url_forum' => [ 'label' => '💬 Forum-URL', 'plugin' => 'WP Business Forum', 'active' => class_exists( 'WBF_DB' ) ], + 'url_team' => [ 'label' => '👥 Team-URL', 'plugin' => '', 'active' => true ], + 'link_discord' => [ 'label' => '💬 Discord-Einladung', 'plugin' => '', 'active' => true ], + 'litebans_dashboard_url' => [ 'label' => '🔨 LiteBans Dashboard-URL', 'plugin' => 'LiteBans Manager', 'active' => class_exists( 'WP_LiteBans_Pro' ) ], + ]; + ?> + + + + + + + + + $cfg ) : + $badge = ''; + if ( $cfg['plugin'] ) { + $cls = $cfg['active'] ? 'badge' : 'badge inactive'; + $txt = $cfg['active'] ? 'aktiv' : 'inaktiv'; + $badge = '' . esc_html( $cfg['plugin'] ) . ' ' . $txt . ''; + } + $saved = $data[ $key ] ?? ''; + $suggested = $autodetect[ $key ] ?? ''; + $show_suggest = $suggested && $suggested !== $saved; + ?> + + + + + +
+
+ + + + + + + + ✔ Erkannt & gesetzt + +
+
+
+ + +
+

4. Individuelle Q&A

+

+ Schlüsselwörter (kommagetrennt) → Antwort. Hat höchste Priorität vor allen Plugin-Abfragen. +

+ + + + + + +
SchlüsselwörterAntwort (HTML erlaubt)
+
+ $item ) : + if ( empty( $item['keys'] ) ) continue; + ?> +
+ + + +
+ +
+ +
+ + +
+
+ + + '🖥️ Server-Status', 'q' => 'server status' ]; + } + + // Regeln – nur wenn URL im Backend gesetzt + if ( ! empty( $data['url_rules'] ) ) { + $quick[] = [ 'label' => '📜 Regelwerk', 'q' => 'regeln' ]; + } + + // Wiki – nur wenn URL im Backend gesetzt + if ( ! empty( $data['url_wiki'] ) ) { + $quick[] = [ 'label' => '📖 Wiki', 'q' => 'wiki' ]; + } + + // Shop – nur wenn URL im Backend gesetzt + global $wpdb; + if ( ! empty( $data['url_shop'] ) ) { + $quick[] = [ 'label' => '🛒 Shop', 'q' => 'shop' ]; + } + + // Ticket / Support – nur wenn URL im Backend gesetzt + if ( ! empty( $data['url_tickets'] ) ) { + $quick[] = [ 'label' => '🎫 Support-Ticket', 'q' => 'ticket erstellen' ]; + } + + // Forum + if ( class_exists( 'WBF_DB' ) && ! empty( $data['url_forum'] ) ) { + $quick[] = [ 'label' => '💬 Forum', 'q' => 'forum' ]; + } + + // Galerie – nur wenn URL im Backend gesetzt + if ( ! empty( $data['url_gallery'] ) ) { + $quick[] = [ 'label' => '📷 Galerie', 'q' => 'galerie' ]; + } + + // Ban-Status + $lb = get_option( 'wp_litebans_pro_settings', [] ); + if ( ! empty( $lb['db_name'] ) ) { + $quick[] = [ 'label' => '🔨 Strafen prüfen', 'q' => 'meine strafen' ]; + } + + // Spieler-History – nur wenn URL im Backend gesetzt + if ( ! empty( $data['url_player_history'] ) ) { + $quick[] = [ 'label' => '⏱️ Spielzeit', 'q' => 'spielzeit' ]; + } + + // Discord + if ( ! empty( $data['link_discord'] ) ) { + $quick[] = [ 'label' => '💬 Discord', 'q' => 'discord' ]; + } + ?> + + +
+ + + + + + +
+ + + + + add_section( 'mm_bewerbung_section', array( + 'title' => __( 'Bewerbungsformular', 'minecraft-modern-theme' ), + 'priority' => 80, + ) ); + + $wp_customize->add_setting( 'mm_bewerbung_enabled', array( + 'default' => false, + 'sanitize_callback' => 'wp_validate_boolean', + ) ); + $wp_customize->add_control( 'mm_bewerbung_enabled', array( + 'label' => __( 'Bewerbungsformular aktivieren', 'minecraft-modern-theme' ), + 'description' => __( 'Schaltet die Bewerbungsseite und das Formular ein.', 'minecraft-modern-theme' ), + 'section' => 'mm_bewerbung_section', + 'type' => 'checkbox', + ) ); + + $wp_customize->add_setting( 'mm_bewerbung_title', array( + 'default' => __( 'Bewirb dich bei uns!', 'minecraft-modern-theme' ), + 'sanitize_callback' => 'sanitize_text_field', + ) ); + $wp_customize->add_control( 'mm_bewerbung_title', array( + 'label' => __( 'Titel der Bewerbungsseite', 'minecraft-modern-theme' ), + 'section' => 'mm_bewerbung_section', + 'type' => 'text', + ) ); + + $wp_customize->add_setting( 'mm_bewerbung_desc', array( + 'default' => __( 'Du möchtest Teil unseres Teams werden? Füll das Formular aus und wir melden uns bei dir.', 'minecraft-modern-theme' ), + 'sanitize_callback' => 'sanitize_textarea_field', + ) ); + $wp_customize->add_control( 'mm_bewerbung_desc', array( + 'label' => __( 'Beschreibungstext', 'minecraft-modern-theme' ), + 'section' => 'mm_bewerbung_section', + 'type' => 'textarea', + ) ); + + $wp_customize->add_setting( 'mm_bewerbung_success_msg', array( + 'default' => __( 'Deine Bewerbung wurde erfolgreich eingereicht! Wir melden uns so bald wie möglich bei dir.', 'minecraft-modern-theme' ), + 'sanitize_callback' => 'sanitize_textarea_field', + ) ); + $wp_customize->add_control( 'mm_bewerbung_success_msg', array( + 'label' => __( 'Erfolgsmeldung nach dem Absenden', 'minecraft-modern-theme' ), + 'section' => 'mm_bewerbung_section', + 'type' => 'textarea', + ) ); + + $wp_customize->add_setting( 'mm_bewerbung_min_alter', array( + 'default' => 14, + 'sanitize_callback' => 'absint', + ) ); + $wp_customize->add_control( 'mm_bewerbung_min_alter', array( + 'label' => __( 'Mindestalter', 'minecraft-modern-theme' ), + 'description' => __( 'Bewerbungen unter diesem Alter werden abgelehnt. 0 = kein Limit.', 'minecraft-modern-theme' ), + 'section' => 'mm_bewerbung_section', + 'type' => 'number', + 'input_attrs' => array( 'min' => 0, 'max' => 99 ), + ) ); + + // ── Positionen ── + $wp_customize->add_setting( 'mm_bewerbung_positionen', array( + 'default' => "Moderator +Supporter +Builder +Developer", + 'sanitize_callback' => 'sanitize_textarea_field', + ) ); + $wp_customize->add_control( 'mm_bewerbung_positionen', array( + 'label' => __( 'Bewerbungs-Positionen', 'minecraft-modern-theme' ), + 'description' => __( 'Eine Position pro Zeile. Der Bewerber wählt genau eine davon aus.', 'minecraft-modern-theme' ), + 'section' => 'mm_bewerbung_section', + 'type' => 'textarea', + ) ); +} +add_action( 'customize_register', 'mm_bewerbung_customizer' ); + +// Hilfsfunktion: Positions-Liste als Array zurückgeben +function mm_bewerbung_get_positionen() { + $raw = get_theme_mod( 'mm_bewerbung_positionen', "Moderator +Supporter +Builder +Developer" ); + $list = array_filter( array_map( 'trim', explode( " +", $raw ) ) ); + return array_values( $list ); +} + + +// --- 2. Custom Post Type: Bewerbung --- +function mm_register_bewerbung_cpt() { + if ( ! get_theme_mod( 'mm_bewerbung_enabled', false ) ) return; + + register_post_type( 'mm_bewerbung', array( + 'labels' => array( + 'name' => __( 'Bewerbungen', 'minecraft-modern-theme' ), + 'singular_name' => __( 'Bewerbung', 'minecraft-modern-theme' ), + 'all_items' => __( 'Alle Bewerbungen', 'minecraft-modern-theme' ), + 'menu_name' => __( 'Bewerbungen', 'minecraft-modern-theme' ), + ), + 'public' => false, + 'show_ui' => true, + 'show_in_menu' => true, + 'menu_icon' => 'dashicons-clipboard', + 'menu_position' => 8, + 'supports' => array( 'title' ), + 'capabilities' => array( + 'create_posts' => 'do_not_allow', // Nur über Frontend erstellbar + ), + 'map_meta_cap' => true, + ) ); +} +add_action( 'init', 'mm_register_bewerbung_cpt' ); + + +// --- 3. Admin: Meta-Box für Bewerbungs-Details --- +function mm_bewerbung_meta_box() { + add_meta_box( + 'mm_bewerbung_details', + __( 'Bewerbungs-Details', 'minecraft-modern-theme' ), + 'mm_bewerbung_meta_box_html', + 'mm_bewerbung', 'normal', 'high' + ); + add_meta_box( + 'mm_bewerbung_status_box', + __( 'Status', 'minecraft-modern-theme' ), + 'mm_bewerbung_status_box_html', + 'mm_bewerbung', 'side', 'high' + ); +} +add_action( 'add_meta_boxes', 'mm_bewerbung_meta_box' ); + +function mm_bewerbung_meta_box_html( $post ) { + $fields = array( + '_mm_bew_mc_name' => __( 'Minecraft Username', 'minecraft-modern-theme' ), + '_mm_bew_discord' => __( 'Discord Username', 'minecraft-modern-theme' ), + '_mm_bew_alter' => __( 'Alter', 'minecraft-modern-theme' ), + '_mm_bew_position' => __( 'Bewirbt sich als', 'minecraft-modern-theme' ), + '_mm_bew_warum' => __( 'Warum möchtest du mitspielen?', 'minecraft-modern-theme' ), + '_mm_bew_erfahrung' => __( 'Erfahrung / Vorstellung', 'minecraft-modern-theme' ), + '_mm_bew_datum' => __( 'Eingereicht am', 'minecraft-modern-theme' ), + '_mm_bew_ip' => __( 'IP-Adresse', 'minecraft-modern-theme' ), + ); + echo ''; + foreach ( $fields as $key => $label ) { + $val = get_post_meta( $post->ID, $key, true ); + if ( ! $val ) continue; + $is_long = in_array( $key, array( '_mm_bew_warum', '_mm_bew_erfahrung' ) ); + echo ''; + echo ''; + echo ''; + } + echo '
' . esc_html($label) . ''; + if ( $is_long ) { + echo '
' . esc_html($val) . '
'; + } else { + echo '' . esc_html($val) . ''; + } + echo '
'; +} + +function mm_bewerbung_status_box_html( $post ) { + $status = get_post_meta( $post->ID, '_mm_bew_status', true ) ?: 'neu'; + wp_nonce_field( 'mm_bew_status_save', 'mm_bew_status_nonce' ); + $options = array( + 'neu' => array( 'label' => __('Neu','minecraft-modern-theme'), 'color' => '#0073aa' ), + 'in_pruef' => array( 'label' => __('In Prüfung','minecraft-modern-theme'), 'color' => '#f0ad4e' ), + 'angenommen' => array( 'label' => __('Angenommen','minecraft-modern-theme'), 'color' => '#46b450' ), + 'abgelehnt' => array( 'label' => __('Abgelehnt','minecraft-modern-theme'), 'color' => '#dc3232' ), + ); + echo ''; + $cur = isset($options[$status]) ? $options[$status] : $options['neu']; + echo '
' . esc_html($cur['label']) . '
'; + + // Admin-Notiz + $notiz = get_post_meta( $post->ID, '_mm_bew_notiz', true ); + echo '
'; + echo ''; + echo ''; +} + +function mm_bewerbung_save_status( $post_id ) { + if ( ! isset($_POST['mm_bew_status_nonce']) || ! wp_verify_nonce($_POST['mm_bew_status_nonce'], 'mm_bew_status_save') ) return; + if ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) return; + if ( ! current_user_can('edit_post', $post_id) ) return; + if ( isset($_POST['mm_bew_status']) ) { + update_post_meta( $post_id, '_mm_bew_status', sanitize_text_field($_POST['mm_bew_status']) ); + } + if ( isset($_POST['mm_bew_notiz']) ) { + update_post_meta( $post_id, '_mm_bew_notiz', sanitize_textarea_field($_POST['mm_bew_notiz']) ); + } +} +add_action( 'save_post', 'mm_bewerbung_save_status' ); + + +// --- 4. Admin-Spalten in der Bewerbungsliste --- +function mm_bewerbung_columns( $cols ) { + return array( + 'cb' => $cols['cb'], + 'title' => __( 'Name', 'minecraft-modern-theme' ), + 'mc_name' => __( 'Minecraft', 'minecraft-modern-theme' ), + 'discord' => __( 'Discord', 'minecraft-modern-theme' ), + 'alter' => __( 'Alter', 'minecraft-modern-theme' ), + 'position' => __( 'Position', 'minecraft-modern-theme' ), + 'status' => __( 'Status', 'minecraft-modern-theme' ), + 'datum' => __( 'Eingereicht', 'minecraft-modern-theme' ), + ); +} +add_filter( 'manage_mm_bewerbung_posts_columns', 'mm_bewerbung_columns' ); + +function mm_bewerbung_column_content( $col, $post_id ) { + $status_colors = array( + 'neu' => '#0073aa', + 'in_pruef' => '#f0ad4e', + 'angenommen' => '#46b450', + 'abgelehnt' => '#dc3232', + ); + $status_labels = array( + 'neu' => __('Neu','minecraft-modern-theme'), + 'in_pruef' => __('In Prüfung','minecraft-modern-theme'), + 'angenommen' => __('Angenommen','minecraft-modern-theme'), + 'abgelehnt' => __('Abgelehnt','minecraft-modern-theme'), + ); + switch ($col) { + case 'mc_name': + $mc = get_post_meta($post_id, '_mm_bew_mc_name', true); + if ($mc) { + echo '
'; + echo ''; + echo esc_html($mc) . '
'; + } + break; + case 'discord': + echo esc_html( get_post_meta($post_id, '_mm_bew_discord', true) ); + break; + case 'alter': + echo esc_html( get_post_meta($post_id, '_mm_bew_alter', true) ); + break; + case 'position': + $pos = get_post_meta($post_id, '_mm_bew_position', true); + if ( $pos ) { + echo '' + . esc_html($pos) . ''; + } else { + echo ''; + } + break; + case 'status': + $s = get_post_meta($post_id, '_mm_bew_status', true) ?: 'neu'; + $color = isset($status_colors[$s]) ? $status_colors[$s] : '#0073aa'; + $label = isset($status_labels[$s]) ? $status_labels[$s] : $s; + echo '' . esc_html($label) . ''; + break; + case 'datum': + echo esc_html( get_post_meta($post_id, '_mm_bew_datum', true) ); + break; + } +} +add_action( 'manage_mm_bewerbung_posts_custom_column', 'mm_bewerbung_column_content', 10, 2 ); + + +// --- 5. AJAX: Formular absenden --- +add_action( 'wp_ajax_mm_submit_bewerbung', 'mm_submit_bewerbung' ); +add_action( 'wp_ajax_nopriv_mm_submit_bewerbung', 'mm_submit_bewerbung' ); + +function mm_submit_bewerbung() { + // Nonce prüfen + if ( ! isset($_POST['nonce']) || ! wp_verify_nonce($_POST['nonce'], 'mm_bewerbung_nonce') ) { + wp_send_json_error( array('msg' => __('Sicherheitsfehler. Bitte Seite neu laden.', 'minecraft-modern-theme')) ); + } + + // Felder validieren + $mc_name = sanitize_text_field( $_POST['mc_name'] ?? '' ); + $discord = sanitize_text_field( $_POST['discord'] ?? '' ); + $alter_raw = absint( $_POST['alter'] ?? 0 ); + $warum = sanitize_textarea_field( $_POST['warum'] ?? '' ); + $erfahrung = sanitize_textarea_field( $_POST['erfahrung'] ?? '' ); + + $position = sanitize_text_field( $_POST['position'] ?? '' ); + $positionen = mm_bewerbung_get_positionen(); + + $errors = array(); + if ( empty($mc_name) ) $errors[] = __('Minecraft Username ist erforderlich.', 'minecraft-modern-theme'); + if ( empty($discord) ) $errors[] = __('Discord Username ist erforderlich.', 'minecraft-modern-theme'); + if ( $alter_raw < 1 ) $errors[] = __('Bitte gib dein Alter an.', 'minecraft-modern-theme'); + if ( ! empty($positionen) && ! in_array($position, $positionen, true) ) { + $errors[] = __('Bitte wähle eine gültige Position aus.', 'minecraft-modern-theme'); + } + if ( empty($warum) ) $errors[] = __('Bitte erkläre warum du mitspielen möchtest.', 'minecraft-modern-theme'); + if ( empty($erfahrung) ) $errors[] = __('Bitte stell dich kurz vor.', 'minecraft-modern-theme'); + + // Mindestalter prüfen + $min_alter = absint( get_theme_mod('mm_bewerbung_min_alter', 14) ); + if ( $min_alter > 0 && $alter_raw < $min_alter ) { + $errors[] = sprintf( __('Du musst mindestens %d Jahre alt sein.', 'minecraft-modern-theme'), $min_alter ); + } + + // Doppelbewerbung prüfen (gleicher MC-Name in den letzten 30 Tagen) + $existing = get_posts( array( + 'post_type' => 'mm_bewerbung', + 'post_status' => 'publish', + 'meta_query' => array( + array( 'key' => '_mm_bew_mc_name', 'value' => $mc_name, 'compare' => '=' ), + ), + 'date_query' => array( + array( 'after' => '30 days ago' ), + ), + 'numberposts' => 1, + ) ); + if ( $existing ) { + $errors[] = __('Mit diesem Minecraft-Namen wurde in den letzten 30 Tagen bereits eine Bewerbung eingereicht.', 'minecraft-modern-theme'); + } + + if ( ! empty($errors) ) { + wp_send_json_error( array('msg' => implode(' ', $errors)) ); + } + + // Bewerbung als Post speichern + $post_id = wp_insert_post( array( + 'post_type' => 'mm_bewerbung', + 'post_status' => 'publish', + 'post_title' => $mc_name . ' – ' . date_i18n('d.m.Y H:i'), + ) ); + + if ( is_wp_error($post_id) ) { + wp_send_json_error( array('msg' => __('Fehler beim Speichern. Bitte versuche es erneut.', 'minecraft-modern-theme')) ); + } + + update_post_meta( $post_id, '_mm_bew_mc_name', $mc_name ); + update_post_meta( $post_id, '_mm_bew_discord', $discord ); + update_post_meta( $post_id, '_mm_bew_alter', $alter_raw ); + update_post_meta( $post_id, '_mm_bew_position', $position ); + update_post_meta( $post_id, '_mm_bew_warum', $warum ); + update_post_meta( $post_id, '_mm_bew_erfahrung', $erfahrung ); + update_post_meta( $post_id, '_mm_bew_status', 'neu' ); + update_post_meta( $post_id, '_mm_bew_datum', date_i18n('d.m.Y H:i:s') ); + update_post_meta( $post_id, '_mm_bew_ip', sanitize_text_field( $_SERVER['REMOTE_ADDR'] ?? '' ) ); + + $success_msg = get_theme_mod( 'mm_bewerbung_success_msg', __('Deine Bewerbung wurde erfolgreich eingereicht! Wir melden uns so bald wie möglich bei dir.', 'minecraft-modern-theme') ); + wp_send_json_success( array('msg' => $success_msg) ); +} + + +// --- 6. Template-Loader --- +function mm_bewerbung_template_loader( $template ) { + if ( ! get_theme_mod('mm_bewerbung_enabled', false) ) return $template; + if ( is_page('bewerbung') ) { + // BUG-FIX: locate_template() statt get_template_directory(), + // damit ein page-bewerbung.php im Child-Theme greift. + $t = locate_template( 'page-bewerbung.php' ); + if ( $t ) return $t; + } + return $template; +} +add_filter( 'template_include', 'mm_bewerbung_template_loader' ); + + +// --- 7. Seite automatisch anlegen --- +function mm_bewerbung_create_page() { + if ( ! get_theme_mod('mm_bewerbung_enabled', false) ) return; + if ( ! get_page_by_path('bewerbung') ) { + wp_insert_post( array( + 'post_title' => __('Bewerbung', 'minecraft-modern-theme'), + 'post_name' => 'bewerbung', + 'post_status' => 'publish', + 'post_type' => 'page', + 'post_author' => 1, + ) ); + } +} +add_action( 'customize_save_after', 'mm_bewerbung_create_page' ); diff --git a/Minecraft-Modern-Theme/inc/cookie-banner.php b/Minecraft-Modern-Theme/inc/cookie-banner.php new file mode 100644 index 0000000..774cc72 --- /dev/null +++ b/Minecraft-Modern-Theme/inc/cookie-banner.php @@ -0,0 +1,283 @@ + +
+ +
+

+

+
+
+ add_section( 'mm_cookie_banner_section', array( + 'title' => __( 'Cookie-Banner (DSGVO)', 'minecraft-modern-theme' ), + 'priority' => 75, + ) ); + + $wp_customize->add_setting( 'mm_cookie_enabled', array( 'default' => true, 'sanitize_callback' => 'wp_validate_boolean', 'transport' => 'postMessage' ) ); + $wp_customize->add_control( 'mm_cookie_enabled', array( 'label' => __( 'Cookie-Banner aktivieren', 'minecraft-modern-theme' ), 'section' => 'mm_cookie_banner_section', 'type' => 'checkbox' ) ); + + $wp_customize->add_setting( 'mm_cookie_style', array( 'default' => 'bar', 'sanitize_callback' => 'sanitize_text_field', 'transport' => 'postMessage' ) ); + $wp_customize->add_control( 'mm_cookie_style', array( + 'label' => __( 'Design-Variante', 'minecraft-modern-theme' ), + 'section' => 'mm_cookie_banner_section', + 'type' => 'select', + 'choices' => array( + 'bar' => __( 'Variante 1 – Schmale Bar (volle Breite)', 'minecraft-modern-theme' ), + 'split' => __( 'Variante 2 – Zweispaltig (3A)', 'minecraft-modern-theme' ), + 'slide' => __( 'Variante 3 – Slide-In von rechts (3B)', 'minecraft-modern-theme' ), + 'stepper' => __( 'Variante 4 – Kompakt-Center mit Stepper (3C)', 'minecraft-modern-theme' ), + ), + ) ); + + $wp_customize->add_setting( 'mm_cookie_text', array( 'default' => __( 'Wir nutzen Cookies und ähnliche Technologien. Einige sind essenziell, andere helfen uns diese Website zu verbessern. Du kannst deine Auswahl jederzeit anpassen.', 'minecraft-modern-theme' ), 'sanitize_callback' => 'wp_kses_post', 'transport' => 'postMessage' ) ); + $wp_customize->add_control( 'mm_cookie_text', array( 'label' => __( 'Banner-Text', 'minecraft-modern-theme' ), 'section' => 'mm_cookie_banner_section', 'type' => 'textarea' ) ); + + $wp_customize->add_setting( 'mm_cookie_privacy_url', array( 'default' => '', 'sanitize_callback' => 'esc_url_raw' ) ); + $wp_customize->add_control( 'mm_cookie_privacy_url', array( 'label' => __( 'URL Datenschutzerklärung', 'minecraft-modern-theme' ), 'description' => __( 'Leer lassen um den Link auszublenden.', 'minecraft-modern-theme' ), 'section' => 'mm_cookie_banner_section', 'type' => 'url' ) ); + + foreach ( array( + 'necessary' => array( 'label' => __( 'Beschreibung: Notwendige', 'minecraft-modern-theme' ), 'default' => __( 'Grundlegende Funktionen der Website. Können nicht deaktiviert werden.', 'minecraft-modern-theme' ) ), + 'statistics' => array( 'label' => __( 'Beschreibung: Statistik', 'minecraft-modern-theme' ), 'default' => __( 'Helfen uns zu verstehen wie Besucher mit der Website interagieren (z.B. Google Analytics).', 'minecraft-modern-theme' ) ), + 'marketing' => array( 'label' => __( 'Beschreibung: Marketing', 'minecraft-modern-theme' ), 'default' => __( 'Werden genutzt um Werbung relevanter zu gestalten (z.B. YouTube, Facebook).', 'minecraft-modern-theme' ) ), + ) as $key => $opts ) { + $wp_customize->add_setting( 'mm_cookie_desc_' . $key, array( 'default' => $opts['default'], 'sanitize_callback' => 'sanitize_textarea_field' ) ); + $wp_customize->add_control( 'mm_cookie_desc_' . $key, array( 'label' => $opts['label'], 'section' => 'mm_cookie_banner_section', 'type' => 'textarea' ) ); + } + + $wp_customize->add_setting( 'mm_cookie_ga_id', array( 'default' => '', 'sanitize_callback' => 'sanitize_text_field' ) ); + $wp_customize->add_control( 'mm_cookie_ga_id', array( 'label' => __( 'Google Analytics ID (optional)', 'minecraft-modern-theme' ), 'description' => __( 'z.B. G-XXXXXXXXXX', 'minecraft-modern-theme' ), 'section' => 'mm_cookie_banner_section', 'type' => 'text' ) ); + + $wp_customize->add_setting( 'mm_cookie_lifetime', array( 'default' => 365, 'sanitize_callback' => 'absint' ) ); + $wp_customize->add_control( 'mm_cookie_lifetime', array( 'label' => __( 'Cookie-Laufzeit (Tage)', 'minecraft-modern-theme' ), 'section' => 'mm_cookie_banner_section', 'type' => 'number', 'input_attrs' => array( 'min' => 1, 'max' => 730 ) ) ); + + if ( class_exists( 'MM_Cookie_Preview_Control' ) ) { + $wp_customize->add_setting( 'mm_cookie_preview_dummy', array( 'sanitize_callback' => 'sanitize_text_field' ) ); + $wp_customize->add_control( new MM_Cookie_Preview_Control( $wp_customize, 'mm_cookie_preview_dummy', array( 'section' => 'mm_cookie_banner_section', 'priority' => 200 ) ) ); + } +} +add_action( 'customize_register', 'mm_cookie_banner_customizer' ); + + +// --- 2. Banner HTML – ALLE 4 LAYOUTS gleichzeitig ausgeben --- +// Im Customizer wechselt JS nur die Klasse auf #mm-cookie-banner. +// CSS zeigt immer nur das passende .mmc-layout-* div. +function mm_cookie_banner_render() { + $is_preview = is_customize_preview(); + if ( ! $is_preview && ! get_theme_mod( 'mm_cookie_enabled', true ) ) return; + + $style = get_theme_mod( 'mm_cookie_style', 'bar' ); + $text = get_theme_mod( 'mm_cookie_text', __( 'Wir nutzen Cookies und ähnliche Technologien. Einige sind essenziell, andere helfen uns diese Website zu verbessern.', 'minecraft-modern-theme' ) ); + $priv_url = get_theme_mod( 'mm_cookie_privacy_url', '' ); + $lifetime = absint( get_theme_mod( 'mm_cookie_lifetime', 365 ) ); + $ga_id = get_theme_mod( 'mm_cookie_ga_id', '' ); + $desc_n = get_theme_mod( 'mm_cookie_desc_necessary', __( 'Grundlegende Funktionen der Website. Können nicht deaktiviert werden.', 'minecraft-modern-theme' ) ); + $desc_s = get_theme_mod( 'mm_cookie_desc_statistics', __( 'Helfen uns zu verstehen wie Besucher mit der Website interagieren (z.B. Google Analytics).', 'minecraft-modern-theme' ) ); + $desc_m = get_theme_mod( 'mm_cookie_desc_marketing', __( 'Werden genutzt um Werbung relevanter zu gestalten (z.B. YouTube, Facebook).', 'minecraft-modern-theme' ) ); + + $priv_link = $priv_url ? '' . __( 'Datenschutzerklärung', 'minecraft-modern-theme' ) . '' : ''; + $preview_class = $is_preview ? ' mmc-visible mmc-preview-mode' : ''; + $inline_style = $is_preview ? '' : 'display:none;'; + + // Kategorien-Block (identisch in allen Varianten mit Kategorien) + $cats = '
+
' . __('Notwendige','minecraft-modern-theme') . '' . __('Immer aktiv','minecraft-modern-theme') . '

' . esc_html($desc_n) . '

+

' . esc_html($desc_s) . '

+

' . esc_html($desc_m) . '

+
'; + + $btn_accept = ''; + $btn_select = ''; + $btn_neces = ''; + + $text_esc = wp_kses_post($text); + ?> + + + + __('Cookie-Einstellungen','minecraft-modern-theme')),$atts);return ''.esc_html($a['text']).'';} +add_shortcode('cookie_settings','mm_cookie_settings_shortcode'); + + +// --- 4. Live-Vorschau: nur Klasse + Text wechseln, HTML ist bereits vollständig --- +function mm_cookie_banner_preview_js(){ + if(!is_customize_preview())return; ?> + + direkt in wp_head - * - slider_loop Setting ergänzt (war in slider-init.js referenziert, aber nie definiert) - * - Scroll-to-Top Toggle ergänzt - * - footer_copyright Default mit aktuellem Jahr zur Laufzeit - * - Import_Export_Control Klasse aus dem Callback extrahiert - */ - -// ========================================================================= -// Import/Export Control – außerhalb des Callbacks definiert -// ========================================================================= -if ( class_exists('WP_Customize_Control') && ! class_exists('MM_Import_Export_Control') ) : -class MM_Import_Export_Control extends WP_Customize_Control { - public $type = 'mm_import_export'; - - public function render_content() { - $export_url = admin_url('admin-post.php?action=export_theme_settings'); - $nonce = wp_create_nonce('theme-import-nonce'); - ?> -
-

- - -

- -
- 📦 Was wird gesichert:
- ✓ Customizer-Einstellungen (Farben, Social Links, Menü-Design, etc.)
- ✓ Livestream API Keys (YouTube, Twitch)
- ✓ Homepage-Seite (Titel, Inhalt, Highlight-Bild)
- ✓ Navigation Menüs inkl. aller Items & Struktur
- ✓ Widget-Konfigurationen
- ✓ Team-Mitglieder (mit UUID, Avatar, Banner)
- ✓ FAQ-Einträge & Kategorien
- ✓ Custom CSS
- ✓ Announcement-Bar Einstellungen -
- - - - - - -
- - - - - -

- ⚠️ -

-
- - - add_section( 'assistant_settings', array( - 'title' => __( 'Virtueller Assistent', 'minecraft-modern-theme' ), - 'priority' => 80, - 'description' => __( 'Steuert den virtuellen Assistenten im Frontend. Avatar basiert auf Minecraft-UUID.', 'minecraft-modern-theme' ), - ) ); - - $wp_customize->add_setting( 'assistant_enabled', array( - 'default' => false, - 'sanitize_callback' => 'wp_validate_boolean', - ) ); - $wp_customize->add_control( 'assistant_enabled', array( - 'label' => __( 'Virtuellen Assistenten aktivieren', 'minecraft-modern-theme' ), - 'section' => 'assistant_settings', - 'type' => 'checkbox', - ) ); - - $wp_customize->add_setting( 'assistant_minecraft_uuid', array( - 'default' => '', - 'sanitize_callback' => 'sanitize_text_field', - ) ); - $wp_customize->add_control( 'assistant_minecraft_uuid', array( - 'label' => __( 'Minecraft UUID für Avatar', 'minecraft-modern-theme' ), - 'section' => 'assistant_settings', - 'type' => 'text', - 'description' => __( 'Gib die Minecraft-UUID für den Avatar des Assistenten ein.', 'minecraft-modern-theme' ), - ) ); - - $wp_customize->add_setting( 'assistant_avatar_view', array( - 'default' => 'head', - 'sanitize_callback' => function ( $v ) { - return in_array( $v, array( 'head', 'body', '3dhead' ), true ) ? $v : 'head'; - }, - ) ); - $wp_customize->add_control( 'assistant_avatar_view', array( - 'label' => __( 'Avatar-Ansicht', 'minecraft-modern-theme' ), - 'section' => 'assistant_settings', - 'type' => 'select', - 'choices' => array( - 'head' => __( 'Kopf', 'minecraft-modern-theme' ), - 'body' => __( 'Ganzkörper', 'minecraft-modern-theme' ), - '3dhead' => __( '3D Kopf', 'minecraft-modern-theme' ), - ), - 'description' => __( 'Wähle, wie der Minecraft-Avatar im Assistenten angezeigt wird.', 'minecraft-modern-theme' ), - ) ); - - // BUG-FIX: assistant_position war in assistant-widget.php in einem eigenen - // customize_register-Hook registriert – also BEVOR diese Section existierte. - // Jetzt korrekt hier innerhalb von minecraft_modern_customize_register(). - $wp_customize->add_setting( 'assistant_position', array( - 'default' => 'bottom_right', - 'transport' => 'refresh', - 'sanitize_callback' => function ( $v ) { - $allowed = array( 'bottom_right', 'bottom_left', 'top_right', 'top_left' ); - return in_array( $v, $allowed, true ) ? $v : 'bottom_right'; - }, - ) ); - $wp_customize->add_control( 'assistant_position', array( - 'label' => __( 'Position des Assistenten', 'minecraft-modern-theme' ), - 'section' => 'assistant_settings', - 'type' => 'select', - 'choices' => array( - 'bottom_right' => __( 'Unten rechts', 'minecraft-modern-theme' ), - 'bottom_left' => __( 'Unten links', 'minecraft-modern-theme' ), - 'top_right' => __( 'Oben rechts', 'minecraft-modern-theme' ), - 'top_left' => __( 'Oben links', 'minecraft-modern-theme' ), - ), - ) ); - - // ========================================================================= - // 1. HEADER SLIDER - // ========================================================================= - $wp_customize->add_section( 'header_slider', array( - 'title' => __('Header Slider', 'minecraft-modern-theme'), - 'priority' => 20, - 'description' => __('Konfiguriere den großen Slider auf der Startseite.', 'minecraft-modern-theme'), - ) ); - - $wp_customize->add_setting( 'slider_enabled', array( 'default' => false, 'transport' => 'refresh', 'sanitize_callback' => 'wp_validate_boolean' ) ); - $wp_customize->add_control( 'slider_enabled', array( 'label' => __('Header Slider aktivieren', 'minecraft-modern-theme'), 'section' => 'header_slider', 'type' => 'checkbox' ) ); - - // FIX: slider_loop war in slider-init.js als sliderSettings.loop referenziert, aber nie definiert - $wp_customize->add_setting( 'slider_loop', array( 'default' => true, 'transport' => 'refresh', 'sanitize_callback' => 'wp_validate_boolean' ) ); - $wp_customize->add_control( 'slider_loop', array( - 'label' => __('Slider Endlos-Loop aktivieren', 'minecraft-modern-theme'), - 'description' => __('Der Slider springt nach dem letzten Slide wieder zum ersten zurück.', 'minecraft-modern-theme'), - 'section' => 'header_slider', - 'type' => 'checkbox', - ) ); - - for ( $i = 1; $i <= 5; $i++ ) { - $wp_customize->add_setting( 'slider_image_' . $i, array( 'sanitize_callback' => 'esc_url_raw' ) ); - $wp_customize->add_control( new WP_Customize_Image_Control( $wp_customize, 'slider_image_' . $i, array( - 'label' => sprintf( __('Banner %d - Bild', 'minecraft-modern-theme'), $i ), 'section' => 'header_slider', - ) ) ); - $wp_customize->add_setting( 'slider_title_' . $i, array( 'sanitize_callback' => 'sanitize_text_field' ) ); - $wp_customize->add_control( 'slider_title_' . $i, array( 'label' => sprintf( __('Banner %d - Titel', 'minecraft-modern-theme'), $i ), 'section' => 'header_slider', 'type' => 'text' ) ); - $wp_customize->add_setting( 'slider_subtitle_' . $i, array( 'sanitize_callback' => 'sanitize_text_field' ) ); - $wp_customize->add_control( 'slider_subtitle_' . $i, array( 'label' => sprintf( __('Banner %d - Untertitel', 'minecraft-modern-theme'), $i ), 'section' => 'header_slider', 'type' => 'text' ) ); - } - - $wp_customize->add_setting( 'slider_font_family', array( 'default' => 'Raleway', 'sanitize_callback' => 'sanitize_text_field' ) ); - $wp_customize->add_control( 'slider_font_family', array( - 'label' => __('Schriftart', 'minecraft-modern-theme'), 'section' => 'header_slider', 'type' => 'select', - 'choices' => array( 'Raleway' => 'Raleway', 'Poppins' => 'Poppins', 'Montserrat' => 'Montserrat', 'Oswald' => 'Oswald', 'Roboto' => 'Roboto', 'Lato' => 'Lato' ), - ) ); - - $wp_customize->add_setting( 'slider_font_size', array( 'default' => 'mittel', 'sanitize_callback' => 'sanitize_text_field' ) ); - $wp_customize->add_control( 'slider_font_size', array( - 'label' => __('Schriftgröße', 'minecraft-modern-theme'), 'section' => 'header_slider', 'type' => 'select', - 'choices' => array( 'klein' => __('Klein', 'minecraft-modern-theme'), 'mittel' => __('Mittel', 'minecraft-modern-theme'), 'gross' => __('Groß', 'minecraft-modern-theme'), 'extra-gross' => __('Extra Groß', 'minecraft-modern-theme') ), - ) ); - - $wp_customize->add_setting( 'slider_font_color', array( 'default' => '#ffffff', 'sanitize_callback' => 'sanitize_hex_color' ) ); - $wp_customize->add_control( new WP_Customize_Color_Control( $wp_customize, 'slider_font_color', array( - 'label' => __('Schriftfarbe', 'minecraft-modern-theme'), 'section' => 'header_slider', - ) ) ); - - $wp_customize->add_setting( 'header_height', array( 'default' => 'mittel', 'sanitize_callback' => 'sanitize_text_field' ) ); - $wp_customize->add_control( 'header_height', array( - 'label' => __('Header-Höhe', 'minecraft-modern-theme'), 'section' => 'header_slider', 'type' => 'select', - 'choices' => array( 'klein' => __('Klein', 'minecraft-modern-theme'), 'mittel' => __('Mittel', 'minecraft-modern-theme'), 'gross' => __('Groß', 'minecraft-modern-theme') ), - ) ); - - $wp_customize->add_setting( 'slider_hide_arrows', array( 'default' => false, 'sanitize_callback' => 'wp_validate_boolean' ) ); - $wp_customize->add_control( 'slider_hide_arrows', array( 'label' => __('Pfeile ausblenden', 'minecraft-modern-theme'), 'section' => 'header_slider', 'type' => 'checkbox' ) ); - - $wp_customize->add_setting( 'slider_hide_pagination', array( 'default' => false, 'sanitize_callback' => 'wp_validate_boolean' ) ); - $wp_customize->add_control( 'slider_hide_pagination', array( 'label' => __('Paginierung (Punkte) ausblenden', 'minecraft-modern-theme'), 'section' => 'header_slider', 'type' => 'checkbox' ) ); - - - // ========================================================================= - // 2. HERO SECTION (Fallback) - // ========================================================================= - $wp_customize->add_section( 'hero_section', array( - 'title' => __('Startseiten-Hero (wenn Slider deaktiviert)', 'minecraft-modern-theme'), - 'priority' => 21, - ) ); - - $wp_customize->add_setting( 'hero_title', array( 'default' => __('Willkommen auf unserem Server', 'minecraft-modern-theme'), 'sanitize_callback' => 'sanitize_text_field' ) ); - $wp_customize->add_control( 'hero_title', array( 'label' => __('Haupttitel', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'text' ) ); - $wp_customize->add_setting( 'hero_subtitle', array( 'default' => __('Trete einer Community voller Abenteuer bei.', 'minecraft-modern-theme'), 'sanitize_callback' => 'sanitize_text_field' ) ); - $wp_customize->add_control( 'hero_subtitle', array( 'label' => __('Untertitel', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'text' ) ); - $wp_customize->add_setting( 'hero_bg_image', array( 'sanitize_callback' => 'esc_url_raw' ) ); - $wp_customize->add_control( new WP_Customize_Image_Control( $wp_customize, 'hero_bg_image', array( 'label' => __('Hintergrundbild', 'minecraft-modern-theme'), 'section' => 'hero_section' ) ) ); - $wp_customize->add_setting( 'hero_button_1_text', array( 'default' => __('Zum Forum', 'minecraft-modern-theme'), 'sanitize_callback' => 'sanitize_text_field' ) ); - $wp_customize->add_control( 'hero_button_1_text', array( 'label' => __('Button 1 Text', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'text' ) ); - $wp_customize->add_setting( 'hero_button_1_url', array( 'default' => '#', 'sanitize_callback' => 'esc_url_raw' ) ); - $wp_customize->add_control( 'hero_button_1_url', array( 'label' => __('Button 1 URL', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'url' ) ); - $wp_customize->add_setting( 'hero_button_2_text', array( 'default' => __('Zum Teamspeak', 'minecraft-modern-theme'), 'sanitize_callback' => 'sanitize_text_field' ) ); - $wp_customize->add_control( 'hero_button_2_text', array( 'label' => __('Button 2 Text', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'text' ) ); - $wp_customize->add_setting( 'hero_button_2_url', array( 'default' => '#', 'sanitize_callback' => 'esc_url_raw' ) ); - $wp_customize->add_control( 'hero_button_2_url', array( 'label' => __('Button 2 URL', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'url' ) ); - $wp_customize->add_setting( 'show_home_title', array( 'default' => false, 'sanitize_callback' => 'wp_validate_boolean' ) ); - $wp_customize->add_control( 'show_home_title', array( 'label' => __('Seitentitel "Home" anzeigen', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'checkbox' ) ); - - - // ========================================================================= - // 3. FARBEN & DARSTELLUNG - // ========================================================================= - $wp_customize->add_section( 'theme_appearance_settings', array( - 'title' => __('Farben & Darstellung', 'minecraft-modern-theme'), 'priority' => 30, - ) ); - - $wp_customize->add_setting( 'theme_color_preset', array( 'default' => 'classic', 'sanitize_callback' => 'sanitize_key', 'transport' => 'refresh' ) ); - $wp_customize->add_control( 'theme_color_preset', array( - 'label' => __('Theme Preset (Farbschema)', 'minecraft-modern-theme'), - 'section' => 'theme_appearance_settings', - 'type' => 'select', - 'priority' => 1, - 'choices' => array( - 'classic' => __('Classic Minecraft (Diamant-Blau)', 'minecraft-modern-theme'), - 'nether' => __('Nether (Lava-Rot)', 'minecraft-modern-theme'), - 'end' => __('The End (Ender-Purpur)', 'minecraft-modern-theme'), - ), - ) ); - - $wp_customize->add_setting( 'primary_accent_color', array( 'default' => '#00d4ff', 'sanitize_callback' => 'sanitize_hex_color', 'transport' => 'refresh' ) ); - $wp_customize->add_control( new WP_Customize_Color_Control( $wp_customize, 'primary_accent_color', array( 'label' => __('Akzentfarbe', 'minecraft-modern-theme'), 'section' => 'theme_appearance_settings' ) ) ); - - $wp_customize->add_setting( 'default_theme_mode', array( 'default' => 'dark', 'sanitize_callback' => 'sanitize_key' ) ); - $wp_customize->add_control( 'default_theme_mode', array( - 'label' => __('Standard-Theme-Modus', 'minecraft-modern-theme'), - 'section' => 'theme_appearance_settings', - 'type' => 'radio', - 'choices' => array( 'dark' => __('Dark Mode', 'minecraft-modern-theme'), 'light' => __('Light Mode', 'minecraft-modern-theme') ), - ) ); - - // FIX: Scroll-to-Top war nie im Customizer steuerbar - $wp_customize->add_setting( 'show_scroll_to_top', array( 'default' => true, 'sanitize_callback' => 'wp_validate_boolean' ) ); - $wp_customize->add_control( 'show_scroll_to_top', array( - 'label' => __('Scroll-to-Top Button anzeigen', 'minecraft-modern-theme'), - 'description' => __('Zeigt einen Button unten rechts zum Hochscrollen an.', 'minecraft-modern-theme'), - 'section' => 'theme_appearance_settings', - 'type' => 'checkbox', - ) ); - - - // ========================================================================= - // 4. SIDEBAR - // ========================================================================= - $wp_customize->add_section( 'sidebar_settings', array( 'title' => __('Sidebar Einstellungen', 'minecraft-modern-theme'), 'priority' => 35 ) ); - $wp_customize->add_setting( 'homepage_sidebar_enabled', array( 'default' => false, 'sanitize_callback' => 'wp_validate_boolean' ) ); - $wp_customize->add_control( 'homepage_sidebar_enabled', array( 'label' => __('Sidebar auf Startseite aktivieren', 'minecraft-modern-theme'), 'section' => 'sidebar_settings', 'type' => 'checkbox' ) ); - $wp_customize->add_setting( 'homepage_sidebar_position', array( 'default' => 'right', 'sanitize_callback' => 'sanitize_key' ) ); - $wp_customize->add_control( 'homepage_sidebar_position', array( - 'label' => __('Sidebar Position', 'minecraft-modern-theme'), 'section' => 'sidebar_settings', 'type' => 'select', - 'choices' => array( 'left' => __('Links', 'minecraft-modern-theme'), 'right' => __('Rechts', 'minecraft-modern-theme') ), - ) ); - - - // ========================================================================= - // 5. SOCIAL MEDIA - // ========================================================================= - $wp_customize->add_section( 'social_links', array( 'title' => __('Social Media Links', 'minecraft-modern-theme'), 'priority' => 40 ) ); - $social_platforms = array( - 'bluesky' => 'BlueSky', 'discord' => 'Discord', 'youtube' => 'YouTube', 'twitter' => 'Twitter (X)', - 'facebook' => 'Facebook', 'instagram' => 'Instagram', 'tiktok' => 'TikTok', - 'twitch' => 'Twitch', 'steam' => 'Steam', 'github' => 'GitHub', - 'linkedin' => 'LinkedIn', 'pinterest' => 'Pinterest', 'reddit' => 'Reddit', - 'mastodon' => 'Mastodon', 'threads' => 'Threads', 'kickstarter' => 'Kickstarter', - 'teamspeak' => 'Teamspeak', 'spotify' => 'Spotify', 'stoat' => 'Stoat', - ); - foreach ( $social_platforms as $key => $label ) { - $wp_customize->add_setting( 'social_' . $key, array( 'sanitize_callback' => 'esc_url_raw' ) ); - $wp_customize->add_control( 'social_' . $key, array( 'label' => $label . ' URL', 'section' => 'social_links', 'type' => 'url' ) ); - } - - // ========================================================================= - // 6. FOOTER - // ========================================================================= - $wp_customize->add_section( 'footer_settings', array( 'title' => __('Footer-Einstellungen', 'minecraft-modern-theme'), 'priority' => 50 ) ); - - // FIX: Default mit aktuellem Jahr zur Laufzeit auswerten, nicht bei class-load - $wp_customize->add_setting( 'footer_copyright', array( - 'default' => '', // Leer = dynamisch berechnet in footer.php - 'sanitize_callback' => 'wp_kses_post', - ) ); - $wp_customize->add_control( 'footer_copyright', array( - 'label' => __('Copyright-Text', 'minecraft-modern-theme'), - 'description' => __('Leer lassen für automatischen Text mit aktuellem Jahr.', 'minecraft-modern-theme'), - 'section' => 'footer_settings', - 'type' => 'textarea', - ) ); - - $wp_customize->add_setting( 'footer_impressum_url', array( 'sanitize_callback' => 'esc_url_raw' ) ); - $wp_customize->add_control( 'footer_impressum_url', array( 'label' => __('URL für Impressum', 'minecraft-modern-theme'), 'section' => 'footer_settings', 'type' => 'url' ) ); - $wp_customize->add_setting( 'footer_datenschutz_url', array( 'sanitize_callback' => 'esc_url_raw' ) ); - $wp_customize->add_control( 'footer_datenschutz_url', array( 'label' => __('URL für Datenschutz', 'minecraft-modern-theme'), 'section' => 'footer_settings', 'type' => 'url' ) ); - $wp_customize->add_setting( 'show_footer_credit', array( 'default' => true, 'sanitize_callback' => 'wp_validate_boolean' ) ); - $wp_customize->add_control( 'show_footer_credit', array( 'label' => __('Footer-Credit anzeigen', 'minecraft-modern-theme'), 'section' => 'footer_settings', 'type' => 'checkbox' ) ); - - - // ========================================================================= - // 7. FAQ & TEAM - // ========================================================================= - $wp_customize->add_section( 'faq_settings', array( 'title' => __('FAQ Einstellungen', 'minecraft-modern-theme'), 'priority' => 60 ) ); - $wp_customize->add_setting( 'faq_enabled', array( 'default' => true, 'sanitize_callback' => 'wp_validate_boolean' ) ); - $wp_customize->add_control( 'faq_enabled', array( 'label' => __('FAQ System aktivieren', 'minecraft-modern-theme'), 'section' => 'faq_settings', 'type' => 'checkbox' ) ); - - $wp_customize->add_section( 'team_settings', array( 'title' => __('Team Einstellungen', 'minecraft-modern-theme'), 'priority' => 65 ) ); - $wp_customize->add_setting( 'team_enabled', array( 'default' => true, 'sanitize_callback' => 'wp_validate_boolean' ) ); - $wp_customize->add_control( 'team_enabled', array( 'label' => __('Team Showcase aktivieren', 'minecraft-modern-theme'), 'section' => 'team_settings', 'type' => 'checkbox' ) ); - - - // ========================================================================= - // 8. LOGIN - // ========================================================================= - $wp_customize->add_section( 'login_settings', array( 'title' => __('Login-Einstellungen', 'minecraft-modern-theme'), 'priority' => 70 ) ); - $wp_customize->add_setting( 'login_background_image', array( 'sanitize_callback' => 'esc_url_raw', 'transport' => 'refresh' ) ); - $wp_customize->add_control( new WP_Customize_Image_Control( $wp_customize, 'login_background_image', array( 'label' => __('Login-Hintergrundbild', 'minecraft-modern-theme'), 'section' => 'login_settings' ) ) ); - $wp_customize->add_setting( 'login_logo', array( 'sanitize_callback' => 'esc_url_raw', 'transport' => 'refresh' ) ); - $wp_customize->add_control( new WP_Customize_Image_Control( $wp_customize, 'login_logo', array( 'label' => __('Login-Logo', 'minecraft-modern-theme'), 'section' => 'login_settings' ) ) ); - - for ( $i = 1; $i <= 5; $i++ ) { - $wp_customize->add_setting( 'login_avatar_uuid_' . $i, array( 'sanitize_callback' => 'sanitize_text_field', 'transport' => 'refresh' ) ); - $wp_customize->add_control( 'login_avatar_uuid_' . $i, array( - 'label' => sprintf( __('Avatar %d UUID', 'minecraft-modern-theme'), $i ), - 'section' => 'login_settings', - 'type' => 'text', - ) ); - } - - $wp_customize->add_setting( 'login_avatar_slider_speed', array( 'default' => 4, 'sanitize_callback' => 'absint', 'transport' => 'refresh' ) ); - $wp_customize->add_control( 'login_avatar_slider_speed', array( - 'label' => __('Avatar-Wechsel (Sekunden)', 'minecraft-modern-theme'), - 'section' => 'login_settings', - 'type' => 'number', - 'input_attrs' => array( 'min' => 2, 'max' => 10, 'step' => 1 ), - ) ); - - - // ========================================================================= - // 8.5. VIDEO / LIVESTREAM EINSTELLUNGEN - // ========================================================================= - $wp_customize->add_section( 'video_livestream_settings', array( - 'title' => __('Video & Livestream', 'minecraft-modern-theme'), - 'description' => __('Einstellungen für YouTube Livestream-Erkennung. Hauptmethode: Livestream-Posts unter "Livestreams" erstellen. Optional: Zusätzlichen Hauptkanal hier eintragen.', 'minecraft-modern-theme'), - 'priority' => 75, - ) ); - - $wp_customize->add_setting( 'youtube_api_key', array( - 'default' => '', - 'sanitize_callback' => 'sanitize_text_field', - 'transport' => 'refresh', - ) ); - $wp_customize->add_control( 'youtube_api_key', array( - 'label' => __('YouTube API Key', 'minecraft-modern-theme'), - 'description' => __('Erforderlich für automatische YouTube Live-Erkennung. Hier API Key erstellen', 'minecraft-modern-theme'), - 'section' => 'video_livestream_settings', - 'type' => 'text', - 'input_attrs' => array( - 'placeholder' => 'AIzaSyD...', - ), - ) ); - - $wp_customize->add_setting( 'twitch_client_id', array( - 'default' => '', - 'sanitize_callback' => 'sanitize_text_field', - 'transport' => 'refresh', - ) ); - $wp_customize->add_control( 'twitch_client_id', array( - 'label' => __('Twitch Client ID', 'minecraft-modern-theme'), - 'description' => __('Erforderlich für Twitch Live-Erkennung. Hier App erstellen', 'minecraft-modern-theme'), - 'section' => 'video_livestream_settings', - 'type' => 'text', - 'input_attrs' => array( - 'placeholder' => 'xxxxxxxxxxxxxx', - ), - ) ); - - $wp_customize->add_setting( 'twitch_client_secret', array( - 'default' => '', - 'sanitize_callback' => 'sanitize_text_field', - 'transport' => 'refresh', - ) ); - $wp_customize->add_control( 'twitch_client_secret', array( - 'label' => __('Twitch Client Secret', 'minecraft-modern-theme'), - 'description' => __('Nur für Live-Prüfung. Wird serverseitig genutzt.', 'minecraft-modern-theme'), - 'section' => 'video_livestream_settings', - 'type' => 'text', - 'input_attrs' => array( - 'placeholder' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', - ), - ) ); - - // ========================================================================= - // 9. EXPORT / IMPORT - // ========================================================================= - $wp_customize->add_section( 'theme_mods_import_export', array( - 'title' => __('Einstellungen sichern', 'minecraft-modern-theme'), - 'priority' => 999, - ) ); - $wp_customize->add_setting( 'import_export_placeholder', array( 'sanitize_callback' => 'sanitize_text_field' ) ); - $wp_customize->add_control( new MM_Import_Export_Control( $wp_customize, 'import_export_placeholder', array( - 'section' => 'theme_mods_import_export', - ) ) ); -} -add_action( 'customize_register', 'minecraft_modern_customize_register' ); - - -// ========================================================================= -// DYNAMISCHES CSS – FIX: Google Font via wp_enqueue_style(), nicht in wp_head -// ========================================================================= -function minecraft_modern_enqueue_dynamic_styles() { - $font = get_theme_mod( 'slider_font_family', 'Raleway' ); - - // Google Font sauber einbinden - $font_url = 'https://fonts.googleapis.com/css2?family=' . urlencode($font) . ':wght@400;600;700&display=swap'; - wp_enqueue_style( 'minecraft-modern-google-font', $font_url, array(), null ); - - // Dynamisches CSS als inline style - $accent_color = get_theme_mod( 'primary_accent_color', '#00d4ff' ); - $slider_color = get_theme_mod( 'slider_font_color', '#ffffff' ); - $slider_size_setting = get_theme_mod( 'slider_font_size', 'mittel' ); - $header_height_setting = get_theme_mod( 'header_height', 'mittel' ); - - $height_map = array( 'klein' => '200px', 'mittel' => '300px', 'gross' => '400px' ); - $header_height_value = isset($height_map[$header_height_setting]) ? $height_map[$header_height_setting] : '300px'; - - $font_sizes = array( - 'klein' => array( 'title' => '2.5rem', 'subtitle' => '1.2rem' ), - 'mittel' => array( 'title' => '3.5rem', 'subtitle' => '1.4rem' ), - 'gross' => array( 'title' => '4.5rem', 'subtitle' => '1.6rem' ), - 'extra-gross' => array( 'title' => '5.5rem', 'subtitle' => '1.8rem' ), - ); - $sizes = isset($font_sizes[$slider_size_setting]) ? $font_sizes[$slider_size_setting] : $font_sizes['mittel']; - - $css = " - :root { - --primary-accent: " . esc_attr($accent_color) . "; - --header-height: " . esc_attr($header_height_value) . "; - } - .slider-title, .slider-subtitle, .hero-title, .hero-subtitle, .hero-button-1, .hero-button-2 { - font-family: '" . esc_attr($font) . "', sans-serif; - color: " . esc_attr($slider_color) . "; - } - .slider-title, .hero-title { font-size: " . esc_attr($sizes['title']) . "; } - .slider-subtitle, .hero-subtitle { font-size: " . esc_attr($sizes['subtitle']) . "; } - .site-header { border-bottom: 4px solid var(--primary-accent); } - .hero-slider { border-bottom: 4px solid var(--primary-accent); } - .site-footer { border-top: 4px solid var(--primary-accent); } - "; - - wp_add_inline_style( 'minecraft-modern-style', $css ); -} -add_action( 'wp_enqueue_scripts', 'minecraft_modern_enqueue_dynamic_styles', 20 ); - - -// ========================================================================= -// PRESET LOGIC (JS im Customizer) -// ========================================================================= -function minecraft_preset_customize_js() { ?> - - direkt in wp_head + * - slider_loop Setting ergänzt (war in slider-init.js referenziert, aber nie definiert) + * - Scroll-to-Top Toggle ergänzt + * - footer_copyright Default mit aktuellem Jahr zur Laufzeit + * - Import_Export_Control Klasse aus dem Callback extrahiert + */ + +// ========================================================================= +// Import/Export Control – außerhalb des Callbacks definiert +// ========================================================================= +if ( class_exists('WP_Customize_Control') && ! class_exists('MM_Import_Export_Control') ) : +class MM_Import_Export_Control extends WP_Customize_Control { + public $type = 'mm_import_export'; + + public function render_content() { + $export_url = admin_url('admin-post.php?action=export_theme_settings'); + $nonce = wp_create_nonce('theme-import-nonce'); + ?> +
+

+ + +

+ +
+ 📦 Was wird gesichert:
+ ✓ Customizer-Einstellungen (Farben, Social Links, Menü-Design, etc.)
+ ✓ Livestream API Keys (YouTube, Twitch)
+ ✓ Homepage-Seite (Titel, Inhalt, Highlight-Bild)
+ ✓ Navigation Menüs inkl. aller Items & Struktur
+ ✓ Widget-Konfigurationen
+ ✓ Team-Mitglieder (mit UUID, Avatar, Banner)
+ ✓ FAQ-Einträge & Kategorien
+ ✓ Custom CSS
+ ✓ Announcement-Bar Einstellungen +
+ + + + + + +
+ + + + + +

+ ⚠️ +

+
+ + + add_section( 'assistant_settings', array( + 'title' => __( 'Virtueller Assistent', 'minecraft-modern-theme' ), + 'priority' => 80, + 'description' => __( 'Steuert den virtuellen Assistenten im Frontend. Avatar basiert auf Minecraft-UUID.', 'minecraft-modern-theme' ), + ) ); + + $wp_customize->add_setting( 'assistant_enabled', array( + 'default' => false, + 'sanitize_callback' => 'wp_validate_boolean', + ) ); + $wp_customize->add_control( 'assistant_enabled', array( + 'label' => __( 'Virtuellen Assistenten aktivieren', 'minecraft-modern-theme' ), + 'section' => 'assistant_settings', + 'type' => 'checkbox', + ) ); + + $wp_customize->add_setting( 'assistant_minecraft_uuid', array( + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + ) ); + $wp_customize->add_control( 'assistant_minecraft_uuid', array( + 'label' => __( 'Minecraft UUID für Avatar', 'minecraft-modern-theme' ), + 'section' => 'assistant_settings', + 'type' => 'text', + 'description' => __( 'Gib die Minecraft-UUID für den Avatar des Assistenten ein.', 'minecraft-modern-theme' ), + ) ); + + $wp_customize->add_setting( 'assistant_avatar_view', array( + 'default' => 'head', + 'sanitize_callback' => function ( $v ) { + return in_array( $v, array( 'head', 'body', '3dhead' ), true ) ? $v : 'head'; + }, + ) ); + $wp_customize->add_control( 'assistant_avatar_view', array( + 'label' => __( 'Avatar-Ansicht', 'minecraft-modern-theme' ), + 'section' => 'assistant_settings', + 'type' => 'select', + 'choices' => array( + 'head' => __( 'Kopf', 'minecraft-modern-theme' ), + 'body' => __( 'Ganzkörper', 'minecraft-modern-theme' ), + '3dhead' => __( '3D Kopf', 'minecraft-modern-theme' ), + ), + 'description' => __( 'Wähle, wie der Minecraft-Avatar im Assistenten angezeigt wird.', 'minecraft-modern-theme' ), + ) ); + + // BUG-FIX: assistant_position war in assistant-widget.php in einem eigenen + // customize_register-Hook registriert – also BEVOR diese Section existierte. + // Jetzt korrekt hier innerhalb von minecraft_modern_customize_register(). + $wp_customize->add_setting( 'assistant_position', array( + 'default' => 'bottom_right', + 'transport' => 'refresh', + 'sanitize_callback' => function ( $v ) { + $allowed = array( 'bottom_right', 'bottom_left', 'top_right', 'top_left' ); + return in_array( $v, $allowed, true ) ? $v : 'bottom_right'; + }, + ) ); + $wp_customize->add_control( 'assistant_position', array( + 'label' => __( 'Position des Assistenten', 'minecraft-modern-theme' ), + 'section' => 'assistant_settings', + 'type' => 'select', + 'choices' => array( + 'bottom_right' => __( 'Unten rechts', 'minecraft-modern-theme' ), + 'bottom_left' => __( 'Unten links', 'minecraft-modern-theme' ), + 'top_right' => __( 'Oben rechts', 'minecraft-modern-theme' ), + 'top_left' => __( 'Oben links', 'minecraft-modern-theme' ), + ), + ) ); + + // ========================================================================= + // 1. HEADER SLIDER + // ========================================================================= + $wp_customize->add_section( 'header_slider', array( + 'title' => __('Header Slider', 'minecraft-modern-theme'), + 'priority' => 20, + 'description' => __('Konfiguriere den großen Slider auf der Startseite.', 'minecraft-modern-theme'), + ) ); + + $wp_customize->add_setting( 'slider_enabled', array( 'default' => false, 'transport' => 'refresh', 'sanitize_callback' => 'wp_validate_boolean' ) ); + $wp_customize->add_control( 'slider_enabled', array( 'label' => __('Header Slider aktivieren', 'minecraft-modern-theme'), 'section' => 'header_slider', 'type' => 'checkbox' ) ); + + // FIX: slider_loop war in slider-init.js als sliderSettings.loop referenziert, aber nie definiert + $wp_customize->add_setting( 'slider_loop', array( 'default' => true, 'transport' => 'refresh', 'sanitize_callback' => 'wp_validate_boolean' ) ); + $wp_customize->add_control( 'slider_loop', array( + 'label' => __('Slider Endlos-Loop aktivieren', 'minecraft-modern-theme'), + 'description' => __('Der Slider springt nach dem letzten Slide wieder zum ersten zurück.', 'minecraft-modern-theme'), + 'section' => 'header_slider', + 'type' => 'checkbox', + ) ); + + for ( $i = 1; $i <= 5; $i++ ) { + $wp_customize->add_setting( 'slider_image_' . $i, array( 'sanitize_callback' => 'esc_url_raw' ) ); + $wp_customize->add_control( new WP_Customize_Image_Control( $wp_customize, 'slider_image_' . $i, array( + 'label' => sprintf( __('Banner %d - Bild', 'minecraft-modern-theme'), $i ), 'section' => 'header_slider', + ) ) ); + $wp_customize->add_setting( 'slider_title_' . $i, array( 'sanitize_callback' => 'sanitize_text_field' ) ); + $wp_customize->add_control( 'slider_title_' . $i, array( 'label' => sprintf( __('Banner %d - Titel', 'minecraft-modern-theme'), $i ), 'section' => 'header_slider', 'type' => 'text' ) ); + $wp_customize->add_setting( 'slider_subtitle_' . $i, array( 'sanitize_callback' => 'sanitize_text_field' ) ); + $wp_customize->add_control( 'slider_subtitle_' . $i, array( 'label' => sprintf( __('Banner %d - Untertitel', 'minecraft-modern-theme'), $i ), 'section' => 'header_slider', 'type' => 'text' ) ); + } + + $wp_customize->add_setting( 'slider_font_family', array( 'default' => 'Raleway', 'sanitize_callback' => 'sanitize_text_field' ) ); + $wp_customize->add_control( 'slider_font_family', array( + 'label' => __('Schriftart', 'minecraft-modern-theme'), 'section' => 'header_slider', 'type' => 'select', + 'choices' => array( 'Raleway' => 'Raleway', 'Poppins' => 'Poppins', 'Montserrat' => 'Montserrat', 'Oswald' => 'Oswald', 'Roboto' => 'Roboto', 'Lato' => 'Lato' ), + ) ); + + $wp_customize->add_setting( 'slider_font_size', array( 'default' => 'mittel', 'sanitize_callback' => 'sanitize_text_field' ) ); + $wp_customize->add_control( 'slider_font_size', array( + 'label' => __('Schriftgröße', 'minecraft-modern-theme'), 'section' => 'header_slider', 'type' => 'select', + 'choices' => array( 'klein' => __('Klein', 'minecraft-modern-theme'), 'mittel' => __('Mittel', 'minecraft-modern-theme'), 'gross' => __('Groß', 'minecraft-modern-theme'), 'extra-gross' => __('Extra Groß', 'minecraft-modern-theme') ), + ) ); + + $wp_customize->add_setting( 'slider_font_color', array( 'default' => '#ffffff', 'sanitize_callback' => 'sanitize_hex_color' ) ); + $wp_customize->add_control( new WP_Customize_Color_Control( $wp_customize, 'slider_font_color', array( + 'label' => __('Schriftfarbe', 'minecraft-modern-theme'), 'section' => 'header_slider', + ) ) ); + + $wp_customize->add_setting( 'header_height', array( 'default' => 'mittel', 'sanitize_callback' => 'sanitize_text_field' ) ); + $wp_customize->add_control( 'header_height', array( + 'label' => __('Header-Höhe', 'minecraft-modern-theme'), 'section' => 'header_slider', 'type' => 'select', + 'choices' => array( 'klein' => __('Klein', 'minecraft-modern-theme'), 'mittel' => __('Mittel', 'minecraft-modern-theme'), 'gross' => __('Groß', 'minecraft-modern-theme') ), + ) ); + + $wp_customize->add_setting( 'slider_hide_arrows', array( 'default' => false, 'sanitize_callback' => 'wp_validate_boolean' ) ); + $wp_customize->add_control( 'slider_hide_arrows', array( 'label' => __('Pfeile ausblenden', 'minecraft-modern-theme'), 'section' => 'header_slider', 'type' => 'checkbox' ) ); + + $wp_customize->add_setting( 'slider_hide_pagination', array( 'default' => false, 'sanitize_callback' => 'wp_validate_boolean' ) ); + $wp_customize->add_control( 'slider_hide_pagination', array( 'label' => __('Paginierung (Punkte) ausblenden', 'minecraft-modern-theme'), 'section' => 'header_slider', 'type' => 'checkbox' ) ); + + + // ========================================================================= + // 2. HERO SECTION (Fallback) + // ========================================================================= + $wp_customize->add_section( 'hero_section', array( + 'title' => __('Startseiten-Hero (wenn Slider deaktiviert)', 'minecraft-modern-theme'), + 'priority' => 21, + ) ); + + $wp_customize->add_setting( 'hero_title', array( 'default' => __('Willkommen auf unserem Server', 'minecraft-modern-theme'), 'sanitize_callback' => 'sanitize_text_field' ) ); + $wp_customize->add_control( 'hero_title', array( 'label' => __('Haupttitel', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'text' ) ); + $wp_customize->add_setting( 'hero_subtitle', array( 'default' => __('Trete einer Community voller Abenteuer bei.', 'minecraft-modern-theme'), 'sanitize_callback' => 'sanitize_text_field' ) ); + $wp_customize->add_control( 'hero_subtitle', array( 'label' => __('Untertitel', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'text' ) ); + $wp_customize->add_setting( 'hero_bg_image', array( 'sanitize_callback' => 'esc_url_raw' ) ); + $wp_customize->add_control( new WP_Customize_Image_Control( $wp_customize, 'hero_bg_image', array( 'label' => __('Hintergrundbild', 'minecraft-modern-theme'), 'section' => 'hero_section' ) ) ); + $wp_customize->add_setting( 'hero_button_1_text', array( 'default' => __('Zum Forum', 'minecraft-modern-theme'), 'sanitize_callback' => 'sanitize_text_field' ) ); + $wp_customize->add_control( 'hero_button_1_text', array( 'label' => __('Button 1 Text', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'text' ) ); + $wp_customize->add_setting( 'hero_button_1_url', array( 'default' => '#', 'sanitize_callback' => 'esc_url_raw' ) ); + $wp_customize->add_control( 'hero_button_1_url', array( 'label' => __('Button 1 URL', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'url' ) ); + $wp_customize->add_setting( 'hero_button_2_text', array( 'default' => __('Zum Teamspeak', 'minecraft-modern-theme'), 'sanitize_callback' => 'sanitize_text_field' ) ); + $wp_customize->add_control( 'hero_button_2_text', array( 'label' => __('Button 2 Text', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'text' ) ); + $wp_customize->add_setting( 'hero_button_2_url', array( 'default' => '#', 'sanitize_callback' => 'esc_url_raw' ) ); + $wp_customize->add_control( 'hero_button_2_url', array( 'label' => __('Button 2 URL', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'url' ) ); + $wp_customize->add_setting( 'show_home_title', array( 'default' => false, 'sanitize_callback' => 'wp_validate_boolean' ) ); + $wp_customize->add_control( 'show_home_title', array( 'label' => __('Seitentitel "Home" anzeigen', 'minecraft-modern-theme'), 'section' => 'hero_section', 'type' => 'checkbox' ) ); + + + // ========================================================================= + // 3. FARBEN & DARSTELLUNG + // ========================================================================= + $wp_customize->add_section( 'theme_appearance_settings', array( + 'title' => __('Farben & Darstellung', 'minecraft-modern-theme'), 'priority' => 30, + ) ); + + $wp_customize->add_setting( 'theme_color_preset', array( 'default' => 'classic', 'sanitize_callback' => 'sanitize_key', 'transport' => 'refresh' ) ); + $wp_customize->add_control( 'theme_color_preset', array( + 'label' => __('Theme Preset (Farbschema)', 'minecraft-modern-theme'), + 'section' => 'theme_appearance_settings', + 'type' => 'select', + 'priority' => 1, + 'choices' => array( + 'classic' => __('Classic Minecraft (Diamant-Blau)', 'minecraft-modern-theme'), + 'nether' => __('Nether (Lava-Rot)', 'minecraft-modern-theme'), + 'end' => __('The End (Ender-Purpur)', 'minecraft-modern-theme'), + ), + ) ); + + $wp_customize->add_setting( 'primary_accent_color', array( 'default' => '#00d4ff', 'sanitize_callback' => 'sanitize_hex_color', 'transport' => 'refresh' ) ); + $wp_customize->add_control( new WP_Customize_Color_Control( $wp_customize, 'primary_accent_color', array( 'label' => __('Akzentfarbe', 'minecraft-modern-theme'), 'section' => 'theme_appearance_settings' ) ) ); + + $wp_customize->add_setting( 'default_theme_mode', array( 'default' => 'dark', 'sanitize_callback' => 'sanitize_key' ) ); + $wp_customize->add_control( 'default_theme_mode', array( + 'label' => __('Standard-Theme-Modus', 'minecraft-modern-theme'), + 'section' => 'theme_appearance_settings', + 'type' => 'radio', + 'choices' => array( 'dark' => __('Dark Mode', 'minecraft-modern-theme'), 'light' => __('Light Mode', 'minecraft-modern-theme') ), + ) ); + + // FIX: Scroll-to-Top war nie im Customizer steuerbar + $wp_customize->add_setting( 'show_scroll_to_top', array( 'default' => true, 'sanitize_callback' => 'wp_validate_boolean' ) ); + $wp_customize->add_control( 'show_scroll_to_top', array( + 'label' => __('Scroll-to-Top Button anzeigen', 'minecraft-modern-theme'), + 'description' => __('Zeigt einen Button unten rechts zum Hochscrollen an.', 'minecraft-modern-theme'), + 'section' => 'theme_appearance_settings', + 'type' => 'checkbox', + ) ); + + + // ========================================================================= + // 4. SIDEBAR + // ========================================================================= + $wp_customize->add_section( 'sidebar_settings', array( 'title' => __('Sidebar Einstellungen', 'minecraft-modern-theme'), 'priority' => 35 ) ); + $wp_customize->add_setting( 'homepage_sidebar_enabled', array( 'default' => false, 'sanitize_callback' => 'wp_validate_boolean' ) ); + $wp_customize->add_control( 'homepage_sidebar_enabled', array( 'label' => __('Sidebar auf Startseite aktivieren', 'minecraft-modern-theme'), 'section' => 'sidebar_settings', 'type' => 'checkbox' ) ); + $wp_customize->add_setting( 'homepage_sidebar_position', array( 'default' => 'right', 'sanitize_callback' => 'sanitize_key' ) ); + $wp_customize->add_control( 'homepage_sidebar_position', array( + 'label' => __('Sidebar Position', 'minecraft-modern-theme'), 'section' => 'sidebar_settings', 'type' => 'select', + 'choices' => array( 'left' => __('Links', 'minecraft-modern-theme'), 'right' => __('Rechts', 'minecraft-modern-theme') ), + ) ); + + + // ========================================================================= + // 5. SOCIAL MEDIA + // ========================================================================= + $wp_customize->add_section( 'social_links', array( 'title' => __('Social Media Links', 'minecraft-modern-theme'), 'priority' => 40 ) ); + $social_platforms = array( + 'bluesky' => 'BlueSky', 'discord' => 'Discord', 'youtube' => 'YouTube', 'twitter' => 'Twitter (X)', + 'facebook' => 'Facebook', 'instagram' => 'Instagram', 'tiktok' => 'TikTok', + 'twitch' => 'Twitch', 'steam' => 'Steam', 'github' => 'GitHub', + 'linkedin' => 'LinkedIn', 'pinterest' => 'Pinterest', 'reddit' => 'Reddit', + 'mastodon' => 'Mastodon', 'threads' => 'Threads', 'kickstarter' => 'Kickstarter', + 'teamspeak' => 'Teamspeak', 'spotify' => 'Spotify', 'stoat' => 'Stoat', + ); + foreach ( $social_platforms as $key => $label ) { + $wp_customize->add_setting( 'social_' . $key, array( 'sanitize_callback' => 'esc_url_raw' ) ); + $wp_customize->add_control( 'social_' . $key, array( 'label' => $label . ' URL', 'section' => 'social_links', 'type' => 'url' ) ); + } + + // ========================================================================= + // 6. FOOTER + // ========================================================================= + $wp_customize->add_section( 'footer_settings', array( 'title' => __('Footer-Einstellungen', 'minecraft-modern-theme'), 'priority' => 50 ) ); + + // FIX: Default mit aktuellem Jahr zur Laufzeit auswerten, nicht bei class-load + $wp_customize->add_setting( 'footer_copyright', array( + 'default' => '', // Leer = dynamisch berechnet in footer.php + 'sanitize_callback' => 'wp_kses_post', + ) ); + $wp_customize->add_control( 'footer_copyright', array( + 'label' => __('Copyright-Text', 'minecraft-modern-theme'), + 'description' => __('Leer lassen für automatischen Text mit aktuellem Jahr.', 'minecraft-modern-theme'), + 'section' => 'footer_settings', + 'type' => 'textarea', + ) ); + + $wp_customize->add_setting( 'footer_impressum_url', array( 'sanitize_callback' => 'esc_url_raw' ) ); + $wp_customize->add_control( 'footer_impressum_url', array( 'label' => __('URL für Impressum', 'minecraft-modern-theme'), 'section' => 'footer_settings', 'type' => 'url' ) ); + $wp_customize->add_setting( 'footer_datenschutz_url', array( 'sanitize_callback' => 'esc_url_raw' ) ); + $wp_customize->add_control( 'footer_datenschutz_url', array( 'label' => __('URL für Datenschutz', 'minecraft-modern-theme'), 'section' => 'footer_settings', 'type' => 'url' ) ); + $wp_customize->add_setting( 'show_footer_credit', array( 'default' => true, 'sanitize_callback' => 'wp_validate_boolean' ) ); + $wp_customize->add_control( 'show_footer_credit', array( 'label' => __('Footer-Credit anzeigen', 'minecraft-modern-theme'), 'section' => 'footer_settings', 'type' => 'checkbox' ) ); + + + // ========================================================================= + // 7. FAQ & TEAM + // ========================================================================= + $wp_customize->add_section( 'faq_settings', array( 'title' => __('FAQ Einstellungen', 'minecraft-modern-theme'), 'priority' => 60 ) ); + $wp_customize->add_setting( 'faq_enabled', array( 'default' => true, 'sanitize_callback' => 'wp_validate_boolean' ) ); + $wp_customize->add_control( 'faq_enabled', array( 'label' => __('FAQ System aktivieren', 'minecraft-modern-theme'), 'section' => 'faq_settings', 'type' => 'checkbox' ) ); + + $wp_customize->add_section( 'team_settings', array( 'title' => __('Team Einstellungen', 'minecraft-modern-theme'), 'priority' => 65 ) ); + $wp_customize->add_setting( 'team_enabled', array( 'default' => true, 'sanitize_callback' => 'wp_validate_boolean' ) ); + $wp_customize->add_control( 'team_enabled', array( 'label' => __('Team Showcase aktivieren', 'minecraft-modern-theme'), 'section' => 'team_settings', 'type' => 'checkbox' ) ); + + + // ========================================================================= + // 8. LOGIN + // ========================================================================= + $wp_customize->add_section( 'login_settings', array( 'title' => __('Login-Einstellungen', 'minecraft-modern-theme'), 'priority' => 70 ) ); + $wp_customize->add_setting( 'login_background_image', array( 'sanitize_callback' => 'esc_url_raw', 'transport' => 'refresh' ) ); + $wp_customize->add_control( new WP_Customize_Image_Control( $wp_customize, 'login_background_image', array( 'label' => __('Login-Hintergrundbild', 'minecraft-modern-theme'), 'section' => 'login_settings' ) ) ); + $wp_customize->add_setting( 'login_logo', array( 'sanitize_callback' => 'esc_url_raw', 'transport' => 'refresh' ) ); + $wp_customize->add_control( new WP_Customize_Image_Control( $wp_customize, 'login_logo', array( 'label' => __('Login-Logo', 'minecraft-modern-theme'), 'section' => 'login_settings' ) ) ); + + for ( $i = 1; $i <= 5; $i++ ) { + $wp_customize->add_setting( 'login_avatar_uuid_' . $i, array( 'sanitize_callback' => 'sanitize_text_field', 'transport' => 'refresh' ) ); + $wp_customize->add_control( 'login_avatar_uuid_' . $i, array( + 'label' => sprintf( __('Avatar %d UUID', 'minecraft-modern-theme'), $i ), + 'section' => 'login_settings', + 'type' => 'text', + ) ); + } + + $wp_customize->add_setting( 'login_avatar_slider_speed', array( 'default' => 4, 'sanitize_callback' => 'absint', 'transport' => 'refresh' ) ); + $wp_customize->add_control( 'login_avatar_slider_speed', array( + 'label' => __('Avatar-Wechsel (Sekunden)', 'minecraft-modern-theme'), + 'section' => 'login_settings', + 'type' => 'number', + 'input_attrs' => array( 'min' => 2, 'max' => 10, 'step' => 1 ), + ) ); + + + // ========================================================================= + // 8.5. VIDEO / LIVESTREAM EINSTELLUNGEN + // ========================================================================= + $wp_customize->add_section( 'video_livestream_settings', array( + 'title' => __('Video & Livestream', 'minecraft-modern-theme'), + 'description' => __('Einstellungen für YouTube Livestream-Erkennung. Hauptmethode: Livestream-Posts unter "Livestreams" erstellen. Optional: Zusätzlichen Hauptkanal hier eintragen.', 'minecraft-modern-theme'), + 'priority' => 75, + ) ); + + $wp_customize->add_setting( 'youtube_api_key', array( + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + 'transport' => 'refresh', + ) ); + $wp_customize->add_control( 'youtube_api_key', array( + 'label' => __('YouTube API Key', 'minecraft-modern-theme'), + 'description' => __('Erforderlich für automatische YouTube Live-Erkennung. Hier API Key erstellen', 'minecraft-modern-theme'), + 'section' => 'video_livestream_settings', + 'type' => 'text', + 'input_attrs' => array( + 'placeholder' => 'AIzaSyD...', + ), + ) ); + + $wp_customize->add_setting( 'twitch_client_id', array( + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + 'transport' => 'refresh', + ) ); + $wp_customize->add_control( 'twitch_client_id', array( + 'label' => __('Twitch Client ID', 'minecraft-modern-theme'), + 'description' => __('Erforderlich für Twitch Live-Erkennung. Hier App erstellen', 'minecraft-modern-theme'), + 'section' => 'video_livestream_settings', + 'type' => 'text', + 'input_attrs' => array( + 'placeholder' => 'xxxxxxxxxxxxxx', + ), + ) ); + + $wp_customize->add_setting( 'twitch_client_secret', array( + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + 'transport' => 'refresh', + ) ); + $wp_customize->add_control( 'twitch_client_secret', array( + 'label' => __('Twitch Client Secret', 'minecraft-modern-theme'), + 'description' => __('Nur für Live-Prüfung. Wird serverseitig genutzt.', 'minecraft-modern-theme'), + 'section' => 'video_livestream_settings', + 'type' => 'text', + 'input_attrs' => array( + 'placeholder' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + ), + ) ); + + // ========================================================================= + // 9. EXPORT / IMPORT + // ========================================================================= + $wp_customize->add_section( 'theme_mods_import_export', array( + 'title' => __('Einstellungen sichern', 'minecraft-modern-theme'), + 'priority' => 999, + ) ); + $wp_customize->add_setting( 'import_export_placeholder', array( 'sanitize_callback' => 'sanitize_text_field' ) ); + $wp_customize->add_control( new MM_Import_Export_Control( $wp_customize, 'import_export_placeholder', array( + 'section' => 'theme_mods_import_export', + ) ) ); +} +add_action( 'customize_register', 'minecraft_modern_customize_register' ); + + +// ========================================================================= +// DYNAMISCHES CSS – Slider-Schrift wird lokal eingebunden (DSGVO) +// ========================================================================= +function minecraft_modern_enqueue_dynamic_styles() { + $font = get_theme_mod( 'slider_font_family', 'Raleway' ); + + // Lokale Schrift statt fonts.googleapis.com (siehe mm_enqueue_local_font()). + // Fällt die Schrift aus, greift die Basis-Schrift bzw. sans-serif aus dem CSS. + mm_enqueue_local_font( $font ); + + // Dynamisches CSS als inline style + $accent_color = get_theme_mod( 'primary_accent_color', '#00d4ff' ); + $slider_color = get_theme_mod( 'slider_font_color', '#ffffff' ); + $slider_size_setting = get_theme_mod( 'slider_font_size', 'mittel' ); + $header_height_setting = get_theme_mod( 'header_height', 'mittel' ); + + $height_map = array( 'klein' => '200px', 'mittel' => '300px', 'gross' => '400px' ); + $header_height_value = isset($height_map[$header_height_setting]) ? $height_map[$header_height_setting] : '300px'; + + $font_sizes = array( + 'klein' => array( 'title' => '2.5rem', 'subtitle' => '1.2rem' ), + 'mittel' => array( 'title' => '3.5rem', 'subtitle' => '1.4rem' ), + 'gross' => array( 'title' => '4.5rem', 'subtitle' => '1.6rem' ), + 'extra-gross' => array( 'title' => '5.5rem', 'subtitle' => '1.8rem' ), + ); + $sizes = isset($font_sizes[$slider_size_setting]) ? $font_sizes[$slider_size_setting] : $font_sizes['mittel']; + + $css = " + :root { + --primary-accent: " . esc_attr($accent_color) . "; + --header-height: " . esc_attr($header_height_value) . "; + } + .slider-title, .slider-subtitle, .hero-title, .hero-subtitle, .hero-button-1, .hero-button-2 { + font-family: '" . esc_attr($font) . "', sans-serif; + color: " . esc_attr($slider_color) . "; + } + .slider-title, .hero-title { font-size: " . esc_attr($sizes['title']) . "; } + .slider-subtitle, .hero-subtitle { font-size: " . esc_attr($sizes['subtitle']) . "; } + .site-header { border-bottom: 4px solid var(--primary-accent); } + .hero-slider { border-bottom: 4px solid var(--primary-accent); } + .site-footer { border-top: 4px solid var(--primary-accent); } + "; + + wp_add_inline_style( 'minecraft-modern-style', $css ); +} +add_action( 'wp_enqueue_scripts', 'minecraft_modern_enqueue_dynamic_styles', 20 ); + + +// ========================================================================= +// PRESET LOGIC (JS im Customizer) +// ========================================================================= +function minecraft_preset_customize_js() { ?> + + array( + 'name' => __( 'FAQs', 'minecraft-modern-theme' ), + 'singular_name' => __( 'FAQ', 'minecraft-modern-theme' ), + 'add_new' => __( 'Neue FAQ hinzufügen', 'minecraft-modern-theme' ), + 'add_new_item' => __( 'Neue FAQ hinzufügen', 'minecraft-modern-theme' ), + 'edit_item' => __( 'FAQ bearbeiten', 'minecraft-modern-theme' ), + 'new_item' => __( 'Neue FAQ', 'minecraft-modern-theme' ), + 'view_item' => __( 'FAQ ansehen', 'minecraft-modern-theme' ), + 'search_items' => __( 'FAQs durchsuchen', 'minecraft-modern-theme' ), + 'not_found' => __( 'Keine FAQs gefunden', 'minecraft-modern-theme' ), + 'not_found_in_trash' => __( 'Keine FAQs im Papierkorb gefunden', 'minecraft-modern-theme' ), + 'all_items' => __( 'Alle FAQs', 'minecraft-modern-theme' ), + ), + 'public' => true, + 'has_archive' => true, + 'menu_icon' => 'dashicons-format-chat', + 'supports' => array( 'title', 'editor', 'page-attributes' ), + 'rewrite' => array( 'slug' => 'faq' ), + 'show_in_rest' => true, + ) ); + register_taxonomy( 'faq_category', 'faq', array( + 'label' => __( 'FAQ Kategorien', 'minecraft-modern-theme' ), + 'rewrite' => array( 'slug' => 'faq-kategorie' ), + 'hierarchical' => true, + 'show_in_rest' => true, + ) ); + } +} +add_action( 'init', 'create_faq_post_type' ); + +// ============================================================================= +// FAQ-Seitenerstellung +// ============================================================================= + +function create_faq_page_automatically() { + if ( ! get_theme_mod( 'faq_enabled', true ) ) return; + + // BUG-FIX: get_page_by_title() ist seit WP 6.2 deprecated. Ersetzt durch WP_Query. + $faq_query = new WP_Query( array( + 'post_type' => 'page', + 'title' => 'FAQ', + 'post_status' => 'publish', + 'posts_per_page' => 1, + 'no_found_rows' => true, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) ); + if ( $faq_query->have_posts() ) return; + + wp_insert_post( array( + 'post_title' => 'FAQ', + 'post_content' => 'Diese Seite zeigt alle FAQs an. Der Inhalt wird automatisch generiert.', + 'post_status' => 'publish', + 'post_type' => 'page', + 'post_author' => 1, + ) ); +} +add_action( 'customize_save_after', 'create_faq_page_automatically' ); + + +function load_faq_page_template( $template ) { + if ( get_theme_mod( 'faq_enabled', true ) && is_page() ) { + global $post; + if ( $post && $post->post_title === 'FAQ' ) { + // BUG-FIX: get_template_directory() erzwang das Parent-Template und + // ignorierte ein archive-faq.php im Child-Theme. locate_template() + // bevorzugt das Child-Theme und fällt sonst aufs Parent zurück. + $t = locate_template( 'archive-faq.php' ); + if ( $t ) return $t; + } + } + return $template; +} +add_filter( 'template_include', 'load_faq_page_template' ); diff --git a/Minecraft-Modern-Theme/inc/frontpage.php b/Minecraft-Modern-Theme/inc/frontpage.php new file mode 100644 index 0000000..792b2a7 --- /dev/null +++ b/Minecraft-Modern-Theme/inc/frontpage.php @@ -0,0 +1,58 @@ + 'page', + 'title' => 'Home', + 'post_status' => 'publish', + 'posts_per_page' => 1, + 'no_found_rows' => true, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) ); + $home_page = $home_query->have_posts() ? $home_query->posts[0] : null; + if ( ! $home_page ) { + $home_page_id = wp_insert_post( array( + 'post_title' => 'Home', + 'post_content' => 'Diese Seite dient als statische Startseite.', + 'post_status' => 'publish', + 'post_type' => 'page', + 'post_author' => 1, + ) ); + } else { + $home_page_id = $home_page->ID; + } + if ( $home_page_id ) { + update_option( 'show_on_front', 'page' ); + update_option( 'page_on_front', $home_page_id ); + } + } +} +add_action( 'after_switch_theme', 'set_static_front_page_automatically' ); + + +function add_home_body_class( $classes ) { + if ( is_front_page() && ! get_theme_mod( 'show_home_title', true ) ) { + $classes[] = 'home-title-hidden'; + } + return $classes; +} +add_filter( 'body_class', 'add_home_body_class' ); diff --git a/Minecraft-Modern-Theme/inc/header-nav.php b/Minecraft-Modern-Theme/inc/header-nav.php new file mode 100644 index 0000000..c260961 --- /dev/null +++ b/Minecraft-Modern-Theme/inc/header-nav.php @@ -0,0 +1,308 @@ + +
+ + + +

+ +

+ + + +

+ +

+ + +
+ + + 'fab fa-bluesky', 'discord' => 'fab fa-discord', 'youtube' => 'fab fa-youtube', + 'twitter' => 'fab fa-x-twitter', 'facebook' => 'fab fa-facebook-f', + 'instagram' => 'fab fa-instagram', 'tiktok' => 'fab fa-tiktok', + 'twitch' => 'fab fa-twitch', 'steam' => 'fab fa-steam', + 'github' => 'fab fa-github', 'linkedin' => 'fab fa-linkedin-in', + 'pinterest' => 'fab fa-pinterest-p', 'reddit' => 'fab fa-reddit-alien', + 'mastodon' => 'fab fa-mastodon', 'threads' => 'fab fa-threads', + 'kickstarter' => 'fab fa-kickstarter', 'teamspeak' => 'fab fa-teamspeak', 'spotify' => 'fab fa-spotify', + 'stoat' => 'fab fa-diaspora', + ); ?> +
+ + +
+add_section( 'header_menu_style_section', array( + 'title' => __( 'Menü-Design', 'minecraft-modern-theme' ), + 'priority' => 30, + ) ); + $wp_customize->add_setting( 'header_menu_style', array( + 'default' => 'classic', + 'sanitize_callback' => 'sanitize_text_field', + 'transport' => 'refresh', + ) ); + $wp_customize->add_control( 'header_menu_style', array( + 'label' => __( 'Menü-Layout wählen', 'minecraft-modern-theme' ), + 'section' => 'header_menu_style_section', + 'type' => 'select', + 'choices' => array( + 'classic' => __( '① Classic – Logo links, Menü Mitte, Icons rechts', 'minecraft-modern-theme' ), + 'centered' => __( '② Zentriert – Logo oben, Menü darunter', 'minecraft-modern-theme' ), + 'sidebar' => __( '③ Sidebar – Menü als vertikale Leiste', 'minecraft-modern-theme' ), + 'mega' => __( '④ Mega-Menü – breite Dropdown-Spalten', 'minecraft-modern-theme' ), + ), + ) ); + + // Branding-Position (gilt für alle Layouts) + $wp_customize->add_setting( 'sidebar_branding_position', array( + 'default' => 'left', + 'sanitize_callback' => 'sanitize_text_field', + 'transport' => 'refresh', + ) ); + $wp_customize->add_control( 'sidebar_branding_position', array( + 'label' => __( 'Logo/Titel Position', 'minecraft-modern-theme' ), + 'description' => __( 'Gilt für alle Menü-Layouts.', 'minecraft-modern-theme' ), + 'section' => 'header_menu_style_section', + 'type' => 'select', + 'choices' => array( + 'left' => __( 'Links', 'minecraft-modern-theme' ), + 'center' => __( 'Mitte', 'minecraft-modern-theme' ), + 'right' => __( 'Rechts', 'minecraft-modern-theme' ), + ), + ) ); +} +add_action( 'customize_register', 'minecraft_modern_menu_style_customizer' ); + + +// === Sidebar-Menü JavaScript (nur wenn Sidebar-Layout aktiv) === +function minecraft_modern_sidebar_menu_script() { + if ( get_theme_mod( 'header_menu_style', 'classic' ) !== 'sidebar' ) return; + ?> + + __( 'Beitrag Sidebar', 'minecraft-modern-theme' ), + 'id' => 'single-post-sidebar', + 'description' => __( 'Widget-Bereich für die Sidebar auf Einzelbeitrags-Seiten.', 'minecraft-modern-theme' ), + 'before_widget' => '', + 'before_title' => '

', + 'after_title' => '

', + ) ); +} +add_action( 'widgets_init', 'minecraft_modern_single_sidebar' ); + + +// === Single-Post Sidebar Render-Funktion === +function minecraft_modern_render_single_sidebar() { + if ( is_active_sidebar( 'single-post-sidebar' ) ) { + dynamic_sidebar( 'single-post-sidebar' ); + } else { + ?> + + + + + + + + add_section( 'single_sidebar_section', array( + 'title' => __( 'Beitrag Sidebar', 'minecraft-modern-theme' ), + 'priority' => 55, + ) ); + $wp_customize->add_setting( 'single_sidebar_enabled', array( + 'default' => true, + 'sanitize_callback' => 'wp_validate_boolean', + ) ); + $wp_customize->add_control( 'single_sidebar_enabled', array( + 'label' => __( 'Sidebar auf Einzelbeiträgen anzeigen', 'minecraft-modern-theme' ), + 'section' => 'single_sidebar_section', + 'type' => 'checkbox', + ) ); + $wp_customize->add_setting( 'single_sidebar_position', array( + 'default' => 'right', + 'sanitize_callback' => 'sanitize_text_field', + ) ); + $wp_customize->add_control( 'single_sidebar_position', array( + 'label' => __( 'Sidebar-Position', 'minecraft-modern-theme' ), + 'section' => 'single_sidebar_section', + 'type' => 'select', + 'choices' => array( + 'right' => __( 'Rechts', 'minecraft-modern-theme' ), + 'left' => __( 'Links', 'minecraft-modern-theme' ), + ), + ) ); +} +add_action( 'customize_register', 'minecraft_modern_single_sidebar_customizer' ); diff --git a/Minecraft-Modern-Theme/inc/login-url.php b/Minecraft-Modern-Theme/inc/login-url.php new file mode 100644 index 0000000..4a2b239 --- /dev/null +++ b/Minecraft-Modern-Theme/inc/login-url.php @@ -0,0 +1,251 @@ + wp-login.php ausliefern + if ( $path === $slug ) { + global $pagenow, $error, $interim_login, $action, $user_login; + $pagenow = 'wp-login.php'; + require_once ABSPATH . 'wp-login.php'; + exit; + } + + // 2) Alte Login-Adresse -> Startseite + if ( $path === 'wp-login.php' ) { + wp_safe_redirect( home_url( '/' ), 302 ); + exit; + } + + // 3) /wp-admin für Nicht-Angemeldete -> Startseite. + // admin-ajax.php und admin-post.php bleiben offen, sonst brechen + // Frontend-AJAX und der Authentik-OAuth-Rückkanal. + if ( $path === 'wp-admin' || strpos( $path, 'wp-admin/' ) === 0 ) { + $datei = basename( $path ); + $erlaubt = array( 'admin-ajax.php', 'admin-post.php' ); + + if ( ! in_array( $datei, $erlaubt, true ) && ! is_user_logged_in() ) { + wp_safe_redirect( home_url( '/' ), 302 ); + exit; + } + } +} +add_action( 'init', 'mm_login_slug_handle_request', 1 ); + + +// ========================================================================= +// CUSTOMIZER: Login-URL einstellen +// ========================================================================= + +function mm_login_slug_sanitize( $value ) { + $value = sanitize_title( (string) $value ); + return in_array( $value, mm_login_slug_reserviert(), true ) ? '' : $value; +} + +/** + * Prüft die Eingabe und meldet Kollisionen direkt im Customizer zurück, + * statt sie stillschweigend zu verwerfen. + */ +function mm_login_slug_validate( $validity, $value ) { + $value = sanitize_title( (string) $value ); + + if ( $value === '' ) { + return $validity; // leer ist erlaubt (= Funktion greift nicht) + } + if ( in_array( $value, mm_login_slug_reserviert(), true ) ) { + $validity->add( 'reserviert', __( 'Dieser Pfad ist von WordPress belegt. Bitte einen anderen wählen.', 'minecraft-modern-theme' ) ); + return $validity; + } + // Kollision mit einer bestehenden Seite/Beitrag? + $vorhanden = get_page_by_path( $value, OBJECT, array( 'page', 'post' ) ); + if ( $vorhanden ) { + $validity->add( 'belegt', sprintf( + /* translators: %s: Titel des kollidierenden Inhalts */ + __( 'Der Pfad wird bereits von "%s" verwendet - diese Seite wäre dann nicht mehr erreichbar.', 'minecraft-modern-theme' ), + $vorhanden->post_title + ) ); + } + return $validity; +} +add_filter( 'customize_validate_mm_login_slug', 'mm_login_slug_validate', 10, 2 ); + +function mm_login_slug_customizer( $wp_customize ) { + $notfall = "define( 'MM_DISABLE_LOGIN_SLUG', true );"; + + $wp_customize->add_section( 'mm_login_url_section', array( + 'title' => __( 'Login-URL', 'minecraft-modern-theme' ), + 'priority' => 85, + 'description' => __( 'Ersetzt die Standard-Adresse /wp-login.php durch einen eigenen Pfad und reduziert so automatisierte Anmeldeversuche.', 'minecraft-modern-theme' ) + . '

' . esc_html__( 'Notfall:', 'minecraft-modern-theme' ) . ' ' + . esc_html__( 'Diese Zeile in die wp-config.php eintragen, dann ist /wp-login.php sofort wieder erreichbar:', 'minecraft-modern-theme' ) + . '
' . esc_html( $notfall ) . '', + ) ); + + $wp_customize->add_setting( 'mm_login_slug', array( + 'default' => '', + 'sanitize_callback' => 'mm_login_slug_sanitize', + 'transport' => 'refresh', + ) ); + $wp_customize->add_control( 'mm_login_slug', array( + 'label' => __( 'Eigener Login-Pfad', 'minecraft-modern-theme' ), + 'description' => sprintf( + /* translators: %s: Beispiel-URL */ + __( 'Nur der Pfad, ohne Schrägstriche. Ergibt: %s', 'minecraft-modern-theme' ), + '' . esc_html( home_url( '/' ) ) . 'DEIN-PFAD/' + ), + 'section' => 'mm_login_url_section', + 'type' => 'text', + ) ); + + $wp_customize->add_setting( 'mm_login_slug_enabled', array( + 'default' => false, + 'sanitize_callback' => 'wp_validate_boolean', + 'transport' => 'refresh', + ) ); + $wp_customize->add_control( 'mm_login_slug_enabled', array( + 'label' => __( 'Eigene Login-URL aktivieren', 'minecraft-modern-theme' ), + 'description' => __( 'Erst aktivieren, wenn oben ein Pfad eingetragen und gespeichert ist. Notiere dir die neue Adresse, bevor du dich abmeldest!', 'minecraft-modern-theme' ), + 'section' => 'mm_login_url_section', + 'type' => 'checkbox', + ) ); +} +add_action( 'customize_register', 'mm_login_slug_customizer' ); + + +/** + * Hinweis im Adminbereich mit der aktuell gültigen Login-Adresse. + */ +function mm_login_slug_admin_notice() { + $slug = mm_login_slug_get(); + if ( ! $slug || ! current_user_can( 'manage_options' ) ) { + return; + } + $screen = get_current_screen(); + if ( ! $screen || ! in_array( $screen->id, array( 'dashboard', 'themes' ), true ) ) { + return; + } + printf( + '

%s %s

', + esc_html__( 'Die Anmeldung erfolgt auf dieser Seite über:', 'minecraft-modern-theme' ), + esc_html( home_url( '/' . $slug . '/' ) ) + ); +} +add_action( 'admin_notices', 'mm_login_slug_admin_notice' ); diff --git a/Minecraft-Modern-Theme/inc/login.php b/Minecraft-Modern-Theme/inc/login.php new file mode 100644 index 0000000..92ae133 --- /dev/null +++ b/Minecraft-Modern-Theme/inc/login.php @@ -0,0 +1,104 @@ +
'); + }); + " ); + + $slider_speed = get_theme_mod( 'login_avatar_slider_speed', 4 ); + wp_localize_script( 'minecraft-avatar-slider-script', 'avatarSliderSettings', array( + 'speed' => $slider_speed * 1000, + ) ); + + $login_bg_image = get_theme_mod( 'login_background_image' ); + if ( $login_bg_image ) { + wp_add_inline_style( 'minecraft-modern-login-style', "body.login { background-image: url('" . esc_url( $login_bg_image ) . "') !important; }" ); + } + $logo_url = get_theme_mod( 'login_logo' ); + if ( $logo_url ) { + wp_add_inline_style( 'minecraft-modern-login-style', ".login h1 a { background-image: url('" . esc_url( $logo_url ) . "') !important; }" ); + } +} +add_action( 'login_enqueue_scripts', 'minecraft_modern_login_assets' ); + + +function add_minecraft_avatar_slider_to_login() { + $avatar_html = '
'; + $has_avatars = false; + + for ( $i = 1; $i <= 5; $i++ ) { + $uuid = get_theme_mod( 'login_avatar_uuid_' . $i ); + if ( ! empty( $uuid ) ) { + $has_avatars = true; + $avatar_url = 'https://visage.surgeplay.com/full/' . $uuid . '.png'; + // FIX: Klasse wird hier gesetzt, JS überschreibt sie trotzdem (kompatibel) + $active_class = ( $i === 1 ) ? 'avatar-slide avatar-slide-active' : 'avatar-slide'; + $avatar_html .= 'Minecraft Avatar ' . $i . ''; + } + } + $avatar_html .= '
'; + + if ( $has_avatars ) { + echo $avatar_html; + } +} +add_action( 'login_form', 'add_minecraft_avatar_slider_to_login' ); + + +function add_post_login_links() { + ?> + + array( 'label' => __( 'Ganz oben (unter Dashboard)', 'minecraft-modern-theme' ), 'pos' => 3 ), + 'inhalte' => array( 'label' => __( 'Nach den Inhalten', 'minecraft-modern-theme' ), 'pos' => 26 ), + 'werkzeug' => array( 'label' => __( 'Bei den Werkzeugen', 'minecraft-modern-theme' ), 'pos' => 76 ), + 'unten' => array( 'label' => __( 'Ganz unten', 'minecraft-modern-theme' ), 'pos' => 101 ), + ); +} + +/** Gespeicherte Links. */ +function mm_menu_links_get() { + $links = get_option( 'mm_menu_links', array() ); + return is_array( $links ) ? $links : array(); +} + +/** + * Prüft ein Linkziel und gibt es zurück - oder '' wenn es nicht zulässig ist. + * + * Erlaubt sind ausschliesslich http/https sowie relative Ziele innerhalb des + * Backends (z. B. edit.php?post_type=video). Alles andere - insbesondere + * javascript: und data: - wird verworfen, da die Adresse später unverändert + * als href in der Adminleiste landet. + */ +function mm_menu_links_url_pruefen( $url ) { + $url = (string) $url; + + // Steuerzeichen und Leerraum entfernen. Ohne das liesse sich die + // Schema-Prüfung mit "java\nscript:..." umgehen - Browser ignorieren + // solche Zeichen beim Auflösen der Adresse. + $url = preg_replace( '/[\x00-\x20\x7F]/', '', $url ); + if ( $url === '' ) { + return ''; + } + + // Protokollrelative Adressen (//example.com) nicht zulassen + if ( strpos( $url, '//' ) === 0 ) { + return ''; + } + + // Enthält das Ziel ein Schema? + if ( preg_match( '#^([a-z][a-z0-9+.\-]*):#i', $url, $t ) ) { + if ( ! in_array( strtolower( $t[1] ), array( 'http', 'https' ), true ) ) { + return ''; + } + return esc_url_raw( $url, array( 'http', 'https' ) ); + } + + // Relatives Ziel im Backend + return sanitize_text_field( $url ); +} + + +// ========================================================================= +// 1. Links in die Seitenleiste einhängen +// ========================================================================= + +/** + * Sucht ab $wunsch den nächsten freien Platz im Menü, damit vorhandene + * Einträge nicht überschrieben werden. + */ +function mm_menu_links_freier_platz( $wunsch ) { + global $menu; + + // Bewusst nur der String-Schlüssel: Bei einem Float-Schlüssel schneidet PHP + // die Nachkommastellen ab, $menu[3.1] würde also $menu[3] abfragen und + // jede Zwischenposition fälschlich als belegt melden. + $pos = (float) $wunsch; + while ( isset( $menu[ (string) $pos ] ) ) { + $pos = round( $pos + 0.1, 1 ); + } + return (string) $pos; +} + +function mm_menu_links_registrieren() { + global $menu; + + $positionen = mm_menu_links_positionen(); + + foreach ( mm_menu_links_get() as $i => $link ) { + $titel = isset( $link['titel'] ) ? $link['titel'] : ''; + + // Zweite Prüfung beim Ausgeben: Die Adresse landet unverändert als + // href in der Adminleiste, deshalb auch hier nichts Ungeprüftes durchlassen. + $url = mm_menu_links_url_pruefen( $link['url'] ?? '' ); + + if ( $titel === '' || $url === '' ) { + continue; + } + + $icon = ! empty( $link['icon'] ) ? $link['icon'] : 'dashicons-admin-links'; + $extern = ! empty( $link['neues_fenster'] ); + $schl = isset( $link['position'] ) ? $link['position'] : 'unten'; + $wunsch = isset( $positionen[ $schl ] ) ? $positionen[ $schl ]['pos'] : 101; + + $klassen = 'menu-top mm-menu-link'; + if ( $extern ) { + $klassen .= ' mm-menu-link-extern'; + } + + // Aufbau wie in wp-admin/menu.php: + // 0 Beschriftung, 1 Fähigkeit, 2 Ziel, 3 Seitentitel, 4 Klassen, 5 Id, 6 Icon + $menu[ mm_menu_links_freier_platz( $wunsch ) ] = array( + $titel, + 'read', // alle mit Backend-Zugang + $url, + $titel, + $klassen, + 'mm-menu-link-' . (int) $i, + $icon, + ); + } +} +add_action( 'admin_menu', 'mm_menu_links_registrieren', 100 ); + +/** + * Externe Links in einem neuen Tab öffnen. + * WordPress kennt kein target-Attribut für Menüpunkte, deshalb per Skript. + */ +function mm_menu_links_target() { + $hat_externe = false; + foreach ( mm_menu_links_get() as $link ) { + if ( ! empty( $link['neues_fenster'] ) ) { + $hat_externe = true; + break; + } + } + if ( ! $hat_externe ) { + return; + } + ?> + + 'array', + 'sanitize_callback' => 'mm_menu_links_sanitize', + 'default' => array(), + ) ); +} +add_action( 'admin_menu', 'mm_menu_links_admin_menu' ); + +function mm_menu_links_sanitize( $roh ) { + $sauber = array(); + if ( ! is_array( $roh ) ) { + return $sauber; + } + $erlaubte_positionen = array_keys( mm_menu_links_positionen() ); + + foreach ( $roh as $z ) { + $titel = sanitize_text_field( $z['titel'] ?? '' ); + $url = mm_menu_links_url_pruefen( $z['url'] ?? '' ); + + // Zeilen ohne Beschriftung oder ohne zulässiges Ziel verwerfen + if ( $titel === '' || $url === '' ) { + continue; + } + + $pos = sanitize_key( $z['position'] ?? 'unten' ); + + $sauber[] = array( + 'titel' => $titel, + 'url' => $url, + 'icon' => sanitize_html_class( $z['icon'] ?? '' ) ?: 'dashicons-admin-links', + 'position' => in_array( $pos, $erlaubte_positionen, true ) ? $pos : 'unten', + 'neues_fenster' => ! empty( $z['neues_fenster'] ) ? 1 : 0, + ); + } + return $sauber; +} + +/** Eine Eingabezeile. */ +function mm_menu_links_zeile( $i, $z ) { + $titel = $z['titel'] ?? ''; + $url = $z['url'] ?? ''; + $icon = $z['icon'] ?? 'dashicons-admin-links'; + $pos = $z['position'] ?? 'unten'; + $extern = ! empty( $z['neues_fenster'] ); + $n = 'mm_menu_links[' . $i . ']'; + ?> +
+ + + + + + + + + + + + + +
+ + + + + + + '3.0', + '_exported' => date( 'Y-m-d H:i:s' ), + '_theme' => $theme_slug, + ); + + // 1. Alle Customizer-Einstellungen (Theme Mods) + // Enthält: Farben, Slider, Hero, Social Links, Menü-Design, Login-Einstellungen, Sidebar-Einstellungen usw. + $data['theme_mods'] = get_theme_mods(); + + // 2. Announcement-Bar (gespeichert als wp_options, nicht als Theme Mod) + $announcement_keys = array( + 'mm_announcement_enabled', 'mm_announcement_text', 'mm_announcement_bg', + 'mm_announcement_color', 'mm_announcement_font_size', 'mm_announcement_font_family', + 'mm_announcement_position', 'mm_announcement_countdown_enabled', + 'mm_announcement_countdown_label', 'mm_announcement_countdown_date', + 'mm_announcement_countdown_expired_msg', + ); + foreach ( $announcement_keys as $key ) { + $data['announcement'][ $key ] = get_option( $key ); + } + + // 3. Widget-Konfigurationen aller Theme-Sidebars + $theme_sidebars = array( + 'single-post-sidebar', + 'homepage-sidebar-top', 'homepage-sidebar-middle-1', + 'homepage-sidebar-middle-2', 'homepage-sidebar-bottom', + 'homepage-sidebar-extra', 'footer-1', 'footer-2', 'footer-3', + ); + $all_sidebars = wp_get_sidebars_widgets(); + $widget_data = array(); + foreach ( $theme_sidebars as $sidebar_id ) { + $widget_data[ $sidebar_id ] = array(); + if ( empty( $all_sidebars[ $sidebar_id ] ) ) continue; + foreach ( $all_sidebars[ $sidebar_id ] as $widget_id ) { + if ( ! preg_match( '/^(.+)-(\d+)$/', $widget_id, $m ) ) continue; + $type = $m[1]; + $index = intval( $m[2] ); + $all = get_option( 'widget_' . $type, array() ); + $widget_data[ $sidebar_id ][] = array( + 'type' => $type, + 'index' => $index, + 'settings' => isset( $all[ $index ] ) ? $all[ $index ] : array(), + ); + } + } + $data['widgets'] = $widget_data; + + // 4. Team-Mitglieder (mit UUID, Banner, Thumbnail) + $team_data = array(); + $team_query = new WP_Query( array( + 'post_type' => 'team_member', + 'posts_per_page' => -1, + 'orderby' => 'menu_order', + 'order' => 'ASC', + ) ); + if ( $team_query->have_posts() ) { + while ( $team_query->have_posts() ) { + $team_query->the_post(); + $post_id = get_the_ID(); + $team_data[] = array( + 'title' => get_the_title(), + 'content' => get_the_content(), + 'rank' => get_post_meta( $post_id, '_team_member_rank', true ), + 'uuid' => get_post_meta( $post_id, '_team_member_uuid', true ), + 'banner_id' => get_post_meta( $post_id, '_team_member_banner', true ), + 'thumbnail_id' => get_post_thumbnail_id( $post_id ), + 'menu_order' => get_post_field( 'menu_order', $post_id ), + ); + } + wp_reset_postdata(); + } + $data['team'] = $team_data; + + // 5. FAQ-Einträge (theme-eigener Custom Post Type) + $faq_data = array(); + $faq_query = new WP_Query( array( + 'post_type' => 'faq', + 'posts_per_page' => -1, + 'orderby' => 'menu_order', + 'order' => 'ASC', + ) ); + if ( $faq_query->have_posts() ) { + while ( $faq_query->have_posts() ) { + $faq_query->the_post(); + $terms = wp_get_post_terms( get_the_ID(), 'faq_category', array( 'fields' => 'names' ) ); + $faq_data[] = array( + 'title' => get_the_title(), + 'content' => get_the_content(), + 'menu_order' => get_post_field( 'menu_order', get_the_ID() ), + 'categories' => is_wp_error( $terms ) ? array() : $terms, + ); + } + wp_reset_postdata(); + } + $data['faqs'] = $faq_data; + + // 6. Custom CSS (Customizer → Zusätzliches CSS) + $data['custom_css'] = wp_get_custom_css(); + + // 7. Homepage-Seite (falls eingestellt) + $homepage_id = get_option( 'page_on_front' ); + if ( $homepage_id ) { + $homepage = get_post( $homepage_id ); + if ( $homepage ) { + $data['homepage'] = array( + 'title' => $homepage->post_title, + 'content' => $homepage->post_content, + 'excerpt' => $homepage->post_excerpt, + 'featured_img' => get_post_thumbnail_id( $homepage_id ), + ); + } + } + + // 8. Navigation Menüs mit Items + $nav_menus = array(); + $all_nav_menus = wp_get_nav_menus(); + foreach ( $all_nav_menus as $menu ) { + $nav_menus[ $menu->slug ] = array( + 'name' => $menu->name, + 'description' => $menu->description, + 'items' => array(), + ); + $menu_items = wp_get_nav_menu_items( $menu->term_id ); + if ( $menu_items ) { + foreach ( $menu_items as $item ) { + $nav_menus[ $menu->slug ]['items'][] = array( + 'title' => $item->title, + 'url' => $item->url, + 'description' => $item->description, + 'type' => $item->type, + 'type_label' => $item->type_label, + 'object' => $item->object, + 'object_id' => $item->object_id, + 'parent' => $item->menu_item_parent, + 'menu_order' => $item->menu_order, + 'target' => get_post_meta( $item->ID, '_menu_item_target', true ), + 'classes' => implode( ' ', (array) $item->classes ), + 'xfn' => get_post_meta( $item->ID, '_menu_item_xfn', true ), + ); + } + } + } + $data['nav_menus'] = $nav_menus; + + // 9. Menü-Positionen (welches Menü ist welchem Theme-Location zugewiesen) + $data['nav_menu_locations'] = get_theme_mod( 'nav_menu_locations', array() ); + + $json = json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE ); + header( 'Content-Type: application/json; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename=' . $theme_slug . '-theme-backup-' . date( 'Y-m-d' ) . '.json' ); + header( 'Pragma: no-cache' ); + header( 'Expires: 0' ); + echo $json; + exit; +} + + +add_action( 'wp_ajax_import_theme_settings', 'handle_theme_settings_import' ); + +function handle_theme_settings_import() { + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_send_json_error( __( 'Keine Berechtigung.', 'minecraft-modern-theme' ) ); + } + check_ajax_referer( 'theme-import-nonce', 'nonce' ); + + if ( empty( $_FILES['import_file']['tmp_name'] ) ) { + wp_send_json_error( __( 'Keine Datei hochgeladen.', 'minecraft-modern-theme' ) ); + } + + $json_content = file_get_contents( $_FILES['import_file']['tmp_name'] ); + $data = json_decode( $json_content, true ); + + if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $data ) ) { + wp_send_json_error( __( 'Ungültige JSON-Datei.', 'minecraft-modern-theme' ) ); + } + + $imported = array(); + $version = isset( $data['_version'] ) ? $data['_version'] : '1.0'; + + // ------------------------------------------------------------------------- + // 1. Customizer / Theme Mods + // ------------------------------------------------------------------------- + $mods = array(); + if ( $version === '3.0' && isset( $data['theme_mods'] ) ) { + $mods = $data['theme_mods']; + } elseif ( $version === '2.0' && isset( $data['theme_mods'] ) ) { + $mods = $data['theme_mods']; + } else { + // Altes v1-Format: alles auf oberster Ebene + $skip = array( '_version', '_exported', '_theme', 'announcement', 'widgets', 'team', 'faqs', 'menus', 'wp_options', 'custom_css', 'team_data' ); + foreach ( $data as $k => $v ) { + if ( ! in_array( $k, $skip ) ) $mods[ $k ] = $v; + } + } + foreach ( $mods as $key => $value ) { + set_theme_mod( $key, $value ); + } + $imported[] = 'Customizer-Einstellungen'; + + // ------------------------------------------------------------------------- + // 2. Announcement-Bar + // ------------------------------------------------------------------------- + $announcement_keys = array( + 'mm_announcement_enabled', 'mm_announcement_text', 'mm_announcement_bg', + 'mm_announcement_color', 'mm_announcement_font_size', 'mm_announcement_font_family', + 'mm_announcement_position', 'mm_announcement_countdown_enabled', + 'mm_announcement_countdown_label', 'mm_announcement_countdown_date', + 'mm_announcement_countdown_expired_msg', + ); + $ann = isset( $data['announcement'] ) ? $data['announcement'] : $data; + foreach ( $announcement_keys as $key ) { + if ( array_key_exists( $key, $ann ) ) { + update_option( $key, $ann[ $key ] ); + } + } + $imported[] = 'Ankündigung'; + + // ------------------------------------------------------------------------- + // 3. Widgets + // ------------------------------------------------------------------------- + if ( ! empty( $data['widgets'] ) ) { + $current_sidebars = get_option( 'sidebars_widgets', array() ); + foreach ( $data['widgets'] as $sidebar_id => $widgets ) { + $current_sidebars[ $sidebar_id ] = array(); + foreach ( $widgets as $widget ) { + $type = sanitize_key( $widget['type'] ); + $index = intval( $widget['index'] ); + $all = get_option( 'widget_' . $type, array() ); + $all[ $index ] = $widget['settings']; + update_option( 'widget_' . $type, $all ); + $current_sidebars[ $sidebar_id ][] = $type . '-' . $index; + } + } + update_option( 'sidebars_widgets', $current_sidebars ); + $imported[] = 'Widgets'; + } + + // ------------------------------------------------------------------------- + // 4. Team-Mitglieder + // ------------------------------------------------------------------------- + $team_data = isset( $data['team'] ) ? $data['team'] : ( isset( $data['team_data'] ) ? $data['team_data'] : array() ); + if ( ! empty( $team_data ) ) { + $existing = new WP_Query( array( 'post_type' => 'team_member', 'posts_per_page' => -1, 'fields' => 'ids' ) ); + if ( $existing->have_posts() ) { + foreach ( $existing->posts as $pid ) wp_delete_post( $pid, true ); + } + wp_reset_postdata(); + foreach ( $team_data as $member ) { + $id = wp_insert_post( array( + 'post_title' => sanitize_text_field( $member['title'] ), + 'post_content' => wp_kses_post( $member['content'] ), + 'post_type' => 'team_member', + 'post_status' => 'publish', + 'menu_order' => intval( isset( $member['menu_order'] ) ? $member['menu_order'] : 0 ), + ) ); + if ( $id && ! is_wp_error( $id ) ) { + // Rank + if ( ! empty( $member['rank'] ) ) { + update_post_meta( $id, '_team_member_rank', sanitize_text_field( $member['rank'] ) ); + } + // UUID + if ( ! empty( $member['uuid'] ) ) { + update_post_meta( $id, '_team_member_uuid', sanitize_text_field( $member['uuid'] ) ); + } + // Thumbnail + if ( ! empty( $member['thumbnail_id'] ) ) { + set_post_thumbnail( $id, intval( $member['thumbnail_id'] ) ); + } + // Banner + if ( ! empty( $member['banner_id'] ) ) { + update_post_meta( $id, '_team_member_banner', intval( $member['banner_id'] ) ); + } + } + } + $imported[] = count( $team_data ) . ' Team-Mitglieder'; + } + + // ------------------------------------------------------------------------- + // 5. FAQs + // ------------------------------------------------------------------------- + if ( ! empty( $data['faqs'] ) ) { + $existing = new WP_Query( array( 'post_type' => 'faq', 'posts_per_page' => -1, 'fields' => 'ids' ) ); + if ( $existing->have_posts() ) { + foreach ( $existing->posts as $pid ) wp_delete_post( $pid, true ); + } + wp_reset_postdata(); + foreach ( $data['faqs'] as $faq ) { + $id = wp_insert_post( array( + 'post_title' => sanitize_text_field( $faq['title'] ), + 'post_content' => wp_kses_post( $faq['content'] ), + 'post_type' => 'faq', + 'post_status' => 'publish', + 'menu_order' => intval( isset( $faq['menu_order'] ) ? $faq['menu_order'] : 0 ), + ) ); + if ( $id && ! is_wp_error( $id ) && ! empty( $faq['categories'] ) ) { + foreach ( (array) $faq['categories'] as $cat_name ) { + $term = term_exists( sanitize_text_field( $cat_name ), 'faq_category' ); + if ( ! $term ) { + $term = wp_insert_term( sanitize_text_field( $cat_name ), 'faq_category' ); + } + if ( ! is_wp_error( $term ) ) { + wp_set_post_terms( $id, array( intval( $term['term_id'] ) ), 'faq_category', true ); + } + } + } + } + $imported[] = count( $data['faqs'] ) . ' FAQs'; + } + + // ------------------------------------------------------------------------- + // 6. Custom CSS + // ------------------------------------------------------------------------- + if ( isset( $data['custom_css'] ) && $data['custom_css'] !== '' ) { + wp_update_custom_css_post( $data['custom_css'] ); + $imported[] = 'Custom CSS'; + } + + // ------------------------------------------------------------------------- + // 7. Homepage-Seite + // ------------------------------------------------------------------------- + if ( ! empty( $data['homepage'] ) ) { + $homepage_id = get_option( 'page_on_front' ); + if ( $homepage_id ) { + wp_update_post( array( + 'ID' => $homepage_id, + 'post_title' => sanitize_text_field( $data['homepage']['title'] ), + 'post_content' => wp_kses_post( $data['homepage']['content'] ), + 'post_excerpt' => sanitize_textarea_field( $data['homepage']['excerpt'] ), + ) ); + if ( ! empty( $data['homepage']['featured_img'] ) ) { + set_post_thumbnail( $homepage_id, intval( $data['homepage']['featured_img'] ) ); + } + $imported[] = 'Homepage-Seite'; + } + } + + // ------------------------------------------------------------------------- + // 8. Navigation Menüs mit Items + // ------------------------------------------------------------------------- + if ( ! empty( $data['nav_menus'] ) ) { + $menu_id_map = array(); // slug => term_id Mapping für Item-Parent-Zuordnung + + // Alle existierenden Menüs löschen + $existing_menus = wp_get_nav_menus(); + foreach ( $existing_menus as $menu ) { + wp_delete_nav_menu( $menu->term_id ); + } + + // Neue Menüs und Items importieren + foreach ( $data['nav_menus'] as $menu_slug => $menu_data ) { + $new_menu = wp_create_nav_menu( $menu_data['name'] ); + if ( ! is_wp_error( $new_menu ) ) { + $menu_id_map[ $menu_slug ] = $new_menu; + + // Items hinzufügen + if ( ! empty( $menu_data['items'] ) ) { + $item_id_map = array(); // Altes Item ID => Neues Item ID Mapping + + // Erste Runde: Root-Items (parent = 0) + foreach ( $menu_data['items'] as $idx => $item ) { + if ( empty( $item['parent'] ) || $item['parent'] == 0 ) { + $item_id = wp_update_nav_menu_item( $new_menu, 0, array( + 'menu-item-title' => sanitize_text_field( $item['title'] ), + 'menu-item-url' => esc_url_raw( $item['url'] ), + 'menu-item-description' => sanitize_text_field( $item['description'] ), + 'menu-item-type' => sanitize_key( $item['type'] ), + 'menu-item-object' => sanitize_key( $item['object'] ), + 'menu-item-object-id' => intval( $item['object_id'] ), + 'menu-item-target' => sanitize_text_field( $item['target'] ), + 'menu-item-classes' => sanitize_text_field( $item['classes'] ), + 'menu-item-xfn' => sanitize_text_field( $item['xfn'] ), + 'menu-item-status' => 'publish', + ) ); + if ( $item_id && ! is_wp_error( $item_id ) ) { + $item_id_map[ $idx ] = $item_id; + } + } + } + + // Zweite Runde: Sub-Items (parent > 0) + foreach ( $menu_data['items'] as $idx => $item ) { + if ( ! empty( $item['parent'] ) && $item['parent'] > 0 ) { + $parent_item_id = isset( $item_id_map[ $item['parent'] - 1 ] ) ? $item_id_map[ $item['parent'] - 1 ] : 0; + $item_id = wp_update_nav_menu_item( $new_menu, 0, array( + 'menu-item-title' => sanitize_text_field( $item['title'] ), + 'menu-item-url' => esc_url_raw( $item['url'] ), + 'menu-item-description' => sanitize_text_field( $item['description'] ), + 'menu-item-type' => sanitize_key( $item['type'] ), + 'menu-item-object' => sanitize_key( $item['object'] ), + 'menu-item-object-id' => intval( $item['object_id'] ), + 'menu-item-parent-id' => $parent_item_id, + 'menu-item-target' => sanitize_text_field( $item['target'] ), + 'menu-item-classes' => sanitize_text_field( $item['classes'] ), + 'menu-item-xfn' => sanitize_text_field( $item['xfn'] ), + 'menu-item-status' => 'publish', + ) ); + if ( $item_id && ! is_wp_error( $item_id ) ) { + $item_id_map[ $idx ] = $item_id; + } + } + } + } + } + } + $imported[] = 'Navigation Menüs (' . count( $data['nav_menus'] ) . ')'; + } + + // ------------------------------------------------------------------------- + // 9. Menü-Positionen + // ------------------------------------------------------------------------- + if ( ! empty( $data['nav_menu_locations'] ) ) { + set_theme_mod( 'nav_menu_locations', $data['nav_menu_locations'] ); + $imported[] = 'Menü-Positionen'; + } + + wp_send_json_success( sprintf( + __( 'Erfolgreich wiederhergestellt: %s', 'minecraft-modern-theme' ), + implode( ', ', $imported ) + ) ); +} diff --git a/Minecraft-Modern-Theme/inc/team.php b/Minecraft-Modern-Theme/inc/team.php new file mode 100644 index 0000000..c50e3c3 --- /dev/null +++ b/Minecraft-Modern-Theme/inc/team.php @@ -0,0 +1,513 @@ + array( + 'name' => __( 'Team', 'minecraft-modern-theme' ), + 'singular_name' => __( 'Teammitglied', 'minecraft-modern-theme' ), + 'add_new' => __( 'Neues Mitglied', 'minecraft-modern-theme' ), + 'menu_name' => __( 'Team', 'minecraft-modern-theme' ), + ), + 'public' => true, + 'has_archive' => true, + 'menu_icon' => 'dashicons-groups', + 'supports' => array( 'title', 'thumbnail', 'page-attributes' ), + 'rewrite' => array( 'slug' => 'team' ), + 'show_in_rest' => true, + 'show_in_menu' => false, + ) ); + } +} +add_action( 'init', 'create_team_post_type' ); + + +function add_team_meta_boxes() { + add_meta_box( 'team_member_rank_box', __( 'Rang & Position', 'minecraft-modern-theme' ), 'team_member_rank_callback', 'team_member', 'side', 'default' ); +} +add_action( 'add_meta_boxes', 'add_team_meta_boxes' ); + +function team_member_rank_callback( $post ) { + wp_nonce_field( 'team_member_rank_save', 'team_member_rank_nonce' ); + $value = get_post_meta( $post->ID, '_team_member_rank', true ); + ?> +

+ + +

+ id !== 'toplevel_page_mm-team-manager' ) return; + echo ''; +} +add_action( 'admin_head', 'mm_team_manager_admin_css' ); + +function mm_team_manager_page_html() { + ?> +
+

Team Verwaltung

+

Hier kannst du Teammitglieder hinzufügen, sortieren und bearbeiten.

+ +
+

Neues Mitglied hinzufügen

+
+
+
+ +
+
+
+ +
+
+
+ +
+ +
+ +
+
+ +

UUID eingeben → Avatar wird automatisch geladen

+
+ +
+
+
+ + + Kein Bild gewählt +
+

Wird ignoriert wenn UUID gesetzt ist

+
+ +
+
+
+ + + Kein Banner gewählt +
+

Hintergrundbild der Card (optional)

+
+
+ +
+
+
+ +
+
+
+ +
+ +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + 'team_member', 'posts_per_page' => -1, 'orderby' => 'menu_order', 'order' => 'ASC' ) ); + if ( $team_query->have_posts() ) : + while ( $team_query->have_posts() ) : $team_query->the_post(); + $id = get_the_ID(); + $name = get_the_title(); + $rank = get_post_meta( $id, '_team_member_rank', true ); + $bio = get_the_content(); + $uuid = get_post_meta( $id, '_team_member_uuid', true ); + $img_id = get_post_thumbnail_id( $id ); + $banner_id = get_post_meta( $id, '_team_member_banner', true ); + $banner_url = $banner_id ? wp_get_attachment_image_url( $banner_id, 'medium' ) : false; + + // Avatar anzeigen: UUID > Bild > Placeholder + if ( $uuid ) { + $avatar_src = 'https://visage.surgeplay.com/bust/' . esc_attr($uuid) . '.png'; + } elseif ( $img_id ) { + $avatar_src = wp_get_attachment_image_url( $img_id, array(40,40) ); + } else { + $avatar_src = false; + } + ?> + + + + + + + + + + '; + endif; + ?> + +
BildNameRangBioUUID / BildSort.Aktionen
+
+ + + + 👤 + +
+
+ +
+ + + +
+ +
+
+ + + + + +
Noch keine Mitglieder vorhanden.
+
+
+ + + $id, 'menu_order' => $pos ) ); + $pos++; + } + wp_send_json_success(); +} + + +add_action( 'wp_ajax_mm_add_team_member', 'handle_mm_add_member' ); +function handle_mm_add_member() { + check_ajax_referer( 'mm_team_nonce', 'nonce' ); + if ( ! current_user_can( 'publish_posts' ) ) wp_send_json_error( 'Keine Berechtigung' ); + + $id = wp_insert_post( array( + 'post_title' => sanitize_text_field( $_POST['name'] ), + 'post_content' => sanitize_textarea_field( $_POST['bio'] ), + 'post_type' => 'team_member', + 'post_status' => 'publish', + 'menu_order' => 999, + ) ); + + if ( ! $id || is_wp_error( $id ) ) { + wp_send_json_error( 'Fehler beim Erstellen' ); + } + + update_post_meta( $id, '_team_member_rank', sanitize_text_field( $_POST['rank'] ) ); + + // UUID speichern (kein externer Request hier – nur als Text speichern) + if ( ! empty( $_POST['uuid'] ) ) { + $uuid = sanitize_text_field( trim( $_POST['uuid'] ) ); + update_post_meta( $id, '_team_member_uuid', $uuid ); + } elseif ( ! empty( $_POST['img_id'] ) ) { + set_post_thumbnail( $id, intval( $_POST['img_id'] ) ); + } + + // Banner-Bild speichern + if ( ! empty( $_POST['banner_id'] ) ) { + update_post_meta( $id, '_team_member_banner', intval( $_POST['banner_id'] ) ); + } + + wp_send_json_success( array( 'id' => $id, 'msg' => 'Hinzugefügt' ) ); +} + +add_action( 'wp_ajax_mm_update_team_member', 'handle_mm_update_team_member' ); +function handle_mm_update_team_member() { + check_ajax_referer( 'mm_team_nonce', 'nonce' ); + if ( ! current_user_can( 'edit_posts' ) ) wp_send_json_error( 'Keine Berechtigung' ); + $id = intval( $_POST['id'] ); + wp_update_post( array( + 'ID' => $id, + 'post_title' => sanitize_text_field( $_POST['title'] ), + 'post_content' => sanitize_textarea_field( $_POST['bio'] ), + ) ); + update_post_meta( $id, '_team_member_rank', sanitize_text_field( $_POST['rank'] ) ); + // UUID speichern oder löschen + if ( isset( $_POST['uuid'] ) ) { + $uuid = sanitize_text_field( trim( $_POST['uuid'] ) ); + if ( $uuid ) { + update_post_meta( $id, '_team_member_uuid', $uuid ); + } else { + delete_post_meta( $id, '_team_member_uuid' ); + } + } + // Bild nur setzen wenn keine UUID + if ( empty( $_POST['uuid'] ) && ! empty( $_POST['img_id'] ) ) { + set_post_thumbnail( $id, intval( $_POST['img_id'] ) ); + } + // Banner speichern + if ( isset( $_POST['banner_id'] ) ) { + if ( ! empty( $_POST['banner_id'] ) ) { + update_post_meta( $id, '_team_member_banner', intval( $_POST['banner_id'] ) ); + } + } + wp_send_json_success( 'Gespeichert' ); +} + +add_action( 'wp_ajax_mm_delete_team_member', 'handle_mm_delete_team_member' ); +function handle_mm_delete_team_member() { + check_ajax_referer( 'mm_team_nonce', 'nonce' ); + if ( ! current_user_can( 'delete_posts' ) ) wp_send_json_error( 'Keine Berechtigung' ); + wp_delete_post( intval( $_POST['id'] ), true ); + wp_send_json_success( 'Gelöscht' ); +} + + +function create_team_page_automatically() { + if ( get_theme_mod( 'team_enabled', true ) && get_page_by_title( 'Team' ) == null ) { + wp_insert_post( array( 'post_title' => 'Team', 'post_status' => 'publish', 'post_type' => 'page', 'post_author' => 1 ) ); + } +} +add_action( 'customize_save_after', 'create_team_page_automatically' ); + + +function load_team_page_template( $template ) { + if ( ! get_theme_mod( 'team_enabled', true ) ) return $template; + + // BUG-FIX: locate_template() statt get_template_directory() – so wird ein + // archive-team.php im Child-Theme berücksichtigt. + $is_team_page = false; + if ( is_post_type_archive( 'team_member' ) ) { + $is_team_page = true; + } elseif ( is_page() ) { + $obj = get_queried_object(); + $is_team_page = ( $obj && $obj->post_name === 'team' ); + } + + if ( $is_team_page ) { + $t = locate_template( 'archive-team.php' ); + if ( $t ) return $t; + } + return $template; +} +add_filter( 'template_include', 'load_team_page_template' ); + + +// Doppelte team_customize_register entfernt – ist jetzt in inc/customizer.php diff --git a/Minecraft-Modern-Theme/inc/theme-setup.php b/Minecraft-Modern-Theme/inc/theme-setup.php new file mode 100644 index 0000000..70c96c1 --- /dev/null +++ b/Minecraft-Modern-Theme/inc/theme-setup.php @@ -0,0 +1,267 @@ + 9999, + 'width' => 9999, + 'flex-height' => true, + 'flex-width' => true, + 'header_text' => array( 'site-title', 'site-description' ), + ) ); + + add_theme_support( 'custom-background' ); + + register_nav_menus( array( + 'primary' => __( 'Hauptmenü', 'minecraft-modern-theme' ), + 'footer' => __( 'Footer-Menü', 'minecraft-modern-theme' ), + ) ); + + // FIX: 'script' und 'style' ergänzt für sauberes HTML5-Output-Format + add_theme_support( 'html5', array( + 'search-form', 'comment-form', 'comment-list', + 'gallery', 'caption', 'script', 'style', + ) ); +} +add_action( 'after_setup_theme', 'minecraft_modern_setup' ); + + +// ========================================================================= +// LOKALE SCHRIFTEN (DSGVO) +// ========================================================================= +// Schriften liegen unter /fonts//font.css samt woff2-Dateien und werden +// lokal ausgeliefert. Dadurch wird beim Seitenaufruf keine Verbindung zu +// fonts.googleapis.com / fonts.gstatic.com aufgebaut und keine Besucher-IP +// an Google übertragen. + +/** + * Wandelt einen Schriftnamen in den Ordnernamen unter /fonts/ um. + * "Open Sans" -> "open-sans" + */ +function mm_font_slug( $font_name ) { + return strtolower( str_replace( ' ', '-', trim( $font_name ) ) ); +} + +/** + * Bindet eine lokal gehostete Schrift ein. + * + * Das Handle wird aus dem Schriftnamen abgeleitet ("Open Sans" -> mm-font-open-sans). + * Dadurch wird dieselbe Schrift automatisch nur einmal eingebunden, auch wenn sie + * an mehreren Stellen angefordert wird (z. B. Basis- und Slider-Schrift beide Raleway). + * + * @param string $font_name Schriftname, z. B. "Open Sans". + * @return bool True, wenn die Schrift lokal vorhanden war und eingebunden wurde. + */ +function mm_enqueue_local_font( $font_name ) { + if ( empty( $font_name ) ) { + return false; + } + $slug = mm_font_slug( $font_name ); + $rel = '/fonts/' . $slug . '/font.css'; + $path = get_template_directory() . $rel; + + if ( ! file_exists( $path ) ) { + return false; + } + wp_enqueue_style( 'mm-font-' . $slug, get_template_directory_uri() . $rel, array(), filemtime( $path ) ); + return true; +} + + +// === Styles & Scripts laden === +function minecraft_modern_scripts() { + // Haupt-Stylesheet des Parent-Themes. + // Bewusst get_template_directory_uri() statt get_stylesheet_uri(): Bei aktivem + // Child-Theme zeigt get_stylesheet_uri() auf das Child. Das Child-Theme belegt + // dasselbe Handle mit der Parent-Datei – zeigten beide auf unterschiedliche + // Dateien, entschiede die Aufrufreihenfolge darüber, welches CSS geladen wird + // (WordPress ignoriert den zweiten Aufruf eines bereits registrierten Handles). + // So liefern beide Seiten dieselbe Datei und die Reihenfolge ist egal. + wp_enqueue_style( + 'minecraft-modern-style', + get_template_directory_uri() . '/style.css', + array(), + filemtime( get_template_directory() . '/style.css' ) + ); + + // Bei aktivem Child-Theme dessen style.css danach laden, damit das Theme auch + // dann vollständig ist, wenn das Child keine eigene Enqueue-Funktion mitbringt. + if ( is_child_theme() && file_exists( get_stylesheet_directory() . '/style.css' ) ) { + wp_enqueue_style( + 'minecraft-modern-child-style', + get_stylesheet_uri(), + array( 'minecraft-modern-style' ), + filemtime( get_stylesheet_directory() . '/style.css' ) + ); + } + + // Basis-Schrift des Themes (lokal, siehe mm_enqueue_local_font) + mm_enqueue_local_font( 'Raleway' ); + + // Font Awesome (lokal statt cdnjs) – vorher als fester in header.php + wp_enqueue_style( + 'font-awesome', + get_template_directory_uri() . '/css/fontawesome.min.css', + array(), + filemtime( get_template_directory() . '/css/fontawesome.min.css' ) + ); + + // Swiper.js CSS (lokal statt CDN) + wp_enqueue_style( + 'swiper-css', + get_template_directory_uri() . '/css/swiper-bundle.min.css', + array(), + filemtime( get_template_directory() . '/css/swiper-bundle.min.css' ) + ); + + // Header-Scroll + Suche Toggle + wp_enqueue_script( + 'minecraft-modern-header-script', + get_template_directory_uri() . '/js/header-scroll.js', + array(), + filemtime( get_template_directory() . '/js/header-scroll.js' ), + true + ); + + // Navigation (Dropdown + Außenklick-Schließen) + wp_enqueue_script( + 'minecraft-navigation', + get_template_directory_uri() . '/js/navigation.js', + array(), + filemtime( get_template_directory() . '/js/navigation.js' ), + true + ); + + // Swiper.js (lokal statt CDN) + wp_enqueue_script( + 'swiper-js', + get_template_directory_uri() . '/js/swiper-bundle.min.js', + array(), + filemtime( get_template_directory() . '/js/swiper-bundle.min.js' ), + true + ); + + // Slider-Init + wp_enqueue_script( + 'minecraft-modern-slider-script', + get_template_directory_uri() . '/js/slider-init.js', + array( 'swiper-js' ), + filemtime( get_template_directory() . '/js/slider-init.js' ), + true + ); + + // Theme Toggle + wp_enqueue_script( + 'theme-toggle-script', + get_template_directory_uri() . '/js/theme-toggle.js', + array(), + filemtime( get_template_directory() . '/js/theme-toggle.js' ), + true + ); + + // BUG-FIX: post_type_exists('faq') prüft nur ob der CPT registriert ist, nicht ob wir + // auf einer FAQ-Seite sind – das Script wurde damit seitenübergreifend geladen. + // Jetzt: nur auf FAQ-Archiv-, Einzel- und Seiten mit FAQ-Template laden. + if ( post_type_exists( 'faq' ) && ( is_post_type_archive( 'faq' ) || is_singular( 'faq' ) || is_page( 'FAQ' ) || is_page( 'faq' ) ) ) { + wp_enqueue_script( + 'faq-accordion-script', + get_template_directory_uri() . '/js/faq-accordion.js', + array(), + filemtime( get_template_directory() . '/js/faq-accordion.js' ), + true + ); + } + + // FIX: 'loop' ergänzt – war in slider-init.js als sliderSettings.loop referenziert, aber nie übergeben + wp_localize_script( + 'minecraft-modern-slider-script', + 'sliderSettings', + array( + 'hideArrows' => get_theme_mod( 'slider_hide_arrows', false ) ? '1' : '0', + 'hidePagination' => get_theme_mod( 'slider_hide_pagination', false ) ? '1' : '0', + 'effect' => get_theme_mod( 'slider_effect', 'fade' ), + 'direction' => get_theme_mod( 'slider_direction', 'horizontal' ), + 'defaultMode' => get_theme_mod( 'default_theme_mode', 'dark' ), + 'loop' => get_theme_mod( 'slider_loop', true ) ? '1' : '0', + 'ajax_url' => admin_url( 'admin-ajax.php' ), + ) + ); + + wp_localize_script( + 'minecraft-modern-header-script', + 'headerSettings', + array( + 'isCustomizer' => is_customize_preview(), + ) + ); +} +add_action( 'wp_enqueue_scripts', 'minecraft_modern_scripts' ); + + +// FIX: Scroll-to-Top via Customizer ein-/ausblendbar +function minecraft_modern_scroll_to_top_css() { + if ( ! get_theme_mod( 'show_scroll_to_top', true ) ) { + wp_add_inline_style( 'minecraft-modern-style', '#scroll-to-top { display: none !important; }' ); + } +} +add_action( 'wp_enqueue_scripts', 'minecraft_modern_scroll_to_top_css', 25 ); + + +// ========================================================================= +// PERFORMANCE: WordPress-Emoji-Ersetzung abschalten +// ========================================================================= +// WordPress prüft per JavaScript, ob der Browser den neuesten Emoji-Satz +// kennt. Fällt der Test negativ aus (unter Windows 10 häufig der Fall), +// werden ALLE Emoji im Seiteninhalt durch SVG-Grafiken von s.w.org ersetzt. +// Auf dieser Seite waren das 9 externe Downloads mit rund 13 Sekunden +// Gesamtladezeit – ausgelöst durch die Emoji in der Ankündigungs-Leiste und +// den Assistent-Buttons. +// +// Ohne das Skript stellt der Browser die Emoji als ganz normale Schriftzeichen +// dar; sie bleiben also sichtbar, es entfällt nur der externe Bilder-Download. +// Zum Rückgängigmachen genügt es, die folgende Funktion samt Hooks zu löschen. +function minecraft_modern_disable_emoji_scripts() { + remove_action( 'wp_head', 'print_emoji_detection_script', 7 ); + remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); + remove_action( 'wp_print_styles', 'print_emoji_styles' ); + remove_action( 'admin_print_styles', 'print_emoji_styles' ); + remove_filter( 'the_content_feed', 'wp_staticize_emoji' ); + remove_filter( 'comment_text_rss', 'wp_staticize_emoji' ); + remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' ); +} +add_action( 'init', 'minecraft_modern_disable_emoji_scripts' ); + +/** + * Entfernt zusätzlich das Emoji-Skript aus dem TinyMCE-Editor. + */ +function minecraft_modern_disable_emoji_tinymce( $plugins ) { + return is_array( $plugins ) ? array_diff( $plugins, array( 'wpemoji' ) ) : array(); +} +add_filter( 'tiny_mce_plugins', 'minecraft_modern_disable_emoji_tinymce' ); + +/** + * Verhindert den DNS-Prefetch auf s.w.org, den WordPress sonst zusätzlich setzt. + */ +function minecraft_modern_remove_emoji_dns_prefetch( $urls, $relation_type ) { + if ( 'dns-prefetch' === $relation_type ) { + $emoji_svg_url = apply_filters( 'emoji_svg_url', 'https://s.w.org/images/core/emoji/' ); + $urls = array_diff( $urls, array( $emoji_svg_url ) ); + } + return $urls; +} +add_filter( 'wp_resource_hints', 'minecraft_modern_remove_emoji_dns_prefetch', 10, 2 ); diff --git a/Minecraft-Modern-Theme/inc/theme-updater.php b/Minecraft-Modern-Theme/inc/theme-updater.php index 1863f67..6ed18b8 100644 --- a/Minecraft-Modern-Theme/inc/theme-updater.php +++ b/Minecraft-Modern-Theme/inc/theme-updater.php @@ -1,141 +1,141 @@ -theme_slug = get_template(); - - add_action( 'admin_notices', [ $this, 'display_update_notice' ] ); - add_action( 'wp_dashboard_setup', [ $this, 'add_dashboard_widget' ] ); - add_action( 'admin_init', [ $this, 'handle_refresh_request' ] ); - } - - /** - * Holt die API-Daten von Gitea (mit Transient-Cache). - */ - private function get_latest_release() { - $update_data = get_transient( $this->transient_key ); - - if ( false === $update_data ) { - $api_url = "https://git.viper.ipv64.net/api/v1/repos/{$this->repo}/releases/latest"; - $response = wp_remote_get( $api_url, [ 'timeout' => 10 ] ); - - if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { - set_transient( $this->transient_key, [ 'error' => true ], 2 * HOUR_IN_SECONDS ); - return [ 'error' => true ]; - } - - $data = json_decode( wp_remote_retrieve_body( $response ) ); - $new_version = ltrim( $data->tag_name, 'vV' ); - - $update_data = [ - 'version' => $new_version, - 'url' => "https://git.viper.ipv64.net/{$this->repo}/releases", - ]; - - set_transient( $this->transient_key, $update_data, 12 * HOUR_IN_SECONDS ); - } - - return $update_data; - } - - /** - * Zeigt die gelbe Info-Box oben im Admin-Bereich an, wenn ein Update verfügbar ist. - */ - public function display_update_notice() { - if ( ! current_user_can( 'update_themes' ) ) return; - - $latest = $this->get_latest_release(); - if ( isset( $latest['error'] ) ) return; - - $current_version = wp_get_theme( $this->theme_slug )->get( 'Version' ); - if ( ! $current_version ) return; - - if ( version_compare( $current_version, $latest['version'], '<' ) ) { - ?> -
-

- - Minecraft Modern Update verfügbar: - Version ist bereit zum Download. - - Zum Download → - -

-
- theme_slug )->get( 'Version' ); - $latest = $this->get_latest_release(); - - echo '
'; - echo '

Installiert: ' . esc_html( $current_version ?: '–' ) . '

'; - - if ( isset( $latest['version'] ) ) { - echo '

Aktuellste: ' . esc_html( $latest['version'] ) . '

'; - - if ( $current_version && version_compare( $current_version, $latest['version'], '<' ) ) { - echo '
'; - echo '

Update verfügbar!

'; - echo 'Download ZIP von Gitea'; - echo '
'; - } else { - echo '

'; - echo ' Theme ist aktuell.

'; - } - } else { - echo '

Prüfung fehlgeschlagen.

'; - } - - $refresh_url = wp_nonce_url( admin_url( 'index.php?mm_refresh_check=1' ), 'mm_refresh_action' ); - echo '

Update-Cache jetzt leeren

'; - echo '
'; - } - - /** - * Verarbeitet den Klick auf "Update-Cache jetzt leeren". - */ - public function handle_refresh_request() { - if ( isset( $_GET['mm_refresh_check'] ) && check_admin_referer( 'mm_refresh_action' ) ) { - delete_transient( $this->transient_key ); - wp_redirect( admin_url( 'index.php' ) ); - exit; - } - } -} - +theme_slug = get_template(); + + add_action( 'admin_notices', [ $this, 'display_update_notice' ] ); + add_action( 'wp_dashboard_setup', [ $this, 'add_dashboard_widget' ] ); + add_action( 'admin_init', [ $this, 'handle_refresh_request' ] ); + } + + /** + * Holt die API-Daten von Gitea (mit Transient-Cache). + */ + private function get_latest_release() { + $update_data = get_transient( $this->transient_key ); + + if ( false === $update_data ) { + $api_url = "https://git.viper.ipv64.net/api/v1/repos/{$this->repo}/releases/latest"; + $response = wp_remote_get( $api_url, [ 'timeout' => 10 ] ); + + if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { + set_transient( $this->transient_key, [ 'error' => true ], 2 * HOUR_IN_SECONDS ); + return [ 'error' => true ]; + } + + $data = json_decode( wp_remote_retrieve_body( $response ) ); + $new_version = ltrim( $data->tag_name, 'vV' ); + + $update_data = [ + 'version' => $new_version, + 'url' => "https://git.viper.ipv64.net/{$this->repo}/releases", + ]; + + set_transient( $this->transient_key, $update_data, 12 * HOUR_IN_SECONDS ); + } + + return $update_data; + } + + /** + * Zeigt die gelbe Info-Box oben im Admin-Bereich an, wenn ein Update verfügbar ist. + */ + public function display_update_notice() { + if ( ! current_user_can( 'update_themes' ) ) return; + + $latest = $this->get_latest_release(); + if ( isset( $latest['error'] ) ) return; + + $current_version = wp_get_theme( $this->theme_slug )->get( 'Version' ); + if ( ! $current_version ) return; + + if ( version_compare( $current_version, $latest['version'], '<' ) ) { + ?> +
+

+ + Minecraft Modern Update verfügbar: + Version ist bereit zum Download. + + Zum Download → + +

+
+ theme_slug )->get( 'Version' ); + $latest = $this->get_latest_release(); + + echo '
'; + echo '

Installiert: ' . esc_html( $current_version ?: '–' ) . '

'; + + if ( isset( $latest['version'] ) ) { + echo '

Aktuellste: ' . esc_html( $latest['version'] ) . '

'; + + if ( $current_version && version_compare( $current_version, $latest['version'], '<' ) ) { + echo '
'; + echo '

Update verfügbar!

'; + echo 'Download ZIP von Gitea'; + echo '
'; + } else { + echo '

'; + echo ' Theme ist aktuell.

'; + } + } else { + echo '

Prüfung fehlgeschlagen.

'; + } + + $refresh_url = wp_nonce_url( admin_url( 'index.php?mm_refresh_check=1' ), 'mm_refresh_action' ); + echo '

Update-Cache jetzt leeren

'; + echo '
'; + } + + /** + * Verarbeitet den Klick auf "Update-Cache jetzt leeren". + */ + public function handle_refresh_request() { + if ( isset( $_GET['mm_refresh_check'] ) && check_admin_referer( 'mm_refresh_action' ) ) { + delete_transient( $this->transient_key ); + wp_redirect( admin_url( 'index.php' ) ); + exit; + } + } +} + new Minecraft_Modern_Theme_Manager(); \ No newline at end of file diff --git a/Minecraft-Modern-Theme/inc/video.php b/Minecraft-Modern-Theme/inc/video.php new file mode 100644 index 0000000..9bffdbf --- /dev/null +++ b/Minecraft-Modern-Theme/inc/video.php @@ -0,0 +1,1272 @@ + array( + 'name' => __( 'Videos', 'minecraft-modern-theme' ), + 'singular_name' => __( 'Video', 'minecraft-modern-theme' ), + 'add_new' => __( 'Neues Video', 'minecraft-modern-theme' ), + 'add_new_item' => __( 'Neues Video hinzufügen', 'minecraft-modern-theme' ), + 'edit_item' => __( 'Video bearbeiten', 'minecraft-modern-theme' ), + 'all_items' => __( 'Alle Videos', 'minecraft-modern-theme' ), + 'menu_name' => __( 'Videos', 'minecraft-modern-theme' ), + ), + 'public' => true, + 'has_archive' => true, + 'menu_icon' => 'dashicons-video-alt3', + 'menu_position' => 7, + 'supports' => array( 'title', 'thumbnail', 'excerpt', 'page-attributes' ), + 'rewrite' => array( 'slug' => 'videos' ), + 'show_in_rest' => true, + ) ); + + register_post_type( 'mm_livestream', array( + 'labels' => array( + 'name' => __( 'Livestreams', 'minecraft-modern-theme' ), + 'singular_name' => __( 'Livestream', 'minecraft-modern-theme' ), + 'add_new' => __( 'Neuer Livestream', 'minecraft-modern-theme' ), + 'add_new_item' => __( 'Neuen Livestream hinzufügen', 'minecraft-modern-theme' ), + 'edit_item' => __( 'Livestream bearbeiten', 'minecraft-modern-theme' ), + 'all_items' => __( 'Alle Livestreams', 'minecraft-modern-theme' ), + 'menu_name' => __( 'Livestreams', 'minecraft-modern-theme' ), + ), + 'public' => true, + 'publicly_queryable' => false, + 'exclude_from_search' => true, + 'show_ui' => true, + 'show_in_menu' => 'edit.php?post_type=mm_video', + 'menu_position' => 8, + 'supports' => array( 'title', 'excerpt', 'page-attributes' ), + 'show_in_rest' => true, + ) ); +} +add_action( 'init', 'mm_register_video_post_type' ); + + +// --- 2. Meta-Box: Video URL + Kategorie --- +function mm_video_meta_boxes() { + add_meta_box( + 'mm_video_data', + __( 'Video-Einstellungen', 'minecraft-modern-theme' ), + 'mm_video_meta_box_html', + 'mm_video', 'normal', 'high' + ); + + add_meta_box( + 'mm_livestream_data', + __( 'Livestream-Einstellungen', 'minecraft-modern-theme' ), + 'mm_livestream_meta_box_html', + 'mm_livestream', 'normal', 'high' + ); +} +add_action( 'add_meta_boxes', 'mm_video_meta_boxes' ); + +function mm_video_meta_box_html( $post ) { + wp_nonce_field( 'mm_video_save', 'mm_video_nonce' ); + $url = get_post_meta( $post->ID, '_mm_video_url', true ); + $category = get_post_meta( $post->ID, '_mm_video_category', true ); + $post_id = $post->ID; + ?> + + + + + + + + + + + + + +
+ +

+ +

+
+ +

+
+ +
+ + +

+ +

+ +
+ ID, '_mm_livestream_url', true ); + $player_url = get_post_meta( $post->ID, '_mm_livestream_player_url', true ); + $owner = get_post_meta( $post->ID, '_mm_livestream_owner', true ); + ?> + + + + + + + + + + + + + + + + + +
+ +

+ +

+
+ +

+ +

+
+ +

+ +

+
+

+
+ Tag ausgegeben, nicht iframe + } + + return false; +} + +function mm_video_get_type( $url ) { + if ( preg_match( '/youtube\.com|youtu\.be/', $url ) ) return 'youtube'; + if ( strpos( $url, 'vimeo.com' ) !== false ) return 'vimeo'; + if ( strpos( $url, 'twitch.tv' ) !== false ) return 'twitch'; + if ( preg_match( '/\.(mp4|webm|ogv|ogg)(\?.*)?$/i', $url ) ) return 'mp4'; + return 'unknown'; +} + +function mm_twitch_get_channel_from_url( $url ) { + if ( preg_match( '/twitch\.tv\/([a-zA-Z0-9_]+)(?:\/|\?|$)/', (string) $url, $m ) ) { + return strtolower( $m[1] ); + } + + return ''; +} + +function mm_twitch_get_app_token() { + $client_id = get_theme_mod( 'twitch_client_id', '' ); + $client_secret = get_theme_mod( 'twitch_client_secret', '' ); + + if ( empty( $client_id ) || empty( $client_secret ) ) { + return false; + } + + $cache_key = 'mm_twitch_app_token'; + $cached = get_transient( $cache_key ); + if ( ! empty( $cached ) ) { + return $cached; + } + + $response = wp_remote_post( 'https://id.twitch.tv/oauth2/token', array( + 'timeout' => 8, + 'body' => array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'grant_type' => 'client_credentials', + ), + ) ); + + if ( is_wp_error( $response ) ) { + return false; + } + + $data = json_decode( wp_remote_retrieve_body( $response ), true ); + if ( empty( $data['access_token'] ) || empty( $data['expires_in'] ) ) { + return false; + } + + $ttl = max( 60, (int) $data['expires_in'] - 60 ); + set_transient( $cache_key, $data['access_token'], $ttl ); + + return $data['access_token']; +} + +function mm_twitch_is_live( $channel ) { + $channel = sanitize_text_field( (string) $channel ); + if ( $channel === '' ) { + return false; + } + + $cache_key = 'mm_twitch_live_' . $channel; + $cached = get_transient( $cache_key ); + if ( false !== $cached ) { + return $cached === 'live'; + } + + $client_id = get_theme_mod( 'twitch_client_id', '' ); + $token = mm_twitch_get_app_token(); + if ( empty( $client_id ) || empty( $token ) ) { + return false; + } + + $url = add_query_arg( array( + 'user_login' => $channel, + ), 'https://api.twitch.tv/helix/streams' ); + + $response = wp_remote_get( $url, array( + 'timeout' => 8, + 'headers' => array( + 'Client-ID' => $client_id, + 'Authorization' => 'Bearer ' . $token, + ), + ) ); + + if ( is_wp_error( $response ) ) { + return false; + } + + $data = json_decode( wp_remote_retrieve_body( $response ), true ); + $is_live = ! empty( $data['data'][0] ); + set_transient( $cache_key, $is_live ? 'live' : 'offline', 2 * MINUTE_IN_SECONDS ); + + return $is_live; +} + +function mm_video_get_youtube_player_url( $identifier, $mode = 'video' ) { + $params = 'autoplay=0&rel=0&modestbranding=1'; + + if ( $mode === 'channel' ) { + return 'https://www.youtube.com/embed/live_stream?channel=' . rawurlencode( $identifier ) . '&' . $params; + } + + if ( $mode === 'handle-live' ) { + return 'https://www.youtube.com/embed/' . ltrim( $identifier ) . '/live?' . $params; + } + + return 'https://www.youtube.com/embed/' . rawurlencode( $identifier ) . '?' . $params; +} + +function mm_video_get_youtube_handle_from_url( $url ) { + if ( preg_match( '~youtube\.com/(@[A-Za-z0-9._-]+)(?:/.*)?$~i', (string) $url, $matches ) ) { + return sanitize_text_field( $matches[1] ); + } + + return ''; +} + +function mm_video_extract_youtube_live_video_id( $html ) { + $patterns = array( + '/"canonicalBaseUrl":"\\/watch\?v=([A-Za-z0-9_-]{11})"/i', + '/"watchEndpoint":\{"videoId":"([A-Za-z0-9_-]{11})"/i', + '/\/watch\?v=([A-Za-z0-9_-]{11})\\u0026/i', + '/"videoId":"([A-Za-z0-9_-]{11})".{0,600}?"isLiveNow":true/is', + '/"videoId":"([A-Za-z0-9_-]{11})".{0,600}?"style":"LIVE"/is', + '/"videoId":"([A-Za-z0-9_-]{11})".{0,600}?LIVE_NOW/is', + '/https:\/\/www\.youtube\.com\/watch\?v=([A-Za-z0-9_-]{11})/i', + '/ 5 ) ); + if ( ! is_wp_error( $response ) ) { + $data = json_decode( wp_remote_retrieve_body( $response ), true ); + if ( ! empty( $data['items'][0] ) ) { + $state = $data['items'][0]['snippet']['liveBroadcastContent'] ?? ''; + $is_live = ( $state === 'live' ); + set_transient( $cache_key, $is_live ? 'live' : 'offline', 2 * MINUTE_IN_SECONDS ); + return $is_live; + } + } + } + + // Fallback: Wenn keine API, gehen wir davon aus dass die video_id aktuell ist + // weil sie von mm_video_resolve_youtube_live_video_id() kam + set_transient( $cache_key, 'live', 2 * MINUTE_IN_SECONDS ); + return true; +} + +function mm_video_resolve_youtube_live_video_id( $profile_url = '', $youtube_channel_id = '' ) { + $profile_url = trim( (string) $profile_url ); + $youtube_channel_id = trim( (string) $youtube_channel_id ); + $cache_key = 'mm_yt_live_v2_' . md5( strtolower( $profile_url . '|' . $youtube_channel_id ) ); + $cached = get_transient( $cache_key ); + + if ( is_array( $cached ) && array_key_exists( 'video_id', $cached ) ) { + return $cached['video_id']; + } + + $candidate_urls = array(); + $handle = mm_video_get_youtube_handle_from_url( $profile_url ); + + if ( $youtube_channel_id && preg_match( '/^UC[a-zA-Z0-9_-]+$/', $youtube_channel_id ) ) { + $candidate_urls[] = 'https://www.youtube.com/channel/' . rawurlencode( $youtube_channel_id ) . '/live'; + $candidate_urls[] = 'https://www.youtube.com/channel/' . rawurlencode( $youtube_channel_id ) . '/streams'; + } + + if ( $handle ) { + $candidate_urls[] = 'https://www.youtube.com/' . rawurlencode( ltrim( $handle, '/' ) ) . '/live'; + $candidate_urls[] = 'https://www.youtube.com/' . rawurlencode( ltrim( $handle, '/' ) ) . '/streams'; + } + + if ( $profile_url ) { + $candidate_urls[] = untrailingslashit( $profile_url ) . '/live'; + $candidate_urls[] = untrailingslashit( $profile_url ) . '/streams'; + } + + $candidate_urls = array_values( array_unique( array_filter( $candidate_urls ) ) ); + $request_args = array( + 'timeout' => 10, + 'redirection' => 5, + 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + 'headers' => array( + 'Accept-Language' => 'en-US,en;q=0.9,de;q=0.8', + ), + ); + + foreach ( $candidate_urls as $candidate_url ) { + $response = wp_remote_get( $candidate_url, $request_args ); + + if ( is_wp_error( $response ) ) { + continue; + } + + $body = wp_remote_retrieve_body( $response ); + if ( ! is_string( $body ) || $body === '' ) { + continue; + } + + $video_id = mm_video_extract_youtube_live_video_id( $body ); + if ( $video_id ) { + set_transient( $cache_key, array( 'video_id' => $video_id ), 2 * MINUTE_IN_SECONDS ); + return $video_id; + } + } + + set_transient( $cache_key, array( 'video_id' => '' ), 60 ); + return ''; +} + +function mm_video_get_livestream_data( $profile_url = '', $player_url = '', $youtube_channel_id = '' ) { + if ( empty( $profile_url ) && empty( $player_url ) && empty( $youtube_channel_id ) ) { + return false; + } + + $profile_url = trim( $profile_url ); + $player_url = trim( $player_url ); + $youtube_channel_id = trim( $youtube_channel_id ); + $source_url = $player_url ? $player_url : $profile_url; + $parent_host = parse_url( home_url(), PHP_URL_HOST ); + $data = array( + 'profile_url' => esc_url_raw( $profile_url ? $profile_url : $player_url ), + 'platform' => 'unknown', + 'label' => __( 'Livestream', 'minecraft-modern-theme' ), + 'icon' => 'fas fa-tower-broadcast', + 'color' => '#00d4ff', + 'cta' => __( 'Profil öffnen', 'minecraft-modern-theme' ), + 'embed_url' => '', + 'video_id' => '', + 'channel' => '', + 'channel_display' => '', + ); + + if ( ! empty( $youtube_channel_id ) && preg_match( '/^UC[a-zA-Z0-9_-]+$/', $youtube_channel_id ) ) { + $resolved_video_id = mm_video_resolve_youtube_live_video_id( $profile_url, $youtube_channel_id ); + + $data['platform'] = 'youtube'; + $data['label'] = __( 'YouTube Livestream', 'minecraft-modern-theme' ); + $data['icon'] = 'fab fa-youtube'; + $data['color'] = '#ff0000'; + $data['cta'] = __( 'Zum YouTube-Kanal', 'minecraft-modern-theme' ); + $data['channel'] = $youtube_channel_id; + $data['channel_display'] = $youtube_channel_id; + $data['video_id'] = $resolved_video_id; + $data['embed_url'] = $resolved_video_id ? mm_video_get_youtube_player_url( $resolved_video_id, 'video' ) : ''; + + if ( preg_match( '~youtube\.com/(@[A-Za-z0-9._-]+)(?:/.*)?$~i', $profile_url, $handle_matches ) ) { + $data['channel_display'] = sanitize_text_field( $handle_matches[1] ); + } + + if ( empty( $data['profile_url'] ) ) { + $data['profile_url'] = 'https://www.youtube.com/channel/' . rawurlencode( $youtube_channel_id ); + } + + if ( empty( $player_url ) ) { + return $data; + } + } + + if ( empty( $source_url ) ) { + return false; + } + + if ( preg_match( '/youtube\.com|youtu\.be/', $source_url ) && preg_match( '/(?:youtube\.com\/(?:watch\?v=|shorts\/|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/', $source_url ) ) { + $data['platform'] = 'youtube'; + $data['label'] = __( 'YouTube Livestream', 'minecraft-modern-theme' ); + $data['icon'] = 'fab fa-youtube'; + $data['color'] = '#ff0000'; + $data['cta'] = __( 'Zum YouTube-Kanal', 'minecraft-modern-theme' ); + preg_match( '/(?:youtube\.com\/(?:watch\?v=|shorts\/|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/', $source_url, $video_match ); + $data['video_id'] = ! empty( $video_match[1] ) ? $video_match[1] : ''; + $data['embed_url'] = ! empty( $video_match[1] ) ? mm_video_get_youtube_player_url( $video_match[1], 'video' ) : ''; + return $data; + } + + if ( preg_match( '~twitch\.tv/([A-Za-z0-9_]+)(?:/)?(?:\?.*)?$~i', $source_url, $matches ) && strtolower( $matches[1] ) !== 'videos' ) { + $channel = sanitize_key( $matches[1] ); + $data['platform'] = 'twitch'; + $data['label'] = __( 'Twitch Livestream', 'minecraft-modern-theme' ); + $data['icon'] = 'fab fa-twitch'; + $data['color'] = '#9146ff'; + $data['cta'] = __( 'Zum Twitch-Kanal', 'minecraft-modern-theme' ); + $data['channel'] = $channel; + $data['channel_display'] = '@' . $channel; + $data['embed_url'] = 'https://player.twitch.tv/?channel=' . rawurlencode( $channel ) . '&parent=' . rawurlencode( $parent_host ) . '&autoplay=false'; + return $data; + } + + if ( preg_match( '~kick\.com/([A-Za-z0-9_]+)(?:/)?(?:\?.*)?$~i', $source_url, $matches ) ) { + $channel = sanitize_key( $matches[1] ); + $data['platform'] = 'kick'; + $data['label'] = __( 'Kick Livestream', 'minecraft-modern-theme' ); + $data['icon'] = 'fas fa-satellite-dish'; + $data['color'] = '#53fc18'; + $data['cta'] = __( 'Zum Kick-Kanal', 'minecraft-modern-theme' ); + $data['channel'] = $channel; + $data['channel_display'] = '@' . $channel; + $data['embed_url'] = 'https://player.kick.com/' . rawurlencode( $channel ); + return $data; + } + + if ( preg_match( '~youtube\.com/channel/(UC[a-zA-Z0-9_-]+)~i', $source_url, $matches ) ) { + $channel_id = sanitize_text_field( $matches[1] ); + $resolved_video_id = mm_video_resolve_youtube_live_video_id( $source_url, $channel_id ); + $data['platform'] = 'youtube'; + $data['label'] = __( 'YouTube Livestream', 'minecraft-modern-theme' ); + $data['icon'] = 'fab fa-youtube'; + $data['color'] = '#ff0000'; + $data['cta'] = __( 'Zum YouTube-Kanal', 'minecraft-modern-theme' ); + $data['channel'] = $channel_id; + $data['channel_display'] = $channel_id; + $data['video_id'] = $resolved_video_id; + $data['embed_url'] = $resolved_video_id ? mm_video_get_youtube_player_url( $resolved_video_id, 'video' ) : ''; + return $data; + } + + if ( preg_match( '~youtube\.com/(@[A-Za-z0-9._-]+)(?:/.*)?$~i', $source_url, $matches ) ) { + $handle = sanitize_text_field( $matches[1] ); + $resolved_video_id = mm_video_resolve_youtube_live_video_id( $source_url, '' ); + $data['platform'] = 'youtube'; + $data['label'] = __( 'YouTube Livestream', 'minecraft-modern-theme' ); + $data['icon'] = 'fab fa-youtube'; + $data['color'] = '#ff0000'; + $data['cta'] = __( 'Zum YouTube-Kanal', 'minecraft-modern-theme' ); + $data['channel'] = ltrim( $handle, '@' ); + $data['channel_display'] = $handle; + $data['video_id'] = $resolved_video_id; + $data['embed_url'] = $resolved_video_id ? mm_video_get_youtube_player_url( $resolved_video_id, 'video' ) : ''; + return $data; + } + + if ( preg_match( '~youtube\.com/(?:c|user)/([A-Za-z0-9._-]+)(?:/.*)?$~i', $source_url, $matches ) ) { + $channel_name = sanitize_text_field( $matches[1] ); + $resolved_video_id = mm_video_resolve_youtube_live_video_id( $source_url, '' ); + $data['platform'] = 'youtube'; + $data['label'] = __( 'YouTube Livestream', 'minecraft-modern-theme' ); + $data['icon'] = 'fab fa-youtube'; + $data['color'] = '#ff0000'; + $data['cta'] = __( 'Zum YouTube-Kanal', 'minecraft-modern-theme' ); + $data['channel'] = $channel_name; + $data['channel_display'] = '@' . ltrim( $channel_name, '@' ); + $data['video_id'] = $resolved_video_id; + $data['embed_url'] = $resolved_video_id ? mm_video_get_youtube_player_url( $resolved_video_id, 'video' ) : ''; + return $data; + } + + if ( strpos( $source_url, 'youtube.com' ) !== false || strpos( $source_url, 'youtu.be' ) !== false ) { + $data['platform'] = 'youtube'; + $data['label'] = __( 'YouTube Livestream', 'minecraft-modern-theme' ); + $data['icon'] = 'fab fa-youtube'; + $data['color'] = '#ff0000'; + $data['cta'] = __( 'Zum YouTube-Kanal', 'minecraft-modern-theme' ); + } + + return $data; +} + +function mm_video_get_livestream_item( $post_id ) { + $stream_url = get_post_meta( $post_id, '_mm_livestream_url', true ); + $player_url = get_post_meta( $post_id, '_mm_livestream_player_url', true ); + $owner_meta = trim( get_post_meta( $post_id, '_mm_livestream_owner', true ) ); + $youtube_channel_id = get_post_meta( $post_id, '_mm_livestream_youtube_channel_id', true ); + $stream = mm_video_get_livestream_data( $stream_url, $player_url, $youtube_channel_id ); + + if ( ! $stream || empty( $stream['profile_url'] ) ) { + return false; + } + + $stream_title = trim( get_the_title( $post_id ) ); + if ( empty( $stream_title ) || $stream_title === '(kein Titel)' || stripos( $stream_title, 'Automatisch gespeicherter Entwurf' ) !== false || stripos( $stream_title, 'Auto Draft' ) !== false ) { + $stream_title = ! empty( $stream['channel_display'] ) ? $stream['channel_display'] : $stream['label']; + } + + $stream_excerpt = has_excerpt( $post_id ) ? get_the_excerpt( $post_id ) : ''; + $owner = $owner_meta; + if ( empty( $owner ) ) { + $owner = ! empty( $stream['channel_display'] ) ? $stream['channel_display'] : $stream_title; + } + + return array( + 'post_id' => $post_id, + 'owner' => $owner, + 'title' => $stream_title, + 'description' => $stream_excerpt, + 'profile_url' => $stream['profile_url'], + 'channel' => $stream['channel'], + 'stream' => $stream, + ); +} + +/** + * Automatische YouTube-Live-Erkennung via @Handle (Optimiert) + */ + +/** + * 1. Hilfsfunktion: Wandelt @Handle in eine Channel-ID um + */ +function mm_get_channel_id_by_handle( $handle ) { + $api_key = get_theme_mod( 'youtube_api_key' ); + if ( empty( $api_key ) || empty( $handle ) ) { + return false; + } + + // Handle normalisieren (ohne @) + $handle = ltrim( $handle, '@' ); + + // Cache für die Channel-ID (diese ändert sich nie, daher 30 Tage speichern) + $cache_key = 'mm_id_for_' . $handle; + $channel_id = get_transient( $cache_key ); + if ( false !== $channel_id ) { + return $channel_id; + } + + // Suche Kanal zum Handle + $url = 'https://www.googleapis.com/youtube/v3/search?part=snippet&q=' . urlencode( '@' . $handle ) . '&type=channel&maxResults=1&key=' . $api_key; + + $response = wp_remote_get( $url ); + if ( is_wp_error( $response ) ) { + return false; + } + + $data = json_decode( wp_remote_retrieve_body( $response ) ); + + if ( ! empty( $data->items[0]->snippet->channelId ) ) { + $id = $data->items[0]->snippet->channelId; + set_transient( $cache_key, $id, DAY_IN_SECONDS * 30 ); + return $id; + } + + return false; +} + +/** + * 2. Hauptfunktion: Findet die Video-ID des aktuellen Livestreams + */ +function mm_get_youtube_live_id_from_handle( $handle ) { + $api_key = get_theme_mod( 'youtube_api_key' ); + if ( empty( $api_key ) ) { + return false; + } + + // 1. Kanal-ID zum Handle finden + $channel_id = mm_get_channel_id_by_handle( $handle ); + if ( ! $channel_id ) { + return false; + } + + // 2. Aktuellen Livestream in diesem Kanal suchen + $cache_key_live = 'mm_live_status_' . $channel_id; + $live_id = get_transient( $cache_key_live ); + if ( false !== $live_id ) { + return ( $live_id === 'none' ) ? false : $live_id; + } + + // Wir suchen direkt nach dem Live-Event des Kanals + $url = 'https://www.googleapis.com/youtube/v3/search?part=id&channelId=' . $channel_id . '&eventType=live&type=video&key=' . $api_key; + + $response = wp_remote_get( $url ); + if ( is_wp_error( $response ) ) { + return false; + } + + $data = json_decode( wp_remote_retrieve_body( $response ) ); + + if ( ! empty( $data->items[0]->id->videoId ) ) { + $video_id = $data->items[0]->id->videoId; + set_transient( $cache_key_live, $video_id, 2 * MINUTE_IN_SECONDS ); // Nur 2 Min für schnellere Reaktion + return $video_id; + } + + // Wenn nicht live, 'none' speichern, um API-Anfragen zu drosseln + set_transient( $cache_key_live, 'none', 2 * MINUTE_IN_SECONDS ); + return false; +} + +/** + * 3. Update für mm_video_get_livestream_groups - Hybrid-System + * Zeigt ALLE konfigurierten Livestreams an: + * - Livestream-Posts (Hauptmethode für mehrere Kanäle) + * - Optional: Customizer @Handle (für einzelnen Hauptkanal) + */ +function mm_video_get_livestream_groups() { + $groups = array(); + $api_key = get_theme_mod( 'youtube_api_key', '' ); + + // === METHODE 1: Livestream-Posts (IMMER abfragen) === + $posts = get_posts( array( + 'post_type' => 'mm_livestream', + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'orderby' => 'ID', + 'order' => 'ASC', + ) ); + + if ( ! empty( $posts ) ) { + foreach ( $posts as $post ) { + $profile_url = get_post_meta( $post->ID, '_mm_livestream_url', true ); + $stream_url = get_post_meta( $post->ID, '_mm_livestream_player_url', true ); + $owner = get_post_meta( $post->ID, '_mm_livestream_owner', true ); + $channel_id = ''; + + // Versuche Channel-ID aus Profil-URL zu extrahieren (nur für YouTube) + if ( ! empty( $profile_url ) && strpos( $profile_url, 'youtube.com' ) !== false ) { + // Channel-URL mit UC-ID + if ( preg_match( '~/channel/(UC[a-zA-Z0-9_-]+)~', $profile_url, $m ) ) { + $channel_id = $m[1]; + } + // @Handle -> über API auflösen + elseif ( preg_match( '~/@([a-zA-Z0-9_.-]+)(?:/|$)~', $profile_url, $m ) && ! empty( $api_key ) ) { + $channel_id = mm_get_channel_id_by_handle( $m[1] ); + } + } + + // YouTube-Livestream: Prüfe ob live via API + if ( ! empty( $channel_id ) && ! empty( $api_key ) ) { + $cache_key_live = 'mm_live_status_' . $channel_id; + $live_id = get_transient( $cache_key_live ); + + if ( false === $live_id ) { + $search_url = add_query_arg( array( + 'part' => 'id', + 'channelId' => $channel_id, + 'eventType' => 'live', + 'type' => 'video', + 'key' => $api_key, + ), 'https://www.googleapis.com/youtube/v3/search' ); + + $response = wp_remote_get( $search_url, array( 'timeout' => 10 ) ); + + if ( ! is_wp_error( $response ) ) { + $data = json_decode( wp_remote_retrieve_body( $response ), true ); + + if ( isset( $data['items'][0]['id']['videoId'] ) ) { + $live_id = $data['items'][0]['id']['videoId']; + set_transient( $cache_key_live, $live_id, 2 * MINUTE_IN_SECONDS ); + } else { + set_transient( $cache_key_live, 'none', 2 * MINUTE_IN_SECONDS ); + $live_id = false; + } + } else { + $live_id = false; + } + } elseif ( $live_id === 'none' ) { + $live_id = false; + } + + // Nur hinzufügen wenn live + if ( $live_id && $live_id !== 'none' ) { + $groups[] = array( + 'title' => $post->post_title, + 'platform' => 'youtube', + 'yt_id' => $live_id, + 'handle' => $profile_url, + ); + } + } + // Andere Plattformen (Twitch, etc.): Prüfe zuerst direkte Stream-URL, sonst Profil-URL + elseif ( ! empty( $stream_url ) || ! empty( $profile_url ) ) { + $use_url = ! empty( $stream_url ) ? $stream_url : $profile_url; + $embed_url = mm_video_get_embed_url( $use_url ); + + // Nur Live-Streams anzeigen (Twitch muss live sein) + if ( $embed_url ) { + $platform = mm_video_get_type( $use_url ); + + if ( $platform !== 'twitch' ) { + continue; + } + + $channel = mm_twitch_get_channel_from_url( $use_url ); + if ( ! mm_twitch_is_live( $channel ) ) { + continue; + } + + $groups[] = array( + 'title' => $post->post_title, + 'platform' => $platform, + 'embed_url' => $embed_url, + 'profile_url' => $profile_url, + 'owner' => $owner, + ); + } + } + } + } + + return $groups; +} + +/** + * Debug-Informationen für Livestream-System + */ +function mm_video_get_api_livestream_debug() { + if ( ! is_user_logged_in() || ! current_user_can( 'edit_posts' ) ) { + return array(); + } + + $api_key = get_theme_mod( 'youtube_api_key', '' ); + + // Zähle Livestream-Posts + $posts_count = wp_count_posts( 'mm_livestream' ); + $published_posts = isset( $posts_count->publish ) ? $posts_count->publish : 0; + + // === Fall 1: Keine Konfiguration === + if ( $published_posts === 0 ) { + return array( + 'status' => 'error', + 'message' => 'Keine Livestream-Posts gefunden', + 'hint' => 'Erstelle Livestream-Posts über "Livestreams → Neu"', + ); + } + + // === Fall 2: API Key fehlt (nur Warnung) === + if ( empty( $api_key ) ) { + return array( + 'status' => 'warning', + 'message' => 'Kein YouTube API Key', + 'hint' => 'API Key wird für YouTube Live-Erkennung benötigt. Twitch-Streams funktionieren ohne.', + 'posts_count' => $published_posts, + ); + } + + // === Fall 3: Alles OK === + return array( + 'status' => 'ok', + 'posts_count' => $published_posts, + 'api_key_length' => strlen( $api_key ), + ); +} + +function mm_video_get_hidden_livestream_count() { + // Für API-basiertes System nicht relevant + return 0; +} + +function mm_video_get_livestream_debug_rows() { + // Ersetzt durch mm_video_get_api_livestream_debug() + return array(); +} + +function mm_video_render_livestream_group( $group, $index = 0 ) { + // Format 1: Neues API-YouTube-Format mit yt_id (Customizer oder Posts) + if ( isset( $group['yt_id'] ) && isset( $group['platform'] ) && $group['platform'] === 'youtube' ) { + $video_id = trim( (string) $group['yt_id'] ); + $title = isset( $group['title'] ) ? $group['title'] : __( 'Live', 'minecraft-modern-theme' ); + $handle = isset( $group['handle'] ) ? trim( (string) $group['handle'] ) : ''; + + if ( empty( $video_id ) ) { + return ''; + } + + $embed_url = 'https://www.youtube-nocookie.com/embed/' . rawurlencode( $video_id ) . '?autoplay=0&rel=0&modestbranding=1&playsinline=1'; + + ob_start(); + ?> +
+
+ +
+
+ + + + + + + + + + + +
+
+ array( 'icon' => 'fab fa-twitch', 'color' => '#9146ff' ), + 'vimeo' => array( 'icon' => 'fab fa-vimeo-v', 'color' => '#1ab7ea' ), + 'youtube' => array( 'icon' => 'fab fa-youtube', 'color' => '#ff0000' ), + ); + $icon = isset( $platform_icons[$platform] ) ? $platform_icons[$platform]['icon'] : 'fas fa-play'; + + ob_start(); + ?> +
+
+ +
+
+ + + + + + + + + + + +
+
+ 1 + ? __( 'Mehrere aktive oder gespeicherte Streams dieses Streamers lassen sich hier direkt umschalten.', 'minecraft-modern-theme' ) + : __( 'Der Bereich wird automatisch aus deinem Profil-Link erzeugt und oberhalb der Videos eingebunden.', 'minecraft-modern-theme' ) ); + + ob_start(); + ?> +
+
+ + + + +

+

+ 1 ) : ?> +
+ + 1 / + +
+
+ $item ) : ?> + + +
+ +
+ + + + + + + +
+

+
+
+ +
+ +
+ + + +
+ +

+
+ +
+
+ ' . __('Ungültige Video-URL.', 'minecraft-modern-theme') . '

'; + + $type = mm_video_get_type( $url ); + $title = isset($args['title']) ? esc_attr($args['title']) : __('Video', 'minecraft-modern-theme'); + + if ( $type === 'mp4' ) { + return '
' + . '
'; + } + + return '
' + . '
'; +} + + +// --- 5. Shortcode: [mm_video url="..."] --- +// Beispiele: +// [mm_video url="https://www.youtube.com/watch?v=dQw4w9WgXcQ"] +// [mm_video url="https://vimeo.com/123456789" title="Mein Video"] +function mm_video_shortcode( $atts ) { + $a = shortcode_atts( array( + 'url' => '', + 'title' => __('Video', 'minecraft-modern-theme'), + ), $atts ); + + if ( empty($a['url']) ) return ''; + return mm_video_render_embed( $a['url'], $a ); +} +add_shortcode( 'mm_video', 'mm_video_shortcode' ); + + +// --- 6. Template-Loader für Video-Archiv und Galerie-Seite --- +function mm_video_template_loader( $template ) { + // BUG-FIX: locate_template() berücksichtigt ein archive-video.php im Child-Theme, + // get_template_directory() hätte immer das Parent-Template erzwungen. + $is_video_page = false; + + // Archiv (domain.de/videos/) + if ( is_post_type_archive('mm_video') ) { + $is_video_page = true; + } elseif ( is_page() ) { + // Seite mit dem Slug "videos" + $obj = get_queried_object(); + $is_video_page = ( $obj && $obj->post_name === 'videos' ); + } + + if ( $is_video_page ) { + $t = locate_template( 'archive-video.php' ); + if ( $t ) return $t; + } + return $template; +} +add_filter( 'template_include', 'mm_video_template_loader' ); + + +// --- 7. "Videos"-Seite automatisch anlegen --- +function mm_video_create_page() { + if ( ! get_page_by_path('videos') ) { + wp_insert_post( array( + 'post_title' => __('Videos', 'minecraft-modern-theme'), + 'post_name' => 'videos', + 'post_status' => 'publish', + 'post_type' => 'page', + 'post_author' => 1, + ) ); + } +} +add_action( 'after_switch_theme', 'mm_video_create_page' ); +// Auch beim Speichern im Customizer anlegen +add_action( 'customize_save_after', 'mm_video_create_page' ); diff --git a/Minecraft-Modern-Theme/inc/widgets.php b/Minecraft-Modern-Theme/inc/widgets.php new file mode 100644 index 0000000..9667b0c --- /dev/null +++ b/Minecraft-Modern-Theme/inc/widgets.php @@ -0,0 +1,129 @@ + __( 'Footer Links', 'minecraft-modern-theme' ), + 'id' => 'footer-left', + 'description' => __( 'Widget-Bereich links im Footer.', 'minecraft-modern-theme' ), + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ) ); + for ( $i = 1; $i <= 3; $i++ ) { + register_sidebar( array( + 'name' => sprintf( __( 'Footer Spalte %d', 'minecraft-modern-theme' ), $i ), + 'id' => 'footer-' . $i, + 'description' => sprintf( __( 'Widget für die %d. Spalte im Footer.', 'minecraft-modern-theme' ), $i ), + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ) ); + } + register_sidebar( array( + 'name' => __( 'Footer Rechts', 'minecraft-modern-theme' ), + 'id' => 'footer-right', + 'description' => __( 'Widget-Bereich rechts im Footer.', 'minecraft-modern-theme' ), + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ) ); +} +add_action( 'widgets_init', 'minecraft_modern_footer_widgets' ); + + +// === Homepage Sidebar registrieren === +function minecraft_modern_homepage_sidebar() { + register_sidebar( array( + 'name' => __( 'Startseiten Sidebar - Oben', 'minecraft-modern-theme' ), + 'id' => 'homepage-sidebar-top', + 'description' => __( 'Widget-Bereich oben in der Sidebar (z.B. für wichtige Infos).', 'minecraft-modern-theme' ), + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ) ); + register_sidebar( array( + 'name' => __( 'Startseiten Sidebar - Mitte 1', 'minecraft-modern-theme' ), + 'id' => 'homepage-sidebar-middle-1', + 'description' => __( 'Widget-Bereich in der Mitte der Sidebar.', 'minecraft-modern-theme' ), + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ) ); + register_sidebar( array( + 'name' => __( 'Startseiten Sidebar - Mitte 2', 'minecraft-modern-theme' ), + 'id' => 'homepage-sidebar-middle-2', + 'description' => __( 'Zweiter Widget-Bereich in der Mitte der Sidebar.', 'minecraft-modern-theme' ), + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ) ); + register_sidebar( array( + 'name' => __( 'Startseiten Sidebar - Unten', 'minecraft-modern-theme' ), + 'id' => 'homepage-sidebar-bottom', + 'description' => __( 'Widget-Bereich unten in der Sidebar.', 'minecraft-modern-theme' ), + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ) ); + register_sidebar( array( + 'name' => __( 'Startseiten Sidebar - Extra', 'minecraft-modern-theme' ), + 'id' => 'homepage-sidebar-extra', + 'description' => __( 'Zusätzlicher Widget-Bereich für spezielle Inhalte.', 'minecraft-modern-theme' ), + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ) ); +} +add_action( 'widgets_init', 'minecraft_modern_homepage_sidebar' ); + + +/** + * Rendert alle Sidebar-Sections der Homepage. + * Ersetzt den doppelten Sidebar-Code in front-page.php. + */ +function minecraft_modern_render_sidebar_sections() { + $sections = array( + 'homepage-sidebar-top', + 'homepage-sidebar-middle-1', + 'homepage-sidebar-middle-2', + 'homepage-sidebar-bottom', + 'homepage-sidebar-extra', + ); + $classes = array( + 'homepage-sidebar-top' => 'sidebar-top', + 'homepage-sidebar-middle-1' => 'sidebar-middle-1', + 'homepage-sidebar-middle-2' => 'sidebar-middle-2', + 'homepage-sidebar-bottom' => 'sidebar-bottom', + 'homepage-sidebar-extra' => 'sidebar-extra', + ); + foreach ( $sections as $sidebar_id ) { + if ( is_active_sidebar( $sidebar_id ) ) { + $class = isset( $classes[ $sidebar_id ] ) ? ' ' . $classes[ $sidebar_id ] : ''; + echo ''; + } + } +} diff --git a/Minecraft-Modern-Theme/js/announcement.js b/Minecraft-Modern-Theme/js/announcement.js index 5836d1f..9b09ad3 100644 --- a/Minecraft-Modern-Theme/js/announcement.js +++ b/Minecraft-Modern-Theme/js/announcement.js @@ -1,76 +1,76 @@ -document.addEventListener("DOMContentLoaded", function () { - const bar = document.getElementById("mm-announcement"); - if (!bar) return; - - // --- 1. Positionierung --- - const position = bar.dataset.position || 'below-header'; - - if (position === "top") { - document.body.insertAdjacentElement('afterbegin', bar); - } else if (position === "below-slider") { - const anchor = document.getElementById("mm-announcement-anchor"); - if (anchor) { - anchor.replaceWith(bar); - } else { - const header = document.getElementById("masthead"); - if (header) header.insertAdjacentElement('afterend', bar); - } - } else { - // "below-header" und alle anderen Positionen - const header = document.getElementById("masthead"); - if (header) header.insertAdjacentElement('afterend', bar); - } - - // --- 2. Schließen-Button --- - const closeBtn = bar.querySelector(".mm-announcement-close"); - if (closeBtn) { - closeBtn.addEventListener("click", function () { - bar.style.transition = 'opacity 0.3s ease'; - bar.style.opacity = '0'; - setTimeout(function() { bar.style.display = "none"; }, 300); - }); - } - - // --- 3. Countdown Timer --- - const timerElement = document.querySelector('.mm-countdown-timer'); - - if (timerElement) { - const targetDateString = timerElement.getAttribute('data-date'); - const expiredMessage = timerElement.getAttribute('data-expired') || 'Abgelaufen'; - - if (!targetDateString) { - timerElement.style.display = 'none'; - return; - } - - const countDownDate = new Date(targetDateString).getTime(); - - // FIX: Tick-Funktion auslagern und SOFORT aufrufen – kein 1s-Flackern beim Laden - function tick() { - const now = new Date().getTime(); - const distance = countDownDate - now; - - if (distance < 0) { - clearInterval(timerInterval); - timerElement.innerHTML = expiredMessage; - timerElement.style.color = '#ff3333'; - return; - } - - const days = Math.floor(distance / (1000 * 60 * 60 * 24)); - const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((distance % (1000 * 60)) / 1000); - - const fDays = days > 0 ? days + 'd ' : ''; - const fHours = String(hours).padStart(2, '0'); - const fMinutes = String(minutes).padStart(2, '0'); - const fSeconds = String(seconds).padStart(2, '0'); - - timerElement.innerHTML = fDays + fHours + ':' + fMinutes + ':' + fSeconds; - } - - tick(); // Sofort ausführen - const timerInterval = setInterval(tick, 1000); - } +document.addEventListener("DOMContentLoaded", function () { + const bar = document.getElementById("mm-announcement"); + if (!bar) return; + + // --- 1. Positionierung --- + const position = bar.dataset.position || 'below-header'; + + if (position === "top") { + document.body.insertAdjacentElement('afterbegin', bar); + } else if (position === "below-slider") { + const anchor = document.getElementById("mm-announcement-anchor"); + if (anchor) { + anchor.replaceWith(bar); + } else { + const header = document.getElementById("masthead"); + if (header) header.insertAdjacentElement('afterend', bar); + } + } else { + // "below-header" und alle anderen Positionen + const header = document.getElementById("masthead"); + if (header) header.insertAdjacentElement('afterend', bar); + } + + // --- 2. Schließen-Button --- + const closeBtn = bar.querySelector(".mm-announcement-close"); + if (closeBtn) { + closeBtn.addEventListener("click", function () { + bar.style.transition = 'opacity 0.3s ease'; + bar.style.opacity = '0'; + setTimeout(function() { bar.style.display = "none"; }, 300); + }); + } + + // --- 3. Countdown Timer --- + const timerElement = document.querySelector('.mm-countdown-timer'); + + if (timerElement) { + const targetDateString = timerElement.getAttribute('data-date'); + const expiredMessage = timerElement.getAttribute('data-expired') || 'Abgelaufen'; + + if (!targetDateString) { + timerElement.style.display = 'none'; + return; + } + + const countDownDate = new Date(targetDateString).getTime(); + + // FIX: Tick-Funktion auslagern und SOFORT aufrufen – kein 1s-Flackern beim Laden + function tick() { + const now = new Date().getTime(); + const distance = countDownDate - now; + + if (distance < 0) { + clearInterval(timerInterval); + timerElement.innerHTML = expiredMessage; + timerElement.style.color = '#ff3333'; + return; + } + + const days = Math.floor(distance / (1000 * 60 * 60 * 24)); + const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((distance % (1000 * 60)) / 1000); + + const fDays = days > 0 ? days + 'd ' : ''; + const fHours = String(hours).padStart(2, '0'); + const fMinutes = String(minutes).padStart(2, '0'); + const fSeconds = String(seconds).padStart(2, '0'); + + timerElement.innerHTML = fDays + fHours + ':' + fMinutes + ':' + fSeconds; + } + + tick(); // Sofort ausführen + const timerInterval = setInterval(tick, 1000); + } }); \ No newline at end of file diff --git a/Minecraft-Modern-Theme/js/assistant-widget.js b/Minecraft-Modern-Theme/js/assistant-widget.js index 8b13789..d3f5a12 100644 --- a/Minecraft-Modern-Theme/js/assistant-widget.js +++ b/Minecraft-Modern-Theme/js/assistant-widget.js @@ -1 +1 @@ - + diff --git a/Minecraft-Modern-Theme/js/menu-links-admin.js b/Minecraft-Modern-Theme/js/menu-links-admin.js new file mode 100644 index 0000000..6b2dea4 --- /dev/null +++ b/Minecraft-Modern-Theme/js/menu-links-admin.js @@ -0,0 +1,116 @@ +/** + * Verwaltungsseite "Menü-Links": Zeilen hinzufügen, entfernen, + * Icon-Vorschau aktualisieren. + */ +( function () { + 'use strict'; + + /** Feldnamen durchnummerieren, damit PHP ein lückenloses Array erhält. */ + function neuNummerieren( liste ) { + var feld = liste.getAttribute( 'data-mm-liste' ); + if ( ! feld ) { + return; + } + var muster = new RegExp( feld.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ) + '\\[[^\\]]*\\]' ); + + liste.querySelectorAll( ':scope > .mm-liste-zeile' ).forEach( function ( zeile, i ) { + zeile.querySelectorAll( '[name^="' + feld + '["]' ).forEach( function ( eingabe ) { + eingabe.name = eingabe.name.replace( muster, feld + '[' + i + ']' ); + } ); + } ); + } + + /** Vorschausymbol links in der Zeile an das Icon-Feld angleichen. */ + function vorschauAktualisieren( zeile ) { + var feld = zeile.querySelector( '.mm-icon-feld' ); + var bild = zeile.querySelector( '.mm-vorschau' ); + if ( ! feld || ! bild ) { + return; + } + var klasse = feld.value.trim() || 'dashicons-admin-links'; + bild.className = 'mm-vorschau dashicons ' + klasse; + } + + // --- Zeile hinzufügen --- + document.addEventListener( 'click', function ( e ) { + var knopf = e.target.closest( '.mm-liste-hinzufuegen' ); + if ( ! knopf ) { + return; + } + e.preventDefault(); + + var feld = knopf.getAttribute( 'data-ziel' ); + var liste = document.querySelector( '.mm-liste[data-mm-liste="' + feld + '"]' ); + var vorlage = document.querySelector( '.mm-liste-vorlage[data-fuer="' + feld + '"]' ); + if ( ! liste || ! vorlage ) { + return; + } + + var huelle = document.createElement( 'div' ); + huelle.innerHTML = vorlage.innerHTML.replace( /__INDEX__/g, String( Date.now() ) ).trim(); + + var zeile = huelle.querySelector( '.mm-liste-zeile' ); + if ( ! zeile ) { + return; + } + + liste.appendChild( zeile ); + neuNummerieren( liste ); + + var hinweis = document.querySelector( '.mm-leer' ); + if ( hinweis ) { + hinweis.remove(); + } + + var erstes = zeile.querySelector( 'input' ); + if ( erstes ) { + erstes.focus(); + } + } ); + + // --- Zeile entfernen --- + document.addEventListener( 'click', function ( e ) { + var knopf = e.target.closest( '.mm-liste-entfernen' ); + if ( ! knopf ) { + return; + } + e.preventDefault(); + + var zeile = knopf.closest( '.mm-liste-zeile' ); + var liste = zeile ? zeile.closest( '.mm-liste' ) : null; + if ( ! zeile || ! liste ) { + return; + } + zeile.remove(); + neuNummerieren( liste ); + } ); + + // --- Icon-Vorschau --- + document.addEventListener( 'input', function ( e ) { + if ( e.target.classList.contains( 'mm-icon-feld' ) ) { + var zeile = e.target.closest( '.mm-liste-zeile' ); + if ( zeile ) { + vorschauAktualisieren( zeile ); + } + } + } ); + + // Klick auf ein Beispielsymbol trägt die Klasse in die letzte Zeile ein + document.addEventListener( 'click', function ( e ) { + var code = e.target.closest( '.mm-icon-liste code' ); + if ( ! code ) { + return; + } + var zeilen = document.querySelectorAll( '.mm-liste-zeile' ); + if ( ! zeilen.length ) { + return; + } + var letzte = zeilen[ zeilen.length - 1 ]; + var feld = letzte.querySelector( '.mm-icon-feld' ); + if ( feld ) { + feld.value = code.textContent.trim(); + vorschauAktualisieren( letzte ); + feld.focus(); + } + } ); +} )(); diff --git a/Minecraft-Modern-Theme/js/minecraft-avatar-skinview.js b/Minecraft-Modern-Theme/js/minecraft-avatar-skinview.js index 1d098c6..8a77460 100644 --- a/Minecraft-Modern-Theme/js/minecraft-avatar-skinview.js +++ b/Minecraft-Modern-Theme/js/minecraft-avatar-skinview.js @@ -1,63 +1,63 @@ -// Lädt skinview3d von CDN -(function(){ - if(document.getElementById('skinview3d-cdn')) return; - var s = document.createElement('script'); - s.id = 'skinview3d-cdn'; - s.src = 'https://unpkg.com/skinview3d@4.1.1/bundles/skinview3d.min.js'; - s.onload = function() { - window.skinview3dReady = true; - }; - document.head.appendChild(s); -})(); - -window.showMinecraftSkinModal = function(uuid) { - if(document.getElementById('minecraft-skin-modal')) return; - var modal = document.createElement('div'); - modal.id = 'minecraft-skin-modal'; - modal.innerHTML = ` -
-
- -
-
- `; - document.body.appendChild(modal); - document.querySelector('.sv3d-modal-close').onclick = function(){ modal.remove(); }; - document.querySelector('.sv3d-modal-bg').onclick = function(){ modal.remove(); }; - - function renderSkin() { - var skinUrl = `https://crafatar.com/skins/${uuid}`; - var canvas = document.createElement('canvas'); - canvas.width = 320; canvas.height = 320; - document.getElementById('sv3d-canvas-wrap').appendChild(canvas); - var viewer = new skinview3d.SkinViewer({ - canvas: canvas, - width: 320, - height: 320, - skin: skinUrl - }); - viewer.controls.enableZoom = false; - viewer.animation = new skinview3d.WalkingAnimation(); - viewer.animation.speed = 1.2; - viewer.animation.play(); - } - if(window.skinview3dReady) renderSkin(); - else { - var check = setInterval(function(){ - if(window.skinview3dReady) { clearInterval(check); renderSkin(); } - }, 100); - } -}; - -// Avatar-Widget-Click-Handler -window.addEventListener('DOMContentLoaded', function(){ - var widget = document.getElementById('minecraft-avatar-widget'); - if(widget) { - widget.style.cursor = 'pointer'; - widget.title = 'Klicke für 3D Skin-Ansicht'; - widget.onclick = function(){ - var uuid = widget.getAttribute('data-uuid'); - if(uuid) window.showMinecraftSkinModal(uuid); - }; - } -}); +// Lädt skinview3d von CDN +(function(){ + if(document.getElementById('skinview3d-cdn')) return; + var s = document.createElement('script'); + s.id = 'skinview3d-cdn'; + s.src = 'https://unpkg.com/skinview3d@4.1.1/bundles/skinview3d.min.js'; + s.onload = function() { + window.skinview3dReady = true; + }; + document.head.appendChild(s); +})(); + +window.showMinecraftSkinModal = function(uuid) { + if(document.getElementById('minecraft-skin-modal')) return; + var modal = document.createElement('div'); + modal.id = 'minecraft-skin-modal'; + modal.innerHTML = ` +
+
+ +
+
+ `; + document.body.appendChild(modal); + document.querySelector('.sv3d-modal-close').onclick = function(){ modal.remove(); }; + document.querySelector('.sv3d-modal-bg').onclick = function(){ modal.remove(); }; + + function renderSkin() { + var skinUrl = `https://crafatar.com/skins/${uuid}`; + var canvas = document.createElement('canvas'); + canvas.width = 320; canvas.height = 320; + document.getElementById('sv3d-canvas-wrap').appendChild(canvas); + var viewer = new skinview3d.SkinViewer({ + canvas: canvas, + width: 320, + height: 320, + skin: skinUrl + }); + viewer.controls.enableZoom = false; + viewer.animation = new skinview3d.WalkingAnimation(); + viewer.animation.speed = 1.2; + viewer.animation.play(); + } + if(window.skinview3dReady) renderSkin(); + else { + var check = setInterval(function(){ + if(window.skinview3dReady) { clearInterval(check); renderSkin(); } + }, 100); + } +}; + +// Avatar-Widget-Click-Handler +window.addEventListener('DOMContentLoaded', function(){ + var widget = document.getElementById('minecraft-avatar-widget'); + if(widget) { + widget.style.cursor = 'pointer'; + widget.title = 'Klicke für 3D Skin-Ansicht'; + widget.onclick = function(){ + var uuid = widget.getAttribute('data-uuid'); + if(uuid) window.showMinecraftSkinModal(uuid); + }; + } +});