diff --git a/package.json b/package.json index 56289bfcec..544d6397e5 100644 --- a/package.json +++ b/package.json @@ -168,6 +168,7 @@ "node-html-parser": "6.1.12", "node-id3": "0.2.6", "peerjs": "1.5.2", + "semver": "7.5.4", "serve": "14.2.1", "simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9", "ts-morph": "21.0.1", @@ -181,6 +182,7 @@ "@types/electron-localshortcut": "3.1.3", "@types/howler": "2.2.11", "@types/html-to-text": "9.0.4", + "@types/semver": "7.5.6", "@typescript-eslint/eslint-plugin": "6.16.0", "bufferutil": "4.0.8", "builtin-modules": "3.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec9aba4fd4..d6c69517db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,9 @@ dependencies: peerjs: specifier: 1.5.2 version: 1.5.2 + semver: + specifier: 7.5.4 + version: 7.5.4 serve: specifier: 14.2.1 version: 14.2.1 @@ -152,6 +155,9 @@ devDependencies: '@types/html-to-text': specifier: 9.0.4 version: 9.0.4 + '@types/semver': + specifier: 7.5.6 + version: 7.5.6 '@typescript-eslint/eslint-plugin': specifier: 6.16.0 version: 6.16.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3) @@ -1113,7 +1119,7 @@ packages: resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.9.1 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true diff --git a/src/i18n/resources/en.json b/src/i18n/resources/en.json index 749595e6c6..cedcfe0126 100644 --- a/src/i18n/resources/en.json +++ b/src/i18n/resources/en.json @@ -170,7 +170,8 @@ }, "plugins": { "enabled": "Enabled", - "label": "Plugins" + "label": "Plugins", + "new": "NEW" }, "view": { "label": "View", diff --git a/src/i18n/resources/ko.json b/src/i18n/resources/ko.json index fad8dc882e..3a231ee333 100644 --- a/src/i18n/resources/ko.json +++ b/src/i18n/resources/ko.json @@ -170,7 +170,8 @@ }, "plugins": { "enabled": "활성화", - "label": "확장" + "label": "확장", + "new": "NEW" }, "view": { "label": "보기", diff --git a/src/menu.ts b/src/menu.ts index c5e0fbd4c7..1004fd14cf 100644 --- a/src/menu.ts +++ b/src/menu.ts @@ -9,6 +9,7 @@ import { shell, } from 'electron'; import prompt from 'custom-electron-prompt'; +import { satisfies } from 'semver'; import { allPlugins } from 'virtual:plugins'; @@ -23,6 +24,8 @@ import promptOptions from './providers/prompt-options'; import { getAllMenuTemplate, loadAllMenuPlugins } from './loader/menu'; import { setLanguage, t } from '@/i18n'; +import packageJson from '../package.json'; + export type MenuTemplate = Electron.MenuItemConstructorOptions[]; // True only if in-app-menu was loaded on launch @@ -31,10 +34,14 @@ const inAppMenuActive = config.plugins.isEnabled('in-app-menu'); const pluginEnabledMenu = ( plugin: string, label = '', + description: string | undefined = undefined, + isNew = false, hasSubmenu = false, refreshMenu: (() => void) | undefined = undefined, ): Electron.MenuItemConstructorOptions => ({ label: label || plugin, + sublabel: isNew ? t('main.menu.plugins.new') : undefined, + toolTip: description, type: 'checkbox', checked: config.plugins.isEnabled(plugin), click(item: Electron.MenuItem) { @@ -66,12 +73,15 @@ export const mainMenuTemplate = async ( const menuResult = Object.entries(getAllMenuTemplate()).map( ([id, template]) => { - const pluginLabel = allPlugins[id]?.name?.() ?? id; + const plugin = allPlugins[id]; + const pluginLabel = plugin?.name?.() ?? id; + const pluginDescription = plugin?.description?.() ?? undefined; + const isNew = plugin?.addedVersion ? satisfies(packageJson.version, plugin.addedVersion) : false; if (!config.plugins.isEnabled(id)) { return [ id, - pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu), + pluginEnabledMenu(id, pluginLabel, pluginDescription, isNew, true, innerRefreshMenu), ] as const; } @@ -79,10 +89,14 @@ export const mainMenuTemplate = async ( id, { label: pluginLabel, + sublabel: isNew ? t('main.menu.plugins.new') : undefined, + toolTip: pluginDescription, submenu: [ pluginEnabledMenu( id, t('main.menu.plugins.enabled'), + undefined, + false, true, innerRefreshMenu, ), @@ -106,9 +120,12 @@ export const mainMenuTemplate = async ( const predefinedTemplate = menuResult.find((it) => it[0] === id); if (predefinedTemplate) return predefinedTemplate[1]; - const pluginLabel = allPlugins[id]?.name?.() ?? id; + const plugin = allPlugins[id]; + const pluginLabel = plugin?.name?.() ?? id; + const pluginDescription = plugin?.description?.() ?? undefined; + const isNew = plugin?.addedVersion ? satisfies(packageJson.version, plugin.addedVersion) : false; - return pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu); + return pluginEnabledMenu(id, pluginLabel, pluginDescription, isNew, true, innerRefreshMenu); }); const availableLanguages = Object.keys(languageResources); diff --git a/src/plugins/album-actions/index.ts b/src/plugins/album-actions/index.ts index 2a1ffc5aab..f61d078fcb 100644 --- a/src/plugins/album-actions/index.ts +++ b/src/plugins/album-actions/index.ts @@ -11,6 +11,7 @@ export default createPlugin({ name: () => t('plugins.album-actions.name'), description: () => t('plugins.album-actions.description'), restartNeeded: false, + addedVersion: '3.2.0', config: { enabled: false, }, diff --git a/src/plugins/in-app-menu/menu/panel.ts b/src/plugins/in-app-menu/menu/panel.ts index e6bef9c9ba..b69261e309 100644 --- a/src/plugins/in-app-menu/menu/panel.ts +++ b/src/plugins/in-app-menu/menu/panel.ts @@ -52,6 +52,18 @@ export const createPanel = ( menu.appendChild(iconWrapper); menu.append(item.label); + if (item.sublabel) { + menu.classList.add('badge'); + const menuBadge = document.createElement('menu-item-badge'); + menuBadge.append(item.sublabel); + menu.append(menuBadge); + } + if (item.toolTip) { + const menuTooltip = document.createElement('menu-item-tooltip'); + menuTooltip.append(item.toolTip); + menu.append(menuTooltip); + } + menu.addEventListener('click', async () => { await window.ipcRenderer.invoke('menu-event', item.commandId); const menuItem = (await window.ipcRenderer.invoke( diff --git a/src/plugins/in-app-menu/titlebar.css b/src/plugins/in-app-menu/titlebar.css index ee966ef2cd..aa7b5475d6 100644 --- a/src/plugins/in-app-menu/titlebar.css +++ b/src/plugins/in-app-menu/titlebar.css @@ -97,6 +97,8 @@ menu-panel.position-by-bottom { } menu-item { + position: relative; + -webkit-app-region: none; min-height: 32px; height: 32px; @@ -109,6 +111,9 @@ menu-item { border-radius: 4px; cursor: pointer; } +menu-item.badge { + grid-template-columns: 32px 1fr auto minmax(32px, auto); +} menu-item:hover { background-color: rgba(255, 255, 255, 0.1); } @@ -128,6 +133,56 @@ menu-separator { background-color: rgba(255, 255, 255, 0.2); } +menu-item-badge { + display: flex; + justify-content: center; + align-items: center; + + min-width: 16px; + height: 16px; + padding: 0 4px; + margin-left: 8px; + + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.2); + color: #f1f1f1; + font-size: 10px; + font-weight: 500; + line-height: 1; +} + +menu-item-tooltip { + position: absolute; + + left: 0; + top: calc(100% + 4px); + + display: flex; + justify-content: center; + align-items: center; + + min-width: 32px; + padding: 4px; + + border-radius: 4px; + background-color: rgba(25, 25, 25, 0.8); + color: #f1f1f1; + font-size: 10px; + + pointer-events: none; + z-index: 1000; + + opacity: 0; + scale: 0.9; + + transform-origin: 50% 0; + transition: all 0.225s ease-out; +} +menu-item:hover > menu-item-tooltip { + opacity: 1; + scale: 1.0; +} + /* classes */ .title-bar-icon { diff --git a/src/plugins/music-together/index.ts b/src/plugins/music-together/index.ts index f67158ba56..4ed408d371 100644 --- a/src/plugins/music-together/index.ts +++ b/src/plugins/music-together/index.ts @@ -38,6 +38,7 @@ export default createPlugin({ name: () => t('plugins.music-together.name'), description: () => t('plugins.music-together.description'), restartNeeded: false, + addedVersion: '3.2.0', config: { enabled: false }, diff --git a/src/types/plugins.ts b/src/types/plugins.ts index 11d019c565..f05df709a0 100644 --- a/src/types/plugins.ts +++ b/src/types/plugins.ts @@ -47,6 +47,7 @@ export interface PluginDef< name: () => string; authors?: Author[]; description?: () => string; + addedVersion?: string; config?: Config; menu?: (