import App from 'resource:///com/github/Aylur/ags/app.js'; import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js'; import { Box, Icon, Label, ListBox, Overlay, Revealer, Scrollable } from 'resource:///com/github/Aylur/ags/widget.js'; import CursorBox from '../misc/cursorbox.js'; const SCROLL_THRESH_H = 200; const SCROLL_THRESH_N = 7; /** * @typedef {import('types/widgets/box').default} Box * @typedef {import('types/service/bluetooth').BluetoothDevice} BluetoothDevice */ /** @param {BluetoothDevice} dev */ const BluetoothDevice = (dev) => Box({ class_name: 'menu-item', attribute: { dev }, children: [Revealer({ reveal_child: true, transition: 'slide_down', child: CursorBox({ on_primary_click_release: () => dev.setConnection(true), child: Box({ hexpand: true, children: [ Icon({ icon: dev.bind('icon_name'), }), Label({ label: dev.bind('name'), }), Icon({ icon: 'object-select-symbolic', hexpand: true, hpack: 'end', }).hook(dev, (self) => { self.setCss(`opacity: ${dev.paired ? '1' : '0'}; `); }), ], }), }), })], }); export const BluetoothMenu = () => { const DevList = new Map(); const topArrow = Revealer({ transition: 'slide_down', child: Icon({ icon: `${App.configDir }/icons/down-large.svg`, class_name: 'scrolled-indicator', size: 16, css: '-gtk-icon-transform: rotate(180deg);', }), }); const bottomArrow = Revealer({ transition: 'slide_up', child: Icon({ icon: `${App.configDir }/icons/down-large.svg`, class_name: 'scrolled-indicator', size: 16, }), }); return Overlay({ pass_through: true, overlays: [ Box({ vpack: 'start', hpack: 'center', css: 'margin-top: 12px', children: [topArrow], }), Box({ vpack: 'end', hpack: 'center', css: 'margin-bottom: 12px', children: [bottomArrow], }), ], child: Box({ class_name: 'menu', child: Scrollable({ hscroll: 'never', vscroll: 'never', setup: (self) => { self.on('edge-reached', (_, pos) => { // Manage scroll indicators if (pos === 2) { topArrow.reveal_child = false; bottomArrow.reveal_child = true; } else if (pos === 3) { topArrow.reveal_child = true; bottomArrow.reveal_child = false; } }); }, child: ListBox({ setup: (self) => { // @ts-expect-error self.set_sort_func( /** * @param {Box} a * @param {Box} b */ (a, b) => { // @ts-expect-error return b.get_children()[0].attribute.dev.paired - // eslint-disable-line // @ts-expect-error a.get_children()[0].attribute.dev.paired; }, ); self.hook(Bluetooth, () => { // Get all devices const Devices = Bluetooth.devices.concat( Bluetooth.connected_devices, ); // Add missing devices Devices.forEach((dev) => { if (!DevList.has(dev) && dev.name) { DevList.set(dev, BluetoothDevice(dev)); // @ts-expect-error self.add(DevList.get(dev)); self.show_all(); } }); // Delete ones that don't exist anymore const difference = Array.from(DevList.keys()) .filter((dev) => !Devices .find((d) => dev === d) && dev.name); difference.forEach((dev) => { const devWidget = DevList.get(dev); if (devWidget) { if (devWidget.toDestroy) { devWidget.get_parent().destroy(); DevList.delete(dev); } else { devWidget.children[0] .reveal_child = false; devWidget.toDestroy = true; } } }); // Start scrolling after a specified height // is reached by the children const height = Math.max( self.get_parent()?.get_allocated_height() || 0, SCROLL_THRESH_H, ); const scroll = self.get_parent()?.get_parent(); if (scroll) { // @ts-expect-error const n_child = self.get_children().length; if (n_child > SCROLL_THRESH_N) { // @ts-expect-error scroll.vscroll = 'always'; // @ts-expect-error scroll.setCss(`min-height: ${height}px;`); // Make bottom scroll indicator appear only // when first getting overflowing children if (!(bottomArrow.reveal_child === true || topArrow.reveal_child === true)) { bottomArrow.reveal_child = true; } } else { // @ts-expect-error scroll.vscroll = 'never'; // @ts-expect-error scroll.setCss(''); topArrow.reveal_child = false; bottomArrow.reveal_child = false; } } // Trigger sort_func // @ts-expect-error self.get_children().forEach( /** @param {Box} ListBoxRow */ (ListBoxRow) => { // @ts-expect-error ListBoxRow.changed(); }, ); }); }, }), }), }), }); };