1class AppSheet extends HTMLElement { 2 constructor() { 3 super(); 4 this.attachShadow({ mode: "open" }); 5 this.currentIndex = 0; 6 this.isAndroid = /Android/i.test(navigator.userAgent); 7 this.language = "en"; 8 } 9 10 static get observedAttributes() { 11 return ["language", "open", "mini", "asdialog", "variant"]; 12 } 13 14 get isDialog() { 15 return this.hasAttribute("asdialog"); 16 } 17 18 // "readmore" swaps the branding copy for a "continue reading in the app" pitch, 19 // reusing the exact same coverflow sheet (full + mini). Absent an explicit variant 20 // attribute, each element instance is randomly assigned one (50/50) the first time 21 // it's checked, and that choice sticks for its lifetime. 22 get variant() { 23 const attr = this.getAttribute("variant"); 24 if (attr === "readmore" || attr === "sheet") return attr; 25 if (!this._resolvedVariant) { 26 this._resolvedVariant = Math.random() < 0.5 ? "readmore" : "sheet"; 27 } 28 return this._resolvedVariant; 29 } 30 31 attributeChangedCallback(name, oldValue, newValue) { 32 if (name === "language" && newValue) { 33 this.language = newValue; 34 if (this.isConnected) { 35 if (this.isReadMore) { 36 // Readmore copy lives in the rendered markup, so swap it via full re-render 37 this.render(); 38 this.loadScreenshots(); 39 this.setupEventListeners(); 40 this.updateVisibility(); 41 } else { 42 this.loadScreenshots(); 43 } 44 } 45 } 46 if (name === "open" || name === "mini") { 47 if (name === "mini" && oldValue !== newValue) { 48 // Mini mode changed - need to re-render to update text and button 49 this.render(); 50 this.loadScreenshots(); 51 this.setupEventListeners(); 52 } 53 this.updateVisibility(); 54 } 55 if (name === "variant" && this.isConnected) { 56 this.render(); 57 this.loadScreenshots(); 58 this.setupEventListeners(); 59 this.updateVisibility(); 60 } 61 } 62 63 get dialogSeen() { 64 return document.cookie.split("; ").some((cookie) => cookie.startsWith("promo_video_export_v1=1")); 65 } 66 67 connectedCallback() { 68 if (this.isDialog && !this.dialogSeen) { 69 this.renderDialog(); 70 return; 71 } 72 73 this.render(); 74 this.loadScreenshots(); 75 this.setupEventListeners(); 76 this.updateVisibility(); 77 } 78 79 get isReadMore() { 80 return this.variant === "readmore"; 81 } 82 83 // Store-button label; the readmore variant sells "keep reading", not "install". 84 get ctaText() { 85 if (this.isReadMore) { 86 return this.language === "de" ? "Kostenlos weiterlesen" : "Keep reading for free"; 87 } 88 return this.language === "de" ? "Jetzt installieren" : "Install now"; 89 } 90 91 get freeText() { 92 if (this.isReadMore) { 93 return this.language === "de" ? "App herunterladen, um weiterzulesen" : "Download the app to keep reading"; 94 } 95 // Short enough to share one line with the stars on a 375px viewport 96 return this.language === "de" ? (this.isAndroid ? "Gratis für Android" : "Gratis für iPhone & iPad") : this.isAndroid ? "Free for Android" : "Free for iPhone & iPad"; 97 } 98 99 render() { 100 const platform = this.isAndroid ? "Android" : "iOS"; 101 const storeName = this.isAndroid ? "Google Play" : "App Store"; 102 const appTitle = this.isReadMore 103 ? this.language === "de" 104 ? "Das Zitat des Tages ist besser in der App" 105 : "The Quote of the Day is better in the app" 106 : this.language === "de" 107 ? "A-Z Zitat des Tages" 108 : "Literally: Quotes and Thoughts"; 109 110 let downloadText; 111 if (this.language === "de") { 112 downloadText = this.isAndroid ? "Jetzt kostenlos bei Google Play holen" : "Jetzt kostenlos im AppStore"; 113 } else { 114 downloadText = this.isAndroid ? "Free on Google Play" : "Free for iPhone and iPad"; 115 } 116 117 const isMini = this.hasAttribute("mini"); 118 119 // In mini mode, show download text as subtitle 120 const appSubtitle = isMini 121 ? downloadText 122 : this.language === "de" 123 ? "Motivierende Zitate, positiven Affirmationen oder herzlichen GrüÃen für besondere Anlässe? Mit dieser App haben Sie all das jederzeit auf Ihrem Handy" 124 : "Daily inspirational quotes, positive affirmations, and heartfelt greetings for special occasions - all at your fingertips"; 125 126 const readmoreHeadline = this.language === "de" ? "Mehr Zitate. <em>Mehr Inspiration.</em> Jeden Tag." : "More quotes. <em>More inspiration.</em> Every day."; 127 const readmoreBody = 128 this.language === "de" ? "Die App zum Zitat des Tages â jeden Morgen frische Inspiration, direkt aufs Handy." : "The Quote of the Day app â fresh inspiration on your phone, every morning."; 129 const readmoreBenefits = 130 this.language === "de" ? ["Jeden Tag ein neues Zitat", "Komplett ohne Werbung", "Zitate als Video teilen"] : ["A new quote every day", "Completely ad-free", "Share quotes as videos"]; 131 const checkIcon = '<svg viewBox="0 0 24 24">
131<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>'; 132 133 const sheetContentBody = this.isReadMore 134 ? ` 135 <div class="readmore-copy"> 136 <div class="readmore-quotemark" aria-hidden="true">â</div> 137 <p class="readmore-headline">${readmoreHeadline}</p> 138 <p class="readmore-body">${readmoreBody}</p> 139 <ul class="readmore-benefits"> 140 ${readmoreBenefits.map((b) => `<li><span class="check">${checkIcon}</span>${b}</li>`).join("")} 141 </ul> 142 </div> 143 ` 144 : ` 145 <div class="coverflow-container"> 146 <button class="nav-button prev" aria-label="Previous"> 147 <svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg> 148 </button> 149 <div class="coverflow-stage"></div> 150 <button class="nav-button next" aria-label="Next"> 151 <svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg> 152 </button> 153 </div> 154 155 <div class="dots"></div> 156 `; 157 158 // Each variant loads its own display font (fallbacks: Georgia / system sans). 159 // readmore: Playfair Display for the editorial "quotes" look. 160 // sheet: Manrope for a clean, premium product look. 161 const fontImport = this.isReadMore 162 ? "@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,600;0,700;0,800;1,600;1,700&display=swap');" 163 : "@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&display=swap');"; 164 165 this.shadowRoot.innerHTML = ` 166 <style> 167 ${fontImport} 168 :host { 169 font-family: var(--sans); 170 --sans: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 171 --serif: 'Playfair Display', 'Iowan Old Style', Georgia, 'Times New Roman', serif; 172 --accent-gradient: linear-gradient(120deg, #ffb547, #ff5f45); 173 --sheet-bg: #ffffff; 174 --text-primary: #1d1d1f; 175 --text-secondary: #86868b; 176 --backdrop-color: rgba(0, 0, 0, 0.4); 177 } 178 179 .backdrop { 180 position: fixed; 181 top: 0; 182 left: 0; 183 right: 0; 184 bottom: 0; 185 background: var(--backdrop-color); 186 z-index: 9998; 187 opacity: 0; 188 visibility: hidden; 189 transition: opacity 0.3s ease, visibility 0.3s ease; 190 } 191 192 .backdrop.show { 193 opacity: 1; 194 visibility: visible; 195 } 196 197 .sheet { 198 position: fixed; 199 bottom: 0; 200 left: 0; 201 right: 0; 202 background: 203 radial-gradient(120% 70% at 50% 0%, rgba(255, 255, 255, 0.9) 0%, rgba(255, 255, 255, 0) 60%), 204 linear-gradient(180deg, #fdfdfd 0%, #f2f2f4 100%); 205 border-radius: 24px 24px 0 0; 206 z-index: 9999; 207 max-width: 600px; 208 margin: 0 auto; 209 max-height: 90vh; 210 overflow: hidden; 211 transform: translateY(100%); 212 transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); 213 box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.16); 214 } 215 216 .sheet.show { 217 transform: translateY(0); 218 } 219 220 .sheet-header { 221 padding: 10px 16px 14px; 222 border-bottom: 1px solid rgba(0, 0, 0, 0.06); 223 position: relative; 224 } 225 226 .sheet-handle { 227 width: 36px; 228 height: 5px; 229 background: rgba(0, 0, 0, 0.16); 230 border-radius: 3px; 231 margin: 0 auto 12px; 232 } 233 234 .header-content { 235 display: flex; 236 align-items: center; 237 gap: 12px; 238 } 239 240 .header-link { 241 display: flex; 242 align-items: center; 243 gap: 12px; 244 text-decoration: none; 245 color: inherit; 246 flex: 1; 247 min-width: 0; 248 transition: opacity 0.2s; 249 } 250 251 .header-link:hover { 252 opacity: 0.7; 253 } 254 255 .app-icon { 256 width: 60px; 257 height: 60px; 258 border-radius: 14px; 259 box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); 260 flex-shrink: 0; 261 } 262 263 .header-text { 264 flex: 1; 265 min-width: 0; 266 text-align: left; 267 padding-right: 36px; 268 } 269 270 .header-title { 271 font-size: 21px; 272 font-weight: 800; 273 letter-spacing: -0.02em; 274 margin: 0 0 4px 0; 275 color: var(--text-primary); 276 white-space: nowrap; 277 overflow: hidden; 278 text-overflow: ellipsis; 279 } 280 281 .header-subtitle { 282 font-size: 13px; 283 color: var(--text-secondary); 284 margin: 0; 285 line-height: 1.4; 286 } 287 288 .rating { 289 display: flex; 290 align-items: center; 291 gap: 4px; 292 margin-top: 4px; 293 min-width: 0; 294 } 295 296 .rating-stars { 297 display: flex; 298 gap: 2px; 299 } 300 301 .rating-stars svg { 302 width: 14px; 303 height: 14px; 304 fill: #ffb800; 305 } 306 307 .rating-text { 308 font-size: 13px; 309 font-weight: 600; 310 color: var(--text-secondary); 311 } 312 313 .rating-separator { 314 font-size: 13px; 315 color: var(--text-secondary); 316 margin: 0 2px; 317 } 318 319 .free-text { 320 font-size: 13px; 321 font-weight: 600; 322 color: var(--text-secondary); 323 white-space: nowrap; 324 overflow: hidden; 325 text-overflow: ellipsis; 326 } 327 328 .rating-stars, 329 .rating-text, 330 .rating-separator { 331 flex-shrink: 0; 332 } 333 334 .close-button { 335 position: absolute; 336 top: 16px; 337 right: 16px; 338 width: 32px; 339 height: 32px; 340 border-radius: 50%; 341 background: rgba(0, 0, 0, 0.06); 342 border: none; 343 cursor: pointer; 344 display: flex; 345 align-items: center; 346 justify-content: center; 347 transition: background 0.2s; 348 padding: 0; 349 } 350 351 .close-button:hover { 352 background: rgba(0, 0, 0, 0.12); 353 } 354 355 .close-button svg { 356 width: 16px; 357 height: 16px; 358 fill: #77777c; 359 } 360 361 /* With the drag handle above, the close button sits level with the title row */ 362 .sheet:not(.mini):not(.readmore) .close-button { 363 top: 30px; 364 } 365 366 .sheet-content { 367 padding: 0 20px 24px; 368 overflow-y: auto; 369 max-height: calc(90vh - 60px); 370 scrollbar-width: none; 371 -ms-overflow-style: none; 372 } 373 374 .sheet-content::-webkit-scrollbar { 375 display: none; 376 } 377 378 379 380 .coverflow-container { 381 position: relative; 382 width: calc(100% + 40px); 383 height: 420px; 384 perspective: 1200px; 385 margin: 0 -20px 24px -20px; 386 overflow: hidden; 387 } 388 389 .coverflow-stage { 390 position: relative; 391 width: 100%; 392 height: 100%; 393 transform-style: preserve-3d; 394 transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); 395 } 396 397 .screenshot { 398 position: absolute; 399 left: 50%; 400 top: 50%; 401 width: 225px; 402 height: 400px; 403 margin-left: -112.5px; 404 margin-top: -200px; 405 border-radius: 20px; 406 overflow: hidden; 407 box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); 408 transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); 409 cursor: pointer; 410 background: #2a2a2a; 411 } 412 413 /* iOS screenshots have different aspect ratio (236:512) */ 414 .screenshot.ios { 415 width: 184px;
416 margin-left: -92px; 417 } 418 419 .screenshot img { 420 width: 100%; 421 height: 100%; 422 object-fit: contain; 423 user-select: none; 424 -webkit-user-drag: none; 425 } 426 427 .nav-button { 428 position: absolute; 429 top: 50%; 430 transform: translateY(-50%); 431 background: rgba(255, 255, 255, 0.92); 432 border: none; 433 width: 40px; 434 height: 40px; 435 border-radius: 50%; 436 cursor: pointer; 437 display: flex; 438 align-items: center; 439 justify-content: center; 440 z-index: 10; 441 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 442 transition: background 0.2s; 443 } 444 445 .nav-button:hover { 446 background: rgba(255, 255, 255, 1); 447 } 448 449 .nav-button.prev { 450 left: 20px; 451 } 452 453 .nav-button.next { 454 right: 20px; 455 } 456 457 .nav-button svg { 458 width: 20px; 459 height: 20px; 460 fill: #000; 461 } 462 463 .store-section { 464 text-align: center; 465 padding: 20px 0; 466 } 467 468 .store-button { 469 display: inline-flex; 470 align-items: center; 471 gap: 12px; 472 padding: 14px 30px; 473 background: linear-gradient(180deg, #2c2c2e 0%, #111113 100%); 474 color: #fff; 475 text-decoration: none; 476 border-radius: 14px; 477 font-size: 16px; 478 font-weight: 700; 479 letter-spacing: 0.01em; 480 box-shadow: 0 8px 22px rgba(0, 0, 0, 0.22); 481 transition: transform 0.2s, opacity 0.2s; 482 } 483 484 .store-button:hover { 485 transform: translateY(-2px); 486 opacity: 0.9; 487 } 488 489 .store-button-lg { 490 display: flex; 491 box-sizing: border-box; 492 width: 100%; 493 justify-content: center; 494 padding: 18px 24px; 495 font-size: 19px; 496 font-weight: 800; 497 border-radius: 14px; 498 box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); 499 } 500 501 /* ===== Read-more variant: dark editorial look ===== */ 502 .sheet.readmore { 503 cursor: pointer; 504 --text-primary: #f7f3e9; 505 --text-secondary: #b8b2a4; 506 background: 507 radial-gradient(130% 100% at 50% 0%, rgba(255, 149, 0, 0.18) 0%, rgba(255, 59, 48, 0.06) 34%, rgba(0, 0, 0, 0) 62%), 508 linear-gradient(180deg, #1d1a24 0%, #131017 55%, #0d0c11 100%); 509 box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.45); 510 } 511 512 .sheet.readmore .sheet-header { 513 border-bottom-color: rgba(255, 255, 255, 0.08); 514 } 515 516 .sheet.readmore .sheet-content { 517 scrollbar-width: none; 518 -ms-overflow-style: none; 519 } 520 521 .sheet.readmore .sheet-content::-webkit-scrollbar { 522 display: none; 523 } 524 525 .sheet.readmore .header-title { 526 font-family: var(--serif); 527 font-size: 19px; 528 font-weight: 700; 529 letter-spacing: 0.01em; 530 } 531 532 .sheet.readmore .close-button { 533 background: rgba(255, 255, 255, 0.1); 534 } 535 536 .sheet.readmore .close-button:hover { 537 background: rgba(255, 255, 255, 0.18); 538 } 539 540 .sheet.readmore .close-button svg { 541 fill: #c9c4b8; 542 } 543 544 .readmore-copy { 545 padding: 14px 8px 4px; 546 text-align: center; 547 } 548 549 .readmore-quotemark { 550 font-family: var(--serif); 551 font-size: 96px; 552 font-weight: 700; 553 line-height: 1; 554 height: 54px; 555 background: var(--accent-gradient); 556 -webkit-background-clip: text; 557 background-clip: text; 558 -webkit-text-fill-color: transparent; 559 color: transparent; 560 user-select: none; 561 } 562 563 .readmore-headline { 564 margin: 0 0 14px; 565 font-family: var(--serif); 566 font-size: 34px; 567 font-weight: 700; 568 line-height: 1.18; 569 letter-spacing: 0; 570 color: var(--text-primary); 571 } 572 573 .readmore-headline em { 574 font-style: italic; 575 background: var(--accent-gradient); 576 -webkit-background-clip: text; 577 background-clip: text; 578 -webkit-text-fill-color: transparent; 579 color: transparent; 580 } 581 582 .readmore-body { 583 margin: 0 auto; 584 max-width: 42ch;
585 font-size: 16px; 586 font-weight: 400; 587 line-height: 1.55; 588 color: var(--text-secondary); 589 } 590 591 .readmore-benefits { 592 list-style: none; 593 margin: 22px auto 0; 594 padding: 0; 595 display: inline-flex; 596 flex-direction: column; 597 align-items: flex-start; 598 gap: 12px; 599 } 600 601 .readmore-benefits li { 602 display: flex; 603 align-items: center; 604 gap: 10px; 605 font-size: 15px; 606 font-weight: 500; 607 color: #ddd8cb; 608 } 609 610 .readmore-benefits .check { 611 width: 22px; 612 height: 22px; 613 border-radius: 50%; 614 background: var(--accent-gradient); 615 display: flex; 616 align-items: center; 617 justify-content: center; 618 flex-shrink: 0; 619 } 620 621 .readmore-benefits .check svg { 622 width: 13px; 623 height: 13px; 624 fill: #fff; 625 } 626 627 .sheet.readmore .store-button { 628 background: linear-gradient(90deg, #ff9500, #ff3b30); 629 border-radius: 16px; 630 box-shadow: 0 10px 28px rgba(255, 96, 48, 0.35); 631 } 632 633 .sheet.readmore .store-button:hover { 634 opacity: 1; 635 } 636 637 .sheet.readmore .more-info-link { 638 color: #938e82; 639 } 640 641 .sheet.readmore .more-info-link:hover { 642 color: var(--text-primary); 643 } 644 645 .store-button.google-play { 646 background: linear-gradient(180deg, #3a3a3c 0%, #1d1d1f 100%); 647 } 648 649 .store-icon { 650 width: 24px; 651 height: 24px; 652 } 653 654 .more-info-link { 655 display: inline-block; 656 margin-top: 12px; 657 color: var(--text-secondary); 658 text-decoration: none; 659 font-size: 14px; 660 transition: color 0.2s; 661 } 662 663 .more-info-link:hover { 664 color: var(--text-primary); 665 text-decoration: underline; 666 } 667 668 .dots { 669 display: flex; 670 justify-content: center; 671 gap: 8px; 672 margin-top: 16px; 673 } 674 675 .dot { 676 width: 8px; 677 height: 8px; 678 border-radius: 50%; 679 background: rgba(0, 0, 0, 0.14); 680 transition: background 0.3s, transform 0.3s; 681 } 682 683 .dot.active { 684 background: #1d1d1f; 685 transform: scale(1.2); 686 } 687 688 /* Mini mode styles */ 689 .sheet.mini { 690 max-height: 74px; 691 } 692 693 .sheet.mini .sheet-content { 694 display: none; 695 } 696 697 .sheet.mini .sheet-header { 698 border-bottom: none; 699 cursor: pointer; 700 padding: 13px 20px; 701 } 702 703 .sheet.mini .header-subtitle { 704 font-weight: 500; 705 } 706 707 /* Sheet variant mini bar: everything one notch smaller so the 708 74px bar holds icon + title + rating without clipping */ 709 .sheet.mini .app-icon { 710 width: 48px; 711 height: 48px; 712 border-radius: 11px; 713 } 714 715 .sheet.mini .header-title { 716 font-size: 17px; 717 margin: 0 0 2px 0; 718 white-space: nowrap; 719 overflow: hidden; 720 text-overflow: ellipsis; 721 } 722 723 .sheet.mini .header-text { 724 padding-right: 36px; 725 } 726 727 .sheet.mini .rating { 728 margin-top: 0; 729 min-width: 0; 730 } 731 732 .sheet.mini .free-text { 733 white-space: nowrap; 734 overflow: hidden; 735 text-overflow: ellipsis; 736 } 737 738 /* Readmore full mode: no header row at all â just the floating close button */ 739 .sheet.readmore .sheet-header { 740 border-bottom: none; 741 padding: 0; 742 } 743 744 .sheet.readmore .readmore-mini-pitch { 745 display: none; 746 } 747 748 /* Readmore mini mode: clean serif pitch bar instead of icon + rating */ 749 .sheet.mini.readmore .sheet-header { 750 padding: 13px 20px; 751 } 752 753 .sheet.mini.readmore .readmore-mini-pitch { 754 display: flex; 755 align-items: center; 756 gap: 12px; 757 padding-right: 44px;
758 } 759 760 .mini-logo { 761 width: 48px; 762 height: 48px; 763 border-radius: 11px; 764 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); 765 flex-shrink: 0; 766 } 767 768 .mini-lines { 769 display: flex; 770 flex-direction: column; 771 gap: 2px; 772 min-width: 0; 773 } 774 775 .mini-text { 776 font-family: var(--serif); 777 font-size: 16px; 778 font-weight: 700; 779 letter-spacing: 0.01em; 780 color: var(--text-primary); 781 white-space: nowrap; 782 overflow: hidden; 783 text-overflow: ellipsis; 784 } 785 786 .mini-text .accent { 787 background: var(--accent-gradient); 788 -webkit-background-clip: text; 789 background-clip: text; 790 -webkit-text-fill-color: transparent; 791 color: transparent; 792 } 793 794 .mini-sub { 795 font-size: 13px; 796 font-weight: 600; 797 background: var(--accent-gradient); 798 -webkit-background-clip: text; 799 background-clip: text; 800 -webkit-text-fill-color: transparent; 801 color: transparent; 802 white-space: nowrap; 803 overflow: hidden; 804 text-overflow: ellipsis; 805 } 806 807 /* Hide backdrop in mini mode */ 808 .backdrop.mini { 809 display: none; 810 opacity: 0; 811 visibility: hidden; 812 pointer-events: none; 813 } 814 815 @media (max-width: 768px) { 816 .sheet { 817 max-height: 75vh; 818 border-radius: 24px 24px 0 0; 819 } 820 821 .sheet-header { 822 padding: 8px 16px 12px; 823 } 824 825 .sheet-content { 826 padding: 0px 16px; 827 max-height: calc(75vh - 100px); 828 padding-bottom: 0; 829 overflow-y: auto; 830 scrollbar-width: none; /* Firefox */ 831 -ms-overflow-style: none; /* IE and Edge */ 832 } 833 834 .sheet-content::-webkit-scrollbar { 835 display: none; /* Chrome, Safari, Opera */ 836 } 837 838 .cta-section { 839 padding: 12px 0; 840 } 841 842 .store-section { 843 padding: 20px 0 0 0; 844 } 845 846 .store-button { 847 padding: 10px 20px; 848 font-size: 15px; 849 } 850 851 .more-info-link { 852 margin-top: 8px; 853 font-size: 13px; 854 } 855 856 .dots { 857 margin-top: 12px; 858 gap: 6px; 859 } 860 861 .screenshot { 862 width: 135px; 863 height: 240px; 864 margin-left: -67.5px; 865 margin-top: -120px; 866 } 867 868 /* iOS screenshots on mobile */ 869 .screenshot.ios { 870 width: 111px; 871 margin-left: -55.5px; 872 } 873 874 .coverflow-container { 875 height: 270px; 876 margin-bottom: 16px; 877 width: calc(100% + 32px); 878 margin-left: -16px; 879 margin-right: -16px; 880 } 881 882 .nav-button { 883 width: 36px; 884 height: 36px; 885 } 886 887 .nav-button.prev { 888 left: 10px; 889 } 890 891 .nav-button.next { 892 right: 10px; 893 } 894 895 .readmore-copy { 896 padding-top: 8px; 897 } 898 899 .readmore-quotemark { 900 font-size: 68px; 901 height: 38px; 902 } 903 904 .readmore-headline { 905 font-size: 26px; 906 } 907 908 .readmore-body { 909 font-size: 15px; 910 } 911 912 .readmore-benefits { 913 margin-top: 16px; 914 gap: 10px; 915 } 916 917 .readmore-benefits li { 918 font-size: 14px; 919 } 920 921 .sheet.readmore .store-section { 922 padding-bottom: max(16px, env(safe-area-inset-bottom)); 923 } 924 925 .mini-logo { 926 width: 44px; 927 height: 44px; 928 border-radius: 10px; 929 } 930 931 .sheet.mini.readmore .readmore-mini-pitch { 932 gap: 10px; 933 } 934 935 .mini-text { 936 font-size: 14px; 937 } 938 939 .mini-sub { 940 font-size: 12px; 941 } 942 } 943 </style> 944 945 <div class="backdrop"></div> 946 <div class="sheet${this.isReadMore ? " readmore" : ""}"> 947 <div class="sheet-header"> 948 ${ 949 this.isReadMore 950 ? ` 951 <div class="readmore-mini-pitch"> 952 <img class="mini-logo" src="${"https://www.zitatdestages.net/app/sheet/icon/Icon-" + this.language + ".png"}" alt="App Icon"> 953 <div class="mini-lines">
954 <span class="mini-text">${this.language === "de" ? '<span class="accent">Aufgepasst:</span> Beliebte Zitate-App' : '<span class="accent">Heads up:</span> Popular quotes app'}</span> 955 <span class="mini-sub">${this.language === "de" ? "Nur mehr kurz kostenlos" : "Free for a short time only"}</span> 956 </div> 957 </div> 958 ` 959 : ` 960 ${isMini ? "" : '<div class="sheet-handle"></div>'} 961 <div class="header-content"> 962 <a href="#" class="header-link" target="_blank" rel="noopener"> 963 <img class="app-icon" src="${"https://www.zitatdestages.net/app/sheet/icon/Icon-" + this.language + ".png"}" alt="App Icon"> 964 <div class="header-text"> 965 <h1 class="header-title">${appTitle}</h1> 966 <div class="rating"> 967 <div class="rating-stars"> 968 <svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg> 969 <svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg> 970 <svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg> 971 <svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg> 972 <svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" opacity="0.9"/></svg> 973 </div> 974 <span class="rating-text">${this.language === "de" ? "4,9" : "4.9"}</span> 975 <span class="rating-separator">â¢</span> 976 <span class="free-text">${this.freeText}</span> 977 </div> 978 </div> 979 </a> 980 </div> 981 ` 982 } 983 <button class="close-button" aria-label="${isMini ? "Expand" : "Minimize"}"> 984 ${ 985 isMini 986 ? '<svg viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>' 987 : '<svg viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>' 988 } 989 </button> 990 </div> 991 <div class="sheet-content"> 992 ${sheetContentBody} 993 994 <div class="store-section"> 995 <a href="#" class="store-button ${this.isReadMore ? "store-button-lg" : ""} ${this.isAndroid ? "google-play" : ""}" target="_blank" rel="noopener"> 996 ${this.isAndroid ? this.getGooglePlayIcon() : this.getAppStoreIcon()} 997 <span>${this.isReadMore ? this.ctaText : downloadText}</span> 998 </a> 999 <br> 1000 <a href="#" class="more-info-link" target="_blank" rel="noopener"> 1001 ${this.language === "de" ? "Mehr Infos" : "More Information"} 1002 </a> 1003 </div> 1004 </div> 1005 </div> 1006 `; 1007 } 1008 1009 getAppStoreIcon() { 1010 return `<svg class="store-icon" viewBox="0 0 24 24" fill="white"> 1011 <path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/> 1012 </svg>`; 1013 } 1014 1015 getGooglePlayIcon() { 1016 return `<svg class="store-icon" viewBox="0 0 24 24" fill="white"> 1017 <path d="M3,20.5V3.5C3,2.91 3.34,2.39 3.84,2.15L13.69,12L3.84,21.85C3.34,21.6 3,21.09 3,20.5M16.81,15.12L6.05,21.34L14.54,12.85L16.81,15.12M20.16,10.81C20.5,11.08 20.75,11.5 20.75,12C20.75,12.5 20.53,12.9 20.18,13.18L17.89,14.5L15.39,12L17.89,9.5L20.16,10.81M6.05,2.66L16.81,8.88L14.54,11.15L6.05,2.66Z"/> 1018 </svg>`; 1019 } 1020 1021 loadScreenshots() { 1022 const platform = this.isAndroid ? "Android" : "iOS"; 1023 const stage = this.shadowRoot.querySelector(".coverflow-stage"); 1024 const dotsContainer = this.shadowRoot.querySelector(".dots"); 1025 1026 // Update app icon and title 1027 const appIcon = this.shadowRoot.querySelector(".app-icon"); 1028 const headerTitle = this.shadowRoot.querySelector(".header-title"); 1029 const storeButton = this.shadowRoot.querySelector(".store-button span"); 1030 const ratingText = this.shadowRoot.querySelector(".rating-text"); 1031 const freeText = this.shadowRoot.querySelector(".free-text"); 1032 1033 if (appIcon) { 1034 appIcon.src = "https://www.zitatdestages.net/app/sheet/icon/Icon-" + this.language + ".png"; 1035 } 1036 if (headerTitle) { 1037 headerTitle.textContent = this.isReadMore 1038 ? this.language === "de" 1039 ? "Das Zitat des Tages ist besser in der App" 1040 : "The Quote of the Day is better in the app" 1041 : this.language === "de" 1042 ? "A-Z Zitat des Tages" 1043 : "Literally: Quotes and Thoughts"; 1044 } 1045 if (ratingText) { 1046 ratingText.textContent = this.language === "de" ? "4,9" : "4.9"; 1047 } 1048 if (freeText) { 1049 freeText.textContent = this.freeText; 1050 } 1051 if (storeButton) { 1052 storeButton.textContent = this.ctaText; 1053 } 1054 1055 // Update more info link text 1056 const moreInfoLink = this.shadowRoot.querySelector(".more-info-link"); 1057 if (moreInfoLink) { 1058 moreInfoLink.textContent = this.language === "de" ? "Mehr Infos" : "More Information"; 1059 } 1060 1061 // Update store links when language changes 1062 this.updateStoreLinks(); 1063 1064 // Only load screenshots if stage and dots exist (not in mini mode) 1065 if (!stage || !dotsContainer) return; 1066 1067 stage.innerHTML = ""; 1068 dotsContainer.innerHTML = ""; 1069 1070 for (let i = 1; i <= 10; i++) { 1071 const screenshot = document.createElement("div"); 1072 screenshot.className = "screenshot"; 1073 if (!this.isAndroid) { 1074 screenshot.classList.add("ios"); 1075 } 1076 screenshot.dataset.index = i - 1; 1077 1078 const img = document.createElement("img"); 1079 img.src = "https://www.zitatdestages.net/app/sheet/images/" + platform
1079+ "/" + this.language + "/" + String(i).padStart(2, "0") + ".jpeg"; 1080 img.alt = "Screenshot " + i; 1081 img.loading = "lazy"; 1082 1083 screenshot.appendChild(img); 1084 1085 // Make screenshot clickable - go to store 1086 screenshot.addEventListener("click", (e) => { 1087 e.stopPropagation(); 1088 const storeUrl = this.getStoreUrl(); 1089 window.open(storeUrl, "_blank", "noopener,noreferrer"); 1090 }); 1091 1092 stage.appendChild(screenshot); 1093 1094 // Create dot 1095 const dot = document.createElement("div"); 1096 dot.className = "dot"; 1097 if (i === 1) dot.classList.add("active"); 1098 dot.dataset.index = i - 1; 1099 dotsContainer.appendChild(dot); 1100 } 1101 1102 this.updateCarousel(); 1103 } 1104 1105 setupEventListeners() { 1106 if (this.isDialog && !this.dialogSeen) { 1107 this.shadowRoot.querySelector(".close")?.addEventListener("click", () => { 1108 this.removeAttribute("open"); 1109 }); 1110 1111 this.shadowRoot.querySelector(".backdrop")?.addEventListener("click", (e) => { 1112 if (e.target.classList.contains("backdrop")) { 1113 this.removeAttribute("open"); 1114 } 1115 }); 1116 1117 return; 1118 } 1119 1120 const backdrop = this.shadowRoot.querySelector(".backdrop"); 1121 const closeBtn = this.shadowRoot.querySelector(".close-button"); 1122 const headerLink = this.shadowRoot.querySelector(".header-link"); 1123 const sheetHeader = this.shadowRoot.querySelector(".sheet-header"); 1124 const prevBtn = this.shadowRoot.querySelector(".nav-button.prev"); 1125 const nextBtn = this.shadowRoot.querySelector(".nav-button.next"); 1126 const stage = this.shadowRoot.querySelector(".coverflow-stage"); 1127 const storeButton = this.shadowRoot.querySelector(".store-button"); 1128 1129 backdrop.addEventListener("click", () => { 1130 // Backdrop only exists in full mode, so always switch to mini 1131 this.switchToMini(); 1132 }); 1133 1134 closeBtn.addEventListener("click", () => { 1135 if (this.hasAttribute("mini")) { 1136 this.expandFromMini(); 1137 } else { 1138 this.switchToMini(); 1139 } 1140 }); 1141 1142 if (prevBtn) prevBtn.addEventListener("click", () => this.navigate(-1)); 1143 if (nextBtn) nextBtn.addEventListener("click", () => this.navigate(1)); 1144 1145 // Mini mode: swipe up or click header to expand 1146 // Full mode: swipe down on header to switch to mini 1147 if (sheetHeader) { 1148 let startY = 0; 1149 sheetHeader.addEventListener("touchstart", (e) => { 1150 startY = e.touches[0].clientY; 1151 }); 1152 sheetHeader.addEventListener("touchend", (e) => { 1153 const endY = e.changedTouches[0].clientY; 1154 const diffY = startY - endY; 1155 1156 if (this.hasAttribute("mini")) { 1157 // Swipe up to expand from mini mode 1158 if (diffY > 40) { 1159 this.expandFromMini(); 1160 } 1161 } else { 1162 // Swipe down to switch to mini mode 1163 if (diffY < -40) { 1164 this.switchToMini(); 1165 } 1166 } 1167 }); 1168 sheetHeader.addEventListener("click", (e) => { 1169 // Readmore: taps go to the store (handled by the sheet-wide listener below) 1170 if (!this.isReadMore && this.hasAttribute("mini") && !e.target.closest(".close-button")) { 1171 this.expandFromMini(); 1172 } 1173 }); 1174 } 1175 1176 // Readmore: the entire sheet is one big store link â any tap opens the store, 1177 // except the chevron button (minimize/expand) and real links (they navigate themselves) 1178 const sheetEl = this.shadowRoot.querySelector(".sheet"); 1179 if (this.isReadMore && sheetEl) { 1180 sheetEl.addEventListener("click", (e) => { 1181 if (e.target.closest(".close-button") || e.target.closest("a")) return; 1182 window.open(this.getStoreUrl(), "_blank", "noopener,noreferrer"); 1183 }); 1184 } 1185 1186 // Touch/swipe support for carousel 1187 let startX = 0; 1188 let isDragging = false; 1189 1190 if (stage) { 1191 stage.addEventListener("touchstart", (e) => { 1192 startX = e.touches[0].clientX; 1193 isDragging = true; 1194 }); 1195 1196 stage.addEventListener( 1197 "touchmove", 1198 (e) => { 1199 if (!isDragging) return; 1200 e.preventDefault(); 1201 }, 1202 { passive: false }, 1203 ); 1204 1205 stage.addEventListener("touchend", (e) => { 1206 if (!isDragging) return; 1207 const endX = e.changedTouches[0].clientX; 1208 const diff = startX - endX; 1209 1210 if (Math.abs(diff) > 50) { 1211 this.navigate(diff > 0 ? 1 : -1); 1212 } 1213 isDragging = false; 1214 }); 1215 1216 // Mouse drag support 1217 stage.addEventListener("mousedown", (e) => { 1218 startX = e.clientX; 1219 isDragging = true; 1220 }); 1221 1222 stage.addEventListener("mousemove", (e) => { 1223 if (!isDragging) return; 1224 e.preventDefault(); 1225 }); 1226 1227 stage.addEventListener("mouseup", (e) => { 1228 if (!isDragging) return; 1229 const endX = e.clientX; 1230 const diff = startX - endX; 1231 1232 if (Math.abs(diff) > 50) { 1233 this.navigate(diff > 0 ? 1 : -1); 1234 } 1235 isDragging = false; 1236 }); 1237 1238 stage.addEventListener("mouseleave", () => { 1239 isDragging = false; 1240 }); 1241 } 1242 1243 // Dot click navigation 1244 const dotsContainer = this.shadowRoot.querySelector(".dots"); 1245 if (dotsContainer) { 1246 dotsContainer.addEventListener("click", (e) => { 1247 if (e.target.classList.contains("dot")) { 1248 this.currentIndex = parseInt(e.target.dataset.index); 1249 this.updateCarousel(); 1250 } 1251 }); 1252 } 1253 1254 // Mouse wheel scrolling 1255 const container = this.shadowRoot.querySelector(".coverflow-container"); 1256 if (container) { 1257 container.addEventListener( 1258 "wheel", 1259 (e) => { 1260 e.preventDefault(); 1261 if (Math.abs(e.deltaY) > 10) { 1262 this.navigate(e.deltaY > 0 ? 1 : -1); 1263 } 1264 }, 1265 { passive: false }, 1266 ); 1267 } 1268 1269 // Keyboard navigation (registered once on the host; setupEventListeners 1270 // can be called repeatedly on re-render, so guard against stacking listeners)
1271 if (!this._keydownHandler) { 1272 this._keydownHandler = (e) => { 1273 if (e.key === "ArrowLeft") { 1274 this.navigate(-1); 1275 } else if (e.key === "ArrowRight") { 1276 this.navigate(1); 1277 } 1278 }; 1279 this.addEventListener("keydown", this._keydownHandler); 1280 } 1281 1282 // Update store links 1283 this.updateStoreLinks(); 1284 } 1285 1286 expandFromMini() { 1287 // Simply remove the mini attribute - the reactive system will handle the rest 1288 this.removeAttribute("mini"); 1289 } 1290 1291 switchToMini() { 1292 // Switch from full mode to mini mode 1293 this.setAttribute("mini", ""); 1294 this.setAttribute("open", ""); 1295 } 1296 1297 getStoreUrl() { 1298 if (this.language === "de") { 1299 // German app: "A-Z Zitat des Tages" 1300 if (this.isAndroid) { 1301 return "https://play.google.com/store/apps/details?id=at.chweb.zitatdestages"; 1302 } else { 1303 return "https://apps.apple.com/app/zitat-des-tages-2025/id6464222313"; 1304 } 1305 } else { 1306 // English app: "Literally: Quotes and Thoughts" 1307 if (this.isAndroid) { 1308 return "https://play.google.com/store/apps/details?id=literally.cc"; 1309 } else { 1310 return "https://apps.apple.com/app/literally-quotes-and-thoughts/id6504204090"; 1311 } 1312 } 1313 } 1314 1315 updateStoreLinks() { 1316 const storeButton = this.shadowRoot.querySelector(".store-button"); 1317 const headerLink = this.shadowRoot.querySelector(".header-link"); 1318 const moreInfoLink = this.shadowRoot.querySelector(".more-info-link"); 1319 const storeUrl = this.getStoreUrl(); 1320 1321 if (storeButton) { 1322 storeButton.href = storeUrl; 1323 } 1324 if (headerLink) { 1325 headerLink.href = storeUrl; 1326 } 1327 if (moreInfoLink) { 1328 moreInfoLink.href = storeUrl; 1329 } 1330 } 1331 1332 navigate(direction) { 1333 this.currentIndex += direction; 1334 const screenshots = this.shadowRoot.querySelectorAll(".screenshot"); 1335 1336 if (this.currentIndex < 0) { 1337 this.currentIndex = screenshots.length - 1; 1338 } else if (this.currentIndex >= screenshots.length) { 1339 this.currentIndex = 0; 1340 } 1341 1342 this.updateCarousel(); 1343 } 1344 1345 updateCarousel() { 1346 const screenshots = this.shadowRoot.querySelectorAll(".screenshot"); 1347 const dots = this.shadowRoot.querySelectorAll(".dot"); 1348 const total = screenshots.length; 1349 1350 screenshots.forEach((screenshot, index) => { 1351 const position = index - this.currentIndex; 1352 const absPosition = Math.abs(position); 1353 1354 let translateX = position * 150; 1355 let translateZ = -absPosition * 180; 1356 let rotateY = position * -35; 1357 let scale = 1 - absPosition * 0.15; 1358 let opacity = absPosition === 0 ? 1 : 0.6 - absPosition * 0.1; 1359 let zIndex = total - absPosition; 1360 1361 if (absPosition > 2) { 1362 opacity = 0; 1363 scale = 0.7; 1364 } 1365 1366 screenshot.style.transform = ` 1367 translateX(${translateX}px) 1368 translateZ(${translateZ}px) 1369 rotateY(${rotateY}deg) 1370 scale(${scale}) 1371 `; 1372 screenshot.style.opacity = opacity; 1373 screenshot.style.zIndex = zIndex; 1374 }); 1375 1376 // Update dots 1377 dots.forEach((dot, index) => { 1378 dot.classList.toggle("active", index === this.currentIndex); 1379 }); 1380 } 1381 1382 open() { 1383 this.setAttribute("open", ""); 1384 } 1385 1386 close() { 1387 this.removeAttribute("open"); 1388 } 1389 1390 updateVisibility() { 1391 if (this.isDialog && !this.dialogSeen) return; 1392 1393 const backdrop = this.shadowRoot.querySelector(".backdrop"); 1394 const sheet = this.shadowRoot.querySelector(".sheet"); 1395 const isOpen = this.hasAttribute("open"); 1396 const isMini = this.hasAttribute("mini"); 1397 1398 if (isOpen || isMini) { 1399 sheet.classList.add("show"); 1400 1401 if (isMini) { 1402 sheet.classList.add("mini"); 1403 backdrop.classList.add("mini"); 1404 backdrop.classList.remove("show"); 1405 } else { 1406 sheet.classList.remove("mini"); 1407 backdrop.classList.remove("mini"); 1408 backdrop.classList.add("show"); 1409 } 1410 } else { 1411 backdrop.classList.remove("show"); 1412 backdrop.classList.remove("mini"); 1413 sheet.classList.remove("show"); 1414 sheet.classList.remove("mini"); 1415 } 1416 } 1417 1418 renderDialog() { 1419 if (this._dialogTimer) { 1420 clearInterval(this._dialogTimer); 1421 this._dialogTimer = null; 1422 } 1423 1424 const isGerman = this.language === "de"; 1425 const isIOS = !this.isAndroid;
1426 const storeUrl = this.getStoreUrl(); 1427 1428 const title = isGerman ? "Zitate als Video exportieren" : "Quotes as Video"; 1429 1430 const buttonText = isGerman ? (isIOS ? "Jetzt gratis im App Store" : "Jetzt gratis bei Google Play") : "Download Free"; 1431 1432 this.shadowRoot.innerHTML = ` 1433 <style> 1434 * { 1435 box-sizing: border-box; 1436 } 1437 1438 .backdrop { 1439 position: fixed; 1440 inset: 0; 1441 background: rgba(0,0,0,.75); 1442 backdrop-filter: blur(6px); 1443 display: flex; 1444 justify-content: center; 1445 align-items: center; 1446 padding: 20px; 1447 z-index: 999999; 1448 } 1449 1450 .dialog { 1451 position: relative; 1452 width: min(560px, 100%); 1453 background: #000; 1454 border-radius: 28px; 1455 overflow: hidden; 1456 box-shadow: 0 30px 80px rgba(0,0,0,.5); 1457 } 1458 1459 .close-button { 1460 position: absolute; 1461 top: 12px; 1462 right: 12px; 1463 1464 width: 42px; 1465 height: 42px; 1466 1467 border: none; 1468 border-radius: 50%; 1469 1470 background: rgba(255,255,255,.15); 1471 color: white; 1472 1473 font-size: 18px; 1474 font-weight: 700; 1475 1476 z-index: 10; 1477 cursor: pointer; 1478 1479 display: flex; 1480 align-items: center; 1481 justify-content: center; 1482 } 1483 1484 .close-button:disabled { 1485 cursor: default; 1486 opacity: .9; 1487 } 1488 1489 .content { 1490 padding: 24px; 1491 } 1492 1493 .title { 1494 color: white; 1495 text-align: center; 1496 font-size: 28px; 1497 font-weight: 800; 1498 line-height: 1.1; 1499 margin-top: 10px; 1500 } 1501 1502 .hero-image { 1503 width: 100%; 1504 display: block; 1505 border-radius: 18px; 1506 cursor: pointer; 1507 } 1508 1509 .download-button { 1510 display: flex; 1511 align-items: center; 1512 justify-content: center; 1513 1514 width: 100%; 1515 min-height: 58px; 1516 1517 margin-top: 20px; 1518 1519 border-radius: 16px; 1520 1521 background: linear-gradient( 1522 90deg, 1523 #ff9500, 1524 #ff3b30 1525 ); 1526 1527 color: white; 1528 text-decoration: none; 1529 1530 font-size: 18px; 1531 font-weight: 800; 1532 1533 transition: transform .15s ease; 1534 } 1535 1536 .download-button:hover { 1537 transform: translateY(-2px); 1538 } 1539 1540 @media (max-width: 600px) { 1541 .content { 1542 padding: 16px; 1543 } 1544 1545 .title { 1546 font-size: 22px; 1547 } 1548 1549 .subtitle { 1550 font-size: 15px; 1551 } 1552 1553 .download-button { 1554 font-size: 16px; 1555 } 1556 } 1557 </style> 1558 1559 <div class="backdrop"> 1560 <div class="dialog"> 1561 1562 <button 1563 class="close-button" 1564 disabled 1565 aria-label="Close" 1566 > 1567 <span class="countdown">5</span> 1568 </button> 1569 1570 <div class="content"> 1571 <img 1572 class="hero-image" 1573 src="https://www.zitatdestages.net/app/sheet/export-video.png" 1574 alt="Video Export" 1575 > 1576 1577 <a 1578 href="${storeUrl}" 1579 target="_blank" 1580 rel="noopener" 1581 class="download-button" 1582 > 1583 ${buttonText} 1584 </a> 1585 1586 </div> 1587 </div> 1588 </div> 1589 `; 1590 1591 const closeButton = this.shadowRoot.querySelector(".close-button"); 1592 const countdown = this.shadowRoot.querySelector(".countdown"); 1593 const backdrop = this.shadowRoot.querySelector(".backdrop"); 1594 const heroImage = this.shadowRoot.querySelector(".hero-image"); 1595 1596 const closeDialog = () => { 1597 clearInterval(this._dialogTimer); 1598 this._dialogTimer = null; 1599 document.cookie = "promo_video_export_v1=1; path=/; max-age=86400; SameSite=Lax"; 1600 this.render(); 1601 this.loadScreenshots(); 1602 this.setupEventListeners(); 1603 this.setAttribute("mini", ""); 1604 this.setAttribute("open", ""); 1605 this.updateVisibility(); 1606 }; 1607 1608 heroImage?.addEventListener("click", () => { 1609 window.open(storeUrl, "_blank", "noopener,noreferrer"); 1610 });
1611 1612 let seconds = 5; 1613 this._dialogTimer = setInterval(() => { 1614 seconds--; 1615 if (seconds > 0) { 1616 countdown.textContent = seconds; 1617 return; 1618 } 1619 clearInterval(this._dialogTimer); 1620 this._dialogTimer = null; 1621 closeButton.disabled = false; 1622 countdown.textContent = "â"; 1623 }, 1000); 1624 1625 closeButton.addEventListener("click", () => { 1626 if (closeButton.disabled) return; 1627 closeDialog(); 1628 }); 1629 1630 backdrop.addEventListener("click", (e) => { 1631 if (e.target === backdrop && !closeButton.disabled) { 1632 closeDialog(); 1633 } 1634 }); 1635 } 1636 1637 disconnectedCallback() { 1638 if (this._dialogTimer) { 1639 clearInterval(this._dialogTimer); 1640 this._dialogTimer = null; 1641 } 1642 } 1643} 1644 1645customElements.define("app-sheet", AppSheet);
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.