mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-15 12:21:47 +00:00
Compare commits
16 Commits
26fa1f85b2
...
v3.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 721271d902 | |||
| a2151930ec | |||
| bdc9f42681 | |||
| 0b3c6f9e1f | |||
| ebe373bdc6 | |||
| ab91e6d735 | |||
| be3ae4d789 | |||
| 336b7fe5e9 | |||
| 5ecd39f324 | |||
| eb50596961 | |||
| aede0cd643 | |||
| 25f51784c3 | |||
| 7acac6d11c | |||
| 1972ef70b5 | |||
| c4455ed3d2 | |||
| c1a06ab955 |
@ -45,7 +45,7 @@ export default defineConfig({
|
|||||||
formats: ['es'],
|
formats: ['es'],
|
||||||
},
|
},
|
||||||
outDir: 'dist/main',
|
outDir: 'dist/main',
|
||||||
rollupOptions: {
|
rolldownOptions: {
|
||||||
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
||||||
input: './src/index.ts',
|
input: './src/index.ts',
|
||||||
},
|
},
|
||||||
@ -96,7 +96,7 @@ export default defineConfig({
|
|||||||
commonjsOptions: {
|
commonjsOptions: {
|
||||||
ignoreDynamicRequires: true,
|
ignoreDynamicRequires: true,
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rolldownOptions: {
|
||||||
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
||||||
input: './src/preload.ts',
|
input: './src/preload.ts',
|
||||||
},
|
},
|
||||||
@ -149,7 +149,7 @@ export default defineConfig({
|
|||||||
name: 'renderer',
|
name: 'renderer',
|
||||||
},
|
},
|
||||||
outDir: 'dist/renderer',
|
outDir: 'dist/renderer',
|
||||||
rollupOptions: {
|
rolldownOptions: {
|
||||||
external: ['electron', ...builtinModules],
|
external: ['electron', ...builtinModules],
|
||||||
input: './src/index.html',
|
input: './src/index.html',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"desktopName": "com.github.th_ch.youtube_music",
|
"desktopName": "com.github.th_ch.youtube_music",
|
||||||
"productName": "YouTube Music",
|
"productName": "YouTube Music",
|
||||||
"version": "3.10.0",
|
"version": "3.11.0",
|
||||||
"description": "YouTube Music Desktop App - including custom plugins",
|
"description": "YouTube Music Desktop App - including custom plugins",
|
||||||
"main": "./dist/main/index.js",
|
"main": "./dist/main/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@ -20,8 +20,8 @@
|
|||||||
"vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect",
|
"vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect",
|
||||||
"start": "pnpm electron-vite preview",
|
"start": "pnpm electron-vite preview",
|
||||||
"start:debug": "pnpm cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
|
"start:debug": "pnpm cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
|
||||||
"dev": "pnpm cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev --watch",
|
"dev": "pnpm cross-env NODE_ENV=development NODE_OPTIONS=--enable-source-maps electron-vite dev --watch",
|
||||||
"dev:renderer": "pnpm cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev",
|
"dev:renderer": "pnpm cross-env NODE_ENV=development NODE_OPTIONS=--enable-source-maps electron-vite dev",
|
||||||
"dev:debug": "pnpm cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
|
"dev:debug": "pnpm cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
|
||||||
"clean": "pnpm del-cli dist && pnpm del-cli pack && pnpm del-cli .vite-inspect",
|
"clean": "pnpm del-cli dist && pnpm del-cli pack && pnpm del-cli .vite-inspect",
|
||||||
"dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never",
|
"dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never",
|
||||||
@ -33,7 +33,7 @@
|
|||||||
"dist:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p never",
|
"dist:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p never",
|
||||||
"dist:win:x64": "pnpm clean && pnpm build && pnpm electron-builder --win nsis-web:x64 -p never",
|
"dist:win:x64": "pnpm clean && pnpm build && pnpm electron-builder --win nsis-web:x64 -p never",
|
||||||
"lint": "pnpm eslint ./src",
|
"lint": "pnpm eslint ./src",
|
||||||
"changelog": "pnpm dlx --yes auto-changelog",
|
"changelog": "pnpm dlx auto-changelog",
|
||||||
"release:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p always -c.snap.publish=github",
|
"release:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p always -c.snap.publish=github",
|
||||||
"release:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac -p always",
|
"release:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac -p always",
|
||||||
"release:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p always",
|
"release:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p always",
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export interface DefaultConfig {
|
|||||||
'plugins': Record<string, unknown>;
|
'plugins': Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConfig: DefaultConfig = {
|
export const defaultConfig: DefaultConfig = {
|
||||||
'window-size': {
|
'window-size': {
|
||||||
width: 1100,
|
width: 1100,
|
||||||
height: 550,
|
height: 550,
|
||||||
@ -74,5 +74,3 @@ const defaultConfig: DefaultConfig = {
|
|||||||
},
|
},
|
||||||
'plugins': {},
|
'plugins': {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defaultConfig;
|
|
||||||
|
|||||||
@ -1,30 +1,36 @@
|
|||||||
import { deepmergeCustom } from 'deepmerge-ts';
|
import { deepmergeCustom } from 'deepmerge-ts';
|
||||||
|
|
||||||
import defaultConfig from './defaults';
|
import { store, type IStore } from './store';
|
||||||
|
|
||||||
import store, { type IStore } from './store';
|
|
||||||
import plugins from './plugins';
|
|
||||||
|
|
||||||
import { restart } from '@/providers/app-controls';
|
import { restart } from '@/providers/app-controls';
|
||||||
|
|
||||||
|
import type { defaultConfig } from './defaults';
|
||||||
|
|
||||||
const deepmerge = deepmergeCustom({
|
const deepmerge = deepmergeCustom({
|
||||||
mergeArrays: false,
|
mergeArrays: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const set = (key: string, value: unknown) => {
|
export { defaultConfig } from './defaults';
|
||||||
|
export * as plugins from './plugins';
|
||||||
|
|
||||||
|
export const set = (key: string, value: unknown) => {
|
||||||
store.set(key, value);
|
store.set(key, value);
|
||||||
};
|
};
|
||||||
const setPartial = (key: string, value: object, defaultValue?: object) => {
|
|
||||||
|
export const setPartial = (
|
||||||
|
key: string,
|
||||||
|
value: object,
|
||||||
|
defaultValue?: object,
|
||||||
|
) => {
|
||||||
const newValue = deepmerge(defaultValue ?? {}, store.get(key) ?? {}, value);
|
const newValue = deepmerge(defaultValue ?? {}, store.get(key) ?? {}, value);
|
||||||
store.set(key, newValue);
|
store.set(key, newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
function setMenuOption(key: string, value: unknown) {
|
export const setMenuOption = (key: string, value: unknown) => {
|
||||||
set(key, value);
|
set(key, value);
|
||||||
if (store.get('options.restartOnConfigChanges')) {
|
if (store.get('options.restartOnConfigChanges')) {
|
||||||
restart();
|
restart();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// MAGIC OF TYPESCRIPT
|
// MAGIC OF TYPESCRIPT
|
||||||
|
|
||||||
@ -74,18 +80,11 @@ type PathValue<T, K extends string> =
|
|||||||
? PathValue<T[A], B>
|
? PathValue<T[A], B>
|
||||||
: T;
|
: T;
|
||||||
|
|
||||||
const get = <Key extends Paths<typeof defaultConfig>>(key: Key) =>
|
export const get = <Key extends Paths<typeof defaultConfig>>(key: Key) =>
|
||||||
store.get(key) as PathValue<typeof defaultConfig, typeof key>;
|
store.get(key) as PathValue<typeof defaultConfig, typeof key>;
|
||||||
|
|
||||||
export default {
|
export const edit = () => store.openInEditor();
|
||||||
defaultConfig,
|
|
||||||
get,
|
export const watch = (cb: Parameters<IStore['onDidAnyChange']>[0]) => {
|
||||||
set,
|
store.onDidAnyChange(cb);
|
||||||
setPartial,
|
|
||||||
setMenuOption,
|
|
||||||
edit: () => store.openInEditor(),
|
|
||||||
watch(cb: Parameters<IStore['onDidAnyChange']>[0]) {
|
|
||||||
store.onDidAnyChange(cb);
|
|
||||||
},
|
|
||||||
plugins,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { deepmerge } from 'deepmerge-ts';
|
import { deepmerge } from 'deepmerge-ts';
|
||||||
import { allPlugins } from 'virtual:plugins';
|
import { allPlugins } from 'virtual:plugins';
|
||||||
|
|
||||||
import store from './store';
|
import { store } from './store';
|
||||||
|
|
||||||
import { restart } from '@/providers/app-controls';
|
import { restart } from '@/providers/app-controls';
|
||||||
|
|
||||||
@ -68,13 +68,3 @@ export function enable(plugin: string) {
|
|||||||
export function disable(plugin: string) {
|
export function disable(plugin: string) {
|
||||||
setMenuOptions(plugin, { enabled: false }, []);
|
setMenuOptions(plugin, { enabled: false }, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
|
||||||
isEnabled,
|
|
||||||
getPlugins,
|
|
||||||
enable,
|
|
||||||
disable,
|
|
||||||
setOptions,
|
|
||||||
setMenuOptions,
|
|
||||||
getOptions,
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
|
|
||||||
import defaults from './defaults';
|
import { defaultConfig as defaults } from './defaults';
|
||||||
|
|
||||||
import { DefaultPresetList, type Preset } from '@/plugins/downloader/types';
|
import { DefaultPresetList, type Preset } from '@/plugins/downloader/types';
|
||||||
|
|
||||||
@ -257,7 +257,7 @@ const migrations = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default new Store({
|
export const store = new Store({
|
||||||
defaults: {
|
defaults: {
|
||||||
...defaults,
|
...defaults,
|
||||||
// README: 'plugin' uses deepmerge to populate the default values, so it is not necessary to include it here
|
// README: 'plugin' uses deepmerge to populate the default values, so it is not necessary to include it here
|
||||||
|
|||||||
@ -813,6 +813,14 @@
|
|||||||
"not-found": "⚠️ No lyrics found for this song."
|
"not-found": "⚠️ No lyrics found for this song."
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
|
"preferred-provider": {
|
||||||
|
"label": "Preferred Provider",
|
||||||
|
"tooltip": "Choose the default provider to use",
|
||||||
|
"none": {
|
||||||
|
"label": "None",
|
||||||
|
"tooltip": "No preferred provider"
|
||||||
|
}
|
||||||
|
},
|
||||||
"default-text-string": {
|
"default-text-string": {
|
||||||
"label": "Default character between lyrics",
|
"label": "Default character between lyrics",
|
||||||
"tooltip": "Choose the default character to use for the gap between lyrics"
|
"tooltip": "Choose the default character to use for the gap between lyrics"
|
||||||
|
|||||||
@ -421,6 +421,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"custom-output-device": {
|
||||||
|
"description": "미디어 출력 장치 구성",
|
||||||
|
"menu": {
|
||||||
|
"device-selector": "장치 선택"
|
||||||
|
},
|
||||||
|
"name": "출력 장치 커스텀",
|
||||||
|
"prompt": {
|
||||||
|
"device-selector": {
|
||||||
|
"label": "사용할 미디어 출력 장치를 선택하세요",
|
||||||
|
"title": "출력 장치 선택"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"disable-autoplay": {
|
"disable-autoplay": {
|
||||||
"description": "노래를 '일시 정지' 모드로 시작하게 합니다",
|
"description": "노래를 '일시 정지' 모드로 시작하게 합니다",
|
||||||
"menu": {
|
"menu": {
|
||||||
@ -444,7 +457,15 @@
|
|||||||
"hide-duration-left": "남은 재생 시간 숨기기",
|
"hide-duration-left": "남은 재생 시간 숨기기",
|
||||||
"hide-github-button": "GitHub 링크 버튼 숨기기",
|
"hide-github-button": "GitHub 링크 버튼 숨기기",
|
||||||
"play-on-youtube-music": "유튜브 뮤직에서 재생",
|
"play-on-youtube-music": "유튜브 뮤직에서 재생",
|
||||||
"set-inactivity-timeout": "비활성 시간 제한 설정"
|
"set-inactivity-timeout": "비활성 시간 제한 설정",
|
||||||
|
"set-status-display-type": {
|
||||||
|
"label": "상태 텍스트",
|
||||||
|
"submenu": {
|
||||||
|
"artist": "{아티스트} 듣는 중",
|
||||||
|
"title": "{곡 제목} 듣는 중",
|
||||||
|
"youtube-music": "YouTube Music 듣는 중"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"name": "디스코드 활동 상태",
|
"name": "디스코드 활동 상태",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
@ -736,6 +757,7 @@
|
|||||||
"listenbrainz": {
|
"listenbrainz": {
|
||||||
"token": "ListenBrainz 유저 토큰 입력"
|
"token": "ListenBrainz 유저 토큰 입력"
|
||||||
},
|
},
|
||||||
|
"scrobble-alternative-artist": "대체 아티스트 명 사용",
|
||||||
"scrobble-alternative-title": "대체 제목 사용하기",
|
"scrobble-alternative-title": "대체 제목 사용하기",
|
||||||
"scrobble-other-media": "다른 미디어 스크로블하기"
|
"scrobble-other-media": "다른 미디어 스크로블하기"
|
||||||
},
|
},
|
||||||
@ -821,6 +843,14 @@
|
|||||||
"label": "가사를 최대한 정교하게 동기화",
|
"label": "가사를 최대한 정교하게 동기화",
|
||||||
"tooltip": "다음 줄의 표시를 밀리초 단위로 계산합니다 (성능에 약간의 영향을 미칠 수 있음)"
|
"tooltip": "다음 줄의 표시를 밀리초 단위로 계산합니다 (성능에 약간의 영향을 미칠 수 있음)"
|
||||||
},
|
},
|
||||||
|
"preferred-provider": {
|
||||||
|
"label": "선호하는 가사 제공자",
|
||||||
|
"none": {
|
||||||
|
"label": "없음",
|
||||||
|
"tooltip": "선호하는 가사 제공자 없음"
|
||||||
|
},
|
||||||
|
"tooltip": "사용할 기본 가사 제공자를 선택하세요"
|
||||||
|
},
|
||||||
"romanization": {
|
"romanization": {
|
||||||
"label": "가사 로마자 변환",
|
"label": "가사 로마자 변환",
|
||||||
"tooltip": "가사가 영어가 아닌 언어로 되어있는 경우, 로마자 표기를 표시합니다."
|
"tooltip": "가사가 영어가 아닌 언어로 되어있는 경우, 로마자 표기를 표시합니다."
|
||||||
@ -853,6 +883,27 @@
|
|||||||
"description": "macOS 사용자를 위한 TouchBar 위젯을 추가합니다",
|
"description": "macOS 사용자를 위한 TouchBar 위젯을 추가합니다",
|
||||||
"name": "TouchBar"
|
"name": "TouchBar"
|
||||||
},
|
},
|
||||||
|
"transparent-player": {
|
||||||
|
"description": "애플리케이션 창을 투명하게 만듭니다",
|
||||||
|
"menu": {
|
||||||
|
"opacity": {
|
||||||
|
"label": "불투명도",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{opacity}}%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"label": "종류",
|
||||||
|
"submenu": {
|
||||||
|
"acrylic": "아크릴",
|
||||||
|
"mica": "미카",
|
||||||
|
"none": "없음",
|
||||||
|
"tabbed": "탭"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "투명 플레이어"
|
||||||
|
},
|
||||||
"tuna-obs": {
|
"tuna-obs": {
|
||||||
"description": "OBS의 확장인 Tuna와의 통합을 활성화합니다",
|
"description": "OBS의 확장인 Tuna와의 통합을 활성화합니다",
|
||||||
"name": "Tuna OBS"
|
"name": "Tuna OBS"
|
||||||
|
|||||||
@ -757,6 +757,7 @@
|
|||||||
"listenbrainz": {
|
"listenbrainz": {
|
||||||
"token": "Введите токен пользователя ListenBrainz"
|
"token": "Введите токен пользователя ListenBrainz"
|
||||||
},
|
},
|
||||||
|
"scrobble-alternative-artist": "Использовать альтернативных исполнителей",
|
||||||
"scrobble-alternative-title": "Использовать альтернативные названия",
|
"scrobble-alternative-title": "Использовать альтернативные названия",
|
||||||
"scrobble-other-media": "Скробблинг других медиа"
|
"scrobble-other-media": "Скробблинг других медиа"
|
||||||
},
|
},
|
||||||
@ -874,6 +875,27 @@
|
|||||||
"description": "Добавляет виджет тачбара для пользователей macOS",
|
"description": "Добавляет виджет тачбара для пользователей macOS",
|
||||||
"name": "Тачбар"
|
"name": "Тачбар"
|
||||||
},
|
},
|
||||||
|
"transparent-player": {
|
||||||
|
"description": "Делает окно приложения прозрачным",
|
||||||
|
"menu": {
|
||||||
|
"opacity": {
|
||||||
|
"label": "Непрозрачность",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{opacity}}%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"label": "Тип",
|
||||||
|
"submenu": {
|
||||||
|
"acrylic": "Acrylic",
|
||||||
|
"mica": "Mica",
|
||||||
|
"none": "Отключено",
|
||||||
|
"tabbed": "Tabbed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Прозрачный плеер"
|
||||||
|
},
|
||||||
"tuna-obs": {
|
"tuna-obs": {
|
||||||
"description": "Интеграция с плагином Tuna от OBS",
|
"description": "Интеграция с плагином Tuna от OBS",
|
||||||
"name": "Tuna OBS"
|
"name": "Tuna OBS"
|
||||||
|
|||||||
@ -421,6 +421,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"custom-output-device": {
|
||||||
|
"description": "Şarkılar için özel bir medya çıkış aygıtı ayarlayın",
|
||||||
|
"menu": {
|
||||||
|
"device-selector": "Aygıt Seçin"
|
||||||
|
},
|
||||||
|
"name": "Özel Çıkış Aygıtı",
|
||||||
|
"prompt": {
|
||||||
|
"device-selector": {
|
||||||
|
"label": "Kullanılacak medya çıkış aygıtını seçin",
|
||||||
|
"title": "Çıkış Aygıtını Seçin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"disable-autoplay": {
|
"disable-autoplay": {
|
||||||
"description": "Şarkıların otomatik olarak duraklatılmasını sağlar",
|
"description": "Şarkıların otomatik olarak duraklatılmasını sağlar",
|
||||||
"menu": {
|
"menu": {
|
||||||
@ -444,7 +457,15 @@
|
|||||||
"hide-duration-left": "Kalan süreyi gizle",
|
"hide-duration-left": "Kalan süreyi gizle",
|
||||||
"hide-github-button": "GitHub bağlantısını gizle",
|
"hide-github-button": "GitHub bağlantısını gizle",
|
||||||
"play-on-youtube-music": "YouTube Music de oynat",
|
"play-on-youtube-music": "YouTube Music de oynat",
|
||||||
"set-inactivity-timeout": "Hareketsizlik zaman aşımını ayarla"
|
"set-inactivity-timeout": "Hareketsizlik zaman aşımını ayarla",
|
||||||
|
"set-status-display-type": {
|
||||||
|
"label": "Durum metni",
|
||||||
|
"submenu": {
|
||||||
|
"artist": "{artist} Dinleniyor",
|
||||||
|
"title": "{song title} Dinleniyor",
|
||||||
|
"youtube-music": "YouTube Müzik Dinleniyor"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"name": "Discord Etkinlik Durumu",
|
"name": "Discord Etkinlik Durumu",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
@ -736,6 +757,7 @@
|
|||||||
"listenbrainz": {
|
"listenbrainz": {
|
||||||
"token": "ListenBrainz kullanıcı kimliğinizi girin"
|
"token": "ListenBrainz kullanıcı kimliğinizi girin"
|
||||||
},
|
},
|
||||||
|
"scrobble-alternative-artist": "Alternatif sanatçıları kullan",
|
||||||
"scrobble-alternative-title": "Alternatif başlıklar kullan",
|
"scrobble-alternative-title": "Alternatif başlıklar kullan",
|
||||||
"scrobble-other-media": "Diğer medya ortamlarında listele"
|
"scrobble-other-media": "Diğer medya ortamlarında listele"
|
||||||
},
|
},
|
||||||
@ -853,6 +875,27 @@
|
|||||||
"description": "macOS kullanıcıları için bir TouchBar widget'ı ekler",
|
"description": "macOS kullanıcıları için bir TouchBar widget'ı ekler",
|
||||||
"name": "TouchBar"
|
"name": "TouchBar"
|
||||||
},
|
},
|
||||||
|
"transparent-player": {
|
||||||
|
"description": "Uygulama penceresini şeffaf yapar",
|
||||||
|
"menu": {
|
||||||
|
"opacity": {
|
||||||
|
"label": "Opaklık",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "%{{opacity}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"label": "Tür",
|
||||||
|
"submenu": {
|
||||||
|
"acrylic": "Akrilik",
|
||||||
|
"mica": "Mika",
|
||||||
|
"none": "Hiçbiri",
|
||||||
|
"tabbed": "Sekmeli"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Şeffaf Oynatıcı"
|
||||||
|
},
|
||||||
"tuna-obs": {
|
"tuna-obs": {
|
||||||
"description": "OBS eklentisi Tuna ile entegrasyon sağlar",
|
"description": "OBS eklentisi Tuna ile entegrasyon sağlar",
|
||||||
"name": "Tuna OBS"
|
"name": "Tuna OBS"
|
||||||
|
|||||||
@ -421,6 +421,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"custom-output-device": {
|
||||||
|
"description": "Cài đặt cho thiết bị đầu ra tùy chỉnh cho bài hát",
|
||||||
|
"menu": {
|
||||||
|
"device-selector": "Chọn thiết bị"
|
||||||
|
},
|
||||||
|
"name": "Thiết bị đầu ra tùy chỉnh",
|
||||||
|
"prompt": {
|
||||||
|
"device-selector": {
|
||||||
|
"label": "Chọn thiết bị phát làm đầu ra để dùng",
|
||||||
|
"title": "Chọn thiết bị đầu ra"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"disable-autoplay": {
|
"disable-autoplay": {
|
||||||
"description": "Làm nhạc bắt đầu ở chế độ \"tạm dừng\". Ngoài ra có thể dừng nhạc khi khởi động ứng dụng (nếu có bật tính năng \"Tiếp tục bài hát cuối cùng khi ứng dụng khởi động\")",
|
"description": "Làm nhạc bắt đầu ở chế độ \"tạm dừng\". Ngoài ra có thể dừng nhạc khi khởi động ứng dụng (nếu có bật tính năng \"Tiếp tục bài hát cuối cùng khi ứng dụng khởi động\")",
|
||||||
"menu": {
|
"menu": {
|
||||||
@ -444,9 +457,17 @@
|
|||||||
"hide-duration-left": "Ẩn thời lượng còn lại",
|
"hide-duration-left": "Ẩn thời lượng còn lại",
|
||||||
"hide-github-button": "Ẩn nút liên kết GitHub",
|
"hide-github-button": "Ẩn nút liên kết GitHub",
|
||||||
"play-on-youtube-music": "Phát trong Youtube Music",
|
"play-on-youtube-music": "Phát trong Youtube Music",
|
||||||
"set-inactivity-timeout": "Đặt thời gian chờ không hoạt động"
|
"set-inactivity-timeout": "Đặt thời gian chờ không hoạt động",
|
||||||
|
"set-status-display-type": {
|
||||||
|
"label": "Văn bản trạng thái",
|
||||||
|
"submenu": {
|
||||||
|
"artist": "Đang nghe nhạc của {artist}",
|
||||||
|
"title": "Đang nghe nhạc {song title}",
|
||||||
|
"youtube-music": "Đang nghe Youtube Music"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"name": "Discord Rich Presence",
|
"name": "Tích hợp trạng thái Discord",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"set-inactivity-timeout": {
|
"set-inactivity-timeout": {
|
||||||
"label": "Nhập thời gian chờ không hoạt động tính bằng giây:",
|
"label": "Nhập thời gian chờ không hoạt động tính bằng giây:",
|
||||||
@ -736,6 +757,7 @@
|
|||||||
"listenbrainz": {
|
"listenbrainz": {
|
||||||
"token": "Nhập mã người dùng ListenBrainz"
|
"token": "Nhập mã người dùng ListenBrainz"
|
||||||
},
|
},
|
||||||
|
"scrobble-alternative-artist": "Dùng nghệ sĩ thay thế",
|
||||||
"scrobble-alternative-title": "Dùng tiêu đề thay thế",
|
"scrobble-alternative-title": "Dùng tiêu đề thay thế",
|
||||||
"scrobble-other-media": "Scrobber nội dung khác"
|
"scrobble-other-media": "Scrobber nội dung khác"
|
||||||
},
|
},
|
||||||
@ -853,6 +875,27 @@
|
|||||||
"description": "Thêm tiện ích TouchBar cho người dùng macOS",
|
"description": "Thêm tiện ích TouchBar cho người dùng macOS",
|
||||||
"name": "TouchBar"
|
"name": "TouchBar"
|
||||||
},
|
},
|
||||||
|
"transparent-player": {
|
||||||
|
"description": "Làm cho cửa sổ ứng dụng có hiệu ứng trong suốt",
|
||||||
|
"menu": {
|
||||||
|
"opacity": {
|
||||||
|
"label": "Độ mờ",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{opacity}}%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"label": "Kiểu nền",
|
||||||
|
"submenu": {
|
||||||
|
"acrylic": "Acrylic",
|
||||||
|
"mica": "Mica",
|
||||||
|
"none": "Không có",
|
||||||
|
"tabbed": "Tabbed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Trình phát trong suốt"
|
||||||
|
},
|
||||||
"tuna-obs": {
|
"tuna-obs": {
|
||||||
"description": "Tích hợp với plugin Tuna của OBS",
|
"description": "Tích hợp với plugin Tuna của OBS",
|
||||||
"name": "Tuna OBS"
|
"name": "Tuna OBS"
|
||||||
|
|||||||
@ -421,6 +421,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"custom-output-device": {
|
||||||
|
"description": "配置歌曲的自定义输出媒体设备",
|
||||||
|
"menu": {
|
||||||
|
"device-selector": "选择设备"
|
||||||
|
},
|
||||||
|
"name": "自定义输出设备",
|
||||||
|
"prompt": {
|
||||||
|
"device-selector": {
|
||||||
|
"label": "选择要使用的输出媒体设备",
|
||||||
|
"title": "选择输出设备"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"disable-autoplay": {
|
"disable-autoplay": {
|
||||||
"description": "让曲目开始时处于 “暂停” 模式",
|
"description": "让曲目开始时处于 “暂停” 模式",
|
||||||
"menu": {
|
"menu": {
|
||||||
@ -444,7 +457,15 @@
|
|||||||
"hide-duration-left": "隐藏剩余时长",
|
"hide-duration-left": "隐藏剩余时长",
|
||||||
"hide-github-button": "隐藏 GitHub 链接按钮",
|
"hide-github-button": "隐藏 GitHub 链接按钮",
|
||||||
"play-on-youtube-music": "转至 YouTube Music 播放",
|
"play-on-youtube-music": "转至 YouTube Music 播放",
|
||||||
"set-inactivity-timeout": "设置非活跃时长"
|
"set-inactivity-timeout": "设置非活跃时长",
|
||||||
|
"set-status-display-type": {
|
||||||
|
"label": "状态文本",
|
||||||
|
"submenu": {
|
||||||
|
"artist": "在听 {artist}",
|
||||||
|
"title": "在听 {song title}",
|
||||||
|
"youtube-music": "在听 YouTube Music"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"name": "Discord Rich Presence 状态显示",
|
"name": "Discord Rich Presence 状态显示",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
@ -736,6 +757,7 @@
|
|||||||
"listenbrainz": {
|
"listenbrainz": {
|
||||||
"token": "输入 ListenBrainz 用户令牌"
|
"token": "输入 ListenBrainz 用户令牌"
|
||||||
},
|
},
|
||||||
|
"scrobble-alternative-artist": "使用替代艺术家",
|
||||||
"scrobble-alternative-title": "使用替代标题",
|
"scrobble-alternative-title": "使用替代标题",
|
||||||
"scrobble-other-media": "记录其他媒体文件"
|
"scrobble-other-media": "记录其他媒体文件"
|
||||||
},
|
},
|
||||||
@ -853,6 +875,27 @@
|
|||||||
"description": "为 macOS 用户启用 TouchBar 支持",
|
"description": "为 macOS 用户启用 TouchBar 支持",
|
||||||
"name": "TouchBar"
|
"name": "TouchBar"
|
||||||
},
|
},
|
||||||
|
"transparent-player": {
|
||||||
|
"description": "把应用窗口变透明",
|
||||||
|
"menu": {
|
||||||
|
"opacity": {
|
||||||
|
"label": "不透明",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{opacity}}%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"label": "类型",
|
||||||
|
"submenu": {
|
||||||
|
"acrylic": "亚克力",
|
||||||
|
"mica": "云母",
|
||||||
|
"none": "无",
|
||||||
|
"tabbed": "标签"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "透明播放器"
|
||||||
|
},
|
||||||
"tuna-obs": {
|
"tuna-obs": {
|
||||||
"description": "与 OBS 的 Tuna 插件集成",
|
"description": "与 OBS 的 Tuna 插件集成",
|
||||||
"name": "Tuna OBS"
|
"name": "Tuna OBS"
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import { allPlugins, mainPlugins } from 'virtual:plugins';
|
|||||||
|
|
||||||
import { languageResources } from 'virtual:i18n';
|
import { languageResources } from 'virtual:i18n';
|
||||||
|
|
||||||
import config from '@/config';
|
import * as config from '@/config';
|
||||||
|
|
||||||
import { refreshMenu, setApplicationMenu } from '@/menu';
|
import { refreshMenu, setApplicationMenu } from '@/menu';
|
||||||
import { fileExists, injectCSS, injectCSSAsFile } from '@/plugins/utils/main';
|
import { fileExists, injectCSS, injectCSSAsFile } from '@/plugins/utils/main';
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { type BrowserWindow, ipcMain } from 'electron';
|
|||||||
import { deepmerge } from 'deepmerge-ts';
|
import { deepmerge } from 'deepmerge-ts';
|
||||||
import { allPlugins, mainPlugins } from 'virtual:plugins';
|
import { allPlugins, mainPlugins } from 'virtual:plugins';
|
||||||
|
|
||||||
import config from '@/config';
|
import * as config from '@/config';
|
||||||
import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
|
import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { deepmerge } from 'deepmerge-ts';
|
import { deepmerge } from 'deepmerge-ts';
|
||||||
import { allPlugins } from 'virtual:plugins';
|
import { allPlugins } from 'virtual:plugins';
|
||||||
|
|
||||||
import config from '@/config';
|
import * as config from '@/config';
|
||||||
import { setApplicationMenu } from '@/menu';
|
import { setApplicationMenu } from '@/menu';
|
||||||
|
|
||||||
import { LoggerPrefix } from '@/utils';
|
import { LoggerPrefix } from '@/utils';
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { allPlugins, preloadPlugins } from 'virtual:plugins';
|
|||||||
|
|
||||||
import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
|
import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
|
||||||
|
|
||||||
import config from '@/config';
|
import * as config from '@/config';
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { allPlugins } from 'virtual:plugins';
|
|||||||
|
|
||||||
import { languageResources } from 'virtual:i18n';
|
import { languageResources } from 'virtual:i18n';
|
||||||
|
|
||||||
import config from './config';
|
import * as config from './config';
|
||||||
|
|
||||||
import { restart } from './providers/app-controls';
|
import { restart } from './providers/app-controls';
|
||||||
import { startingPages } from './providers/extracted-data';
|
import { startingPages } from './providers/extracted-data';
|
||||||
|
|||||||
@ -35,6 +35,7 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
ctx.ipc.send('ytmd:setup-repeat-changed-listener');
|
ctx.ipc.send('ytmd:setup-repeat-changed-listener');
|
||||||
ctx.ipc.send('ytmd:setup-like-changed-listener');
|
ctx.ipc.send('ytmd:setup-like-changed-listener');
|
||||||
ctx.ipc.send('ytmd:setup-volume-changed-listener');
|
ctx.ipc.send('ytmd:setup-volume-changed-listener');
|
||||||
|
ctx.ipc.send('ytmd:setup-shuffle-changed-listener');
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.ipc.on(
|
ctx.ipc.on(
|
||||||
@ -69,6 +70,7 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
// Custom
|
// Custom
|
||||||
init(backendCtx) {
|
init(backendCtx) {
|
||||||
this.app = new Hono();
|
this.app = new Hono();
|
||||||
|
|
||||||
const ws = createNodeWebSocket({
|
const ws = createNodeWebSocket({
|
||||||
app: this.app,
|
app: this.app,
|
||||||
});
|
});
|
||||||
@ -120,7 +122,7 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
() => this.volumeState,
|
() => this.volumeState,
|
||||||
);
|
);
|
||||||
registerAuth(this.app, backendCtx);
|
registerAuth(this.app, backendCtx);
|
||||||
registerWebsocket(this.app, ws);
|
registerWebsocket(this.app, backendCtx, ws);
|
||||||
|
|
||||||
// swagger
|
// swagger
|
||||||
this.app.openAPIRegistry.registerComponent(
|
this.app.openAPIRegistry.registerComponent(
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { createRoute, z } from '@hono/zod-openapi';
|
import { createRoute, z } from '@hono/zod-openapi';
|
||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
|
|
||||||
import getSongControls from '@/providers/song-controls';
|
import { getSongControls } from '@/providers/song-controls';
|
||||||
import {
|
import {
|
||||||
LikeType,
|
LikeType,
|
||||||
type RepeatMode,
|
type RepeatMode,
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { ipcMain } from 'electron';
|
|
||||||
import { createRoute } from '@hono/zod-openapi';
|
import { createRoute } from '@hono/zod-openapi';
|
||||||
|
|
||||||
import { type NodeWebSocket } from '@hono/node-ws';
|
import { type NodeWebSocket } from '@hono/node-ws';
|
||||||
@ -15,6 +14,8 @@ import type { WSContext } from 'hono/ws';
|
|||||||
import type { Context, Next } from 'hono';
|
import type { Context, Next } from 'hono';
|
||||||
import type { RepeatMode, VolumeState } from '@/types/datahost-get-state';
|
import type { RepeatMode, VolumeState } from '@/types/datahost-get-state';
|
||||||
import type { HonoApp } from '../types';
|
import type { HonoApp } from '../types';
|
||||||
|
import type { BackendContext } from '@/types/contexts';
|
||||||
|
import type { APIServerConfig } from '@/plugins/api-server/config';
|
||||||
|
|
||||||
enum DataTypes {
|
enum DataTypes {
|
||||||
PlayerInfo = 'PLAYER_INFO',
|
PlayerInfo = 'PLAYER_INFO',
|
||||||
@ -23,6 +24,7 @@ enum DataTypes {
|
|||||||
PositionChanged = 'POSITION_CHANGED',
|
PositionChanged = 'POSITION_CHANGED',
|
||||||
VolumeChanged = 'VOLUME_CHANGED',
|
VolumeChanged = 'VOLUME_CHANGED',
|
||||||
RepeatChanged = 'REPEAT_CHANGED',
|
RepeatChanged = 'REPEAT_CHANGED',
|
||||||
|
ShuffleChanged = 'SHUFFLE_CHANGED',
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlayerState = {
|
type PlayerState = {
|
||||||
@ -32,11 +34,17 @@ type PlayerState = {
|
|||||||
position: number;
|
position: number;
|
||||||
volume: number;
|
volume: number;
|
||||||
repeat: RepeatMode;
|
repeat: RepeatMode;
|
||||||
|
shuffle: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const register = (app: HonoApp, nodeWebSocket: NodeWebSocket) => {
|
export const register = (
|
||||||
|
app: HonoApp,
|
||||||
|
{ ipc }: BackendContext<APIServerConfig>,
|
||||||
|
{ upgradeWebSocket }: NodeWebSocket,
|
||||||
|
) => {
|
||||||
let volumeState: VolumeState | undefined = undefined;
|
let volumeState: VolumeState | undefined = undefined;
|
||||||
let repeat: RepeatMode = 'NONE';
|
let repeat: RepeatMode = 'NONE';
|
||||||
|
let shuffle = false;
|
||||||
let lastSongInfo: SongInfo | undefined = undefined;
|
let lastSongInfo: SongInfo | undefined = undefined;
|
||||||
|
|
||||||
const sockets = new Set<WSContext<WebSocket>>();
|
const sockets = new Set<WSContext<WebSocket>>();
|
||||||
@ -51,10 +59,12 @@ export const register = (app: HonoApp, nodeWebSocket: NodeWebSocket) => {
|
|||||||
songInfo,
|
songInfo,
|
||||||
volumeState,
|
volumeState,
|
||||||
repeat,
|
repeat,
|
||||||
|
shuffle,
|
||||||
}: {
|
}: {
|
||||||
songInfo?: SongInfo;
|
songInfo?: SongInfo;
|
||||||
volumeState?: VolumeState;
|
volumeState?: VolumeState;
|
||||||
repeat: RepeatMode;
|
repeat: RepeatMode;
|
||||||
|
shuffle: boolean;
|
||||||
}): PlayerState => ({
|
}): PlayerState => ({
|
||||||
song: songInfo,
|
song: songInfo,
|
||||||
isPlaying: songInfo ? !songInfo.isPaused : false,
|
isPlaying: songInfo ? !songInfo.isPaused : false,
|
||||||
@ -62,6 +72,7 @@ export const register = (app: HonoApp, nodeWebSocket: NodeWebSocket) => {
|
|||||||
position: songInfo?.elapsedSeconds ?? 0,
|
position: songInfo?.elapsedSeconds ?? 0,
|
||||||
volume: volumeState?.state ?? 100,
|
volume: volumeState?.state ?? 100,
|
||||||
repeat,
|
repeat,
|
||||||
|
shuffle,
|
||||||
});
|
});
|
||||||
|
|
||||||
registerCallback((songInfo, event) => {
|
registerCallback((songInfo, event) => {
|
||||||
@ -83,7 +94,7 @@ export const register = (app: HonoApp, nodeWebSocket: NodeWebSocket) => {
|
|||||||
lastSongInfo = { ...songInfo };
|
lastSongInfo = { ...songInfo };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('ytmd:volume-changed', (_, newVolumeState: VolumeState) => {
|
ipc.on('ytmd:volume-changed', (newVolumeState: VolumeState) => {
|
||||||
volumeState = newVolumeState;
|
volumeState = newVolumeState;
|
||||||
send(DataTypes.VolumeChanged, {
|
send(DataTypes.VolumeChanged, {
|
||||||
volume: volumeState.state,
|
volume: volumeState.state,
|
||||||
@ -91,15 +102,20 @@ export const register = (app: HonoApp, nodeWebSocket: NodeWebSocket) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('ytmd:repeat-changed', (_, mode: RepeatMode) => {
|
ipc.on('ytmd:repeat-changed', (mode: RepeatMode) => {
|
||||||
repeat = mode;
|
repeat = mode;
|
||||||
send(DataTypes.RepeatChanged, { repeat });
|
send(DataTypes.RepeatChanged, { repeat });
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('ytmd:seeked', (_, t: number) => {
|
ipc.on('ytmd:seeked', (t: number) => {
|
||||||
send(DataTypes.PositionChanged, { position: t });
|
send(DataTypes.PositionChanged, { position: t });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipc.on('ytmd:shuffle-changed', (newShuffle: boolean) => {
|
||||||
|
shuffle = newShuffle;
|
||||||
|
send(DataTypes.ShuffleChanged, { shuffle });
|
||||||
|
});
|
||||||
|
|
||||||
app.openapi(
|
app.openapi(
|
||||||
createRoute({
|
createRoute({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@ -112,7 +128,7 @@ export const register = (app: HonoApp, nodeWebSocket: NodeWebSocket) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
nodeWebSocket.upgradeWebSocket(() => ({
|
upgradeWebSocket(() => ({
|
||||||
onOpen(_, ws) {
|
onOpen(_, ws) {
|
||||||
// "Unsafe argument of type `WSContext<WebSocket>` assigned to a parameter of type `WSContext<WebSocket>`. (@typescript-eslint/no-unsafe-argument)" ????? what?
|
// "Unsafe argument of type `WSContext<WebSocket>` assigned to a parameter of type `WSContext<WebSocket>`. (@typescript-eslint/no-unsafe-argument)" ????? what?
|
||||||
sockets.add(ws as WSContext<WebSocket>);
|
sockets.add(ws as WSContext<WebSocket>);
|
||||||
@ -124,6 +140,7 @@ export const register = (app: HonoApp, nodeWebSocket: NodeWebSocket) => {
|
|||||||
songInfo: lastSongInfo,
|
songInfo: lastSongInfo,
|
||||||
volumeState,
|
volumeState,
|
||||||
repeat,
|
repeat,
|
||||||
|
shuffle,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
import net from 'net';
|
import * as net from 'node:net';
|
||||||
|
|
||||||
import { SocksClient, type SocksClientOptions } from 'socks';
|
import { SocksClient, type SocksClientOptions } from 'socks';
|
||||||
|
|
||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
|
|
||||||
import { createBackend, LoggerPrefix } from '@/utils';
|
import { createBackend, LoggerPrefix } from '@/utils';
|
||||||
|
|
||||||
import { type BackendType } from './types';
|
import * as config from '@/config';
|
||||||
|
|
||||||
import config from '@/config';
|
|
||||||
|
|
||||||
import { type AuthProxyConfig, defaultAuthProxyConfig } from '../config';
|
import { type AuthProxyConfig, defaultAuthProxyConfig } from '../config';
|
||||||
|
|
||||||
|
import type { BackendType } from './types';
|
||||||
import type { BackendContext } from '@/types/contexts';
|
import type { BackendContext } from '@/types/contexts';
|
||||||
|
|
||||||
// Parsing the upstream authentication SOCK proxy URL
|
// Parsing the upstream authentication SOCK proxy URL
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { createSignal } from 'solid-js';
|
|||||||
|
|
||||||
import { render } from 'solid-js/web';
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
import defaultConfig from '@/config/defaults';
|
import { defaultConfig } from '@/config/defaults';
|
||||||
import { getSongMenu } from '@/providers/dom-elements';
|
import { getSongMenu } from '@/providers/dom-elements';
|
||||||
import { getSongInfo } from '@/providers/song-info-front';
|
import { getSongInfo } from '@/providers/song-info-front';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import previousIcon from '@assets/media-icons-black/previous.png?asset&asarUnpac
|
|||||||
|
|
||||||
import { notificationImage, secondsToMinutes, ToastStyles } from './utils';
|
import { notificationImage, secondsToMinutes, ToastStyles } from './utils';
|
||||||
|
|
||||||
import getSongControls from '@/providers/song-controls';
|
import { getSongControls } from '@/providers/song-controls';
|
||||||
import {
|
import {
|
||||||
registerCallback,
|
registerCallback,
|
||||||
type SongInfo,
|
type SongInfo,
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import { type BrowserWindow, globalShortcut } from 'electron';
|
|||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
import { register as registerElectronLocalShortcut } from 'electron-localshortcut';
|
import { register as registerElectronLocalShortcut } from 'electron-localshortcut';
|
||||||
|
|
||||||
import registerMPRIS from './mpris';
|
import { registerMPRIS } from './mpris';
|
||||||
import getSongControls from '@/providers/song-controls';
|
import { getSongControls } from '@/providers/song-controls';
|
||||||
|
|
||||||
import type { ShortcutMappingType, ShortcutsPluginConfig } from './index';
|
import type { ShortcutMappingType, ShortcutsPluginConfig } from './index';
|
||||||
|
|
||||||
|
|||||||
@ -19,8 +19,8 @@ import {
|
|||||||
type SongInfo,
|
type SongInfo,
|
||||||
SongInfoEvent,
|
SongInfoEvent,
|
||||||
} from '@/providers/song-info';
|
} from '@/providers/song-info';
|
||||||
import getSongControls from '@/providers/song-controls';
|
import { getSongControls } from '@/providers/song-controls';
|
||||||
import config from '@/config';
|
import * as config from '@/config';
|
||||||
import { LoggerPrefix } from '@/utils';
|
import { LoggerPrefix } from '@/utils';
|
||||||
|
|
||||||
import type { RepeatMode, VolumeState } from '@/types/datahost-get-state';
|
import type { RepeatMode, VolumeState } from '@/types/datahost-get-state';
|
||||||
@ -84,7 +84,7 @@ function setupMPRIS() {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerMPRIS(win: BrowserWindow) {
|
export function registerMPRIS(win: BrowserWindow) {
|
||||||
const songControls = getSongControls(win);
|
const songControls = getSongControls(win);
|
||||||
const {
|
const {
|
||||||
playPause,
|
playPause,
|
||||||
@ -363,5 +363,3 @@ function registerMPRIS(win: BrowserWindow) {
|
|||||||
console.trace(error);
|
console.trace(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default registerMPRIS;
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import { providerNames } from './providers';
|
||||||
|
|
||||||
import type { MenuItemConstructorOptions } from 'electron';
|
import type { MenuItemConstructorOptions } from 'electron';
|
||||||
import type { MenuContext } from '@/types/contexts';
|
import type { MenuContext } from '@/types/contexts';
|
||||||
import type { SyncedLyricsPluginConfig } from './types';
|
import type { SyncedLyricsPluginConfig } from './types';
|
||||||
@ -10,6 +12,35 @@ export const menu = async (
|
|||||||
const config = await ctx.getConfig();
|
const config = await ctx.getConfig();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
label: t('plugins.synced-lyrics.menu.preferred-provider.label'),
|
||||||
|
toolTip: t('plugins.synced-lyrics.menu.preferred-provider.tooltip'),
|
||||||
|
type: 'submenu',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: t('plugins.synced-lyrics.menu.preferred-provider.none.label'),
|
||||||
|
toolTip: t(
|
||||||
|
'plugins.synced-lyrics.menu.preferred-provider.none.tooltip',
|
||||||
|
),
|
||||||
|
type: 'radio',
|
||||||
|
checked: config.preferredProvider === undefined,
|
||||||
|
click() {
|
||||||
|
ctx.setConfig({ preferredProvider: undefined });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...providerNames.map(
|
||||||
|
(provider) =>
|
||||||
|
({
|
||||||
|
label: provider,
|
||||||
|
type: 'radio',
|
||||||
|
checked: config.preferredProvider === provider,
|
||||||
|
click() {
|
||||||
|
ctx.setConfig({ preferredProvider: provider });
|
||||||
|
},
|
||||||
|
}) as const,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t('plugins.synced-lyrics.menu.precise-timing.label'),
|
label: t('plugins.synced-lyrics.menu.precise-timing.label'),
|
||||||
toolTip: t('plugins.synced-lyrics.menu.precise-timing.tooltip'),
|
toolTip: t('plugins.synced-lyrics.menu.precise-timing.tooltip'),
|
||||||
@ -93,47 +124,23 @@ export const menu = async (
|
|||||||
toolTip: t('plugins.synced-lyrics.menu.default-text-string.tooltip'),
|
toolTip: t('plugins.synced-lyrics.menu.default-text-string.tooltip'),
|
||||||
type: 'submenu',
|
type: 'submenu',
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{ label: '♪', value: '♪' },
|
||||||
label: '♪',
|
{ label: '" "', value: ' ' },
|
||||||
type: 'radio',
|
{ label: '...', value: ['.', '..', '...'] },
|
||||||
checked: config.defaultTextString === '♪',
|
{ label: '•••', value: ['•', '••', '•••'] },
|
||||||
click() {
|
{ label: '———', value: '———' },
|
||||||
ctx.setConfig({
|
].map(({ label, value }) => ({
|
||||||
defaultTextString: '♪',
|
label,
|
||||||
});
|
type: 'radio',
|
||||||
},
|
checked:
|
||||||
|
typeof value === 'string'
|
||||||
|
? config.defaultTextString === value
|
||||||
|
: JSON.stringify(config.defaultTextString) ===
|
||||||
|
JSON.stringify(value),
|
||||||
|
click() {
|
||||||
|
ctx.setConfig({ defaultTextString: value });
|
||||||
},
|
},
|
||||||
{
|
})),
|
||||||
label: '" "',
|
|
||||||
type: 'radio',
|
|
||||||
checked: config.defaultTextString === ' ',
|
|
||||||
click() {
|
|
||||||
ctx.setConfig({
|
|
||||||
defaultTextString: ' ',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '...',
|
|
||||||
type: 'radio',
|
|
||||||
checked: config.defaultTextString === '...',
|
|
||||||
click() {
|
|
||||||
ctx.setConfig({
|
|
||||||
defaultTextString: '...',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '———',
|
|
||||||
type: 'radio',
|
|
||||||
checked: config.defaultTextString === '———',
|
|
||||||
click() {
|
|
||||||
ctx.setConfig({
|
|
||||||
defaultTextString: '———',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('plugins.synced-lyrics.menu.romanization.label'),
|
label: t('plugins.synced-lyrics.menu.romanization.label'),
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
import { LRC } from '../parsers/lrc';
|
import { LRC } from '../parsers/lrc';
|
||||||
import { netFetch } from '../renderer';
|
import { netFetch } from '../renderer';
|
||||||
|
|||||||
@ -1,191 +1,21 @@
|
|||||||
import { createStore } from 'solid-js/store';
|
import * as z from 'zod';
|
||||||
|
|
||||||
import { createMemo } from 'solid-js';
|
import type { LyricResult } from '../types';
|
||||||
|
|
||||||
import { LRCLib } from './LRCLib';
|
export enum ProviderNames {
|
||||||
import { LyricsGenius } from './LyricsGenius';
|
YTMusic = 'YTMusic',
|
||||||
import { MusixMatch } from './MusixMatch';
|
LRCLib = 'LRCLib',
|
||||||
import { YTMusic } from './YTMusic';
|
MusixMatch = 'MusixMatch',
|
||||||
|
LyricsGenius = 'LyricsGenius',
|
||||||
|
// Megalobiz = 'Megalobiz',
|
||||||
|
}
|
||||||
|
|
||||||
import { getSongInfo } from '@/providers/song-info-front';
|
export const ProviderNameSchema = z.enum(ProviderNames);
|
||||||
|
export type ProviderName = z.infer<typeof ProviderNameSchema>;
|
||||||
import type { LyricProvider, LyricResult } from '../types';
|
export const providerNames = ProviderNameSchema.options;
|
||||||
import type { SongInfo } from '@/providers/song-info';
|
|
||||||
|
|
||||||
export const providers = {
|
|
||||||
YTMusic: new YTMusic(),
|
|
||||||
LRCLib: new LRCLib(),
|
|
||||||
MusixMatch: new MusixMatch(),
|
|
||||||
LyricsGenius: new LyricsGenius(),
|
|
||||||
// Megalobiz: new Megalobiz(), // Disabled because it is too unstable and slow
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type ProviderName = keyof typeof providers;
|
|
||||||
export const providerNames = Object.keys(providers) as ProviderName[];
|
|
||||||
|
|
||||||
export type ProviderState = {
|
export type ProviderState = {
|
||||||
state: 'fetching' | 'done' | 'error';
|
state: 'fetching' | 'done' | 'error';
|
||||||
data: LyricResult | null;
|
data: LyricResult | null;
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LyricsStore = {
|
|
||||||
provider: ProviderName;
|
|
||||||
current: ProviderState;
|
|
||||||
lyrics: Record<ProviderName, ProviderState>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialData = () =>
|
|
||||||
providerNames.reduce(
|
|
||||||
(acc, name) => {
|
|
||||||
acc[name] = { state: 'fetching', data: null, error: null };
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as LyricsStore['lyrics'],
|
|
||||||
);
|
|
||||||
|
|
||||||
export const [lyricsStore, setLyricsStore] = createStore<LyricsStore>({
|
|
||||||
provider: providerNames[0],
|
|
||||||
lyrics: initialData(),
|
|
||||||
get current(): ProviderState {
|
|
||||||
return this.lyrics[this.provider];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const currentLyrics = createMemo(() => {
|
|
||||||
const provider = lyricsStore.provider;
|
|
||||||
return lyricsStore.lyrics[provider];
|
|
||||||
});
|
|
||||||
|
|
||||||
type VideoId = string;
|
|
||||||
|
|
||||||
type SearchCacheData = Record<ProviderName, ProviderState>;
|
|
||||||
interface SearchCache {
|
|
||||||
state: 'loading' | 'done';
|
|
||||||
data: SearchCacheData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Maybe use localStorage for the cache.
|
|
||||||
const searchCache = new Map<VideoId, SearchCache>();
|
|
||||||
export const fetchLyrics = (info: SongInfo) => {
|
|
||||||
if (searchCache.has(info.videoId)) {
|
|
||||||
const cache = searchCache.get(info.videoId)!;
|
|
||||||
|
|
||||||
if (cache.state === 'loading') {
|
|
||||||
setTimeout(() => {
|
|
||||||
fetchLyrics(info);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getSongInfo().videoId === info.videoId) {
|
|
||||||
setLyricsStore('lyrics', () => {
|
|
||||||
// weird bug with solid-js
|
|
||||||
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cache: SearchCache = {
|
|
||||||
state: 'loading',
|
|
||||||
data: initialData(),
|
|
||||||
};
|
|
||||||
|
|
||||||
searchCache.set(info.videoId, cache);
|
|
||||||
if (getSongInfo().videoId === info.videoId) {
|
|
||||||
setLyricsStore('lyrics', () => {
|
|
||||||
// weird bug with solid-js
|
|
||||||
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const tasks: Promise<void>[] = [];
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
for (
|
|
||||||
const [providerName, provider] of Object.entries(providers) as [
|
|
||||||
ProviderName,
|
|
||||||
LyricProvider,
|
|
||||||
][]
|
|
||||||
) {
|
|
||||||
const pCache = cache.data[providerName];
|
|
||||||
|
|
||||||
tasks.push(
|
|
||||||
provider
|
|
||||||
.search(info)
|
|
||||||
.then((res) => {
|
|
||||||
pCache.state = 'done';
|
|
||||||
pCache.data = res;
|
|
||||||
|
|
||||||
if (getSongInfo().videoId === info.videoId) {
|
|
||||||
setLyricsStore('lyrics', (old) => {
|
|
||||||
return {
|
|
||||||
...old,
|
|
||||||
[providerName]: {
|
|
||||||
state: 'done',
|
|
||||||
data: res ? { ...res } : null,
|
|
||||||
error: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error: Error) => {
|
|
||||||
pCache.state = 'error';
|
|
||||||
pCache.error = error;
|
|
||||||
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
if (getSongInfo().videoId === info.videoId) {
|
|
||||||
setLyricsStore('lyrics', (old) => {
|
|
||||||
return {
|
|
||||||
...old,
|
|
||||||
[providerName]: { state: 'error', error, data: null },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.allSettled(tasks).then(() => {
|
|
||||||
cache.state = 'done';
|
|
||||||
searchCache.set(info.videoId, cache);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const retrySearch = (provider: ProviderName, info: SongInfo) => {
|
|
||||||
setLyricsStore('lyrics', (old) => {
|
|
||||||
const pCache = {
|
|
||||||
state: 'fetching',
|
|
||||||
data: null,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...old,
|
|
||||||
[provider]: pCache,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
providers[provider]
|
|
||||||
.search(info)
|
|
||||||
.then((res) => {
|
|
||||||
setLyricsStore('lyrics', (old) => {
|
|
||||||
return {
|
|
||||||
...old,
|
|
||||||
[provider]: { state: 'done', data: res, error: null },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setLyricsStore('lyrics', (old) => {
|
|
||||||
return {
|
|
||||||
...old,
|
|
||||||
[provider]: { state: 'error', data: null, error },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
13
src/plugins/synced-lyrics/providers/renderer.ts
Normal file
13
src/plugins/synced-lyrics/providers/renderer.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ProviderNames } from './index';
|
||||||
|
import { YTMusic } from './YTMusic';
|
||||||
|
import { LRCLib } from './LRCLib';
|
||||||
|
import { MusixMatch } from './MusixMatch';
|
||||||
|
import { LyricsGenius } from './LyricsGenius';
|
||||||
|
|
||||||
|
export const providers = {
|
||||||
|
[ProviderNames.YTMusic]: new YTMusic(),
|
||||||
|
[ProviderNames.LRCLib]: new LRCLib(),
|
||||||
|
[ProviderNames.MusixMatch]: new MusixMatch(),
|
||||||
|
[ProviderNames.LyricsGenius]: new LyricsGenius(),
|
||||||
|
// [ProviderNames.Megalobiz]: new Megalobiz(), // Disabled because it is too unstable and slow
|
||||||
|
} as const;
|
||||||
@ -2,7 +2,7 @@ import { t } from '@/i18n';
|
|||||||
|
|
||||||
import { getSongInfo } from '@/providers/song-info-front';
|
import { getSongInfo } from '@/providers/song-info-front';
|
||||||
|
|
||||||
import { lyricsStore, retrySearch } from '../../providers';
|
import { lyricsStore, retrySearch } from '../store';
|
||||||
|
|
||||||
interface ErrorDisplayProps {
|
interface ErrorDisplayProps {
|
||||||
error: Error;
|
error: Error;
|
||||||
|
|||||||
@ -11,18 +11,24 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
} from 'solid-js';
|
} from 'solid-js';
|
||||||
|
|
||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
currentLyrics,
|
|
||||||
lyricsStore,
|
|
||||||
type ProviderName,
|
type ProviderName,
|
||||||
providerNames,
|
providerNames,
|
||||||
|
ProviderNameSchema,
|
||||||
type ProviderState,
|
type ProviderState,
|
||||||
setLyricsStore,
|
|
||||||
} from '../../providers';
|
} from '../../providers';
|
||||||
|
import { currentLyrics, lyricsStore, setLyricsStore } from '../store';
|
||||||
import { _ytAPI } from '../index';
|
import { _ytAPI } from '../index';
|
||||||
|
import { config } from '../renderer';
|
||||||
|
|
||||||
import type { YtIcons } from '@/types/icons';
|
import type { YtIcons } from '@/types/icons';
|
||||||
|
import type { PlayerAPIEvents } from '@/types/player-api-events';
|
||||||
|
|
||||||
|
const LocalStorageSchema = z.object({
|
||||||
|
provider: ProviderNameSchema,
|
||||||
|
});
|
||||||
|
|
||||||
export const providerIdx = createMemo(() =>
|
export const providerIdx = createMemo(() =>
|
||||||
providerNames.indexOf(lyricsStore.provider),
|
providerNames.indexOf(lyricsStore.provider),
|
||||||
@ -45,11 +51,19 @@ const providerBias = (p: ProviderName) =>
|
|||||||
(lyricsStore.lyrics[p].data?.lyrics ? 1 : -1);
|
(lyricsStore.lyrics[p].data?.lyrics ? 1 : -1);
|
||||||
|
|
||||||
const pickBestProvider = () => {
|
const pickBestProvider = () => {
|
||||||
|
const preferred = config()?.preferredProvider;
|
||||||
|
if (preferred) {
|
||||||
|
const data = lyricsStore.lyrics[preferred].data;
|
||||||
|
if (Array.isArray(data?.lines) || data?.lyrics) {
|
||||||
|
return { provider: preferred, force: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const providers = Array.from(providerNames);
|
const providers = Array.from(providerNames);
|
||||||
|
|
||||||
providers.sort((a, b) => providerBias(b) - providerBias(a));
|
providers.sort((a, b) => providerBias(b) - providerBias(a));
|
||||||
|
|
||||||
return providers[0];
|
return { provider: providers[0], force: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
const [hasManuallySwitchedProvider, setHasManuallySwitchedProvider] =
|
const [hasManuallySwitchedProvider, setHasManuallySwitchedProvider] =
|
||||||
@ -58,34 +72,91 @@ const [hasManuallySwitchedProvider, setHasManuallySwitchedProvider] =
|
|||||||
export const LyricsPicker = (props: {
|
export const LyricsPicker = (props: {
|
||||||
setStickRef: Setter<HTMLElement | null>;
|
setStickRef: Setter<HTMLElement | null>;
|
||||||
}) => {
|
}) => {
|
||||||
|
const [videoId, setVideoId] = createSignal<string | null>(null);
|
||||||
|
const [starredProvider, setStarredProvider] =
|
||||||
|
createSignal<ProviderName | null>(null);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const id = videoId();
|
||||||
|
if (id === null) {
|
||||||
|
setStarredProvider(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `ytmd-sl-starred-${id}`;
|
||||||
|
const value = localStorage.getItem(key);
|
||||||
|
if (!value) {
|
||||||
|
setStarredProvider(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseResult = LocalStorageSchema.safeParse(JSON.parse(value));
|
||||||
|
if (parseResult.success) {
|
||||||
|
setLyricsStore('provider', parseResult.data.provider);
|
||||||
|
setStarredProvider(parseResult.data.provider);
|
||||||
|
} else {
|
||||||
|
setStarredProvider(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleStar = () => {
|
||||||
|
const id = videoId();
|
||||||
|
if (id === null) return;
|
||||||
|
|
||||||
|
const key = `ytmd-sl-starred-${id}`;
|
||||||
|
|
||||||
|
setStarredProvider((starredProvider) => {
|
||||||
|
if (lyricsStore.provider === starredProvider) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = lyricsStore.provider;
|
||||||
|
localStorage.setItem(key, JSON.stringify({ provider }));
|
||||||
|
|
||||||
|
return provider;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const videoDataChangeHandler = (
|
||||||
|
name: string,
|
||||||
|
{ videoId }: PlayerAPIEvents['videodatachange']['value'],
|
||||||
|
) => {
|
||||||
|
setVideoId(videoId);
|
||||||
|
|
||||||
|
if (name !== 'dataloaded') return;
|
||||||
|
setHasManuallySwitchedProvider(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
{
|
||||||
|
onMount(() => _ytAPI?.addEventListener('videodatachange', videoDataChangeHandler));
|
||||||
|
onCleanup(() => _ytAPI?.removeEventListener('videodatachange', videoDataChangeHandler));
|
||||||
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
// fallback to the next source, if the current one has an error
|
|
||||||
if (!hasManuallySwitchedProvider()) {
|
if (!hasManuallySwitchedProvider()) {
|
||||||
const bestProvider = pickBestProvider();
|
const starred = starredProvider();
|
||||||
|
if (starred !== null) {
|
||||||
|
setLyricsStore('provider', starred);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const allProvidersFailed = providerNames.every((p) =>
|
const allProvidersFailed = providerNames.every((p) =>
|
||||||
shouldSwitchProvider(lyricsStore.lyrics[p]),
|
shouldSwitchProvider(lyricsStore.lyrics[p]),
|
||||||
);
|
);
|
||||||
if (allProvidersFailed) return;
|
if (allProvidersFailed) return;
|
||||||
|
|
||||||
if (providerBias(lyricsStore.provider) < providerBias(bestProvider)) {
|
const { provider, force } = pickBestProvider();
|
||||||
setLyricsStore('provider', bestProvider);
|
if (
|
||||||
|
force ||
|
||||||
|
providerBias(lyricsStore.provider) < providerBias(provider)
|
||||||
|
) {
|
||||||
|
setLyricsStore('provider', provider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const videoDataChangeHandler = (name: string) => {
|
|
||||||
if (name !== 'dataloaded') return;
|
|
||||||
setHasManuallySwitchedProvider(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
_ytAPI?.addEventListener('videodatachange', videoDataChangeHandler);
|
|
||||||
onCleanup(() =>
|
|
||||||
_ytAPI?.removeEventListener('videodatachange', videoDataChangeHandler),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
setHasManuallySwitchedProvider(true);
|
setHasManuallySwitchedProvider(true);
|
||||||
setLyricsStore('provider', (prevProvider) => {
|
setLyricsStore('provider', (prevProvider) => {
|
||||||
@ -176,9 +247,9 @@ export const LyricsPicker = (props: {
|
|||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={currentLyrics().state === 'error'}>
|
<Match when={currentLyrics().state === 'error'}>
|
||||||
<yt-icon-button
|
<yt-icon
|
||||||
icon={errorIcon}
|
icon={errorIcon}
|
||||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
style={{ padding: '5px', transform: 'scale(0.8)' }}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
@ -189,9 +260,9 @@ export const LyricsPicker = (props: {
|
|||||||
currentLyrics().data?.lyrics)
|
currentLyrics().data?.lyrics)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<yt-icon-button
|
<yt-icon
|
||||||
icon={successIcon}
|
icon={successIcon}
|
||||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
style={{ padding: '5px', transform: 'scale(0.8)' }}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
@ -202,9 +273,9 @@ export const LyricsPicker = (props: {
|
|||||||
!currentLyrics().data?.lyrics
|
!currentLyrics().data?.lyrics
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<yt-icon-button
|
<yt-icon
|
||||||
icon={notFoundIcon}
|
icon={notFoundIcon}
|
||||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
style={{ padding: '5px', transform: 'scale(0.8)' }}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
@ -213,6 +284,20 @@ export const LyricsPicker = (props: {
|
|||||||
class="description ytmusic-description-shelf-renderer"
|
class="description ytmusic-description-shelf-renderer"
|
||||||
text={{ runs: [{ text: provider() }] }}
|
text={{ runs: [{ text: provider() }] }}
|
||||||
/>
|
/>
|
||||||
|
<yt-icon
|
||||||
|
icon={
|
||||||
|
starredProvider() === provider()
|
||||||
|
? 'yt-sys-icons:star-filled'
|
||||||
|
: 'yt-sys-icons:star'
|
||||||
|
}
|
||||||
|
onClick={toggleStar}
|
||||||
|
style={{
|
||||||
|
padding: '5px',
|
||||||
|
transform: 'scale(0.8)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
tabindex="-1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Index>
|
</Index>
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { createEffect, createMemo, For, Show, createSignal } from 'solid-js';
|
import { createEffect, For, Show, createSignal, createMemo } from 'solid-js';
|
||||||
|
|
||||||
import { type VirtualizerHandle } from 'virtua/solid';
|
import { type VirtualizerHandle } from 'virtua/solid';
|
||||||
|
|
||||||
import { type LineLyrics } from '@/plugins/synced-lyrics/types';
|
import { type LineLyrics } from '@/plugins/synced-lyrics/types';
|
||||||
|
|
||||||
import { config } from '../renderer';
|
import { config, currentTime } from '../renderer';
|
||||||
import { _ytAPI } from '..';
|
import { _ytAPI } from '..';
|
||||||
|
|
||||||
import { canonicalize, romanize, simplifyUnicode } from '../utils';
|
import { canonicalize, romanize, simplifyUnicode } from '../utils';
|
||||||
@ -17,37 +17,84 @@ interface SyncedLineProps {
|
|||||||
status: 'upcoming' | 'current' | 'previous';
|
status: 'upcoming' | 'current' | 'previous';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SyncedLine = (props: SyncedLineProps) => {
|
const EmptyLine = (props: SyncedLineProps) => {
|
||||||
const text = createMemo(() => {
|
const states = createMemo(() => {
|
||||||
if (!props.line.text.trim()) {
|
const defaultText = config()?.defaultTextString ?? '';
|
||||||
return config()?.defaultTextString ?? '';
|
return Array.isArray(defaultText) ? defaultText : [defaultText];
|
||||||
}
|
|
||||||
|
|
||||||
return props.line.text;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [romanization, setRomanization] = createSignal('');
|
const index = createMemo(() => {
|
||||||
|
const progress = currentTime() - props.line.timeInMs;
|
||||||
|
const total = props.line.duration;
|
||||||
|
|
||||||
|
const percentage = Math.min(1, progress / total);
|
||||||
|
return Math.max(0, Math.floor((states().length - 1) * percentage));
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={`synced-line ${props.status}`}
|
||||||
|
onClick={() => {
|
||||||
|
_ytAPI?.seekTo((props.line.timeInMs + 10) / 1000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="description ytmusic-description-shelf-renderer" dir="auto">
|
||||||
|
<yt-formatted-string
|
||||||
|
text={{
|
||||||
|
runs: [
|
||||||
|
{
|
||||||
|
text: config()?.showTimeCodes ? `[${props.line.time}] ` : '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="text-lyrics">
|
||||||
|
<span>
|
||||||
|
<span>
|
||||||
|
<Show
|
||||||
|
fallback={
|
||||||
|
<yt-formatted-string
|
||||||
|
text={{ runs: [{ text: states()[0] }] }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
when={states().length > 1}
|
||||||
|
>
|
||||||
|
<yt-formatted-string
|
||||||
|
text={{
|
||||||
|
runs: [
|
||||||
|
{
|
||||||
|
text: states().at(
|
||||||
|
props.status === 'current' ? index() : -1,
|
||||||
|
)!,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SyncedLine = (props: SyncedLineProps) => {
|
||||||
|
const text = createMemo(() => props.line.text.trim());
|
||||||
|
|
||||||
|
const [romanization, setRomanization] = createSignal('');
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
const input = canonicalize(text());
|
||||||
if (!config()?.romanization) return;
|
if (!config()?.romanization) return;
|
||||||
|
|
||||||
const input = canonicalize(text());
|
|
||||||
romanize(input).then((result) => {
|
romanize(input).then((result) => {
|
||||||
setRomanization(canonicalize(result));
|
setRomanization(canonicalize(result));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show fallback={<EmptyLine {...props} />} when={text()}>
|
||||||
fallback={
|
|
||||||
<yt-formatted-string
|
|
||||||
text={{
|
|
||||||
runs: [{ text: '' }],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
when={text()}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class={`synced-line ${props.status}`}
|
class={`synced-line ${props.status}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@ -3,8 +3,7 @@ import { waitForElement } from '@/utils/wait-for-element';
|
|||||||
|
|
||||||
import { selectors, tabStates } from './utils';
|
import { selectors, tabStates } from './utils';
|
||||||
import { setConfig, setCurrentTime } from './renderer';
|
import { setConfig, setCurrentTime } from './renderer';
|
||||||
|
import { fetchLyrics } from './store';
|
||||||
import { fetchLyrics } from '../providers';
|
|
||||||
|
|
||||||
import type { RendererContext } from '@/types/contexts';
|
import type { RendererContext } from '@/types/contexts';
|
||||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import {
|
|||||||
PlainLyrics,
|
PlainLyrics,
|
||||||
} from './components';
|
} from './components';
|
||||||
|
|
||||||
import { currentLyrics } from '../providers';
|
import { currentLyrics } from './store';
|
||||||
|
|
||||||
import type { LineLyrics, SyncedLyricsPluginConfig } from '../types';
|
import type { LineLyrics, SyncedLyricsPluginConfig } from '../types';
|
||||||
|
|
||||||
|
|||||||
175
src/plugins/synced-lyrics/renderer/store.ts
Normal file
175
src/plugins/synced-lyrics/renderer/store.ts
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import { createStore } from 'solid-js/store';
|
||||||
|
import { createMemo } from 'solid-js';
|
||||||
|
|
||||||
|
import { getSongInfo } from '@/providers/song-info-front';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ProviderName,
|
||||||
|
providerNames,
|
||||||
|
type ProviderState,
|
||||||
|
} from '../providers';
|
||||||
|
import { providers } from '../providers/renderer';
|
||||||
|
|
||||||
|
import type { LyricProvider } from '../types';
|
||||||
|
import type { SongInfo } from '@/providers/song-info';
|
||||||
|
|
||||||
|
type LyricsStore = {
|
||||||
|
provider: ProviderName;
|
||||||
|
current: ProviderState;
|
||||||
|
lyrics: Record<ProviderName, ProviderState>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialData = () =>
|
||||||
|
providerNames.reduce(
|
||||||
|
(acc, name) => {
|
||||||
|
acc[name] = { state: 'fetching', data: null, error: null };
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as LyricsStore['lyrics'],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const [lyricsStore, setLyricsStore] = createStore<LyricsStore>({
|
||||||
|
provider: providerNames[0],
|
||||||
|
lyrics: initialData(),
|
||||||
|
get current(): ProviderState {
|
||||||
|
return this.lyrics[this.provider];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const currentLyrics = createMemo(() => {
|
||||||
|
const provider = lyricsStore.provider;
|
||||||
|
return lyricsStore.lyrics[provider];
|
||||||
|
});
|
||||||
|
|
||||||
|
type VideoId = string;
|
||||||
|
|
||||||
|
type SearchCacheData = Record<ProviderName, ProviderState>;
|
||||||
|
interface SearchCache {
|
||||||
|
state: 'loading' | 'done';
|
||||||
|
data: SearchCacheData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Maybe use localStorage for the cache.
|
||||||
|
const searchCache = new Map<VideoId, SearchCache>();
|
||||||
|
export const fetchLyrics = (info: SongInfo) => {
|
||||||
|
if (searchCache.has(info.videoId)) {
|
||||||
|
const cache = searchCache.get(info.videoId)!;
|
||||||
|
|
||||||
|
if (cache.state === 'loading') {
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchLyrics(info);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getSongInfo().videoId === info.videoId) {
|
||||||
|
setLyricsStore('lyrics', () => {
|
||||||
|
// weird bug with solid-js
|
||||||
|
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache: SearchCache = {
|
||||||
|
state: 'loading',
|
||||||
|
data: initialData(),
|
||||||
|
};
|
||||||
|
|
||||||
|
searchCache.set(info.videoId, cache);
|
||||||
|
if (getSongInfo().videoId === info.videoId) {
|
||||||
|
setLyricsStore('lyrics', () => {
|
||||||
|
// weird bug with solid-js
|
||||||
|
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks: Promise<void>[] = [];
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
for (
|
||||||
|
const [providerName, provider] of Object.entries(providers) as [
|
||||||
|
ProviderName,
|
||||||
|
LyricProvider,
|
||||||
|
][]
|
||||||
|
) {
|
||||||
|
const pCache = cache.data[providerName];
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
provider
|
||||||
|
.search(info)
|
||||||
|
.then((res) => {
|
||||||
|
pCache.state = 'done';
|
||||||
|
pCache.data = res;
|
||||||
|
|
||||||
|
if (getSongInfo().videoId === info.videoId) {
|
||||||
|
setLyricsStore('lyrics', (old) => {
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
[providerName]: {
|
||||||
|
state: 'done',
|
||||||
|
data: res ? { ...res } : null,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
pCache.state = 'error';
|
||||||
|
pCache.error = error;
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
if (getSongInfo().videoId === info.videoId) {
|
||||||
|
setLyricsStore('lyrics', (old) => {
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
[providerName]: { state: 'error', error, data: null },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.allSettled(tasks).then(() => {
|
||||||
|
cache.state = 'done';
|
||||||
|
searchCache.set(info.videoId, cache);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const retrySearch = (provider: ProviderName, info: SongInfo) => {
|
||||||
|
setLyricsStore('lyrics', (old) => {
|
||||||
|
const pCache = {
|
||||||
|
state: 'fetching',
|
||||||
|
data: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
[provider]: pCache,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
providers[provider]
|
||||||
|
.search(info)
|
||||||
|
.then((res) => {
|
||||||
|
setLyricsStore('lyrics', (old) => {
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
[provider]: { state: 'done', data: res, error: null },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setLyricsStore('lyrics', (old) => {
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
[provider]: { state: 'error', data: null, error },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import type { SongInfo } from '@/providers/song-info';
|
import type { SongInfo } from '@/providers/song-info';
|
||||||
|
import type { ProviderName } from './providers';
|
||||||
|
|
||||||
export type SyncedLyricsPluginConfig = {
|
export type SyncedLyricsPluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
preferredProvider?: ProviderName;
|
||||||
preciseTiming: boolean;
|
preciseTiming: boolean;
|
||||||
showTimeCodes: boolean;
|
showTimeCodes: boolean;
|
||||||
defaultTextString: string;
|
defaultTextString: string | string[];
|
||||||
showLyricsEvenIfInexact: boolean;
|
showLyricsEvenIfInexact: boolean;
|
||||||
lineEffect: LineEffect;
|
lineEffect: LineEffect;
|
||||||
romanization: boolean;
|
romanization: boolean;
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import nextIcon from '@assets/media-icons-black/next.png?asset&asarUnpack';
|
|||||||
import previousIcon from '@assets/media-icons-black/previous.png?asset&asarUnpack';
|
import previousIcon from '@assets/media-icons-black/previous.png?asset&asarUnpack';
|
||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import getSongControls from '@/providers/song-controls';
|
import { getSongControls } from '@/providers/song-controls';
|
||||||
import {
|
import {
|
||||||
registerCallback,
|
registerCallback,
|
||||||
type SongInfo,
|
type SongInfo,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { nativeImage, type NativeImage, TouchBar } from 'electron';
|
import { nativeImage, type NativeImage, TouchBar } from 'electron';
|
||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import getSongControls from '@/providers/song-controls';
|
import { getSongControls } from '@/providers/song-controls';
|
||||||
import { registerCallback, SongInfoEvent } from '@/providers/song-info';
|
import { registerCallback, SongInfoEvent } from '@/providers/song-info';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const typeList = Object.values(MaterialType);
|
|||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: () => t('plugins.transparent-player.name'),
|
name: () => t('plugins.transparent-player.name'),
|
||||||
description: () => t('plugins.transparent-player.description'),
|
description: () => t('plugins.transparent-player.description'),
|
||||||
addedVersion: '3.10.x',
|
addedVersion: '3.11.x',
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
platform: Platform.Windows,
|
platform: Platform.Windows,
|
||||||
config: defaultConfig,
|
config: defaultConfig,
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
} from 'electron';
|
} from 'electron';
|
||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
|
|
||||||
import config from './config';
|
import * as config from './config';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
forceLoadPreloadPlugin,
|
forceLoadPreloadPlugin,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import path from 'node:path';
|
|||||||
|
|
||||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||||
|
|
||||||
import config from '@/config';
|
import * as config from '@/config';
|
||||||
|
|
||||||
export const restart = () => restartInternal();
|
export const restart = () => restartInternal();
|
||||||
|
|
||||||
|
|||||||
@ -2,5 +2,3 @@ export const getSongMenu = () =>
|
|||||||
document.querySelector<HTMLElement>(
|
document.querySelector<HTMLElement>(
|
||||||
'ytmusic-menu-popup-renderer tp-yt-paper-listbox',
|
'ytmusic-menu-popup-renderer tp-yt-paper-listbox',
|
||||||
);
|
);
|
||||||
|
|
||||||
export default { getSongMenu };
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import path from 'node:path';
|
|||||||
|
|
||||||
import { app, type BrowserWindow } from 'electron';
|
import { app, type BrowserWindow } from 'electron';
|
||||||
|
|
||||||
import getSongControls from './song-controls';
|
import { getSongControls } from './song-controls';
|
||||||
|
|
||||||
export const APP_PROTOCOL = 'youtubemusic';
|
export const APP_PROTOCOL = 'youtubemusic';
|
||||||
|
|
||||||
@ -36,10 +36,3 @@ export function changeProtocolHandler(
|
|||||||
) {
|
) {
|
||||||
protocolHandler = f;
|
protocolHandler = f;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
|
||||||
APP_PROTOCOL,
|
|
||||||
setupProtocolHandler,
|
|
||||||
handleProtocol,
|
|
||||||
changeProtocolHandler,
|
|
||||||
};
|
|
||||||
|
|||||||
@ -36,7 +36,7 @@ const parseStringFromArgsType = (args: ArgsType<string>) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (win: BrowserWindow) => {
|
export const getSongControls = (win: BrowserWindow) => {
|
||||||
return {
|
return {
|
||||||
// Playback
|
// Playback
|
||||||
previous: () => win.webContents.send('ytmd:previous-video'),
|
previous: () => win.webContents.send('ytmd:previous-video'),
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import type {
|
|||||||
import type { SongInfo } from './song-info';
|
import type { SongInfo } from './song-info';
|
||||||
import type { VideoDataChanged } from '@/types/video-data-changed';
|
import type { VideoDataChanged } from '@/types/video-data-changed';
|
||||||
|
|
||||||
|
const DATAUPDATED_FALLBACK_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
let songInfo: SongInfo = {} as SongInfo;
|
let songInfo: SongInfo = {} as SongInfo;
|
||||||
export const getSongInfo = () => songInfo;
|
export const getSongInfo = () => songInfo;
|
||||||
|
|
||||||
@ -145,6 +147,7 @@ export const setupShuffleChangedListener = singleton(() => {
|
|||||||
|
|
||||||
observer.observe(playerBar, {
|
observer.observe(playerBar, {
|
||||||
attributes: true,
|
attributes: true,
|
||||||
|
attributeFilter: ['shuffle-on'],
|
||||||
childList: false,
|
childList: false,
|
||||||
subtree: false,
|
subtree: false,
|
||||||
});
|
});
|
||||||
@ -168,6 +171,7 @@ export const setupFullScreenChangedListener = singleton(() => {
|
|||||||
|
|
||||||
observer.observe(playerBar, {
|
observer.observe(playerBar, {
|
||||||
attributes: true,
|
attributes: true,
|
||||||
|
attributeFilter: ['player-fullscreened'],
|
||||||
childList: false,
|
childList: false,
|
||||||
subtree: false,
|
subtree: false,
|
||||||
});
|
});
|
||||||
@ -189,7 +193,7 @@ export const setupAutoPlayChangedListener = singleton(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default (api: YoutubePlayer) => {
|
export const setupSongInfo = (api: YoutubePlayer) => {
|
||||||
window.ipcRenderer.on('ytmd:setup-time-changed-listener', () => {
|
window.ipcRenderer.on('ytmd:setup-time-changed-listener', () => {
|
||||||
setupTimeChangedListener();
|
setupTimeChangedListener();
|
||||||
});
|
});
|
||||||
@ -251,12 +255,25 @@ export default (api: YoutubePlayer) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const waitingEvent = new Set<string>();
|
const waitingEvent = new Set<string>();
|
||||||
|
const waitingTimeouts = new Map<string, NodeJS.Timeout>();
|
||||||
|
|
||||||
|
const clearVideoTimeout = (videoId: string) => {
|
||||||
|
const timeoutId = waitingTimeouts.get(videoId);
|
||||||
|
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
waitingTimeouts.delete(videoId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Name = "dataloaded" and abit later "dataupdated"
|
// Name = "dataloaded" and abit later "dataupdated"
|
||||||
|
// Sometimes "dataupdated" is not fired, so we need to fallback to "dataloaded"
|
||||||
api.addEventListener('videodatachange', (name, videoData) => {
|
api.addEventListener('videodatachange', (name, videoData) => {
|
||||||
videoEventDispatcher(name, videoData);
|
videoEventDispatcher(name, videoData);
|
||||||
|
|
||||||
if (name === 'dataupdated' && waitingEvent.has(videoData.videoId)) {
|
if (name === 'dataupdated' && waitingEvent.has(videoData.videoId)) {
|
||||||
waitingEvent.delete(videoData.videoId);
|
waitingEvent.delete(videoData.videoId);
|
||||||
|
clearVideoTimeout(videoData.videoId);
|
||||||
sendSongInfo(videoData);
|
sendSongInfo(videoData);
|
||||||
} else if (name === 'dataloaded') {
|
} else if (name === 'dataloaded') {
|
||||||
const video = document.querySelector<HTMLVideoElement>('video');
|
const video = document.querySelector<HTMLVideoElement>('video');
|
||||||
@ -267,7 +284,18 @@ export default (api: YoutubePlayer) => {
|
|||||||
video?.addEventListener(status, playPausedHandlers[status]);
|
video?.addEventListener(status, playPausedHandlers[status]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearVideoTimeout(videoData.videoId);
|
||||||
waitingEvent.add(videoData.videoId);
|
waitingEvent.add(videoData.videoId);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (waitingEvent.has(videoData.videoId)) {
|
||||||
|
waitingEvent.delete(videoData.videoId);
|
||||||
|
waitingTimeouts.delete(videoData.videoId);
|
||||||
|
sendSongInfo(videoData);
|
||||||
|
}
|
||||||
|
}, DATAUPDATED_FALLBACK_TIMEOUT_MS);
|
||||||
|
|
||||||
|
waitingTimeouts.set(videoData.videoId, timeoutId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { type BrowserWindow, ipcMain, nativeImage, net } from 'electron';
|
|||||||
|
|
||||||
import { Mutex } from 'async-mutex';
|
import { Mutex } from 'async-mutex';
|
||||||
|
|
||||||
import config from '@/config';
|
import * as config from '@/config';
|
||||||
|
|
||||||
import type { GetPlayerResponse } from '@/types/get-player-response';
|
import type { GetPlayerResponse } from '@/types/get-player-response';
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
|
|
||||||
import { startingPages } from './providers/extracted-data';
|
import { startingPages } from './providers/extracted-data';
|
||||||
import setupSongInfo from './providers/song-info-front';
|
import { setupSongInfo } from './providers/song-info-front';
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
forceLoadRendererPlugin,
|
forceLoadRendererPlugin,
|
||||||
|
|||||||
2
src/reset.d.ts
vendored
2
src/reset.d.ts
vendored
@ -3,7 +3,7 @@ import '@total-typescript/ts-reset';
|
|||||||
import type { ipcRenderer as electronIpcRenderer } from 'electron';
|
import type { ipcRenderer as electronIpcRenderer } from 'electron';
|
||||||
import type is from 'electron-is';
|
import type is from 'electron-is';
|
||||||
|
|
||||||
import type config from './config';
|
import type * as config from './config';
|
||||||
import type { VideoDataChanged } from '@/types/video-data-changed';
|
import type { VideoDataChanged } from '@/types/video-data-changed';
|
||||||
import type { t } from '@/i18n';
|
import type { t } from '@/i18n';
|
||||||
import type { trustedTypes } from 'trusted-types';
|
import type { trustedTypes } from 'trusted-types';
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import is from 'electron-is';
|
|||||||
import defaultTrayIconAsset from '@assets/youtube-music-tray.png?asset&asarUnpack';
|
import defaultTrayIconAsset from '@assets/youtube-music-tray.png?asset&asarUnpack';
|
||||||
import pausedTrayIconAsset from '@assets/youtube-music-tray-paused.png?asset&asarUnpack';
|
import pausedTrayIconAsset from '@assets/youtube-music-tray-paused.png?asset&asarUnpack';
|
||||||
|
|
||||||
import config from './config';
|
import * as config from './config';
|
||||||
|
|
||||||
import { restart } from './providers/app-controls';
|
import { restart } from './providers/app-controls';
|
||||||
import { registerCallback, SongInfoEvent } from './providers/song-info';
|
import { registerCallback, SongInfoEvent } from './providers/song-info';
|
||||||
import getSongControls from './providers/song-controls';
|
import { getSongControls } from './providers/song-controls';
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1 @@
|
|||||||
export const isTesting = () => process.env.NODE_ENV === 'test';
|
export const isTesting = () => process.env.NODE_ENV === 'test';
|
||||||
|
|
||||||
export default { isTesting };
|
|
||||||
|
|||||||
4
src/yt-web-components.d.ts
vendored
4
src/yt-web-components.d.ts
vendored
@ -48,8 +48,8 @@ declare module 'solid-js' {
|
|||||||
'tp-yt-paper-icon-button': ComponentProps<'div'> &
|
'tp-yt-paper-icon-button': ComponentProps<'div'> &
|
||||||
TpYtPaperIconButtonProps;
|
TpYtPaperIconButtonProps;
|
||||||
'yt-icon-button': ComponentProps<'div'> & TpYtPaperIconButtonProps;
|
'yt-icon-button': ComponentProps<'div'> & TpYtPaperIconButtonProps;
|
||||||
'tp-yt-iron-icon': ComponentProps<'div'>;
|
'tp-yt-iron-icon': ComponentProps<'div'> & TpYtPaperIconButtonProps;
|
||||||
'yt-icon': ComponentProps<'div'>;
|
'yt-icon': ComponentProps<'div'> & TpYtPaperIconButtonProps;
|
||||||
// input type="range" slider component
|
// input type="range" slider component
|
||||||
'tp-yt-paper-slider': ComponentProps<'input'> & {
|
'tp-yt-paper-slider': ComponentProps<'input'> & {
|
||||||
'value'?: number | string;
|
'value'?: number | string;
|
||||||
|
|||||||
Reference in New Issue
Block a user