1<!DOCTYPE html> 2<html lang="en" data-site-language="en" data-site-rendered="1"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Feedback zu den Apps | wiesing.de</title> 7 <meta name="description" content="Teilen Sie Ihr Feedback zu den Apps von Holger Wiesing mit: schnell, direkt und mit Mensch-Verifikation."> 8 <meta name="robots" content="index,follow"> 9 <link rel="canonical" href="https://wiesing.de/feedback.html"> 10 <meta property="og:locale" content="de_DE"> 11 <meta property="og:type" content="website"> 12 <meta property="og:site_name" content="wiesing.de"> 13 <meta property="og:title" content="Feedback zu den Apps | wiesing.de"> 14 <meta property="og:description" content="Teilen Sie Ihr Feedback zu den Apps von Holger Wiesing mit: schnell, direkt und mit Mensch-Verifikation."> 15 <meta property="og:url" content="https://wiesing.de/feedback.html"> 16 <meta property="og:image" content="https://wiesing.de/assets/icons_logos/kapieren-de-logo-app-orange.png"> 17 <meta name="twitter:card" content="summary_large_image"> 18 <meta name="twitter:title" content="Feedback zu den Apps | wiesing.de"> 19 <meta name="twitter:description" content="Teilen Sie Ihr Feedback zu den Apps von Holger Wiesing mit: schnell, direkt und mit Mensch-Verifikation."> 20 <meta name="twitter:image" content="https://wiesing.de/assets/icons_logos/kapieren-de-logo-app-orange.png"> 21 <link rel="stylesheet" href="./assets/css/ui-shared.css?v=20260729-language6"> 22 <link rel="icon" type="image/svg+xml" href="./assets/favicon.svg"> 23 <link rel="shortcut icon" href="./assets/favicon.svg"> 24
24<script type="application/ld+json"> 25 { 26 "@context": "https://schema.org", 27 "@type": "ContactPage", 28 "@id": "https://wiesing.de/feedback.html#webpage", 29 "url": "https://wiesing.de/feedback.html", 30 "name": "Feedback zu den Apps | wiesing.de", 31 "description": "Teilen Sie Ihr Feedback zu den Apps von Holger Wiesing mit: schnell, direkt und mit Mensch-Verifikation.", 32 "isPartOf": { 33 "@type": "WebSite", 34 "@id": "https://wiesing.de/#website", 35 "url": "https://wiesing.de/", 36 "name": "wiesing.de" 37 }, 38 "inLanguage": "de" 39 } 40 </script>
40 41
41<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
41 42
42<script> 43 (function enforceCanonicalUrl() { 44 var canonicalHost = "wiesing.de"; 45 var hostname = (window.location.hostname || "").toLowerCase(); 46 var protocol = window.location.protocol; 47 var isLocal = 48 hostname === "localhost" || 49 hostname === "127.0.0.1" || 50 hostname === "[::1]"; 51 52 if (isLocal || protocol === "file:") { 53 return; 54 } 55 56 var needsHttps = protocol !== "https:"; 57 var needsCanonicalHost = hostname !== canonicalHost; 58 if (!needsHttps && !needsCanonicalHost) { 59 return; 60 } 61 62 var target = 63 "https://" + 64 canonicalHost + 65 window.location.pathname + 66 window.location.search + 67 window.location.hash; 68 window.location.replace(target); 69 })(); 70 </script>
70 71 <style> 72 :root { 73 --bg: #ffffff; 74 --surface: #f8f7f4; 75 --surface-soft: #f2efe8; 76 --text: #2f2b25; 77 --text-soft: #5f5a52; 78 --border: #c8c2b6; 79 --accent: #7a3618; 80 --button: #0d6a48; 81 --button-strong: #084c34; 82 --shadow: 0 8px 17px rgba(52, 45, 36, 0.24); 83 } 84 85 * { 86 box-sizing: border-box; 87 } 88 89 html, 90 body { 91 height: 100%; 92 margin: 0; 93 padding: 0; 94 } 95 96 body { 97 min-height: 100vh; 98 min-height: 100svh; 99 min-height: 100dvh; 100 display: flex; 101 flex-direction: column; 102 font-family: "Avenir Next", "Segoe UI", "Helvetica Neue", sans-serif; 103 color: var(--text); 104 background: var(--bg); 105 } 106 107 .page { 108 width: min(920px, calc(100% - 2rem)); 109 margin: 2.2rem auto 1.2rem; 110 flex: 1 0 auto; 111 } 112 113 .panel { 114 position: relative; 115 background: linear-gradient(160deg, #fbfaf7 0%, #f6f4ef 68%, #f2efe8 100%); 116 border: 1px solid var(--border); 117 box-shadow: var(--shadow); 118 border-radius: 12px; 119 padding: clamp(1.15rem, 3.2vw, 2rem); 120 padding-top: clamp(2.9rem, 5vw, 3.4rem); 121 } 122 123 h1 { 124 margin: 0.24rem 0 0.78rem; 125 font-size: clamp(1.54rem, 3.8vw, 2.2rem); 126 line-height: 1.2; 127 } 128 129 .panel-home-link { 130 position: absolute; 131 top: 0.85rem; 132 right: 0.95rem; 133 text-decoration: none; 134 color: #6e3620; 135 font-size: 0.88rem; 136 font-weight: 700; 137 } 138 139 .panel-home-link:hover, 140 .panel-home-link:focus-visible { 141 color: #5c2d1a; 142 text-decoration: underline; 143 text-underline-offset: 0.12em; 144 outline: none; 145 } 146 147 .intro { 148 margin: 0; 149 color: var(--text-soft); 150 line-height: 1.6; 151 max-width: 72ch; 152 } 153 154 .app-preview { 155 margin: 0 0 0.95rem; 156 } 157 158 .app-preview-title { 159 margin: 0 0 0.52rem; 160 font-size: 0.86rem; 161 letter-spacing: 0.08em; 162 text-transform: uppercase; 163 color: var(--accent); 164 font-weight: 800; 165 } 166 167 .app-strip { 168 display: flex; 169 flex-wrap: wrap; 170 gap: 0.56rem; 171 } 172 173 .app-chip { 174 display: inline-flex; 175 align-items: center; 176 gap: 0.45rem; 177 border: 1px solid #d1c9bc; 178 border-radius: 999px; 179 background: #fcfbf9; 180 text-decoration: none; 181 color: var(--text); 182 padding: 0.28rem 0.56rem 0.28rem 0.34rem; 183 } 184 185 .app-chip:hover, 186 .app-chip:focus-visible { 187 border-color: #b7ab9b; 188 box-shadow: 0 2px 5px rgba(70, 53, 38, 0.24); 189 outline: none; 190 } 191 192 .app-chip-icon { 193 width: 28px; 194 height: 28px; 195 object-fit: cover; 196 border-radius: 7px; 197 border: 1px solid #d4cec2; 198 background: #ece9e2; 199 flex-shrink: 0; 200 } 201 202 .app-chip-name { 203 font-size: 0.88rem; 204 font-weight: 600; 205 } 206 207 .app-strip-status { 208 margin: 0.48rem 0 0; 209 color: var(--text-soft); 210 font-size: 0.86rem; 211 } 212 213 .feedback-form { 214 margin-top: 0; 215 transition: opacity 0.22s ease, filter 0.22s ease; 216 } 217 218 .feedback-form-stage { 219 position: relative; 220 margin-top: 1.2rem; 221 } 222 223 .feedback-form-stage.is-submitted .feedback-form { 224 opacity: 0.26; 225 filter: grayscale(1); 226 pointer-events: none; 227 user-select: none; 228 } 229 230 .feedback-success-overlay { 231 position: absolute; 232 inset: 0; 233 display: flex; 234 align-items: center; 235 justify-content: center; 236 padding: 1rem; 237 opacity: 0; 238 pointer-events: none; 239 transition: opacity 0.22s ease; 240 } 241 242 .feedback-form-stage.is-submitted .feedback-success-overlay { 243 opacity: 1; 244 pointer-events: auto; 245 } 246 247 .feedback-success-card { 248 width: min(620px, calc(100% - 0.6rem)); 249 background: rgba(255, 253, 248, 0.96); 250 border: 1px solid #d7cec0; 251 border-radius: 12px; 252 box-shadow: 0 9px 18px rgba(49, 38, 28, 0.28); 253 padding: clamp(1rem, 2.8vw, 1.35rem); 254 text-align: center; 255 } 256 257 .feedback-success-title { 258 margin: 0; 259 color: #14543b; 260 font-size: 1.06rem; 261 font-weight: 800; 262 } 263 264 .feedback-success-text { 265 margin: 0.5rem 0 0; 266 color: var(--text-soft); 267 line-height: 1.55; 268 } 269 270 .feedback-success-action { 271 margin-top: 0.84rem; 272 } 273 274 .feedback-success-link { 275 display: inline-flex; 276 align-items: center; 277 justify-content: center; 278 text-decoration: none; 279 border: 1px solid #0a5238; 280 background: linear-gradient(135deg, #0d6a48 0%, #0a5238 100%); 281 color: #ffffff; 282 font-weight: 700; 283 padding: 0.56rem 1.02rem; 284 border-radius: 9px; 285 box-shadow: 0 3px 7px rgba(10, 82, 56, 0.34); 286 } 287 288 .feedback-success-link:hover, 289 .feedback-success-link:focus-visible { 290 filter: brightness(1.06); 291 box-shadow: 0 4px 9px rgba(10, 82, 56, 0.42); 292 outline: none; 293 } 294 295 .form-grid { 296 display: grid; 297 grid-template-columns: repeat(2, minmax(0, 1fr)); 298 gap: 0.9rem; 299 } 300 301 .field { 302 display: flex; 303 flex-direction: column; 304 gap: 0.35rem; 305 } 306 307 .field-full { 308 grid-column: 1 / -1; 309 } 310 311 label { 312 font-weight: 600; 313 font-size: 0.95rem; 314 } 315 316 .label-optional { 317 color: var(--text-soft); 318 font-weight: 500; 319 } 320 321 input, 322 select, 323 textarea { 324 width: 100%; 325 border: 1px solid #bdb6aa; 326 border-radius: 8px; 327 padding: 0.62rem 0.7rem; 328 background: #fffefb; 329 color: var(--text); 330 font: inherit; 331 } 332 333 textarea { 334 min-height: 150px; 335 resize: vertical; 336 } 337 338 input:focus-visible, 339 select:focus-visible, 340 textarea:focus-visible { 341 outline: 2px solid #21654a; 342 outline-offset: 1px; 343 } 344 345 .checkbox-wrap { 346 margin-top: 0.25rem; 347 display: flex; 348 flex-direction: column; 349 gap: 0.6rem; 350 } 351 352 .checkbox-row { 353 display: flex; 354 align-items: flex-start; 355 gap: 0.55rem; 356 background: var(--surface-soft); 357 border: 1px solid #d5cec2; 358 border-radius: 9px; 359 padding: 0.75rem 0.85rem; 360 } 361 362 .checkbox-row input[type="checkbox"] { 363 width: 1.08rem; 364 height: 1.08rem; 365 margin-top: 0.12rem; 366 flex-shrink: 0; 367 } 368 369 .checkbox-row label { 370 margin: 0; 371 font-weight: 500; 372 line-height: 1.5; 373 } 374 375 .captcha-title { 376 margin: 0 0 0.35rem; 377 font-size: 0.95rem; 378 font-weight: 700; 379 color: var(--accent); 380 } 381 382 .captcha-hint { 383 margin: 0 0 0.7rem; 384 color: var(--text-soft); 385 font-size: 0.9rem; 386 } 387 388 .turnstile-wrap { 389 background: var(--surface-soft); 390 border: 1px solid #d5cec2; 391 border-radius: 9px; 392 padding: 0.75rem 0.85rem; 393 } 394 395 .captcha-error { 396 margin: 0.55rem 0 0; 397 color: #9d2a1b; 398 font-weight: 700; 399 font-size: 0.9rem; 400 } 401 402 .actions { 403 margin-top: 1rem; 404 display: flex; 405 flex-wrap: wrap; 406 gap: 0.8rem; 407 align-items: center; 408 } 409 410 .submit-button { 411 border: 1px solid #0a5238; 412 background: linear-gradient(135deg, #0d6a48 0%, #0a5238 100%); 413 color: #ffffff; 414 font-weight: 700; 415 font-size: 0.98rem; 416 padding: 0.62rem 1.12rem; 417 border-radius: 9px; 418 cursor: pointer; 419 box-shadow: 0 3px 7px rgba(10, 82, 56, 0.34); 420 } 421 422 .submit-button:hover, 423 .submit-button:focus-visible { 424 filter: brightness(1.06); 425 box-shadow: 0 4px 9px rgba(10, 82, 56, 0.42); 426 } 427 428 .submit-button:focus-visible { 429 outline: 2px solid #0a5238; 430 outline-offset: 2px; 431 } 432 433 .error-message { 434 margin: 0; 435 color: #9d2a1b; 436 font-weight: 700; 437 } 438
439 .spam-trap { 440 position: absolute; 441 left: -9999px; 442 width: 1px; 443 height: 1px; 444 overflow: hidden; 445 } 446 447 .site-footer { 448 margin-top: auto; 449 flex-shrink: 0; 450 background: #3c3c3c; 451 border-top: 1px solid #2a2a2a; 452 } 453 454 .site-footer-inner { 455 width: min(920px, calc(100% - 2rem)); 456 margin: 0 auto; 457 padding: 1rem 0; 458 display: flex; 459 justify-content: flex-end; 460 gap: 0.7rem; 461 flex-wrap: wrap; 462 } 463 464 .site-footer-link { 465 color: #f4f4f4; 466 text-decoration: none; 467 font-size: 0.92rem; 468 font-weight: 600; 469 padding: 0.45rem 0.7rem; 470 border: 1px solid #8a8a8a; 471 border-radius: 6px; 472 } 473 474 .site-footer-link.mediafiles-link { 475 min-width: 180px; 476 text-align: center; 477 background: #0d6a48; 478 border-color: #0b5a3d; 479 margin-right: auto; 480 } 481 482 .site-footer-link:hover, 483 .site-footer-link:focus-visible { 484 color: #ffffff; 485 border-color: #c5c5c5; 486 } 487 488 .site-footer-link.mediafiles-link:hover, 489 .site-footer-link.mediafiles-link:focus-visible { 490 border-color: #16734f; 491 background: #0f7751; 492 } 493 494 @media (max-width: 700px) { 495 .page { 496 width: min(920px, calc(100% - 1.1rem)); 497 margin-top: 1.2rem; 498 } 499 500 .site-footer-inner { 501 width: min(920px, calc(100% - 1.1rem)); 502 } 503 504 .form-grid { 505 grid-template-columns: 1fr; 506 gap: 0.8rem; 507 } 508 509 .app-strip { 510 gap: 0.44rem; 511 } 512 513 .app-chip { 514 padding-right: 0.5rem; 515 } 516 517 .panel { 518 padding-top: 3.2rem; 519 } 520 521 .panel-home-link { 522 top: 0.74rem; 523 right: 0.72rem; 524 font-size: 0.81rem; 525 } 526 } 527 </style> 528 </head> 529 <body> 530 <main class="page"> 531 <section class="panel title-accent"> 532 <a class="panel-home-link" href="./index.html?lang=en" aria-label="Back to home">Holger Wiesing · kapieren.de</a> 533 <section class="app-preview" aria-labelledby="app-preview-title"> 534 <p id="app-preview-title" class="app-preview-title">Current apps</p> 535 <div id="app-strip" class="app-strip" role="list" aria-label="Aktuelle Apps aus dem Katalog"></div> 536 <p id="app-strip-status" class="app-strip-status" hidden></p> 537 </section> 538 539 <h1>Your feedback on our apps</h1> 540 <p class="intro"> 541 Your feedback and suggestions reach us directly and help us make the apps even better. 542 </p> 543 544 <div id="feedback-form-stage" class="feedback-form-stage"> 545 <form id="feedback-form" class="feedback-form" novalidate> 546 <div class="form-grid"> 547 <div class="field"> 548 <label for="vorname">First name *</label> 549 <input id="vorname" name="vorname" type="text" autocomplete="given-name" required> 550 </div> 551 552 <div class="field"> 553 <label for="name">Last name *</label> 554 <input id="name" name="name" type="text" autocomplete="family-name" required> 555 </div> 556 557 <div class="field"> 558 <label for="email">E-Mail *</label> 559 <input id="email" name="email" type="email" autocomplete="email" required> 560 </div> 561 562 <div class="field"> 563 <label for="schule">School <span class="label-optional">(optional)</span></label> 564 <input id="schule" name="schule" type="text" autocomplete="organization"> 565 </div> 566 567 <div class="field"> 568 <label for="funktion">Role at school <span class="label-optional">(optional)</span></label> 569 <input id="funktion" name="funktion" type="text" autocomplete="organization-title"> 570 </div> 571 572 <div class="field"> 573 <label for="app">Which app is this about? *</label> 574 <select id="app" name="app" required> 575 <option value="">Loading apps ...</option> 576 </select> 577 </div> 578 579 <div class="field"> 580 <label for="plattform">Which platform did you use the app on? *</label> 581 <select id="plattform" name="plattform" required> 582 <option value="">Please select</option> 583 <option value="iPhone">iPhone</option> 584 <option value="iPad">iPad</option> 585 <option value="Mac">Mac</option> 586 <option value="Windows">Windows</option> 587 </select> 588 </div> 589 590 <div class="field field-full"> 591 <label for="feedback-text">Your feedback *</label> 592 <textarea id="feedback-text" name="feedback_text" required></textarea> 593 </div> 594 </div> 595 596 <div class="checkbox-wrap"> 597 <div class="checkbox-row"> 598 <input id="newsletter-optin" name="newsletter_optin" type="checkbox"> 599 <label for="newsletter-optin"> 600 Please email me about future features and other news about these apps. 601 </label> 602 </div> 603 604 <div> 605 <p class="captcha-title">Human verification (CAPTCHA)</p> 606 <p class="captcha-hint">Please complete the security check.</p> 607 <div class="turnstile-wrap"> 608 <div id="turnstile-container"></div> 609 </div> 610 <p id="captcha-error" class="captcha-error" role="alert" hidden> 611 Please complete the CAPTCHA verification. 612 </p> 613 </div> 614 </div> 615
616 <div class="spam-trap" aria-hidden="true"> 617 <label for="website">Website</label> 618 <input id="website" name="website" type="text" tabindex="-1" autocomplete="off"> 619 </div> 620 621 <div class="actions"> 622 <button class="submit-button" type="submit">Submit</button> 623 <p id="error-message" class="error-message" role="alert" hidden> 624 Something went wrong while sending. Please try again. 625 </p> 626 </div> 627 </form> 628 629 <div id="feedback-success-overlay" class="feedback-success-overlay" hidden> 630 <div class="feedback-success-card"> 631 <p class="feedback-success-title">Thank you very much for your feedback.</p> 632 <p class="feedback-success-text"> 633 Your feedback has been received successfully. 634 </p> 635 <p class="feedback-success-action"> 636 <a class="feedback-success-link" href="./index.html?lang=en">Back to home</a> 637 </p> 638 </div> 639 </div> 640 </div> 641 </section> 642 </main> 643 644 <footer class="site-footer"> 645 <div class="site-footer-inner"> 646 <a class="site-footer-link mediafiles-link" href="./mediafiles/__auth/login.html?next=/mediafiles/">Media files (internal)</a> 647 <a class="site-footer-link" href="./index.html?lang=en">Home</a> 648 <a class="site-footer-link" href="./impressum.html?lang=en">Legal notice</a> 649 </div> 650 </footer> 651 652
652<script src="./assets/js/site-i18n.js?v=20260909-stats1"></script>
652 653
653<script> 654 (function initFeedbackForm() { 655 var defaultLocale = window.siteI18n && window.siteI18n.getLanguage ? window.siteI18n.getLanguage() : "de"; 656 var tr = window.siteI18n && window.siteI18n.t ? window.siteI18n.t : function (value) { return value; }; 657 var defaultSiteKey = "0x4AAAAAAC0_Q7Kezk3bRSYR"; 658 var defaultCatalogConfig = { 659 country: "de", 660 storefronts: ["de", "us"], 661 language: "de-de", 662 developerName: "Holger Wiesing", 663 artistId: null 664 }; 665 var captchaToken = ""; 666 var turnstileWidgetId = null; 667 var turnstileReadyPromise = null; 668 var appStrip = document.getElementById("app-strip"); 669 var appStripStatus = document.getElementById("app-strip-status"); 670 var appSelect = document.getElementById("app"); 671 var formStage = document.getElementById("feedback-form-stage"); 672 var successOverlay = document.getElementById("feedback-success-overlay"); 673 674 var form = document.getElementById("feedback-form"); 675 var error = document.getElementById("error-message"); 676 var captchaError = document.getElementById("captcha-error"); 677 var submitButton = form ? form.querySelector('button[type="submit"]') : null; 678 var extraFeedbackApps = ["Vektoris3D 2.5 PLUS (PC/Mac)"]; 679 if (!form || !error || !captchaError || !submitButton || !formStage || !successOverlay || !appSelect) { 680 return; 681 } 682 683 function setPending(isPending) { 684 submitButton.disabled = isPending; 685 submitButton.textContent = tr(isPending ? "Wird gesendet..." : "Absenden"); 686 } 687 688 function setError(message) { 689 error.textContent = message; 690 error.hidden = false; 691 } 692 693 function setAppStripStatus(message) { 694 if (!appStripStatus) { 695 return; 696 } 697 appStripStatus.textContent = message; 698 appStripStatus.hidden = !message; 699 } 700 701 function waitForTurnstile() { 702 if (window.turnstile && typeof window.turnstile.render === "function") { 703 return Promise.resolve(); 704 } 705 if (turnstileReadyPromise) { 706 return turnstileReadyPromise; 707 } 708 709 turnstileReadyPromise = new Promise(function (resolve, reject) { 710 var checks = 0; 711 var maxChecks = 80; 712 var intervalId = window.setInterval(function () { 713 checks += 1; 714 if (window.turnstile && typeof window.turnstile.render === "function") { 715 window.clearInterval(intervalId); 716 resolve(); 717 return; 718 } 719 if (checks >= maxChecks) { 720 window.clearInterval(intervalId); 721 reject(new Error("Turnstile konnte nicht geladen werden.")); 722 } 723 }, 100); 724 }); 725 726 return turnstileReadyPromise; 727 } 728 729 function resetCaptcha() { 730 captchaToken = ""; 731 if (window.turnstile && typeof window.turnstile.reset === "function") { 732 if (turnstileWidgetId !== null) { 733 window.turnstile.reset(turnstileWidgetId); 734 } else { 735 window.turnstile.reset(); 736 } 737 } 738 } 739 740 function initTurnstileWidget(siteKey) { 741 return waitForTurnstile().then(function () { 742 var container = document.getElementById("turnstile-container"); 743 if (!container) { 744 return; 745 } 746 747 turnstileWidgetId = window.turnstile.render(container, { 748 sitekey: siteKey || defaultSiteKey, 749 language: "de", 750 theme: "light", 751 callback: function (token) { 752 captchaToken = String(token || ""); 753 captchaError.hidden = true; 754 }, 755 "expired-callback": function () { 756 captchaToken = ""; 757 }, 758 "error-callback": function () { 759 captchaToken = ""; 760 } 761 }); 762 }); 763 } 764 765 function loadFeedbackConfig() { 766 return fetch("/api/feedback-config.php", { cache: "no-store" }) 767 .then(function (response) { 768 if (!response.ok) { 769 throw new Error("Konfiguration konnte nicht geladen werden."); 770 } 771 return response.json(); 772 }) 773 .catch(function () { 774 return { siteKey: defaultSiteKey }; 775 }); 776 } 777 778 function normalize(value) { 779 return String(value || "") 780 .trim() 781 .toLowerCase(); 782 } 783 784 function buildItunesUrl(path, params) { 785 return "https://itunes.apple.com" + path + "?" + new URLSearchParams(params).toString(); 786 } 787 788 function getStorefronts(config) { 789 var all = [config.country].concat(config.storefronts || []) 790 .map(function (entry) { 791 return normalize(entry); 792 }) 793 .filter(function (entry) { 794 return /^[a-z]{2}$/.test(entry); 795 }); 796 return Array.from(new Set(all)); 797 } 798 799 function isSoftwareResult(item) { 800 return Boolean(item) && (item.wrapperType === "software" || item.kind === "software"); 801 } 802 803 function matchesDeveloper(app, config) { 804 var needle = normalize(config.developerName); 805 if (!needle) { 806 return true; 807 }
808 var tokens = needle.split(/\s+/).filter(Boolean); 809 var artist = normalize(app.artistName); 810 var seller = normalize(app.sellerName); 811 var haystack = (artist + " " + seller).trim(); 812 813 return ( 814 artist === needle || 815 seller === needle || 816 artist.indexOf(needle) >= 0 || 817 seller.indexOf(needle) >= 0 || 818 tokens.every(function (token) { 819 return haystack.indexOf(token) >= 0; 820 }) 821 ); 822 } 823 824 function byRecentRelease(a, b) { 825 var dateA = new Date(a.currentVersionReleaseDate || a.releaseDate || 0).getTime(); 826 var dateB = new Date(b.currentVersionReleaseDate || b.releaseDate || 0).getTime(); 827 return dateB - dateA; 828 } 829 830 function uniqueByTrackId(items) { 831 var byId = new Map(); 832 items.forEach(function (item) { 833 if (item.trackId && !byId.has(item.trackId)) { 834 byId.set(item.trackId, item); 835 } 836 }); 837 return Array.from(byId.values()); 838 } 839 840 function fetchJsonp(url) { 841 return new Promise(function (resolve, reject) { 842 var callbackName = "itunesCallback_" + Date.now() + "_" + Math.random().toString(36).slice(2, 9); 843 var script = document.createElement("script"); 844 var timeout = window.setTimeout(function () { 845 cleanup(); 846 reject(new Error("Timeout while loading JSONP")); 847 }, 10000); 848 849 function cleanup() { 850 window.clearTimeout(timeout); 851 if (script.parentNode) { 852 script.parentNode.removeChild(script); 853 } 854 delete window[callbackName]; 855 } 856 857 window[callbackName] = function (data) { 858 cleanup(); 859 resolve(data); 860 }; 861 862 script.onerror = function () { 863 cleanup(); 864 reject(new Error("JSONP request failed")); 865 }; 866 script.src = url + (url.indexOf("?") >= 0 ? "&" : "?") + "callback=" + callbackName; 867 document.head.appendChild(script); 868 }); 869 } 870 871 async function fetchItunesData(url) { 872 try { 873 var response = await fetch(url, { cache: "no-store" }); 874 if (!response.ok) { 875 throw new Error("HTTP " + response.status); 876 } 877 return response.json(); 878 } catch (_err) { 879 return fetchJsonp(url); 880 } 881 } 882 883 async function discoverArtistIds(config) { 884 if (config.artistId) { 885 return [String(config.artistId)]; 886 } 887 if (!config.developerName) { 888 return []; 889 } 890 891 var artistIds = new Set(); 892 var storefronts = getStorefronts(config); 893 await Promise.all(storefronts.map(async function (storefront) { 894 var url = buildItunesUrl("/search", { 895 term: config.developerName, 896 entity: "software", 897 attribute: "softwareDeveloper", 898 country: storefront, 899 lang: config.language, 900 limit: "200" 901 }); 902 try { 903 var payload = await fetchItunesData(url); 904 (payload.results || []).forEach(function (item) { 905 if (isSoftwareResult(item) && item.artistId && matchesDeveloper(item, config)) { 906 artistIds.add(String(item.artistId)); 907 } 908 }); 909 } catch (_searchError) { 910 // Ignore single-store lookup failures. 911 } 912 })); 913 914 return Array.from(artistIds.values()); 915 } 916 917 async function loadAppsForArtistIds(artistIds, config) { 918 var storefronts = getStorefronts(config); 919 var requests = []; 920 artistIds.forEach(function (artistId) { 921 storefronts.forEach(function (storefront) { 922 var url = buildItunesUrl("/lookup", { 923 id: artistId, 924 entity: "software", 925 country: storefront, 926 lang: config.language, 927 limit: "200" 928 }); 929 requests.push(fetchItunesData(url).catch(function () { 930 return null; 931 })); 932 }); 933 }); 934 935 var payloads = await Promise.all(requests); 936 var allApps = []; 937 payloads.forEach(function (payload) { 938 (payload && payload.results ? payload.results : []).forEac
938h(function (item) { 939 if (isSoftwareResult(item)) { 940 allApps.push(item); 941 } 942 }); 943 }); 944 return allApps; 945 } 946 947 async function fallbackSearchApps(config) { 948 if (!config.developerName) { 949 return []; 950 } 951 var storefronts = getStorefronts(config); 952 var payloads = await Promise.all( 953 storefronts.map(async function (storefront) { 954 var url = buildItunesUrl("/search", { 955 term: config.developerName, 956 entity: "software", 957 country: storefront, 958 lang: config.language, 959 limit: "200" 960 }); 961 try { 962 return await fetchItunesData(url); 963 } catch (_err) { 964 return null; 965 } 966 }) 967 ); 968 969 var allApps = []; 970 payloads.forEach(function (payload) { 971 (payload && payload.results ? payload.results : []).forEach(function (item) { 972 if (isSoftwareResult(item) && matchesDeveloper(item, config)) { 973 allApps.push(item); 974 } 975 }); 976 }); 977 return allApps; 978 } 979 980 async function fetchCatalogApps(config) { 981 var artistIds = await discoverArtistIds(config); 982 var apps = []; 983 var loadedViaArtistLookup = false; 984 if (artistIds.length) { 985 apps = await loadAppsForArtistIds(artistIds, config); 986 loadedViaArtistLookup = apps.length > 0; 987 } 988 if (!apps.length) { 989 apps = await fallbackSearchApps(config); 990 } 991 var filtered = loadedViaArtistLookup || config.artistId 992 ? apps 993 : apps.filter(function (item) { 994 return matchesDeveloper(item, config); 995 }); 996 return uniqueByTrackId(filtered).sort(byRecentRelease); 997 } 998 999 function createAppChip(app) { 1000 var chip = document.createElement("a"); 1001 chip.className = "app-chip"; 1002 chip.href = window.siteI18n.appStoreUrl(app); 1003 chip.referrerPolicy = "same-origin"; 1004 chip.target = "_blank"; 1005 chip.rel = "noopener"; 1006 chip.setAttribute("role", "listitem"); 1007 1008 var icon = document.createElement("img"); 1009 icon.className = "app-chip-icon"; 1010 icon.src = String(app.artworkUrl100 || app.artworkUrl60 || ""); 1011 icon.alt = String((app.trackName || "App") + " Icon"); 1012 icon.loading = "lazy"; 1013 1014 var name = document.createElement("span"); 1015 name.className = "app-chip-name"; 1016 name.textContent = String(app.trackName || tr("Unbenannte App")); 1017 1018 chip.appendChild(icon); 1019 chip.appendChild(name); 1020 return chip; 1021 } 1022 1023 function populateAppSelect(apps) { 1024 appSelect.textContent = ""; 1025 1026 var optionNames = []; 1027 apps.forEach(function (app) { 1028 var name = String(app.trackName || "").trim(); 1029 if (name && optionNames.indexOf(name) === -1) { 1030 optionNames.push(name); 1031 } 1032 }); 1033 extraFeedbackApps.forEach(function (name) { 1034 if (optionNames.indexOf(name) === -1) { 1035 optionNames.push(name); 1036 } 1037 }); 1038 1039 var placeholder = document.createElement("option"); 1040 placeholder.value = ""; 1041 placeholder.textContent = tr(optionNames.length ? "Bitte App auswählen" : "Aktuell keine Apps verfügbar"); 1042 appSelect.appendChild(placeholder); 1043 1044 optionNames.forEach(function (name) { 1045 var option = document.createElement("option"); 1046 option.value = name; 1047 option.textContent = name; 1048 appSelect.appendChild(option); 1049 }); 1050 } 1051 1052 async function loadAppPreview() { 1053 if (!appStrip) { 1054 return; 1055 } 1056 setAppStripStatus("Lade aktuelle Apps ..."); 1057 try { 1058 var localeResponse = await fetch("./locales/" + defaultLocale + ".json", { 1059 cache: "no-store" 1060 }); 1061 if (!localeResponse.ok) { 1062 throw new Error("Locale konnte nicht geladen werden."); 1063 } 1064 var locale = await localeResponse.json(); 1065 var config = Object.assign({}, defaultCatalogConfig, locale && locale.catalogConfig ? locale.catalogConfig : {}); 1066 if (!Array.isArray(config.storefronts)) { 1067 config.storefronts = defaultCatalogConfig.storefronts.slice(); 1068 } 1069 var apps = await fetchCatalogApps(config); 1070 populateAppSelect(apps); 1071 var currentApps = apps.slice(0, 8); 1072 appStrip.textContent = ""; 1073 currentApps.forEach(function (app) { 1074 appStrip.appendChild(createAppChip(app)); 1075 }); 1076 setAppStripStatus(currentApps.length ? "" : "Aktuell konnten keine Apps geladen werden."); 1077 } catch (_err) { 1078 populateAppSelect([]); 1079 appStrip.textContent = ""; 1080 setAppStripStatus("Aktuell konnten keine Apps geladen werden."); 1081 } 1082 } 1083 1084 loadAppPreview(); 1085 1086 loadFeedbackConfig() 1087 .then(function (config) { 1088 return initTurnstileWidget(config.siteKey); 1089 }) 1090 .catch(function (initError) { 1091 setError(initError.message || "Captcha konnte nicht initialisiert werden."); 1092 }); 1093 1094 form.addEventListener("submit", async function (event) { 1095 event.preventDefault(); 1096 error.hidden = true; 1097 captchaError.hidden = true; 1098 1099 if (!form.reportValidity()) { 1100 return; 1101 } 1102 1103 var trap = document.getElementById("website"); 1104 if (trap && String(trap.value || "").trim()) { 1105 return; 1106 } 1107 1108 if (!captchaToken) { 1109 captchaError.hidden = false;
1110 return; 1111 } 1112 1113 setPending(true); 1114 try { 1115 var payload = { 1116 vorname: String(form.vorname.value || "").trim(), 1117 name: String(form.name.value || "").trim(), 1118 email: String(form.email.value || "").trim(), 1119 schule: String(form.schule.value || "").trim(), 1120 funktion: String(form.funktion.value || "").trim(), 1121 app: String(form.app.value || "").trim(), 1122 plattform: String(form.plattform.value || "").trim(), 1123 feedbackText: String(form.feedback_text.value || "").trim(), 1124 newsletterOptIn: Boolean(form.newsletter_optin.checked), 1125 website: trap ? String(trap.value || "").trim() : "", 1126 turnstileToken: captchaToken 1127 }; 1128 1129 var response = await fetch("/api/feedback.php", { 1130 method: "POST", 1131 headers: { "Content-Type": "application/json" }, 1132 body: JSON.stringify(payload) 1133 }); 1134 var result = await response.json().catch(function () { 1135 return {}; 1136 }); 1137 1138 if (!response.ok || !result.ok) { 1139 if (result.code === "captcha_invalid") { 1140 captchaError.textContent = tr("Captcha ungültig oder abgelaufen. Bitte erneut bestätigen."); 1141 captchaError.hidden = false; 1142 } else { 1143 setError(result.error || tr("Beim Senden ist ein Fehler aufgetreten.")); 1144 } 1145 resetCaptcha(); 1146 return; 1147 } 1148 1149 form.reset(); 1150 resetCaptcha(); 1151 captchaError.textContent = tr("Bitte führen Sie die Captcha-Verifikation durch."); 1152 formStage.classList.add("is-submitted"); 1153 successOverlay.hidden = false; 1154 } catch (_err) { 1155 setError("Der Server ist momentan nicht erreichbar. Bitte später erneut versuchen."); 1156 resetCaptcha(); 1157 } finally { 1158 setPending(false); 1159 } 1160 }); 1161 })(); 1162 </script>
1162 1163 </body> 1164</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.