feat: UI-feinschliff, scan-vorlagenerkennung, einmal-laden, DE/EN-sprachen

UI:
- Ein-/Ausklappen jetzt mit grossem +/- Icon statt kleinem Pfeil.
- "Entfernen" ist ein Papierkorb-Symbol (dashicon).
- Aktiver Tab klar gekennzeichnet (Akzent-Unterstrich + Farbe).
- 20px Abstand zwischen Tabs und Inhalt.

Funktionen:
- Scan erkennt Anbieter, fuer die es eine Vorlage gibt ("Vorlage verfuegbar"),
  und "Vorlage uebernehmen" fuellt die komplette Vorlage statt nur Host/Pattern.
- Platzhalter: Checkbox "Diesen Dienst kuenftig immer laden" (Standard AN).
  Abgewaehlt -> Inhalt wird nur einmal geladen, keine dauerhafte Einwilligung.

i18n:
- Sprachumschaltung: Deutsch fuer alle de_* Locales, Englisch fuer alle anderen
  (plugin_locale-Filter). Vollstaendige englische Uebersetzung (126 Strings,
  inkl. Vorlagentexte/Empfaenger) als gdpr-content-blocker-en_US.po/.mo.
- Helper-Skripte (extract/build) in hilfsdaten/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-06-07 15:06:16 +02:00
parent ecb5e1bd22
commit 3c37bf63cc
10 changed files with 752 additions and 16 deletions

View File

@@ -244,6 +244,10 @@ class CB_Renderer {
$name
)
. '</button>'
. '<label class="cb-blocker__remember">'
. '<input type="checkbox" class="cb-blocker__remember-cb" checked> '
. esc_html__( 'Diesen Dienst künftig immer laden', 'gdpr-content-blocker' )
. '</label>'
. '</div>'
. '</div>';
}

View File

@@ -137,6 +137,7 @@ class CB_Settings {
return;
}
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_style( 'dashicons' );
wp_enqueue_script(
'cb-admin',
CB_URL . 'assets/admin.js',
@@ -164,6 +165,8 @@ class CB_Settings {
'thirdParty' => __( 'Drittanbieter', 'gdpr-content-blocker' ),
'firstParty' => __( 'eigene Domain', 'gdpr-content-blocker' ),
'addService' => __( 'Als Dienst übernehmen', 'gdpr-content-blocker' ),
'useTemplate' => __( 'Vorlage übernehmen', 'gdpr-content-blocker' ),
'templateAvail' => __( 'Vorlage verfügbar', 'gdpr-content-blocker' ),
'scannedPages'=> __( 'Gescannte Seiten:', 'gdpr-content-blocker' ),
'foundOn' => __( 'Gefunden auf', 'gdpr-content-blocker' ),
],
@@ -180,7 +183,7 @@ class CB_Settings {
'id' => 'google-maps',
'name' => 'Google Maps',
'match_pattern' => 'google.com/maps',
'recipient' => 'Google Ireland Ltd., Irland / Google LLC, USA',
'recipient' => __( 'Google Ireland Ltd., Irland / Google LLC, USA', 'gdpr-content-blocker' ),
'third_country' => true,
'sets_cookie' => true,
'loads_script' => true,
@@ -191,7 +194,7 @@ class CB_Settings {
'id' => 'youtube',
'name' => 'YouTube',
'match_pattern' => 'youtube',
'recipient' => 'Google Ireland Ltd., Irland / Google LLC, USA',
'recipient' => __( 'Google Ireland Ltd., Irland / Google LLC, USA', 'gdpr-content-blocker' ),
'third_country' => true,
'sets_cookie' => true,
'loads_script' => true,
@@ -202,7 +205,7 @@ class CB_Settings {
'id' => 'openstreetmap',
'name' => 'OpenStreetMap',
'match_pattern' => 'openstreetmap.org',
'recipient' => 'OpenStreetMap Foundation, Großbritannien',
'recipient' => __( 'OpenStreetMap Foundation, Großbritannien', 'gdpr-content-blocker' ),
'third_country' => true,
'sets_cookie' => false,
'loads_script' => true,
@@ -213,7 +216,7 @@ class CB_Settings {
'id' => 'vimeo',
'name' => 'Vimeo',
'match_pattern' => 'player.vimeo.com',
'recipient' => 'Vimeo LLC, USA',
'recipient' => __( 'Vimeo LLC, USA', 'gdpr-content-blocker' ),
'third_country' => true,
'sets_cookie' => true,
'loads_script' => true,
@@ -406,6 +409,57 @@ class CB_Settings {
.cb-admin-wrap .cb-switch input:checked + .cb-switch__slider::before {
transform: translateX(18px);
}
/* expand/collapse +/- icon */
.cb-admin-wrap .cb-service-toggle {
width: 30px;
height: 30px;
font-size: 22px;
line-height: 1;
font-weight: 400;
color: #2043B7;
background: #f0f3fc;
border: 1px solid #c7d2f5;
border-radius: 6px;
padding: 0;
}
.cb-admin-wrap .cb-service-toggle:hover {
background: #e3e9fb;
}
/* trash remove button */
.cb-admin-wrap .cb-remove-service {
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: #888;
display: inline-flex;
align-items: center;
}
.cb-admin-wrap .cb-remove-service:hover {
color: #b32d2e;
}
.cb-admin-wrap .cb-remove-service .dashicons {
font-size: 20px;
width: 20px;
height: 20px;
}
/* active tab indicator */
.cb-admin-wrap .nav-tab-wrapper {
margin-bottom: 0;
}
.cb-admin-wrap .nav-tab-active,
.cb-admin-wrap .nav-tab-active:focus,
.cb-admin-wrap .nav-tab-active:hover {
background: #fff;
color: #2043B7;
border-bottom: 3px solid #2043B7;
font-weight: 600;
margin-bottom: -1px;
}
/* 20px breathing room between tabs and content */
.cb-admin-wrap .cb-tab-content {
padding-top: 20px;
}
.cb-admin-wrap .cb-field label {
display: block;
font-weight: 600;
@@ -449,7 +503,7 @@ class CB_Settings {
?>
<div class="cb-service-box" data-cb-index="<?php echo $idx; ?>">
<div class="cb-service-head">
<button type="button" class="cb-service-toggle" aria-expanded="false" title="<?php esc_attr_e( 'Details anzeigen/ausblenden', 'gdpr-content-blocker' ); ?>"></button>
<button type="button" class="cb-service-toggle" aria-expanded="false" aria-label="<?php esc_attr_e( 'Details anzeigen/ausblenden', 'gdpr-content-blocker' ); ?>">+</button>
<span class="cb-service-title"><?php echo esc_html( $headname ); ?></span>
<label class="cb-switch" title="<?php esc_attr_e( 'Blocker aktiv/inaktiv', 'gdpr-content-blocker' ); ?>">
@@ -458,8 +512,8 @@ class CB_Settings {
<span class="cb-switch__slider"></span>
</label>
<button type="button" class="button-link-delete cb-remove-service">
<?php esc_html_e( 'Entfernen', 'gdpr-content-blocker' ); ?>
<button type="button" class="cb-remove-service" aria-label="<?php esc_attr_e( 'Dienst entfernen', 'gdpr-content-blocker' ); ?>" title="<?php esc_attr_e( 'Dienst entfernen', 'gdpr-content-blocker' ); ?>">
<span class="dashicons dashicons-trash"></span>
</button>
</div>
<div class="cb-service-grid" style="display:none;">
@@ -710,11 +764,14 @@ class CB_Settings {
wp_send_json_error( [ 'message' => $msg ] );
}
// Annotate each finding: is it already covered by a configured service?
// Annotate each finding: already covered by a configured service? And is
// there a ready-made template (preset) for it?
$services = self::get_services();
$presets = self::get_presets();
$findings = is_array( $data['findings'] ?? null ) ? $data['findings'] : [];
foreach ( $findings as &$f ) {
$f['covered'] = self::is_covered( $f, $services );
$f['preset'] = self::match_preset( $f, $presets ); // preset key or ''
}
unset( $f );
@@ -724,12 +781,17 @@ class CB_Settings {
] );
}
/** True if any configured service's match_pattern matches this finding. */
private static function is_covered( array $finding, array $services ): bool {
$haystacks = array_merge(
/** Collect host + sample URLs of a finding for substring matching. */
private static function finding_haystacks( array $finding ): array {
return array_merge(
[ (string) ( $finding['host'] ?? '' ) ],
array_map( 'strval', (array) ( $finding['sample_urls'] ?? [] ) )
);
}
/** True if any configured service's match_pattern matches this finding. */
private static function is_covered( array $finding, array $services ): bool {
$haystacks = self::finding_haystacks( $finding );
foreach ( $services as $svc ) {
$pattern = $svc['match_pattern'] ?? '';
if ( $pattern === '' ) {
@@ -743,4 +805,21 @@ class CB_Settings {
}
return false;
}
/** Return the key of a preset whose match_pattern fits this finding, or ''. */
private static function match_preset( array $finding, array $presets ): string {
$haystacks = self::finding_haystacks( $finding );
foreach ( $presets as $key => $preset ) {
$pattern = $preset['match_pattern'] ?? '';
if ( $pattern === '' ) {
continue;
}
foreach ( $haystacks as $h ) {
if ( $h !== '' && str_contains( $h, $pattern ) ) {
return (string) $key;
}
}
}
return '';
}
}