diff --git a/RELEASES-COMMITS-MAP.md b/RELEASES-COMMITS-MAP.md index 0d1af2a..f62b961 100644 --- a/RELEASES-COMMITS-MAP.md +++ b/RELEASES-COMMITS-MAP.md @@ -38,3 +38,4 @@ Ship recipe: **`HOWTO-DEVEL-DEPLOY.md`**. - `04b43a0` fix: music templates by tag overlap - uncommitted: `DeskApp.Sync()` matches `downloadAll` (disabled / master-off clips) | 0.6.81 | `02aff5715a386a68333dc65aa44af05836e6adb3` | dirty | 2026-09-15T21:06:49-03:00 | `0a0266909c800a58620b3e0c8a47919a8315000a19accc1c1405f76fc6e0b113` | build | +| 0.7.0 | `60b19e108776101a1e3c038c9a6819da01c8e6cf` | dirty | 2026-09-15T22:36:42-03:00 | `9098dfbf31e60e9fada3141d0b0ae91f9d5b5b4e5c0ba2dfaf46a72794c704a2` | build | diff --git a/frontend/index.html b/frontend/index.html index 4d26e33..8543d86 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,5 +1,5 @@ - + @@ -7,88 +7,96 @@
-
-
-
- - + -
- - - -
- -
- -
-
- — - Rádio Indoor - - Parado +
+
+
+
+
Rádio Indoor
+
+ — + + Parado +
-
+
+
+ +
0:00 -
+
-
- - - 85 + + 85
- - -
- +
diff --git a/frontend/src/app.css b/frontend/src/app.css index 2b95e76..a1b3e07 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1,407 +1,239 @@ -/* Indoor Radio — theme layer over the Industry tokens. Vanilla CSS, no build step. */ - -:root, [data-theme="light"] { - --color-bg: #f2f2f3; - --color-surface: #e9e9ea; - --color-text: #1d1f20; - --color-accent: #5980a6; - --color-divider: color-mix(in srgb, #1d1f20 16%, transparent); - --muted: color-mix(in srgb, #1d1f20 52%, transparent); - --tint: color-mix(in srgb, #1d1f20 5%, transparent); - --tint-strong: color-mix(in srgb, #1d1f20 10%, transparent); - --accent-tint: color-mix(in srgb, #5980a6 12%, transparent); - --onbrand: #f2f2f3; - --field: #1d2d3d; -} - -[data-theme="dark"] { - --color-bg: #17191a; - --color-surface: #202325; - --color-text: #eff0f2; - --color-accent: #749dc4; - --color-divider: color-mix(in srgb, #eff0f2 18%, transparent); - --muted: color-mix(in srgb, #eff0f2 52%, transparent); - --tint: color-mix(in srgb, #eff0f2 7%, transparent); - --tint-strong: color-mix(in srgb, #eff0f2 13%, transparent); - --accent-tint: color-mix(in srgb, #749dc4 16%, transparent); - --onbrand: #14171a; - --field: #0f1418; +/* Spotify-style dark theme. Tokens from changes-request/004-layout-change/DESIGN.md. */ +@font-face { + font-family: "Figtree"; + font-style: normal; + font-weight: 300 900; + font-display: swap; + src: url("./assets/fonts/figtree-latin-variable.woff2") format("woff2"); +} + +:root { + --bg: #121212; + --surface: #181818; + --mid: #1f1f1f; + --card-hover: #2a2a2a; + --text: #ffffff; + --muted: #b3b3b3; + --border: #4d4d4d; + --border-light: #7c7c7c; + --green: #1ed760; + --green-border: #1db954; + --red: #f3727f; + --orange: #ffa42b; + --blue: #539df5; + --shadow-heavy: rgba(0, 0, 0, .5) 0 8px 24px; + --shadow-mid: rgba(0, 0, 0, .3) 0 8px 8px; + --inset: rgb(18, 18, 18) 0 1px 0, rgb(124, 124, 124) 0 0 0 1px inset; + --font: "Figtree", "Helvetica Neue", Helvetica, Arial, sans-serif; + --side-w: 300px; + --bar-h: 96px; } -html, body { height: 100%; } +* { box-sizing: border-box; } +html, body { height: 100%; margin: 0; } body { - margin: 0; - background: var(--color-bg); - color: var(--color-text); - font-family: var(--font-body); + background: #000; + color: var(--text); + font-family: var(--font); + font-size: 14px; + -webkit-font-smoothing: antialiased; overflow: hidden; - -webkit-user-select: none; user-select: none; } -* { box-sizing: border-box; } -a { color: var(--color-accent); text-decoration: none; } -a:hover { color: var(--color-accent-700); } -::-webkit-scrollbar { width: 10px; } -::-webkit-scrollbar-thumb { background: var(--tint-strong); } -::-webkit-scrollbar-track { background: transparent; } - -.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; } -.kicker { - font-family: var(--font-heading); - font-size: 11px; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--muted); -} - -.icon-btn { - display: inline-flex; align-items: center; justify-content: center; - width: 34px; height: 34px; - border: 1px solid var(--color-divider); - background: transparent; color: var(--color-text); - cursor: pointer; -} -.icon-btn:hover { background: var(--tint); } -.icon-btn:active { background: var(--tint-strong); } -.icon-btn[aria-pressed="true"] { background: var(--tint-strong); } - -/* spot buttons — blueprint cells, the one grid that repeats */ -.spot { - position: relative; - display: flex; flex-direction: column; gap: 10px; - min-height: 118px; padding: 13px; - border: 1px solid var(--color-divider); - background: transparent; color: var(--color-text); - font-family: var(--font-body); text-align: left; cursor: pointer; -} -.spot:hover { background: var(--tint); } -.spot:active { background: var(--tint-strong); } -.spot[aria-current="true"] { border-color: var(--color-accent); background: var(--accent-tint); } -.spot[disabled] { opacity: 0.45; cursor: not-allowed; } -.spot.is-pending, .spot.is-failed { opacity: 1; cursor: not-allowed; } -.spot-veil { - position: absolute; inset: 0; z-index: 2; - display: flex; align-items: center; justify-content: center; - padding: 12px; text-align: center; - background: color-mix(in srgb, var(--color-canvas, #111) 78%, transparent); - font-family: var(--font-body); font-size: 13px; line-height: 1.3; - color: var(--color-text); -} -.spot-title { font-family: var(--font-heading); font-size: 18px; line-height: 1.14; } -.spot-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; } -.spot-foot { margin-top: auto; } -.spot.has-art { - background-size: cover; - background-position: center; - background-repeat: no-repeat; - min-height: 140px; - justify-content: flex-end; -} -.spot.has-art.has-label::before { - content: ""; - position: absolute; - inset: 0; - z-index: 0; - pointer-events: none; - background: linear-gradient(to top, color-mix(in srgb, var(--color-bg) 72%, transparent), transparent 58%); -} -.spot.has-art .spot-title, -.spot.has-art .kicker, -.spot.has-art .spot-foot { - position: relative; - z-index: 1; - color: var(--color-text); -} - -.filter { - font-family: var(--font-heading); font-size: 13px; letter-spacing: 0.06em; - text-transform: uppercase; padding: 6px 12px; - border: 1px solid var(--color-divider); background: transparent; - color: var(--muted); cursor: pointer; -} -.filter:hover { background: var(--tint); color: var(--color-text); } -.filter[aria-pressed="true"] { background: var(--color-accent); border-color: var(--color-accent); color: var(--onbrand); } - -.slider { width: 104px; height: 24px; display: flex; align-items: center; cursor: pointer; } -.slider-track { position: relative; width: 100%; height: 4px; background: var(--tint-strong); } -.slider-fill { display: block; height: 100%; background: var(--color-text); } -.slider-knob { position: absolute; top: -4px; width: 2px; height: 12px; background: var(--color-text); } - -:focus { outline: none; } -:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; } - -@keyframes bars { 0%, 100% { transform: scaleY(0.3); } 50% { transform: scaleY(1); } } -@keyframes chime { 0% { opacity: 0.25; } 50% { opacity: 1; } 100% { opacity: 0.25; } } -.eq { - display: flex; align-items: flex-end; gap: 2px; - height: 14px; flex: none; -} -.eq[hidden] { display: none; } -.eq i { - display: block; width: 2px; height: 14px; - background: var(--color-accent); transform-origin: bottom; - animation: bars 900ms ease-in-out infinite; -} -.eq i:nth-child(2) { animation-delay: 140ms; } -.eq i:nth-child(3) { animation-delay: 280ms; } -.eq i:nth-child(4) { animation-delay: 420ms; } -.chiming { animation: chime 700ms ease-in-out infinite; } - -/* hover states declarados aqui (o export estático não usa atributos style-hover) */ -#chimeToggle:hover { background: var(--tint); } -#play:hover { filter: brightness(1.08); } - -.shell { - position: relative; - width: 100%; height: 100vh; min-width: 980px; min-height: 640px; - display: grid; - grid-template-rows: 66px auto 54px minmax(0, 1fr) 112px; - grid-template-areas: - "top" - "update" - "toolbar" - "main" - "bar"; - background: var(--color-bg); color: var(--color-text); overflow: hidden; -} -.top { grid-area: top; } -.update-bar { grid-area: update; } -.toolbar { grid-area: toolbar; } -#main { grid-area: main; } -.bar { grid-area: bar; } -.top { - display: grid; grid-template-columns: auto 1fr auto; align-items: center; - gap: 20px; padding: 0 20px; border-bottom: 1px solid var(--color-divider); -} -.brand-block { display: flex; align-items: center; gap: 16px; } -.logo-slot { - width: 168px; height: 42px; display: flex; align-items: center; justify-content: center; - overflow: hidden; -} -.logo-slot img { max-height: 40px; max-width: 160px; object-fit: contain; } -.logo-ph { font-size: 10px; letter-spacing: 0.16em; color: var(--muted); } -.store-name { font-family: var(--font-heading); font-size: 17px; line-height: 1.1; } -.store-unit-row { display: flex; align-items: center; gap: 6px; margin-top: 2px; } -.store-unit { font-size: 11px; letter-spacing: 0.08em; color: var(--muted); } -.unit-copy { width: 22px; height: 22px; flex: 0 0 22px; } -.unit-copy svg { display: block; } -.top-right { display: flex; align-items: center; gap: 12px; } -.conn-pill { - display: flex; align-items: center; gap: 7px; padding: 5px 11px; - border: 1px solid var(--color-divider); -} -.conn-pill i { width: 7px; height: 7px; display: block; background: var(--muted); } -.conn-pill.ok i { background: var(--color-accent); } -.clock { font-size: 12px; color: var(--muted); } - -.update-bar { - display: flex; align-items: center; gap: 14px; padding: 10px 20px; - background: var(--field); color: #f2f2f3; font-size: 14px; -} -.update-bar[hidden] { display: none; } -.update-bar.warn { background: #3a2a12; color: #f2c56b; } - -.toolbar { - display: flex; align-items: center; gap: 16px; padding: 0 20px; - border-bottom: 1px solid var(--color-divider); -} -.filters { display: flex; gap: 6px; } -.grid-count { font-size: 11px; color: var(--muted); margin-left: auto; } -.sync-mini { - display: flex; flex-direction: column; gap: 3px; - flex: 1; min-width: 220px; max-width: 460px; -} -.sync-line { display: flex; align-items: center; gap: 8px; } -.sync-track { flex: 1; height: 4px; background: var(--tint-strong); } -.sync-track i { display: block; height: 100%; width: 0; background: var(--color-accent); } -.sync-detail { font-size: 11px; color: var(--muted); line-height: 1.3; } -#chimeToggle { - display: flex; align-items: center; gap: 9px; padding: 5px 11px; - border: 1px solid var(--color-divider); background: transparent; - color: var(--color-text); cursor: pointer; -} -#chimeState { font-family: var(--font-heading); font-size: 13px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-accent); } - -#main { overflow: auto; padding: 16px 20px 20px; } -#main.grid-spots { - display: grid; grid-template-columns: repeat(auto-fill, minmax(238px, 1fr)); - gap: 12px; align-content: start; -} - -.hist-line { - display: grid; - grid-template-columns: 58px 88px minmax(0, 1fr); - grid-template-areas: "time kind title"; - align-items: center; gap: 14px; padding: 8px 4px; - border-top: 1px solid color-mix(in srgb, var(--color-text) 7%, transparent); -} -.hist-time { grid-area: time; } -.hist-line .badge { grid-area: kind; } -.hist-title { grid-area: title; } -.hist-line .badge { - font-family: var(--font-heading); font-size: 10px; letter-spacing: 0.12em; - text-transform: uppercase; text-align: center; padding: 2px 0; - border: 1px solid var(--color-divider); color: var(--muted); -} -.hist-line .badge:empty { border-color: transparent; } -.hist-line .badge.is-spot { background: color-mix(in srgb, var(--color-text) 8%, transparent); } -.hist-title { - display: block; width: 100%; min-width: 0; - margin: 0; padding: 0; border: 0; background: none; - color: inherit; font: inherit; text-align: left; cursor: pointer; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; -} -.hist-title:hover { color: var(--color-accent); } - -.call-wrap { max-width: 520px; display: flex; flex-direction: column; gap: 14px; } -.call-tabs { display: flex; gap: 6px; } -.call-wrap label { font-family: var(--font-heading); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); } -.call-wrap input:not([type="radio"]), .call-wrap select, .call-wrap textarea { - width: 100%; padding: 8px 10px; border: 1px solid var(--color-divider); - background: var(--color-surface); color: var(--color-text); font-family: var(--font-body); font-size: 15px; -} -.gender-pick { display: flex; gap: 8px; } -.gender-pick label { - flex: 1; display: flex; flex-direction: row; align-items: center; gap: 8px; - margin: 0; padding: 8px 12px; border: 1px solid var(--color-divider); - text-transform: none; letter-spacing: 0; font-size: 15px; font-family: var(--font-body); - color: var(--color-text); cursor: pointer; background: var(--color-surface); -} -.gender-pick input[type="radio"] { width: auto; padding: 0; accent-color: var(--color-accent); } -.gender-pick label:has(input:checked) { border-color: var(--color-accent); background: var(--accent-tint); } -.call-wrap textarea { min-height: 90px; resize: vertical; } -.call-actions { display: flex; gap: 10px; align-items: center; } - -.bar { - display: grid; grid-template-columns: 88px 1fr 260px; align-items: center; - gap: 22px; padding: 0 20px; border-top: 1px solid var(--color-divider); - background: var(--color-surface); - height: 112px; min-height: 112px; max-height: 112px; -} -.play-btn { - width: 72px; height: 78px; display: flex; flex-direction: column; - align-items: center; justify-content: center; gap: 4px; - border: 1px solid var(--color-accent); background: var(--color-accent); color: var(--onbrand); cursor: pointer; -} -.play-label { - font-family: var(--font-heading); font-size: 11px; letter-spacing: 0.08em; - text-transform: uppercase; line-height: 1; -} -.now { display: flex; flex-direction: column; gap: 8px; min-width: 0; } -.now-row { display: flex; align-items: center; gap: 12px; min-width: 0; } -.np-kind { font-size: 10px; padding: 3px 8px; border: 1px solid var(--color-divider); flex: none; } -#npTitle { font-family: var(--font-heading); font-size: 21px; line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.np-state { font-size: 11px; color: var(--muted); margin-left: auto; flex: none; } -.prog { flex: 1; height: 4px; background: var(--tint-strong); } -.prog i { display: block; height: 100%; width: 0; background: var(--color-accent); } -.vol-wrap { display: flex; align-items: center; justify-content: flex-end; gap: 12px; } -.mute-btn { - display: inline-flex; flex-direction: column; align-items: center; - width: auto; min-width: 44px; height: auto; padding: 6px 8px; - gap: 4px; -} -.mute-label { - font-family: var(--font-heading); font-size: 10px; letter-spacing: 0.08em; - text-transform: uppercase; line-height: 1; -} -#vol { width: 104px; accent-color: var(--color-text); } -#volRead { font-size: 11px; color: var(--muted); width: 28px; text-align: right; } - -.state-banner { - position: fixed; - left: 50%; top: 42%; - transform: translate(-50%, -50%); - z-index: 40; - pointer-events: none; - padding: 14px 28px; - background: color-mix(in srgb, var(--color-text) 88%, transparent); - color: var(--color-bg); - font-family: var(--font-heading); - font-size: 18px; - letter-spacing: 0.04em; - text-align: center; - max-width: 72%; -} -.state-banner-mute { top: 52%; } -.state-banner[hidden] { display: none; } - -.debug-overlay { - position: fixed; inset: 0; z-index: 80; - background: color-mix(in srgb, #000 45%, transparent); - display: flex; align-items: center; justify-content: center; - padding: 24px; -} -.debug-overlay[hidden] { display: none; } -.debug-panel { - width: min(560px, 100%); - max-height: min(80vh, 720px); - overflow: auto; - background: var(--color-surface); - color: var(--color-text); - border: 1px solid var(--color-divider); - padding: 16px 18px 18px; -} -.debug-head { - display: flex; align-items: center; justify-content: space-between; - gap: 12px; margin-bottom: 12px; -} -.debug-head b { font-family: var(--font-heading); font-size: 16px; letter-spacing: 0.06em; text-transform: uppercase; } -.debug-body { font-size: 13px; line-height: 1.45; } -.debug-row { - display: grid; grid-template-columns: 148px 1fr; gap: 8px; - padding: 5px 0; border-top: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent); -} +button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; } +button:disabled { cursor: default; } +:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; } +.mono { font-variant-numeric: tabular-nums; letter-spacing: .02em; } +.kicker { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.4px; color: var(--muted); } +.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } +[hidden] { display: none !important; } + +/* shell */ +.shell { height: 100vh; display: grid; grid-template-rows: 1fr var(--bar-h); } +.body { display: grid; grid-template-columns: var(--side-w) 1fr; min-height: 0; padding: 8px 8px 0; gap: 8px; } +@media (max-width: 1100px) { :root { --side-w: 240px; } } + +/* sidebar */ +.side { background: var(--bg); border-radius: 8px; display: flex; flex-direction: column; min-height: 0; } +.brand { padding: 20px 20px 12px; display: flex; flex-direction: column; gap: 14px; } +.logo-slot { height: 44px; display: flex; align-items: center; justify-content: center; } +.logo-slot img { max-height: 44px; max-width: 220px; width: auto; } +.logo-ph { font-size: 10px; letter-spacing: .16em; color: var(--muted); } +.store-name { font-size: 16px; font-weight: 700; line-height: 1.2; } +.store-unit-row { display: flex; align-items: center; gap: 8px; margin-top: 4px; } +.store-unit { font-size: 12px; color: var(--muted); letter-spacing: .08em; } +.nav { display: flex; flex-direction: column; gap: 4px; padding: 8px 12px; } +.nav .filter { display: flex; align-items: center; gap: 14px; padding: 10px 12px; border-radius: 6px; font-size: 14px; font-weight: 400; color: var(--muted); text-align: left; } +.nav .filter svg { width: 22px; height: 22px; fill: currentColor; flex: none; } +.nav .filter:hover { color: var(--text); } +.nav .filter[aria-pressed="true"] { color: var(--text); font-weight: 700; background: var(--mid); } +.nav .count { margin-left: auto; font-size: 12px; color: var(--muted); font-weight: 400; } +.side-foot { margin-top: auto; padding: 12px 20px 18px; display: flex; flex-direction: column; gap: 12px; border-top: 1px solid #222; } +.sync-mini { display: flex; flex-direction: column; gap: 6px; } +.cache-row { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); } +.track { height: 4px; border-radius: 2px; background: var(--border); overflow: hidden; } +.track i { display: block; height: 100%; width: 0; background: var(--green); transition: width .3s; } +.sync-detail { font-size: 11px; color: var(--muted); } +.toggle { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-radius: 9999px; background: var(--mid); font-size: 13px; font-weight: 700; } +.toggle .sw { width: 36px; height: 20px; border-radius: 10px; background: var(--border); position: relative; transition: background .15s; } +.toggle .sw::after { content: ""; position: absolute; top: 3px; left: 3px; width: 14px; height: 14px; border-radius: 50%; background: #fff; transition: left .15s; } +.toggle[aria-pressed="true"] .sw { background: var(--green); } +.toggle[aria-pressed="true"] .sw::after { left: 19px; } +.conn { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); } +.conn i { width: 8px; height: 8px; border-radius: 50%; background: var(--red); } +.conn.ok i { background: var(--green); box-shadow: 0 0 8px var(--green); } +.version { font-size: 11px; color: var(--muted); display: flex; justify-content: space-between; } + +/* main pane */ +.main { background: var(--bg); border-radius: 8px; min-height: 0; display: flex; flex-direction: column; overflow: hidden; background-image: linear-gradient(180deg, #2a2a2a 0, var(--bg) 260px); } +.topbar { display: flex; align-items: center; gap: 12px; padding: 14px 24px 0; } +.grid-count { font-size: 11px; color: var(--muted); letter-spacing: .08em; } +.clock { margin-left: auto; font-size: 13px; color: var(--muted); } +.icon-btn { width: 32px; height: 32px; border-radius: 50%; background: rgba(0, 0, 0, .6); display: inline-flex; align-items: center; justify-content: center; color: var(--muted); font-weight: 700; } +.icon-btn:hover { color: var(--text); background: #000; } +.icon-btn-sm { width: 24px; height: 24px; } +.icon-btn-sm svg { width: 12px; height: 12px; } +.update-bar { margin: 12px 24px 0; padding: 10px 16px; border-radius: 8px; background: var(--blue); color: #000; font-size: 13px; font-weight: 700; box-shadow: var(--shadow-mid); } +.state-banner { margin: 12px 24px 0; padding: 12px 20px; border-radius: 8px; background: var(--orange); color: #000; font-weight: 700; box-shadow: var(--shadow-mid); } +.state-banner-idle { background: var(--mid); color: var(--muted); font-weight: 400; box-shadow: none; } +.state-banner-mute { background: var(--red); } +.head { padding: 22px 24px 8px; display: flex; align-items: baseline; gap: 14px; } +.head h1 { font-size: 24px; font-weight: 700; margin: 0; } +.head .sub { font-size: 13px; color: var(--muted); } +#main { padding: 8px 24px 24px; overflow: auto; min-height: 0; flex: 1; } +.empty { color: var(--muted); font-size: 14px; } + +/* spots */ +.grid-spots { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 16px; align-content: start; } +.spot { position: relative; padding: 14px; border-radius: 8px; background: var(--surface); text-align: left; display: flex; flex-direction: column; gap: 12px; transition: background .2s; } +.spot:hover { background: var(--card-hover); } +.spot .art { aspect-ratio: 1; border-radius: 6px; background: linear-gradient(135deg, #2b2b2b, #3d3d3d); box-shadow: var(--shadow-mid); display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; background-size: cover; background-position: center; } +.spot .glyph { font-size: 38px; font-weight: 700; color: #6b6b6b; letter-spacing: -.04em; } +.spot.has-art .glyph { display: none; } +.spot .spot-title { font-size: 15px; font-weight: 700; line-height: 1.25; } +.spot .kicker { display: block; margin-top: 2px; } +.play-fab { position: absolute; right: 10px; bottom: 10px; width: 44px; height: 44px; border-radius: 50%; background: var(--green); display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow-mid); opacity: 0; transform: translateY(8px); transition: all .2s; } +.play-fab svg { width: 20px; height: 20px; fill: #000; margin-left: 2px; } +.spot:hover .play-fab { opacity: 1; transform: translateY(0); } +.spot:disabled { cursor: default; } +.spot:disabled:hover { background: var(--surface); } +.spot:disabled .play-fab { display: none; } +.spot.is-pending .art, .spot.is-failed .art { filter: grayscale(1) brightness(.6); } +.spot .state { font-size: 12px; font-weight: 600; margin-top: 4px; } +.spot .state.dl { color: var(--orange); } +.spot .state.err { color: var(--red); } +.badge { display: inline-block; padding: 2px 8px; border-radius: 2px; font-size: 10.5px; font-weight: 600; text-transform: capitalize; background: var(--mid); color: var(--muted); } +.badge.is-spot { background: var(--green); color: #000; } +.badge.is-live { background: var(--red); color: #000; } + +/* history */ +.hist-line { display: grid; grid-template-columns: 56px 80px 1fr 40px; align-items: center; gap: 14px; padding: 10px 12px; border-radius: 6px; font-size: 14px; } +.hist-line:hover { background: rgba(255, 255, 255, .06); } +.hist-time { color: var(--muted); font-size: 13px; } +.hist-title { text-align: left; padding: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.hist-replay { width: 32px; height: 32px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: var(--muted); opacity: 0; } +.hist-line:hover .hist-replay { opacity: 1; color: var(--text); } +.hist-replay svg { width: 16px; height: 16px; fill: currentColor; } + +/* calls */ +.call-wrap { display: flex; flex-direction: column; gap: 20px; } +.call-tabs { display: flex; gap: 8px; } +.call-tabs .filter { padding: 8px 16px; border-radius: 9999px; background: var(--mid); color: var(--muted); font-size: 13px; font-weight: 700; } +.call-tabs .filter[aria-pressed="true"] { background: var(--text); color: #000; } +.call-form { display: grid; grid-template-columns: repeat(2, minmax(220px, 360px)); gap: 16px 20px; } +.call-form label { display: flex; flex-direction: column; gap: 8px; font-size: 12px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: 1.4px; } +.call-form input, .call-form textarea { font: inherit; font-size: 15px; color: var(--text); background: var(--mid); border: 0; border-radius: 500px; padding: 12px 20px; box-shadow: var(--inset); outline: 0; user-select: text; } +.call-form textarea { border-radius: 16px; min-height: 96px; resize: vertical; } +.call-form label.wide { grid-column: 1 / -1; } +.call-form input:focus, .call-form textarea:focus { box-shadow: rgb(18, 18, 18) 0 1px 0, #fff 0 0 0 1px inset; } +.call-actions { grid-column: 1 / -1; display: flex; align-items: center; gap: 14px; margin-top: 6px; } +.gender-pick { display: flex; gap: 16px; font-size: 14px; color: var(--text); text-transform: none; letter-spacing: 0; font-weight: 400; } +.cta { padding: 14px 32px; border-radius: 500px; background: var(--green); color: #000; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.4px; transition: transform .1s; } +.cta:hover { transform: scale(1.04); background: #3be477; } +.cta:disabled { background: var(--border); color: var(--muted); transform: none; } + +/* unregistered support */ +.support-main { display: block; } +.support-card { max-width: 620px; margin: 24px auto; padding: 32px; border-radius: 8px; background: var(--surface); box-shadow: var(--shadow-mid); display: grid; grid-template-columns: 1fr 148px; gap: 24px; align-items: center; } +.support-copy p { margin: 0 0 10px; } +.support-lead { font-size: 18px; font-weight: 700; } +.support-lines { color: var(--muted); } +.support-qr { padding: 0; border-radius: 8px; overflow: hidden; background: #fff; } +.support-qr img { display: block; } + +/* now playing bar */ +.now { background: #000; display: grid; grid-template-columns: 30% 1fr 30%; align-items: center; padding: 0 16px; gap: 16px; } +.np-left { display: flex; align-items: center; gap: 14px; min-width: 0; } +.cover { width: 56px; height: 56px; border-radius: 4px; background: linear-gradient(135deg, #3a3a3a, #202020); flex: none; display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow-mid); } +.cover svg { width: 24px; height: 24px; fill: var(--muted); } +.np-meta { min-width: 0; display: flex; flex-direction: column; gap: 4px; } +.np-title { font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.np-sub { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 8px; } +.np-center { display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 0; } +.play { width: 40px; height: 40px; border-radius: 50%; background: #fff; color: #000; display: flex; align-items: center; justify-content: center; transition: transform .1s; } +.play:hover { transform: scale(1.06); } +.play:disabled { background: var(--border); transform: none; } +.play svg { width: 18px; height: 18px; fill: #000; } +.play #playIcon { display: inline-flex; } +.cut { width: 32px; height: 32px; border-radius: 50%; color: var(--muted); display: flex; align-items: center; justify-content: center; } +.cut:hover, .cut.on { color: var(--text); } +.cut svg { width: 18px; height: 18px; fill: currentColor; } +.prog { width: 100%; max-width: 560px; display: flex; align-items: center; gap: 10px; font-size: 11px; color: var(--muted); } +.bar { flex: 1; height: 4px; border-radius: 2px; background: var(--border); position: relative; overflow: hidden; } +.bar i { display: block; height: 100%; width: 0; border-radius: 2px; background: #fff; } +.prog:hover .bar i { background: var(--green); } +.np-right { display: flex; align-items: center; justify-content: flex-end; gap: 10px; } +.vol { width: 110px; -webkit-appearance: none; appearance: none; height: 4px; border-radius: 2px; background: linear-gradient(90deg, #fff 0 var(--vol-pct, 85%), var(--border) var(--vol-pct, 85%)); } +.vol::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #fff; opacity: 0; } +.vol:hover { background: linear-gradient(90deg, var(--green) 0 var(--vol-pct, 85%), var(--border) var(--vol-pct, 85%)); } +.vol:hover::-webkit-slider-thumb { opacity: 1; } +.vol-read { font-size: 12px; color: var(--muted); width: 24px; } +.eq { display: inline-flex; align-items: flex-end; gap: 2px; height: 12px; } +.eq i { width: 3px; background: var(--green); animation: eq 1s infinite ease-in-out; } +.eq i:nth-child(1) { height: 60%; animation-delay: 0s; } +.eq i:nth-child(2) { height: 100%; animation-delay: .2s; } +.eq i:nth-child(3) { height: 40%; animation-delay: .4s; } +.eq i:nth-child(4) { height: 80%; animation-delay: .1s; } +@keyframes eq { 0%, 100% { transform: scaleY(.4); } 50% { transform: scaleY(1); } } + +/* on air */ +.onair { position: fixed; left: 50%; bottom: calc(var(--bar-h) + 16px); transform: translateX(-50%); display: flex; align-items: center; gap: 16px; padding: 12px 12px 12px 20px; border-radius: 9999px; background: var(--surface); box-shadow: var(--shadow-heavy); border: 1px solid var(--green-border); max-width: 70vw; z-index: 20; } +.onair .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--red); box-shadow: 0 0 10px var(--red); animation: blink 1s infinite; flex: none; } +@keyframes blink { 50% { opacity: .3; } } +.onair .txt { display: flex; flex-direction: column; gap: 2px; font-size: 14px; font-weight: 700; min-width: 0; } +.onair .txt #airText { font-weight: 400; color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.onair .air-time { color: var(--muted); font-size: 12px; flex: none; } +.onair .stop { padding: 8px 18px; border-radius: 9999px; border: 1px solid var(--border-light); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.4px; flex: none; } +.onair .stop:hover { border-color: #fff; } + +/* toasts */ +.toasts { position: fixed; right: 24px; bottom: calc(var(--bar-h) + 16px); display: flex; flex-direction: column; gap: 8px; z-index: 30; } +.toast { padding: 12px 18px; border-radius: 8px; background: var(--surface); box-shadow: var(--shadow-heavy); border-left: 4px solid var(--green); font-size: 13px; min-width: 220px; } +.toast.err { border-left-color: var(--red); } +.toast b { display: block; } + +/* service desk dialog */ +.debug-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, .7); display: flex; align-items: center; justify-content: center; z-index: 40; } +.debug-panel { width: min(720px, 92vw); max-height: 88vh; display: flex; flex-direction: column; background: var(--surface); border-radius: 8px; box-shadow: var(--shadow-heavy); } +.debug-head { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; font-size: 18px; } +.debug-body { padding: 0 20px 12px; overflow: auto; font-size: 13px; user-select: text; } +.debug-contact { display: grid; grid-template-columns: 1fr 120px; gap: 16px; align-items: center; padding: 12px; border-radius: 8px; background: var(--mid); margin-bottom: 12px; } +.debug-contact img { width: 120px; height: 120px; border-radius: 6px; cursor: pointer; } +.debug-contact p { margin: 0 0 6px; } +.debug-row { display: grid; grid-template-columns: 150px 1fr; gap: 12px; padding: 6px 0; border-bottom: 1px solid #222; } .debug-row span:first-child { color: var(--muted); } -.debug-contact { display: flex; align-items: center; gap: 16px; margin: 8px 0 14px; } -.debug-contact img { width: 96px; height: 96px; background: #fff; padding: 6px; cursor: pointer; border: 1px solid var(--color-divider); } -.debug-actions { margin-top: 14px; display: flex; justify-content: flex-end; } - -.onair { - position: fixed; left: 20px; right: 20px; bottom: 124px; - display: flex; align-items: center; gap: 12px; padding: 8px 14px; - background: var(--color-text); color: var(--color-bg); z-index: 50; -} -.onair[hidden] { display: none; } -.toasts { position: fixed; top: 16px; right: 16px; display: flex; flex-direction: column; gap: 8px; z-index: 60; } -.toast { padding: 10px 14px; background: var(--color-surface); border: 1px solid var(--color-divider); max-width: 320px; } -.toast.err { border-color: #c46a4a; } -.toast b { display: block; font-family: var(--font-heading); } -.empty { color: var(--muted); font-size: 14px; padding: 40px; text-align: center; } - -#main.support-main { - display: flex; - align-items: center; - justify-content: center; - min-height: 100%; -} -.support-card { - display: flex; - align-items: center; - gap: 28px; - max-width: 720px; - padding: 8px 12px; -} -.support-copy { min-width: 0; } -.support-lead { - margin: 0 0 12px; - font-family: var(--font-heading); - font-size: 20px; - line-height: 1.35; -} -.support-lines { - margin: 0; - color: var(--muted); - font-size: 15px; - line-height: 1.5; -} -.support-lines + .support-lines { margin-top: 4px; } -.support-qr { - flex: none; - margin: 0; - padding: 8px; - border: 1px solid var(--color-divider); - background: #fff; - cursor: pointer; -} -.support-qr:hover { outline: 2px solid var(--color-accent); outline-offset: 2px; } -.support-qr img { display: block; width: 148px; height: 148px; } +.debug-actions { padding: 12px 20px 20px; display: flex; justify-content: flex-end; } + +/* combo suggestions (replaces the native datalist) */ +.combo { position: relative; display: flex; flex-direction: column; } +.combo input { width: 100%; } +.combo-menu { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 15; background: var(--surface); border-radius: 8px; box-shadow: var(--shadow-heavy); padding: 6px; max-height: 240px; overflow: auto; display: flex; flex-direction: column; gap: 2px; } +.combo-item { text-align: left; padding: 10px 12px; border-radius: 6px; font-size: 14px; font-weight: 400; color: var(--text); text-transform: none; letter-spacing: 0; } +.combo-item:hover, .combo-item.is-active { background: var(--card-hover); } + +/* frameless window: drag regions and controls */ +.topbar, .brand { --wails-draggable: drag; } +.topbar button, .brand button, .win { --wails-draggable: no-drag; } +.win { display: inline-flex; gap: 6px; margin-left: 8px; padding-left: 12px; border-left: 1px solid #333; } +.win .icon-btn svg { width: 14px; height: 14px; fill: currentColor; } +.win-close:hover { background: var(--red); color: #000; } diff --git a/frontend/src/main.js b/frontend/src/main.js index e4612d7..6b0bd89 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,6 +1,5 @@ -import './industry.css'; import './app.css'; -import {BrowserOpenURL, ClipboardSetText, EventsOn} from '../wailsjs/runtime/runtime'; +import {BrowserOpenURL, ClipboardSetText, EventsOn, Quit, WindowMinimise, WindowToggleMaximise} from '../wailsjs/runtime/runtime'; import qrWhatsapp from './assets/images/whatsapp-suporte.png'; import { AnnounceCall, @@ -23,21 +22,18 @@ const el = (tag, cls, text) => { return n; }; const ICON = { - play: '', - pause: '', - sun: '', - moon: '', - vol: '', - muted: '', - copy: '', -}; -const marks = node => { - ['tl', 'tr', 'bl', 'br'].forEach(p => { - const i = document.createElement('i'); - i.className = 'corner ' + p; - node.appendChild(i); - }); + play: '', + pause: '', + vol: '', + muted: '', + copy: '', + note: '', + spots: '', + history: '', + calls: '', }; +const HISTORY_LIMIT = 15; +const VIEW_TITLE = {spots: 'Spots', history: 'Últimos tocados', calls: 'Chamadas'}; const state = { view: 'spots', @@ -104,12 +100,6 @@ function tickClock() { $('clock').textContent = String(n.getHours()).padStart(2, '0') + ':' + String(n.getMinutes()).padStart(2, '0'); } -function applyTheme(theme) { - document.documentElement.setAttribute('data-theme', theme); - $('theme').innerHTML = theme === 'dark' ? ICON.sun : ICON.moon; - localStorage.setItem('theme', theme); -} - function paintFilters() { const host = $('filters'); host.replaceChildren(); @@ -122,12 +112,17 @@ function paintFilters() { if (state.view === 'spots' && !(state.deskapp.buttons || []).length) state.view = tabs[0] ? tabs[0].id : 'history'; if (state.view === 'calls' && !(state.deskapp.calls || []).length) state.view = tabs[0] ? tabs[0].id : 'history'; for (const t of tabs) { - const b = el('button', 'filter', t.name); + const b = el('button', 'filter'); b.type = 'button'; + b.innerHTML = ICON[t.id] || ''; + b.append(el('span', null, t.name)); + if (t.id === 'spots') b.append(el('span', 'count', String((state.deskapp.buttons || []).length))); b.setAttribute('aria-pressed', String(state.view === t.id)); b.addEventListener('click', () => { state.viewLocked = true; state.view = t.id; paint(); }); host.append(b); } + $('viewTitle').textContent = state.unregistered && state.view === 'history' ? 'Player não cadastrado' : (VIEW_TITLE[state.view] || ''); + $('viewSub').textContent = state.view === 'spots' ? 'Toque um botão para anunciar' : state.view === 'calls' && state.deskapp.voice && state.deskapp.voice.name ? 'Voz: ' + state.deskapp.voice.name : ''; } function paintChime() { @@ -151,32 +146,28 @@ function paintSpots() { const imageUrl = String(b.imageUrl || b.imageURL || '').trim(); const hasArt = /^https:\/\//i.test(imageUrl); const label = String(b.label || '').trim(); - const card = el('button', 'spot blueprint' + const card = el('button', 'spot' + (pending ? ' is-pending' : b.failed ? ' is-failed' : '') - + (hasArt ? ' has-art' : '') - + (hasArt && label ? ' has-label' : '')); + + (hasArt ? ' has-art' : '')); card.type = 'button'; card.disabled = !b.ready || fireLocked(); card.setAttribute('aria-label', label || b.id); + card.title = b.failed ? 'Indisponível no momento: falha no download' : pending ? 'Indisponível no momento: arquivo baixando' : label; + const art = el('span', 'art'); if (hasArt) { - card.style.backgroundImage = 'url("' + imageUrl.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '")'; - } - if (label) { - const row = el('span', 'spot-row'); - row.append(el('span', 'kicker', b.group || 'Spot')); - card.append(row); - card.append(el('span', 'spot-title', label)); - } - const foot = el('span', 'spot-row spot-foot mono'); - foot.append(el('span', null, b.failed ? 'Falhou' : b.ready ? '' : 'Baixando')); - card.append(foot); - if (pending || b.failed) { - const veil = el('span', 'spot-veil', b.failed - ? 'Indisponível no momento — falha no download' - : 'Indisponível no momento — arquivo baixando'); - card.append(veil); + art.style.backgroundImage = 'url("' + imageUrl.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '")'; } - marks(card); + art.append(el('span', 'glyph', glyphFor(label || b.id))); + const fab = el('span', 'play-fab'); + fab.innerHTML = ICON.play; + art.append(fab); + card.append(art); + const meta = el('span'); + meta.append(el('span', 'spot-title', label || b.id)); + meta.append(el('span', 'kicker', b.group || 'Spot')); + if (pending) meta.append(el('span', 'state dl', 'Baixando…')); + if (b.failed) meta.append(el('span', 'state err', 'Falha no download')); + card.append(meta); card.addEventListener('click', () => { if (!b.ready || fireLocked()) return; withFire(() => PlayDeskApp(b.id)); @@ -185,6 +176,13 @@ function paintSpots() { } } +function glyphFor(label) { + const words = String(label || '').trim().split(/\s+/).filter(Boolean); + if (!words.length) return ''; + if (words.length === 1) return words[0].slice(0, 2).toUpperCase(); + return (words[0][0] + words[1][0]).toUpperCase(); +} + function paintUnregisteredSupport() { const main = $('main'); main.className = 'support-main'; @@ -219,7 +217,7 @@ async function paintHistory() { main.className = ''; main.replaceChildren(); let rows = []; - try { rows = await Timeline() || []; } catch { rows = []; } + try { rows = (await Timeline() || []).slice(0, HISTORY_LIMIT); } catch { rows = []; } $('gridCount').textContent = rows.length ? rows.length + ' ITENS' : ''; if (!rows.length) { main.append(el('p', 'empty', '')); @@ -229,16 +227,23 @@ async function paintHistory() { const line = el('div', 'hist-line'); line.append(el('span', 'mono hist-time', row.time || '')); const kind = row.kind || ''; - const badge = el('span', 'badge' + (kind === 'spot' ? ' is-spot' : ''), kind); + const badge = el('span', 'badge' + (kind === 'spot' ? ' is-spot' : kind === 'chamada' || kind === 'call' ? ' is-live' : ''), kind); line.append(badge); const title = el('button', 'hist-title', row.title || ''); title.type = 'button'; + title.title = 'Copiar nome'; + title.addEventListener('click', () => copyText(row.title || '', 'Nome')); + line.append(title); if (row.replayId) { - title.addEventListener('click', () => { - withFire(() => PlayDeskApp(row.replayId)); - }); + const replay = el('button', 'hist-replay'); + replay.type = 'button'; + replay.title = 'Tocar de novo'; + replay.innerHTML = ICON.play; + replay.addEventListener('click', () => withFire(() => PlayDeskApp(row.replayId))); + line.append(replay); + } else { + line.append(el('span')); } - line.append(title); main.append(line); } } @@ -274,29 +279,55 @@ function paintCalls() { tabs.append(b); } wrap.append(tabs); + const form = el('div', 'call-form'); + wrap.append(form); const vals = state.values[state.call] || {}; const setVal = (id, value) => { state.values[state.call] = {...(state.values[state.call] || {}), [id]: value}; }; const addCombo = (id, label, list) => { - wrap.append(el('label', null, label)); + const lab = el('label', null, label); + form.append(lab); + const box = el('div', 'combo'); const i = el('input'); i.type = 'text'; - i.setAttribute('list', 'dl-' + list + '-' + id); i.value = vals[id] || ''; i.autocomplete = 'off'; - const dl = el('datalist'); - dl.id = 'dl-' + list + '-' + id; - for (const opt of listFor(list)) { - const o = document.createElement('option'); - o.value = opt; - dl.append(o); - } - i.addEventListener('input', () => setVal(id, i.value)); - wrap.append(i, dl); + const menu = el('div', 'combo-menu'); + menu.hidden = true; + const options = listFor(list); + let active = -1; + const fold = v => String(v || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); + const close = () => { menu.hidden = true; active = -1; }; + const pick = value => { i.value = value; setVal(id, value); close(); }; + const render = () => { + const q = fold(i.value.trim()); + const hits = options.filter(o => !q || fold(o).includes(q)).slice(0, 8); + menu.replaceChildren(); + hits.forEach((o, idx) => { + const item = el('button', 'combo-item' + (idx === active ? ' is-active' : ''), o); + item.type = 'button'; + item.addEventListener('pointerdown', ev => { ev.preventDefault(); pick(o); }); + menu.append(item); + }); + menu.hidden = !hits.length; + }; + i.addEventListener('input', () => { active = -1; setVal(id, i.value); render(); }); + i.addEventListener('focus', render); + i.addEventListener('blur', () => setTimeout(close, 120)); + i.addEventListener('keydown', ev => { + const items = menu.querySelectorAll('.combo-item'); + if (ev.key === 'ArrowDown' && items.length) { ev.preventDefault(); active = (active + 1) % items.length; render(); } + else if (ev.key === 'ArrowUp' && items.length) { ev.preventDefault(); active = (active - 1 + items.length) % items.length; render(); } + else if (ev.key === 'Enter' && active >= 0 && items[active]) { ev.preventDefault(); pick(items[active].textContent); } + else if (ev.key === 'Escape') close(); + }); + box.append(i, menu); + lab.append(box); }; const addGender = () => { - wrap.append(el('label', null, 'Gênero')); + const glab = el('label', null, 'Gênero'); + form.append(glab); const row = el('div', 'gender-pick'); [['m', 'Masculino'], ['f', 'Feminino']].forEach(([id, name]) => { const lab = document.createElement('label'); @@ -307,13 +338,13 @@ function paintCalls() { i.checked = vals.genero === id; i.addEventListener('change', () => { setVal('genero', id); - const btn = wrap.querySelector('.btn-primary'); + const btn = wrap.querySelector('.cta'); if (btn) btn.disabled = false; }); lab.append(i, document.createTextNode(' ' + name)); row.append(lab); }); - wrap.append(row); + glab.append(row); }; const addField = (id, label, type, list) => { if (id === 'genero' || type === 'gender') { @@ -323,24 +354,25 @@ function paintCalls() { addCombo(id, label, list || id); return; } - wrap.append(el('label', null, label)); + const lab = el('label', type === 'area' ? 'wide' : null, label); + form.append(lab); if (type === 'area') { const t = el('textarea'); t.value = vals[id] || ''; t.addEventListener('input', () => setVal(id, t.value)); - wrap.append(t); + lab.append(t); } else { const i = el('input'); i.type = 'text'; i.value = vals[id] || ''; if (id === 'placa' || type === 'plate') i.maxLength = 7; i.addEventListener('input', () => setVal(id, i.value)); - wrap.append(i); + lab.append(i); } }; - const form = (state.deskapp.callForms || {})[state.call]; - if (form && form.fields && form.fields.length) { - const fields = form.fields.slice(); + const serverForm = (state.deskapp.callForms || {})[state.call]; + if (serverForm && serverForm.fields && serverForm.fields.length) { + const fields = serverForm.fields.slice(); // gender radios hidden for now — DeepSeek infers funcionário/funcionária from the name for (const f of fields) { addField(f.id, f.label || f.id, f.type || 'text', f.list); @@ -356,7 +388,7 @@ function paintCalls() { addField('text', 'Texto', 'area'); } const actions = el('div', 'call-actions'); - const go = el('button', 'btn btn-primary', 'Anunciar'); + const go = el('button', 'cta', 'Anunciar'); go.type = 'button'; go.disabled = fireLocked(); go.addEventListener('click', () => { @@ -364,7 +396,7 @@ function paintCalls() { withFire(() => AnnounceCall(state.call, state.values[state.call] || {})); }); actions.append(go); - wrap.append(actions); + form.append(actions); main.append(wrap); } @@ -400,6 +432,8 @@ function paintTransport(snap) { $('playIcon').innerHTML = snap.wanted ? ICON.pause : ICON.play; $('playLabel').textContent = snap.wanted ? 'Pausar' : 'Tocar'; $('play').setAttribute('aria-label', snap.wanted ? 'Pausar' : 'Tocar'); + $('play').title = snap.wanted ? 'Pausar' : 'Tocar'; + $('npKind').className = 'badge' + (snap.nowKind === 'spot' ? ' is-spot' : snap.busy ? ' is-live' : ''); $('eq').hidden = !live; $('elapsed').textContent = snap.elapsedLabel || '0:00'; $('npKind').textContent = snap.nowKind || '—'; @@ -429,13 +463,12 @@ function paintTransport(snap) { } $('muteBanner').hidden = !snap.muted || !!snap.unregistered; if (!state.volDrag && snap.volume != null) { - $('vol').value = String(snap.volume); - $('volRead').textContent = String(snap.volume); + setVolumeUI(snap.volume); } const conn = $('conn'); const offline = snap.status === 'stopped' || snap.ws === 'down'; - conn.className = 'conn-pill' + (snap.ws === 'up' || snap.status === 'live' ? ' ok' : ''); + conn.className = 'conn' + (snap.ws === 'up' || snap.status === 'live' ? ' ok' : ''); $('connText').textContent = offline && !snap.wanted ? 'Offline' : snap.ws === 'up' || snap.status === 'live' ? 'Conectado' : 'Conectando'; const sync = $('syncMini'); @@ -467,7 +500,7 @@ function paintTransport(snap) { $('onair').hidden = false; $('stage').textContent = snap.stage || ''; $('airText').textContent = snap.airText || ''; - $('airTime').textContent = (snap.airTime || 0).toFixed(1) + 's'; + $('airTime').textContent = Math.round(snap.airTime || 0) + 's'; } else { $('onair').hidden = true; } @@ -493,15 +526,21 @@ function paintTransport(snap) { function applyDesk(ui) { state.deskapp = ui || {buttons: [], calls: ['interno', 'estacionamento', 'livre'], lists: {}}; - if (state.deskapp.accent) document.documentElement.style.setProperty('--color-accent', state.deskapp.accent); paint(); } +function setVolumeUI(v) { + $('vol').value = String(v); + $('vol').style.setProperty('--vol-pct', v + '%'); + $('volRead').textContent = String(v); +} + async function boot() { const data = await Bootstrap(); state.templates = data.templates; state.chimeEnabled = data.chimeEnabled !== false; - if (data.version) document.title = 'Rádio Indoor ' + data.version; + if (data.version) document.title = 'MegaPlay Radio ' + data.version; + $('appVersion').textContent = data.version || ''; state.unregistered = !!data.unregistered; $('storeName').textContent = data.networkName || 'Player não encontrado'; setUnit(data.deviceUid); @@ -520,18 +559,16 @@ async function boot() { setInterval(() => { if (state.view === 'history') paintHistory(); }, 2000); } -applyTheme(localStorage.getItem('theme') || 'dark'); -$('theme').addEventListener('click', () => { - applyTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'); -}); $('play').addEventListener('click', () => TogglePlay()); +$('playIcon').innerHTML = ICON.play; +$('cover').innerHTML = ICON.note; $('muteIcon').innerHTML = ICON.vol; $('mute').addEventListener('click', () => ToggleMute()); $('vol').addEventListener('pointerdown', () => { state.volDrag = true; }); $('vol').addEventListener('pointerup', () => { state.volDrag = false; }); $('vol').addEventListener('pointercancel', () => { state.volDrag = false; }); $('vol').addEventListener('input', ev => { - $('volRead').textContent = ev.target.value; + setVolumeUI(ev.target.value); SetVolume(Number(ev.target.value)); }); $('cancel').addEventListener('click', () => CancelAnnounce()); @@ -545,13 +582,15 @@ function setUnit(uid) { $('copyUid').hidden = !state.deviceUid; } -$('copyUid').innerHTML = ICON.copy; -$('copyUid').addEventListener('click', () => { - if (!state.deviceUid) return; - ClipboardSetText(state.deviceUid).then(ok => { - toast(ok === false ? 'Falha' : 'UID', ok === false ? 'não foi possível copiar' : state.deviceUid + ' copiado', ok === false); +function copyText(text, label) { + if (!text) return; + ClipboardSetText(text).then(ok => { + toast(ok === false ? 'Falha' : label, ok === false ? 'não foi possível copiar' : text + ' copiado', ok === false); }).catch(err => toast('Falha', String(err), true)); -}); +} + +$('copyUid').innerHTML = ICON.copy; +$('copyUid').addEventListener('click', () => copyText(state.deviceUid, 'UID')); EventsOn('player', paintTransport); EventsOn('deskapp', applyDesk); @@ -658,6 +697,9 @@ function closeDebug() { $('debugOverlay').hidden = true; } +$('winMin').addEventListener('click', () => WindowMinimise()); +$('winMax').addEventListener('click', () => WindowToggleMaximise()); +$('winClose').addEventListener('click', () => Quit()); $('debugBtn').addEventListener('click', openDebug); $('debugClose').addEventListener('click', closeDebug); $('debugOverlay').addEventListener('click', ev => { diff --git a/main.go b/main.go index 1f1a00e..286518b 100644 --- a/main.go +++ b/main.go @@ -49,6 +49,7 @@ func main() { Height: 800, MinWidth: 980, MinHeight: 640, + Frameless: true, AssetServer: &assetserver.Options{ Assets: assets, },