Feat: basic media control

Fixed dragging undefined tabs
This commit is contained in:
Slowlife01
2025-03-09 09:53:07 +07:00
parent 3df1973ac9
commit e6552c8dda
14 changed files with 638 additions and 3 deletions

View File

@@ -37,6 +37,8 @@ var gZenUIManager = {
window.addEventListener('TabClose', this.onTabClose.bind(this));
this.tabsWrapper.addEventListener('scroll', this.saveScrollbarState.bind(this));
gZenMediaController.init();
},
updateTabsToolbar() {
@@ -768,3 +770,252 @@ var gZenVerticalTabsManager = {
this._tabEdited = null;
},
};
var gZenMediaController = {
_currentMediaController: null,
_currentBrowser: null,
_mediaUpdateInterval: null,
mediaTitle: null,
mediaArtist: null,
mediaControlBar: null,
mediaServiceIcon: null,
mediaServiceTitle: null,
mediaProgressBar: null,
mediaCurrentTime: null,
mediaDuration: null,
mediaFocusButton: null,
supportedKeys: ['playpause', 'previoustrack', 'nexttrack'],
init() {
this.mediaTitle = document.querySelector('#zen-media-title');
this.mediaArtist = document.querySelector('#zen-media-artist');
this.mediaControlBar = document.querySelector('#zen-media-controls-toolbar');
this.mediaServiceIcon = document.querySelector('#zen-media-service-button > image');
this.mediaServiceTitle = document.querySelector('#zen-media-service-title');
this.mediaProgressBar = document.querySelector('#zen-media-progress-bar');
this.mediaCurrentTime = document.querySelector('#zen-media-current-time');
this.mediaDuration = document.querySelector('#zen-media-duration');
this.mediaFocusButton = document.querySelector('#zen-media-focus-button');
},
/**
* Deinitializes a media controller, removing all event listeners and resetting state.
* @param {Object} mediaController - The media controller to deinitialize.
*/
deinitMediaController(mediaController) {
if (!mediaController) return;
mediaController.onpositionstatechange = null;
mediaController.onplaybackstatechange = null;
mediaController.onsupportedkeyschange = null;
mediaController.onmetadatachange = null;
mediaController.ondeactivated = null;
this._currentMediaController = null;
this._currentBrowser = null;
if (this._mediaUpdateInterval) {
clearInterval(this._mediaUpdateInterval);
this._mediaUpdateInterval = null;
}
this.mediaControlBar.setAttribute('hidden', 'true');
this.mediaControlBar.removeAttribute('muted');
this.mediaControlBar.classList.remove('playing');
},
/**
* Sets up the media control UI with metadata and position state.
* @param {Object} metadata - The media metadata (title, artist, etc.).
* @param {Object} positionState - The position state (position, duration).
*/
setupMediaControl(metadata, positionState) {
if (!this.mediaControlBar.classList.contains('playing')) {
this.mediaControlBar.classList.add('playing');
}
this.mediaServiceTitle.textContent = this._currentBrowser._originalURI.displayHost;
this.mediaServiceIcon.src = this._currentBrowser.mIconURL;
this.mediaFocusButton.style.listStyleImage = `url(${this._currentBrowser.mIconURL})`;
this.mediaControlBar.removeAttribute('hidden');
this.mediaTitle.textContent = metadata.title || '';
this.mediaArtist.textContent = metadata.artist || '';
this._currentPosition = positionState.position;
this._currentDuration = positionState.duration;
this.updateMediaPosition();
for (const key of this.supportedKeys) {
const button = this.mediaControlBar.querySelector(`#zen-media-${key}-button`);
button.disabled = !this._currentMediaController.supportedKeys.includes(key);
}
},
/**
* @param {Object} mediaController - The media controller to activate.
* @param {Object} browser - The browser associated with the media controller.
*/
activateMediaControls(mediaController, browser) {
if (this._currentBrowser?.browserId === browser.browserId) return;
else this._currentBrowser = browser;
mediaController.onpositionstatechange = this.onPositionstateChange.bind(this);
mediaController.onplaybackstatechange = this.onPlaybackstateChange.bind(this);
mediaController.onsupportedkeyschange = this.onSupportedKeysChange.bind(this);
mediaController.onmetadatachange = this.onMetadataChange.bind(this);
mediaController.ondeactivated = this.onDeactivated.bind(this);
if (this._currentMediaController === mediaController) return;
else this.deinitMediaController(this._currentMediaController);
this._currentMediaController = mediaController;
const metadata = mediaController.getMetadata();
const positionState = mediaController.getPositionState();
this.setupMediaControl(metadata, positionState);
},
/**
* @param {Event} event - The deactivation event.
*/
onDeactivated(event) {
if (event.target === this._currentMediaController) {
this.deinitMediaController(event.target);
}
},
/**
* Updates playback state and UI based on changes.
* @param {Event} event - The playback state change event.
*/
onPlaybackstateChange(event) {
this.mediaControlBar.classList.toggle('playing', event.target.isPlaying);
},
/**
* Updates supported keys in the UI.
* @param {Event} event - The supported keys change event.
*/
onSupportedKeysChange(event) {
for (const key of this.supportedKeys) {
const button = this.mediaControlBar.querySelector(`#zen-media-${key}-button`);
button.disabled = !event.target.supportedKeys.includes(key);
}
},
/**
* Updates position state and UI when the media position changes.
* @param {Event} event - The position state change event.
*/
onPositionstateChange(event) {
if (event.target !== this._currentMediaController) return;
this._currentPosition = event.position;
this._currentDuration = event.duration;
this.updateMediaPosition();
},
/**
* Updates the media progress bar and time display.
*/
updateMediaPosition() {
if (this._mediaUpdateInterval) {
clearInterval(this._mediaUpdateInterval);
this._mediaUpdateInterval = null;
}
this.mediaCurrentTime.textContent = this.formatSecondsToMinutes(this._currentPosition);
this.mediaDuration.textContent = this.formatSecondsToMinutes(this._currentDuration);
this.mediaProgressBar.value = (this._currentPosition / this._currentDuration) * 100;
if (this._currentMediaController?.isPlaying) {
this._mediaUpdateInterval = setInterval(() => {
if (this._currentMediaController?.isPlaying) {
this._currentPosition += 1;
if (this._currentPosition > this._currentDuration) {
this._currentPosition = this._currentDuration;
}
this.mediaCurrentTime.textContent = this.formatSecondsToMinutes(this._currentPosition);
this.mediaProgressBar.value = (this._currentPosition / this._currentDuration) * 100;
} else {
clearInterval(this._mediaUpdateInterval);
this._mediaUpdateInterval = null;
}
}, 1000);
}
},
/**
* Formats seconds into a minutes:seconds string.
* @param {number} seconds - The time in seconds.
* @returns {string} Formatted time string.
*/
formatSecondsToMinutes(seconds) {
const totalSeconds = Math.ceil(seconds);
const minutes = Math.floor(totalSeconds / 60);
const remainingSeconds = totalSeconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
},
/**
* Updates metadata in the UI.
* @param {Event} event - The metadata change event.
*/
onMetadataChange(event) {
const metadata = event.target.getMetadata();
this.mediaTitle.textContent = metadata.title || '';
this.mediaArtist.textContent = metadata.artist || '';
},
onMediaPlayPrev() {
if (this._currentMediaController?.supportedKeys.includes('previoustrack')) {
this._currentMediaController.prevTrack();
}
},
onMediaPlayNext() {
if (this._currentMediaController?.supportedKeys.includes('nexttrack')) {
this._currentMediaController.nextTrack();
}
},
onMediaSeekDrag(event) {
this._currentMediaController?.pause();
const newTime = (event.target.value / 100) * this._currentDuration;
this.mediaCurrentTime.textContent = this.formatSecondsToMinutes(newTime);
},
onMediaSeekComplete(event) {
const newPosition = (event.target.value / 100) * this._currentDuration;
if (this._currentMediaController?.supportedKeys.includes('seekto')) {
this._currentMediaController.seekTo(newPosition);
this._currentMediaController.play();
}
},
onMediaFocus() {
this._currentMediaController?.focus();
},
onMediaMute() {
if (!this.mediaControlBar.hasAttribute('muted')) {
this._currentBrowser.mute();
this.mediaControlBar.setAttribute('muted', '');
} else {
this._currentBrowser.unmute();
this.mediaControlBar.removeAttribute('muted');
}
},
onMediaToggle() {
if (this.mediaControlBar.classList.contains('playing')) {
this._currentMediaController?.pause();
} else {
this._currentMediaController?.play();
}
},
};

View File

@@ -1,5 +1,5 @@
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
index a0a382643a2f74b6d789f3641ef300eed202d5e9..a962e155f1452362a2a35df89c8f56e1c0d9968c 100644
index 2a57e254c876589bf64bfbd5df188a2b435d8482..c4aecd9738baed13b8dd5687edc95695b0d5af37 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -2,7 +2,7 @@
@@ -73,7 +73,7 @@ index a0a382643a2f74b6d789f3641ef300eed202d5e9..a962e155f1452362a2a35df89c8f56e1
</tabs>
<toolbarbutton id="new-tab-button"
@@ -100,11 +108,12 @@
@@ -100,11 +108,13 @@
#include private-browsing-indicator.inc.xhtml
<toolbarbutton id="content-analysis-indicator"
class="toolbarbutton-1 content-analysis-indicator-icon"/>
@@ -82,6 +82,7 @@ index a0a382643a2f74b6d789f3641ef300eed202d5e9..a962e155f1452362a2a35df89c8f56e1
#include titlebar-items.inc.xhtml
-
+#endif
+#include zen-media-controller.inc.xhtml
+#include zen-sidebar-icons.inc.xhtml
</toolbar>
-
@@ -89,7 +90,7 @@ index a0a382643a2f74b6d789f3641ef300eed202d5e9..a962e155f1452362a2a35df89c8f56e1
<toolbar id="nav-bar"
class="browser-toolbar chromeclass-location"
data-l10n-id="navbar-accessible"
@@ -490,10 +499,12 @@
@@ -490,10 +500,12 @@
consumeanchor="PanelUI-button"
data-l10n-id="appmenu-menu-button-closed2"/>
</toolbaritem>

View File

@@ -26,6 +26,7 @@
<link rel="stylesheet" type="text/css" href="chrome://browser/skin/zen-icons/icons.css" />
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-branding.css" />
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-welcome.css" />
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-media-controls.css" />
</linkset>
# Scripts used all over the browser

View File

@@ -49,6 +49,7 @@
content/browser/zen-styles/zen-rices.css (content/zen-styles/zen-rices.css)
content/browser/zen-styles/zen-branding.css (content/zen-styles/zen-branding.css)
content/browser/zen-styles/zen-welcome.css (content/zen-styles/zen-welcome.css)
content/browser/zen-styles/zen-media-controls.css (content/zen-styles/zen-media-controls.css)
content/browser/zen-styles/zen-panels/bookmarks.css (content/zen-styles/zen-panels/bookmarks.css)
content/browser/zen-styles/zen-panels/extensions.css (content/zen-styles/zen-panels/extensions.css)

View File

@@ -0,0 +1,48 @@
<toolbar id="zen-media-controls-toolbar"
class="browser-toolbar customization-target zen-sidebar-toolbar"
context="toolbar-context-menu"
mode="icons"
hidden="true">
<toolbaritem>
<vbox id="zen-media-main-vbox">
<hbox id="zen-media-service-hbox" class="show-on-hover">
<html:div id="zen-media-service-button">
<image/>
</html:div>
<label id="zen-media-service-title" fadein="true"/>
</hbox>
<vbox id="zen-media-info-vbox" class="show-on-hover">
<label id="zen-media-title" fadein="true"/>
<label id="zen-media-artist" fadein="true"/>
</vbox>
<hbox id="zen-media-progress-hbox" class="show-on-hover">
<label id="zen-media-current-time">0:00</label>
<html:input type="range" id="zen-media-progress-bar"
value="0" min="0" max="100" step="0.1"
oninput="gZenMediaController.onMediaSeekDrag(event);"
onchange="gZenMediaController.onMediaSeekComplete(event);"/>
<label id="zen-media-duration">0:00</label>
</hbox>
<hbox id="zen-media-controls-hbox">
<toolbarbutton id="zen-media-focus-button"
class="toolbarbutton-1"
oncommand="gZenMediaController.onMediaFocus();" />
<toolbarbutton id="zen-media-previoustrack-button"
class="toolbarbutton-1"
oncommand="gZenMediaController.onMediaPlayPrev();" />
<toolbarbutton id="zen-media-playpause-button"
class="toolbarbutton-1"
oncommand="gZenMediaController.onMediaToggle();" />
<toolbarbutton id="zen-media-nexttrack-button"
class="toolbarbutton-1"
oncommand="gZenMediaController.onMediaPlayNext();" />
<toolbarbutton id="zen-media-mute-button"
class="toolbarbutton-1"
oncommand="gZenMediaController.onMediaMute();" />
</hbox>
</vbox>
</toolbaritem>
</toolbar>

View File

@@ -0,0 +1,191 @@
#zen-media-controls-toolbar {
--progress-height: 5px;
--button-spacing: 2px;
display: flex;
justify-content: space-between;
min-width: 0;
padding: 5px;
border-radius: 10px;
background: var(--zen-toolbar-element-bg) !important;
container-type: inline-size;
.toolbarbutton-1 {
border-radius: 5px;
color: white;
}
#zen-media-prev-button,
#zen-media-play-pause-button,
#zen-media-next-button {
margin: 0;
}
image.toolbarbutton-icon {
padding: 5px;
width: 26px;
height: 26px;
}
#zen-media-progress-bar {
appearance: none;
width: 100%;
height: var(--progress-height);
margin: 0 8px;
border-radius: 2px;
background-color: rgba(255, 255, 255, 0.2);
cursor: pointer;
transition: height 0.15s ease-out;
&::-moz-range-track {
background: var(--zen-colors-border);
border-radius: 999px;
height: var(--progress-height);
}
&::-moz-range-progress {
background: var(--zen-primary-color);
border-radius: 999px;
height: var(--progress-height);
}
&::-moz-range-thumb {
background: var(--zen-primary-color);
border: none;
width: 14px;
height: 14px;
border-radius: 50%;
cursor: pointer;
}
}
&:hover {
.show-on-hover {
max-height: 50px;
opacity: 1;
transform: translateY(0);
}
}
#zen-media-focus-button {
width: 34px;
height: 26px;
align-self: center;
transition: opacity 0.2s ease, transform 0.2s ease;
@container (max-width: 185px) {
width: 0;
height: 0;
opacity: 0;
padding: 0;
transform: translateX(-20px);
}
@container (min-width: 185px) {
opacity: 1;
transform: translateX(0);
}
}
toolbaritem {
flex-grow: 1;
padding: 0;
transition: padding 0.3s ease-out;
}
.show-on-hover {
padding-inline: 4px;
max-height: 0;
opacity: 0;
overflow: hidden;
transform: translateY(5px);
transition: max-height 0.35s ease-in-out,
opacity 0.25s ease-in-out,
transform 0.3s ease-in-out;
}
#zen-media-current-time,
#zen-media-duration {
margin: 0 0 0 1px;
}
}
@keyframes zen-media-controls-show {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
#zen-media-controls-toolbar {
&:not([hidden]) {
animation: zen-media-controls-show 0.2s ease-out;
display: flex;
}
&[hidden] {
display: none;
animation: none;
transition: none;
}
}
#zen-media-service-title,
#zen-media-title,
#zen-media-artist {
white-space: nowrap;
width: 0;
margin-left: 0;
}
#zen-media-service-title {
align-self: center;
font-size: 13px;
margin-bottom: 5px;
margin-left: 6px;
}
#zen-media-title,
#zen-media-artist {
align-self: start;
margin-top: 5px;
}
#zen-media-title {
height: 16px;
font-size: 16px;
font-weight: bold;
}
#zen-media-artist {
height: 5px;
padding-bottom: 20px;
}
#zen-media-main-vbox,
#zen-media-service-hbox,
#zen-media-info-vbox,
#zen-media-progress-hbox {
width: 100%;
}
#zen-media-main-vbox {
height: 100%;
justify-content: space-between;
overflow: hidden;
}
#zen-media-progress-hbox {
flex-grow: 1;
align-items: center;
}
#zen-media-controls-hbox {
align-items: flex-end;
justify-content: space-evenly;
max-width: 100%;
overflow: hidden;
}
#zen-media-service-button image {
width: 20px;
height: 20px;
}