mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
AudioPanel: add app volume controls
This commit is contained in:
@@ -956,6 +956,15 @@
|
||||
"label": "Visualisierungstyp"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"applications": {
|
||||
"empty": "Derzeit geben keine Anwendungen Audio wieder."
|
||||
},
|
||||
"tabs": {
|
||||
"applications": "Anwendungen",
|
||||
"devices": "Geräte"
|
||||
}
|
||||
},
|
||||
"title": "Audio",
|
||||
"volumes": {
|
||||
"input-volume": {
|
||||
@@ -2068,6 +2077,11 @@
|
||||
"downloaded": "Heruntergeladen",
|
||||
"not-downloaded": "Nicht heruntergeladen"
|
||||
},
|
||||
"hot-reload": {
|
||||
"description": "Plugins automatisch neu laden, wenn sich ihre Dateien ändern. Nützlich für die Plugin-Entwicklung.",
|
||||
"label": "Heißes Neuladen (Entwicklermodus)"
|
||||
},
|
||||
"hot-reloaded": "Plugin neu geladen: {name}",
|
||||
"install": "Installieren",
|
||||
"install-error": "Installation fehlgeschlagen: {error}",
|
||||
"install-success": "{plugin} erfolgreich installiert",
|
||||
@@ -2633,6 +2647,7 @@
|
||||
"move-to-center-section": "Mittlere Sektion",
|
||||
"move-to-left-section": "Linke Sektion",
|
||||
"move-to-right-section": "Rechte Sektion",
|
||||
"mute": "Stummschalten",
|
||||
"next-media": "Nächster Titel",
|
||||
"next-month": "Nächster Monat",
|
||||
"night-light-disabled": "Nachtlicht",
|
||||
@@ -2667,6 +2682,7 @@
|
||||
"stop-screen-recording": "Bildschirmrekorder",
|
||||
"switch-to-dark-mode": "Dunkler Modus",
|
||||
"switch-to-light-mode": "Heller Modus",
|
||||
"unmute": "Stummschaltung aufheben",
|
||||
"up": "Übergeordnetes Verzeichnis",
|
||||
"volume-at": "Lautstärke: {volume}%",
|
||||
"wallpaper-selector": "Hintergrundbild-Auswahl",
|
||||
|
||||
@@ -956,6 +956,15 @@
|
||||
"label": "Visualization type"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"applications": {
|
||||
"empty": "No applications are currently playing audio"
|
||||
},
|
||||
"tabs": {
|
||||
"applications": "Applications",
|
||||
"devices": "Devices"
|
||||
}
|
||||
},
|
||||
"title": "Audio",
|
||||
"volumes": {
|
||||
"input-volume": {
|
||||
@@ -2068,6 +2077,11 @@
|
||||
"downloaded": "Downloaded",
|
||||
"not-downloaded": "Not Downloaded"
|
||||
},
|
||||
"hot-reload": {
|
||||
"description": "Automatically reload plugins when their files change. Useful for plugin development.",
|
||||
"label": "Hot reload (dev mode)"
|
||||
},
|
||||
"hot-reloaded": "Reloaded plugin: {name}",
|
||||
"install": "Install",
|
||||
"install-error": "Failed to install: {error}",
|
||||
"install-success": "Successfully installed {plugin}",
|
||||
@@ -2123,12 +2137,7 @@
|
||||
"update-incompatible": "Requires Noctalia v{version} or higher",
|
||||
"update-success": "Updated {plugin} to v{version}",
|
||||
"update-version": "v{current} → v{new}",
|
||||
"updating": "Updating {plugin}...",
|
||||
"hot-reload": {
|
||||
"description": "Automatically reload plugins when their files change. Useful for plugin development.",
|
||||
"label": "Hot reload (dev mode)"
|
||||
},
|
||||
"hot-reloaded": "Reloaded plugin: {name}"
|
||||
"updating": "Updating {plugin}..."
|
||||
},
|
||||
"screen-recorder": {
|
||||
"audio": {
|
||||
@@ -2638,6 +2647,7 @@
|
||||
"move-to-center-section": "Center section",
|
||||
"move-to-left-section": "Left section",
|
||||
"move-to-right-section": "Right section",
|
||||
"mute": "Mute",
|
||||
"next-media": "Next track",
|
||||
"next-month": "Next month",
|
||||
"night-light-disabled": "Night Light",
|
||||
@@ -2672,6 +2682,7 @@
|
||||
"stop-screen-recording": "Screen recorder",
|
||||
"switch-to-dark-mode": "Dark Mode",
|
||||
"switch-to-light-mode": "Light Mode",
|
||||
"unmute": "Unmute",
|
||||
"up": "Parent directory",
|
||||
"volume-at": "Output volume: {volume}%",
|
||||
"wallpaper-selector": "Wallpaper selector",
|
||||
|
||||
@@ -956,6 +956,15 @@
|
||||
"label": "Tipo de visualización"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"applications": {
|
||||
"empty": "Actualmente no hay aplicaciones reproduciendo audio."
|
||||
},
|
||||
"tabs": {
|
||||
"applications": "Aplicaciones",
|
||||
"devices": "Dispositivos"
|
||||
}
|
||||
},
|
||||
"title": "Audio",
|
||||
"volumes": {
|
||||
"input-volume": {
|
||||
@@ -2068,6 +2077,11 @@
|
||||
"downloaded": "Descargado",
|
||||
"not-downloaded": "No descargado"
|
||||
},
|
||||
"hot-reload": {
|
||||
"description": "Recarga automáticamente los plugins cuando sus archivos cambian. Útil para el desarrollo de plugins.",
|
||||
"label": "Recarga en caliente (modo desarrollo)"
|
||||
},
|
||||
"hot-reloaded": "Plugin recargado: {name}",
|
||||
"install": "Instalar",
|
||||
"install-error": "Error al instalar: {error}",
|
||||
"install-success": "Se instaló {plugin} correctamente.",
|
||||
@@ -2633,6 +2647,7 @@
|
||||
"move-to-center-section": "Sección central",
|
||||
"move-to-left-section": "Sección izquierda",
|
||||
"move-to-right-section": "Sección derecha",
|
||||
"mute": "Silenciar",
|
||||
"next-media": "Siguiente pista",
|
||||
"next-month": "Mes siguiente",
|
||||
"night-light-disabled": "Luz nocturna",
|
||||
@@ -2667,6 +2682,7 @@
|
||||
"stop-screen-recording": "Grabadora de pantalla",
|
||||
"switch-to-dark-mode": "Modo oscuro",
|
||||
"switch-to-light-mode": "Modo claro",
|
||||
"unmute": "Activar el audio",
|
||||
"up": "Directorio superior",
|
||||
"volume-at": "Volumen de salida: {volume}%",
|
||||
"wallpaper-selector": "Selector de fondos de pantalla",
|
||||
|
||||
@@ -956,6 +956,15 @@
|
||||
"label": "Type de visualisation"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"applications": {
|
||||
"empty": "Aucune application ne lit actuellement de l'audio."
|
||||
},
|
||||
"tabs": {
|
||||
"applications": "Applications",
|
||||
"devices": "Appareils"
|
||||
}
|
||||
},
|
||||
"title": "Audio",
|
||||
"volumes": {
|
||||
"input-volume": {
|
||||
@@ -2068,6 +2077,11 @@
|
||||
"downloaded": "Téléchargé",
|
||||
"not-downloaded": "Non téléchargé"
|
||||
},
|
||||
"hot-reload": {
|
||||
"description": "Recharger automatiquement les plugins lorsque leurs fichiers sont modifiés. Utile pour le développement de plugins.",
|
||||
"label": "Rechargement à chaud (mode développement)"
|
||||
},
|
||||
"hot-reloaded": "Plugin rechargé : {name}",
|
||||
"install": "Installer",
|
||||
"install-error": "Échec de l'installation : {error}",
|
||||
"install-success": "Installation de {plugin} réussie.",
|
||||
@@ -2633,6 +2647,7 @@
|
||||
"move-to-center-section": "Section centrale",
|
||||
"move-to-left-section": "Section de gauche",
|
||||
"move-to-right-section": "Section de droite",
|
||||
"mute": "Muet / Couper le son",
|
||||
"next-media": "Piste suivante",
|
||||
"next-month": "Mois suivant",
|
||||
"night-light-disabled": "Éclairage nocturne",
|
||||
@@ -2667,6 +2682,7 @@
|
||||
"stop-screen-recording": "Enregistreur d'écran",
|
||||
"switch-to-dark-mode": "Mode sombre",
|
||||
"switch-to-light-mode": "Mode clair",
|
||||
"unmute": "Réactiver le micro",
|
||||
"up": "Répertoire parent",
|
||||
"volume-at": "Volume de sortie : {volume}%",
|
||||
"wallpaper-selector": "Sélecteur de fond d'écran",
|
||||
|
||||
@@ -956,6 +956,15 @@
|
||||
"label": "ビジュアライザーの種類"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"applications": {
|
||||
"empty": "現在、音声を再生しているアプリケーションはありません。"
|
||||
},
|
||||
"tabs": {
|
||||
"applications": "アプリケーション",
|
||||
"devices": "デバイス"
|
||||
}
|
||||
},
|
||||
"title": "サウンド",
|
||||
"volumes": {
|
||||
"input-volume": {
|
||||
@@ -2068,6 +2077,11 @@
|
||||
"downloaded": "ダウンロード済み",
|
||||
"not-downloaded": "ダウンロードされていません"
|
||||
},
|
||||
"hot-reload": {
|
||||
"description": "プラグインのファイルが変更されたときに、自動的にプラグインを再読み込みします。プラグイン開発に便利です。",
|
||||
"label": "ホットリロード(開発モード)"
|
||||
},
|
||||
"hot-reloaded": "プラグインを再読み込みしました: {name}",
|
||||
"install": "インストール",
|
||||
"install-error": "インストールに失敗しました: {error}",
|
||||
"install-success": "{plugin} のインストールに成功しました。",
|
||||
@@ -2633,6 +2647,7 @@
|
||||
"move-to-center-section": "中央セクション",
|
||||
"move-to-left-section": "左セクション",
|
||||
"move-to-right-section": "右セクション",
|
||||
"mute": "ミュート",
|
||||
"next-media": "次のトラック",
|
||||
"next-month": "翌月",
|
||||
"night-light-disabled": "夜間モード",
|
||||
@@ -2667,6 +2682,7 @@
|
||||
"stop-screen-recording": "画面録画",
|
||||
"switch-to-dark-mode": "ダークモード",
|
||||
"switch-to-light-mode": "ライトモード",
|
||||
"unmute": "ミュート解除",
|
||||
"up": "親ディレクトリ",
|
||||
"volume-at": "出力音量: {volume}%",
|
||||
"wallpaper-selector": "壁紙選択パネル",
|
||||
|
||||
@@ -956,6 +956,15 @@
|
||||
"label": "Type visualisatie"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"applications": {
|
||||
"empty": "Er spelen momenteel geen applicaties audio af."
|
||||
},
|
||||
"tabs": {
|
||||
"applications": "Toepassingen",
|
||||
"devices": "Apparaten"
|
||||
}
|
||||
},
|
||||
"title": "Audio",
|
||||
"volumes": {
|
||||
"input-volume": {
|
||||
@@ -2068,6 +2077,11 @@
|
||||
"downloaded": "Gedownload",
|
||||
"not-downloaded": "Niet gedownload"
|
||||
},
|
||||
"hot-reload": {
|
||||
"description": "Laad plugins automatisch opnieuw wanneer hun bestanden veranderen. Handig voor plugin-ontwikkeling.",
|
||||
"label": "Hot reload (ontwikkelmodus)"
|
||||
},
|
||||
"hot-reloaded": "Plugin opnieuw geladen: {name}",
|
||||
"install": "Installeren",
|
||||
"install-error": "Installatie mislukt: {error}",
|
||||
"install-success": "{plugin} succesvol geïnstalleerd",
|
||||
@@ -2633,6 +2647,7 @@
|
||||
"move-to-center-section": "Middelste sectie",
|
||||
"move-to-left-section": "Linker sectie",
|
||||
"move-to-right-section": "Rechter sectie",
|
||||
"mute": "Dempen",
|
||||
"next-media": "Volgende track",
|
||||
"next-month": "Volgende maand",
|
||||
"night-light-disabled": "Nachtlicht",
|
||||
@@ -2667,6 +2682,7 @@
|
||||
"stop-screen-recording": "Schermrecorder",
|
||||
"switch-to-dark-mode": "Donkere modus",
|
||||
"switch-to-light-mode": "Lichte modus",
|
||||
"unmute": "Dempen opheffen",
|
||||
"up": "Bovenliggende map",
|
||||
"volume-at": "Uitvoervolume: {volume}%",
|
||||
"wallpaper-selector": "Achtergrondkiezer",
|
||||
|
||||
@@ -956,6 +956,15 @@
|
||||
"label": "Tipo de visualização"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"applications": {
|
||||
"empty": "Nenhum aplicativo está reproduzindo áudio no momento."
|
||||
},
|
||||
"tabs": {
|
||||
"applications": "Aplicações",
|
||||
"devices": "Dispositivos"
|
||||
}
|
||||
},
|
||||
"title": "Áudio",
|
||||
"volumes": {
|
||||
"input-volume": {
|
||||
@@ -2068,6 +2077,11 @@
|
||||
"downloaded": "Baixado",
|
||||
"not-downloaded": "Não Baixado"
|
||||
},
|
||||
"hot-reload": {
|
||||
"description": "Recarrega automaticamente os plugins quando seus arquivos são alterados. Útil para o desenvolvimento de plugins.",
|
||||
"label": "Recarga a quente (modo de desenvolvimento)"
|
||||
},
|
||||
"hot-reloaded": "Plugin recarregado: {name}",
|
||||
"install": "Instalar",
|
||||
"install-error": "Falha ao instalar: {error}",
|
||||
"install-success": "{plugin} instalado com sucesso.",
|
||||
@@ -2633,6 +2647,7 @@
|
||||
"move-to-center-section": "Seção central",
|
||||
"move-to-left-section": "Seção esquerda",
|
||||
"move-to-right-section": "Seção direita",
|
||||
"mute": "Silenciar",
|
||||
"next-media": "Próxima faixa",
|
||||
"next-month": "Próximo mês",
|
||||
"night-light-disabled": "Luz noturna",
|
||||
@@ -2667,6 +2682,7 @@
|
||||
"stop-screen-recording": "Gravador de tela",
|
||||
"switch-to-dark-mode": "Modo escuro",
|
||||
"switch-to-light-mode": "Modo claro",
|
||||
"unmute": "Desativar o mudo",
|
||||
"up": "Diretório superior",
|
||||
"volume-at": "Volume de saída: {volume}%",
|
||||
"wallpaper-selector": "Seletor de papel de parede",
|
||||
|
||||
@@ -956,6 +956,15 @@
|
||||
"label": "Тип визуализации"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"applications": {
|
||||
"empty": "В данный момент нет приложений, воспроизводящих звук."
|
||||
},
|
||||
"tabs": {
|
||||
"applications": "Приложения",
|
||||
"devices": "Устройства"
|
||||
}
|
||||
},
|
||||
"title": "Аудио",
|
||||
"volumes": {
|
||||
"input-volume": {
|
||||
@@ -2068,6 +2077,11 @@
|
||||
"downloaded": "Скачано",
|
||||
"not-downloaded": "Не загружено"
|
||||
},
|
||||
"hot-reload": {
|
||||
"description": "Автоматически перезагружать плагины при изменении их файлов. Полезно для разработки плагинов.",
|
||||
"label": "Горячая перезагрузка (режим разработки)"
|
||||
},
|
||||
"hot-reloaded": "Перезагружен плагин: {name}",
|
||||
"install": "Установить",
|
||||
"install-error": "Не удалось установить: {error}",
|
||||
"install-success": "Успешно установлен {plugin}",
|
||||
@@ -2633,6 +2647,7 @@
|
||||
"move-to-center-section": "Центральная секция",
|
||||
"move-to-left-section": "Левая секция",
|
||||
"move-to-right-section": "Правая секция",
|
||||
"mute": "Отключить звук",
|
||||
"next-media": "Следующий трек",
|
||||
"next-month": "Следующий месяц",
|
||||
"night-light-disabled": "Ночной свет",
|
||||
@@ -2667,6 +2682,7 @@
|
||||
"stop-screen-recording": "Запись экрана",
|
||||
"switch-to-dark-mode": "Темный режим",
|
||||
"switch-to-light-mode": "Светлый режим",
|
||||
"unmute": "Включить звук",
|
||||
"up": "Родительский каталог",
|
||||
"volume-at": "Громкость вывода: {volume}%",
|
||||
"wallpaper-selector": "Выбор обоев",
|
||||
|
||||
@@ -956,6 +956,15 @@
|
||||
"label": "Görselleştirme türü"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"applications": {
|
||||
"empty": "Şu anda ses çalan uygulama yok."
|
||||
},
|
||||
"tabs": {
|
||||
"applications": "Uygulamalar",
|
||||
"devices": "Cihazlar"
|
||||
}
|
||||
},
|
||||
"title": "Ses",
|
||||
"volumes": {
|
||||
"input-volume": {
|
||||
@@ -2068,6 +2077,11 @@
|
||||
"downloaded": "İndirildi",
|
||||
"not-downloaded": "İndirilmedi"
|
||||
},
|
||||
"hot-reload": {
|
||||
"description": "Eklenti dosyaları değiştiğinde eklentileri otomatik olarak yeniden yükle. Eklenti geliştirme için kullanışlıdır.",
|
||||
"label": "Hızlı yeniden yükleme (geliştirme modu)"
|
||||
},
|
||||
"hot-reloaded": "Yeniden yüklenen eklenti: {name}",
|
||||
"install": "Yükle",
|
||||
"install-error": "Yükleme başarısız oldu: {error}",
|
||||
"install-success": "{plugin} başarıyla kuruldu",
|
||||
@@ -2633,6 +2647,7 @@
|
||||
"move-to-center-section": "Orta bölüm",
|
||||
"move-to-left-section": "Sol bölüm",
|
||||
"move-to-right-section": "Sağ bölüm",
|
||||
"mute": "Sessiz",
|
||||
"next-media": "Sonraki parça",
|
||||
"next-month": "Sonraki ay",
|
||||
"night-light-disabled": "Gece ışığı",
|
||||
@@ -2667,6 +2682,7 @@
|
||||
"stop-screen-recording": "Ekran kaydedici",
|
||||
"switch-to-dark-mode": "Koyu mod",
|
||||
"switch-to-light-mode": "Açık mod",
|
||||
"unmute": "Sessizliği aç",
|
||||
"up": "Üst dizin",
|
||||
"volume-at": "Çıkış sesi: %{volume}",
|
||||
"wallpaper-selector": "Duvar kâğıdı seçici",
|
||||
|
||||
@@ -956,6 +956,15 @@
|
||||
"label": "Тип візуалізації"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"applications": {
|
||||
"empty": "Зараз жоден додаток не відтворює аудіо."
|
||||
},
|
||||
"tabs": {
|
||||
"applications": "Застосунки",
|
||||
"devices": "Пристрої"
|
||||
}
|
||||
},
|
||||
"title": "Аудіо",
|
||||
"volumes": {
|
||||
"input-volume": {
|
||||
@@ -2068,6 +2077,11 @@
|
||||
"downloaded": "Завантажено",
|
||||
"not-downloaded": "Не завантажено"
|
||||
},
|
||||
"hot-reload": {
|
||||
"description": "Автоматично перезавантажувати плагіни при зміні їхніх файлів. Корисно для розробки плагінів.",
|
||||
"label": "Гаряче перезавантаження (режим розробки)"
|
||||
},
|
||||
"hot-reloaded": "Перезавантажено плагін: {name}",
|
||||
"install": "Встановити",
|
||||
"install-error": "Не вдалося встановити: {error}",
|
||||
"install-success": "Успішно встановлено {plugin}",
|
||||
@@ -2633,6 +2647,7 @@
|
||||
"move-to-center-section": "Центральна секція",
|
||||
"move-to-left-section": "Ліва секція",
|
||||
"move-to-right-section": "Права секція",
|
||||
"mute": "Вимкнути звук",
|
||||
"next-media": "Наступний трек",
|
||||
"next-month": "Наступний місяць",
|
||||
"night-light-disabled": "Нічне світло",
|
||||
@@ -2667,6 +2682,7 @@
|
||||
"stop-screen-recording": "Запис екрана",
|
||||
"switch-to-dark-mode": "Темний режим",
|
||||
"switch-to-light-mode": "Світлий режим",
|
||||
"unmute": "Увімкнути звук",
|
||||
"up": "Батьківський каталог",
|
||||
"volume-at": "Вихідна гучність: {volume}%",
|
||||
"wallpaper-selector": "Вибір шпалер",
|
||||
|
||||
@@ -956,6 +956,15 @@
|
||||
"label": "可视化类型"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"applications": {
|
||||
"empty": "目前没有应用程序正在播放音频。"
|
||||
},
|
||||
"tabs": {
|
||||
"applications": "应用",
|
||||
"devices": "设备"
|
||||
}
|
||||
},
|
||||
"title": "音频",
|
||||
"volumes": {
|
||||
"input-volume": {
|
||||
@@ -2068,6 +2077,11 @@
|
||||
"downloaded": "已下载",
|
||||
"not-downloaded": "未下载"
|
||||
},
|
||||
"hot-reload": {
|
||||
"description": "当插件文件发生更改时自动重新加载插件。对插件开发很有用。",
|
||||
"label": "热重载(开发模式)"
|
||||
},
|
||||
"hot-reloaded": "已重新加载插件:{name}",
|
||||
"install": "安装",
|
||||
"install-error": "安装失败:{error}",
|
||||
"install-success": "成功安装 {plugin}",
|
||||
@@ -2633,6 +2647,7 @@
|
||||
"move-to-center-section": "中央部分",
|
||||
"move-to-left-section": "左侧部分",
|
||||
"move-to-right-section": "右侧部分",
|
||||
"mute": "静音",
|
||||
"next-media": "下一首",
|
||||
"next-month": "下个月",
|
||||
"night-light-disabled": "夜间模式",
|
||||
@@ -2667,6 +2682,7 @@
|
||||
"stop-screen-recording": "屏幕录制器",
|
||||
"switch-to-dark-mode": "深色模式",
|
||||
"switch-to-light-mode": "浅色模式",
|
||||
"unmute": "取消静音",
|
||||
"up": "上级目录",
|
||||
"volume-at": "输出音量:{volume}%",
|
||||
"wallpaper-selector": "壁纸选择器",
|
||||
|
||||
@@ -19,6 +19,73 @@ SmartPanel {
|
||||
property bool localInputVolumeChanging: false
|
||||
property int lastSourceId: -1
|
||||
|
||||
property int currentTabIndex: 0
|
||||
|
||||
// Find application streams that are actually playing audio (connected to default sink)
|
||||
// Use linkGroups to find nodes connected to the default audio sink
|
||||
// Note: We need to use link IDs since source/target properties require binding
|
||||
readonly property var appStreams: {
|
||||
if (!Pipewire.ready || !AudioService.sink) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var defaultSink = AudioService.sink;
|
||||
var defaultSinkId = defaultSink.id;
|
||||
var connectedStreamIds = {};
|
||||
var connectedStreams = [];
|
||||
|
||||
// Use PwNodeLinkTracker to get properly bound link groups
|
||||
if (!sinkLinkTracker.linkGroups) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if linkGroups is an array or ObjectModel
|
||||
var linkGroupsCount = 0;
|
||||
if (sinkLinkTracker.linkGroups.length !== undefined) {
|
||||
linkGroupsCount = sinkLinkTracker.linkGroups.length;
|
||||
} else if (sinkLinkTracker.linkGroups.count !== undefined) {
|
||||
linkGroupsCount = sinkLinkTracker.linkGroups.count;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get source nodes from link groups (these are properly bound via PwNodeLinkTracker)
|
||||
for (var i = 0; i < linkGroupsCount; i++) {
|
||||
var linkGroup;
|
||||
if (sinkLinkTracker.linkGroups.get) {
|
||||
// ObjectModel
|
||||
linkGroup = sinkLinkTracker.linkGroups.get(i);
|
||||
} else {
|
||||
// Array
|
||||
linkGroup = sinkLinkTracker.linkGroups[i];
|
||||
}
|
||||
|
||||
if (!linkGroup || !linkGroup.source) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var sourceNode = linkGroup.source;
|
||||
|
||||
// Include stream nodes (applications) - these can be virtual sinks created by apps
|
||||
// Virtual sinks (isStream: true, isSink: true) are application-created nodes
|
||||
// Hardware sinks (isStream: false, isSink: true) are actual audio devices
|
||||
if (sourceNode.isStream && sourceNode.audio) {
|
||||
// Avoid duplicates
|
||||
if (!connectedStreamIds[sourceNode.id]) {
|
||||
connectedStreamIds[sourceNode.id] = true;
|
||||
connectedStreams.push(sourceNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return connectedStreams;
|
||||
}
|
||||
|
||||
// Track links to the default sink using PwNodeLinkTracker (properly binds links)
|
||||
PwNodeLinkTracker {
|
||||
id: sinkLinkTracker
|
||||
node: AudioService.sink
|
||||
}
|
||||
|
||||
preferredWidth: Math.round(440 * Style.uiScaleRatio)
|
||||
preferredHeight: Math.round(420 * Style.uiScaleRatio)
|
||||
|
||||
@@ -202,127 +269,473 @@ SmartPanel {
|
||||
}
|
||||
}
|
||||
|
||||
NScrollView {
|
||||
// Tab Bar
|
||||
NTabBar {
|
||||
id: tabBar
|
||||
Layout.fillWidth: true
|
||||
currentIndex: root.currentTabIndex
|
||||
onCurrentIndexChanged: root.currentTabIndex = currentIndex
|
||||
|
||||
NTabButton {
|
||||
Layout.fillWidth: true
|
||||
text: I18n.tr("settings.audio.panel.tabs.devices")
|
||||
tabIndex: 0
|
||||
checked: tabBar.currentIndex === 0
|
||||
}
|
||||
|
||||
NTabButton {
|
||||
Layout.fillWidth: true
|
||||
text: I18n.tr("settings.audio.panel.tabs.applications")
|
||||
tabIndex: 1
|
||||
checked: tabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
|
||||
// Content Stack
|
||||
StackLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
clip: true
|
||||
contentWidth: availableWidth
|
||||
currentIndex: root.currentTabIndex
|
||||
|
||||
// AudioService Devices
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
width: parent.width
|
||||
// Devices Tab
|
||||
NScrollView {
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
clip: true
|
||||
contentWidth: availableWidth
|
||||
|
||||
// -------------------------------
|
||||
// Output Devices
|
||||
ButtonGroup {
|
||||
id: sinks
|
||||
}
|
||||
// AudioService Devices
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
width: parent.width
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: outputColumn.implicitHeight + (Style.marginM * 2)
|
||||
// -------------------------------
|
||||
// Output Devices
|
||||
ButtonGroup {
|
||||
id: sinks
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: outputColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: outputColumn.implicitHeight + (Style.marginM * 2)
|
||||
|
||||
NText {
|
||||
text: I18n.tr("settings.audio.devices.output-device.label")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
ColumnLayout {
|
||||
id: outputColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
// Output Volume Slider
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: localOutputVolume
|
||||
stepSize: 0.01
|
||||
heightRatio: 0.5
|
||||
onMoved: localOutputVolume = value
|
||||
onPressedChanged: localOutputVolumeChanging = pressed
|
||||
text: Math.round(localOutputVolume * 100) + "%"
|
||||
Layout.bottomMargin: Style.marginM
|
||||
}
|
||||
NText {
|
||||
text: I18n.tr("settings.audio.devices.output-device.label")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: AudioService.sinks
|
||||
NRadioButton {
|
||||
ButtonGroup.group: sinks
|
||||
required property PwNode modelData
|
||||
pointSize: Style.fontSizeS
|
||||
text: modelData.description
|
||||
checked: AudioService.sink?.id === modelData.id
|
||||
onClicked: {
|
||||
AudioService.setAudioSink(modelData);
|
||||
localOutputVolume = AudioService.volume;
|
||||
}
|
||||
// Output Volume Slider
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: localOutputVolume
|
||||
stepSize: 0.01
|
||||
heightRatio: 0.5
|
||||
onMoved: function (value) {
|
||||
localOutputVolume = value;
|
||||
}
|
||||
onPressedChanged: function (pressed) {
|
||||
localOutputVolumeChanging = pressed;
|
||||
}
|
||||
text: Math.round(localOutputVolume * 100) + "%"
|
||||
Layout.bottomMargin: Style.marginM
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: AudioService.sinks
|
||||
NRadioButton {
|
||||
ButtonGroup.group: sinks
|
||||
required property PwNode modelData
|
||||
pointSize: Style.fontSizeS
|
||||
text: modelData.description
|
||||
checked: AudioService.sink?.id === modelData.id
|
||||
onClicked: {
|
||||
AudioService.setAudioSink(modelData);
|
||||
localOutputVolume = AudioService.volume;
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// Input Devices
|
||||
ButtonGroup {
|
||||
id: sources
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: inputColumn.implicitHeight + (Style.marginM * 2)
|
||||
|
||||
ColumnLayout {
|
||||
id: inputColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("settings.audio.devices.input-device.label")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
// Input Volume Slider
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: localInputVolume
|
||||
stepSize: 0.01
|
||||
heightRatio: 0.5
|
||||
onMoved: function (value) {
|
||||
localInputVolume = value;
|
||||
}
|
||||
onPressedChanged: function (pressed) {
|
||||
localInputVolumeChanging = pressed;
|
||||
}
|
||||
text: Math.round(localInputVolume * 100) + "%"
|
||||
Layout.bottomMargin: Style.marginM
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: AudioService.sources
|
||||
NRadioButton {
|
||||
ButtonGroup.group: sources
|
||||
required property PwNode modelData
|
||||
pointSize: Style.fontSizeS
|
||||
text: modelData.description
|
||||
checked: AudioService.source?.id === modelData.id
|
||||
onClicked: AudioService.setAudioSource(modelData)
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// Input Devices
|
||||
ButtonGroup {
|
||||
id: sources
|
||||
}
|
||||
// Applications Tab
|
||||
NScrollView {
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
clip: true
|
||||
contentWidth: availableWidth
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: inputColumn.implicitHeight + (Style.marginM * 2)
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
width: parent.width
|
||||
|
||||
ColumnLayout {
|
||||
id: inputColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
// Bind all app stream nodes to access their audio properties
|
||||
PwObjectTracker {
|
||||
id: appStreamsTracker
|
||||
objects: root.appStreams
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("settings.audio.devices.input-device.label")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
Repeater {
|
||||
model: root.appStreams
|
||||
|
||||
// Input Volume Slider
|
||||
NValueSlider {
|
||||
NBox {
|
||||
id: appBox
|
||||
required property PwNode modelData
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: localInputVolume
|
||||
stepSize: 0.01
|
||||
heightRatio: 0.5
|
||||
onMoved: localInputVolume = value
|
||||
onPressedChanged: localInputVolumeChanging = pressed
|
||||
text: Math.round(localInputVolume * 100) + "%"
|
||||
Layout.bottomMargin: Style.marginM
|
||||
}
|
||||
Layout.preferredHeight: appRow.implicitHeight + (Style.marginM * 2)
|
||||
visible: !isCaptureStream
|
||||
|
||||
Repeater {
|
||||
model: AudioService.sources
|
||||
NRadioButton {
|
||||
ButtonGroup.group: sources
|
||||
required property PwNode modelData
|
||||
pointSize: Style.fontSizeS
|
||||
text: modelData.description
|
||||
checked: AudioService.source?.id === modelData.id
|
||||
onClicked: AudioService.setAudioSource(modelData)
|
||||
Layout.fillWidth: true
|
||||
// Track individual node to ensure properties are bound
|
||||
PwObjectTracker {
|
||||
objects: modelData ? [modelData] : []
|
||||
}
|
||||
|
||||
property PwNodeAudio nodeAudio: (modelData && modelData.audio) ? modelData.audio : null
|
||||
property real appVolume: (nodeAudio && nodeAudio.volume !== undefined) ? nodeAudio.volume : 0.0
|
||||
property bool appMuted: (nodeAudio && nodeAudio.muted !== undefined) ? nodeAudio.muted : false
|
||||
property bool volumeChanging: false
|
||||
|
||||
// Check if this is a capture stream (after node is bound)
|
||||
readonly property bool isCaptureStream: {
|
||||
if (!modelData || !modelData.properties)
|
||||
return false;
|
||||
const props = modelData.properties;
|
||||
// Exclude capture streams - check for stream.capture.sink property
|
||||
if (props["stream.capture.sink"] !== undefined) {
|
||||
return true;
|
||||
}
|
||||
const mediaClass = props["media.class"] || "";
|
||||
// Exclude Stream/Input (capture) but allow Stream/Output (playback)
|
||||
if (mediaClass.includes("Capture") || mediaClass === "Stream/Input" || mediaClass === "Stream/Input/Audio") {
|
||||
return true;
|
||||
}
|
||||
const mediaRole = props["media.role"] || "";
|
||||
if (mediaRole === "Capture") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get app name from properties (reactive computed property)
|
||||
// Access modelData.ready to ensure reactivity when node becomes ready
|
||||
readonly property string appName: {
|
||||
if (!modelData) {
|
||||
return "Unknown App";
|
||||
}
|
||||
|
||||
var props = modelData.properties;
|
||||
var desc = modelData.description || "";
|
||||
var name = modelData.name || "";
|
||||
|
||||
// If properties aren't available yet, try description or name
|
||||
if (!props) {
|
||||
if (desc) {
|
||||
return desc;
|
||||
}
|
||||
if (name) {
|
||||
// Try to extract meaningful name from node name
|
||||
var nameParts = name.split(/[-_]/);
|
||||
if (nameParts.length > 0) {
|
||||
var extracted = nameParts[0];
|
||||
if (extracted) {
|
||||
return extracted.charAt(0).toUpperCase() + extracted.slice(1);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
return "Unknown App";
|
||||
}
|
||||
|
||||
// Try to get application name from various properties
|
||||
var computedAppName = props["application.name"] || "";
|
||||
var mediaName = props["media.name"] || "";
|
||||
var mediaTitle = props["media.title"] || "";
|
||||
var appId = props["application.id"] || "";
|
||||
var binaryName = props["application.process.binary"] || "";
|
||||
|
||||
// If we have application.id, try to extract app name from it (e.g., "firefox.desktop" -> "firefox")
|
||||
if (!computedAppName && appId) {
|
||||
var parts = appId.split(".");
|
||||
if (parts.length > 0) {
|
||||
computedAppName = parts[0];
|
||||
// Capitalize first letter and format nicely
|
||||
if (computedAppName) {
|
||||
computedAppName = computedAppName.charAt(0).toUpperCase() + computedAppName.slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try binary name as fallback
|
||||
if (!computedAppName && binaryName) {
|
||||
var binParts = binaryName.split("/");
|
||||
if (binParts.length > 0) {
|
||||
computedAppName = binParts[binParts.length - 1];
|
||||
if (computedAppName) {
|
||||
computedAppName = computedAppName.charAt(0).toUpperCase() + computedAppName.slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority: application.name > media.title > media.name > binary > description > name
|
||||
var result = computedAppName || mediaTitle || mediaName || binaryName || desc || name;
|
||||
|
||||
// If we still don't have a good name, try to extract from node name
|
||||
if (!result || result === "" || result === "Unknown App") {
|
||||
if (name) {
|
||||
// Try to extract meaningful name from node name (e.g., "firefox-1234" -> "firefox")
|
||||
var nameParts = name.split(/[-_]/);
|
||||
if (nameParts.length > 0) {
|
||||
result = nameParts[0];
|
||||
// Capitalize first letter
|
||||
if (result) {
|
||||
result = result.charAt(0).toUpperCase() + result.slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result || "Unknown App";
|
||||
}
|
||||
|
||||
// Get app icon from properties (returns file path)
|
||||
readonly property string appIcon: {
|
||||
if (!modelData) {
|
||||
return ThemeIcons.iconFromName("application-x-executable", "application-x-executable");
|
||||
}
|
||||
|
||||
var props = modelData.properties;
|
||||
if (!props) {
|
||||
// Try to get icon from app name
|
||||
var name = modelData.name || "";
|
||||
if (name) {
|
||||
// Extract app name from node name (e.g., "firefox-1234" -> "firefox")
|
||||
var nameParts = name.split(/[-_]/);
|
||||
if (nameParts.length > 0) {
|
||||
var appName = nameParts[0].toLowerCase();
|
||||
return ThemeIcons.iconFromName(appName, "application-x-executable");
|
||||
}
|
||||
}
|
||||
return ThemeIcons.iconFromName("application-x-executable", "application-x-executable");
|
||||
}
|
||||
|
||||
// Try application.icon-name first (from Pipewire)
|
||||
var iconName = props["application.icon-name"] || "";
|
||||
if (iconName) {
|
||||
var iconPath = ThemeIcons.iconFromName(iconName, "");
|
||||
if (iconPath && iconPath !== "") {
|
||||
return iconPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get app ID and resolve from desktop entry
|
||||
var appId = props["application.id"] || "";
|
||||
if (appId) {
|
||||
var iconPathFromId = ThemeIcons.iconForAppId(appId.toLowerCase(), "");
|
||||
if (iconPathFromId && iconPathFromId !== "") {
|
||||
return iconPathFromId;
|
||||
}
|
||||
}
|
||||
|
||||
// Try application.name
|
||||
var appName = props["application.name"] || "";
|
||||
if (appName) {
|
||||
var iconPathFromName = ThemeIcons.iconFromName(appName.toLowerCase(), "");
|
||||
if (iconPathFromName && iconPathFromName !== "") {
|
||||
return iconPathFromName;
|
||||
}
|
||||
}
|
||||
|
||||
// Try binary name
|
||||
var binaryName = props["application.process.binary"] || "";
|
||||
if (binaryName) {
|
||||
var binParts = binaryName.split("/");
|
||||
if (binParts.length > 0) {
|
||||
var binName = binParts[binParts.length - 1].toLowerCase();
|
||||
var iconPathFromBinary = ThemeIcons.iconFromName(binName, "");
|
||||
if (iconPathFromBinary && iconPathFromBinary !== "") {
|
||||
return iconPathFromBinary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try node name as fallback
|
||||
var name = modelData.name || "";
|
||||
if (name) {
|
||||
var nameParts = name.split(/[-_]/);
|
||||
if (nameParts.length > 0) {
|
||||
var extractedName = nameParts[0].toLowerCase();
|
||||
var iconPathFromNodeName = ThemeIcons.iconFromName(extractedName, "");
|
||||
if (iconPathFromNodeName && iconPathFromNodeName !== "") {
|
||||
return iconPathFromNodeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback
|
||||
return ThemeIcons.iconFromName("application-x-executable", "application-x-executable");
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: appRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
// App Icon
|
||||
Image {
|
||||
id: appIconImage
|
||||
Layout.preferredWidth: Style.baseWidgetSize
|
||||
Layout.preferredHeight: Style.baseWidgetSize
|
||||
source: appBox.appIcon
|
||||
sourceSize.width: Style.baseWidgetSize * 2
|
||||
sourceSize.height: Style.baseWidgetSize * 2
|
||||
smooth: true
|
||||
mipmap: true
|
||||
antialiasing: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
cache: true
|
||||
asynchronous: true
|
||||
|
||||
// Fallback icon if image fails to load
|
||||
NIcon {
|
||||
anchors.fill: parent
|
||||
icon: "apps"
|
||||
pointSize: Style.fontSizeXL
|
||||
color: Color.mPrimary
|
||||
visible: appIconImage.status === Image.Error || appIconImage.status === Image.Null || appBox.appIcon === ""
|
||||
}
|
||||
}
|
||||
|
||||
// App Name and Volume Slider
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXS
|
||||
|
||||
NText {
|
||||
text: appBox.appName || "Unknown App"
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: (appBox.appVolume !== undefined) ? appBox.appVolume : 0.0
|
||||
stepSize: 0.01
|
||||
heightRatio: 0.5
|
||||
enabled: !!(appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true)
|
||||
onMoved: function (value) {
|
||||
if (appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true) {
|
||||
appBox.nodeAudio.volume = value;
|
||||
}
|
||||
}
|
||||
onPressedChanged: function (pressed) {
|
||||
appBox.volumeChanging = pressed;
|
||||
}
|
||||
text: Math.round((appBox.appVolume !== undefined ? appBox.appVolume : 0.0) * 100) + "%"
|
||||
}
|
||||
}
|
||||
|
||||
// Mute Button
|
||||
NIconButton {
|
||||
icon: (appBox.appMuted === true) ? "volume-mute" : "volume-high"
|
||||
tooltipText: (appBox.appMuted === true) ? I18n.tr("tooltips.unmute") : I18n.tr("tooltips.mute")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
enabled: !!(appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true)
|
||||
onClicked: {
|
||||
if (appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true) {
|
||||
appBox.nodeAudio.muted = !appBox.appMuted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
NText {
|
||||
visible: root.appStreams.length === 0
|
||||
text: I18n.tr("settings.audio.panel.applications.empty")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user