From a23d0bfa354fe2370eac5652fd2da7a0618a75cb Mon Sep 17 00:00:00 2001 From: Isaac <76838845+isaxk@users.noreply.github.com> Date: Fri, 30 Aug 2024 21:44:27 +0100 Subject: [PATCH] custom theming --- .eslintrc.cjs | 26 +- .prettierrc.yaml | 2 +- electron-builder.yml | 12 +- electron.vite.config.ts | 36 +- src/main/discord/index.ts | 61 -- src/main/env.d.ts | 4 +- src/main/index.ts | 132 +-- src/main/intergrations/discord.ts | 58 ++ src/main/player/index.ts | 55 +- src/main/tabs/index.ts | 737 +++++++++-------- src/main/utils/customcss.ts | 72 ++ src/main/utils/index.ts | 30 +- src/main/utils/types.ts | 62 ++ src/main/windows/index.ts | 27 +- src/preload/index.d.ts | 9 +- src/preload/index.ts | 93 +-- src/preload/music.ts | 768 +++++++++--------- src/preload/types.ts | 21 + src/renderer/src/App.svelte | 61 +- src/renderer/src/assets/app.css | 10 +- .../src/components/menubar/Navigation.svelte | 27 +- .../src/components/menubar/Tab.svelte | 91 ++- .../src/components/menubar/TabBar.svelte | 36 +- .../src/components/miniplayer/Progress.svelte | 30 +- .../components/miniplayer/VolumeBar.svelte | 44 +- .../components/settings/KeybindInput.svelte | 14 +- .../src/components/settings/Select.svelte | 49 ++ .../components/settings/SettingItem.svelte | 145 +++- .../src/components/settings/SidebarTab.svelte | 34 +- .../src/components/ui/MacSpace.svelte | 4 +- src/renderer/src/components/ui/Spinner.svelte | 90 +- src/renderer/src/lib/stores.ts | 3 + src/renderer/src/lib/types.ts | 37 + src/renderer/src/lib/utils.ts | 12 +- src/renderer/src/main.ts | 10 +- src/renderer/src/views/MenuBar.svelte | 13 +- src/renderer/src/views/MiniPlayer.svelte | 104 +-- src/renderer/src/views/Settings.svelte | 84 +- svelte.config.mjs | 6 +- tailwind.config.ts | 4 +- website/package.json | 52 +- website/postcss.config.js | 4 +- website/src/app.css | 4 +- website/src/app.d.ts | 14 +- website/src/app.html | 20 +- website/src/routes/+layout.svelte | 2 +- website/src/routes/+page.svelte | 95 ++- website/svelte.config.js | 22 +- website/tailwind.config.ts | 8 +- website/vite.config.ts | 6 +- 50 files changed, 1918 insertions(+), 1422 deletions(-) delete mode 100644 src/main/discord/index.ts create mode 100644 src/main/intergrations/discord.ts create mode 100644 src/main/utils/customcss.ts create mode 100644 src/main/utils/types.ts create mode 100644 src/preload/types.ts create mode 100644 src/renderer/src/components/settings/Select.svelte create mode 100644 src/renderer/src/lib/types.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index fd35482..3b52a1c 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,23 +1,23 @@ module.exports = { parserOptions: { - extraFileExtensions: ['.svelte'] + extraFileExtensions: [".svelte"], }, extends: [ - 'eslint:recommended', - 'plugin:svelte/recommended', - '@electron-toolkit/eslint-config-ts/recommended', - '@electron-toolkit/eslint-config-prettier' + "eslint:recommended", + "plugin:svelte/recommended", + "@electron-toolkit/eslint-config-ts/recommended", + "@electron-toolkit/eslint-config-prettier", ], overrides: [ { - files: ['*.svelte'], - parser: 'svelte-eslint-parser', + files: ["*.svelte"], + parser: "svelte-eslint-parser", parserOptions: { - parser: '@typescript-eslint/parser' - } - } + parser: "@typescript-eslint/parser", + }, + }, ], rules: { - 'svelte/no-unused-svelte-ignore': 'off' - } -} + "svelte/no-unused-svelte-ignore": "off", + }, +}; diff --git a/.prettierrc.yaml b/.prettierrc.yaml index 1d62719..2fb8ee7 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -5,6 +5,6 @@ trailingComma: none plugins: - prettier-plugin-svelte overrides: - - files: '*.svelte' + - files: "*.svelte" options: parser: svelte diff --git a/electron-builder.yml b/electron-builder.yml index 9ee1db4..aebb4d5 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -3,12 +3,12 @@ productName: YT Desk directories: buildResources: build files: - - '!**/.vscode/*' - - '!src/*' - - '!electron.vite.config.{js,ts,mjs,cjs}' - - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' - - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' - - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' + - "!**/.vscode/*" + - "!src/*" + - "!electron.vite.config.{js,ts,mjs,cjs}" + - "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}" + - "!{.env,.env.*,.npmrc,pnpm-lock.yaml}" + - "!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}" asarUnpack: - resources/** win: diff --git a/electron.vite.config.ts b/electron.vite.config.ts index e8659b2..c260f26 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -1,24 +1,24 @@ -import { defineConfig, externalizeDepsPlugin } from 'electron-vite' -import { svelte } from '@sveltejs/vite-plugin-svelte' -import { resolve } from 'path' +import { defineConfig, externalizeDepsPlugin } from "electron-vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { resolve } from "path"; export default defineConfig({ main: { - plugins: [externalizeDepsPlugin()] + plugins: [externalizeDepsPlugin()], }, preload: { - plugins: [externalizeDepsPlugin()], - build: { - rollupOptions: { - input: { - main: resolve(__dirname, "./src/preload/index.ts"), - yt: resolve(__dirname, "./src/preload/yt.ts"), - music: resolve(__dirname, "./src/preload/music.ts"), - } - } - } - }, + plugins: [externalizeDepsPlugin()], + build: { + rollupOptions: { + input: { + main: resolve(__dirname, "./src/preload/index.ts"), + yt: resolve(__dirname, "./src/preload/yt.ts"), + music: resolve(__dirname, "./src/preload/music.ts"), + }, + }, + }, + }, renderer: { - plugins: [svelte()] - } -}) + plugins: [svelte()], + }, +}); diff --git a/src/main/discord/index.ts b/src/main/discord/index.ts deleted file mode 100644 index 9353bca..0000000 --- a/src/main/discord/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Client } from '@xhayper/discord-rpc' -import { ipcMain } from 'electron' -import { factory } from 'electron-json-config' - -const store = factory() - -const client = new Client({ - clientId: '1265008196876242944' -}) - - -export function setVideo(id, title, author) { - client.user?.setActivity({ - state: author, - details: title, - type: 3, // Watching... - largeImageKey: `https://img.youtube.com/vi/${id}/0.jpg`, - smallImageKey: `https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/YouTube_social_white_square_%282017%29.svg/2048px-YouTube_social_white_square_%282017%29.svg.png` - }) -} - -export function setMusic(title, author, thumbnail) { - client.user?.setActivity({ - state: author, - details: title, - type: 2, // Listening... - largeImageKey: thumbnail, - smallImageKey: `https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Youtube_Music_icon.svg/512px-Youtube_Music_icon.svg.png` - }) -} - -export function clearActivity() { - client.user?.clearActivity() -} - -export function discordClient() { - client.login(); - - return { - setMusic, - setVideo, - clearActivity, - enable: () => { - return - } - } -} - -ipcMain.on('ytView:videoDataChanged', (_, data) => { - if (data && store.get('discord-rpc')) { - setVideo(data.video_id, data.title, data.author) - } else { - client.user?.clearActivity() - } -}) - -ipcMain.on('ytmView:videoDataChanged', (_, data) => { - if (data === null || store.get('discord-rpc') === false) return - console.log(data.thumbnail.thumbnails); - setMusic(data.title, data.author, data.thumbnail.thumbnails[0].url) -}) diff --git a/src/main/env.d.ts b/src/main/env.d.ts index 4b0092f..cd62cb9 100644 --- a/src/main/env.d.ts +++ b/src/main/env.d.ts @@ -1,5 +1,5 @@ /// interface ImportMeta { - readonly env: ImportMetaEnv - } \ No newline at end of file + readonly env: ImportMetaEnv; +} diff --git a/src/main/index.ts b/src/main/index.ts index 4f7e18f..2c36eaa 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,16 +1,17 @@ +import { electronApp, optimizer } from "@electron-toolkit/utils"; import { app, BrowserWindow, + globalShortcut, ipcMain, nativeTheme, - globalShortcut, } from "electron"; -import { electronApp, optimizer } from "@electron-toolkit/utils"; -import { createTabManager } from "./tabs"; import { factory } from "electron-json-config"; -import { clearActivity, discordClient } from "./discord"; -import { createMainWindowManager, MainWindowManager } from "./windows"; +import { discordClient } from "./intergrations/discord"; import { initPlayerEvents } from "./player"; +import { createTabManager } from "./tabs"; +import { createMainWindowManager, MainWindowManager } from "./windows"; +import { downloadCss, loadCss } from "./utils/customcss"; const store = factory(); @@ -25,8 +26,8 @@ app.whenReady().then(async () => { mainWindowManager = createMainWindowManager(); initPlayerEvents(mainWindowManager); - let tabsManager = await createTabManager(mainWindowManager.window); + let discord = discordClient(); mainWindowManager.window.webContents.on("did-finish-load", () => { tabsManager.getTabs()[0].view.webContents.on("did-finish-load", () => { @@ -40,10 +41,23 @@ app.whenReady().then(async () => { "ad-blocking": false, "discord-rpc": false, "studio-tab": false, + "force-cinema": false, "miniplayer-on-top": false, - "playback-bind": "Ctrl+Shift+Space", - "next-bind": "Ctrl+Shift+Right", - "previous-bind": "Ctrl+Shift+Left", + "playback-bind": "Ctrl+Alt+Space", + "next-bind": "Ctrl+Alt+Right", + "previous-bind": "Ctrl+Alt+Left", + "music-css": JSON.stringify({ + enabled: false, + type: "url", + url: "", + css: "", + }), + "yt-css": JSON.stringify({ + enabled: false, + type: "url", + url: "", + css: "", + }) }; ipcMain.handle("get-config", (_, key: string) => { @@ -54,60 +68,78 @@ app.whenReady().then(async () => { } }); + let shortcuts = [ + { + key: "playback-bind", + handler: () => { + if (tabsManager.getTabs()[0]) { + tabsManager + .getTabs()[0] + .view.webContents.send("remoteControl:execute", "playPause"); + } + }, + }, + { + key: "next-bind", + handler: () => { + if (tabsManager.getTabs()[0]) { + tabsManager + .getTabs()[0] + .view.webContents.send("remoteControl:execute", "next"); + } + }, + }, + { + key: "previous-bind", + handler: () => { + if (tabsManager.getTabs()[0]) { + tabsManager + .getTabs()[0] + .view.webContents.send("remoteControl:execute", "previous"); + } + }, + }, + ]; + function registerShortcuts() { globalShortcut.unregisterAll(); - if (store.get("playback-bind", defaults["playback-bind"])! !== "Unbound") { - globalShortcut.register( - store.get("playback-bind", defaults["playback-bind"])!, - () => { - if (tabsManager.getTabs()[0]) { - tabsManager - .getTabs()[0] - .view.webContents.send("remoteControl:execute", "playPause"); - } - }, - ); - } - if (store.get("next-bind", defaults["next-bind"])! !== "Unbound") { - globalShortcut.register( - store.get("next-bind", defaults["next-bind"])!, - () => { - if (tabsManager.getTabs()[0]) { - tabsManager - .getTabs()[0] - .view.webContents.send("remoteControl:execute", "next"); - } - }, - ); - } - if (store.get("previous-bind", defaults["next-bind"])! !== "Unbound") { - globalShortcut.register( - store.get("previous-bind", defaults["previous-bind"])!, - () => { - if (tabsManager.getTabs()[0]) { - tabsManager - .getTabs()[0] - .view.webContents.send("remoteControl:execute", "previous"); - } - }, - ); - } + shortcuts.forEach((bind) => { + if (store.get(bind.key, defaults[bind.key])! !== "Unbound") { + globalShortcut.register( + store.get(bind.key, defaults[bind.key])!, + () => { + console.log(bind.key, "executed"); + bind.handler(); + }, + ); + } + }); } ipcMain.on("set-config", (_, e) => { - console.log(e.key, e.value) store.set(e.key, e.value); - mainWindowManager.applyTheme() + console.log("Config set:", e.key, e.value) + + mainWindowManager.applyTheme(); + if (e.key === "discord-rpc") { if (e.value === false) { - clearActivity(); + discord.clear(); } else { - discordClient().enable(); + discord.enable(); } } + if (e.key === "miniplayer-on-top" && mainWindowManager.miniPlayerOpen()) { - mainWindowManager.window.setAlwaysOnTop(store.get("miniplayer-on-top") == true); + mainWindowManager.window.setAlwaysOnTop( + store.get("miniplayer-on-top") == true, + ); } + + if (e.key === "music-css" || e.key === "yt-css") { + tabsManager.updateCustomCss(); + } + if (tabsManager.getTabs().length > 0) { registerShortcuts(); } diff --git a/src/main/intergrations/discord.ts b/src/main/intergrations/discord.ts new file mode 100644 index 0000000..7fcd503 --- /dev/null +++ b/src/main/intergrations/discord.ts @@ -0,0 +1,58 @@ +import { Client } from "@xhayper/discord-rpc"; +import { playerManager } from "../player"; +import { playerState } from "../utils/types"; + +const client = new Client({ + clientId: "1265008196876242944", +}); + +client.login(); + +function setVideo(id: string, title: string, author: string) { + client.user?.setActivity({ + state: author, + details: title, + type: 3, // Watching... + largeImageKey: `https://img.youtube.com/vi/${id}/0.jpg`, + smallImageKey: `https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/YouTube_social_white_square_%282017%29.svg/2048px-YouTube_social_white_square_%282017%29.svg.png`, + }); +} + +function setMusic(title: string, author: string, thumbnail: string) { + client.user?.setActivity({ + state: author, + details: title, + type: 2, // Listening... + largeImageKey: thumbnail, + smallImageKey: `https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Youtube_Music_icon.svg/512px-Youtube_Music_icon.svg.png`, + }); +} + +function clear() { + client.user?.clearActivity(); +} + +export function discordClient() { + return { + setMusic, + setVideo, + clear, + enable: () => { + let music = playerManager().getData().music; + let video = playerManager().getData().video; + + if ( + (video !== null && music.state !== playerState.Playing && music.data) || + music.data !== null + ) { + setMusic( + music.data?.title, + music.data?.author, + music.data?.thumbnail.thumbnails[2].url, + ); + } else if (video !== null) { + setVideo(video.video_id, video.title, video.author); + } + }, + }; +} diff --git a/src/main/player/index.ts b/src/main/player/index.ts index a0a6399..8b4baa8 100644 --- a/src/main/player/index.ts +++ b/src/main/player/index.ts @@ -1,12 +1,13 @@ import { ipcMain } from "electron"; import { MainWindowManager } from "../windows"; -import { discordClient } from "../discord"; +import { discordClient } from "../intergrations/discord"; import { factory } from "electron-json-config"; +import { musicDataType, playerState, videoDataType } from "../utils/types"; -let musicData: null | any = null; -let videoData: null | any = null; -let videoState = 0; -let volume = 0; +let musicData: musicDataType | null = null; +let videoData: videoDataType | null = null; +let videoState: playerState = playerState.Unstarted; +let volume: number = 0; const store = factory(); @@ -17,9 +18,13 @@ export function initPlayerEvents(mainWindowManager: MainWindowManager) { musicData = data; mainWindowManager.window.webContents.send("video-data-changed", data); if (data !== null && store.get("discord-rpc") === true) { - discord.setMusic(data.title, data.author, data.thumbnail.thumbnails[2]); + discord.setMusic( + data.title, + data.author, + data.thumbnail.thumbnails[2].url, + ); } else { - discord.clearActivity(); + discord.clear(); } }); @@ -28,7 +33,7 @@ export function initPlayerEvents(mainWindowManager: MainWindowManager) { mainWindowManager.window.webContents.send("volume-changed", v); }); - ipcMain.on("ytmView:videoStateChanged", (_, data) => { + ipcMain.on("ytmView:videoStateChanged", (_, data: playerState) => { videoState = data; console.log(data); mainWindowManager.window.webContents.send("video-state-changed", data); @@ -47,28 +52,42 @@ export function initPlayerEvents(mainWindowManager: MainWindowManager) { return volume; }); - ipcMain.on("ytmView:videoProgressChanged", (_, data) => { + ipcMain.on("ytmView:videoProgressChanged", (_, data: number) => { mainWindowManager.window.webContents.send("video-progress-changed", data); }); - ipcMain.on("ytView:videoDataChanged", (_, data) => { + ipcMain.on("ytView:videoDataChanged", (_, data: videoDataType) => { + console.log(data); videoData = data; - if (data!==null && store.get("discord-rpc")===true) { + if (data !== null && store.get("discord-rpc") === true) { discord.setVideo(data.video_id, data.title, data.author); } else { - discord.clearActivity(); + discord.clear(); } }); } -export function getMusicData() { +function getMusicData(): { + data: musicDataType | null; + state: playerState; +} { return { data: musicData, - state: videoState - } + state: videoState, + }; } - -export function getVideoData() { +function getVideoData(): videoDataType | null { return videoData; -} \ No newline at end of file +} + +export function playerManager() { + return { + getData: () => { + return { + music: getMusicData(), + video: getVideoData(), + }; + }, + }; +} diff --git a/src/main/tabs/index.ts b/src/main/tabs/index.ts index 13fcc5c..6977bdd 100644 --- a/src/main/tabs/index.ts +++ b/src/main/tabs/index.ts @@ -1,347 +1,412 @@ -import { ElectronBlocker } from '@cliqz/adblocker-electron'; import { - BrowserWindow, - ipcMain, - Menu, - shell, - WebContentsView, - WindowOpenHandlerResponse -} from 'electron'; -import { factory } from 'electron-json-config'; -import { readFileSync, writeFileSync } from 'fs'; -import { join } from 'path'; -import { sleep } from '../utils'; - -export async function createBlocker() { - const blocker = await ElectronBlocker.fromLists( - fetch, - [''], - { - enableCompression: true, - enableInMemoryCache: true - }, - { - path: 'engine.bin', - read: async (...args) => readFileSync(...args), - write: async (...args) => writeFileSync(...args) - } - ); - return blocker; -} + BrowserWindow, + ipcMain, + Menu, + shell, + WebContentsView, + WindowOpenHandlerResponse, +} from "electron"; +import { factory } from "electron-json-config"; +import { join } from "path"; +import { sleep } from "../utils"; +import { loadCss } from "../utils/customcss"; function windowOpenHandler(details): WindowOpenHandlerResponse { - shell.openExternal(details.url); - return { - action: 'deny' - }; + shell.openExternal(details.url); + return { + action: "deny", + }; +} + +async function updateCustomCss( + key: string, + oldCssKey: string | null, + view: WebContentsView, +) { + let ytCss = JSON.parse(store.get(key, "")!); + return new Promise(async (resolve) => { + if (ytCss.enabled===true) { + if (ytCss.type === "url") { + await loadCss(ytCss.url).then(async (css) => { + resolve(await view.webContents.insertCSS(css)); + }); + } else { + resolve(await view.webContents.insertCSS(ytCss.css)); + } + } else { + resolve(null); + } + }); } const store = factory(); let currentTab: number | null = 0; -let isFullscreen = false; +let isFullscreen: boolean = false; export async function createTabManager(mainWindow: BrowserWindow) { - function bounds() { - return { - x: 0, - y: isFullscreen ? 0 : 40, - width: mainWindow.getBounds().width, - height: mainWindow.getBounds().height - (isFullscreen ? 0 : 40) - }; - } - - function createMusicTab() { - const view = new WebContentsView({ - webPreferences: { - transparent: true, - preload: join(__dirname, '../preload/music.mjs'), - sandbox: false, - contextIsolation: true - } - }); - view.webContents.loadURL('https://music.youtube.com'); - view.setVisible(false); - mainWindow.contentView.addChildView(view); - mainWindow.on('resize', () => { - view.setBounds(bounds()); - }); - view.setBounds(bounds()); - - view.webContents.setWindowOpenHandler(windowOpenHandler); - - ipcMain.on('music-remote', (_, command, value) => { - view.webContents.send('remoteControl:execute', command, value); - }); - - let loaded = false; - ipcMain.once('ytmView:loaded', async () => { - if (!loaded) { - await sleep(500); - view.setVisible(true); - loaded = true; - } - updateTabs(); - }); - - let id = 0; - view.webContents.on('did-navigate', () => updateTabs()); - view.webContents.on('did-navigate-in-page', () => updateTabs()); - - // blocker.enableBlockingInSession(view.webContents.session); - - return { - view, - unset: () => { - mainWindow.contentView.removeChildView(view); - }, - set: () => { - mainWindow.contentView.addChildView(view); - }, - data: () => { - return { - title: view.webContents.getTitle(), - url: view.webContents.getURL(), - type: 'music', - id - }; - } - }; - } - - function createStudioTab() { - const view = new WebContentsView({ - webPreferences: { - // preload: join(__dirname, '../preload/music.js'), - sandbox: false, - contextIsolation: true - } - }); - - mainWindow.on('resize', () => { - view.setBounds(bounds()); - }); - view.setBounds(bounds()); - view.webContents.loadURL('https://studio.youtube.com'); - view.webContents.setWindowOpenHandler(windowOpenHandler); - - let id = 1; - - let loaded = false; - view.webContents.on('did-finish-load', () => { - if (!loaded) { - view.setVisible(true); - loaded = false; - } - updateTabs(); - }); - view.webContents.on('did-navigate', () => updateTabs()); - view.webContents.on('did-navigate-in-page', () => updateTabs()); - - // blocker.enableBlockingInSession(view.webContents.session); - - return { - view, - unset: () => { - mainWindow.contentView.removeChildView(view); - }, - set: () => { - mainWindow.contentView.addChildView(view); - }, - data: () => { - return { - title: view.webContents.getTitle(), - url: view.webContents.getURL(), - type: 'studio', - id - }; - } - }; - } - - function createTab() { - const view = new WebContentsView({ - webPreferences: { - transparent: true, - preload: join(__dirname, '../preload/yt.mjs'), - sandbox: false, - contextIsolation: true, - allowRunningInsecureContent: true, - webSecurity: false - } - }); - view.setVisible(false); - mainWindow.contentView.addChildView(view); - mainWindow.on('resize', () => { - view.setBounds(bounds()); - }); - view.setBounds(bounds()); - view.webContents.loadURL('https://www.youtube.com'); - view.webContents.setWindowOpenHandler(windowOpenHandler); - - let id = Date.now(); - - let loaded = false; - view.webContents.on('did-finish-load', () => { - if (!loaded) { - view.setVisible(true); - loaded = false; - } - updateTabs(); - }); - view.webContents.on('did-navigate', () => updateTabs()); - view.webContents.on('did-navigate-in-page', () => { - updateTabs(); - view.webContents.send('navigate'); - }); - - view.webContents.on('enter-html-full-screen', () => { - isFullscreen = true; - view.setBounds(bounds()); - }); - view.webContents.on('leave-html-full-screen', () => { - isFullscreen = false; - view.setBounds(bounds()); - }); - - view.webContents.on('context-menu', (e) => { - const menu = Menu.buildFromTemplate([ - { - label: "Enter Picture in Picture", - click: () => { - view.webContents.send("picture-in-picture"); - } - } - ]) - e.preventDefault(); - menu.popup(); - }) - - return { - view, - unset: () => { - mainWindow.contentView.removeChildView(view); - }, - set: () => { - mainWindow.contentView.addChildView(view); - }, - data: () => { - return { - title: view.webContents.getTitle(), - url: view.webContents.getURL(), - type: 'yt', - id - }; - } - }; - } - - let tabs = [createMusicTab()]; - - if (store.get('studio-tab')) { - tabs.push(createStudioTab()); - } - - function switchTab(i: number) { - if (currentTab!==null) { - tabs[currentTab].unset(); - } - tabs[i].set(); - - currentTab = i; - } - - function updateTabs() { - mainWindow.webContents.send('update-tabs', { tabs: tabs.map((o) => o.data()), currentTab }); - } - - ipcMain.on('switch-tab', (_, i) => { - switchTab(i); - updateTabs(); - }); - - ipcMain.on('new-tab', () => { - tabs.push(createTab()); - if (currentTab !== null) tabs[currentTab].unset(); - switchTab(tabs.length - 1); - updateTabs(); - }); - - ipcMain.on('close-tab', (_, i) => { - if (tabs.length < 2) { - return; - } else { - tabs[i].unset(); - tabs[i].view.webContents.close(); - tabs.splice(i, 1); - if (tabs[i] == null) { - currentTab = null; - switchTab(i - 1); - } else { - currentTab = null; - switchTab(i); - } - } - updateTabs(); - }); - - ipcMain.on('page-action', (_, i) => { - if (currentTab !== null) { - switch (i) { - case 'back': - tabs[currentTab].view.webContents.goBack(); - break; - case 'forward': - tabs[currentTab].view.webContents.goForward(); - break; - case 'reload': - tabs[currentTab].view.webContents.reload(); - break; - } - } - }); - - ipcMain.on('open-settings', () => { - if(currentTab!==null) { - tabs[currentTab].view.setVisible(false); - tabs[currentTab].unset(); - } - }); - - ipcMain.on('close-settings', () => { - if(currentTab!==null) { - tabs[currentTab].view.setVisible(true); - tabs[currentTab].set(); - } - }); - - ipcMain.on('open-miniplayer', () => { - if(currentTab!==null) { - tabs[currentTab].view.setVisible(false) - tabs[currentTab].unset(); - } - }); - - ipcMain.on('close-miniplayer', () => { - if(currentTab!==null) { - tabs[currentTab].view.setVisible(true) - tabs[currentTab].set(); - } - }); - - let loaded = false; - - mainWindow.on('ready-to-show', async () => { - if (!loaded) { - updateTabs(); - switchTab(0); - loaded = true; - } - }); - - return { - getTabs: ()=>{ - return tabs - } - } + function bounds() { + return { + x: 0, + y: isFullscreen ? 0 : 40, + width: mainWindow.getBounds().width, + height: mainWindow.getBounds().height - (isFullscreen ? 0 : 40), + }; + } + + function createMusicTab() { + const view = new WebContentsView({ + webPreferences: { + transparent: true, + preload: join(__dirname, "../preload/music.mjs"), + sandbox: false, + contextIsolation: true, + }, + }); + view.webContents.loadURL("https://music.youtube.com"); + view.setVisible(false); + mainWindow.contentView.addChildView(view); + mainWindow.on("resize", () => { + view.setBounds(bounds()); + }); + view.setBounds(bounds()); + + view.webContents.setWindowOpenHandler(windowOpenHandler); + + ipcMain.on("music-remote", (_, command, value) => { + view.webContents.send("remoteControl:execute", command, value); + }); + + view.webContents.toggleDevTools(); + + let loaded = false; + let oldCssKey: string | null = null; + + ipcMain.once("ytmView:loaded", async () => { + if (!loaded) { + await sleep(500); + view.setVisible(true); + loaded = true; + } + updateTabs(); + }); + + view.webContents.on("dom-ready", async () => { + await updateCustomCss("music-css", oldCssKey, view).then((key) => { + oldCssKey = key; + }); + }); + + view.webContents.toggleDevTools(); + + // async function updateCustomCss() { + // let ytCss = JSON.parse(store.get("music-css", "")!); + // if (ytCss.enabled) { + // if (ytCss.type === "url") { + // await loadCss(ytCss.url).then(async (css: string) => { + // console.log("oldCssKey", oldCssKey); + // if (oldCssKey !== null) { + // await view.webContents.removeInsertedCSS(oldCssKey); + // } + // oldCssKey = await view.webContents.insertCSS(css); + // }); + // } else if (ytCss.type === "css") { + // if (oldCssKey !== null) { + // await view.webContents.removeInsertedCSS(oldCssKey); + // } + // oldCssKey = await view.webContents.insertCSS(ytCss.css); + // } + // } + // } + + let id = 0; + view.webContents.on("did-navigate", () => updateTabs()); + view.webContents.on("did-navigate-in-page", () => updateTabs()); + + return { + view, + unset: () => { + mainWindow.contentView.removeChildView(view); + }, + set: () => { + mainWindow.contentView.addChildView(view); + }, + data: () => { + return { + title: view.webContents.getTitle(), + url: view.webContents.getURL(), + type: "music", + id, + }; + }, + updateCss: async () => { + if (oldCssKey !== null) { + await view.webContents.removeInsertedCSS(oldCssKey).then(()=>console.log("remove")); + } + await updateCustomCss("music-css", oldCssKey, view).then((key) => { + oldCssKey = key; + + }); + }, + }; + } + + function createStudioTab() { + const view = new WebContentsView({ + webPreferences: { + // preload: join(__dirname, '../preload/music.js'), + sandbox: false, + contextIsolation: true, + }, + }); + + mainWindow.on("resize", () => { + view.setBounds(bounds()); + }); + view.setBounds(bounds()); + view.webContents.loadURL("https://studio.youtube.com"); + view.webContents.setWindowOpenHandler(windowOpenHandler); + + let id = 1; + + let loaded = false; + view.webContents.on("did-finish-load", () => { + if (!loaded) { + view.setVisible(true); + loaded = false; + } + updateTabs(); + }); + view.webContents.on("did-navigate", () => updateTabs()); + view.webContents.on("did-navigate-in-page", () => updateTabs()); + + return { + view, + unset: () => { + mainWindow.contentView.removeChildView(view); + }, + set: () => { + mainWindow.contentView.addChildView(view); + }, + data: () => { + return { + title: view.webContents.getTitle(), + url: view.webContents.getURL(), + type: "studio", + id, + }; + }, + updateCss: async () => {}, + }; + } + + function createTab() { + const view = new WebContentsView({ + webPreferences: { + transparent: true, + preload: join(__dirname, "../preload/yt.mjs"), + sandbox: false, + contextIsolation: true, + allowRunningInsecureContent: true, + webSecurity: false, + }, + }); + view.setVisible(false); + mainWindow.contentView.addChildView(view); + mainWindow.on("resize", () => { + view.setBounds(bounds()); + }); + view.setBounds(bounds()); + view.webContents.loadURL("https://www.youtube.com"); + view.webContents.setWindowOpenHandler(windowOpenHandler); + + let id = Date.now(); + + let loaded = false; + view.webContents.on("did-finish-load", () => { + if (!loaded) { + view.setVisible(true); + loaded = false; + } + updateTabs(); + }); + view.webContents.on("did-navigate", () => updateTabs()); + view.webContents.on("did-navigate-in-page", () => { + updateTabs(); + view.webContents.send("navigate"); + }); + + view.webContents.on("enter-html-full-screen", () => { + isFullscreen = true; + view.setBounds(bounds()); + }); + view.webContents.on("leave-html-full-screen", () => { + isFullscreen = false; + view.setBounds(bounds()); + }); + + view.webContents.on("context-menu", (e) => { + const menu = Menu.buildFromTemplate([ + { + label: "Enter Picture in Picture", + click: () => { + view.webContents.send("picture-in-picture"); + }, + }, + ]); + e.preventDefault(); + menu.popup(); + }); + + let oldCssKey: string | null = null; + + view.webContents.on("dom-ready", async () => { + await updateCustomCss("yt-css", oldCssKey, view).then((key) => { + oldCssKey = key; + }); + }); + + return { + view, + unset: () => { + mainWindow.contentView.removeChildView(view); + }, + set: () => { + mainWindow.contentView.addChildView(view); + }, + data: () => { + return { + title: view.webContents.getTitle(), + url: view.webContents.getURL(), + type: "yt", + id, + }; + }, + updateCss: async () => { + if (oldCssKey !== null) { + await view.webContents.removeInsertedCSS(oldCssKey); + } + await updateCustomCss("yt-css", oldCssKey, view).then((key) => { + oldCssKey = key; + }); + }, + }; + } + + let tabs = [createMusicTab()]; + + if (store.get("studio-tab")) { + tabs.push(createStudioTab()); + } + + function switchTab(i: number) { + if (currentTab !== null) { + tabs[currentTab].unset(); + } + tabs[i].set(); + + currentTab = i; + } + + function updateTabs() { + mainWindow.webContents.send("update-tabs", { + tabs: tabs.map((o) => o.data()), + currentTab, + }); + } + + ipcMain.on("switch-tab", (_, i) => { + switchTab(i); + updateTabs(); + }); + + ipcMain.on("new-tab", () => { + tabs.push(createTab()); + if (currentTab !== null) tabs[currentTab].unset(); + switchTab(tabs.length - 1); + updateTabs(); + }); + + ipcMain.on("close-tab", (_, i) => { + if (tabs.length < 2) { + return; + } else { + tabs[i].unset(); + tabs[i].view.webContents.close(); + tabs.splice(i, 1); + if (tabs[i] == null) { + currentTab = null; + switchTab(i - 1); + } else { + currentTab = null; + switchTab(i); + } + } + updateTabs(); + }); + + ipcMain.on("page-action", (_, i) => { + if (currentTab !== null) { + switch (i) { + case "back": + tabs[currentTab].view.webContents.goBack(); + break; + case "forward": + tabs[currentTab].view.webContents.goForward(); + break; + case "reload": + tabs[currentTab].view.webContents.reload(); + break; + } + } + }); + + ipcMain.on("open-settings", () => { + if (currentTab !== null) { + tabs[currentTab].view.setVisible(false); + tabs[currentTab].unset(); + } + }); + + ipcMain.on("close-settings", () => { + if (currentTab !== null) { + tabs[currentTab].view.setVisible(true); + tabs[currentTab].set(); + } + }); + + ipcMain.on("open-miniplayer", () => { + if (currentTab !== null) { + tabs[currentTab].view.setVisible(false); + tabs[currentTab].unset(); + } + }); + + ipcMain.on("close-miniplayer", () => { + if (currentTab !== null) { + tabs[currentTab].view.setVisible(true); + tabs[currentTab].set(); + } + }); + + let loaded = false; + + mainWindow.on("ready-to-show", async () => { + if (!loaded) { + updateTabs(); + switchTab(0); + loaded = true; + } + }); + + return { + getTabs: () => { + return tabs; + }, + updateCustomCss: () => { + tabs.forEach((tab) => { + tab.updateCss(); + }); + }, + }; } diff --git a/src/main/utils/customcss.ts b/src/main/utils/customcss.ts new file mode 100644 index 0000000..5017fcd --- /dev/null +++ b/src/main/utils/customcss.ts @@ -0,0 +1,72 @@ +import { app } from "electron"; +import https from "https"; // or 'https' for https:// URLs +import { join } from "path"; +import fs from "fs"; + +export function downloadCss(url: string) { + return new Promise((resolve) => { + const file = fs.createWriteStream( + join(app.getPath("appData"), "music.css"), + ); + https.get(url, function (response) { + response.pipe(file); + + // after download completed close filestream + file.on("finish", () => { + file.close(); + resolve(true); + }); + }); + }); +} + +// export function loadCss(): any { +// fs.readFile(join(app.getPath("appData"), "music.css"), "utf-8", (err, data) => { +// if (!err && data) { +// console.log(data); +// return data.toString(); +// } else { +// return ""; +// } +// }); +// } + +// export const loadCss = new Promise((resolve) => { +// fs.readFile( +// join(app.getPath("appData"), "music.css"), +// "utf-8", +// (err, data) => { +// if (!err && data) { +// resolve(data); +// } else { +// resolve(""); +// } +// }, +// ); +// }); + +export function loadCss(url:string) { + return new Promise((resolve) => { + if(url==="") { + resolve(""); + return; + } + https.get(url, res => { + let data = '' + const headerDate = res.headers && res.headers.date ? res.headers.date : 'no response date'; + + // @ts-ignore + res.on('data', (chunk:any) => { + data += chunk; + }); + + res.on('end', () => { + resolve(data) + }); + }).on('error', err => { + console.log('Error: ', err.message); + }); + } +) + +} diff --git a/src/main/utils/index.ts b/src/main/utils/index.ts index 6f14a7d..5849c34 100644 --- a/src/main/utils/index.ts +++ b/src/main/utils/index.ts @@ -1,12 +1,12 @@ -import { BrowserWindow } from "electron" +import { BrowserWindow } from "electron"; export const debounce = (fn: Function, time = 300) => { - let timeoutId: ReturnType + let timeoutId: ReturnType; return function (this: any, ...args: any[]) { - clearTimeout(timeoutId) - timeoutId = setTimeout(() => fn.apply(this, args), time) - } -} + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), time); + }; +}; export function sleep(ms) { return new Promise((resolve) => { @@ -15,11 +15,13 @@ export function sleep(ms) { } export function loadVite(win: BrowserWindow, path: string): void { - console.log(import.meta.env.MAIN_VITE_ELECTRON_RENDERER_URL + path); - win.loadURL(import.meta.env.MAIN_VITE_ELECTRON_RENDERER_URL + path).catch((e) => { - console.log('Error loading URL, retrying', e); - setTimeout(() => { - loadVite(win, path); - }, 200); - }); -} \ No newline at end of file + console.log(import.meta.env.MAIN_VITE_ELECTRON_RENDERER_URL + path); + win + .loadURL(import.meta.env.MAIN_VITE_ELECTRON_RENDERER_URL + path) + .catch((e) => { + console.log("Error loading URL, retrying", e); + setTimeout(() => { + loadVite(win, path); + }, 200); + }); +} diff --git a/src/main/utils/types.ts b/src/main/utils/types.ts new file mode 100644 index 0000000..08a3983 --- /dev/null +++ b/src/main/utils/types.ts @@ -0,0 +1,62 @@ +export enum playerState { + Unstarted = -1, + Ended = 0, + Playing = 1, + Paused = 2, + Buffering = 3, + VideoCued = 5, +} + +export type musicDataType = { + videoId: string; + title: string; + author: string; + channelId: string; + isCrawlable: boolean; + isLiveContent: boolean; + isOwnerViewing: boolean; + isUnpluggedCorpus: boolean; + lengthSeconds: string; + musicVideoType: string; + thumbnail: { + thumbnails: { + height: number; + width: number; + url: string; + }[]; + }; + viewCount: string; + allowRatings: boolean; +}; + +export type videoDataType = { + video_id: string; + author: string; + title: string; + isPlayable: boolean; + errorCode: any; + video_quality: string; + video_quality_features: Array; + backgroundable: boolean; + eventId: string; + cpn: string; + isLive: boolean; + isWindowedLive: boolean; + isManifestless: boolean; + allowLiveDvr: boolean; + isListed: boolean; + isMultiChannelAudio: boolean; + hasProgressBarBoundaries: boolean; + isPremiere: boolean; + itct: string; + playerResponseCpn: string; + progressBarStartPositionUtcTimeMillis: any; + progressBarEndPositionUtcTimeMillis: any; + ypcOriginalItct: any; + ypcPreview: any; + paidContentOverlayText: any; + paidContentOverlayDurationMs: number; + transitionEndpointAtEndOfStream: any; + currentTime: number; + duration: number; +}; diff --git a/src/main/windows/index.ts b/src/main/windows/index.ts index 9a2dd4c..12efe3c 100644 --- a/src/main/windows/index.ts +++ b/src/main/windows/index.ts @@ -3,7 +3,8 @@ import { app, BrowserWindow, ipcMain, nativeTheme, shell } from "electron"; import { join } from "path"; import { sleep } from "../utils"; import { factory } from "electron-json-config"; -import { getMusicData } from "../player"; +import { playerState } from "../utils/types"; +import { playerManager } from "../player"; const store = factory(); @@ -46,17 +47,14 @@ switch (store.get("theme")) { export type MainWindowManager = { window: BrowserWindow; applyTheme: Function; - miniPlayerOpen: Function -} - + miniPlayerOpen: Function; +}; app.on("before-quit", () => { isAppQuitting = true; }); - export function createMainWindowManager() { - const mainWindow = new BrowserWindow({ minWidth: 800, minHeight: 450, @@ -110,18 +108,21 @@ export function createMainWindowManager() { ipcMain.on("close-miniplayer", async () => { miniPlayerOpen = true; mainWindow.setMaximumSize(100000, 100000); - mainWindow.setPosition(oldPos[0], oldPos[1], true); - mainWindow.setSize(1200, 700, true); + mainWindow.setPosition(oldPos[0], oldPos[1], false); + mainWindow.setSize(1200, 700, false); mainWindow.setResizable(true); mainWindow.setMaximizable(true); mainWindow.setFullScreenable(true); mainWindow.setAlwaysOnTop(false); - await sleep(500); mainWindow.setMinimumSize(800, 450); }); mainWindow.on("close", (e) => { - if (!isAppQuitting && process.platform === "darwin" && getMusicData().state === 1) { + if ( + !isAppQuitting && + process.platform === "darwin" && + playerManager().getData().music.state === playerState.Playing + ) { e.preventDefault(); mainWindow.hide(); } @@ -134,12 +135,12 @@ export function createMainWindowManager() { return { window: mainWindow, applyTheme: () => { - updateTheme(mainWindow) + updateTheme(mainWindow); }, miniPlayerOpen: () => { return miniPlayerOpen; - } - } + }, + }; } function handleWindowAction(event, message) { diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index d08c142..072b882 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -1,10 +1,9 @@ -import { ElectronAPI } from '@electron-toolkit/preload' -import { api } from './' +import { ElectronAPI } from "@electron-toolkit/preload"; +import { api } from "./"; declare global { interface Window { - electron: ElectronAPI - api: typeof api + electron: ElectronAPI; + api: typeof api; } } - diff --git a/src/preload/index.ts b/src/preload/index.ts index f509f56..c2eda0c 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,104 +1,105 @@ -import { contextBridge, ipcRenderer } from 'electron' +import { contextBridge, ipcRenderer } from "electron"; +import { type musicData } from "./types"; // Custom APIs for renderer export const api = { openSettings: () => { - ipcRenderer.send('open-settings') + ipcRenderer.send("open-settings"); }, closeSettings: () => { - ipcRenderer.send('close-settings') + ipcRenderer.send("close-settings"); }, onUpdateTabs: (listener: Function) => { - ipcRenderer.on('update-tabs', (_, data) => { - listener(data) - }) + ipcRenderer.on("update-tabs", (_, data) => { + listener(data); + }); }, newTab: () => { - ipcRenderer.send('new-tab') + ipcRenderer.send("new-tab"); }, closeTab: (i: number) => { - ipcRenderer.send('close-tab', i) + ipcRenderer.send("close-tab", i); }, switchTab: (i: number) => { - ipcRenderer.send('switch-tab', i) + ipcRenderer.send("switch-tab", i); }, - pageAction: (action: 'back' | 'forward' | 'reload') => { - ipcRenderer.send('page-action', action) + pageAction: (action: "back" | "forward" | "reload") => { + ipcRenderer.send("page-action", action); }, - windowAction: (action: 'close' | 'maximize' | 'minimize') => { - ipcRenderer.send('window-action', action) + windowAction: (action: "close" | "maximize" | "minimize") => { + ipcRenderer.send("window-action", action); }, getConfig: (key: string) => { - return ipcRenderer.invoke('get-config', key) + return ipcRenderer.invoke("get-config", key); }, setConfig: (key: string, value: string) => { - ipcRenderer.send('set-config', { key, value }) + ipcRenderer.send("set-config", { key, value }); }, onUpdateTheme: (listener: Function) => { - ipcRenderer.on('update-theme', (_, theme) => { - listener(theme) - }) + ipcRenderer.on("update-theme", (_, theme) => { + listener(theme); + }); }, getTheme: () => { - return ipcRenderer.invoke('get-theme') + return ipcRenderer.invoke("get-theme"); }, openMiniplayer: () => { - ipcRenderer.send('open-miniplayer') + ipcRenderer.send("open-miniplayer"); }, closeMiniplayer: () => { - ipcRenderer.send('close-miniplayer') + ipcRenderer.send("close-miniplayer"); }, - onVideoDataChange: (listener:Function) => { - ipcRenderer.on('video-data-changed', (_, data)=>{ + onVideoDataChange: (listener: Function) => { + ipcRenderer.on("video-data-changed", (_, data) => { listener(data); - }) + }); }, - musicRemote: (command:string,value:number) => { + musicRemote: (command: string, value: number) => { ipcRenderer.send("music-remote", command, value); }, getVideoData: () => { - return ipcRenderer.invoke('get-video-data'); + return ipcRenderer.invoke("get-video-data"); }, - onVideoProgressChange: (listener:Function) => { - ipcRenderer.on('video-progress-changed', (_, data)=>{ + onVideoProgressChange: (listener: Function) => { + ipcRenderer.on("video-progress-changed", (_, data) => { listener(data); - }) + }); }, - onVideoStateChange: (listener:Function) => { - ipcRenderer.on('video-state-changed', (_, data)=>{ + onVideoStateChange: (listener: Function) => { + ipcRenderer.on("video-state-changed", (_, data) => { listener(data); - }) + }); }, - onVolumeChange: (listener:Function) => { - ipcRenderer.on('volume-changed', (_, data)=>{ + onVolumeChange: (listener: Function) => { + ipcRenderer.on("volume-changed", (_, data) => { listener(data); - }) + }); }, getVideoState: async () => { - return await ipcRenderer.invoke('get-video-state'); + return await ipcRenderer.invoke("get-video-state"); }, getVolume: async () => { - return await ipcRenderer.invoke('get-volume'); + return await ipcRenderer.invoke("get-volume"); }, - onPushSPA: (listener) =>{ - ipcRenderer.on('push-spa', (_, path)=>{ + onPushSPA: (listener) => { + ipcRenderer.on("push-spa", (_, path) => { console.log(path); - listener(path) - }) + listener(path); + }); }, - platform: process.platform -} + platform: process.platform, +}; // Use `contextBridge` APIs to expose Electron APIs to // renderer only if context isolation is enabled, otherwise // just add to the DOM global. if (process.contextIsolated) { try { - contextBridge.exposeInMainWorld('api', api) + contextBridge.exposeInMainWorld("api", api); } catch (error) { - console.error(error) + console.error(error); } } else { // @ts-ignore (define in dts) - window.electron = api + window.electron = api; } diff --git a/src/preload/music.ts b/src/preload/music.ts index 495aafc..f76d350 100644 --- a/src/preload/music.ts +++ b/src/preload/music.ts @@ -12,7 +12,7 @@ /* @vite-ignore */ -import { contextBridge, ipcRenderer, webFrame } from 'electron'; +import { contextBridge, ipcRenderer, webFrame } from "electron"; const playerBarControlsScript = ` (function() { @@ -633,66 +633,87 @@ const toggleDislikeScript = ` `; class Store { - public set(key: string, value?: unknown) { - return ipcRenderer.send('settings:set', key, value); - } - - public async get(_key: keyof TSchema) { - // return await ipcRenderer.invoke("settings:get", key); - return null; - } - - public reset(key: keyof TSchema) { - return ipcRenderer.send('settings:reset', key); - } - - public onDidAnyChange(callback: (newState: TSchema, oldState: TSchema) => void) { - return ipcRenderer.on('settings:stateChanged', (_event, newState, oldState) => { - callback(newState, oldState); - }); - } + public set(key: string, value?: unknown) { + return ipcRenderer.send("settings:set", key, value); + } + + public async get(_key: keyof TSchema) { + // return await ipcRenderer.invoke("settings:get", key); + return null; + } + + public reset(key: keyof TSchema) { + return ipcRenderer.send("settings:reset", key); + } + + public onDidAnyChange( + callback: (newState: TSchema, oldState: TSchema) => void, + ) { + return ipcRenderer.on( + "settings:stateChanged", + (_event, newState, oldState) => { + callback(newState, oldState); + }, + ); + } } const store = new Store(); -contextBridge.exposeInMainWorld('ytmd', { - sendVideoProgress: (volume: number) => ipcRenderer.send('ytmView:videoProgressChanged', volume), - sendVideoState: (state: number) => ipcRenderer.send('ytmView:videoStateChanged', state), - sendVideoData: ( - videoDetails: unknown, - playlistId: string, - album: { id: string; text: string }, - likeStatus: unknown - ) => ipcRenderer.send('ytmView:videoDataChanged', videoDetails, playlistId, album, likeStatus), - sendStoreUpdate: ( - queueState: unknown, - likeStatus: string, - volume: number, - muted: boolean, - adPlaying: boolean - ) => - ipcRenderer.send('ytmView:storeStateChanged', queueState, likeStatus, volume, muted, adPlaying), - sendCreatePlaylistObservation: (playlist: unknown) => - ipcRenderer.send('ytmView:createPlaylistObserved', playlist), - sendDeletePlaylistObservation: (playlistId: string) => - ipcRenderer.send('ytmView:deletePlaylistObserved', playlistId) +contextBridge.exposeInMainWorld("ytmd", { + sendVideoProgress: (volume: number) => + ipcRenderer.send("ytmView:videoProgressChanged", volume), + sendVideoState: (state: number) => + ipcRenderer.send("ytmView:videoStateChanged", state), + sendVideoData: ( + videoDetails: unknown, + playlistId: string, + album: { id: string; text: string }, + likeStatus: unknown, + ) => + ipcRenderer.send( + "ytmView:videoDataChanged", + videoDetails, + playlistId, + album, + likeStatus, + ), + sendStoreUpdate: ( + queueState: unknown, + likeStatus: string, + volume: number, + muted: boolean, + adPlaying: boolean, + ) => + ipcRenderer.send( + "ytmView:storeStateChanged", + queueState, + likeStatus, + volume, + muted, + adPlaying, + ), + sendCreatePlaylistObservation: (playlist: unknown) => + ipcRenderer.send("ytmView:createPlaylistObserved", playlist), + sendDeletePlaylistObservation: (playlistId: string) => + ipcRenderer.send("ytmView:deletePlaylistObserved", playlistId), }); function createStyleSheet() { - const css = document.createElement('style'); - css.appendChild( - document.createTextNode(` - `) - ); - document.head.appendChild(css); + const css = document.createElement("style"); + css.appendChild( + document.createTextNode(` + `), + ); + document.head.appendChild(css); } function createMaterialSymbolsLink() { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = - 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,100,0,0'; - return link; + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = + "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,100,0,0"; + return link; } // function createNavigationMenuArrows() { @@ -752,51 +773,52 @@ function createMaterialSymbolsLink() { // } function createKeyboardNavigation() { - const keyboardNavigation = document.createElement('div'); - keyboardNavigation.tabIndex = 32767; - keyboardNavigation.onfocus = () => { - keyboardNavigation.blur(); - ipcRenderer.send('ytmView:switchFocus', 'main'); - }; - document.body.appendChild(keyboardNavigation); + const keyboardNavigation = document.createElement("div"); + keyboardNavigation.tabIndex = 32767; + keyboardNavigation.onfocus = () => { + keyboardNavigation.blur(); + ipcRenderer.send("ytmView:switchFocus", "main"); + }; + document.body.appendChild(keyboardNavigation); } async function createAdditionalPlayerBarControls() { - (await webFrame.executeJavaScript(playerBarControlsScript))(); + (await webFrame.executeJavaScript(playerBarControlsScript))(); } async function hideChromecastButton() { - ( - await webFrame.executeJavaScript(` + ( + await webFrame.executeJavaScript(` (function() { window.__YTMD_HOOK__.ytmStore.dispatch({ type: 'SET_CAST_AVAILABLE', payload: false }); }) `) - )(); + )(); } async function hookPlayerApiEvents() { - (await webFrame.executeJavaScript(hookPlayerApiEventsScript))(); + (await webFrame.executeJavaScript(hookPlayerApiEventsScript))(); } function overrideHistoryButtonDisplay() { - // @ts-expect-error Style is reported as readonly but this still works - document.querySelector('#history-link tp-yt-paper-icon-button').style = - 'display: inline-block !important;'; + // @ts-expect-error Style is reported as readonly but this still works + document.querySelector( + "#history-link tp-yt-paper-icon-button", + ).style = "display: inline-block !important;"; } function getYTMTextRun(runs: { text: string }[]) { - let final = ''; - for (const run of runs) { - final += run.text; - } - return final; + let final = ""; + for (const run of runs) { + final += run.text; + } + return final; } // This function helps hook YTM (async function () { - ( - await webFrame.executeJavaScript(` + ( + await webFrame.executeJavaScript(` (function() { let ytmdHookedObjects = []; @@ -822,24 +844,24 @@ function getYTMTextRun(runs: { text: string }[]) { window.__YTMD_HOOK_OBJS__ = ytmdHookedObjects; }) `) - )(); + )(); })(); -window.addEventListener('load', async () => { - if (window.location.hostname !== 'music.youtube.com') { - if ( - window.location.hostname === 'consent.youtube.com' || - window.location.hostname === 'accounts.google.com' - ) { - ipcRenderer.send('ytmView:loaded'); - } - return; - } - - await new Promise((resolve) => { - const interval = setInterval(async () => { - const hooked = ( - await webFrame.executeJavaScript(` +window.addEventListener("load", async () => { + if (window.location.hostname !== "music.youtube.com") { + if ( + window.location.hostname === "consent.youtube.com" || + window.location.hostname === "accounts.google.com" + ) { + ipcRenderer.send("ytmView:loaded"); + } + return; + } + + await new Promise((resolve) => { + const interval = setInterval(async () => { + const hooked = ( + await webFrame.executeJavaScript(` (function() { for (const hookedObj of window.__YTMD_HOOK_OBJS__) { if (hookedObj.is) { @@ -870,313 +892,318 @@ window.addEventListener('load', async () => { return false; }) `) - )(); - - if (hooked) { - clearInterval(interval); - resolve(); - } - }, 250); - }); - - let materialSymbolsLoaded = false; - - const materialSymbols = createMaterialSymbolsLink(); - materialSymbols.onload = () => { - materialSymbolsLoaded = true; - }; - document.head.appendChild(materialSymbols); - - await new Promise((resolve) => { - const interval = setInterval(async () => { - const playerApiReady: boolean = ( - await webFrame.executeJavaScript(` + )(); + + if (hooked) { + clearInterval(interval); + resolve(); + } + }, 250); + }); + + let materialSymbolsLoaded = false; + + const materialSymbols = createMaterialSymbolsLink(); + materialSymbols.onload = () => { + materialSymbolsLoaded = true; + }; + document.head.appendChild(materialSymbols); + + await new Promise((resolve) => { + const interval = setInterval(async () => { + const playerApiReady: boolean = ( + await webFrame.executeJavaScript(` (function() { return document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.isReady(); }) `) - )(); - - if (materialSymbolsLoaded && playerApiReady) { - clearInterval(interval); - resolve(); - } - }, 250); - }); - - createStyleSheet(); - // createNavigationMenuArrows(); - createKeyboardNavigation(); - await createAdditionalPlayerBarControls(); - await hideChromecastButton(); - await hookPlayerApiEvents(); - overrideHistoryButtonDisplay(); - - // const integrationScripts: { [integrationName: string]: { [scriptName: string]: string } } = await ipcRenderer.invoke("ytmView:getIntegrationScripts"); - - // @ts-ignore: Unreachable code error - const integrationScripts: { [integrationName: string]: { [scriptName: string]: string } } = null; - const state: any = await store.get('state'); - // const continueWhereYouLeftOff = (await store.get("playback")).continueWhereYouLeftOff; - const continueWhereYouLeftOff = false; - - if (continueWhereYouLeftOff) { - // The last page the user was on is already a page where it will be playing a song from (no point telling YTM to play it again) - if (!state?.lastUrl.startsWith('https://music.youtube.com/watch')) { - if (state.lastVideoId) { - // This height transition check is a hack to fix the `Start playback` hint from not being in the correct position https://github.com/ytmdesktop/ytmdesktop/issues/1159 - let heightTransitionCount = 0; - const transitionEnd = async (e: TransitionEvent) => { - if (e.target === document.querySelector('ytmusic-app-layout>ytmusic-player-bar')) { - if (e.propertyName === 'height') { - ( - await webFrame.executeJavaScript(` + )(); + + if (materialSymbolsLoaded && playerApiReady) { + clearInterval(interval); + resolve(); + } + }, 250); + }); + + createStyleSheet(); + // createNavigationMenuArrows(); + createKeyboardNavigation(); + await createAdditionalPlayerBarControls(); + await hideChromecastButton(); + await hookPlayerApiEvents(); + overrideHistoryButtonDisplay(); + + // const integrationScripts: { [integrationName: string]: { [scriptName: string]: string } } = await ipcRenderer.invoke("ytmView:getIntegrationScripts"); + + // @ts-ignore: Unreachable code error + const integrationScripts: { + [integrationName: string]: { [scriptName: string]: string }; + } = null; + const state: any = await store.get("state"); + // const continueWhereYouLeftOff = (await store.get("playback")).continueWhereYouLeftOff; + const continueWhereYouLeftOff = false; + + if (continueWhereYouLeftOff) { + // The last page the user was on is already a page where it will be playing a song from (no point telling YTM to play it again) + if (!state?.lastUrl.startsWith("https://music.youtube.com/watch")) { + if (state.lastVideoId) { + // This height transition check is a hack to fix the `Start playback` hint from not being in the correct position https://github.com/ytmdesktop/ytmdesktop/issues/1159 + let heightTransitionCount = 0; + const transitionEnd = async (e: TransitionEvent) => { + if ( + e.target === + document.querySelector("ytmusic-app-layout>ytmusic-player-bar") + ) { + if (e.propertyName === "height") { + ( + await webFrame.executeJavaScript(` (function() { document.querySelector("ytmusic-popup-container").refitPopups_(); }) `) - )(); - heightTransitionCount++; - if (heightTransitionCount >= 2) { - // @ts-ignore: Unreachable code error - document - .querySelector('ytmusic-app-layout>ytmusic-player-bar') + )(); + heightTransitionCount++; + if (heightTransitionCount >= 2) { + // @ts-ignore: Unreachable code error + document + .querySelector("ytmusic-app-layout>ytmusic-player-bar") // @ts-ignore: Unreachable code error - .removeEventListener('transitionend', transitionEnd); - } - } - } - }; - // @ts-ignore: Unreachable code error - document - .querySelector('ytmusic-app-layout>ytmusic-player-bar') + .removeEventListener("transitionend", transitionEnd); + } + } + } + }; + // @ts-ignore: Unreachable code error + document + .querySelector("ytmusic-app-layout>ytmusic-player-bar") // @ts-ignore: Unreachable code error - .addEventListener('transitionend', transitionEnd); - - document.dispatchEvent( - new CustomEvent('yt-navigate', { - detail: { - endpoint: { - watchEndpoint: { - videoId: state.lastVideoId, - playlistId: state.lastPlaylistId - } - } - } - }) - ); - } - } else { - ( - await webFrame.executeJavaScript(` + .addEventListener("transitionend", transitionEnd); + + document.dispatchEvent( + new CustomEvent("yt-navigate", { + detail: { + endpoint: { + watchEndpoint: { + videoId: state.lastVideoId, + playlistId: state.lastPlaylistId, + }, + }, + }, + }), + ); + } + } else { + ( + await webFrame.executeJavaScript(` (function() { window.ytmd.sendVideoData(document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.getPlayerResponse().videoDetails, document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.getPlaylistId()); }) `) - )(); - } - } - - // const alwaysShowVolumeSlider = (await store.get("appearance")).alwaysShowVolumeSlider; - const alwaysShowVolumeSlider = false; - - if (alwaysShowVolumeSlider && document) { - document - .querySelector('ytmusic-app-layout>ytmusic-player-bar #volume-slider') - ?.classList.add('ytmd-persist-volume-slider'); - } - - ipcRenderer.on('remoteControl:execute', async (_event, command, value) => { - switch (command) { - case 'playPause': { - ( - await webFrame.executeJavaScript(` + )(); + } + } + + // const alwaysShowVolumeSlider = (await store.get("appearance")).alwaysShowVolumeSlider; + const alwaysShowVolumeSlider = false; + + if (alwaysShowVolumeSlider && document) { + document + .querySelector("ytmusic-app-layout>ytmusic-player-bar #volume-slider") + ?.classList.add("ytmd-persist-volume-slider"); + } + + ipcRenderer.on("remoteControl:execute", async (_event, command, value) => { + switch (command) { + case "playPause": { + ( + await webFrame.executeJavaScript(` (function() { document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playing ? document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.pauseVideo() : document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.playVideo(); }) `) - )(); - break; - } + )(); + break; + } - case 'play': { - ( - await webFrame.executeJavaScript(` + case "play": { + ( + await webFrame.executeJavaScript(` (function() { document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.playVideo(); }) `) - )(); - break; - } + )(); + break; + } - case 'pause': { - ( - await webFrame.executeJavaScript(` + case "pause": { + ( + await webFrame.executeJavaScript(` (function() { document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.pauseVideo(); }) `) - )(); - break; - } + )(); + break; + } - case 'next': { - ( - await webFrame.executeJavaScript(` + case "next": { + ( + await webFrame.executeJavaScript(` (function() { document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.nextVideo(); }) `) - )(); - break; - } + )(); + break; + } - case 'previous': { - ( - await webFrame.executeJavaScript(` + case "previous": { + ( + await webFrame.executeJavaScript(` (function() { document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.previousVideo(); }) `) - )(); - break; - } - - case 'toggleLike': { - (await webFrame.executeJavaScript(toggleLikeScript))(); - break; - } - - case 'toggleDislike': { - (await webFrame.executeJavaScript(toggleDislikeScript))(); - break; - } - - case 'volumeUp': { - const currentVolumeUp: number = ( - await webFrame.executeJavaScript(` + )(); + break; + } + + case "toggleLike": { + (await webFrame.executeJavaScript(toggleLikeScript))(); + break; + } + + case "toggleDislike": { + (await webFrame.executeJavaScript(toggleDislikeScript))(); + break; + } + + case "volumeUp": { + const currentVolumeUp: number = ( + await webFrame.executeJavaScript(` (function() { return document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.getVolume(); }) `) - )(); - - let newVolumeUp = currentVolumeUp + 10; - if (currentVolumeUp > 100) { - newVolumeUp = 100; - } - ( - await webFrame.executeJavaScript(` + )(); + + let newVolumeUp = currentVolumeUp + 10; + if (currentVolumeUp > 100) { + newVolumeUp = 100; + } + ( + await webFrame.executeJavaScript(` (function(newVolumeUp) { document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.setVolume(newVolumeUp); window.__YTMD_HOOK__.ytmStore.dispatch({ type: 'SET_VOLUME', payload: newVolumeUp }); }) `) - )(newVolumeUp); - break; - } + )(newVolumeUp); + break; + } - case 'volumeDown': { - const currentVolumeDown: number = ( - await webFrame.executeJavaScript(` + case "volumeDown": { + const currentVolumeDown: number = ( + await webFrame.executeJavaScript(` (function() { return document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.getVolume(); }) `) - )(); - - let newVolumeDown = currentVolumeDown - 10; - if (currentVolumeDown < 0) { - newVolumeDown = 0; - } - ( - await webFrame.executeJavaScript(` + )(); + + let newVolumeDown = currentVolumeDown - 10; + if (currentVolumeDown < 0) { + newVolumeDown = 0; + } + ( + await webFrame.executeJavaScript(` (function(newVolumeDown) { document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.setVolume(newVolumeDown); window.__YTMD_HOOK__.ytmStore.dispatch({ type: 'SET_VOLUME', payload: newVolumeDown }); }) `) - )(newVolumeDown); - break; - } - - case 'setVolume': { - const valueInt: number = parseInt(value); - // Check if Volume is a number and between 0 and 100 - if (isNaN(valueInt) || valueInt < 0 || valueInt > 100) { - return; - } - - ( - await webFrame.executeJavaScript(` + )(newVolumeDown); + break; + } + + case "setVolume": { + const valueInt: number = parseInt(value); + // Check if Volume is a number and between 0 and 100 + if (isNaN(valueInt) || valueInt < 0 || valueInt > 100) { + return; + } + + ( + await webFrame.executeJavaScript(` (function(valueInt) { document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.setVolume(valueInt); window.__YTMD_HOOK__.ytmStore.dispatch({ type: 'SET_VOLUME', payload: valueInt }); }) `) - )(valueInt); - break; - } + )(valueInt); + break; + } - case 'mute': - ( - await webFrame.executeJavaScript(` + case "mute": + ( + await webFrame.executeJavaScript(` (function() { document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.mute(); window.__YTMD_HOOK__.ytmStore.dispatch({ type: 'SET_MUTED', payload: true }); }) `) - )(); - break; + )(); + break; - case 'unmute': - ( - await webFrame.executeJavaScript(` + case "unmute": + ( + await webFrame.executeJavaScript(` (function() { document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.unMute(); window.__YTMD_HOOK__.ytmStore.dispatch({ type: 'SET_MUTED', payload: false }); }) `) - )(); - break; + )(); + break; - case 'repeatMode': - ( - await webFrame.executeJavaScript(` + case "repeatMode": + ( + await webFrame.executeJavaScript(` (function(value) { window.__YTMD_HOOK__.ytmStore.dispatch({ type: 'SET_REPEAT', payload: value }); }) `) - )(value); - break; + )(value); + break; - case 'seekTo': - ( - await webFrame.executeJavaScript(` + case "seekTo": + ( + await webFrame.executeJavaScript(` (function(value) { document.querySelector("ytmusic-app-layout>ytmusic-player-bar").playerApi.seekTo(value); }) `) - )(value); - break; + )(value); + break; - case 'shuffle': - ( - await webFrame.executeJavaScript(` + case "shuffle": + ( + await webFrame.executeJavaScript(` (function() { document.querySelector("ytmusic-app-layout>ytmusic-player-bar").queue.shuffle(); }) `) - )(); - break; + )(); + break; - case 'playQueueIndex': { - const index: number = parseInt(value); + case "playQueueIndex": { + const index: number = parseInt(value); - ( - await webFrame.executeJavaScript(` + ( + await webFrame.executeJavaScript(` (function(index) { const state = window.__YTMD_HOOK__.ytmStore.getState(); const queue = state.queue; @@ -1215,56 +1242,58 @@ window.addEventListener('load', async () => { ); }) `) - )(index); - - break; - } - - case 'navigate': { - const endpoint = value; - document.dispatchEvent( - new CustomEvent('yt-navigate', { - detail: { - endpoint - } - }) - ); - break; - } - } - }); - - ipcRenderer.on('ytmView:getPlaylists', async (_event, requestId) => { - const rawPlaylists = await (await webFrame.executeJavaScript(getPlaylistsScript))(); - - const playlists: Array = []; - for (const rawPlaylist of rawPlaylists) { - const playlist = rawPlaylist.playlistAddToOptionRenderer; - playlists.push({ - id: playlist.playlistId, - title: getYTMTextRun(playlist.title.runs) - }); - } - ipcRenderer.send(`ytmView:getPlaylists:response:${requestId}`, playlists); - }); - - store.onDidAnyChange((newState) => { - if (newState.appearance.alwaysShowVolumeSlider) { - const volumeSlider = document.querySelector('#volume-slider'); - if (!volumeSlider?.classList.contains('ytmd-persist-volume-slider')) { - volumeSlider?.classList.add('ytmd-persist-volume-slider'); - } - } else { - const volumeSlider = document.querySelector('#volume-slider'); - if (volumeSlider?.classList.contains('ytmd-persist-volume-slider')) { - volumeSlider?.classList.remove('ytmd-persist-volume-slider'); - } - } - }); - - ipcRenderer.on('ytmView:refitPopups', async () => { - // Update 4/14/2024: Broken until a hook is provided for this - /* + )(index); + + break; + } + + case "navigate": { + const endpoint = value; + document.dispatchEvent( + new CustomEvent("yt-navigate", { + detail: { + endpoint, + }, + }), + ); + break; + } + } + }); + + ipcRenderer.on("ytmView:getPlaylists", async (_event, requestId) => { + const rawPlaylists = await ( + await webFrame.executeJavaScript(getPlaylistsScript) + )(); + + const playlists: Array = []; + for (const rawPlaylist of rawPlaylists) { + const playlist = rawPlaylist.playlistAddToOptionRenderer; + playlists.push({ + id: playlist.playlistId, + title: getYTMTextRun(playlist.title.runs), + }); + } + ipcRenderer.send(`ytmView:getPlaylists:response:${requestId}`, playlists); + }); + + store.onDidAnyChange((newState) => { + if (newState.appearance.alwaysShowVolumeSlider) { + const volumeSlider = document.querySelector("#volume-slider"); + if (!volumeSlider?.classList.contains("ytmd-persist-volume-slider")) { + volumeSlider?.classList.add("ytmd-persist-volume-slider"); + } + } else { + const volumeSlider = document.querySelector("#volume-slider"); + if (volumeSlider?.classList.contains("ytmd-persist-volume-slider")) { + volumeSlider?.classList.remove("ytmd-persist-volume-slider"); + } + } + }); + + ipcRenderer.on("ytmView:refitPopups", async () => { + // Update 4/14/2024: Broken until a hook is provided for this + /* ( await webFrame.executeJavaScript(` (function() { @@ -1273,18 +1302,21 @@ window.addEventListener('load', async () => { `) )(); */ - }); - - ipcRenderer.on('ytmView:executeScript', async (_event, integrationName, scriptName) => { - const scripts = integrationScripts[integrationName]; - if (scripts) { - const script = scripts[scriptName]; - if (script) { - (await webFrame.executeJavaScript(script))(); - } - } - }); - - ipcRenderer.send('ytmView:loaded'); + }); + + ipcRenderer.on( + "ytmView:executeScript", + async (_event, integrationName, scriptName) => { + const scripts = integrationScripts[integrationName]; + if (scripts) { + const script = scripts[scriptName]; + if (script) { + (await webFrame.executeJavaScript(script))(); + } + } + }, + ); + + ipcRenderer.send("ytmView:loaded"); }); -ipcRenderer.send('ytmView:videoDataChanged', null); \ No newline at end of file +ipcRenderer.send("ytmView:videoDataChanged", null); diff --git a/src/preload/types.ts b/src/preload/types.ts new file mode 100644 index 0000000..ac8d366 --- /dev/null +++ b/src/preload/types.ts @@ -0,0 +1,21 @@ +export type musicData = { + videoId: string; + title: string; + author: string; + channelId: string; + isCrawlable: boolean; + isLiveContent: boolean; + isOwnerViewing: boolean; + isUnpluggedCorpus: boolean; + lengthSeconds: string; + musicVideoType: string; + thumbnail: { + thumbnails: { + height: number; + width: number; + url: string; + }[]; + }; + viewCount: string; + allowRatings: boolean; +}; diff --git a/src/renderer/src/App.svelte b/src/renderer/src/App.svelte index 07ec9d1..cd7a614 100644 --- a/src/renderer/src/App.svelte +++ b/src/renderer/src/App.svelte @@ -1,20 +1,17 @@ -{#if theme!==null} -
-
- {#if $activeView==="topbar"} - - {:else if $activeView==="settings"} - - {:else if $activeView==="miniplayer"} - +{#if $theme !== null} +
+
+ {#if $activeView === "topbar"} + + {:else if $activeView === "settings"} + + {:else if $activeView === "miniplayer"} + {/if}
diff --git a/src/renderer/src/assets/app.css b/src/renderer/src/assets/app.css index 6d0561a..2a5b281 100644 --- a/src/renderer/src/assets/app.css +++ b/src/renderer/src/assets/app.css @@ -1,6 +1,6 @@ @font-face { - font-family: 'Inter'; - src: url('./Inter.ttf'); + font-family: "Inter"; + src: url("./Inter.ttf"); } @tailwind base; @@ -8,8 +8,8 @@ @tailwind utilities; .drag { - -webkit-app-region: drag; + -webkit-app-region: drag; } .no-drag { - -webkit-app-region: no-drag; -} \ No newline at end of file + -webkit-app-region: no-drag; +} diff --git a/src/renderer/src/components/menubar/Navigation.svelte b/src/renderer/src/components/menubar/Navigation.svelte index 0c594da..134388f 100644 --- a/src/renderer/src/components/menubar/Navigation.svelte +++ b/src/renderer/src/components/menubar/Navigation.svelte @@ -1,12 +1,17 @@ - -
- window.api.pageAction('back')} /> - window.api.pageAction('forward')} /> - window.api.pageAction('reload')} /> -
- \ No newline at end of file + import { ArrowLeft, ArrowRight, RefreshCw } from "lucide-svelte"; + + import IconButton from "../ui/IconButton.svelte"; + + +
+ window.api.pageAction("back")} /> + window.api.pageAction("forward")} + /> + window.api.pageAction("reload")} + /> +
diff --git a/src/renderer/src/components/menubar/Tab.svelte b/src/renderer/src/components/menubar/Tab.svelte index 9f136a3..3a2c08d 100644 --- a/src/renderer/src/components/menubar/Tab.svelte +++ b/src/renderer/src/components/menubar/Tab.svelte @@ -1,55 +1,58 @@
- + - {#if active && tab.type !== 'music' && tab.type !== 'studio'} - - {/if} + {#if active && tab.type !== "music" && tab.type !== "studio"} + + {/if}
diff --git a/src/renderer/src/components/menubar/TabBar.svelte b/src/renderer/src/components/menubar/TabBar.svelte index d3ed399..f80c85b 100644 --- a/src/renderer/src/components/menubar/TabBar.svelte +++ b/src/renderer/src/components/menubar/TabBar.svelte @@ -1,22 +1,24 @@ -
- {#each tabs as tab, i (tab.id)} -
- -
- {/each} +
+ {#each tabs as tab, i (tab.id)} +
+ +
+ {/each}
diff --git a/src/renderer/src/components/miniplayer/Progress.svelte b/src/renderer/src/components/miniplayer/Progress.svelte index 4899ef2..568f1eb 100644 --- a/src/renderer/src/components/miniplayer/Progress.svelte +++ b/src/renderer/src/components/miniplayer/Progress.svelte @@ -1,22 +1,22 @@ - - {#each thumbs as thumb} - - {/each} + + {#each thumbs as thumb} + + {/each} diff --git a/src/renderer/src/components/miniplayer/VolumeBar.svelte b/src/renderer/src/components/miniplayer/VolumeBar.svelte index fbdb5f2..a1c67f5 100644 --- a/src/renderer/src/components/miniplayer/VolumeBar.svelte +++ b/src/renderer/src/components/miniplayer/VolumeBar.svelte @@ -1,28 +1,30 @@ {#if value !== null} - window.api.musicRemote('setVolume', e[0])} - let:thumbs - class="relative ml-20 mr-2 flex w-full touch-none select-none items-center" - > - - - - {#each thumbs as thumb} - - {/each} - + window.api.musicRemote("setVolume", e[0])} + let:thumbs + class="relative ml-20 mr-2 flex w-full touch-none select-none items-center" + > + + + + {#each thumbs as thumb} + + {/each} + {/if} diff --git a/src/renderer/src/components/settings/KeybindInput.svelte b/src/renderer/src/components/settings/KeybindInput.svelte index 4f20cab..1ac10b5 100644 --- a/src/renderer/src/components/settings/KeybindInput.svelte +++ b/src/renderer/src/components/settings/KeybindInput.svelte @@ -30,7 +30,7 @@ } function validateKey(event: KeyboardEvent) { - console.log(event.key) + console.log(event.key); if (event.key === " ") return "Space"; if (event.code === "NumpadEnter") return "Enter"; if (event.code === "NumpadAdd") return "NumAdd"; @@ -85,7 +85,7 @@ if (event.shiftKey) newKeybind += "Shift+"; newKeybind += validateKey(event); - newKeybind = newKeybind.replace('\u00a0', "Space"); + newKeybind = newKeybind.replace("\u00a0", "Space"); input.blur(); console.log(newKeybind); value = newKeybind; @@ -97,7 +97,13 @@ type="text" bind:this={input} on:keydown={handleKeyDown} - class="w-52 rounded border bg-zinc-50 text-neutral-800 dark:text-zinc-200 px-2 py-1 text-left dark:border-neutral-700 dark:bg-neutral-900" + class="w-52 rounded border bg-zinc-50 text-neutral-800 dark:text-zinc-200 px-2 py-1 text-left dark:border-neutral-700 dark:bg-neutral-900" {value} /> -{value="Unbound"; dispatch("change", "Unbound")}}/> + { + value = "Unbound"; + dispatch("change", "Unbound"); + }} +/> diff --git a/src/renderer/src/components/settings/Select.svelte b/src/renderer/src/components/settings/Select.svelte new file mode 100644 index 0000000..3ef62f8 --- /dev/null +++ b/src/renderer/src/components/settings/Select.svelte @@ -0,0 +1,49 @@ + + + o.value == value)[0]} + onSelectedChange={handleSelect} +> + + + + + + {#each options as option} + +
{option.label}
+ + + + +
+ {/each} +
+
diff --git a/src/renderer/src/components/settings/SettingItem.svelte b/src/renderer/src/components/settings/SettingItem.svelte index 8766023..f64732f 100644 --- a/src/renderer/src/components/settings/SettingItem.svelte +++ b/src/renderer/src/components/settings/SettingItem.svelte @@ -1,9 +1,9 @@ -
+
{label} {#if restart} @@ -43,36 +52,7 @@
{#if value !== null} {#if type === "select"} - o.value == value)[0]} - onSelectedChange={handleSelect} - > - - - - - - {#each options as option} - -
{option.label}
- - - - -
- {/each} -
-
+ { + window.api.setConfig( + key, + JSON.stringify({ + ...value, + }), + ); + }} + /> + + + + +
+
+ +
+ {/if} +{/if} diff --git a/src/renderer/src/components/settings/SidebarTab.svelte b/src/renderer/src/components/settings/SidebarTab.svelte index 0f28f57..d97d33b 100644 --- a/src/renderer/src/components/settings/SidebarTab.svelte +++ b/src/renderer/src/components/settings/SidebarTab.svelte @@ -1,17 +1,19 @@ - - -
- -
-
- {label} -
-
- \ No newline at end of file + export let value: string; + export let label: string; + export let icon: typeof SvelteComponent; + import { Tabs } from "bits-ui"; + import type { SvelteComponent } from "svelte"; + + + +
+ +
+
+ {label} +
+
diff --git a/src/renderer/src/components/ui/MacSpace.svelte b/src/renderer/src/components/ui/MacSpace.svelte index 45855db..996693f 100644 --- a/src/renderer/src/components/ui/MacSpace.svelte +++ b/src/renderer/src/components/ui/MacSpace.svelte @@ -1,3 +1,3 @@ {#if window.api.platform === "darwin"} -
-{/if} \ No newline at end of file +
+{/if} diff --git a/src/renderer/src/components/ui/Spinner.svelte b/src/renderer/src/components/ui/Spinner.svelte index d20a684..14d19f1 100644 --- a/src/renderer/src/components/ui/Spinner.svelte +++ b/src/renderer/src/components/ui/Spinner.svelte @@ -1,50 +1,50 @@
-
-
-
-
+
+
+
+
diff --git a/src/renderer/src/lib/stores.ts b/src/renderer/src/lib/stores.ts index 8036af1..979d2ad 100644 --- a/src/renderer/src/lib/stores.ts +++ b/src/renderer/src/lib/stores.ts @@ -1,3 +1,6 @@ import { writable } from "svelte/store"; +import type { musicStore } from "./types"; export const activeView = writable("topbar"); +export const musicDataStore = writable(null); +export const theme = writable<"light" | "dark" | null>(null); diff --git a/src/renderer/src/lib/types.ts b/src/renderer/src/lib/types.ts new file mode 100644 index 0000000..afa2582 --- /dev/null +++ b/src/renderer/src/lib/types.ts @@ -0,0 +1,37 @@ +export type musicData = { + videoId: string; + title: string; + author: string; + channelId: string; + isCrawlable: boolean; + isLiveContent: boolean; + isOwnerViewing: boolean; + isUnpluggedCorpus: boolean; + lengthSeconds: string; + musicVideoType: string; + thumbnail: { + thumbnails: { + height: number; + width: number; + url: string; + }[]; + }; + viewCount: string; + allowRatings: boolean; +}; + +export enum playerState { + Unstarted = -1, + Ended = 0, + Playing = 1, + Paused = 2, + Buffering = 3, + VideoCued = 5, +} + +export type musicStore = { + data: musicData; + state: playerState; + progress: number; + volume: number; +}; diff --git a/src/renderer/src/lib/utils.ts b/src/renderer/src/lib/utils.ts index 1b683c5..ab47204 100644 --- a/src/renderer/src/lib/utils.ts +++ b/src/renderer/src/lib/utils.ts @@ -1,7 +1,7 @@ const debounce = (fn: Function, ms = 300) => { - let timeoutId: ReturnType; - return function (this: any, ...args: any[]) { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => fn.apply(this, args), ms); - }; - }; \ No newline at end of file + let timeoutId: ReturnType; + return function (this: any, ...args: any[]) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), ms); + }; +}; diff --git a/src/renderer/src/main.ts b/src/renderer/src/main.ts index c726773..711e0e8 100644 --- a/src/renderer/src/main.ts +++ b/src/renderer/src/main.ts @@ -1,9 +1,9 @@ -import './assets/app.css' +import "./assets/app.css"; -import App from './App.svelte' +import App from "./App.svelte"; const app = new App({ - target: document.getElementById('app') -}) + target: document.getElementById("app"), +}); -export default app +export default app; diff --git a/src/renderer/src/views/MenuBar.svelte b/src/renderer/src/views/MenuBar.svelte index caa9ddf..fb38144 100644 --- a/src/renderer/src/views/MenuBar.svelte +++ b/src/renderer/src/views/MenuBar.svelte @@ -6,20 +6,15 @@ import WindowControl from "../components/ui/WindowControl.svelte"; import Navigation from "../components/menubar/Navigation.svelte"; import MacSpace from "../components/ui/MacSpace.svelte"; - import { activeView } from "../lib/stores"; - import { onMount } from "svelte"; + import { activeView, musicDataStore } from "../lib/stores"; export let active; export let tabs; - let miniplayerEnabled = false; + $: miniplayerEnabled = $musicDataStore !== null; - onMount(async () => { - miniplayerEnabled = (await window.api.getVideoData()) !== null; - }); - - window.api.onVideoDataChange((e) => { - miniplayerEnabled = e !== null; + musicDataStore.subscribe((e) => { + console.log("musicDataStore", e); }); diff --git a/src/renderer/src/views/MiniPlayer.svelte b/src/renderer/src/views/MiniPlayer.svelte index 774b0f3..2b2d11c 100644 --- a/src/renderer/src/views/MiniPlayer.svelte +++ b/src/renderer/src/views/MiniPlayer.svelte @@ -1,5 +1,4 @@
- -
-
+ {#if $musicDataStore} + Album Art +
+
- {#if videoData}
- {videoData.title} + {$musicDataStore.data.title}
- {videoData.author} + {$musicDataStore.data.author}
{/if} - {#if state !== 2} + {#if $musicDataStore.state !== playerState.Paused}
@@ -162,21 +126,19 @@
{#if progress !== null} {/if}
- {:else} -
- -
- {/if} -
+
+ {:else} +
+ +
+ {/if}
diff --git a/src/renderer/src/views/Settings.svelte b/src/renderer/src/views/Settings.svelte index ad70304..214435d 100644 --- a/src/renderer/src/views/Settings.svelte +++ b/src/renderer/src/views/Settings.svelte @@ -5,13 +5,15 @@ CircleArrowOutDownRight, Cpu, Keyboard, + Paintbrush, + Settings2, Wifi, X, } from "lucide-svelte"; import MacSpace from "../components/ui/MacSpace.svelte"; import TabSidebarItem from "../components/settings/SidebarTab.svelte"; import WindowControl from "../components/ui/WindowControl.svelte"; - import { Tabs } from "bits-ui"; + import { ScrollArea, Tabs } from "bits-ui"; import SettingsItem from "../components/settings/SettingItem.svelte"; import IconButton from "../components/ui/IconButton.svelte"; import { activeView } from "../lib/stores"; @@ -21,9 +23,9 @@ let tabs = [ { - value: "app", - label: "Application", - icon: Cpu, + value: "appearance", + label: "Appearance", + icon: Paintbrush, settings: [ { label: "App Theme", @@ -45,20 +47,35 @@ ], }, { - label: "Youtube Studio Tab", - key: "studio-tab", - restart: true, - type: "switch", + label: "YT Music Custom Theme", + key: "music-css", + type: "css", + }, + { + label: "Youtube Custom Theme", + key: "yt-css", + type: "css", }, ], }, { - value: "miniplayer", - label: "Miniplayer", - icon: CircleArrowOutDownRight, + value: "app", + label: "Behaviour", + icon: Settings2, settings: [ { - label: "Keep on top", + label: "Youtube Studio Tab", + key: "studio-tab", + restart: true, + type: "switch", + }, + { + label: "Force cinema mode", + key: "force-cinema", + type: "switch", + }, + { + label: "Keep miniplayer on top", key: "miniplayer-on-top", type: "switch", }, @@ -114,7 +131,7 @@ --> -
+

Settings

{/each} -
- {#each tabs as tab} - -
- {#if tab.value == "keybinds"} -
These keybinds work anywhere on your computer unless another app conflicts. (Supports music playback only)
- {/if} - {#if tab.settings} - {#each tab.settings as setting} - - {/each} - {/if} -
-
- {/each} -
+ + {#each tabs as tab} + +
+ {#if tab.value == "keybinds"} +
+ These keybinds work anywhere on your computer unless another + app conflicts. (Supports music playback only) +
+ {/if} + {#if tab.settings} + {#each tab.settings as setting} + + {/each} + {/if} +
+
+ {/each}
-
+
diff --git a/svelte.config.mjs b/svelte.config.mjs index 1a90ad4..de2ddd6 100644 --- a/svelte.config.mjs +++ b/svelte.config.mjs @@ -1,7 +1,7 @@ -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; export default { // Consult https://svelte.dev/docs#compile-time-svelte-preprocess // for more information about preprocessors - preprocess: vitePreprocess() -} + preprocess: vitePreprocess(), +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index e79ed0e..e7e3ed5 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -6,8 +6,8 @@ export default { theme: { extend: { fontFamily: { - sans: ["Inter", "sans-serif"] - } + sans: ["Inter", "sans-serif"], + }, }, }, darkMode: "class", diff --git a/website/package.json b/website/package.json index e22f26a..e8060e5 100644 --- a/website/package.json +++ b/website/package.json @@ -1,28 +1,28 @@ { - "name": "website", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" - }, - "devDependencies": { - "@sveltejs/adapter-auto": "^3.0.0", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "@tailwindcss/typography": "^0.5.14", - "autoprefixer": "^10.4.20", - "svelte": "^4.2.7", - "svelte-check": "^3.6.0", - "tailwindcss": "^3.4.9", - "typescript": "^5.0.0", - "vite": "^5.0.3" - }, - "type": "module", - "dependencies": { - "svelte-inview": "^4.0.2" - } + "name": "website", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@tailwindcss/typography": "^0.5.14", + "autoprefixer": "^10.4.20", + "svelte": "^4.2.7", + "svelte-check": "^3.6.0", + "tailwindcss": "^3.4.9", + "typescript": "^5.0.0", + "vite": "^5.0.3" + }, + "type": "module", + "dependencies": { + "svelte-inview": "^4.0.2" + } } diff --git a/website/postcss.config.js b/website/postcss.config.js index ba80730..2aa7205 100644 --- a/website/postcss.config.js +++ b/website/postcss.config.js @@ -1,6 +1,6 @@ export default { plugins: { tailwindcss: {}, - autoprefixer: {} - } + autoprefixer: {}, + }, }; diff --git a/website/src/app.css b/website/src/app.css index 0136d2b..0b0d796 100644 --- a/website/src/app.css +++ b/website/src/app.css @@ -1,7 +1,5 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); @import "tailwindcss/base"; @import "tailwindcss/components"; @import "tailwindcss/utilities"; - - diff --git a/website/src/app.d.ts b/website/src/app.d.ts index 743f07b..ede601a 100644 --- a/website/src/app.d.ts +++ b/website/src/app.d.ts @@ -1,13 +1,13 @@ // See https://kit.svelte.dev/docs/types#app // for information about these interfaces declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } } export {}; diff --git a/website/src/app.html b/website/src/app.html index 4a5ade2..b73b476 100644 --- a/website/src/app.html +++ b/website/src/app.html @@ -1,13 +1,13 @@ - - - - - YT Desk - YT & YTM Supercharged - %sveltekit.head% - - -
%sveltekit.body%
- + + + + + YT Desk - YT & YTM Supercharged + %sveltekit.head% + + +
%sveltekit.body%
+ diff --git a/website/src/routes/+layout.svelte b/website/src/routes/+layout.svelte index e551b53..872f217 100644 --- a/website/src/routes/+layout.svelte +++ b/website/src/routes/+layout.svelte @@ -1,5 +1,5 @@ diff --git a/website/src/routes/+page.svelte b/website/src/routes/+page.svelte index 41599d8..c6c110f 100644 --- a/website/src/routes/+page.svelte +++ b/website/src/routes/+page.svelte @@ -1,55 +1,72 @@
-
- - -
+
+ + +
-
-

YT Desk

-

Youtube & YT Music supercharged

- Download -
+
+

YT Desk

+

+ Youtube & YT Music supercharged +

+ Download +
-
- -
- IGOR Miniplayer -
-
-
-
Miniplayer
-
    -
  • Simple and clean
  • -
  • Quick access to frequent controls
  • -
  • Always on top (optional)
  • -
-
-
+
+ +
+ IGOR Miniplayer +
+
+
+
Miniplayer
+
    +
  • Simple and clean
  • +
  • Quick access to frequent controls
  • +
  • Always on top (optional)
  • +
+
+
-
- +
+ -
-
Discord Presence
- Share what your listening to/watching with your discord friends. -
-
- IGOR Miniplayer -
+
+
Discord Presence
+ Share what your listening to/watching with your discord friends. +
+
+ IGOR Miniplayer +
diff --git a/website/svelte.config.js b/website/svelte.config.js index 4a82086..973bdc9 100644 --- a/website/svelte.config.js +++ b/website/svelte.config.js @@ -1,18 +1,18 @@ -import adapter from '@sveltejs/adapter-auto'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; +import adapter from "@sveltejs/adapter-auto"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; /** @type {import('@sveltejs/kit').Config} */ const config = { - // Consult https://kit.svelte.dev/docs/integrations#preprocessors - // for more information about preprocessors - preprocess: vitePreprocess(), + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), - kit: { - // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://kit.svelte.dev/docs/adapters for more information about adapters. - adapter: adapter() - } + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter(), + }, }; export default config; diff --git a/website/tailwind.config.ts b/website/tailwind.config.ts index 580ab11..79aa9f4 100644 --- a/website/tailwind.config.ts +++ b/website/tailwind.config.ts @@ -6,10 +6,10 @@ export default { theme: { extend: { fontFamily: { - sans: ["Inter", "sans-serif"] - } - } + sans: ["Inter", "sans-serif"], + }, + }, }, - plugins: [require("@tailwindcss/typography")] + plugins: [require("@tailwindcss/typography")], } as Config; diff --git a/website/vite.config.ts b/website/vite.config.ts index bbf8c7d..80864b9 100644 --- a/website/vite.config.ts +++ b/website/vite.config.ts @@ -1,6 +1,6 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; export default defineConfig({ - plugins: [sveltekit()] + plugins: [sveltekit()], });