Rewild Your Web
18
fork

Configure Feed

Select the types of activity you want to include in your feed.

system: media session controls

webbeef 9f6b387c 7c5a0ed7

+329
+3
ui/system/index.html
··· 14 14 <script src="./third_party/fend/fend_wasm.js"></script> 15 15 <script type="module" src="//shared.localhost:8888/theme.js"></script> 16 16 <script type="module" src="notification_panel.js"></script> 17 + <script type="module" src="media_control.js"></script> 17 18 <script type="module" src="toasts.js"></script> 18 19 <script type="module" src="index.js"></script> 19 20 </head> ··· 31 32 <lucide-icon name="bell" id="notifications-icon"></lucide-icon> 32 33 <span id="notifications-badge" class="hidden">0</span> 33 34 </div> 35 + <lucide-icon name="music" id="media-icon" class="hidden"></lucide-icon> 34 36 <lucide-icon name="plus" id="plus-icon"></lucide-icon> 35 37 </div> 36 38 <notification-panel id="notification-panel"></notification-panel> 39 + <media-control id="media-control"></media-control> 37 40 <div id="root"></div> 38 41 </main> 39 42 <toast-manager id="toast-manager"></toast-manager>
+73
ui/system/index.js
··· 912 912 }); 913 913 }); 914 914 915 + // Media session control 916 + const mediaControl = document.getElementById("media-control"); 917 + const mediaIcon = document.getElementById("media-icon"); 918 + let mediaSessionWebviewId = null; 919 + 920 + if (mediaIcon) { 921 + mediaIcon.onclick = () => { 922 + if (mediaControl) { 923 + mediaControl.open = !mediaControl.open; 924 + } 925 + }; 926 + } 927 + 928 + function updateMediaControl(ctrl, eventType, detail) { 929 + if (!ctrl) { 930 + return; 931 + } 932 + if (eventType === "metadata") { 933 + ctrl.title = detail.title || ""; 934 + ctrl.artist = detail.artist || ""; 935 + ctrl.album = detail.album || ""; 936 + ctrl.hasMedia = true; 937 + if (mediaIcon) mediaIcon.classList.remove("hidden"); 938 + } else if (eventType === "playbackstate") { 939 + ctrl.playbackState = detail.playbackState || "none"; 940 + if (detail.playbackState === "none") { 941 + ctrl.hasMedia = false; 942 + ctrl.open = false; 943 + if (mediaIcon) mediaIcon.classList.add("hidden"); 944 + } else { 945 + ctrl.hasMedia = true; 946 + if (mediaIcon) mediaIcon.classList.remove("hidden"); 947 + } 948 + } else if (eventType === "positionstate") { 949 + ctrl.duration = detail.duration || 0; 950 + ctrl.position = detail.position || 0; 951 + } 952 + } 953 + 954 + document 955 + .getElementById("root") 956 + .addEventListener("webview-mediasession", (e) => { 957 + const { webviewId, eventType } = e.detail; 958 + mediaSessionWebviewId = webviewId; 959 + updateMediaControl(mediaControl, eventType, e.detail); 960 + // Also update mobile notification sheet's media control 961 + if (mobileNotificationSheet) { 962 + const mobileCtrl = mobileNotificationSheet.shadowRoot?.querySelector( 963 + "#mobile-media-control", 964 + ); 965 + updateMediaControl(mobileCtrl, eventType, e.detail); 966 + } 967 + }); 968 + 969 + function handleMediaAction(event) { 970 + if (mediaSessionWebviewId == null) { 971 + return; 972 + } 973 + const entry = layoutManager.webviews.get(mediaSessionWebviewId); 974 + if (entry) { 975 + console.log(`[MediaControl] Action "${event.detail.action}" triggered for webview ${mediaSessionWebviewId}`); 976 + entry.webview.ensureIframe(); 977 + entry.webview.iframe.mediaSessionAction(event.detail.action); 978 + } 979 + } 980 + 981 + if (mediaControl) { 982 + mediaControl.addEventListener("media-action", handleMediaAction); 983 + } 984 + if (mobileNotificationSheet) { 985 + mobileNotificationSheet.addEventListener("media-action", handleMediaAction); 986 + } 987 + 915 988 // Listen for virtual keyboard show/hide events from webviews 916 989 document 917 990 .getElementById("root")
+1
ui/system/index_mobile.html
··· 21 21 <script src="./third_party/fend/fend_wasm.js"></script> 22 22 <script type="module" src="//shared.localhost:8888/theme.js"></script> 23 23 <script type="module" src="notification_panel.js"></script> 24 + <script type="module" src="media_control.js"></script> 24 25 <script type="module" src="toasts.js"></script> 25 26 <script type="module" src="index.js"></script> 26 27 </head>
+132
ui/system/media_control.css
··· 1 + /* SPDX-License-Identifier: AGPL-3.0-or-later */ 2 + 3 + :host { 4 + position: fixed; 5 + bottom: 0; 6 + left: var(--sidebar-width); 7 + width: var(--size-panel-width); 8 + z-index: var(--z-panel); 9 + pointer-events: none; 10 + font-family: var(--font-family-base); 11 + } 12 + 13 + :host([open]) { 14 + pointer-events: auto; 15 + } 16 + 17 + .panel { 18 + position: absolute; 19 + bottom: 0; 20 + left: 0; 21 + width: 100%; 22 + background: var(--bg-menu); 23 + box-shadow: 4px -4px 16px var(--color-shadow-menu); 24 + border-radius: var(--radius-md) var(--radius-md) 0 0; 25 + transform: translateY(100%); 26 + transition: transform var(--transition-fast), visibility var(--transition-fast); 27 + visibility: hidden; 28 + padding: var(--spacing-md); 29 + } 30 + 31 + :host([open]) .panel { 32 + transform: translateY(0); 33 + visibility: visible; 34 + } 35 + 36 + /* Mobile: inline display, no panel positioning */ 37 + :host(.inline) { 38 + position: static; 39 + width: auto; 40 + pointer-events: auto; 41 + } 42 + 43 + :host(.inline) .panel { 44 + position: static; 45 + transform: none; 46 + visibility: visible; 47 + box-shadow: none; 48 + border-radius: 0; 49 + border-bottom: 1px solid var(--color-border); 50 + width: auto; 51 + } 52 + 53 + :host(.inline:not([has-media])) { 54 + display: none; 55 + } 56 + 57 + .media-info { 58 + margin-bottom: var(--spacing-xs); 59 + overflow: hidden; 60 + } 61 + 62 + .media-title { 63 + font-size: var(--font-size-menu); 64 + font-weight: var(--font-weight-bold); 65 + color: var(--color-text); 66 + white-space: nowrap; 67 + overflow: hidden; 68 + text-overflow: ellipsis; 69 + } 70 + 71 + .media-artist { 72 + font-size: var(--font-size-sm); 73 + color: var(--color-text-secondary); 74 + white-space: nowrap; 75 + overflow: hidden; 76 + text-overflow: ellipsis; 77 + } 78 + 79 + .media-buttons { 80 + display: flex; 81 + align-items: center; 82 + justify-content: center; 83 + gap: var(--spacing-sm); 84 + } 85 + 86 + .media-btn { 87 + background: none; 88 + border: none; 89 + color: var(--color-text); 90 + cursor: pointer; 91 + padding: var(--spacing-xs); 92 + border-radius: var(--radius-sm); 93 + display: flex; 94 + align-items: center; 95 + justify-content: center; 96 + } 97 + 98 + .media-btn:hover { 99 + background: var(--bg-hover); 100 + } 101 + 102 + .play-btn lucide-icon { 103 + font-size: larger; 104 + } 105 + 106 + .media-progress { 107 + display: flex; 108 + align-items: center; 109 + gap: var(--spacing-xs); 110 + margin-top: var(--spacing-xs); 111 + } 112 + 113 + .media-time { 114 + font-size: var(--font-size-xs); 115 + color: var(--color-text-tertiary); 116 + min-width: 3em; 117 + text-align: center; 118 + } 119 + 120 + .media-progress-bar { 121 + flex: 1; 122 + height: 4px; 123 + background: var(--color-border); 124 + border-radius: 2px; 125 + overflow: hidden; 126 + } 127 + 128 + .media-progress-fill { 129 + height: 100%; 130 + background: var(--color-primary); 131 + border-radius: 2px; 132 + }
+102
ui/system/media_control.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + import { 4 + LitElement, 5 + html, 6 + css, 7 + } from "//shared.localhost:8888/third_party/lit/lit-all.min.js"; 8 + 9 + export class MediaControl extends LitElement { 10 + static properties = { 11 + title: { type: String }, 12 + artist: { type: String }, 13 + album: { type: String }, 14 + playbackState: { type: String }, 15 + duration: { type: Number }, 16 + position: { type: Number }, 17 + open: { type: Boolean, reflect: true }, 18 + hasMedia: { type: Boolean, reflect: true, attribute: "has-media" }, 19 + }; 20 + 21 + static styles = css` 22 + @import url("//system.localhost:8888/media_control.css"); 23 + `; 24 + 25 + constructor() { 26 + super(); 27 + this.title = ""; 28 + this.artist = ""; 29 + this.album = ""; 30 + this.playbackState = "none"; 31 + this.duration = 0; 32 + this.position = 0; 33 + this.open = false; 34 + this.hasMedia = false; 35 + } 36 + 37 + handleAction(action) { 38 + this.dispatchEvent( 39 + new CustomEvent("media-action", { 40 + bubbles: true, 41 + composed: true, 42 + detail: { action }, 43 + }), 44 + ); 45 + } 46 + 47 + formatTime(seconds) { 48 + if (!seconds || !isFinite(seconds)) return "0:00"; 49 + const mins = Math.floor(seconds / 60); 50 + const secs = Math.floor(seconds % 60); 51 + return `${mins}:${secs.toString().padStart(2, "0")}`; 52 + } 53 + 54 + render() { 55 + const isPlaying = this.playbackState === "playing"; 56 + 57 + return html` 58 + <div class="panel"> 59 + <div class="media-info"> 60 + <div class="media-title">${this.title || "Untitled"}</div> 61 + ${this.artist 62 + ? html`<div class="media-artist">${this.artist}</div>` 63 + : ""} 64 + </div> 65 + <div class="media-buttons"> 66 + <button 67 + class="media-btn" 68 + @click=${() => this.handleAction("previoustrack")} 69 + > 70 + <lucide-icon name="skip-back"></lucide-icon> 71 + </button> 72 + <button 73 + class="media-btn play-btn" 74 + @click=${() => this.handleAction(isPlaying ? "pause" : "play")} 75 + > 76 + <lucide-icon name="${isPlaying ? "pause" : "play"}"></lucide-icon> 77 + </button> 78 + <button 79 + class="media-btn" 80 + @click=${() => this.handleAction("nexttrack")} 81 + > 82 + <lucide-icon name="skip-forward"></lucide-icon> 83 + </button> 84 + </div> 85 + ${this.duration > 0 86 + ? html`<div class="media-progress"> 87 + <span class="media-time">${this.formatTime(this.position)}</span> 88 + <div class="media-progress-bar"> 89 + <div 90 + class="media-progress-fill" 91 + style="width: ${(this.position / this.duration) * 100}%" 92 + ></div> 93 + </div> 94 + <span class="media-time">${this.formatTime(this.duration)}</span> 95 + </div>` 96 + : ""} 97 + </div> 98 + `; 99 + } 100 + } 101 + 102 + customElements.define("media-control", MediaControl);
+2
ui/system/mobile_notification_sheet.js
··· 169 169 </button> 170 170 </div> 171 171 172 + <media-control id="mobile-media-control" class="inline"></media-control> 173 + 172 174 <div class="notifications-list"> 173 175 ${this.notifications.length === 0 174 176 ? html`
+16
ui/system/web_view.js
··· 349 349 ); 350 350 } 351 351 352 + onmediasessionevent(event) { 353 + const detail = event.detail; 354 + console.log(`[MediaSession] ${detail.eventType}:`, detail); 355 + this.dispatchEvent( 356 + new CustomEvent("webview-mediasession", { 357 + bubbles: true, 358 + composed: true, 359 + detail: { 360 + webviewId: this.webviewId, 361 + ...detail, 362 + }, 363 + }), 364 + ); 365 + } 366 + 352 367 handleDialogConfirm(inputValue = null) { 353 368 this.ensureIframe(); 354 369 const dialog = this.currentDialog; ··· 1125 1140 @embeddialogshow=${this.ondialogshow} 1126 1141 @embednotificationshow=${this.onnotificationshow} 1127 1142 @embedloadstatuschange=${this.onloadstatuschange} 1143 + @embedmediasessionevent=${this.onmediasessionevent} 1128 1144 ></iframe> 1129 1145 ${this.renderDialog()} ${this.renderPermissionPrompt()} 1130 1146 <select-control