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!