-
-
Notifications
You must be signed in to change notification settings - Fork 241
Feature/youtube buttons #2388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bwhalley
wants to merge
5
commits into
mbnuqw:v5
Choose a base branch
from
bwhalley:feature/youtube-buttons
base: v5
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Feature/youtube buttons #2388
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
94ead07
feat(sidebar): YouTube playback controls bar for tabs panel
bwhalley 5717a25
fix(youtube-bar): detection, active-tab fallback, row layout above bo…
bwhalley 688298e
fix(youtube-bar): native audible probe + chevron icons
bwhalley 0a3084d
chore(youtube-bar): drop empty-state hint, trim service and styles
bwhalley 88a8e63
defaults: turn off YouTube controls bar by default (subPanelYouTube)
bwhalley File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| import type { Tab, Reactivator } from 'src/types' | ||
| import { NOID } from 'src/defaults' | ||
| import * as Tabs from 'src/services/tabs.fg' | ||
| import * as Windows from 'src/services/windows.fg' | ||
| import * as Logs from 'src/services/logs' | ||
| import * as Permissions from 'src/services/permissions.fg' | ||
|
|
||
| export interface YouTubeReactiveState { | ||
| probeTick: number | ||
| } | ||
|
|
||
| export let reactive: YouTubeReactiveState = { probeTick: 0 } | ||
|
|
||
| export function reactivate(r: Reactivator<YouTubeReactiveState>): void { | ||
| reactive = r(reactive) | ||
| } | ||
|
|
||
| let nativeAudibleYtTabId: ID = NOID | ||
| let probeTimeout: number | undefined | ||
| let probeInterval: number | undefined | ||
| let listenersBound = false | ||
|
|
||
| function tabUrl(t: Tab): string { | ||
| return t.url || t.reactive.url || '' | ||
| } | ||
|
|
||
| function isYouTubePlaybackUrl(url: string | undefined): boolean { | ||
| if (!url || url.startsWith('about:')) return false | ||
| try { | ||
| const u = new URL(url) | ||
| const h = u.hostname.toLowerCase() | ||
| if (h === 'youtu.be') return true | ||
| if (h === 'music.youtube.com') return u.pathname.startsWith('/watch') | ||
| if (h === 'm.youtube.com' || h.endsWith('.youtube.com') || h === 'youtube.com') { | ||
| const p = u.pathname | ||
| return ( | ||
| p.startsWith('/watch') || | ||
| p.startsWith('/shorts/') || | ||
| p.startsWith('/live/') || | ||
| p.startsWith('/embed/') | ||
| ) | ||
| } | ||
| } catch { | ||
| /* ignore */ | ||
| } | ||
| return ( | ||
| url.includes('youtu.be/') || | ||
| url.includes('youtube.com/watch') || | ||
| url.includes('youtube.com/shorts/') || | ||
| url.includes('youtube.com/live/') || | ||
| url.includes('youtube.com/embed/') || | ||
| url.includes('music.youtube.com/watch') | ||
| ) | ||
| } | ||
|
|
||
| async function refreshNativeAudibleYouTubeTabId(): Promise<void> { | ||
| if (!Windows.id) return | ||
| try { | ||
| const audibleTabs = await browser.tabs.query({ audible: true, windowId: Windows.id }) | ||
| let next: ID = NOID | ||
| for (const nt of audibleTabs) { | ||
| if (nt.id === undefined || !nt.url) continue | ||
| if (isYouTubePlaybackUrl(nt.url)) { | ||
| next = nt.id | ||
| break | ||
| } | ||
| } | ||
| if (next !== nativeAudibleYtTabId) { | ||
| nativeAudibleYtTabId = next | ||
| reactive.probeTick++ | ||
| } | ||
| } catch (err) { | ||
| Logs.err('YouTube: audible probe failed', err) | ||
| } | ||
| } | ||
|
|
||
| function scheduleNativeAudibleProbe(): void { | ||
| clearTimeout(probeTimeout) | ||
| probeTimeout = setTimeout(() => { | ||
| probeTimeout = undefined | ||
| void refreshNativeAudibleYouTubeTabId() | ||
| }, 80) as unknown as number | ||
| } | ||
|
|
||
| /** Sync audible YouTube tab id from Firefox (Sidebery tab updates can lag). */ | ||
| export function setupYouTubeBarListeners(): void { | ||
| if (listenersBound) return | ||
| listenersBound = true | ||
|
|
||
| const onUpdated = (_tabId: ID, change: browser.tabs.ChangeInfo): void => { | ||
| if ( | ||
| change.audible !== undefined || | ||
| change.url !== undefined || | ||
| change.status !== undefined | ||
| ) { | ||
| scheduleNativeAudibleProbe() | ||
| } | ||
| } | ||
| browser.tabs.onUpdated.addListener(onUpdated) | ||
| browser.tabs.onActivated.addListener(scheduleNativeAudibleProbe) | ||
| void refreshNativeAudibleYouTubeTabId() | ||
|
|
||
| probeInterval = setInterval(() => { | ||
| void refreshNativeAudibleYouTubeTabId() | ||
| }, 3000) as unknown as number | ||
| } | ||
|
|
||
| /** Resolve target: native audible match, then Sidebery state, then active-tab fallback. */ | ||
| export function getTargetTab(): Tab | undefined { | ||
| for (const t of Tabs.list) { | ||
| void t.reactive.mediaAudible | ||
| void t.reactive.mediaPaused | ||
| void t.reactive.mediaMuted | ||
| void t.reactive.url | ||
| void t.reactive.active | ||
| } | ||
| void reactive.probeTick | ||
|
|
||
| const urlOk = (t: Tab) => !t.discarded && isYouTubePlaybackUrl(tabUrl(t)) | ||
|
|
||
| if (nativeAudibleYtTabId !== NOID) { | ||
| const t = Tabs.byId[nativeAudibleYtTabId] | ||
| if (t && urlOk(t)) return t | ||
| } | ||
|
|
||
| const audible = Tabs.list.find( | ||
| t => urlOk(t) && (t.reactive.mediaAudible || t.audible === true) | ||
| ) | ||
| if (audible) return audible | ||
|
|
||
| const paused = Tabs.list.find(t => urlOk(t) && (t.reactive.mediaPaused || t.mediaPaused)) | ||
| if (paused) return paused | ||
|
|
||
| const act = Tabs.byId[Tabs.activeId] | ||
| if (act && urlOk(act)) return act | ||
|
|
||
| return undefined | ||
| } | ||
|
|
||
| export function isTargetMuted(tab: Tab): boolean { | ||
| return tab.reactive.mediaMuted || tab.mutedInfo?.muted === true | ||
| } | ||
|
|
||
| export async function play(): Promise<void> { | ||
| const tab = getTargetTab() | ||
| if (!tab) return | ||
| await Tabs.playTabMedia(tab.id) | ||
| } | ||
|
|
||
| export async function pause(): Promise<void> { | ||
| const tab = getTargetTab() | ||
| if (!tab) return | ||
| await Tabs.pauseTabMedia(tab.id) | ||
| } | ||
|
|
||
| export function mute(): void { | ||
| const tab = getTargetTab() | ||
| if (!tab) return | ||
| browser.tabs.update(tab.id, { muted: true }).catch(err => { | ||
| Logs.err('YouTube: mute tab failed', err) | ||
| }) | ||
| } | ||
|
|
||
| export function unmute(): void { | ||
| const tab = getTargetTab() | ||
| if (!tab) return | ||
| browser.tabs.update(tab.id, { muted: false }).catch(err => { | ||
| Logs.err('YouTube: unmute tab failed', err) | ||
| }) | ||
| } | ||
|
|
||
| async function ensureScriptingPermission(): Promise<boolean> { | ||
| if (!Permissions.reactive.webData) { | ||
| const result = await Permissions.request('<all_urls>') | ||
| if (!result) return false | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| export async function prevVideo(): Promise<void> { | ||
| if (!(await ensureScriptingPermission())) return | ||
| const tab = getTargetTab() | ||
| if (!tab) return | ||
| browser.tabs | ||
| .executeScript(tab.id, { | ||
| code: "void document.querySelector('.ytp-prev-button')?.click()", | ||
| runAt: 'document_idle', | ||
| }) | ||
| .catch(err => { | ||
| Logs.err('YouTube: prev executeScript failed', err) | ||
| }) | ||
| } | ||
|
|
||
| export async function nextVideo(): Promise<void> { | ||
| if (!(await ensureScriptingPermission())) return | ||
| const tab = getTargetTab() | ||
| if (!tab) return | ||
| browser.tabs | ||
| .executeScript(tab.id, { | ||
| code: "void document.querySelector('.ytp-next-button')?.click()", | ||
| runAt: 'document_idle', | ||
| }) | ||
| .catch(err => { | ||
| Logs.err('YouTube: next executeScript failed', err) | ||
| }) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| <template lang="pug"> | ||
| .YouTubeControlsBar | ||
| .YouTubeControlsBar-inner | ||
| .tool-btn( | ||
| :data-disabled="!tab" | ||
| @click="onPrev" | ||
| :title="translate('bar.youtube.prev')") | ||
| svg: use(href="#icon_chevron_left") | ||
| .tool-btn( | ||
| :data-disabled="!tab" | ||
| @click="onPlayPause" | ||
| :title="tab && isPaused ? translate('bar.youtube.play') : translate('bar.youtube.pause')") | ||
| svg(v-if="tab && isPaused"): use(href="#icon_play") | ||
| svg(v-else): use(href="#icon_pause") | ||
| .tool-btn( | ||
| :data-disabled="!tab" | ||
| @click="onNext" | ||
| :title="translate('bar.youtube.next')") | ||
| svg: use(href="#icon_chevron_right") | ||
| .tool-btn( | ||
| :data-disabled="!tab" | ||
| @click="onMuteToggle" | ||
| :title="tab && muted ? translate('bar.youtube.unmute') : translate('bar.youtube.mute')") | ||
| svg(v-if="tab && muted"): use(href="#icon_mute") | ||
| svg(v-else): use(href="#icon_loud") | ||
| </template> | ||
|
|
||
| <script lang="ts" setup> | ||
| import { computed } from 'vue' | ||
| import { translate } from 'src/dict' | ||
| import * as Tabs from 'src/services/tabs.fg' | ||
| import * as YouTube from 'src/services/youtube.fg' | ||
|
|
||
| const tab = computed(() => YouTube.getTargetTab()) | ||
|
|
||
| const isPaused = computed(() => { | ||
| const t = tab.value | ||
| if (!t) return true | ||
| if (t.reactive.mediaPaused || t.mediaPaused) return true | ||
| if (t.reactive.mediaAudible || t.audible) return false | ||
| if (t.id === Tabs.activeId || t.reactive.active) return false | ||
| return true | ||
| }) | ||
|
|
||
| const muted = computed(() => { | ||
| const t = tab.value | ||
| if (!t) return false | ||
| return YouTube.isTargetMuted(t) | ||
| }) | ||
|
|
||
| function onPlayPause(): void { | ||
| if (!tab.value) return | ||
| if (isPaused.value) void YouTube.play() | ||
| else void YouTube.pause() | ||
| } | ||
|
|
||
| function onMuteToggle(): void { | ||
| const t = tab.value | ||
| if (!t) return | ||
| if (YouTube.isTargetMuted(t)) YouTube.unmute() | ||
| else YouTube.mute() | ||
| } | ||
|
|
||
| function onPrev(): void { | ||
| void YouTube.prevVideo() | ||
| } | ||
|
|
||
| function onNext(): void { | ||
| void YouTube.nextVideo() | ||
| } | ||
| </script> | ||
|
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using YouTube's own translations would be much better for correctness and consistency.
Picking this one just as an example. YouTube's translation is "Désactiver le son". The addition of de l'onglet is a separate decision, which might be worth discussing as well.
Missing languages should be easy to add too, but I'm not sure what the Chinese ones correspond to on YouTube's language list which has Simplified, Traditional, and Hong Kong