Skip to content

Commit

Permalink
fix(ui): dynamic remove doesnt decrement counter
Browse files Browse the repository at this point in the history
Add a function to decrement the value
of the count of videos in the playlist

Closes #19
  • Loading branch information
avallete committed Mar 24, 2021
1 parent dbe22e6 commit 58dd78f
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 72 deletions.
2 changes: 1 addition & 1 deletion src/lib/get-elements-by-xpath.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Query DOM Xpath and return an array of Node
// Usage is similar to $x in console
export default function getElementsByXPath(xpath: string, parent?: HTMLElement): Node[] {
export default function getElementsByXPath(xpath: string, parent?: Element): Node[] {
const results = []
const query = document.evaluate(xpath, parent || document, undefined, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
const length = query.snapshotLength
Expand Down
44 changes: 44 additions & 0 deletions src/lib/list-map-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Search into two lists of objects needles and haystack using hashmap
* If all elements from needles has been found into haystack it return the hashmap
* If some elements are missing into haystack, we return false.
*
* The hashmap keys are generated using needleGetter and haystackGetter
* The needles must not contain duplicates
*
* @param needles unique list of element to search
* @param haystack list of elements to search in
* @param needleKeyGetter get the value to use as key from needles
* @param haystackKeyGetter get the value to match with needle key
*/
export default function listMapSearch<T, U, K extends keyof any>(
needles: Array<T>,
haystack: Array<U>,
needleKeyGetter: (item: T) => K,
haystackKeyGetter: (item: U) => K
): Record<K, U> | false {
const searchMap: Record<K, U | undefined> = {} as Record<K, U>
// We cannot found all our needles into our haystack
if (haystack.length < needles.length) {
return false
}
// Fill our searchMap keys with needles to search
for (const needle of needles) {
searchMap[needleKeyGetter(needle)] = undefined
}
// matches elements from needles with haystack
let found = 0
for (const item of haystack) {
const itemKey = haystackKeyGetter(item)
// if key exist in the searchMap and value is still undefined
if (Object.prototype.hasOwnProperty.call(searchMap, itemKey) === true && searchMap[itemKey] === undefined) {
searchMap[itemKey] = item
found += 1
// early break if all elements have already been found
if (found === needles.length) {
return searchMap as Record<K, U>
}
}
}
return found === needles.length ? (searchMap as Record<K, U>) : false
}
32 changes: 32 additions & 0 deletions src/operations/ui/hide-videos-from-playlist-ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { XPATH } from '~src/selectors'
import getElementsByXpath from '~src/lib/get-elements-by-xpath'
import listMapSearch from '~src/lib/list-map-search'
import { PlaylistVideo } from '~src/youtube'

// We don't remove but only hide the videos since
// Youtube webapp use the indexes to handle some actions (remove, reorder)
// and removing videos from the DOM collide with that behavior
export default function hideVideosFromPlaylistUI(videosToDelete: PlaylistVideo[]) {
// cast Node as any to access .data property availlable on ytd-playlist-video-renderer elements
const playlistVideoRendererNodes = getElementsByXpath(XPATH.YT_PLAYLIST_VIDEO_RENDERERS) as any[]
// All videos to remove MAY be present in the UI because if there is more videos to remove
// than videos found into the UI, some removed videos aren't loaded in the UI
if (playlistVideoRendererNodes.length >= videosToDelete.length) {
const searchMap = listMapSearch(
videosToDelete,
playlistVideoRendererNodes,
(video) => video.videoId,
(node) => node.data.videoId
)
// if all videos to remove are present in the UI
if (searchMap) {
const htmlElements: HTMLElement[] = Object.values(searchMap) as HTMLElement[]
for (const element of htmlElements) {
// hide each item from UI
element.hidden = true
}
return
}
}
throw new Error('some videos are missing from the UI, cannot dynamically delete')
}
9 changes: 4 additions & 5 deletions src/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import U from '~src/userscript'

export const XPATH = {
APP_RENDER_ROOT: '//ytd-playlist-sidebar-renderer/div[@id="items"]/*[last()]',
YT_PLAYLIST_SIDEBAR_ITEMS: '//ytd-playlist-sidebar-renderer/div[@id="items"]',
YT_PLAYLIST_VIDEO_RENDERERS: '//yt-playlist-video-renderer',
YT_PLAYLIST_VIDEO_RENDERERS: '//ytd-playlist-video-renderer',
YT_NUMBERS_OF_VIDEOS_IN_PLAYLIST: '//ytd-playlist-sidebar-primary-info-renderer/div/yt-formatted-string/span[1]',
}

export const ID = {
APP_ROOT: `${U.id}-root`,
export default {
XPATH,
}
89 changes: 23 additions & 66 deletions src/youtube.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,14 @@
* List of types and defines useful for youtube webapp/api interaction
*/

// All the events who are fired by youtube webapp
export type YTEvent =
| 'yt-consent-bump-display-changed'
| 'yt-rendererstamper-finished'
| 'yt-autonav-pause-focus'
| 'yt-autonav-pause-blur'
| 'yt-autonav-pause-guide-opened'
| 'yt-autonav-pause-guide-closed'
| 'yt-report-form-opened'
| 'yt-report-form-closed'
| 'yt-autonav-pause-scroll'
| 'yt-autonav-pause-player'
| 'yt-autonav-pause-player-ended'
| 'yt-history-load'
| 'yt-history-pop'
| 'yt-navigate'
| 'yt-navigate-set-page-offset'
| 'yt-update-title'
| 'yt-update-unseen-notification-count'
| 'yt-service-request-completed'
| 'yt-service-request-sent'
| 'yt-service-request-error'
| 'yt-add-element-to-app'
| 'yt-guide-hover'
| 'yt-masthead-height-changed'
| 'yt-page-type-changed'
| 'yt-request-panel-mode-change'
| 'yt-set-theater-mode-enabled'
| 'yt-set-fullerscreen-styles'
| 'yt-focus-searchbox'
| 'yt-open-hotkey-dialog'
| 'yt-page-data-updated'
| 'yt-about-this-ad-closed'
| 'yt-page-manager-navigate-start'
| 'yt-navigate-start'
| 'yt-navigate-finish'
| 'yt-navigate-error'
| 'yt-page-data-fetched'
| 'yt-navigate-redirect'
| 'yt-visibility-refresh'
| 'yt-visibility-monitor-refreshed'
| 'yt-toggle-button'
| 'yt-get-context-provider'
| 'yt-player-attached'
| 'yt-player-detached'
| 'yt-update-notifications-unseen-count-action'
| 'yt-autoplay-on-changed'
| 'yt-page-navigate-start'
| 'yt-retrieve-location'
| 'yt-subscription-changed'
| 'yt-page-data-will-update'
| 'yt-show-survey'
| 'yt-popup-opened'
| 'yt-popup-closed'
| 'yt-popup-canceled'
| 'yt-lockup-requested'
| 'yt-load-next-continuation'
| 'yt-load-reload-continuation'
| 'yt-dismissible-item-undismissed'
| 'yt-dismissible-item-dismissed'
// Add the ytcfg object to the global window
declare global {
interface Window {
ytcfg: {
data_: any
}
}
}

export interface YTConfigData {
DEVICE: string
Expand All @@ -77,10 +25,19 @@ export interface YTConfigData {
ORIGIN_URL: string
}

declare global {
interface Window {
ytcfg: {
data_: any
}
}
export interface PlaylistVideo {
videoId: string
percentDurationWatched: number
}

export interface PlaylistContinuation {
videos: PlaylistVideo[]
continuationToken?: string
}

export interface Playlist {
playlistId: string
isEditable: boolean
canReorder: boolean
continuations: PlaylistContinuation[]
}

0 comments on commit 58dd78f

Please sign in to comment.