Migrating to gnome 45 - import errors

SyntaxError: ambiguous indirect export: GObject @ file:///home/bernhard/.local/share/gnome-shell/extensions/audio-switcher@ahoi.io/extension.js:1:9

import { GObject } from 'gi://GObject';
import { PopupMenu }  from  'resource:///org/gnome/shell/ui/popupMenu.js';
import { Extension as GExtension } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';

const AudioOutputSubMenu = GObject.registerClass({
    GTypeName: 'ASAudioOutputSubMenu',
}, class AudioOutputSubMenu extends PopupMenu.PopupSubMenuMenuItem {
    _init() {
		super._init('Audio Output: Connecting...', true);

		this._control = Main.panel.statusArea.aggregateMenu._volume._control;

		this._controlSignal = this._control.connect('default-sink-changed', () => {
			this._updateDefaultSink();
		});

		this._updateDefaultSink();

		this.menu.connect('open-state-changed', (menu, isOpen) => {
			if (isOpen)
				this._updateSinkList();
		});

		//Unless there is at least one item here, no 'open' will be emitted...
		let item = new PopupMenu.PopupMenuItem('Connecting...');
		this.menu.addMenuItem(item);
	}

	_updateDefaultSink() {
		let defsink = this._control.get_default_sink();
		//Unfortunately, Gvc neglects some pulse-devices, such as all "Monitor of ..."
		if (defsink == null)
			this.label.set_text("Other");
		else
			this.label.set_text(defsink.get_description());
	}

	_updateSinkList() {
		this.menu.removeAll();

		let defsink = this._control.get_default_sink();
		let sinklist = this._control.get_sinks();
		let control = this._control;
		let item;

		for (let i=0; i<sinklist.length; i++) {
			let sink = sinklist[i];
			if (sink === defsink)
				continue;
			item = new PopupMenu.PopupMenuItem(sink.get_description());
			item.connect('activate', () => {
				control.set_default_sink(sink);
			});
			this.menu.addMenuItem(item);
		}
		if (sinklist.length == 0 ||
			(sinklist.length == 1 && sinklist[0] === defsink)) {
			item = new PopupMenu.PopupMenuItem("No more Devices");
			this.menu.addMenuItem(item);
		}
	}

	destroy() {
		this._control.disconnect(this._controlSignal);
		super.destroy();
	}
});

const AudioInputSubMenu = GObject.registerClass({
    GTypeName: 'ASAudioInputSubMenu',
}, class AudioInputSubMenu extends PopupMenu.PopupSubMenuMenuItem {
    _init() {
		super._init('Audio Input: Connecting...', true);

		this._control = Main.panel.statusArea.aggregateMenu._volume._control;


		this._controlSignal = this._control.connect('default-source-changed', () => {
			this._updateDefaultSource();
		});

		this._updateDefaultSource();

		this.menu.connect('open-state-changed', (menu, isOpen) => {
			if (isOpen)
				this._updateSourceList();
		});

		//Unless there is at least one item here, no 'open' will be emitted...
		let item = new PopupMenu.PopupMenuItem('Connecting...');
		this.menu.addMenuItem(item);
	}

	_updateDefaultSource() {
		let defsource = this._control.get_default_source();
		//Unfortunately, Gvc neglects some pulse-devices, such as all "Monitor of ..."
		if (defsource == null)
			this.label.set_text("Other");
		else
			this.label.set_text(defsource.get_description());
	}

	_updateSourceList() {
		this.menu.removeAll();

		let defsource = this._control.get_default_source();
		let sourcelist = this._control.get_sources();
		let control = this._control;
		let item;

		for (var i = 0; i < sourcelist.length; i++) {
			let source = sourcelist[i];
			if (source === defsource) {
				continue;
			}
			item = new PopupMenu.PopupMenuItem(source.get_description());
			item.connect('activate', () => {
				control.set_default_source(source);
			});
			this.menu.addMenuItem(item);
		}
		if (sourcelist.length == 0 ||
			(sourcelist.length == 1 && sourcelist[0] === defsource)) {
			item = new PopupMenu.PopupMenuItem("No more Devices");
			this.menu.addMenuItem(item);
		}
	}

	destroy() {
		this._control.disconnect(this._controlSignal);
		super.destroy();
	}
});


export default class AudioSwitcherExtension extends GExtension {
	constructor(metadata) {
		super(metadata)

		this.audioOutputSubMenu = null;
		this.audioInputSubMenu = null;
		this.savedUpdateVisibility = null;
	}
	enable() {
		if ((audioInputSubMenu != null) || (audioOutputSubMenu != null))
			return;
		this.audioInputSubMenu = new AudioInputSubMenu();
		this.audioOutputSubMenu = new AudioOutputSubMenu();

		//Try to add the switchers right below the sliders...
		let volMen = Main.panel.statusArea.aggregateMenu._volume._volumeMenu;
		let items = volMen._getMenuItems();
		let i = 0;
		let addedInput, addedOutput = false;
		while (i < items.length){
			if (items[i] === volMen._output.item){
				volMen.addMenuItem(audioOutputSubMenu, i+1);
				addedOutput = true;
			} else if (items[i] === volMen._input.item){
				volMen.addMenuItem(audioInputSubMenu, i+2);
				addedInput = true;
			}
			if (addedOutput && addedInput){
				break;
			}
			i++;
		}

		//Make input-slider allways visible.
		this.savedUpdateVisibility = Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._updateVisibility;
		Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._updateVisibility = function () {};
		Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input.item.actor.visible = true;
	}
	disable() {
		this.audioInputSubMenu.destroy();
		this.audioInputSubMenu = null;
		this.audioOutputSubMenu.destroy();
		this.audioOutputSubMenu = null;

		Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._updateVisibility = savedUpdateVisibility;
		this.savedUpdateVisibility = null;
		Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._updateVisibility();
	}
}

The full plugin can be found GitHub - drahnr/audio-switcher: Easily switch between your audio inputs/outputs from the system menu in GNOME 42

Note that i.e. caffeine does the same GObject import and works.

Port Extensions to GNOME Shell 45 | GNOME JavaScript doesn’t help much.

I’d appreciate any help!

You need to drop those {} in line 1 and 2:

import GObject from 'gi://GObject';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
1 Like

Hey, yes, that got me one step further.

Note that Anatomy of an Extension | GNOME JavaScript appears to suggest to do what I had initially, so if you could sheld some light on why it doesn’t work, that’d be much appreciated.

The next error message is

TypeError: Extension is not a constructor

get from

import GObject from 'gi://GObject';
import * as PopupMenu  from  'resource:///org/gnome/shell/ui/popupMenu.js';
import * as Extension from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';

const AudioOutputSubMenu = GObject.registerClass({
    GTypeName: 'ASAudioOutputSubMenu',
}, class AudioOutputSubMenu extends PopupMenu.PopupSubMenuMenuItem {
    _init() {
		super._init('Audio Output: Connecting...', true);

		this._control = Main.panel.statusArea.aggregateMenu._volume._control;

		this._controlSignal = this._control.connect('default-sink-changed', () => {
			this._updateDefaultSink();
		});

		this._updateDefaultSink();

		this.menu.connect('open-state-changed', (menu, isOpen) => {
			if (isOpen)
				this._updateSinkList();
		});

		//Unless there is at least one item here, no 'open' will be emitted...
		let item = new PopupMenu.PopupMenuItem('Connecting...');
		this.menu.addMenuItem(item);
	}

	_updateDefaultSink() {
		let defsink = this._control.get_default_sink();
		//Unfortunately, Gvc neglects some pulse-devices, such as all "Monitor of ..."
		if (defsink == null)
			this.label.set_text("Other");
		else
			this.label.set_text(defsink.get_description());
	}

	_updateSinkList() {
		this.menu.removeAll();

		let defsink = this._control.get_default_sink();
		let sinklist = this._control.get_sinks();
		let control = this._control;
		let item;

		for (let i=0; i<sinklist.length; i++) {
			let sink = sinklist[i];
			if (sink === defsink)
				continue;
			item = new PopupMenu.PopupMenuItem(sink.get_description());
			item.connect('activate', () => {
				control.set_default_sink(sink);
			});
			this.menu.addMenuItem(item);
		}
		if (sinklist.length == 0 ||
			(sinklist.length == 1 && sinklist[0] === defsink)) {
			item = new PopupMenu.PopupMenuItem("No more Devices");
			this.menu.addMenuItem(item);
		}
	}

	destroy() {
		this._control.disconnect(this._controlSignal);
		super.destroy();
	}
});

const AudioInputSubMenu = GObject.registerClass({
    GTypeName: 'ASAudioInputSubMenu',
}, class AudioInputSubMenu extends PopupMenu.PopupSubMenuMenuItem {
    _init() {
		super._init('Audio Input: Connecting...', true);

		this._control = Main.panel.statusArea.aggregateMenu._volume._control;


		this._controlSignal = this._control.connect('default-source-changed', () => {
			this._updateDefaultSource();
		});

		this._updateDefaultSource();

		this.menu.connect('open-state-changed', (menu, isOpen) => {
			if (isOpen)
				this._updateSourceList();
		});

		//Unless there is at least one item here, no 'open' will be emitted...
		let item = new PopupMenu.PopupMenuItem('Connecting...');
		this.menu.addMenuItem(item);
	}

	_updateDefaultSource() {
		let defsource = this._control.get_default_source();
		//Unfortunately, Gvc neglects some pulse-devices, such as all "Monitor of ..."
		if (defsource == null)
			this.label.set_text("Other");
		else
			this.label.set_text(defsource.get_description());
	}

	_updateSourceList() {
		this.menu.removeAll();

		let defsource = this._control.get_default_source();
		let sourcelist = this._control.get_sources();
		let control = this._control;
		let item;

		for (var i = 0; i < sourcelist.length; i++) {
			let source = sourcelist[i];
			if (source === defsource) {
				continue;
			}
			item = new PopupMenu.PopupMenuItem(source.get_description());
			item.connect('activate', () => {
				control.set_default_source(source);
			});
			this.menu.addMenuItem(item);
		}
		if (sourcelist.length == 0 ||
			(sourcelist.length == 1 && sourcelist[0] === defsource)) {
			item = new PopupMenu.PopupMenuItem("No more Devices");
			this.menu.addMenuItem(item);
		}
	}

	destroy() {
		this._control.disconnect(this._controlSignal);
		super.destroy();
	}
});


export default class AudioSwitcherExtension extends Extension {
	audioOutputSubMenu = null;
	audioInputSubMenu = null;
	savedUpdateVisibility = null;

	// constructor(metadata) {
		// super(metadata)
// 
		// this.audioOutputSubMenu = null;
		// this.audioInputSubMenu = null;
		// this.savedUpdateVisibility = null;
	// }
	enable() {
		if ((this.audioInputSubMenu != null) || (this.audioOutputSubMenu != null))
			return;
		this.audioInputSubMenu = new AudioInputSubMenu();
		this.audioOutputSubMenu = new AudioOutputSubMenu();

		//Try to add the switchers right below the sliders...
		let volMen = Main.panel.statusArea.aggregateMenu._volume._volumeMenu;
		let items = volMen._getMenuItems();
		let i = 0;
		let addedInput, addedOutput = false;
		while (i < items.length){
			if (items[i] === volMen._output.item){
				volMen.addMenuItem(audioOutputSubMenu, i+1);
				addedOutput = true;
			} else if (items[i] === volMen._input.item){
				volMen.addMenuItem(audioInputSubMenu, i+2);
				addedInput = true;
			}
			if (addedOutput && addedInput){
				break;
			}
			i++;
		}

		//Make input-slider allways visible.
		this.savedUpdateVisibility = Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._updateVisibility;
		Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._updateVisibility = function () {};
		Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input.item.actor.visible = true;
	}
	disable() {
		this.audioInputSubMenu.destroy();
		this.audioInputSubMenu = null;
		this.audioOutputSubMenu.destroy();
		this.audioOutputSubMenu = null;

		Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._updateVisibility = savedUpdateVisibility;
		this.savedUpdateVisibility = null;
		Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._updateVisibility();
	}
}

regardless if I comment constructor(metdata) {..} or not. Could you shed some light on that too.

Thank you very much for your time and help!

As I mentioned before, that change is only related to the line 1 and 2 in your first code.

That doesn’t work, if I only apply your suggested changes to line 1 and 2, I get:

SyntaxError: ambiguous indirect export: default @ file:///home/bernhard/.local/share/gnome-shell/extensions/audio-switcher@ahoi.io/extension.js:3:7

Edith: tried again, now the only thing left to figure is how to replace the removed aggregate member on the panel.

Thanks again!

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.