├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug---error-report.md └── images │ ├── QS-PresentationIMG_inkscape.svg │ ├── dev-preview.png │ ├── layout.png │ ├── media-control.png │ ├── notifications.png │ ├── overlay.png │ ├── quick-settings-tweaker.png │ ├── screen_audio-mixer.png │ ├── screen_media-controls.png │ ├── screen_notifications.png │ ├── screenshot_5446_SpzYu18.png │ └── volume-mixer.png ├── .gitignore ├── LICENSE ├── README.md ├── Todo.md ├── dirzsh.zsh ├── docker-compose.example.yml ├── install.sh ├── media ├── Changelog.md ├── dbus.xml ├── hicolor │ └── scalable │ │ └── actions │ │ ├── qst-github-logo-symbolic.svg │ │ ├── qst-gnome-extension-logo-symbolic.svg │ │ └── qst-weblate-logo-symbolic.svg ├── licenses.json ├── qst-project-icon.svg └── rounded_corners.frag ├── metadata.json ├── old ├── features │ └── volumeMixer.ts ├── inputOutput.ts ├── layoutCustomize.ts ├── libs │ ├── streamSlider.ts │ └── volumeMixerHandlerNotImpled.ts ├── menus.ts ├── other.ts ├── panel.ts ├── prefsPages │ └── volumeMixer.ts ├── qst-patreon-logo-symbolic.svg ├── sidebarPrefs.ts └── widgetManager.ts ├── package-lock.json ├── package.json ├── po ├── ca.po ├── cs.po ├── en.po ├── ja.po ├── ko.po ├── pl.po ├── pt.po ├── quick-settings-tweaks@qwreey.pot ├── ru.po └── zh_Hans.po ├── schemas └── org.gnome.shell.extensions.quick-settings-tweaks.gschema.xml ├── scripts ├── contributor-labels.json ├── reindent.js └── version │ ├── gnome-docker-version │ ├── latest-build-number │ ├── latest-middle-version │ ├── latest-minor-version │ └── major-version ├── src ├── ambient.d.ts ├── config.ts ├── extension.ts ├── features │ ├── debug.ts │ ├── layout │ │ ├── dateMenu.ts │ │ ├── sound.ts │ │ ├── systemIndicator.ts │ │ ├── systemItems.ts │ │ └── toggles.ts │ ├── menuAnimation.ts │ ├── overlayMenu.ts │ ├── toggle │ │ ├── dndQuickToggle.ts │ │ └── unsafeQuickToggle.ts │ └── widget │ │ ├── media.ts │ │ ├── notifications.ts │ │ ├── volumeMixer.ts │ │ └── weather.ts ├── global.scss ├── global.ts ├── libs │ ├── prefs │ │ └── components.ts │ ├── shared │ │ ├── colors.ts │ │ ├── imageUtils.ts │ │ ├── jsUtils.ts │ │ ├── logger.ts │ │ ├── maid.ts │ │ └── styleClass.ts │ ├── shell │ │ ├── advani.ts │ │ ├── compat.ts │ │ ├── effects.ts │ │ ├── feature.ts │ │ ├── gesture.ts │ │ ├── quickSettingsUtils.ts │ │ └── styler.ts │ └── types │ │ ├── quickSettingsOrderItem.ts │ │ ├── systemIndicatorOrderItem.ts │ │ └── toggleOrderItem.ts ├── prefPages │ ├── about.ts │ ├── layout.ts │ ├── menu.ts │ ├── toggles.ts │ └── widgets.ts ├── prefs.ts ├── styles │ ├── date-menu.scss │ ├── debug.scss │ ├── media-widget.scss │ ├── message-compact.scss │ ├── message-remove-shadow.scss │ ├── notification-widget.scss │ ├── system-indicator.scss │ ├── volume-mixer-widget.scss │ └── weather-widget.scss ├── stylesheet.scss └── types.d.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [qwreey] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug---error-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug & Error report 3 | about: Create a report to help us improve 4 | title: "[bug] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 33 | 34 | ## Describe the bug 35 | 36 | 37 | ## To Reproduce 38 | 45 | 46 | ## Expected behavior 47 | 51 | 52 | ## Screenshots 53 | 57 | 58 | ## Environment 59 | 60 | 1. gnome-shell version: (ex) GNOME Shell 47.5 61 | 2. distro information: 62 | ``` 63 | (ex) 64 | NAME="Arch Linux" 65 | PRETTY_NAME="Arch Linux" 66 | ID=arch 67 | BUILD_ID=rolling 68 | ``` 69 | 3. Extension version: 2.1 - stable 70 | -------------------------------------------------------------------------------- /.github/images/QS-PresentationIMG_inkscape.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 261 | -------------------------------------------------------------------------------- /.github/images/dev-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwreey/quick-settings-tweaks/a73308887722663166c03652a2644dca2d930ca1/.github/images/dev-preview.png -------------------------------------------------------------------------------- /.github/images/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwreey/quick-settings-tweaks/a73308887722663166c03652a2644dca2d930ca1/.github/images/layout.png -------------------------------------------------------------------------------- /.github/images/media-control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwreey/quick-settings-tweaks/a73308887722663166c03652a2644dca2d930ca1/.github/images/media-control.png -------------------------------------------------------------------------------- /.github/images/notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwreey/quick-settings-tweaks/a73308887722663166c03652a2644dca2d930ca1/.github/images/notifications.png -------------------------------------------------------------------------------- /.github/images/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwreey/quick-settings-tweaks/a73308887722663166c03652a2644dca2d930ca1/.github/images/overlay.png -------------------------------------------------------------------------------- /.github/images/quick-settings-tweaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwreey/quick-settings-tweaks/a73308887722663166c03652a2644dca2d930ca1/.github/images/quick-settings-tweaker.png -------------------------------------------------------------------------------- /.github/images/screen_audio-mixer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwreey/quick-settings-tweaks/a73308887722663166c03652a2644dca2d930ca1/.github/images/screen_audio-mixer.png -------------------------------------------------------------------------------- /.github/images/screen_media-controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwreey/quick-settings-tweaks/a73308887722663166c03652a2644dca2d930ca1/.github/images/screen_media-controls.png -------------------------------------------------------------------------------- /.github/images/screen_notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwreey/quick-settings-tweaks/a73308887722663166c03652a2644dca2d930ca1/.github/images/screen_notifications.png -------------------------------------------------------------------------------- /.github/images/screenshot_5446_SpzYu18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwreey/quick-settings-tweaks/a73308887722663166c03652a2644dca2d930ca1/.github/images/screenshot_5446_SpzYu18.png -------------------------------------------------------------------------------- /.github/images/volume-mixer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwreey/quick-settings-tweaks/a73308887722663166c03652a2644dca2d930ca1/.github/images/volume-mixer.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | i 3 | *.po~ 4 | host/ 5 | docker-compose.yml 6 | node_modules/ 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quick Settings Tweaks [QuickSettings-Tweaker SkeletonUI](https://extensions.gnome.org/extension/5446/quick-settings-tweaker/) 2 | 3 |
4 | 5 | ### Let's tweak Gnome Quick Settings! 6 | 7 | [Get it on GNOME Extensions](https://extensions.gnome.org/extension/5446/quick-settings-tweaker/) 8 | 9 | 10 | Translation status 11 | 12 |
13 |
14 | Quick Settings Tweaker is a Gnome 46+ extension which allows you to customize the new Quick Settings Panel to your liking! 15 | 16 |
17 |
18 |
19 | 20 | ## Features 21 | 22 | |

With this extension, you can...

| How it will appear | 23 | |:-------------------------------|:--------------------:| 24 | |

**Add the Media Controls Widget**

Control your music and videos directly from the Quick Settings, instead of the Date Menu.

For a cooler look, you can also get colors from the cover image and create a gradient.

| Media controls widget screenshot | 25 | |

**Add the Volume Mixer Widget**

Adjust application volumes, without opening extra application.

Place the menu button next to the output slider for a compact and natural layout.

| Volume mixer widget screenshot | 26 | |

**Add the Notifications Widget**

You can check what has been sent to your mailbox or messenger, without missing!

| Notifications widget screenshot | 27 | |

**Layout customize**

Hide, re-order, re-color your panel and Quick Settings layout

Make it simple and keep organized!

| Notifications widget screenshot | 28 | |

**Overlay menu layout**

Your Quick Settings panel is too big?

Try overlay layout! you can customize background and animation style too.

| Notifications widget screenshot | 29 | 30 | ## Sponsor 31 | 32 | You can promote and support development by [github sponsor!](https://github.com/sponsors/qwreey) You can help keep this project maintained 33 | 34 | Here is my sponsors, thank for your support! 35 | 36 | [![sponsors](https://readme-contribs.as93.net/sponsors/qwreey?shape=square&margin=16&perRow=15&title=Qwreey's%20Sponsors&textColor=f5acff&backgroundColor=0e001a&fontFamily=cursive&fontSize=14&limit=90&footerText=none&outerBorderRadius=24)](https://github.com/sponsors/qwreey) 37 | 38 | ## Stars 39 | 40 | [![Star History Chart](https://api.star-history.com/svg?repos=qwreey/quick-settings-tweaks&type=Date)](https://star-history.com/#qwreey/quick-settings-tweaks&Date) 41 | 42 | ## Development 43 | 44 | ### Translations 45 | 46 | 47 | 48 | You can help translate this extension by open a pull request, or using [weblate](http://weblate.paring.moe/engage/gs-quick-settings-tweaks/) 49 | 50 | ### Building 51 | 52 | > Prerequirements: You need to install nodejs, bash, and gnome-shell for compiling extension from source 53 | 54 | You can create development build by executing `TARGET=dev ./install.sh create-release`. make sure run `npm i` first to ensure all build dependencies installed 55 | 56 | Or, you can get nightly preview build from [github releases tab](https://github.com/qwreey/quick-settings-tweaks/releases). Build extension from `dev` branch is not encouraged, because the `dev` branch has unchecked bleeding-edge features not guaranteed to work. github-preview build is tested by developer and much stable than building `dev` branch. 57 | 58 | ### Contribution and Issues 59 | 60 | Keep in mind that there may be one or a few developers, but there may be many issues and users. I think you know how to behave with manners without even having to say it. 61 | 62 | Please check github [project board page](https://github.com/users/qwreey/projects/2) for issue priority and progress. 63 | 64 | #### Raise an issue 65 | 66 | If you want to raise an issue, First, **you must search your issue first.** duplicated issue will be closed, and disturb developer's time. 67 | 68 | Second, **you must attach a related log files, gnome version and extension version informations.** if you don't provide information much about your issue, it is hard to solve your issue. and to be clear, Please use `[migration]`, `[feature]`, `[bug]` prefix for issue title, It is very useful for searching and organizing issues 69 | 70 | And last, **you must use well-formed english** You can use a translator or AI to write it, so avoid wasting time by having the developer translate and take notes. This takes up a surprising amount of the developer's time, making analysis very difficult, especially if the logs are mixed in English and other languages. 71 | 72 | #### PR and code contribution 73 | 74 | If you want to contribute, **you must pull `dev` branch, Not master branch.** master branch is release branch, because AUR and some user distributions use master branch as build source. **If you create pull-request to master branch, it will be closed.** you should re-open PR to dev branch. 75 | 76 | ### Testing 77 | 78 | ![gnome-docker devlopment screenshot](.github/images/dev-preview.png) 79 | 80 | You can test extension with command `./install.sh dev`. You will need tigervnc client and docker in host. Tested in arch linux but it should working on any systemd based platform 81 | 82 | You can re-build extension by log out and close vnc window or send SIGINT to exit dev docker. 83 | -------------------------------------------------------------------------------- /Todo.md: -------------------------------------------------------------------------------- 1 | Scroll view layout for media widget 2 | Touch enchantments: 3 | Notifications: slide to discard 4 | 5 | Readme file 6 | homepage? 7 | 8 | 배터리 알약 아이콘 9 | 10 | widgets: 11 | sound output: 12 | device to hide 13 | show device name 14 | sound input: 15 | always show slider 16 | show device name 17 | device to hide 18 | volume mixer 19 | item to hide 20 | show hidden item button (temporary) 21 | as submenu (like background apps) 22 | 23 | 키보드로 포커스 가능해야함 24 | 25 | 더보기 아이콘 sound card 로 바꾸고 싶은데 그런 옵션이 있으면 좋겠음 26 | 27 | Add cover image round clip effect option 28 | 이미지 비율 설정 기능 29 | 30 | 날씨 로딩중에 라벨 위치 31 | 32 | 패널 전체 오더링 << 가능했으면 좋겠음.......... 왼쪽 오른쪽 중간 고르기 가능하고 33 | 위젯 오더링 34 | 35 | 습도계 36 | 시스템 메뉴에 시간 표시 37 | datemenu format 38 | 백그라운드 앱스 스타일: 일반, 퀵토글, 시스템레이아웃 39 | 열리는 애니메이션 40 | 미디어 타이틀 움직이도록 41 | 42 | show full title 43 | never 44 | when hover, move around 45 | always, move around 46 | when hover, as tooltip 47 | 48 | 어나더: 자동으로 지우는 알림 리스트 (타이틀 regex) 49 | datemenu 왼쪽 레이아웃 오른쪽레이아웃, 메뉴 열지 않기 50 | more dimmer 51 | 52 | option to hide widget header? 53 | 믹서 디바이스 변경 구현하기 54 | -------------------------------------------------------------------------------- /dirzsh.zsh: -------------------------------------------------------------------------------- 1 | $ID:load() { 2 | lib:path-alloc2 "PATHALLOC_$ID" ./scripts : 3 | alias execute="ID=$ID $ID:execute" 4 | alias lg="ID=$ID $ID:lg" 5 | alias dbus-docker="ID=$ID $ID:dbus-docker" 6 | alias notify="ID=$ID $ID:notify" 7 | alias notify-test="ID=$ID $ID:notify-test" 8 | alias prefs="ID=$ID $ID:prefs" 9 | } 10 | 11 | $ID:unload() { 12 | lib:path-free "PATHALLOC_$ID" 13 | unalias execute 14 | unalias lg 15 | unalias dbus-docker 16 | unalias notify 17 | unalias notify-test 18 | unalias prefs 19 | } 20 | 21 | $ID:execute() { 22 | if [ -e ./host/vncready ]; then 23 | sudo docker compose exec -u gnome gnome-docker env DISPLAY=":0" DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus gdbus call -e -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval "$1" 24 | else 25 | gdbus call -e -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval "$1" 26 | fi 27 | } 28 | $ID:dbus-docker() { 29 | sudo docker compose exec -u gnome gnome-docker env DISPLAY=":0" DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus $@ 30 | } 31 | $ID:lg() { 32 | $ID:execute "Main.createLookingGlass().toggle()" 33 | } 34 | $ID:notify() { 35 | if [ -e ./host/vncready ]; then 36 | $ID:dbus-docker notify-send $@ 37 | else 38 | notify-send $@ 39 | fi 40 | } 41 | $ID:notify-test() { 42 | $ID:notify test testmessage -u normal -t 0 43 | } 44 | $ID:prefs() { 45 | if [ -e ./host/vncready ]; then 46 | $ID:dbus-docker gnome-extensions prefs quick-settings-tweaks@qwreey 47 | else 48 | gnome-extensions prefs quick-settings-tweaks@qwreey 49 | fi 50 | } 51 | -------------------------------------------------------------------------------- /docker-compose.example.yml: -------------------------------------------------------------------------------- 1 | services: 2 | gnome-docker: 3 | container_name: gnome-docker 4 | hostname: gnome-docker 5 | build: ./host/gnome-docker 6 | 7 | environment: 8 | VNC_OPTION: -SecurityTypes=None -rfbunixpath /host/vncsocket -rfbport 5900 9 | BEFORE_GNOME: "/extension/install.sh dev-guest" 10 | ROOTMODE: false 11 | XVFB_GPU: false 12 | XVFB_SCREEN_WIDTH: 1080 13 | RDP_OPTION: "-auth /monitors:0" 14 | 15 | # Remote 16 | ports: 17 | # for rdp 18 | - 3389:3389 19 | # for vnc 20 | - 5900:5900 21 | 22 | # For systemd 23 | security_opt: 24 | - seccomp:unconfined 25 | - apparmor:unconfined 26 | cgroup: host 27 | 28 | # Mounts 29 | volumes: 30 | # Systemd requirements 31 | - type: tmpfs 32 | target: /tmp 33 | - type: tmpfs 34 | target: /run 35 | - /sys/fs/cgroup:/sys/fs/cgroup:ro 36 | - /sys/fs/cgroup/system.slice:/sys/fs/cgroup/system.slice:rw 37 | 38 | # Host dir 39 | - ./host:/host:rw 40 | - ./:/extension 41 | - ./host/home:/home/gnome 42 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "$(readlink -f "$0")")" 3 | 4 | function update-po() { 5 | build 6 | 7 | echo '' > messages.po 8 | [ "$?" != "0" ] && echo "update-po: Unable to create ./messages.po file" && return 1 9 | 10 | which xgettext 2>/dev/null >/dev/null 11 | [ "$?" != "0" ] && echo "update-po: xgettext is not installed on this system. please install and try again" && return 1 12 | 13 | find ./target/out -type f \( -name "*.ui" -or -name "*.js" \) | xgettext --from-code utf-8 -j messages.po -f - 14 | [ "$?" != "0" ] && echo "update-po: Unable to update messages.po file by xgettext" && return 1 15 | 16 | sed -i 's|"Content\-Type: text/plain; charset=CHARSET\\n"|"Content-Type: text/plain; charset=UTF-8\\n"|g' messages.po 17 | [ "$?" != "0" ] && echo "update-po: Unable to set charset in messages.po file" && return 1 18 | 19 | find ./po -type f -name "*.po" | xargs -i msgmerge {} messages.po -N --no-wrap -U 20 | [ "$?" != "0" ] && echo "update-po: Failed to update *.po files (msgmerge error)" && return 1 21 | 22 | mv messages.po $(find ./po -type f -name "*.pot") 23 | [ "$?" != "0" ] && echo "update-po: Unable to move messages.po file (pot file not found)" && return 1 24 | 25 | return 0 26 | } 27 | 28 | function fetch-contributors() { 29 | LABELS=$(cat scripts/contributor-labels.json) 30 | echo "[" 31 | FIRST="1" 32 | curl -Ls "https://api.github.com/repos/qwreey/quick-settings-tweaks/contributors?per_page=16&page=1" | while read line; do 33 | if echo $line | grep -oP '^ *{ *$' > /dev/null; then 34 | [ "$FIRST" = "0" ] && echo -e "\t}," 35 | FIRST="0" 36 | echo -e "\t{" 37 | fi 38 | 39 | if NAME=$(echo $line | grep -oP '(?<="login": ").*(?=")'); then 40 | USER_LABEL=$(printf "%s" "$LABELS" | grep -oP "(?<=\"$NAME\": \").*(?=\")") 41 | echo -e "\t\t\"name\": \"$NAME\"," 42 | echo -e "\t\t\"image\": \"$NAME\"," 43 | echo -en "\t\t" 44 | echo "\"label\": \"${USER_LABEL:-ETC}\"," 45 | curl -Lso target/contributors/$NAME.png "https://github.com/$NAME.png?size=38" 46 | fi 47 | if HOMEPAGE=$(echo $line | grep -oP '(?<="html_url": ").*(?=")'); then 48 | echo -e "\t\t\"link\": \"$HOMEPAGE\"" 49 | fi 50 | done 51 | echo -e "\t}" 52 | echo "]" 53 | } 54 | 55 | function build() { 56 | rm -rf target/out 57 | mkdir -p target/out 58 | 59 | # Typescript compiling 60 | ( 61 | npx tsc --noCheck 62 | cp -r target/tsc/* target/out 63 | ) & 64 | TSC_PID=$! 65 | 66 | # Stylesheet compiling 67 | ( 68 | npx sass\ 69 | --no-source-map\ 70 | src/stylesheet.scss:target/out/stylesheet.css 71 | sed $'s/^ /\t/g' -i target/out/stylesheet.css 72 | ) & 73 | SASS_PID=$! 74 | 75 | # Fetch contributors & Copy assets 76 | ( 77 | if [ ! -e target/contributors ]; then 78 | mkdir -p target/contributors 79 | fetch-contributors > target/contributors/data.json 80 | fi 81 | cp metadata.json target/out 82 | cp -r schemas target/out 83 | cp -r media target/out 84 | cp -r target/contributors target/out/media 85 | ) & 86 | COPYING_PID=$! 87 | 88 | # Wait for tasks 89 | wait $TSC_PID 90 | wait $SASS_PID 91 | wait $COPYING_PID 92 | 93 | # Update config metadata 94 | case "$TARGET" in 95 | dev ) 96 | sed 's/isDevelopmentBuild: false/isDevelopmentBuild: true/' -i target/out/config.js 97 | ;; 98 | preview ) 99 | ;; 100 | release ) 101 | sed 's/isReleaseBuild: false/isReleaseBuild: true/' -i target/out/config.js 102 | ;; 103 | github-release ) 104 | sed 's/isReleaseBuild: false/isReleaseBuild: true/' -i target/out/config.js 105 | sed 's/isGithubBuild: false/isGithubBuild: true/' -i target/out/config.js 106 | ;; 107 | github-preview ) 108 | sed 's/isGithubBuild: false/isGithubBuild: true/' -i target/out/config.js 109 | ;; 110 | esac 111 | if [ -z "$VERSION" ]; then 112 | VERSION=$(git branch --show-current) 113 | fi 114 | sed "s/version: \"unknown\"/version: \"$VERSION\"/" -i target/out/config.js 115 | [ ! -z "$BUILD_NUMBER" ] && sed "s/buildNumber: 0/buildNumber: $BUILD_NUMBER/" -i target/out/config.js 116 | 117 | # Change indents for reducing size of target 118 | node scripts/reindent.js -- target/out/**/*.js 119 | 120 | # Pack extension 121 | gnome-extensions pack target/out\ 122 | --podir=../../po\ 123 | --extra-source=features\ 124 | --extra-source=libs\ 125 | --extra-source=prefPages\ 126 | --extra-source=media\ 127 | --extra-source=global.js\ 128 | --extra-source=config.js\ 129 | --out-dir=target\ 130 | --force 131 | [ "$?" != "0" ] && echo "Failed to pack extension" && return 1 132 | 133 | return 0 134 | } 135 | 136 | function enable() { 137 | gnome-extensions enable quick-settings-tweaks@qwreey 138 | } 139 | 140 | function install() { 141 | gnome-extensions install\ 142 | target/quick-settings-tweaks@qwreey.shell-extension.zip\ 143 | --force 144 | [ "$?" != "0" ] && echo "Failed to install extension" && return 1 145 | echo "Extension was installed. logout and login shell, and check extension list." 146 | 147 | return 0 148 | } 149 | 150 | function log() { 151 | journalctl /usr/bin/gnome-shell -f -q --output cat | grep '\[EXTENSION QSTweaks\] ' 152 | } 153 | 154 | function clear-old-po() { 155 | rm ./po/*.po~ 156 | } 157 | 158 | function compile-preferences() { 159 | glib-compile-schemas --targetdir=target/out/schemas schemas 160 | [ "$?" != "0" ] && echo "compile-preferences: glib-compile-schemas command failed" && return 1 161 | 162 | return 0 163 | } 164 | 165 | function increase-middle-version() { 166 | echo $(( $(cat scripts/version/latest-middle-version) + 1 )) > scripts/version/latest-middle-version 167 | echo $(( $(cat scripts/version/latest-build-number) + 1 )) > scripts/version/latest-build-number 168 | echo 1 > scripts/version/latest-minor-version 169 | } 170 | function increase-minor-version() { 171 | echo $(( $(cat scripts/version/latest-build-number) + 1 )) > scripts/version/latest-build-number 172 | echo $(( $(cat scripts/version/latest-minor-version) + 1 )) > scripts/version/latest-minor-version 173 | } 174 | 175 | function create-release() { 176 | VERSION_MAJOR=$(cat scripts/version/major-version) 177 | VERSION_MIDDLE=$(cat scripts/version/latest-middle-version) 178 | VERSION_MINOR=$(cat scripts/version/latest-minor-version) 179 | BUILD_NUMBER=$(cat scripts/version/latest-build-number) 180 | VERSION_TAG="" 181 | case "$TARGET" in 182 | dev ) 183 | VERSION_TAG="-dev$VERSION_MINOR" 184 | ;; 185 | preview ) 186 | VERSION_TAG="-pre$VERSION_MINOR" 187 | ;; 188 | release ) 189 | VERSION_TAG="" 190 | ;; 191 | github-release ) 192 | VERSION_TAG="" 193 | ;; 194 | github-preview ) 195 | VERSION_TAG="-pre$VERSION_MINOR" 196 | ;; 197 | esac 198 | VERSION="$VERSION_MAJOR.$VERSION_MIDDLE$VERSION_TAG" 199 | VERSION=$VERSION BUILD_NUMBER=$BUILD_NUMBER build 200 | cp target/quick-settings-tweaks@qwreey.shell-extension.zip target/$VERSION-$TARGET.zip 201 | } 202 | 203 | function dev() { 204 | if ! sudo echo > /dev/null; then 205 | return 206 | fi 207 | mkdir -p host 208 | [ -e host/extension-ready ] && rm host/extension-ready 209 | mkfifo host/extension-ready 210 | [ -e host/extension-build ] && rm host/extension-build 211 | mkfifo host/extension-build 212 | 213 | # Build 214 | ( 215 | TARGET="${TARGET:-dev}" create-release 216 | echo > host/extension-ready 217 | ) & 218 | 219 | # Watch Build Request 220 | read -d '' INNER_BUILDWATCH << EOF 221 | cat host/extension-build > /dev/null 222 | while true; do 223 | cat host/extension-build > /dev/null 224 | if [ ! -e host/vncready ]; then 225 | break 226 | fi 227 | TARGET="\${TARGET:-dev}" create-release 228 | echo > host/extension-ready 229 | done 230 | EOF 231 | setsid bash -c "$INNER_BUILDWATCH" & 232 | BUILDWATCH_PID=$! 233 | 234 | [ ! -e ./docker-compose.yml ] && cp ./docker-compose.example.yml ./docker-compose.yml 235 | 236 | CURTAG="" 237 | if [ -e "./host/gnome-docker" ]; then 238 | CURTAG="$(git -C host/gnome-docker describe --tags --always --abbrev=0 HEAD)" 239 | else 240 | git clone https://github.com/qwreey/gnome-docker host/gnome-docker --recursive --tags 241 | fi 242 | 243 | TARTAG="$(cat scripts/version/gnome-docker-version)" 244 | if [[ "$CURTAG" != "$TARTAG" ]]; then 245 | git -C host/gnome-docker pull origin master --tags 246 | git -C host/gnome-docker submodule update 247 | git -C host/gnome-docker checkout "$TARTAG" 248 | sudo docker compose -f ./docker-compose.yml build 249 | fi 250 | 251 | COMPOSEFILE="./docker-compose.yml" ./host/gnome-docker/test.sh 252 | rm host/extension-build host/extension-ready 253 | kill $BUILDWATCH_PID 2> /dev/null 254 | wait $BUILDWATCH_PID 255 | exit 0 256 | } 257 | 258 | function dev-guest() { 259 | echo > /host/extension-build 260 | cat /host/extension-ready > /dev/null 261 | install 262 | enable 263 | } 264 | 265 | function usage() { 266 | echo 'Usage: ./install.sh COMMAND' 267 | echo 'COMMAND:' 268 | echo " install install the extension in the user's home directory" 269 | echo ' under ~/.local' 270 | echo ' build Creates a zip file of the extension' 271 | echo ' update-po Update po files to match source files' 272 | echo ' dev Run dev docker' 273 | echo ' log show extension logs (live)' 274 | echo ' clear-old-po clear *.po~' 275 | echo ' enable enable extension' 276 | echo ' install-enable install and enable' 277 | echo ' compile-preferences compile schema file (test)' 278 | } 279 | 280 | case "$1" in 281 | "install" ) 282 | install 283 | ;; 284 | 285 | "install-enable" ) 286 | install 287 | enable 288 | ;; 289 | 290 | "build" ) 291 | build 292 | ;; 293 | 294 | "log" ) 295 | log 296 | ;; 297 | 298 | "update-po" ) 299 | update-po 300 | ;; 301 | 302 | "clear-old-po" ) 303 | clear-old-po 304 | ;; 305 | 306 | "enable" ) 307 | enable 308 | ;; 309 | 310 | "dev" ) 311 | dev 312 | ;; 313 | "dev-guest" ) 314 | dev-guest 315 | ;; 316 | 317 | "compile-preferences") 318 | compile-preferences 319 | ;; 320 | 321 | "increase-minor-version") 322 | increase-minor-version 323 | ;; 324 | 325 | "increase-middle-version") 326 | increase-middle-version 327 | ;; 328 | 329 | "create-release") 330 | create-release 331 | ;; 332 | 333 | * ) 334 | usage 335 | ;; 336 | esac 337 | exit 338 | -------------------------------------------------------------------------------- /media/Changelog.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # 2.1-stable 6 | 7 | 8 | 9 | 10 | 11 | {{HEADER}} 12 | 13 | - Layout editor shows only useful items 14 | > For example, a DND quick toggle only appears when enabled 15 | - Migration for gnome 48 (partially) 16 | - 'vertical' property migration 17 | - Fix some typescript type errors 18 | - Update locales 19 | - Add github sponsor (replace original one) 20 | 21 | ## New Features 22 | 23 | - Menu animation 24 | - Add background brightness option 25 | - Weather widget 26 | - Add show or hide location label option 27 | - Add max forecasts option 28 | - Add weather interval option 29 | - System Indicators 30 | - Monochrome option for privacy indicators 31 | 32 | ## Fix 33 | 34 | - System Indicators 35 | - Fix accent indicators color not match with shell accent color 36 | - Weather widget 37 | - Fix status label style 38 | - Fix project name 39 | 40 | # 2.1-pre7 41 | 42 | 43 | 44 | 45 | 46 | {{HEADER}} 47 | 48 | - Improve ordering editors 49 | - Update locale files 50 | - Migration for gnome 48 (partially) 51 | - Media widget: Lowered GNOME Shell API dependency for backwards compatibility 52 | 53 | ## New Features 54 | 55 | - Quick toggle layout 56 | - Add GType name filter option 57 | - Now you can hide 'Unsorted items' 58 | - System indicators layout 59 | - Add ordering and hiding option 60 | - Add accent screen sharing & recording indicators option 61 | - Add accent privacy indicators option 62 | 63 | ## Fix 64 | 65 | - Default value optimization for menu animation 66 | - Fix broken scrollbar padding 67 | - Fix mixer description only shows 'Playback Stream' 68 | - Fix smooth scroll cause scrolling issue on media widget 69 | - Fix mixer widget menu section initial state 70 | - Fix #170, some quick toggles are not hiding 71 | - Fix 'has been already disposed' error on weather widget 72 | - Fix '(intermediate value).Extension.features is null' error when extension unloading 73 | 74 | # 2.1-pre6 75 | 76 | 77 | 78 | 79 | 80 | {{HEADER}} 81 | 82 | - Reduce reloading cost 83 | 84 | ## New Features 85 | 86 | - Media widget 87 | - Add adjust smooth scroll speed option 88 | - Overlay menu 89 | - Smoother animation 90 | - Better animation start offset for flyout style 91 | - Volume mixer widget 92 | - Add show stream icon option 93 | - Add attach menu to output slider option 94 | 95 | ## Fix 96 | 97 | - Media widget 98 | - Fix 'event.moveStartCoords is undefined' error 99 | 100 | # 2.1-pre5 101 | 102 | 103 | 104 | 105 | 106 | {{HEADER}} 107 | 108 | - List the license in more detail 109 | - Weather feature is now stable 110 | 111 | ## New Features 112 | 113 | - Media widget 114 | - Add round clip effect detailed option 115 | - Support trackpad and trackpoint smooth scroll 116 | - Date menu 117 | - Add hide left box option 118 | - Add hide right box option 119 | - Add menu disable option 120 | - Add menu button hide option 121 | - VolumeMixer widget 122 | - Migrated from 1.18 123 | 124 | ## Fix 125 | 126 | - Fix 'st_widget_get_theme_node called on the widget which is not in the stage' issue 127 | 128 | ## Prefs QOL patch 129 | 130 | - Add detailed button on some options 131 | 132 | # 2.1-pre4 133 | 134 | 135 | 136 | 137 | 138 | {{HEADER}} 139 | 140 | - Changelog viewer enchantments 141 | - Show build number, git hash, and date in detailed view 142 | 143 | ## New Features 144 | 145 | - Media widget 146 | - Add gradient background option 147 | - Add progress bar style option 148 | - Add contorl button opacity option 149 | - Implement swipe to switch page 150 | - Implement round clip effect to make transition better 151 | - Weather widget 152 | - Add click command option 153 | - Debugging 154 | - Expose features 155 | 156 | ## Fix 157 | 158 | - Fix gnome-shell segfault on dragging media widget 159 | - Media widget 160 | - Fix page indicator click action 161 | 162 | ## New Licenses 163 | 164 | - rounded-window-corners 165 | - Auther: yilozt 166 | - URL: https://github.com/yilozt/rounded-window-corners 167 | 168 | # 2.1-pre3 169 | 170 | 171 | 172 | 173 | 174 | {{HEADER}} 175 | 176 | - Update repo url in metadata 177 | 178 | ## New Features 179 | 180 | - Option to hide dnd indicator completely 181 | - Debugging 182 | - Use better logging format 183 | - Add logging level option 184 | - Add extension environment expose option 185 | - Notifications widget 186 | - Add vfade offset option 187 | - Add scrollbar visibility option 188 | 189 | ## Fix 190 | 191 | - Fix 'PageIndicators has been already disposed' issue 192 | - Add more promise catch handlers and source naming for debugging 193 | - Removed fixStScrollViewScrollbarOverflow, use vscrollbar_policy instead 194 | 195 | ## Known issues 196 | 197 | - Cannot hide keyboard quick toggle 198 | 199 | # 2.1-pre2 200 | 201 | 202 | 203 | 204 | 205 | {{HEADER}} 206 | 207 | ## New Features 208 | 209 | - Add quick toggle ordering and hiding 210 | - Add page indicator on media widget 211 | - Add DND indicator position option 212 | - Add save last session state option on unsafe mode quick toggle 213 | 214 | ## Fix 215 | 216 | - Fix changelog text align 217 | - Fix pref page scroll flickering issue 218 | - Fix weather widget empty when no location selected issue 219 | 220 | ## Prefs QOL patch 221 | 222 | - Move contributor rows to dialog 223 | 224 | # 2.1-pre1 225 | 226 | 227 | 228 | 229 | 230 | {{HEADER}} 231 | 232 | - **Droped gnome-shell 43 and 44 support COMPLETELY** due to ESM incompatible 233 | - Using major.middle.minor version system 234 | - Using girs and typescript, for better development 235 | - New stable, github-stable, github-preview release channel 236 | 237 | ## Shell version bump 238 | 239 | shell-version >= 45, >= 48 240 | 241 | ## Prefs QOL patch 242 | 243 | - Some space rich option groups are now using bottom sheet layout 244 | - Add button for reset modified options 245 | - Organize about section 246 | - Add changelogs subpage 247 | 248 | ## New Features 249 | 250 | - Reorder and hide system items 251 | - Reanimate menu, overlay menu mode 252 | - Weather widget 253 | 254 | ## Known issues 255 | 256 | - Sometime, the media progress bar displayed even should't be displayed 257 | - Weather widget shows empty box when region wasn't selected 258 | -------------------------------------------------------------------------------- /media/dbus.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /media/hicolor/scalable/actions/qst-github-logo-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /media/hicolor/scalable/actions/qst-gnome-extension-logo-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /media/hicolor/scalable/actions/qst-weblate-logo-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /media/licenses.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "licenseUri": "https://github.com/qwreey/quick-settings-tweaks/blob/master/LICENSE", 4 | "name": "quick-settings-tweaks", 5 | "author": "qwreey", 6 | "url": "https://github.com/qwreey/quick-settings-tweaks", 7 | "licenseSummary": "LGPL v3.0" 8 | }, 9 | { 10 | "licenseUri": "https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/COPYING", 11 | "name": "gnome-shell", 12 | "author": "gnome", 13 | "url": "https://gitlab.gnome.org/GNOME/gnome-shell", 14 | "licenseSummary": "GPL v2" 15 | }, 16 | { 17 | "licenseUri": "https://github.com/microsoft/TypeScript/blob/main/LICENSE.txt", 18 | "name": "TypeScript", 19 | "author": "microsoft", 20 | "url": "https://www.typescriptlang.org", 21 | "description": "Typescript compiler", 22 | "licenseSummary": "Apache License 2.0" 23 | }, 24 | { 25 | "licenseUri": "https://github.com/gjsify/gnome-shell/blob/main/LICENSE", 26 | "name": "girs", 27 | "author": "gisify", 28 | "url": "https://github.com/gjsify/gnome-shell", 29 | "description": "Gnome shell typescript types", 30 | "licenseSummary": "MIT License" 31 | }, 32 | { 33 | "licenseUri": "https://github.com/sass/dart-sass/blob/main/LICENSE", 34 | "name": "sass", 35 | "url": "https://sass-lang.com/", 36 | "description": "Stylesheet pre-processor", 37 | "licenseSummary": "MIT License" 38 | }, 39 | { 40 | "name": "gnome-volume-mixer", 41 | "author": "mymindstorm", 42 | "url": "https://github.com/mymindstorm/gnome-volume-mixer", 43 | "licenseUri": "https://github.com/mymindstorm/gnome-volume-mixer/blob/master/LICENSE", 44 | "description": "Volume mixer widget", 45 | "affectedFiles": [ 46 | "features/widget/volumeMixer.ts" 47 | ], 48 | "licenseSummary": "MIT License" 49 | }, 50 | { 51 | "licenseUri": "https://github.com/yilozt/rounded-window-corners/blob/main/LICENSE", 52 | "name": "rounded-window-corners", 53 | "author": "yilozt", 54 | "url": "https://github.com/yilozt/rounded-window-corners", 55 | "affectedFiles": [ 56 | "media/rounded_corners.frag", 57 | "libs/roundClip.ts" 58 | ], 59 | "description": "Round clip shader" 60 | } 61 | ] 62 | -------------------------------------------------------------------------------- /media/qst-project-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /media/rounded_corners.frag: -------------------------------------------------------------------------------- 1 | // This shader is copied from Mutter project: 2 | // https://gitlab.gnome.org/GNOME/mutter/-/blob/main/src/compositor/meta-background-content.c 3 | // 4 | // With a litte change to make it works well with windows 5 | 6 | // The uniforms variables for controls 7 | uniform vec4 bounds; // x, y: top left; z, w: bottom right 8 | uniform float clip_radius; 9 | uniform vec4 inner_bounds; 10 | uniform float inner_clip_radius; 11 | uniform vec2 pixel_step; 12 | uniform float border_width; 13 | uniform vec4 border_color; 14 | uniform float exponent; 15 | 16 | 17 | float circle_bounds(vec2 p, vec2 center, float clip_radius) { 18 | vec2 delta = p - vec2(center.x, center.y); 19 | float dist_squared = dot(delta, delta); 20 | 21 | // Fully outside the circle 22 | float outer_radius = clip_radius + 0.5; 23 | if(dist_squared >= (outer_radius * outer_radius)) 24 | return 0.0; 25 | 26 | // Fully inside the circle 27 | float inner_radius = clip_radius - 0.5; 28 | if(dist_squared <= (inner_radius * inner_radius)) 29 | return 1.0; 30 | 31 | // Only pixels on the edge of the curve need expensive antialiasing 32 | return outer_radius - sqrt(dist_squared); 33 | } 34 | 35 | float squircle_bounds(vec2 p, vec2 center, float clip_radius, float exponent) { 36 | vec2 delta = abs(p - center); 37 | 38 | float pow_dx = pow(delta.x, exponent); 39 | float pow_dy = pow(delta.y, exponent); 40 | 41 | float dist = pow(pow_dx + pow_dy, 1.0 / exponent); 42 | 43 | return clamp(clip_radius - dist + 0.5, 0.0, 1.0); 44 | } 45 | 46 | float rounded_rect_coverage(vec2 p, vec4 bounds, float clip_radius, float exponent) { 47 | // Outside the bounds 48 | if(p.x < bounds.x || p.x > bounds.z || p.y < bounds.y || p.y > bounds.w) { 49 | return 0.0; 50 | } 51 | 52 | vec2 center; 53 | 54 | float center_left = bounds.x + clip_radius; 55 | float center_right = bounds.z - clip_radius; 56 | 57 | if(p.x < center_left) 58 | center.x = center_left; 59 | else if(p.x > center_right) 60 | center.x = center_right; 61 | else 62 | return 1.0; // The vast majority of pixels exit early here 63 | 64 | float center_top = bounds.y + clip_radius; 65 | float center_bottom = bounds.w - clip_radius; 66 | 67 | if(p.y < center_top) 68 | center.y = center_top; 69 | else if(p.y > center_bottom) 70 | center.y = center_bottom; 71 | else 72 | return 1.0; 73 | 74 | if(exponent <= 2.0) { 75 | return circle_bounds(p, center, clip_radius); 76 | } else { 77 | return squircle_bounds(p, center, clip_radius, exponent); 78 | } 79 | } 80 | 81 | void main() { 82 | vec2 texture_coord = cogl_tex_coord0_in.xy / pixel_step; 83 | 84 | float outer_alpha = rounded_rect_coverage(texture_coord, bounds, clip_radius, exponent); 85 | 86 | if(border_width > 0.9 || border_width < -0.9) { 87 | float inner_alpha = rounded_rect_coverage(texture_coord, inner_bounds, inner_clip_radius, exponent); 88 | float border_alpha = clamp(abs(outer_alpha - inner_alpha), 0.0, 1.0); 89 | if (border_width > 0.0) { 90 | // Clip corners of window first 91 | cogl_color_out *= outer_alpha; 92 | // Then mix Rounded window and border 93 | cogl_color_out = mix(cogl_color_out, vec4(border_color.rgb, 1.0), border_alpha * border_color.a); 94 | } else { 95 | // Fill an rounded rectangle with border color first 96 | vec4 border_rect = vec4(border_color.rgb, 1.0) * inner_alpha * border_color.a; 97 | // Then mix rounded window and border, rounded window is smaller than border_rect 98 | cogl_color_out = mix(border_rect, cogl_color_out, outer_alpha); 99 | } 100 | } else { 101 | cogl_color_out *= outer_alpha; 102 | } 103 | } -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Quick Settings Tweaks", 3 | "uuid": "quick-settings-tweaks@qwreey", 4 | "description": "Let's tweak gnome's quick settings! You can add Media Controls, Notifications, Volume Mixer on quick settings and remove useless buttons!", 5 | "shell-version": ["46", "47", "48"], 6 | "url": "https://github.com/qwreey/quick-settings-tweaks", 7 | "settings-schema": "org.gnome.shell.extensions.quick-settings-tweaks", 8 | "gettext-domain": "quick-settings-tweaks", 9 | "donations": { 10 | "github": "qwreey" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /old/features/volumeMixer.ts: -------------------------------------------------------------------------------- 1 | import { featureReloader } from "../libs/utility.js" 2 | import { VolumeMixer } from "../libs/volumeMixerHandler.js" 3 | import { Global } from "../global.js" 4 | 5 | export class VolumeMixerFeature { 6 | load() { 7 | let settings = Global.Settings 8 | 9 | // setup reloader 10 | featureReloader.enableWithSettingKeys(this, [ 11 | "volume-mixer-enabled", 12 | "volume-mixer-position", 13 | "volume-mixer-filtered-apps", 14 | "volume-mixer-show-description", 15 | "volume-mixer-show-icon", 16 | "volume-mixer-filter-mode", 17 | "volume-mixer-use-regex", 18 | "volume-mixer-check-description" 19 | ]) 20 | 21 | // check is feature enabled 22 | if (!Global.Settings.get_boolean("volume-mixer-enabled")) return 23 | 24 | // Make volume mixer 25 | this.volumeMixer = new VolumeMixer({ 26 | 'volume-mixer-filtered-apps': settings.get_strv("volume-mixer-filtered-apps"), 27 | 'volume-mixer-filter-mode': settings.get_string("volume-mixer-filter-mode"), 28 | 'volume-mixer-show-description': settings.get_boolean("volume-mixer-show-description"), 29 | 'volume-mixer-show-icon': settings.get_boolean("volume-mixer-show-icon"), 30 | 'volume-mixer-check-description': settings.get_boolean("volume-mixer-check-description"), 31 | 'volume-mixer-use-regex': settings.get_boolean("volume-mixer-use-regex") 32 | }) 33 | 34 | // Insert volume mixer into Quick Settings 35 | Global.QuickSettingsMenu.addItem(this.volumeMixer.actor, 2) 36 | if (Global.Settings.get_string("volume-mixer-position") === "top") { 37 | Global.GetStreamSlider(({ InputStreamSlider }) => { 38 | Global.QuickSettingsMenu._grid.set_child_above_sibling( 39 | this.volumeMixer.actor, 40 | InputStreamSlider 41 | ) 42 | }) 43 | } 44 | } 45 | 46 | unload() { 47 | // disable feature reloader 48 | featureReloader.disable(this) 49 | if (this.volumeMixer) this.volumeMixer.destroy() 50 | this.volumeMixer = null 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /old/inputOutput.ts: -------------------------------------------------------------------------------- 1 | import { Global } from "./global.js" 2 | import St from "gi://St" 3 | import * as Volume from "resource:///org/gnome/shell/ui/status/volume.js" 4 | import * as PopupMenu from "resource:///org/gnome/shell/ui/popupMenu.js" 5 | import { FeatureBase, type SettingLoader } from "./libs/shell/feature.js" 6 | import { logger } from "./libs/shared/logger.js" 7 | export class SoundTweakFeature extends FeatureBase { 8 | // #region settings 9 | outputShowSelected: boolean 10 | inputShowSelected: boolean 11 | inputAlwaysShow: boolean 12 | outputHide: {name: string}[] 13 | inputHide: {name: string}[] 14 | loadSettings(loader: SettingLoader): void { 15 | this.outputShowSelected = loader.loadBoolean("sound-output-show-selected") 16 | this.inputShowSelected = loader.loadBoolean("sound-input-show-selected") 17 | this.inputAlwaysShow = loader.loadBoolean("sound-input-always-show") 18 | this.outputHide = loader.loadValue("sound-output-hide") 19 | this.inputHide = loader.loadValue("sound-input-hide") 20 | } 21 | // #endregion settings 22 | 23 | onLoad() { 24 | if (this.outputShowSelected) { 25 | const label = this.maid.destroyJob(new St.Label({ 26 | style_class: "QSTWEAKS-volume-mixer-label" 27 | })) 28 | Global.QuickSettingsMenu.addItem(label, 2) 29 | Global.GetStreamSlider().then(({ OutputStreamSlider }) => { 30 | Global.QuickSettingsGrid.set_child_below_sibling(label, OutputStreamSlider) 31 | }).catch(logger.error) 32 | } 33 | } 34 | onUnload(): void {} 35 | } 36 | 37 | unload() { 38 | // disable feature reloader 39 | featureReloader.disable(this) 40 | 41 | if (this._inputListener) { 42 | this._detachInputLabel() 43 | Volume.getMixerControl().disconnect(this._inputListener) 44 | this._inputListener = null 45 | } 46 | if (this._inputVisibilityListener) { 47 | let inputVisibilityListener = this._inputVisibilityListener 48 | this._inputVisibilityListener = null 49 | Global.GetStreamSlider(({ InputStreamSlider }) => { 50 | InputStreamSlider.disconnect(inputVisibilityListener) 51 | InputStreamSlider.visible = InputStreamSlider._shouldBeVisible() 52 | }) 53 | } 54 | if (this._outputListener) { 55 | this._detachOutputLabel() 56 | Volume.getMixerControl().disconnect(this._outputListener) 57 | this._outputListener = null 58 | } 59 | } 60 | 61 | // =========================================== Ouput =========================================== 62 | _setupOutputChangedListener() { 63 | this._attachOutputLabel() 64 | this._outputListener = Volume.getMixerControl().connect('active-output-update', (c, id) => this._onOutputDeviceChanged(id)) 65 | } 66 | 67 | _onOutputDeviceChanged(deviceId) { 68 | const device = Volume.getMixerControl().lookup_output_id(deviceId) 69 | this.outputLabel.text = this._getDeviceName(device) 70 | } 71 | 72 | _attachOutputLabel() { 73 | this.outputLabel = new St.Label() 74 | this.outputLabel.style_class = "QSTWEAKS-volume-mixer-label" 75 | Global.QuickSettingsMenu.addItem(this.outputLabel, 2) 76 | this.outputLabel.visible = Global.Settings.get_boolean("output-show-selected") 77 | Global.GetStreamSlider(({ OutputStreamSlider }) => { 78 | Global.QuickSettingsGrid.set_child_below_sibling(this.outputLabel, OutputStreamSlider) 79 | this.outputLabel.text = this._findActiveDevice(OutputStreamSlider) 80 | }) 81 | } 82 | 83 | _detachOutputLabel() { 84 | if (this.outputLabel && this.outputLabel.get_parent()) { 85 | this.outputLabel.get_parent().remove_child(this.outputLabel) 86 | this.outputLabel = null 87 | } 88 | } 89 | 90 | // =========================================== Input =========================================== 91 | _setupInputChangedListener() { 92 | this._attachInputLabel() 93 | this._inputListener = Volume.getMixerControl().connect('active-input-update', (c, id) => this._onInputDeviceChanged(id)) 94 | } 95 | 96 | _attachInputLabel() { 97 | this.inputLabel = new St.Label() 98 | this.inputLabel.style_class = "QSTWEAKS-volume-mixer-label" 99 | Global.QuickSettingsMenu.addItem(this.inputLabel, 2) 100 | Global.GetStreamSlider(({ InputStreamSlider }) => { 101 | Global.QuickSettingsGrid.set_child_below_sibling(this.inputLabel, InputStreamSlider) 102 | this.inputLabel.text = this._findActiveDevice(InputStreamSlider) 103 | }) 104 | this._setInputLabelVisibility() 105 | } 106 | 107 | _onInputDeviceChanged(deviceId) { 108 | const device = Volume.getMixerControl().lookup_input_id(deviceId) 109 | this.inputLabel.text = this._getDeviceName(device) 110 | } 111 | 112 | _detachInputLabel() { 113 | if (this.inputLabel && this.inputLabel.get_parent()) { 114 | this.inputLabel.get_parent().remove_child(this.inputLabel) 115 | this.inputLabel = null 116 | } 117 | } 118 | 119 | // =========================================== Input Visbility =========================================== 120 | _setupInputVisibilityObserver() { 121 | Global.GetStreamSlider(({ InputStreamSlider }) => { 122 | this._inputVisibilityListener = InputStreamSlider.connect("notify::visible", () => this._onInputStreamSliderSynced()) 123 | this._onInputStreamSliderSynced() 124 | }) 125 | } 126 | 127 | _onInputStreamSliderSynced() { 128 | this._setInputStreamSliderVisibility() 129 | if (this._inputListener) { 130 | this._setInputLabelVisibility() 131 | } 132 | } 133 | 134 | _setInputStreamSliderVisibility() { 135 | Global.GetStreamSlider(({ InputStreamSlider }) => { 136 | InputStreamSlider.visible = InputStreamSlider._shouldBeVisible() || Global.Settings.get_boolean("input-always-show") 137 | }) 138 | } 139 | 140 | 141 | _setInputLabelVisibility() { 142 | Global.GetStreamSlider(({ InputStreamSlider }) => { 143 | this.inputLabel.visible = InputStreamSlider.visible && Global.Settings.get_boolean("input-show-selected") 144 | }) 145 | } 146 | 147 | 148 | // =========================================== Utils =========================================== 149 | _findActiveDevice(sliderObject) { 150 | // find the current selected input and grab the input text from that 151 | let menuChildren = sliderObject.menu.box.get_children()[1].get_children() 152 | for (let index = 0; index < menuChildren.length; index++) { 153 | let item = menuChildren[index] 154 | if (item._ornament == PopupMenu.Ornament.CHECK) { 155 | return item.label.text 156 | } 157 | } 158 | return null 159 | } 160 | 161 | _getDeviceName(device) { 162 | if (!device) 163 | return 164 | 165 | const { description, origin } = device 166 | const name = origin 167 | ? `${description} – ${origin}` 168 | : description 169 | 170 | return name 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /old/layoutCustomize.ts: -------------------------------------------------------------------------------- 1 | import St from "gi://St" 2 | import { Global } from "../global.js" 3 | import { FeatureBase, type SettingLoader } from "../libs/feature.js" 4 | import { QuickMenuToggle } from "resource:///org/gnome/shell/ui/quickSettings.js" 5 | 6 | export class LayoutCustomize extends FeatureBase { 7 | _scroll: St.ScrollView 8 | _sections: St.BoxLayout 9 | 10 | // #region settings 11 | override loadSettings(loader: SettingLoader): void { 12 | } 13 | // #endregion settings 14 | 15 | onChild(actor: QuickMenuToggle) { 16 | actor.get_parent().remove_child(actor) 17 | this._sections.add_child(actor) 18 | } 19 | checkChildren() { 20 | for (const item of Global.QuickSettingsGrid.get_children()) { 21 | if (item instanceof QuickMenuToggle) this.onChild(item) 22 | } 23 | } 24 | 25 | update() { 26 | 27 | } 28 | 29 | override onLoad(): void { 30 | Global.QuickSettingsBox.vertical = false 31 | // Global.QuickSettingsBox.add_child( 32 | // new St.Button({height: 100, width: 100, style: "background-color:red;"}) 33 | // ) 34 | 35 | this.maid.connectJob( 36 | Global.QuickSettingsBox, "notify::mapped", ()=>{ 37 | if (Global.QuickSettingsBox.mapped) this.update() 38 | } 39 | ) 40 | } 41 | override onUnload(): void { 42 | Global.QuickSettingsBox.vertical = true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /old/libs/streamSlider.ts: -------------------------------------------------------------------------------- 1 | import GObject from "gi://GObject" 2 | import Gio from "gi://Gio" 3 | import GLib from "gi://GLib" 4 | import Gvc from "gi://Gvc" 5 | 6 | import { QuickSlider } from "resource:///org/gnome/shell/ui/quickSettings.js" 7 | import * as PopupMenu from "resource:///org/gnome/shell/ui/popupMenu.js" 8 | 9 | const ALLOW_AMPLIFIED_VOLUME_KEY = 'allow-volume-above-100-percent' 10 | 11 | class StreamSlider extends QuickSlider { 12 | _init(control) { 13 | super._init() 14 | 15 | this._connections = [] // ADDED BY QWREEY 16 | this._control = control 17 | 18 | this._inDrag = false 19 | this._notifyVolumeChangeId = 0 20 | 21 | this._soundSettings = new Gio.Settings({ 22 | schema_id: 'org.gnome.desktop.sound', 23 | }) 24 | 25 | // MODED BY QWREEY 26 | this._connections.push([ 27 | this._soundSettings, 28 | this._soundSettings.connect(`changed::${ALLOW_AMPLIFIED_VOLUME_KEY}`, 29 | () => this._amplifySettingsChanged()) 30 | ]) 31 | this._amplifySettingsChanged() 32 | 33 | this._sliderChangedId = this.slider.connect('notify::value', 34 | () => this._sliderChanged()) 35 | this._connections.push([ // ADDED BY QWREEY 36 | this.slider, this._sliderChangedId 37 | ]) 38 | this._connections.push([ // MODED BY QWREEY 39 | this.slider, 40 | this.slider.connect('drag-begin', () => (this._inDrag = true)) 41 | ]) 42 | this._connections.push([ // MODED BY QWREEY 43 | this.slider, 44 | this.slider.connect('drag-end', () => { 45 | this._inDrag = false 46 | this._notifyVolumeChange() 47 | }) 48 | ]) 49 | 50 | this._deviceItems = new Map() 51 | 52 | this._deviceSection = new PopupMenu.PopupMenuSection() 53 | this.menu.addMenuItem(this._deviceSection) 54 | 55 | this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()) 56 | this.menu.addSettingsAction(_('Sound Settings'), 'gnome-sound-panel.desktop') 57 | 58 | this._stream = null 59 | this._volumeCancellable = null 60 | this._icons = [] 61 | 62 | this._sync() 63 | this._connections.push([ // ADDED BY QWREEY 64 | this, 65 | this.connect('destroy', this._destroy.bind(this)) 66 | ]) 67 | } 68 | 69 | get stream() { 70 | return this._stream 71 | } 72 | 73 | set stream(stream) { 74 | this._stream?.disconnectObject(this) 75 | 76 | this._stream = stream 77 | 78 | if (this._stream) { 79 | this._connectStream(this._stream) 80 | this._updateVolume() 81 | } else { 82 | this.emit('stream-updated') 83 | } 84 | 85 | this._sync() 86 | } 87 | 88 | _connectStream(stream) { 89 | stream.connectObject( 90 | 'notify::is-muted', this._updateVolume.bind(this), 91 | 'notify::volume', this._updateVolume.bind(this), this) 92 | } 93 | 94 | _lookupDevice(_id) { 95 | throw new GObject.NotImplementedError( 96 | `_lookupDevice in ${this.constructor.name}`) 97 | } 98 | 99 | _activateDevice(_device) { 100 | throw new GObject.NotImplementedError( 101 | `_activateDevice in ${this.constructor.name}`) 102 | } 103 | 104 | _addDevice(id) { 105 | if (this._deviceItems.has(id)) 106 | return 107 | 108 | const device = this._lookupDevice(id) 109 | if (!device) 110 | return 111 | 112 | const { description, origin } = device 113 | const name = origin 114 | ? `${description} – ${origin}` 115 | : description 116 | const item = new PopupMenu.PopupImageMenuItem(name, device.get_gicon()) 117 | this._connections.push([ 118 | item, 119 | item.connect('activate', () => this._activateDevice(device)) 120 | ]) 121 | 122 | this._deviceSection.addMenuItem(item) 123 | this._deviceItems.set(id, item) 124 | 125 | this._sync() 126 | } 127 | 128 | _removeDevice(id) { 129 | this._deviceItems.get(id)?.destroy() 130 | if (this._deviceItems.delete(id)) 131 | this._sync() 132 | } 133 | 134 | _setActiveDevice(activeId) { 135 | for (const [id, item] of this._deviceItems) { 136 | item.setOrnament(id === activeId 137 | ? PopupMenu.Ornament.CHECK 138 | : PopupMenu.Ornament.NONE) 139 | } 140 | } 141 | 142 | _shouldBeVisible() { 143 | return this._stream != null 144 | } 145 | 146 | _sync() { 147 | this.visible = this._shouldBeVisible() 148 | this.menuEnabled = this._deviceItems.size > 1 149 | } 150 | 151 | _sliderChanged() { 152 | if (!this._stream) 153 | return 154 | 155 | let value = this.slider.value 156 | let volume = value * this._control.get_vol_max_norm() 157 | let prevMuted = this._stream.is_muted 158 | let prevVolume = this._stream.volume 159 | if (volume < 1) { 160 | this._stream.volume = 0 161 | if (!prevMuted) 162 | this._stream.change_is_muted(true) 163 | } else { 164 | this._stream.volume = volume 165 | if (prevMuted) 166 | this._stream.change_is_muted(false) 167 | } 168 | this._stream.push_volume() 169 | 170 | let volumeChanged = this._stream.volume !== prevVolume 171 | if (volumeChanged && !this._notifyVolumeChangeId && !this._inDrag) { 172 | this._notifyVolumeChangeId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 30, () => { 173 | this._notifyVolumeChange() 174 | this._notifyVolumeChangeId = 0 175 | return GLib.SOURCE_REMOVE 176 | }) 177 | GLib.Source.set_name_by_id(this._notifyVolumeChangeId, 178 | '[gnome-shell] this._notifyVolumeChangeId') 179 | } 180 | } 181 | 182 | _notifyVolumeChange() { 183 | if (this._volumeCancellable) 184 | this._volumeCancellable.cancel() 185 | this._volumeCancellable = null 186 | 187 | if (this._stream.state === Gvc.MixerStreamState.RUNNING) 188 | return // feedback not necessary while playing 189 | 190 | this._volumeCancellable = new Gio.Cancellable() 191 | let player = global.display.get_sound_player() 192 | player.play_from_theme('audio-volume-change', 193 | _('Volume changed'), this._volumeCancellable) 194 | } 195 | 196 | _changeSlider(value) { 197 | this.slider.block_signal_handler(this._sliderChangedId) 198 | this.slider.value = value 199 | this.slider.unblock_signal_handler(this._sliderChangedId) 200 | } 201 | 202 | _updateVolume() { 203 | let muted = this._stream.is_muted 204 | this._changeSlider(muted 205 | ? 0 : this._stream.volume / this._control.get_vol_max_norm()) 206 | this.emit('stream-updated') 207 | } 208 | 209 | _amplifySettingsChanged() { 210 | this._allowAmplified = this._soundSettings.get_boolean(ALLOW_AMPLIFIED_VOLUME_KEY) 211 | 212 | this.slider.maximum_value = this._allowAmplified 213 | ? this.getMaxLevel() : 1 214 | 215 | if (this._stream) 216 | this._updateVolume() 217 | } 218 | 219 | getIcon() { 220 | if (!this._stream) 221 | return null 222 | 223 | let volume = this._stream.volume 224 | let n 225 | if (this._stream.is_muted || volume <= 0) { 226 | n = 0 227 | } else { 228 | n = Math.ceil(3 * volume / this._control.get_vol_max_norm()) 229 | n = Math.clamp(n, 1, this._icons.length - 1) 230 | } 231 | return this._icons[n] 232 | } 233 | 234 | getLevel() { 235 | if (!this._stream) 236 | return null 237 | 238 | return this._stream.volume / this._control.get_vol_max_norm() 239 | } 240 | 241 | getMaxLevel() { 242 | let maxVolume = this._control.get_vol_max_norm() 243 | if (this._allowAmplified) 244 | maxVolume = this._control.get_vol_max_amplified() 245 | 246 | return maxVolume / this._control.get_vol_max_norm() 247 | } 248 | 249 | // ADDED BY QWREEY 250 | _destroy() { 251 | GLib.Source.remove(this._notifyVolumeChangeId) 252 | for (const item of this._connections) { 253 | item[0].disconnect(item[1]) 254 | } 255 | this._connections = null 256 | } 257 | } 258 | GObject.registerClass({ 259 | Signals: { 260 | 'stream-updated': {}, 261 | }, 262 | }, StreamSlider) 263 | -------------------------------------------------------------------------------- /old/libs/volumeMixerHandlerNotImpled.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is partially licensed under the gnome-volume-mixer license. 3 | * For more details, please check the license page in the about tab of the extension settings. 4 | */ 5 | import St from "gi://St" 6 | import Gvc from "gi://Gvc" 7 | import GObject from "gi://GObject" 8 | import Gio from "gi://Gio" 9 | import GLib from "gi://GLib" 10 | import { QuickSlider } from "resource:///org/gnome/shell/ui/quickSettings.js" 11 | import * as PopupMenu from "resource:///org/gnome/shell/ui/popupMenu.js" 12 | import * as Volume from "resource:///org/gnome/shell/ui/status/volume.js" 13 | import Maid from "./libs/maid.js" 14 | 15 | const ALLOW_AMPLIFIED_VOLUME_KEY = 'allow-volume-above-100-percent' 16 | 17 | // 디바이스 변경 구현하기 18 | // 루프는 오직 컨테이너가 보이는 상태에서만 작동해야함 19 | class StreamSlider extends QuickSlider { 20 | _init(control: Gvc.MixerControl, stream: Gvc.MixerStream) { 21 | // ... 22 | this._maid.connectJob(this.slider, "drag-begin", () => { this._inDrag = true }) 23 | this._maid.connectJob(this.slider, "drag-end", () => { this._inDrag = false }) 24 | 25 | // this._deviceItems = new Map() 26 | 27 | // this._deviceSection = new PopupMenu.PopupMenuSection() 28 | // this.menu.addMenuItem(this._deviceSection) 29 | 30 | this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()) 31 | this.menu.addSettingsAction(_('Sound Settings'), 'gnome-sound-panel.desktop') 32 | // ... 33 | } 34 | 35 | // ... 36 | _connectStream(stream: Gvc.MixerStream) { 37 | } 38 | // ... 39 | 40 | // _lookupDevice(_id) { 41 | // throw new GObject.NotImplementedError( 42 | // `_lookupDevice in ${this.constructor.name}`) 43 | // } 44 | 45 | // _activateDevice(_device) { 46 | // throw new GObject.NotImplementedError( 47 | // `_activateDevice in ${this.constructor.name}`) 48 | // } 49 | 50 | // _addDevice(id) { 51 | // if (this._deviceItems.has(id)) 52 | // return 53 | 54 | // const device = this._lookupDevice(id) 55 | // if (!device) 56 | // return 57 | 58 | // const { description, origin } = device 59 | // const name = origin 60 | // ? `${description} – ${origin}` 61 | // : description 62 | // const item = new PopupMenu.PopupImageMenuItem(name, device.get_gicon()) 63 | // this._connections.push([ 64 | // item, 65 | // item.connect('activate', () => this._activateDevice(device)) 66 | // ]) 67 | 68 | // this._deviceSection.addMenuItem(item) 69 | // this._deviceItems.set(id, item) 70 | 71 | // this._sync() 72 | // } 73 | 74 | // _removeDevice(id) { 75 | // this._deviceItems.get(id)?.destroy() 76 | // if (this._deviceItems.delete(id)) 77 | // this._sync() 78 | // } 79 | 80 | // _setActiveDevice(activeId) { 81 | // for (const [id, item] of this._deviceItems) { 82 | // item.setOrnament(id === activeId 83 | // ? PopupMenu.Ornament.CHECK 84 | // : PopupMenu.Ornament.NONE) 85 | // } 86 | // } 87 | 88 | // ... 89 | _shouldBeVisible() { 90 | return this._stream != null 91 | } 92 | 93 | // not used 94 | getIcon() { 95 | if (!this._stream) 96 | return null 97 | 98 | let volume = this._stream.volume 99 | let n 100 | if (this._stream.is_muted || volume <= 0) { 101 | n = 0 102 | } else { 103 | n = Math.ceil(3 * volume / this._control.get_vol_max_norm()) 104 | n = Math.clamp(n, 1, this._icons.length - 1) 105 | } 106 | return this._icons[n] 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /old/menus.ts: -------------------------------------------------------------------------------- 1 | // Sound output menu 2 | // Sound input menu 3 | // Bluetooth 4 | // Wifi 5 | // VPN 6 | // Power mode (detailed?) 7 | // Wired 8 | // Hide item from... 9 | -------------------------------------------------------------------------------- /old/other.ts: -------------------------------------------------------------------------------- 1 | import Adw from "gi://Adw" 2 | import GObject from "gi://GObject" 3 | import Gio from "gi://Gio" 4 | import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js" 5 | import Config from "../config.js" 6 | import type QstExtensionPreferences from "../prefs.js" 7 | import { 8 | Group, 9 | SwitchRow, 10 | fixPageScrollIssue, 11 | } from "../libs/prefComponents.js" 12 | 13 | export const OtherPage = GObject.registerClass({ 14 | GTypeName: Config.baseGTypeName+"OtherPage", 15 | }, class OtherPage extends Adw.PreferencesPage { 16 | constructor(settings: Gio.Settings, _prefs: QstExtensionPreferences, window: Adw.PreferencesWindow) { 17 | super({ 18 | name: "Other", 19 | title: _("Other"), 20 | iconName: "preferences-system-symbolic", 21 | }) 22 | fixPageScrollIssue(this) 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /old/panel.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwreey/quick-settings-tweaks/a73308887722663166c03652a2644dca2d930ca1/old/panel.ts -------------------------------------------------------------------------------- /old/prefsPages/volumeMixer.ts: -------------------------------------------------------------------------------- 1 | import Adw from "gi://Adw" 2 | import Gtk from "gi://Gtk" 3 | import GObject from "gi://GObject" 4 | import Gio from "gi://Gio" 5 | 6 | import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js" 7 | 8 | import { 9 | baseGTypeName, 10 | SwitchRow, 11 | makeDropdown 12 | } from "../libs/prefComponents.js" 13 | 14 | export const VolumeMixerAddFilterDialog = GObject.registerClass({ 15 | GTypeName: baseGTypeName+'VolumeMixerAddFilterDialog', 16 | }, class VolumeMixerAddFilterDialog extends Gtk.Dialog { 17 | appNameEntry 18 | filterListData 19 | 20 | constructor(callingWidget, filterListData) { 21 | super({ 22 | use_header_bar: true, 23 | transient_for: callingWidget.get_root(), 24 | destroy_with_parent: true, 25 | modal: true, 26 | resizable: false, 27 | title: _("Add Application to filtering") 28 | }) 29 | 30 | this.filterListData = filterListData 31 | 32 | const addButton = this.add_button(_("Add"), Gtk.ResponseType.OK) 33 | addButton.get_style_context().add_class('suggested-action') 34 | addButton.sensitive = false 35 | this.add_button(_("Cancel"), Gtk.ResponseType.CANCEL) 36 | 37 | const dialogContent = this.get_content_area() 38 | dialogContent.margin_top = 20 39 | dialogContent.margin_bottom = 20 40 | dialogContent.margin_end = 20 41 | dialogContent.margin_start = 20 42 | 43 | const appNameLabel = new Gtk.Label({ 44 | label: _("Application name"), 45 | halign: Gtk.Align.START, 46 | margin_bottom: 10 47 | }) 48 | dialogContent.append(appNameLabel) 49 | 50 | this.appNameEntry = new Gtk.Entry() 51 | this.appNameEntry.connect('activate', () => { 52 | if (this.checkInputValid()) { 53 | this.response(Gtk.ResponseType.OK) 54 | } 55 | }) 56 | dialogContent.append(this.appNameEntry) 57 | 58 | this.appNameEntry.connect("changed", () => { 59 | addButton.sensitive = this.checkInputValid() 60 | }) 61 | } 62 | 63 | checkInputValid() { 64 | if (this.appNameEntry.text.length === 0) { 65 | return false 66 | } else if (this.filterListData.indexOf(this.appNameEntry.text) !== -1) { 67 | return false 68 | } else { 69 | return true 70 | } 71 | } 72 | }) 73 | 74 | export const FilterMode = GObject.registerClass({ 75 | Properties: { 76 | 'name': GObject.ParamSpec.string( 77 | 'name', 'name', 'name', 78 | GObject.ParamFlags.READWRITE, 79 | null), 80 | 'value': GObject.ParamSpec.string( 81 | 'value', 'value', 'value', 82 | GObject.ParamFlags.READWRITE, 83 | null), 84 | }, 85 | }, class FilterMode extends GObject.Object { 86 | _init(name, value) { 87 | super._init({ name, value }) 88 | } 89 | }) 90 | 91 | export const volumeMixerPage = GObject.registerClass({ 92 | GTypeName: baseGTypeName+'volumeMixerPage', 93 | }, class volumeMixerPage extends Adw.PreferencesPage { 94 | constructor(settings) { 95 | // group config 96 | super({ 97 | name: 'volumeMixer', 98 | title: _('Volume Mixer'), 99 | iconName: 'audio-volume-high-symbolic', 100 | }) 101 | 102 | this.settings = settings 103 | this.filterListData = this.settings.get_strv("volume-mixer-filtered-apps") 104 | 105 | const generalGroup = new Adw.PreferencesGroup({ 106 | title: _("General"), 107 | description: _("Enchant input/output slider") 108 | }) 109 | SwitchRow({ 110 | parent: generalGroup, 111 | title: _("Show current audio output selection"), 112 | value: settings.get_boolean("output-show-selected"), 113 | subtitle: _("Always show the current audio output selection above the volume slider"), 114 | bind: "output-show-selected] 115 | }) 116 | SwitchRow({ 117 | parent: generalGroup, 118 | title: _("Show current audio input selection"), 119 | value: settings.get_boolean("input-show-selected"), 120 | subtitle: _("Always show the current audio input selection above the volume slider"), 121 | bind: "input-show-selected] 122 | }) 123 | SwitchRow({ 124 | parent: generalGroup, 125 | title: _("Always show input"), 126 | value: settings.get_boolean("input-always-show"), 127 | subtitle: _("Always show the audio input volume slider, even when there is no audio input stream."), 128 | bind: "input-always-show] 129 | }) 130 | this.add(generalGroup) 131 | 132 | // volumeMixerGroup 133 | const volumeMixerGroup = new Adw.PreferencesGroup({ 134 | title: _("Add volume mixer (PulseAudio, Pipewire)"), 135 | description: _("Turn on to make the volume mixer visible\nForked from https://github.com/mymindstorm/gnome-volume-mixer"), 136 | headerSuffix: SwitchRow({ 137 | title: "", 138 | value: settings.get_boolean("volume-mixer-enabled"), 139 | bind: "volume-mixer-enabled", 140 | }) 141 | }) 142 | makeDropdown({ // move to bottom 143 | parent: volumeMixerGroup, 144 | title: _("Position"), 145 | subtitle: _("Set volume mixer position"), 146 | value: this.settings.get_string('volume-mixer-position'), 147 | type: "string", 148 | bind: [this.settings, 'volume-mixer-position'], 149 | items: [ 150 | {name: _("Top (Below Output/Input slider)"), value: "top"}, 151 | {name: _("Bottom"), value: "bottom"}, 152 | ], 153 | sensitiveBind: "volume-mixer-enabled", 154 | }) 155 | SwitchRow({ // show-description 156 | title: _('Show stream Description'), 157 | subtitle: _('Show audio stream description above the slider'), 158 | value: this.settings.get_boolean('volume-mixer-show-description'), 159 | parent: volumeMixerGroup, 160 | bind: [this.settings, 'volume-mixer-show-description'], 161 | sensitiveBind: "volume-mixer-enabled", 162 | }) 163 | SwitchRow({ // show-icon 164 | title: _('Show stream Icon'), 165 | subtitle: _('Show application icon in front of the slider'), 166 | value: this.settings.get_boolean('volume-mixer-show-icon'), 167 | parent: volumeMixerGroup, 168 | bind: [this.settings, 'volume-mixer-show-icon'], 169 | sensitiveBind: "volume-mixer-enabled", 170 | }) 171 | this.add(volumeMixerGroup) 172 | 173 | // Application filter settings group 174 | const filterGroup = new Adw.PreferencesGroup({ 175 | title: "", 176 | description: _('Filter applications shown in the volume mixer.'), 177 | }) 178 | this.add(filterGroup) 179 | 180 | // filter-mode 181 | makeDropdown({ 182 | parent: filterGroup, 183 | title: _("Filter Mode"), 184 | value: this.settings.get_string('volume-mixer-filter-mode'), 185 | type: "string", 186 | bind: [this.settings, 'volume-mixer-filter-mode'], 187 | items: [ 188 | {name: _("Blacklist"), value: "block"}, 189 | {name: _("Whitelist"), value: "allow"}, 190 | ], 191 | sensitiveBind: "volume-mixer-enabled", 192 | }) 193 | SwitchRow({ 194 | parent: filterGroup, 195 | title: _('Using Javascript Regex'), 196 | subtitle: _('Use Javascript RegExp for filtering app name or description'), 197 | value: this.settings.get_boolean('volume-mixer-use-regex'), 198 | bind: [this.settings, 'volume-mixer-use-regex'], 199 | sensitiveBind: "volume-mixer-enabled", 200 | }) 201 | SwitchRow({ 202 | parent: filterGroup, 203 | title: _("Check Stream Description"), 204 | subtitle: _("Check Description also"), 205 | value: this.settings.get_boolean('volume-mixer-check-description'), 206 | bind: [this.settings, 'volume-mixer-check-description'], 207 | sensitiveBind: "volume-mixer-enabled", 208 | }) 209 | 210 | // group to act as spacer for filter list 211 | this.filteredAppsGroup = new Adw.PreferencesGroup() 212 | settings.bind( 213 | "volume-mixer-enabled", 214 | this.filteredAppsGroup,'sensitive', 215 | Gio.SettingsBindFlags.DEFAULT 216 | ) 217 | this.filteredAppsGroup.sensitive = settings.get_boolean("volume-mixer-enabled") 218 | this.add(this.filteredAppsGroup) 219 | 220 | // List of filtered apps 221 | for (const filteredAppName of this.filterListData) { 222 | this.filteredAppsGroup.add(this.buildFilterListRow(filteredAppName)) 223 | } 224 | 225 | // Add filter entry button 226 | this.createAddFilteredAppButtonRow() 227 | 228 | // TODO: modes 229 | // - group by application 230 | // - group by application but as a dropdown with streams 231 | // - show all streams 232 | // TODO: go thru github issues 233 | // popularity: page 26, 5th from the top 234 | // TODO: style 235 | } 236 | 237 | createAddFilteredAppButtonRow() { 238 | // I wanted to use Adw.PrefrencesRow, but you can't get the 'row-activated' signal unless it's part of a Gtk.ListBox. 239 | // Adw.PrefrencesGroup doesn't extend Gtk.ListBox. 240 | // TODO: Learn a less hacky to do this. I'm currently too new to GTK to know the best practice. 241 | this.addFilteredAppButtonRow = new Adw.ActionRow() 242 | const addIcon = Gtk.Image.new_from_icon_name("list-add") 243 | addIcon.height_request = 40 244 | this.addFilteredAppButtonRow.set_child(addIcon) 245 | this.filteredAppsGroup.add(this.addFilteredAppButtonRow) 246 | // It won't send 'activated' signal w/o this being set. 247 | this.addFilteredAppButtonRow.activatable_widget = addIcon 248 | this.addFilteredAppButtonRow.connect('activated', (callingWidget) => { 249 | this.showFilteredAppDialog(callingWidget, this.filterListData) 250 | }) 251 | } 252 | 253 | buildFilterListRow(filteredAppName) { 254 | const filterListRow = new Adw.PreferencesRow({ 255 | title: filteredAppName, 256 | activatable: false, 257 | }) 258 | 259 | // Make box for custom row 260 | const filterListBox = new Gtk.Box({ 261 | margin_bottom:6, 262 | margin_top: 6, 263 | margin_end: 15, 264 | margin_start: 15 265 | }) 266 | 267 | // Add title 268 | const filterListLabel = Gtk.Label.new(filterListRow.title) 269 | filterListLabel.hexpand = true 270 | filterListLabel.halign = Gtk.Align.START 271 | filterListBox.append(filterListLabel) 272 | 273 | // Add remove button 274 | const filterListButton = new Gtk.Button({ 275 | halign: Gtk.Align.END 276 | }) 277 | 278 | // Add icon to remove button 279 | const filterListImage = Gtk.Image.new_from_icon_name("user-trash-symbolic") 280 | filterListButton.set_child(filterListImage) 281 | 282 | // Tie action to remove button 283 | filterListButton.connect("clicked", (_button) => this.removeFilteredApp(filteredAppName, filterListRow)) 284 | 285 | filterListBox.append(filterListButton) 286 | filterListRow.set_child(filterListBox) 287 | 288 | return filterListRow 289 | } 290 | 291 | removeFilteredApp(filteredAppName, filterListRow) { 292 | this.filterListData.splice(this.filterListData.indexOf(filteredAppName), 1) 293 | this.settings.set_strv("volume-mixer-filtered-apps", this.filterListData) 294 | this.filteredAppsGroup.remove(filterListRow) 295 | } 296 | 297 | addFilteredApp(filteredAppName) { 298 | this.filterListData.push(filteredAppName) 299 | this.settings.set_strv("volume-mixer-filtered-apps", this.filterListData) 300 | this.filteredAppsGroup.remove(this.addFilteredAppButtonRow) 301 | this.filteredAppsGroup.add(this.buildFilterListRow(filteredAppName)) 302 | this.filteredAppsGroup.add(this.addFilteredAppButtonRow) 303 | } 304 | 305 | showFilteredAppDialog(callingWidget, filterListData) { 306 | const dialog = new VolumeMixerAddFilterDialog(callingWidget, filterListData) 307 | dialog.connect('response', (_dialog, response) => { 308 | if (response === Gtk.ResponseType.OK) { 309 | this.addFilteredApp(dialog.appNameEntry.text) 310 | } 311 | dialog.close() 312 | dialog.destroy() 313 | }) 314 | dialog.show() 315 | } 316 | }) 317 | -------------------------------------------------------------------------------- /old/qst-patreon-logo-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /old/sidebarPrefs.ts: -------------------------------------------------------------------------------- 1 | import Gtk from "gi://Gtk" 2 | import Gdk from "gi://Gdk" 3 | import Gio from "gi://Gio" 4 | import Adw from "gi://Adw" 5 | import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js" 6 | import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js" 7 | import { WidgetsPage } from "./prefPages/widgets.js" 8 | import { TogglesPage } from "./prefPages/toggles.js" 9 | import { OtherPage } from "./prefPages/other.js" 10 | import { AboutPage } from "./prefPages/about.js" 11 | import { MenuPage } from "./prefPages/menu.js" 12 | import { ContributorsRow, LicenseRow, Row, Group } from "./libs/prefComponents.js" 13 | import Config from "./config.js" 14 | 15 | var pageList = [ 16 | WidgetsPage, 17 | TogglesPage, 18 | MenuPage, 19 | OtherPage, 20 | AboutPage, 21 | ] 22 | 23 | export default class QstExtensionPreferences extends ExtensionPreferences { 24 | appendIconPath(path: string) { 25 | const iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) 26 | if (!iconTheme.get_search_path().includes(path)) 27 | iconTheme.add_search_path(path) 28 | } 29 | 30 | readExtensionFile(path: string) { 31 | const decoder = new TextDecoder() 32 | const file = Gio.File.new_for_path(`${this.path}/${path}`) 33 | const content = file.load_contents(null)[1] 34 | return decoder.decode(content) 35 | } 36 | 37 | getContributorRows(): ContributorsRow.Contributor[][] { 38 | const contributors = JSON.parse( 39 | this.readExtensionFile("media/contributors/data.json") 40 | ) as ContributorsRow.Contributor[] 41 | if (!contributors.length) return [] 42 | const rows: ContributorsRow.Contributor[][] = [[]] 43 | contributors.reduce((currentRow: ContributorsRow.Contributor[], obj: ContributorsRow.Contributor)=>{ 44 | if (currentRow.length >= 4) rows.push(currentRow = []) 45 | currentRow.push(obj) 46 | return currentRow 47 | }, rows[0]) 48 | return rows 49 | } 50 | 51 | getLicenses(): LicenseRow.License[] { 52 | const licenses = JSON.parse( 53 | this.readExtensionFile("media/licenses.json") 54 | ) as LicenseRow.License[] 55 | for (const item of licenses) { 56 | if (item.file) { 57 | item.content = async () => this.readExtensionFile(item.file) 58 | } 59 | } 60 | return licenses 61 | } 62 | 63 | getVersionString(): string { 64 | let version = Config.version.toUpperCase().replace(/-.*?$/, "") 65 | if (this.metadata.version) { 66 | version += "." + this.metadata.version 67 | } 68 | version += " — " 69 | if (Config.isReleaseBuild) { 70 | version += _("Stable") 71 | } else if (Config.isDevelopmentBuild) { 72 | version += _("Development") 73 | } else { 74 | version += _("Preview") 75 | } 76 | if (Config.isGithubBuild) { 77 | version += " " + _("(Github Release)") 78 | } else if (!this.metadata.version) { 79 | version += " " + _("(Built from source)") 80 | } 81 | return version 82 | } 83 | 84 | getChangelog(): string { 85 | return this.readExtensionFile("media/Changelog.md") 86 | } 87 | 88 | async fillPreferencesWindow(window: Adw.PreferencesWindow) { 89 | let settings = this.getSettings() 90 | 91 | // Register icon path 92 | this.appendIconPath(this.path + "/media") 93 | this.appendIconPath(this.path + "/media/contributors") 94 | 95 | // Set window options 96 | window.set_search_enabled(true) 97 | window.set_default_size(720, 640) 98 | 99 | // Create sidebar area 100 | const sidebar = new Adw.NavigationPage({ 101 | title: this.metadata.name, 102 | width_request: 196, 103 | }) 104 | const sidebarToolbar = new Adw.ToolbarView() 105 | const sidebarHeader = new Adw.HeaderBar() 106 | sidebarToolbar.add_top_bar(sidebarHeader) 107 | sidebar.set_child(sidebarToolbar) 108 | const sidebarPage = new Adw.PreferencesPage() 109 | sidebarToolbar.set_content(sidebarPage) 110 | 111 | // Create content area 112 | const content = new Adw.NavigationPage({ 113 | title: "undefined" 114 | }) 115 | const contentToolbar = new Adw.ToolbarView() 116 | const contentHeader = new Adw.HeaderBar() 117 | contentToolbar.add_top_bar(contentHeader) 118 | content.set_child(contentToolbar) 119 | 120 | // Create navigation 121 | const navigation = new Adw.NavigationSplitView({ 122 | vexpand: true, 123 | hexpand: true, 124 | }) 125 | navigation.set_show_content(true) 126 | navigation.set_sidebar(sidebar) 127 | navigation.set_content(content) 128 | window.set_content(navigation) 129 | window.add(new Adw.PreferencesPage()) 130 | 131 | const open = (page: Adw.PreferencesPage) => { 132 | contentToolbar.content = page 133 | content.title = page.title 134 | } 135 | const sidebarGroup = Group({ 136 | parent: sidebarPage, 137 | }) 138 | for (const PageClass of pageList) { 139 | const page = new PageClass(settings, this, window) 140 | const row = Row({ 141 | parent: sidebarGroup, 142 | title: page.title, 143 | icon: page.iconName, 144 | noLinkIcon: true, 145 | action: ()=>{ 146 | open(page) 147 | } 148 | }) 149 | if (!contentToolbar.content) open(page) 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /old/widgetManager.ts: -------------------------------------------------------------------------------- 1 | import St from "gi://St" 2 | import { Global } from "../global.js" 3 | import Maid from "./maid.js" 4 | 5 | export class WidgetManager { 6 | _scroll: St.ScrollView 7 | _sections: St.BoxLayout 8 | _maid: Maid 9 | _boxes: St.BoxLayout[] 10 | 11 | update() { 12 | this._boxes[] 13 | } 14 | 15 | load(): void { 16 | this._maid = new Maid() 17 | Global.QuickSettingsBox.vertical = false 18 | 19 | this._maid.connectJob( 20 | Global.Settings, 21 | "changed::layout", 22 | this.update.bind(this) 23 | ) 24 | 25 | this._maid.connectJob( 26 | Global.QuickSettingsBox, "notify::mapped", ()=>{ 27 | if (Global.QuickSettingsBox.mapped) this.update() 28 | } 29 | ) 30 | } 31 | unload(): void { 32 | this._maid.destroy() 33 | Global.QuickSettingsBox.vertical = true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quik-settings-tweaks", 3 | "description": "A TypeScript GNOME Extension", 4 | "version": "0.0.0", 5 | "author": { 6 | "email": "me@qwreey.moe", 7 | "name": "qwreey", 8 | "url": "https://github.com/qwreey" 9 | }, 10 | "type": "module", 11 | "sideEffects": false, 12 | "license": "LGPL-3.0-or-later", 13 | "homepage": "https://github.com/qwreey/quick-settings-tweaks#readme", 14 | "private": true, 15 | "bugs": { 16 | "url": "https://github.com/qwreey/quick-settings-tweaks/issues" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/qwreey/quick-settings-tweaks.git" 21 | }, 22 | "devDependencies": { 23 | "sass": "^1.83.4", 24 | "typescript": "^5.7.3" 25 | }, 26 | "dependencies": { 27 | "@girs/gjs": "^4.0.0-beta.19", 28 | "@girs/gnome-shell": "^47.0.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scripts/contributor-labels.json: -------------------------------------------------------------------------------- 1 | { 2 | "qwreey": "Owner", 3 | "DaPigGuy": "Gnome 45 Port", 4 | "Leleat": "Style & Code\nimprove", 5 | "kgdn": "Gnome 45 Port", 6 | "DodoLeDev": "Readme & Code\nimprove", 7 | "andia89": "Code improve", 8 | "DanGLES3": "Portuguese\ntranslation", 9 | "jcatfor": "Catalan\ntranslation", 10 | "ondra05": "Czech\ntranslation", 11 | "daudix": "Russian\ntranslation", 12 | "JohnOberhauser": "Audio\nInput/Output", 13 | "314eter": "Bug fix\nCode improve", 14 | "Tuba2": "Portuguese\ntranslation", 15 | "mechtifs": "Chinese Simplified\ntranslation", 16 | "MrXvnov": "Russian\ntranslation", 17 | "ryzendew": "Gnome 47 Port", 18 | "prapooskur": "Gnome 46 Port", 19 | "alewicki95": "Polish\ntranslation", 20 | "nulta": "Korean\ntranslation" 21 | } 22 | -------------------------------------------------------------------------------- /scripts/reindent.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs" 2 | function getItems() { 3 | let allowed = false 4 | const items = process.argv.filter(item => { 5 | if (allowed) return true 6 | if (item == "--") allowed = true 7 | return false 8 | }) 9 | return items 10 | } 11 | async function main() { 12 | await Promise.all(getItems().map(item => 13 | fs.readFile(`./${item}`, { encoding: "utf-8" }) 14 | .then(content => 15 | content.replaceAll(/[^\n]*/g, 16 | substr => substr.replace( 17 | /^ */, 18 | indent => "\t".repeat(Math.floor(indent.length / 4)) 19 | ) 20 | )) 21 | .then(fs.writeFile.bind(fs, item)) 22 | )) 23 | } 24 | main() 25 | -------------------------------------------------------------------------------- /scripts/version/gnome-docker-version: -------------------------------------------------------------------------------- 1 | 1.0.3 -------------------------------------------------------------------------------- /scripts/version/latest-build-number: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /scripts/version/latest-middle-version: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /scripts/version/latest-minor-version: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /scripts/version/major-version: -------------------------------------------------------------------------------- 1 | 2 -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | isDevelopmentBuild: false, 3 | isReleaseBuild: false, 4 | isGithubBuild: false, 5 | version: "unknown", 6 | buildNumber: 0, 7 | baseGTypeName: "quick-settings-tweaks_", 8 | loggerPrefix: "[quick-settings-tweaks]", 9 | } 10 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Viewer Note: 3 | * stylesheet and javascript files are compiled from scss and typescript. 4 | * To modify this extension, please check original source-codes from repository 5 | * https://github.com/qwreey/quick-settings-tweaks 6 | */ 7 | import { Extension } from "resource:///org/gnome/shell/extensions/extension.js" 8 | import Logger from "./libs/shared/logger.js" 9 | import Global from "./global.js" 10 | import Config from "./config.js" 11 | import { type FeatureBase } from "./libs/shell/feature.js" 12 | import { DndQuickToggleFeature } from "./features/toggle/dndQuickToggle.js" 13 | import { UnsafeQuickToggleFeature } from "./features/toggle/unsafeQuickToggle.js" 14 | import { MediaWidgetFeature } from "./features/widget/media.js" 15 | import { WeatherWidgetFeature } from "./features/widget/weather.js" 16 | import { NotificationsWidgetFeature } from "./features/widget/notifications.js" 17 | import { TogglesLayoutFeature } from "./features/layout/toggles.js" 18 | import { SystemItemsLayoutFeature } from "./features/layout/systemItems.js" 19 | import { DateMenuLayoutFeature } from "./features/layout/dateMenu.js" 20 | import { OverlayMenu } from "./features/overlayMenu.js" 21 | import { MenuAnimation } from "./features/menuAnimation.js" 22 | import { DebugFeature } from "./features/debug.js" 23 | import { VolumeMixerWidgetFeature } from "./features/widget/volumeMixer.js" 24 | import { SystemIndicatorLayoutFeature } from "./features/layout/systemIndicator.js" 25 | 26 | export default class QstExtension extends Extension { 27 | private features: FeatureBase[] 28 | private debug: DebugFeature 29 | 30 | disable() { 31 | Logger(`Extension ${this.metadata.name} deactivation started`) 32 | let start = +Date.now() 33 | 34 | // Unload debug feature 35 | this.debug.unload() 36 | this.debug = null 37 | 38 | // Unload features 39 | for (const feature of this.features) { 40 | Logger(`Unload feature '${feature.constructor.name}'`) 41 | feature.unload() 42 | } 43 | this.features = null // Null-out all features, loaded objects, arrays should be GC'd 44 | 45 | // Unload global context 46 | Global.unload() 47 | 48 | Logger("Diabled. " + (+new Date() - start) + "ms taken") 49 | } 50 | 51 | enable() { 52 | // Load global context 53 | Global.load(this) 54 | 55 | // Create features 56 | this.features = [ 57 | new DndQuickToggleFeature(), 58 | new UnsafeQuickToggleFeature(), 59 | // new InputOutputFeature(), 60 | new NotificationsWidgetFeature(), 61 | new MediaWidgetFeature(), 62 | new VolumeMixerWidgetFeature(), 63 | new DateMenuLayoutFeature(), 64 | new WeatherWidgetFeature(), 65 | new OverlayMenu(), 66 | new MenuAnimation(), 67 | new SystemItemsLayoutFeature(), 68 | new TogglesLayoutFeature(), 69 | new SystemIndicatorLayoutFeature(), 70 | ] 71 | 72 | // Load debug feature 73 | this.debug = new DebugFeature() 74 | this.debug.load() 75 | Logger(`Extension activation started, version: ${Config.version}`) 76 | 77 | // Load features 78 | Logger.debug("Initializing features ...") 79 | let start = +Date.now() 80 | for (const feature of this.features) { 81 | Logger.debug(()=>`Loading feature '${feature.constructor.name}'`) 82 | feature.load() 83 | } 84 | Logger(`Extension Loaded, ${+Date.now() - start}ms taken`) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/features/debug.ts: -------------------------------------------------------------------------------- 1 | import { FeatureBase, type SettingLoader } from "../libs/shell/feature.js" 2 | import Global from "../global.js" 3 | import Logger from "../libs/shared/logger.js" 4 | import Config from "../config.js" 5 | 6 | export class DebugFeature extends FeatureBase { 7 | disableDebugMessage = true 8 | 9 | // #region settings 10 | expose: boolean 11 | showLayoutBorder: boolean 12 | logLevel: number 13 | override loadSettings(loader: SettingLoader): void { 14 | this.expose = loader.loadBoolean("debug-expose") 15 | this.showLayoutBorder = loader.loadBoolean("debug-show-layout-border") 16 | this.logLevel = loader.loadInt("debug-log-level") 17 | } 18 | // #endregion settings 19 | 20 | override onLoad() { 21 | Logger.setHeader(Config.loggerPrefix) 22 | Logger.setLogLevel(this.logLevel) 23 | Logger.debug(()=>`Logger initialized, LogLevel: ${this.logLevel}`) 24 | if (this.expose) { 25 | globalThis.qst = Global 26 | for (const feature of (Global.Extension as any).features) { 27 | Global[feature.constructor.name] = feature 28 | } 29 | this.maid.functionJob(()=>{ 30 | for (const feature of (Global.Extension as any).features) { 31 | delete Global[feature.constructor.name] 32 | } 33 | delete globalThis.qst 34 | }) 35 | Logger.debug("Extension environment expose enabled") 36 | } 37 | if (this.showLayoutBorder) { 38 | // @ts-ignore Box pointer is private 39 | Global.QuickSettingsMenu._boxPointer.style_class += " QSTWEAKS-debug-show-layout" 40 | this.maid.functionJob(()=>{ 41 | // @ts-ignore Box pointer is private 42 | Global.QuickSettingsMenu._boxPointer.style_class = 43 | // @ts-ignore Box pointer is private 44 | Global.QuickSettingsMenu._boxPointer.style_class.replace(/ QSTWEAKS-debug-show-layout/, "") 45 | }) 46 | Logger.debug("Show layout border enabled") 47 | } 48 | } 49 | override onUnload(): void {} 50 | } 51 | -------------------------------------------------------------------------------- /src/features/layout/dateMenu.ts: -------------------------------------------------------------------------------- 1 | import { FeatureBase, type SettingLoader } from "../../libs/shell/feature.js" 2 | import { StyleClass } from "../../libs/shared/styleClass.js" 3 | import Global from "../../global.js" 4 | import Logger from "../../libs/shared/logger.js" 5 | 6 | export class DateMenuLayoutFeature extends FeatureBase { 7 | // #region settings 8 | hideMediaControl: boolean 9 | hideNotifications: boolean 10 | hideLeftBox: boolean 11 | hideRightBox: boolean 12 | disableMenu: boolean 13 | override loadSettings(loader: SettingLoader): void { 14 | this.hideMediaControl = loader.loadBoolean("datemenu-hide-media-control") 15 | this.hideNotifications = loader.loadBoolean("datemenu-hide-notifications") 16 | this.hideLeftBox = loader.loadBoolean("datemenu-hide-left-box") 17 | this.hideRightBox = loader.loadBoolean("datemenu-hide-right-box") 18 | this.disableMenu = loader.loadBoolean("datemenu-disable-menu") 19 | } 20 | // #endregion settings 21 | 22 | override onLoad() { 23 | const style = new StyleClass((Global.DateMenuBox as any).style_class) 24 | 25 | // Hide media control from date menu 26 | if (this.hideMediaControl) { 27 | this.maid.hideJob( 28 | Global.MediaSection, 29 | ()=>true 30 | ) 31 | } 32 | 33 | // Hide notifications from date menu 34 | if (this.hideNotifications) { 35 | this.maid.hideJob( 36 | Global.NotificationSection, 37 | ()=>true 38 | ) 39 | } 40 | 41 | // Hide left box from date menu 42 | if (this.hideLeftBox) { 43 | const leftBox = Global.DateMenuHolder.get_first_child() 44 | if (leftBox) { 45 | this.maid.hideJob( 46 | leftBox, 47 | ()=>true 48 | ) 49 | } else { 50 | Logger.error("Failed to get date menu left box") 51 | } 52 | style.add("QSTWEAKS-hide-left-box") 53 | } 54 | 55 | // Hide right box from date menu 56 | if (this.hideRightBox) { 57 | const rightBox = Global.DateMenuHolder.get_last_child() 58 | if (rightBox) { 59 | this.maid.hideJob( 60 | rightBox, 61 | ()=>true 62 | ) 63 | } else { 64 | Logger.error("Failed to get date menu right box") 65 | } 66 | style.add("QSTWEAKS-hide-right-box") 67 | } 68 | 69 | // Disable menu open action 70 | if (this.disableMenu) { 71 | Global.DateMenu.reactive = false 72 | this.maid.functionJob(()=>{ 73 | Global.DateMenu.reactive = true 74 | }) 75 | } 76 | 77 | // Modify style class 78 | if (style.modified) { 79 | (Global.DateMenuBox as any).style_class = style.stringify() 80 | } 81 | } 82 | override onUnload(): void { 83 | if ((Global.MediaSection as any)._shouldShow()) Global.MediaSection.show() 84 | if ((Global.NotificationSection as any)._shouldShow()) Global.NotificationSection.show() 85 | 86 | // Remove modified styles 87 | const style = new StyleClass((Global.DateMenuBox as any).style_class) 88 | .remove("QSTWEAKS-hide-right-box") 89 | .remove("QSTWEAKS-hide-left-box") 90 | if (style.modified) { 91 | (Global.DateMenuBox as any).style_class = style.stringify() 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/features/layout/sound.ts: -------------------------------------------------------------------------------- 1 | import { FeatureBase, SettingLoader } from "../../libs/shell/feature.js" 2 | import GObject from "gi://GObject" 3 | 4 | // TODO: migration from qst 1.8 5 | export class SoundLayoutFeature extends FeatureBase { 6 | // #region settings 7 | 8 | override loadSettings(loader: SettingLoader): void { 9 | throw GObject.NotImplementedError 10 | } 11 | // #endregion settings 12 | 13 | override onLoad(): void { 14 | throw GObject.NotImplementedError 15 | } 16 | override onUnload(): void { 17 | throw GObject.NotImplementedError 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/features/layout/systemIndicator.ts: -------------------------------------------------------------------------------- 1 | import * as Main from "resource:///org/gnome/shell/ui/main.js" 2 | import { SystemIndicator } from "resource:///org/gnome/shell/ui/quickSettings.js" 3 | import { FeatureBase, type SettingLoader } from "../../libs/shell/feature.js" 4 | import { SystemIndicatorTracker } from "../../libs/shell/quickSettingsUtils.js" 5 | import { SystemIndicatorOrderItem } from "../../libs/types/systemIndicatorOrderItem.js" 6 | import { StyleClass } from "../../libs/shared/styleClass.js" 7 | import Maid from "../../libs/shared/maid.js" 8 | import Global from "../../global.js" 9 | 10 | export class SystemIndicatorLayoutFeature extends FeatureBase { 11 | // #region settings 12 | orderEnabled: boolean 13 | order: SystemIndicatorOrderItem[] 14 | unordered: SystemIndicatorOrderItem 15 | privacyIndicatorStyle: "default" | "monochrome" | "accent" 16 | accentScreenSharingIndicator: boolean 17 | accentScreenRecordingIndicator: boolean 18 | override loadSettings(loader: SettingLoader): void { 19 | this.orderEnabled = loader.loadBoolean("system-indicator-layout-enabled") 20 | this.order = loader.loadValue("system-indicator-layout-order") 21 | this.unordered = this.order.find(item => item.nonOrdered) 22 | this.privacyIndicatorStyle = loader.loadString("system-indicator-privacy-indicator-style") as SystemIndicatorLayoutFeature["privacyIndicatorStyle"] 23 | this.accentScreenSharingIndicator = loader.loadBoolean("system-indicator-screen-sharing-indicator-use-accent") 24 | this.accentScreenRecordingIndicator = loader.loadBoolean("system-indicator-screen-recording-indicator-use-accent") 25 | } 26 | // #endregion settings 27 | 28 | onIndicatorCreated(maid: Maid, indicator: SystemIndicator): void { 29 | const rule: SystemIndicatorOrderItem = 30 | this.order.find(item => SystemIndicatorOrderItem.indicatorMatch(item, indicator)) 31 | ?? this.unordered 32 | if (rule.hide) maid.hideJob(indicator) 33 | } 34 | onUpdate(): void { 35 | const children = Global.Indicators.get_children() 36 | const head: SystemIndicator[] = [] 37 | const middle: SystemIndicator[] = children.filter(child => child instanceof SystemIndicator) as any 38 | const tail: SystemIndicator[] = [] 39 | let overNonOrdered: boolean = false 40 | for (const item of this.order) { 41 | if (item.nonOrdered) { 42 | overNonOrdered = true 43 | continue 44 | } 45 | const middleIndex = middle.findIndex(toggle => SystemIndicatorOrderItem.indicatorMatch(item, toggle)) 46 | if (middleIndex == -1) continue 47 | const toggle = middle[middleIndex] 48 | middle.splice(middleIndex, 1); 49 | (overNonOrdered ? tail : head).push(toggle) 50 | } 51 | let last: SystemIndicator|null = null 52 | for (const item of [head, middle, tail].flat()) { 53 | if (last) Global.Indicators.set_child_above_sibling(item, last) 54 | last = item 55 | } 56 | } 57 | 58 | tracker: SystemIndicatorTracker 59 | override onLoad(): void { 60 | // Colored privacy indicator 61 | const privacyIndicatorStyle = new StyleClass(Global.Indicators.style_class) 62 | if (this.privacyIndicatorStyle == "accent") { 63 | privacyIndicatorStyle.add("QSTWEAKS-privacy-indicator-use-accent") 64 | } else if (this.privacyIndicatorStyle == "monochrome") { 65 | privacyIndicatorStyle.add("QSTWEAKS-privacy-indicator-use-monochrome") 66 | } 67 | if (privacyIndicatorStyle.modified) { 68 | Global.Indicators.style_class = privacyIndicatorStyle.stringify() 69 | this.maid.functionJob(()=>{ 70 | Global.Indicators.style_class = 71 | new StyleClass(Global.Indicators.style_class) 72 | .remove("QSTWEAKS-privacy-indicator-use-accent") 73 | .remove("QSTWEAKS-privacy-indicator-use-monochrome") 74 | .stringify() 75 | }) 76 | } 77 | 78 | // Colored screen sharing indicator 79 | if (this.accentScreenSharingIndicator) { 80 | Main.panel.statusArea["screenSharing"].style_class = 81 | new StyleClass(Main.panel.statusArea["screenSharing"].style_class) 82 | .add("QSTWEAKS-screen-sharing-indicator-use-accent") 83 | .stringify() 84 | this.maid.functionJob(()=>{ 85 | Main.panel.statusArea["screenSharing"].style_class = 86 | new StyleClass(Main.panel.statusArea["screenSharing"].style_class) 87 | .remove("QSTWEAKS-screen-sharing-indicator-use-accent") 88 | .stringify() 89 | }) 90 | } 91 | 92 | // Colored screen recording indicator 93 | if (this.accentScreenRecordingIndicator) { 94 | Main.panel.statusArea["screenRecording"].style_class = 95 | new StyleClass(Main.panel.statusArea["screenRecording"].style_class) 96 | .add("QSTWEAKS-screen-recording-indicator-use-accent") 97 | .stringify() 98 | this.maid.functionJob(()=>{ 99 | Main.panel.statusArea["screenRecording"].style_class = 100 | new StyleClass(Main.panel.statusArea["screenRecording"].style_class) 101 | .remove("QSTWEAKS-screen-recording-indicator-use-accent") 102 | .stringify() 103 | }) 104 | } 105 | 106 | // Ordering 107 | if (!this.orderEnabled) return 108 | this.tracker = new SystemIndicatorTracker() 109 | this.tracker.onIndicatorCreated = this.onIndicatorCreated.bind(this) 110 | this.tracker.onUpdate = this.onUpdate.bind(this) 111 | this.tracker.load() 112 | } 113 | override onUnload(): void { 114 | const tracker = this.tracker 115 | if (tracker) { 116 | this.tracker = null 117 | tracker.unload() 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/features/layout/systemItems.ts: -------------------------------------------------------------------------------- 1 | import Clutter from "gi://Clutter" 2 | import { type PowerToggle } from "resource:///org/gnome/shell/ui/status/system.js" 3 | import { type QuickSettingsItem } from "resource:///org/gnome/shell/ui/quickSettings.js" 4 | import { type SystemItem } from "resource:///org/gnome/shell/ui/status/system.js" 5 | import { FeatureBase, type SettingLoader } from "../../libs/shell/feature.js" 6 | import Logger from "../../libs/shared/logger.js" 7 | import Global from "../../global.js" 8 | 9 | export class SystemItemsLayoutFeature extends FeatureBase { 10 | // #region settings 11 | hideScreenshot: boolean 12 | hideSettings: boolean 13 | hideLock: boolean 14 | hideShutdown: boolean 15 | hideBattery: boolean 16 | hideLayout: boolean 17 | enabled: boolean 18 | order: string[] 19 | override loadSettings(loader: SettingLoader): void { 20 | this.hideScreenshot = loader.loadBoolean("system-items-layout-hide-screenshot") 21 | this.hideSettings = loader.loadBoolean("system-items-layout-hide-settings") 22 | this.hideLock = loader.loadBoolean("system-items-layout-hide-lock") 23 | this.hideShutdown = loader.loadBoolean("system-items-layout-hide-shutdown") 24 | this.hideBattery = loader.loadBoolean("system-items-layout-hide-battery") 25 | this.hideLayout = loader.loadBoolean("system-items-layout-hide") 26 | this.enabled = loader.loadBoolean("system-items-layout-enabled") 27 | this.order = loader.loadStrv("system-items-layout-order") 28 | } 29 | // #endregion settings 30 | 31 | async getItmes(): Promise<{ 32 | screenshot: QuickSettingsItem, 33 | settings: QuickSettingsItem, 34 | lock: QuickSettingsItem, 35 | shutdown: QuickSettingsItem, 36 | battery: PowerToggle, 37 | box: SystemItem, 38 | laptopSpacer: Clutter.Actor, 39 | desktopSpacer: Clutter.Actor, 40 | }> { 41 | const systemItem = await Global.QuickSettingsSystemItem 42 | const children = systemItem.child.get_children() 43 | let screenshotItem: QuickSettingsItem 44 | let settingsItem: QuickSettingsItem 45 | let lockItem: QuickSettingsItem 46 | let shutdownItem: QuickSettingsItem 47 | for (const child of children) { 48 | if (child.constructor.name == "ScreenshotItem") { 49 | screenshotItem = child as QuickSettingsItem 50 | continue 51 | } 52 | if (child.constructor.name == "SettingsItem") { 53 | settingsItem = child as QuickSettingsItem 54 | continue 55 | } 56 | if (child.constructor.name == "LockItem") { 57 | lockItem = child as QuickSettingsItem 58 | continue 59 | } 60 | if (child.constructor.name == "ShutdownItem") { 61 | shutdownItem = child as QuickSettingsItem 62 | } 63 | } 64 | return { 65 | screenshot: screenshotItem, 66 | settings: settingsItem, 67 | lock: lockItem, 68 | shutdown: shutdownItem, 69 | battery: systemItem.powerToggle, 70 | laptopSpacer: (systemItem as any)._laptopSpacer, 71 | desktopSpacer: (systemItem as any)._desktopSpacer, 72 | box: systemItem 73 | } 74 | } 75 | 76 | override onLoad() { 77 | if (!this.enabled) return 78 | this.getItmes().then(items => { 79 | if (this.hideLayout) { 80 | this.maid.hideJob(items.box, ()=>true) 81 | return 82 | } 83 | if (this.hideBattery) { 84 | this.maid.hideJob(items.battery, ()=>{ 85 | (items.battery as any)._sync() 86 | }) 87 | } 88 | if (this.hideScreenshot) { 89 | this.maid.hideJob(items.screenshot, ()=>true) 90 | } 91 | if (this.hideLock) { 92 | this.maid.hideJob(items.lock, ()=>true) 93 | } 94 | if (this.hideShutdown) { 95 | this.maid.hideJob(items.shutdown, ()=>true) 96 | } 97 | if (this.hideSettings) { 98 | this.maid.hideJob(items.settings, ()=>true) 99 | } 100 | let last: any 101 | for (const [index, item] of this.order.entries()) { 102 | const current = items[item] 103 | if (index) items.box.child.set_child_above_sibling(current, last) 104 | last = current 105 | } 106 | }).catch(Logger.error) 107 | } 108 | override onUnload(): void {} 109 | } 110 | -------------------------------------------------------------------------------- /src/features/layout/toggles.ts: -------------------------------------------------------------------------------- 1 | import { 2 | QuickToggle, 3 | QuickMenuToggle, 4 | } from "resource:///org/gnome/shell/ui/quickSettings.js" 5 | import { FeatureBase, type SettingLoader } from "../../libs/shell/feature.js" 6 | import { QuickSettingsToggleTracker } from "../../libs/shell/quickSettingsUtils.js" 7 | import { ToggleOrderItem } from "../../libs/types/toggleOrderItem.js" 8 | import Maid from "../../libs/shared/maid.js" 9 | import Global from "../../global.js" 10 | 11 | export class TogglesLayoutFeature extends FeatureBase { 12 | // #region settings 13 | enabled: boolean 14 | order: ToggleOrderItem[] 15 | unordered: ToggleOrderItem 16 | override loadSettings(loader: SettingLoader): void { 17 | this.enabled = loader.loadBoolean("toggles-layout-enabled") 18 | this.order = loader.loadValue("toggles-layout-order") 19 | for (const orderItem of this.order) { 20 | if (orderItem.titleRegex) { 21 | orderItem.cachedTitleRegex = new RegExp(orderItem.titleRegex) 22 | } 23 | if (orderItem.nonOrdered) { 24 | this.unordered = orderItem 25 | } 26 | } 27 | } 28 | // #endregion settings 29 | 30 | onToggleCreated(maid: Maid, toggle: QuickToggle|QuickMenuToggle): void { 31 | const rule: ToggleOrderItem = 32 | this.order.find(item => ToggleOrderItem.toggleMatch(item, toggle)) 33 | ?? this.unordered 34 | if (rule.hide) maid.hideJob(toggle) 35 | } 36 | onUpdate(): void { 37 | const children = Global.QuickSettingsGrid.get_children() 38 | const head: QuickToggle[] = [] 39 | const middle: QuickToggle[] = children.filter(child => 40 | ( 41 | (child instanceof QuickMenuToggle) 42 | || (child instanceof QuickToggle) 43 | ) 44 | && child.constructor.name != "BackgroundAppsToggle" 45 | ) as any 46 | const tail: QuickToggle[] = [] 47 | let overNonOrdered: boolean = false 48 | for (const item of this.order) { 49 | if (item.nonOrdered) { 50 | overNonOrdered = true 51 | continue 52 | } 53 | const middleIndex = middle.findIndex(toggle => ToggleOrderItem.toggleMatch(item, toggle)) 54 | if (middleIndex == -1) continue 55 | const toggle = middle[middleIndex] 56 | middle.splice(middleIndex, 1); 57 | (overNonOrdered ? tail : head).push(toggle) 58 | } 59 | let last: QuickToggle|null = null 60 | for (const item of [head, middle, tail].flat()) { 61 | if (last) Global.QuickSettingsGrid.set_child_above_sibling(item, last) 62 | last = item 63 | } 64 | } 65 | 66 | tracker: QuickSettingsToggleTracker 67 | override onLoad(): void { 68 | if (!this.enabled) return 69 | this.tracker = new QuickSettingsToggleTracker() 70 | this.tracker.onToggleCreated = this.onToggleCreated.bind(this) 71 | this.tracker.onUpdate = this.onUpdate.bind(this) 72 | this.tracker.load() 73 | } 74 | override onUnload(): void { 75 | const tracker = this.tracker 76 | if (tracker) { 77 | this.tracker = null 78 | tracker.unload() 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/features/menuAnimation.ts: -------------------------------------------------------------------------------- 1 | import Clutter from "gi://Clutter" 2 | import Shell from "gi://Shell" 3 | import { type QuickSettingsMenu } from "resource:///org/gnome/shell/ui/quickSettings.js" 4 | import { FeatureBase, type SettingLoader } from "../libs/shell/feature.js" 5 | import { QuickSettingsMenuTracker } from "../libs/shell/quickSettingsUtils.js" 6 | import Global from "../global.js" 7 | import Maid from "../libs/shared/maid.js" 8 | 9 | export class MenuAnimation extends FeatureBase { 10 | // #region settings 11 | enabled: boolean 12 | backgroundBlurRadius: number 13 | bakgroundBrightness: number 14 | backgroundOpacity: number 15 | backgroundScaleX: number 16 | backgroundScaleY: number 17 | gridContentOpacity: number 18 | openDuration: number 19 | closeDuration: number 20 | override loadSettings(loader: SettingLoader): void { 21 | this.enabled = loader.loadBoolean("menu-animation-enabled") 22 | this.backgroundBlurRadius = loader.loadInt("menu-animation-background-blur-radius") 23 | this.bakgroundBrightness = loader.loadInt("menu-animation-background-brightness") / 1000 24 | this.backgroundOpacity = loader.loadInt("menu-animation-background-opacity") 25 | this.backgroundScaleX = loader.loadInt("menu-animation-background-scale-x") / 1000 26 | this.backgroundScaleY = loader.loadInt("menu-animation-background-scale-y") / 1000 27 | this.openDuration = loader.loadInt("menu-animation-open-duration") 28 | this.closeDuration = loader.loadInt("menu-animation-close-duration") 29 | this.gridContentOpacity = loader.loadInt("menu-animation-grid-content-opacity") 30 | } 31 | // #endregion settings 32 | 33 | onOpen(_maid: Maid, _menu: QuickSettingsMenu, isOpen: boolean) { 34 | if (this.blur) this.blur.enabled = isOpen 35 | if (isOpen) { 36 | Global.QuickSettingsBox.set_pivot_point(0.5, 0.5) 37 | Global.QuickSettingsBox.ease({ 38 | duration: this.openDuration, 39 | mode: Clutter.AnimationMode.EASE_OUT_QUINT, 40 | scaleX: this.backgroundScaleX, 41 | scaleY: this.backgroundScaleY, 42 | opacity: this.backgroundOpacity, 43 | }) 44 | Global.QuickSettingsGrid.ease({ 45 | duration: this.openDuration, 46 | mode: Clutter.AnimationMode.EASE_OUT_QUINT, 47 | opacity: this.gridContentOpacity, 48 | }) 49 | } else { 50 | Global.QuickSettingsBox.ease({ 51 | duration: this.closeDuration, 52 | mode: Clutter.AnimationMode.EASE_OUT_QUINT, 53 | scaleX: 1, 54 | scaleY: 1, 55 | opacity: 255, 56 | onComplete: ()=>{ 57 | Global.QuickSettingsBox.set_pivot_point(0, 0) 58 | } 59 | }) 60 | Global.QuickSettingsGrid.ease({ 61 | duration: this.openDuration, 62 | mode: Clutter.AnimationMode.EASE_OUT_QUINT, 63 | opacity: 255, 64 | }) 65 | } 66 | } 67 | 68 | blur: Shell.BlurEffect 69 | tracker: QuickSettingsMenuTracker 70 | override onLoad(): void { 71 | if (!this.enabled) return 72 | 73 | if (this.backgroundBlurRadius) { 74 | this.blur = new Shell.BlurEffect({ 75 | enabled: false, 76 | mode: Shell.BlurMode.ACTOR, 77 | radius: this.backgroundBlurRadius, 78 | brightness: this.bakgroundBrightness, 79 | }) 80 | // @ts-ignore Box pointer is private 81 | Global.QuickSettingsMenu._boxPointer.add_effect_with_name("blur", this.blur) 82 | } 83 | 84 | this.tracker = new QuickSettingsMenuTracker() 85 | this.tracker.onMenuOpen = this.onOpen.bind(this) 86 | this.tracker.load() 87 | } 88 | override onUnload(): void { 89 | const tracker = this.tracker 90 | if (!tracker) return 91 | this.tracker = null 92 | tracker.unload() 93 | if (this.blur) { 94 | // @ts-ignore Box pointer is private 95 | Global.QuickSettingsMenu._boxPointer.remove_effect(this.blur) 96 | this.blur = null 97 | } 98 | Global.QuickSettingsBox.remove_all_transitions() 99 | Global.QuickSettingsBox.scaleX = 1 100 | Global.QuickSettingsBox.scaleY = 1 101 | Global.QuickSettingsBox.opacity = 255 102 | Global.QuickSettingsBox.set_pivot_point(0, 0) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/features/overlayMenu.ts: -------------------------------------------------------------------------------- 1 | import Clutter from "gi://Clutter" 2 | import { QuickSlider, type QuickSettingsMenu } from "resource:///org/gnome/shell/ui/quickSettings.js" 3 | import { FeatureBase, type SettingLoader } from "../libs/shell/feature.js" 4 | import { QuickSettingsMenuTracker } from "../libs/shell/quickSettingsUtils.js" 5 | import * as AdvAni from "../libs/shell/advani.js" 6 | import Global from "../global.js" 7 | import Maid from "../libs/shared/maid.js" 8 | 9 | export class OverlayMenu extends FeatureBase { 10 | // #region settings 11 | enabled: boolean 12 | width: number 13 | duration: number 14 | animationStyle: string 15 | override loadSettings(loader: SettingLoader): void { 16 | this.enabled = loader.loadBoolean("overlay-menu-enabled") 17 | this.width = loader.loadInt("overlay-menu-width") 18 | this.duration = loader.loadInt("overlay-menu-animate-duration") 19 | this.animationStyle = loader.loadString("overlay-menu-animate-style") 20 | } 21 | // #endregion settings 22 | 23 | getCoords(menu: QuickSettingsMenu): { 24 | outerHeight: number, 25 | targetHeight: number, 26 | targetWidth: number, 27 | sourceX: number, 28 | sourceY: number, 29 | sourceHeight: number, 30 | sourceWidth: number, 31 | offsetY: number, 32 | offsetX: number, 33 | } { 34 | menu.actor.height = -1 35 | let [outerHeight] = menu.actor.get_preferred_height(-1) 36 | const targetWidth = menu.actor.width - menu.box.marginLeft - menu.box.marginRight 37 | const targetHeight = outerHeight - menu.box.marginTop 38 | const offsetY = Math.max( 39 | Math.floor((Global.QuickSettingsBox.height - targetHeight) / 2), 40 | 0 41 | ) 42 | const isSlider = menu.sourceActor instanceof QuickSlider 43 | const sourceHeight = Math.floor(menu.sourceActor.height + 0.5) 44 | const sourceBaseWidth = Math.floor(menu.sourceActor.width + 0.5) 45 | const sourceWidth = isSlider ? sourceHeight : sourceBaseWidth 46 | const sourceBaseX = Math.floor(Global.QuickSettingsGrid.x + menu.sourceActor.x + 0.5) 47 | const sourceY = Math.floor(Global.QuickSettingsGrid.y + menu.sourceActor.y + 0.5) 48 | const sourceX = sourceBaseX + (isSlider ? (sourceBaseWidth - sourceWidth) : 0) 49 | const offsetX = Math.floor((Global.QuickSettingsBox.width - targetWidth) / 2) 50 | return { 51 | outerHeight, 52 | targetHeight, 53 | targetWidth, 54 | sourceX, 55 | sourceY, 56 | sourceHeight, 57 | sourceWidth, 58 | offsetY, 59 | offsetX, 60 | } 61 | } 62 | 63 | onOpen(_maid: Maid, menu: QuickSettingsMenu, isOpen: boolean) { 64 | if (!isOpen || !this.duration) menu.actor.set_easing_duration(0) 65 | else menu.actor.remove_all_transitions() 66 | if (!isOpen) return 67 | 68 | const coords = this.getCoords(menu) 69 | this.yconstraint.offset = coords.offsetY 70 | 71 | if (this.duration) { 72 | menu.box.opacity = 0 73 | menu.box.ease({ 74 | opacity: 255, 75 | duration: Math.floor(this.duration / 3), 76 | }) 77 | if (this.animationStyle == "flyout") { 78 | menu.box.translation_x = Math.floor(coords.sourceX - coords.offsetX + menu.box.marginLeft) 79 | menu.box.translation_y = Math.floor(coords.sourceY - coords.offsetY + menu.box.marginTop) 80 | menu.box.scale_x = coords.sourceWidth / coords.targetWidth 81 | menu.box.scale_y = coords.sourceHeight / coords.targetHeight 82 | AdvAni.ease(menu.box, { 83 | translation_x: 0, 84 | translation_y: 0, 85 | scale_x: 1, 86 | scale_y: 1, 87 | mode: AdvAni.AdvAnimationMode.LowBackover, 88 | // mode: Clutter.AnimationMode.EASE_OUT_EXPO, 89 | duration: this.duration, 90 | }) 91 | } else if (this.animationStyle == "dialog") { 92 | menu.box.translation_x = 0.2*coords.targetWidth*.5 93 | menu.box.translation_y = 0.2*coords.targetHeight*.5 94 | menu.box.scale_x = 0.8 95 | menu.box.scale_y = 0.8 96 | AdvAni.ease(menu.box, { 97 | translation_x: 0, 98 | translation_y: 0, 99 | scale_x: 1, 100 | scale_y: 1, 101 | mode: AdvAni.AdvAnimationMode.MiddleBackover, 102 | // mode: Clutter.AnimationMode.EASE_OUT_EXPO, 103 | duration: this.duration, 104 | }) 105 | } 106 | } 107 | } 108 | 109 | onMenuCreated(maid: Maid, menu: QuickSettingsMenu) { 110 | menu.actor.get_constraints()[0].enabled = false 111 | if (this.width) { 112 | menu.actor.width = this.width 113 | menu.actor.x_expand = false 114 | } 115 | maid.connectJob(menu.box, "notify::height", ()=>{ 116 | if (!menu.isOpen) return 117 | const coords = this.getCoords(menu) 118 | this.yconstraint.offset = coords.offsetY 119 | }) 120 | } 121 | 122 | tracker: QuickSettingsMenuTracker 123 | yconstraint: Clutter.BindConstraint 124 | override reload(changedKey?: string): void { 125 | if (changedKey == "overlay-menu-animate-duration") return 126 | if (changedKey == "overlay-menu-animate-style") return 127 | super.reload(changedKey) 128 | } 129 | override onLoad(): void { 130 | if (!this.enabled) return 131 | 132 | // Offset handle 133 | this.yconstraint = new Clutter.BindConstraint({ 134 | coordinate: Clutter.BindCoordinate.Y, 135 | // @ts-ignore Box pointer is private 136 | source: Global.QuickSettingsMenu._boxPointer, 137 | }) 138 | 139 | // Disable Y sync (overlay y offset) 140 | // @ts-ignore Overlay is private field 141 | Global.QuickSettingsMenu._overlay.get_constraints()[0].enabled = false 142 | // @ts-ignore Overlay is private field 143 | Global.QuickSettingsMenu._overlay.add_constraint(this.yconstraint) 144 | 145 | // Disable Placeholder height sync (grid height increase) 146 | // @ts-ignore Overlay is private field 147 | Global.QuickSettingsGrid.layout_manager._overlay.get_constraints()[0].enabled = false 148 | 149 | this.tracker = new QuickSettingsMenuTracker() 150 | this.tracker.onMenuCreated = this.onMenuCreated.bind(this) 151 | this.tracker.onMenuOpen = this.onOpen.bind(this) 152 | this.tracker.load() 153 | } 154 | override onUnload(): void { 155 | const tracker = this.tracker 156 | if (!tracker) return 157 | this.tracker = null 158 | for (const menu of tracker.items) { 159 | menu.actor.x_expand = true 160 | menu.actor.get_constraints()[0].enabled = true 161 | } 162 | tracker.unload() 163 | // @ts-ignore Overlay is private field 164 | Global.QuickSettingsMenu._overlay.get_constraints()[0].enabled = true 165 | // @ts-ignore Overlay is private field 166 | Global.QuickSettingsGrid.layout_manager._overlay.get_constraints()[0].enabled = true 167 | // @ts-ignore Overlay is private field 168 | Global.QuickSettingsMenu._overlay.remove_constraint(this.yconstraint) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/features/toggle/dndQuickToggle.ts: -------------------------------------------------------------------------------- 1 | import St from "gi://St" 2 | import Gio from "gi://Gio" 3 | import GObject from "gi://GObject" 4 | import { QuickToggle, SystemIndicator } from "resource:///org/gnome/shell/ui/quickSettings.js" 5 | import { FeatureBase, type SettingLoader } from "../../libs/shell/feature.js" 6 | import Global from "../../global.js" 7 | 8 | // #region DndQuickToggle 9 | class DndQuickToggle extends QuickToggle { 10 | _settings: Gio.Settings 11 | _init() { 12 | super._init({ 13 | title: _("Do Not Disturb"), 14 | iconName: "notifications-disabled-symbolic", 15 | } as Partial) 16 | 17 | this._settings = new Gio.Settings({ 18 | schema_id: "org.gnome.desktop.notifications", 19 | }) 20 | this._settings.connectObject("changed::show-banners", this._sync.bind(this), this) 21 | 22 | this.connect("clicked", this._toggleMode.bind(this)) 23 | this._sync() 24 | } 25 | 26 | // Update icon to match current state 27 | _updateIcon() { 28 | this.iconName = 29 | this.checked 30 | ? "notifications-disabled-symbolic" 31 | : "notifications-symbolic" 32 | } 33 | 34 | // Toggle DND Mode 35 | _toggleMode() { 36 | this._settings.set_boolean( 37 | "show-banners", 38 | !this._settings.get_boolean("show-banners") 39 | ) 40 | } 41 | 42 | // Sync DND state 43 | _sync() { 44 | const checked = !this._settings.get_boolean("show-banners") 45 | if (this.checked !== checked) this.set({ checked }) 46 | this._updateIcon() 47 | } 48 | 49 | // Nullout 50 | destroy() { 51 | this._settings = null 52 | super.destroy() 53 | } 54 | } 55 | GObject.registerClass(DndQuickToggle) 56 | // #endregion DndQuickToggle 57 | 58 | // #region DndIndicator 59 | class DndIndicator extends SystemIndicator { 60 | _indicator: St.Icon 61 | _settings: Gio.Settings 62 | _showIndicator: boolean 63 | constructor(showIndicator: boolean) { 64 | super(showIndicator as any) 65 | } 66 | // @ts-ignore 67 | _init(showIndicator: boolean) { 68 | super._init() 69 | 70 | this.quickSettingsItems.push(new DndQuickToggle()) 71 | 72 | if (showIndicator) { 73 | this._indicator = this._addIndicator() 74 | this._indicator.icon_name = "notifications-disabled-symbolic" 75 | this._settings = new Gio.Settings({ 76 | schema_id: "org.gnome.desktop.notifications", 77 | }) 78 | this._settings.connectObject("changed::show-banners", this._sync.bind(this), this) 79 | this._sync() 80 | } 81 | } 82 | 83 | _sync() { 84 | const checked = !this._settings.get_boolean("show-banners") 85 | if (checked) { 86 | this._indicator.visible = true 87 | } else { 88 | this._indicator.visible = false 89 | } 90 | } 91 | 92 | destroy() { 93 | this.quickSettingsItems.forEach(item => item.destroy()) 94 | this._settings = null 95 | super.destroy() 96 | } 97 | } 98 | GObject.registerClass(DndIndicator) 99 | export { DndIndicator } 100 | // #endregion DndIndicator 101 | 102 | // #region DndQuickToggleFeature 103 | export class DndQuickToggleFeature extends FeatureBase { 104 | // #region settings 105 | enabled: boolean 106 | indicatorPosition: "system-tray" | "date-menu" | "hide" 107 | override loadSettings(loader: SettingLoader): void { 108 | this.enabled = loader.loadBoolean("dnd-quick-toggle-enabled") 109 | this.indicatorPosition = loader.loadString("dnd-quick-toggle-indicator-position") as any 110 | } 111 | // #endregion settings 112 | 113 | indicator: DndIndicator 114 | override onLoad(): void { 115 | if (!this.enabled) return 116 | 117 | // Create Indicator 118 | this.maid.destroyJob( 119 | this.indicator = new DndIndicator(this.indicatorPosition == "system-tray") 120 | ) 121 | 122 | // Hide DateMenu DND State Icon 123 | if (this.indicatorPosition != "date-menu") { 124 | this.maid.hideJob(Global.DateMenuIndicator, ()=> 125 | !(new Gio.Settings({ 126 | schema_id: "org.gnome.desktop.notifications", 127 | })).get_boolean("show-banners") 128 | ) 129 | } 130 | 131 | // Add to QS 132 | // @ts-expect-error Missing addExternalIndicator method (QuickSettings is private class) 133 | Global.QuickSettings.addExternalIndicator(this.indicator) 134 | } 135 | override onUnload(): void { 136 | this.indicator = null 137 | } 138 | } 139 | // #endregion DndQuickToggleFeature 140 | -------------------------------------------------------------------------------- /src/features/toggle/unsafeQuickToggle.ts: -------------------------------------------------------------------------------- 1 | import GObject from "gi://GObject" 2 | import { gettext as _ } from "resource:///org/gnome/shell/extensions/extension.js" 3 | import { QuickToggle, SystemIndicator } from "resource:///org/gnome/shell/ui/quickSettings.js" 4 | import { FeatureBase, type SettingLoader } from "../../libs/shell/feature.js" 5 | import Global from "../../global.js" 6 | 7 | // #region UnsafeQuickToggle 8 | class UnsafeQuickToggle extends QuickToggle { 9 | _onUpdate: (value: boolean)=>void 10 | constructor(onUpdate: UnsafeQuickToggle["_onUpdate"]) { super(onUpdate as any) } 11 | _init(onUpdate: any) { 12 | super._init({ 13 | title: _("Unsafe Mode"), 14 | iconName: "channel-insecure-symbolic", 15 | }) 16 | this._onUpdate = onUpdate 17 | 18 | // bind click 19 | this.connect("clicked", this._toggleMode.bind(this)) 20 | 21 | // Fetch global context 22 | this._sync() 23 | } 24 | 25 | _updateIcon() { 26 | this.iconName = this.checked ? "channel-insecure-symbolic" : "channel-secure-symbolic" 27 | } 28 | 29 | // Toggle context 30 | _toggleMode() { 31 | this.checked = !global.context.unsafe_mode 32 | global.context.unsafe_mode = this.checked 33 | this._updateIcon() 34 | this._onUpdate(this.checked) 35 | } 36 | 37 | // Sync context 38 | _sync() { 39 | this.checked = global.context.unsafe_mode 40 | this._updateIcon() 41 | } 42 | } 43 | GObject.registerClass(UnsafeQuickToggle) 44 | // #endregion UnsafeQuickToggle 45 | 46 | // #region UnsafeIndicator 47 | class UnsafeIndicator extends SystemIndicator { 48 | constructor(onUpdate: UnsafeQuickToggle["_onUpdate"]) { super(onUpdate as any) } 49 | // @ts-ignore 50 | _init(onUpdate: any) { 51 | super._init() 52 | this.quickSettingsItems.push(new UnsafeQuickToggle(onUpdate)) 53 | } 54 | destroy() { 55 | this.quickSettingsItems.forEach(item => item.destroy()) 56 | super.destroy() 57 | } 58 | } 59 | GObject.registerClass(UnsafeIndicator) 60 | export { UnsafeIndicator } 61 | // #endregion UnsafeIndicator 62 | 63 | // #region UnsafeQuickToggleFeature 64 | export class UnsafeQuickToggleFeature extends FeatureBase { 65 | // #region settings 66 | enabled: boolean 67 | override loadSettings(loader: SettingLoader): void { 68 | this.enabled = loader.loadBoolean("unsafe-quick-toggle-enabled") 69 | } 70 | // #endregion settings 71 | 72 | indicator: UnsafeIndicator 73 | override onLoad(): void { 74 | if (!this.enabled) return 75 | 76 | // Load last state 77 | if (Global.Settings.get_boolean("unsafe-quick-toggle-save-last-state")) { 78 | global.context.unsafe_mode = Global.Settings.get_boolean("unsafe-quick-toggle-last-state") 79 | } 80 | 81 | // Add Unsafe Quick Toggle 82 | this.maid.destroyJob( 83 | this.indicator = new UnsafeIndicator( 84 | (state) => Global.Settings.set_boolean("unsafe-quick-toggle-last-state", state) 85 | ) 86 | ) 87 | // @ts-expect-error Missing addExternalIndicator method (QuickSettings is private class) 88 | Global.QuickSettings.addExternalIndicator(this.indicator) 89 | } 90 | override onUnload(): void { 91 | global.context.unsafe_mode = false 92 | this.indicator = null 93 | } 94 | } 95 | // #endregion UnsafeQuickToggleFeature 96 | -------------------------------------------------------------------------------- /src/global.scss: -------------------------------------------------------------------------------- 1 | $p: ".QSTWEAKS"; 2 | $v: "-QSTWEAKS"; 3 | -------------------------------------------------------------------------------- /src/global.ts: -------------------------------------------------------------------------------- 1 | import Clutter from "gi://Clutter" 2 | import St from "gi://St" 3 | import GLib from "gi://GLib" 4 | import Gio from "gi://Gio" 5 | import * as Main from "resource:///org/gnome/shell/ui/main.js" 6 | import { type Extension } from "resource:///org/gnome/shell/extensions/extension.js" 7 | import { type MessageTray } from "resource:///org/gnome/shell/ui/messageTray.js" 8 | import { type DateMenuButton } from "resource:///org/gnome/shell/ui/dateMenu.js" 9 | import * as PanelMenu from "resource:///org/gnome/shell/ui/panelMenu.js" 10 | import { 11 | type NotificationSection, 12 | type CalendarMessageList 13 | } from "resource:///org/gnome/shell/ui/calendar.js"; 14 | import { type MediaSection } from "resource:///org/gnome/shell/ui/mpris.js" 15 | import { 16 | type SystemItem, 17 | type Indicator as SystemIndicator 18 | } from "resource:///org/gnome/shell/ui/status/system.js" 19 | import { type PopupMenu } from "resource:///org/gnome/shell/ui/popupMenu.js" 20 | import { type QuickSlider, type QuickSettingsMenu } from "resource:///org/gnome/shell/ui/quickSettings.js" 21 | import Logger from "./libs/shared/logger.js" 22 | 23 | type StreamSlider = { 24 | VolumeInput: any, 25 | InputStreamSlider: QuickSlider, 26 | OutputStreamSlider: QuickSlider, 27 | } 28 | const Global = new (class Global { 29 | QuickSettings: PanelMenu.Button 30 | QuickSettingsMenu: QuickSettingsMenu 31 | QuickSettingsGrid: St.Widget 32 | QuickSettingsBox: St.BoxLayout 33 | QuickSettingsActor: St.Widget 34 | get QuickSettingsSystemIndicator(): Promise { 35 | return new Promise(resolve => { 36 | let system = (this.QuickSettings as any)._system 37 | if (system) { 38 | resolve(system) 39 | return 40 | } 41 | GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { 42 | system = (this.QuickSettings as any)._system 43 | if (!system) return GLib.SOURCE_CONTINUE 44 | resolve(system) 45 | return GLib.SOURCE_REMOVE 46 | }) 47 | }) 48 | } 49 | get QuickSettingsSystemItem(): Promise { 50 | return this.QuickSettingsSystemIndicator 51 | .then(system=>(system as any)._systemItem) 52 | .catch(Logger.error) 53 | } 54 | Indicators: St.BoxLayout 55 | 56 | DateMenu: DateMenuButton 57 | DateMenuMenu: PopupMenu 58 | DateMenuBox: Clutter.Actor 59 | DateMenuHolder: Clutter.Actor 60 | 61 | MessageTray: MessageTray 62 | 63 | Extension: Extension 64 | Settings: Gio.Settings 65 | 66 | get MessageList(): CalendarMessageList { 67 | return (this.DateMenu as any)._messageList 68 | } 69 | get NotificationSection(): NotificationSection { 70 | return (this.DateMenu as any)._messageList._notificationSection 71 | } 72 | get MediaSection(): MediaSection { 73 | return (this.DateMenu as any)._messageList._mediaSection 74 | } 75 | get DateMenuIndicator(): Clutter.Actor { 76 | return (this.DateMenu as any)._indicator 77 | } 78 | 79 | GetShutdownMenuBox(): Promise { 80 | // To prevent freeze, priority should be PRIORITY_DEFAULT_IDLE instead of PRIORITY_DEFAULT 81 | return new Promise(resolve => { 82 | GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { 83 | if (!(this.QuickSettings as any)._system) 84 | return GLib.SOURCE_CONTINUE 85 | resolve((this.QuickSettings as any)._system._systemItem.menu.box) 86 | return GLib.SOURCE_REMOVE 87 | }) 88 | }) 89 | } 90 | 91 | private StreamSliderGetter(): StreamSlider|null { 92 | if (!(this.QuickSettings as any)._volumeInput) 93 | return null 94 | return { 95 | VolumeInput: (this.QuickSettings as any)._volumeInput, 96 | InputStreamSlider: (this.QuickSettings as any)._volumeInput._input, 97 | OutputStreamSlider: (this.QuickSettings as any)._volumeOutput._output, 98 | } 99 | } 100 | GetStreamSlider(): Promise { 101 | return new Promise(resolve => { 102 | let streamSlider = this.StreamSliderGetter() 103 | if (streamSlider) { 104 | resolve(streamSlider) 105 | return 106 | } 107 | GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { 108 | streamSlider = this.StreamSliderGetter() 109 | if (!streamSlider) return GLib.SOURCE_CONTINUE 110 | resolve(streamSlider) 111 | return GLib.SOURCE_REMOVE 112 | }) 113 | }) 114 | } 115 | 116 | private DBusFiles: Map 117 | private Decoder: TextDecoder 118 | GetDbusInterface(path: string, interfaceName: string) { 119 | let cachedInfo = this.DBusFiles.get(path) 120 | if (!cachedInfo) { 121 | const DbusFile = Gio.File.new_for_path(`${this.Extension.path}/${path}`) 122 | cachedInfo = Gio.DBusNodeInfo.new_for_xml(this.Decoder.decode(DbusFile.load_contents(null)[1])) 123 | this.DBusFiles.set(path, cachedInfo) 124 | } 125 | return cachedInfo.lookup_interface(interfaceName) 126 | } 127 | 128 | private Shaders: Map 129 | GetShader(path: string): [string, string] { 130 | let cachedInfo = this.Shaders.get(path) 131 | if (!cachedInfo) { 132 | const shaderFile = Gio.File.new_for_path(`${this.Extension.path}/${path}`) 133 | const [declarations, main] = this.Decoder.decode(shaderFile.load_contents(null)[1]).split( 134 | /^.*?main\(\s?\)\s?/m 135 | ) as [string, string] 136 | cachedInfo = [ 137 | declarations.trim(), 138 | main.trim().replace(/^[{}]/gm, '').trim() 139 | ] 140 | this.Shaders.set(path, cachedInfo) 141 | } 142 | return cachedInfo 143 | } 144 | 145 | unload() { 146 | this.QuickSettings = null 147 | this.QuickSettingsMenu = null 148 | this.QuickSettingsGrid = null 149 | this.QuickSettingsBox = null 150 | this.QuickSettingsActor = null 151 | this.Indicators = null 152 | this.DateMenu = null 153 | this.DateMenuMenu = null 154 | this.DateMenuBox = null 155 | this.DateMenuHolder = null 156 | this.MessageTray = null 157 | this.Extension = null 158 | this.Settings = null 159 | this.DBusFiles = null 160 | this.Shaders = null 161 | this.Decoder = null 162 | } 163 | load(extension: Extension) { 164 | this.Extension = extension 165 | this.Settings = extension.getSettings() 166 | this.Shaders = new Map() 167 | this.DBusFiles = new Map() 168 | this.Decoder = new TextDecoder("utf-8") 169 | 170 | // Quick Settings Items 171 | const QuickSettings = this.QuickSettings = Main.panel.statusArea.quickSettings 172 | this.QuickSettingsMenu = QuickSettings.menu 173 | this.QuickSettingsGrid = QuickSettings.menu._grid 174 | this.QuickSettingsBox = QuickSettings.menu.box 175 | this.QuickSettingsActor = QuickSettings.menu.actor 176 | this.Indicators = QuickSettings._indicators 177 | 178 | // Date Menu 179 | const DateMenu = this.DateMenu = Main.panel.statusArea.dateMenu 180 | const DateMenuMenu = this.DateMenuMenu = DateMenu.menu as any 181 | this.DateMenuBox = DateMenuMenu.box 182 | this.DateMenuHolder = DateMenuMenu.box.first_child.first_child 183 | 184 | // Message 185 | this.MessageTray = Main.messageTray 186 | } 187 | })() 188 | export default Global 189 | -------------------------------------------------------------------------------- /src/libs/shared/colors.ts: -------------------------------------------------------------------------------- 1 | export type Rgba = [number, number, number, number] 2 | export namespace Rgba { 3 | export function formatCss(color: Rgba): string { 4 | const [r,g,b,a] = color 5 | return `rgba(${r},${g},${b},${a/1000})` 6 | } 7 | } 8 | export type Rgb = [number, number, number] 9 | -------------------------------------------------------------------------------- /src/libs/shared/imageUtils.ts: -------------------------------------------------------------------------------- 1 | import GdkPixbuf from "gi://GdkPixbuf" 2 | import GLib from "gi://GLib" 3 | 4 | namespace ImageMeanColor { 5 | const BASE_SIZE = 128 6 | const SKIP_RATE = 4 7 | const MAX_DIST = 255+255+255 8 | const CHANNEL_DIFF_MAX = 255 9 | const CHANNEL_DIFF_MAX_DOUBLE = CHANNEL_DIFF_MAX * CHANNEL_DIFF_MAX 10 | const CHANNEL_DIFF_CUT = 32 11 | const DIV = CHANNEL_DIFF_MAX_DOUBLE * MAX_DIST 12 | const CACHE_INDEXER_Y = BASE_SIZE 13 | const CACHE_SIZE = BASE_SIZE * BASE_SIZE 14 | export function getImageMeanColor( 15 | image: GdkPixbuf.Pixbuf 16 | ): Promise { 17 | return new Promise(resolve=>{ 18 | // const id = Math.floor(Math.random()*1000) 19 | // console.time("getImageMeanColor_"+id) 20 | const baseImage = image.scale_simple(BASE_SIZE, BASE_SIZE, GdkPixbuf.InterpType.NEAREST) 21 | const channels = baseImage.n_channels 22 | if (channels < 3) return null 23 | const rowstride = baseImage.rowstride 24 | const pixbuf = baseImage.get_pixels() 25 | let maxGravity = 0 26 | let colorR = 0, colorG = 0, colorB = 0 27 | const channelDiffCaches = new Array(CACHE_SIZE) 28 | let curY=0, curX=-1 29 | GLib.idle_add(GLib.PRIORITY_LOW, ()=>{ 30 | // Move current cursor 31 | curX += SKIP_RATE 32 | if (curX >= BASE_SIZE) { curX = 0; curY += SKIP_RATE } 33 | if (curY >= BASE_SIZE) { 34 | // console.timeEnd("getImageMeanColor_"+id) 35 | resolve([colorR, colorG, colorB]) 36 | return GLib.SOURCE_REMOVE 37 | } 38 | 39 | // Get current pixel 40 | const index = curY * rowstride + curX * channels 41 | const r = pixbuf[index] 42 | const g = pixbuf[index+1] 43 | const b = pixbuf[index+2] 44 | 45 | // Get channel difference of current pixel 46 | // Note: spidermonkey js engine doesn't have fastapi like v8, so Math.abs much slower 47 | const cacheIndex1 = curY * CACHE_INDEXER_Y + curX 48 | let channelDiff1 = channelDiffCaches[cacheIndex1] 49 | if (channelDiff1 === undefined) { 50 | const da = r-g, db = r-b, dc = g-b 51 | channelDiff1 = channelDiffCaches[cacheIndex1] = Math.max(da<0?-da:da, db<0?-db:db, dc<0?-dc:dc) 52 | } 53 | if (channelDiff1 <= CHANNEL_DIFF_CUT) return GLib.SOURCE_CONTINUE 54 | 55 | // Get gravity 56 | let gravity = 0 57 | for (let x=0; x deepEqual(value, b[index])) 18 | } 19 | if (a instanceof Object && b instanceof Object) { 20 | for (const [key, value] of Object.entries(a)) { 21 | if (!deepEqual(b[key], value)) return false 22 | } 23 | return true 24 | } 25 | return false 26 | } 27 | -------------------------------------------------------------------------------- /src/libs/shared/logger.ts: -------------------------------------------------------------------------------- 1 | // Prefixed, leveled logger 2 | function Logger(str: string|(()=>string)) { 3 | if (str instanceof Function) str = str() 4 | if (Logger.show_info) console.log(Logger.LOG_INFO_HEADER + str) 5 | } 6 | namespace Logger { 7 | export let LOG_HEADER_PREFIX: string = "" 8 | export let LOG_INFO_HEADER: string = "" 9 | export let LOG_DEBUG_HEADER: string = "" 10 | export let LOG_ERROR_HEADER: string = "" 11 | export let show_info: boolean = true 12 | export function setHeader(header: string) { 13 | LOG_HEADER_PREFIX = header 14 | LOG_INFO_HEADER = `${header} (info) ` 15 | LOG_DEBUG_HEADER = `${header} (debug) ` 16 | LOG_ERROR_HEADER = `${header} (error) ` 17 | } 18 | 19 | export enum LogLevel { 20 | none = -1, 21 | error = 0, 22 | info = 1, 23 | debug = 2, 24 | } 25 | 26 | const void_function = (()=>{}) as (str: string)=>void 27 | export let debug: (str: string|(()=>string))=>void 28 | function debug_internal(str: string|(()=>string)) { 29 | if (str instanceof Function) str = str() 30 | console.log(LOG_DEBUG_HEADER + str) 31 | } 32 | export let error: (str: string|(()=>string))=>void 33 | function error_internal(str: string|(()=>string)) { 34 | if (str instanceof Function) str = str() 35 | console.log(`${LOG_ERROR_HEADER}${str}\n${new Error().stack}`) 36 | } 37 | 38 | export let currentLevel: number 39 | export function setLogLevel(level: number) { 40 | debug = level >= LogLevel.debug 41 | ? debug_internal 42 | : void_function 43 | error = level >= LogLevel.error 44 | ? error_internal 45 | : void_function 46 | show_info = level >= LogLevel.info 47 | currentLevel = level 48 | } 49 | } 50 | export default Logger 51 | -------------------------------------------------------------------------------- /src/libs/shared/maid.ts: -------------------------------------------------------------------------------- 1 | // Connection destroyer 2 | class Maid { 3 | private records: [Maid.TaskType, number, ...any][] 4 | 5 | constructor() { 6 | this.records = [] 7 | } 8 | 9 | connectJob( 10 | signalObject: any, 11 | signalName: string, 12 | handleFunc: (...args: any)=>any, 13 | priority: number = 0 14 | ): number { 15 | const id = signalObject.connect(signalName, handleFunc) 16 | this.getRecords().push([Maid.TaskType.Connect, priority, signalObject, id]) 17 | return id 18 | } 19 | 20 | functionJob(func: (...args: any)=>any, priority: number = 0) { 21 | this.getRecords().push([Maid.TaskType.Function, priority, func]) 22 | } 23 | 24 | disposeJobvoid }>(object: T, priority: number = 0): T { 25 | this.getRecords().push([Maid.TaskType.Dispose, priority, object]) 26 | return object 27 | } 28 | 29 | runDisposeJobvoid }>(object: any, priority: number = 0): T { 30 | this.getRecords().push([Maid.TaskType.RunDispose, priority, object]) 31 | return object 32 | } 33 | 34 | destroyJobvoid }>(object: T, priority: number = 0): T { 35 | this.getRecords().push([Maid.TaskType.Destroy, priority, object]) 36 | return object 37 | } 38 | 39 | destroy() { 40 | this.clear() 41 | this.records = null 42 | } 43 | 44 | getRecords(): Maid["records"] { 45 | if (!this.records) Error("Maid object already destroyed") 46 | return this.records 47 | } 48 | 49 | patchJob( 50 | patchObject: any, 51 | patchName: string, 52 | handleFunc: (...args: any)=>any, 53 | priority: number = 0 54 | ) { 55 | const original = patchObject[patchName] 56 | this.getRecords().push([Maid.TaskType.Patch, priority, patchObject, patchName, original]) 57 | patchObject[patchName] = handleFunc(original) 58 | } 59 | 60 | // [ patchObject, connection, original, undo? ] 61 | hideJob( 62 | patchObject: T, 63 | undo?: (old: boolean, patchObject: T)=>(boolean|null|void|undefined), 64 | priority: number = 0 65 | ) { 66 | const original = patchObject.visible 67 | const connection = patchObject.connect("show", ()=>{ 68 | patchObject.hide() 69 | }) 70 | patchObject.hide() 71 | this.getRecords().push([Maid.TaskType.Hide, priority, patchObject, connection, original, undo]) 72 | } 73 | 74 | clear() { 75 | const records = this.getRecords() 76 | records.sort((a, b) => b[1] - a[1]) 77 | for (const record of records) { 78 | switch (record[0]) { 79 | case Maid.TaskType.Connect: 80 | record[2].disconnect(record[3]) 81 | break 82 | case Maid.TaskType.Function: 83 | record[2]() 84 | break 85 | case Maid.TaskType.Dispose: 86 | record[2].dispose() 87 | break 88 | case Maid.TaskType.RunDispose: 89 | record[2].run_dispose() 90 | break 91 | case Maid.TaskType.Destroy: 92 | record[2].destroy() 93 | break 94 | case Maid.TaskType.Patch: 95 | record[2][record[1]] = record[2] 96 | break 97 | case Maid.TaskType.Hide: 98 | { 99 | const patchObject = record[2] 100 | const original = record[4] 101 | const undo = record[5] 102 | patchObject.disconnect(record[3]) 103 | if (undo) { 104 | const result = undo(original, patchObject) 105 | if (result === true) { 106 | patchObject.show() 107 | } 108 | } else { 109 | if (original) patchObject.show() 110 | } 111 | } 112 | break 113 | default: 114 | throw Error("Unknown task type.") 115 | } 116 | } 117 | this.records = [] 118 | } 119 | } 120 | namespace Maid { 121 | export enum TaskType { 122 | Connect, 123 | Function, 124 | Dispose, 125 | RunDispose, 126 | Destroy, 127 | Patch, 128 | Hide, 129 | } 130 | export const Priority = { 131 | High: 2000, 132 | Default: 0, 133 | Low: -2000, 134 | } 135 | } 136 | export default Maid 137 | -------------------------------------------------------------------------------- /src/libs/shared/styleClass.ts: -------------------------------------------------------------------------------- 1 | // Re-layout & painting only once 2 | // We use StyleClass instead of add_style_class_name in this extension 3 | export class StyleClass { 4 | classArray: string[] 5 | modified: boolean 6 | constructor(classString: string) { 7 | this.modified = false 8 | this.classArray = classString.split(" ") 9 | } 10 | remove(className: string): StyleClass { 11 | const lastLen = this.classArray.length 12 | this.classArray = this.classArray.filter( 13 | i => i != className 14 | ) 15 | if (this.classArray.length != lastLen) { 16 | this.modified = true 17 | } 18 | return this 19 | } 20 | add(className: string): StyleClass { 21 | if (this.classArray.includes(className)) return this 22 | this.classArray.push(className) 23 | this.modified = true 24 | return this 25 | } 26 | stringify(): string { 27 | return this.classArray.join(" ") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/libs/shell/advani.ts: -------------------------------------------------------------------------------- 1 | import Clutter from "gi://Clutter" 2 | import Graphene from "gi://Graphene" 3 | 4 | // Gnome base ease function params 5 | export interface EasingParams { 6 | repeatCount?: number 7 | autoReverse?: boolean 8 | animationRequired?: boolean 9 | duration?: number 10 | delay?: number 11 | mode?: Clutter.AnimationMode | any 12 | [key: string]: any 13 | } 14 | 15 | // AdvAni ease function params 16 | export interface AdvEasingParams extends EasingParams { 17 | mode: Clutter.AnimationMode | AdvAnimationMode, 18 | } 19 | 20 | // AdvAni ease mode define type 21 | export interface ModeDefineIface { 22 | mode: Clutter.AnimationMode 23 | getCubicBezierProgress?: ()=>[Graphene.Point, Graphene.Point] 24 | cubicBezierProgress?: [Graphene.Point, Graphene.Point] 25 | } 26 | export interface ModeDefine extends ModeDefineIface {} 27 | export class ModeDefine { 28 | constructor(params: ModeDefineIface) { 29 | for (const [key, value] of Object.entries(params)) { 30 | this[key] = value 31 | } 32 | } 33 | } 34 | 35 | // Utility functions 36 | export function createBezier( 37 | x1: number, y1: number, x2: number, y2: number 38 | ):[Graphene.Point, Graphene.Point] { 39 | return [ 40 | new Graphene.Point({ x: x1, y: y1 }), 41 | new Graphene.Point({ x: x2, y: y2 }) 42 | ] 43 | } 44 | 45 | // Template AdvAni animations 46 | export enum AdvAnimationMode { 47 | LowBackover = 2000, 48 | MiddleBackover = 2001, 49 | } 50 | export const AdvAnimationModeDefines = [ 51 | new ModeDefine({ 52 | mode: Clutter.AnimationMode.CUBIC_BEZIER, 53 | getCubicBezierProgress: ()=>createBezier(.225,1.2,.45,1) 54 | }), 55 | new ModeDefine({ 56 | mode: Clutter.AnimationMode.CUBIC_BEZIER, 57 | getCubicBezierProgress: ()=>createBezier(.4,1.35,.55,1) 58 | }), 59 | ] as ModeDefine[] 60 | 61 | // Main AdvAni ease function 62 | export function ease(actor: Clutter.Actor, params: AdvEasingParams) { 63 | // Get mode defines 64 | let modeDefine: ModeDefine|null 65 | if (params.mode && params.mode > Clutter.AnimationMode.ANIMATION_LAST) { 66 | modeDefine = AdvAnimationModeDefines[params.mode - AdvAnimationMode.LowBackover] 67 | params.mode = modeDefine.mode 68 | } else if ((typeof params.mode == "object") && ((params.mode as any) instanceof ModeDefine)) { 69 | modeDefine = params.mode 70 | params.mode = modeDefine.mode 71 | } 72 | 73 | // Run gnome ease function 74 | actor.ease(params) 75 | if (!modeDefine) return 76 | 77 | // Adjust bezier progress if option exist 78 | let { getCubicBezierProgress, cubicBezierProgress } = modeDefine 79 | if (getCubicBezierProgress) cubicBezierProgress = getCubicBezierProgress() 80 | if (cubicBezierProgress) { 81 | for (const key in params) { 82 | const transition = actor.get_transition(key.replace(/_/g, '-')) 83 | if (!transition) continue 84 | transition.set_cubic_bezier_progress(...cubicBezierProgress) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/libs/shell/compat.ts: -------------------------------------------------------------------------------- 1 | import { PACKAGE_VERSION } from 'resource:///org/gnome/shell/misc/config.js' 2 | import Clutter from "gi://Clutter" 3 | 4 | export const GnomeVersion = Number.parseFloat(PACKAGE_VERSION) 5 | 6 | export const VerticalProp = ( 7 | GnomeVersion >= 48 8 | ? { orientation: Clutter.Orientation.VERTICAL } 9 | : { vertical: true } 10 | ) 11 | 12 | export function setVertical(actor: any, value: boolean) { 13 | if (GnomeVersion >= 48) { 14 | actor.orientation = 15 | value 16 | ? Clutter.Orientation.VERTICAL 17 | : Clutter.Orientation.HORIZONTAL 18 | } else { 19 | actor.vertical = value 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/libs/shell/effects.ts: -------------------------------------------------------------------------------- 1 | import GObject from 'gi://GObject' 2 | import Shell from 'gi://Shell' 3 | import Clutter from 'gi://Clutter' 4 | import Global from '../../global.js' 5 | 6 | // #region RoundClipEffect 7 | export class RoundClipEffect extends Shell.GLSLEffect { 8 | static uniforms: RoundClipEffect.Uniforms|null = null 9 | 10 | vfunc_build_pipeline (): void { 11 | const [declarations, code] = Global.GetShader("media/rounded_corners.frag") 12 | this.add_glsl_snippet( 13 | Shell.SnippetHook.FRAGMENT, 14 | declarations, 15 | code, 16 | false 17 | ) 18 | } 19 | 20 | vfunc_paint_target (node: Clutter.PaintNode, ctx: Clutter.PaintContext) { 21 | // Reset to default blend string. 22 | this.get_pipeline()?.set_blend( 23 | 'RGBA = ADD(SRC_COLOR, DST_COLOR*(1-SRC_COLOR[A]))' 24 | ) 25 | super.vfunc_paint_target(node, ctx) 26 | } 27 | 28 | updateUniforms ( 29 | scale_factor: number, 30 | corners_cfg: { 31 | padding?: { left: number, right: number, top: number, bottom: number }, 32 | border_radius: number, 33 | smoothing: number, 34 | }, 35 | outer_bounds: { x1: number, x2: number, y1: number, y2: number }, 36 | border?: { 37 | width: number 38 | color: [number, number, number, number] 39 | }, 40 | pixel_step?: [number, number] 41 | ) { 42 | const border_width = (border?.width ?? 0) * scale_factor 43 | const border_color = border?.color ?? [0, 0, 0, 0] 44 | 45 | const outer_radius = corners_cfg.border_radius * scale_factor 46 | const { padding, smoothing } = corners_cfg 47 | 48 | const bounds = [ 49 | outer_bounds.x1 + (padding ? (padding.left * scale_factor) : 0), 50 | outer_bounds.y1 + (padding ? (padding.top * scale_factor) : 0), 51 | outer_bounds.x2 - (padding ? (padding.right * scale_factor) : 0), 52 | outer_bounds.y2 - (padding ? (padding.bottom * scale_factor) : 0), 53 | ] 54 | 55 | const inner_bounds = [ 56 | bounds[0] + border_width, 57 | bounds[1] + border_width, 58 | bounds[2] - border_width, 59 | bounds[3] - border_width, 60 | ] 61 | 62 | let inner_radius = outer_radius - border_width 63 | if (inner_radius < 0.001) { 64 | inner_radius = 0.0 65 | } 66 | 67 | if (!pixel_step) { 68 | const actor = this.actor 69 | pixel_step = [1 / actor.get_width (), 1 / actor.get_height ()] 70 | } 71 | 72 | // Setup with squircle shape 73 | let exponent = smoothing * 10.0 + 2.0 74 | let radius = outer_radius * 0.5 * exponent 75 | const max_radius = Math.min (bounds[3] - bounds[0], bounds[4] - bounds[1]) 76 | if (radius > max_radius) { 77 | exponent *= max_radius / radius 78 | radius = max_radius 79 | } 80 | inner_radius *= radius / outer_radius 81 | 82 | const location = this.getLocation() 83 | this.set_uniform_float(location.bounds, 4, bounds) 84 | this.set_uniform_float(location.inner_bounds, 4, inner_bounds) 85 | this.set_uniform_float(location.pixel_step, 2, pixel_step) 86 | this.set_uniform_float(location.border_width, 1, [border_width]) 87 | this.set_uniform_float(location.exponent, 1, [exponent]) 88 | this.set_uniform_float(location.clip_radius, 1, [radius]) 89 | this.set_uniform_float(location.border_color, 4, border_color) 90 | this.set_uniform_float(location.inner_clip_radius, 1, [inner_radius]) 91 | this.queue_repaint() 92 | } 93 | 94 | getLocation(): RoundClipEffect.Uniforms { 95 | let location = RoundClipEffect.uniforms 96 | if (!location) { 97 | location = new RoundClipEffect.Uniforms() 98 | for (const key in location) { 99 | location[key] = this.get_uniform_location(key) 100 | } 101 | RoundClipEffect.uniforms = location 102 | } 103 | return location 104 | } 105 | } 106 | GObject.registerClass(RoundClipEffect) 107 | export namespace RoundClipEffect { 108 | // Uniform location cache 109 | export class Uniforms { 110 | bounds = 0 111 | clip_radius = 0 112 | exponent = 0 113 | inner_bounds = 0 114 | inner_clip_radius = 0 115 | pixel_step = 0 116 | border_width = 0 117 | border_color = 0 118 | } 119 | } 120 | // #endregion RoundClipEffect 121 | -------------------------------------------------------------------------------- /src/libs/shell/feature.ts: -------------------------------------------------------------------------------- 1 | import { type Rgb, type Rgba } from "../shared/colors.js" 2 | import Maid from "../shared/maid.js"; 3 | import Global from "../../global.js"; 4 | import Logger from "../shared/logger.js"; 5 | 6 | export class SettingLoader { 7 | records: Set 8 | listeners: number[] 9 | onChange: (key: string)=>void 10 | parent: FeatureBase 11 | constructor( 12 | onChange: SettingLoader["onChange"], 13 | parent: FeatureBase, 14 | ) { 15 | this.parent = parent 16 | this.records = new Set() 17 | this.listeners = [] 18 | this.onChange = onChange 19 | } 20 | 21 | private push(key: string) { 22 | if (this.records.has(key)) return 23 | this.records.add(key) 24 | this.listeners.push( 25 | Global.Settings.connect( 26 | `changed::${key}`, 27 | () => this.onChange(key) 28 | ) 29 | ) 30 | if (!this.parent.disableDebugMessage) 31 | Logger.debug(()=>`Setting listener for key '${key}' added for feature ${this.parent.constructor.name}`) 32 | } 33 | clear() { 34 | for (const source of this.listeners) { 35 | Global.Settings.disconnect(source) 36 | } 37 | this.listeners = [] 38 | this.records.clear() 39 | if (!this.parent.disableDebugMessage) { 40 | Logger.debug(()=>`Disconnected setting listeners for feature ${this.parent.constructor.name }`) 41 | } 42 | } 43 | 44 | loadBoolean(key: string): boolean { 45 | this.push(key) 46 | return Global.Settings.get_boolean(key) 47 | } 48 | loadString(key: string): string { 49 | this.push(key) 50 | return Global.Settings.get_string(key) 51 | } 52 | loadInt(key: string): number { 53 | this.push(key) 54 | return Global.Settings.get_int(key) 55 | } 56 | loadStrv(key: string): string[] { 57 | this.push(key) 58 | return Global.Settings.get_strv(key) 59 | } 60 | loadValue(key: string): T { 61 | this.push(key) 62 | return Global.Settings.get_value(key).recursiveUnpack() 63 | } 64 | loadRgb(key: string): Rgb|null { 65 | this.push(key) 66 | const color = Global.Settings.get_value(key).recursiveUnpack() 67 | if (!color.length) return null 68 | return color 69 | } 70 | loadRgba(key: string): Rgba|null { 71 | this.push(key) 72 | const color = Global.Settings.get_value(key).recursiveUnpack() 73 | if (!color.length) return null 74 | return color 75 | } 76 | } 77 | 78 | export abstract class FeatureBase { 79 | disableDebugMessage: boolean = false 80 | loader: SettingLoader 81 | maid: Maid 82 | 83 | constructor() { 84 | this.maid = new Maid() 85 | this.loader = new SettingLoader((key: string)=>{ 86 | this.loader.clear() 87 | this.loadSettings(this.loader) 88 | this.reload(key) 89 | }, this) 90 | } 91 | 92 | load(noSettingsLoad?: boolean): void { 93 | if (!noSettingsLoad) this.loadSettings(this.loader) 94 | this.onLoad() 95 | } 96 | unload(noSettingsUnload?: boolean): void { 97 | if (!noSettingsUnload) this.loader.clear() 98 | this.onUnload() 99 | this.maid.clear() 100 | } 101 | abstract onLoad(): void 102 | abstract onUnload(): void 103 | reload(changedKey?: string): void { 104 | this.unload(true) 105 | this.load(true) 106 | } 107 | abstract loadSettings(loader: SettingLoader): void 108 | } 109 | -------------------------------------------------------------------------------- /src/libs/shell/gesture.ts: -------------------------------------------------------------------------------- 1 | import St from "gi://St" 2 | import Clutter from "gi://Clutter" 3 | 4 | // #region Drag 5 | export abstract class Drag extends St.Bin { 6 | _dragging: boolean 7 | _dragIsClick: boolean 8 | _dragStartCoords: Drag.Coords 9 | _dragMoveStartCoords: Drag.Coords 10 | _grab: Clutter.Grab 11 | _grabbedDevice: Clutter.InputDevice 12 | _grabbedSequence: Clutter.EventSequence 13 | dfunc_drag_end: (event: Drag.Event)=>void 14 | dfunc_drag_start: (event: Drag.Event)=>void 15 | dfunc_drag_motion: (event: Drag.Event)=>void 16 | 17 | _dragStart(event: Clutter.Event): boolean { 18 | if (this._dragging) return Clutter.EVENT_PROPAGATE 19 | this._dragging = true 20 | this._dragIsClick = true 21 | this._dragStartCoords = event.get_coords() 22 | this._grabbedDevice = event.get_device() 23 | this._grabbedSequence = event.get_event_sequence() 24 | 25 | // @ts-expect-error Types not implemented 26 | this._grab = global.stage.grab(this) 27 | 28 | const dragEvent: Drag.Event = event as Drag.Event 29 | dragEvent.isClick = true 30 | dragEvent.startCoords = this._dragStartCoords 31 | dragEvent.coords = this._dragStartCoords 32 | dragEvent.moveStartCoords = this._dragMoveStartCoords 33 | if (this.dfunc_drag_start) this.dfunc_drag_start(dragEvent) 34 | 35 | return Clutter.EVENT_STOP 36 | } 37 | _dragEnd(event: Clutter.Event): boolean { 38 | if (!this._dragging) { 39 | return Clutter.EVENT_PROPAGATE 40 | } 41 | 42 | if (this._grab) { 43 | this._grab.dismiss() 44 | this._grab = null 45 | } 46 | 47 | this._grabbedSequence = null 48 | this._grabbedDevice = null 49 | this._dragging = false 50 | 51 | const coords = event.get_coords() 52 | this._checkDragIsClick(coords) 53 | 54 | const dragEvent: Drag.Event = event as Drag.Event 55 | dragEvent.isClick = this._dragIsClick 56 | dragEvent.startCoords = this._dragStartCoords 57 | dragEvent.coords = coords 58 | dragEvent.moveStartCoords = this._dragMoveStartCoords 59 | 60 | this._dragStartCoords = 61 | this._dragMoveStartCoords = null 62 | 63 | if (this.dfunc_drag_end) this.dfunc_drag_end(dragEvent) 64 | return Clutter.EVENT_STOP 65 | } 66 | _dragMotion(event: Clutter.Event): boolean { 67 | const coords = event.get_coords() 68 | this._checkDragIsClick(coords) 69 | 70 | const dragEvent: Drag.Event = event as Drag.Event 71 | dragEvent.isClick = this._dragIsClick 72 | dragEvent.startCoords = this._dragStartCoords 73 | dragEvent.coords = coords 74 | dragEvent.moveStartCoords = this._dragMoveStartCoords 75 | if (this.dfunc_drag_motion) this.dfunc_drag_motion(dragEvent) 76 | return Clutter.EVENT_STOP 77 | } 78 | _checkDragIsClick(coords: Drag.Coords) { 79 | if (!this._dragIsClick) return 80 | if ( 81 | Drag.getCoordsDistanceSquare( 82 | coords, this._dragStartCoords 83 | ) > Drag.DragMinPixelSquare 84 | ) { 85 | this._dragMoveStartCoords = coords 86 | this._dragIsClick = false 87 | } 88 | } 89 | 90 | vfunc_button_press_event(event: Clutter.Event): boolean { 91 | return this._dragStart(event) 92 | } 93 | 94 | vfunc_button_release_event(event: Clutter.Event): boolean { 95 | return this._dragEnd(event) 96 | } 97 | 98 | vfunc_touch_event(event: Clutter.Event): boolean { 99 | const sequence = event.get_event_sequence() 100 | const slotSame = this._grabbedSequence && sequence.get_slot() === this._grabbedSequence.get_slot() 101 | 102 | switch (event.type()) { 103 | case Clutter.EventType.TOUCH_BEGIN: 104 | return this._dragStart(event) 105 | case Clutter.EventType.TOUCH_UPDATE: 106 | if (!slotSame) return Clutter.EVENT_PROPAGATE 107 | return this._dragMotion(event) 108 | case Clutter.EventType.TOUCH_END: 109 | if (!slotSame) return Clutter.EVENT_PROPAGATE 110 | return this._dragEnd(event) 111 | } 112 | 113 | return Clutter.EVENT_PROPAGATE; 114 | } 115 | 116 | vfunc_motion_event(event: Clutter.Event): boolean { 117 | if (this._dragging && !this._grabbedSequence) { 118 | return this._dragMotion(event) 119 | } 120 | return Clutter.EVENT_PROPAGATE 121 | } 122 | 123 | static applyTo(widgetClass: any) { 124 | const widgetProto = widgetClass.prototype 125 | const dragProto = Drag.prototype 126 | for (const methodName of Object.getOwnPropertyNames(dragProto)) { 127 | Object.defineProperty(widgetProto, methodName, { 128 | value: dragProto[methodName], 129 | configurable: true, 130 | writable: true, 131 | }) 132 | } 133 | } 134 | static getCoordsDistanceSquare(coordsA: Drag.Coords, coordsB: Drag.Coords): number { 135 | const [ax, ay] = coordsA 136 | const [bx, by] = coordsB 137 | const xdist = ax - bx 138 | const ydist = ay - by 139 | return xdist*xdist + ydist*ydist 140 | } 141 | } 142 | export namespace Drag { 143 | export const DragMinPixel = 6 144 | export const DragMinPixelSquare = DragMinPixel*DragMinPixel 145 | export type Coords = [number, number] 146 | export type Event = Clutter.Event & { 147 | isClick: boolean, 148 | moveStartCoords: Coords, 149 | startCoords: Coords, 150 | coords: Coords, 151 | } 152 | } 153 | // #endregion Drag 154 | 155 | // #region Scroll 156 | export abstract class Scroll extends St.Bin { 157 | _scrollSumY: number 158 | _scrollSumX: number 159 | _scrolling: boolean 160 | dfunc_scroll_start: (event: Scroll.Event)=>void 161 | dfunc_scroll_motion: (event: Scroll.Event)=>void 162 | dfunc_scroll_end: (event: Scroll.Event)=>void 163 | 164 | vfunc_scroll_event(event: Clutter.Event): boolean { 165 | if ( 166 | event.get_scroll_direction() != Clutter.ScrollDirection.SMOOTH 167 | || event.get_scroll_source() == Clutter.ScrollSource.WHEEL 168 | ) return Clutter.EVENT_PROPAGATE 169 | const finish = event.get_scroll_finish_flags() 170 | const [dx, dy] = event.get_scroll_delta() 171 | if (!this._scrolling) { 172 | this._scrolling = true 173 | this._scrollSumX = dx 174 | this._scrollSumY = dy 175 | if (this.dfunc_scroll_start) { 176 | const scrollEvent: Scroll.Event = event as Scroll.Event 177 | scrollEvent.scrollSumX = dx 178 | scrollEvent.scrollSumY = dy 179 | scrollEvent.dx = dx 180 | scrollEvent.dy = dy 181 | this.dfunc_scroll_start(scrollEvent) 182 | } 183 | } else { 184 | this._scrollSumX += dx 185 | this._scrollSumY += dy 186 | if (this.dfunc_scroll_motion) { 187 | const scrollEvent: Scroll.Event = event as Scroll.Event 188 | scrollEvent.scrollSumX = this._scrollSumX 189 | scrollEvent.scrollSumY = this._scrollSumY 190 | scrollEvent.dx = dx 191 | scrollEvent.dy = dy 192 | this.dfunc_scroll_motion(scrollEvent) 193 | } 194 | } 195 | if (finish != Clutter.ScrollFinishFlags.NONE) { 196 | this._scrolling = false 197 | if (this.dfunc_scroll_end) { 198 | const scrollEvent: Scroll.Event = event as Scroll.Event 199 | scrollEvent.scrollSumX = this._scrollSumX 200 | scrollEvent.scrollSumY = this._scrollSumY 201 | scrollEvent.dx = dx 202 | scrollEvent.dy = dy 203 | scrollEvent.finish = finish 204 | this.dfunc_scroll_end(scrollEvent) 205 | } 206 | } 207 | } 208 | 209 | static applyTo(widgetClass: any) { 210 | const widgetProto = widgetClass.prototype 211 | const scrollProto = Scroll.prototype 212 | for (const methodName of Object.getOwnPropertyNames(scrollProto)) { 213 | Object.defineProperty(widgetProto, methodName, { 214 | value: scrollProto[methodName], 215 | configurable: true, 216 | writable: true, 217 | }) 218 | } 219 | } 220 | } 221 | export namespace Scroll { 222 | export type Event = Clutter.Event & { 223 | scrollSumX: number 224 | scrollSumY: number 225 | dx: number 226 | dy: number 227 | finish?: Clutter.ScrollFinishFlags 228 | } 229 | } 230 | // #endregion Scroll 231 | -------------------------------------------------------------------------------- /src/libs/shell/quickSettingsUtils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | QuickMenuToggle, 3 | QuickToggle, 4 | SystemIndicator, 5 | type QuickSettingsMenu, 6 | } from "resource:///org/gnome/shell/ui/quickSettings.js" 7 | import { type PopupMenuBase, PopupSeparatorMenuItem } from "resource:///org/gnome/shell/ui/popupMenu.js" 8 | import Global from "../../global.js" 9 | import Maid from "../shared/maid.js" 10 | 11 | export abstract class ChildrenTrackerBase { 12 | appliedChild: Map 13 | addConnection: number 14 | connectTarget: any 15 | protected abstract getConnectTarget(): any 16 | protected abstract catchChild(child: any): void 17 | load(): void { 18 | const connectTarget = this.connectTarget = this.getConnectTarget() 19 | this.appliedChild = new Map() 20 | this.addConnection = connectTarget.connect("child-added", (_: any, child: any)=>{ 21 | this.catchChild(child) 22 | if (this.onUpdate) this.onUpdate() 23 | }) 24 | for (const child of connectTarget.get_children()) { 25 | this.catchChild(child) 26 | } 27 | if (this.onUpdate) this.onUpdate() 28 | } 29 | unload(): void { 30 | for (const maid of this.appliedChild.values()) { 31 | maid.destroy() 32 | } 33 | this.connectTarget.disconnect(this.addConnection) 34 | this.addConnection = null 35 | this.appliedChild = null 36 | } 37 | get items(): T[] { 38 | if (!this.appliedChild) return [] 39 | return [...this.appliedChild.keys()] 40 | } 41 | onUpdate: ()=>void 42 | } 43 | 44 | export class QuickSettingsMenuTracker extends ChildrenTrackerBase { 45 | onMenuOpen: (maid: Maid, menu: QuickSettingsMenu, isOpen: boolean)=>void 46 | onMenuCreated: (maid: Maid, menu: QuickSettingsMenu)=>void 47 | protected override catchChild(child: any): void { 48 | const menu = child.menu 49 | if (!menu) return 50 | if (this.appliedChild.has(menu)) return 51 | 52 | const menuMaid = new Maid() 53 | menuMaid.functionJob(()=>{ 54 | this.appliedChild.delete(menu) 55 | }) 56 | menuMaid.connectJob(menu, "open-state-changed", (_: any, isOpen: boolean) => { 57 | if (this.onMenuOpen) this.onMenuOpen(menuMaid, menu, isOpen) 58 | }) 59 | menuMaid.connectJob(menu, "destroy", ()=>{ 60 | menuMaid.destroy() 61 | }) 62 | if (this.onMenuCreated) this.onMenuCreated(menuMaid, menu) 63 | this.appliedChild.set(menu, menuMaid) 64 | } 65 | protected override getConnectTarget() { 66 | return Global.QuickSettingsGrid 67 | } 68 | get menus() { 69 | if (!this.appliedChild) return [] 70 | return [...this.appliedChild.keys()] 71 | } 72 | } 73 | 74 | export class QuickSettingsToggleTracker extends ChildrenTrackerBase { 75 | onToggleCreated: (maid: Maid, toggle: QuickToggle|QuickMenuToggle)=>void 76 | protected override catchChild(child: any): void { 77 | if ( 78 | !(child instanceof QuickToggle) 79 | && !(child instanceof QuickMenuToggle) 80 | ) return 81 | if (this.appliedChild.has(child)) return 82 | 83 | const toggleMaid = new Maid() 84 | toggleMaid.functionJob(()=>{ 85 | this.appliedChild.delete(child) 86 | }) 87 | toggleMaid.connectJob(child, "destroy", ()=>{ 88 | toggleMaid.destroy() 89 | }) 90 | if (this.onToggleCreated) this.onToggleCreated(toggleMaid, child) 91 | this.appliedChild.set(child, toggleMaid) 92 | } 93 | protected override getConnectTarget() { 94 | return Global.QuickSettingsGrid 95 | } 96 | } 97 | 98 | export class SystemIndicatorTracker extends ChildrenTrackerBase { 99 | onIndicatorCreated: (maid: Maid, indicator: SystemIndicator)=>void 100 | protected override catchChild(child: any): void { 101 | if ( 102 | !(child instanceof SystemIndicator) 103 | ) return 104 | if (this.appliedChild.has(child)) return 105 | 106 | const indicatorMaid = new Maid() 107 | indicatorMaid.functionJob(()=>{ 108 | this.appliedChild.delete(child) 109 | }) 110 | indicatorMaid.connectJob(child, "destroy", ()=>{ 111 | indicatorMaid.destroy() 112 | }) 113 | if (this.onIndicatorCreated) this.onIndicatorCreated(indicatorMaid, child) 114 | this.appliedChild.set(child, indicatorMaid) 115 | } 116 | protected override getConnectTarget() { 117 | return Global.Indicators 118 | } 119 | } 120 | 121 | export function updateMenuSeparators(menu: PopupMenuBase) { 122 | for (const item of (menu as any)._getMenuItems()) { 123 | if (!(item instanceof PopupSeparatorMenuItem)) { 124 | continue 125 | } 126 | (menu as any)._updateSeparatorVisibility(item) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/libs/shell/styler.ts: -------------------------------------------------------------------------------- 1 | import St from "gi://St" 2 | import { type SettingLoader } from "./feature.js" 3 | import { Rgba } from "../shared/colors.js" 4 | 5 | export namespace StyledSlider { 6 | export interface Options { 7 | style: "slim" | "default" 8 | activeBackgroundColor: Rgba | null 9 | handleRadius: number 10 | handleColor: Rgba | null 11 | backgroundColor: Rgba | null 12 | height: number 13 | } 14 | export function getStyle(options: Options): string { 15 | const { 16 | style, 17 | activeBackgroundColor, 18 | handleRadius, 19 | handleColor, 20 | backgroundColor, 21 | height, 22 | } = options 23 | const styleList = [] 24 | switch (style) { 25 | case "slim": 26 | styleList.push("-slider-handle-radius:0px") 27 | if (activeBackgroundColor) { 28 | styleList.push("color:"+Rgba.formatCss(activeBackgroundColor)) 29 | } else { 30 | styleList.push("color:-st-accent-color") 31 | } 32 | break 33 | case "default": 34 | default: 35 | if (handleRadius) { 36 | styleList.push(`-slider-handle-radius:${handleRadius}px`) 37 | } 38 | if (handleColor) { 39 | styleList.push(`color:${Rgba.formatCss(handleColor)}`) 40 | } 41 | break 42 | } 43 | if (height) styleList.push(`-barlevel-height:${height}px`) 44 | if (activeBackgroundColor) styleList.push( 45 | `-barlevel-active-background-color:${Rgba.formatCss(activeBackgroundColor)}` 46 | ) 47 | if (backgroundColor) styleList.push( 48 | `-barlevel-background-color:${Rgba.formatCss(backgroundColor)}` 49 | ) 50 | const result = styleList.join(";") 51 | return result 52 | } 53 | export namespace Options { 54 | export function fromLoader(loader: SettingLoader, prefix: string): Options { 55 | return { 56 | style: loader.loadString(prefix+"-style") as Options["style"], 57 | handleColor: loader.loadRgba(prefix+"-handle-color"), 58 | handleRadius: loader.loadInt(prefix+"-handle-radius"), 59 | backgroundColor: loader.loadRgba(prefix+"-background-color"), 60 | height: loader.loadInt(prefix+"-height"), 61 | activeBackgroundColor: loader.loadRgba(prefix+"-active-background-color"), 62 | } 63 | } 64 | export function isStyleKey(prefix: string, key: string): boolean { 65 | if (key == prefix + "-style") return true 66 | if (key == prefix + "-handle-color") return true 67 | if (key == prefix + "-handle-radius") return true 68 | if (key == prefix + "-background-color") return true 69 | if (key == prefix + "-height") return true 70 | if (key == prefix + "-active-background-color") return true 71 | return false 72 | } 73 | } 74 | } 75 | 76 | export namespace StyledScroll { 77 | export interface Options { 78 | showScrollbar: boolean 79 | fadeOffset: number 80 | } 81 | export function updateStyle(scroll: St.ScrollView, options: Options) { 82 | scroll.style_class = 83 | options.fadeOffset 84 | ? "vfade" 85 | : "" 86 | scroll.vscrollbar_policy = 87 | options.showScrollbar 88 | ? St.PolicyType.AUTOMATIC 89 | : St.PolicyType.EXTERNAL 90 | scroll.style = 91 | options.fadeOffset 92 | ? `-st-vfade-offset:${options.fadeOffset}px;` 93 | : "" 94 | } 95 | export namespace Options { 96 | export function fromLoader(loader: SettingLoader, prefix: string): Options { 97 | return { 98 | showScrollbar: loader.loadBoolean(prefix+"-show-scrollbar"), 99 | fadeOffset: loader.loadInt(prefix+"-fade-offset"), 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/libs/types/quickSettingsOrderItem.ts: -------------------------------------------------------------------------------- 1 | export interface QuickSettingsOrderItem { 2 | id?: string 3 | lineBreak?: boolean 4 | pageBreak?: boolean 5 | hide?: boolean // not used 6 | friendlyName?: string // not used 7 | } 8 | export namespace QuickSettingsOrderItem { 9 | export function match(a: QuickSettingsOrderItem, b: QuickSettingsOrderItem) { 10 | return a.id == b.id 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/libs/types/systemIndicatorOrderItem.ts: -------------------------------------------------------------------------------- 1 | import GObject from "gi://GObject" 2 | import { 3 | type SystemIndicator, 4 | } from "resource:///org/gnome/shell/ui/quickSettings.js" 5 | 6 | export interface SystemIndicatorOrderItem { 7 | gtypeName?: string 8 | constructorName?: string 9 | friendlyName?: string 10 | nonOrdered?: boolean 11 | isSystem?: boolean 12 | hide?: boolean 13 | } 14 | export namespace SystemIndicatorOrderItem { 15 | export function match(a: SystemIndicatorOrderItem, b: SystemIndicatorOrderItem) { 16 | if ( 17 | a.isSystem != b.isSystem 18 | || a.nonOrdered != b.nonOrdered 19 | || a.hide != b.hide 20 | ) return false 21 | if (a.nonOrdered) return true 22 | if (a.isSystem) return a.gtypeName == b.gtypeName 23 | return ( 24 | a.constructorName == b.constructorName 25 | && a.friendlyName == b.friendlyName 26 | && a.gtypeName == b.gtypeName 27 | ) 28 | } 29 | export function indicatorMatch(item: SystemIndicatorOrderItem, indicator: SystemIndicator): boolean { 30 | if (item.nonOrdered) return false 31 | if (item.gtypeName && GObject.type_name_from_instance(indicator as any) != item.gtypeName) 32 | return false 33 | if (item.constructorName && indicator.constructor.name != item.constructorName) 34 | return false 35 | return true 36 | } 37 | export const Default: SystemIndicatorOrderItem = { 38 | hide: false, 39 | constructorName: "", 40 | friendlyName: "", 41 | gtypeName: "", 42 | } 43 | export function create(friendlyName: string): SystemIndicatorOrderItem { 44 | return { 45 | ...Default, 46 | friendlyName, 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/libs/types/toggleOrderItem.ts: -------------------------------------------------------------------------------- 1 | import GObject from "gi://GObject" 2 | import { 3 | type QuickToggle, 4 | type QuickMenuToggle, 5 | } from "resource:///org/gnome/shell/ui/quickSettings.js" 6 | 7 | export interface ToggleOrderItem { 8 | gtypeName?: string 9 | constructorName?: string 10 | titleRegex?: string 11 | friendlyName?: string 12 | nonOrdered?: boolean 13 | isSystem?: boolean 14 | cachedTitleRegex?: RegExp 15 | hide?: boolean 16 | } 17 | export namespace ToggleOrderItem { 18 | export function match(a: ToggleOrderItem, b: ToggleOrderItem) { 19 | if ( 20 | a.isSystem != b.isSystem 21 | || a.nonOrdered != b.nonOrdered 22 | || a.hide != b.hide 23 | ) return false 24 | if (a.nonOrdered) return true 25 | if (a.isSystem) return a.constructorName == b.constructorName 26 | return ( 27 | a.constructorName == b.constructorName 28 | && a.titleRegex == b.titleRegex 29 | && a.friendlyName == b.friendlyName 30 | && a.gtypeName == b.gtypeName 31 | ) 32 | } 33 | export function toggleMatch(item: ToggleOrderItem, toggle: QuickToggle|QuickMenuToggle): boolean { 34 | if (item.nonOrdered) return false 35 | if (item.gtypeName && GObject.type_name_from_instance(toggle as any) != item.gtypeName) 36 | return false 37 | if (item.constructorName && toggle.constructor.name != item.constructorName) 38 | return false 39 | if (item.cachedTitleRegex && toggle.title.match(item.cachedTitleRegex) == null) 40 | return false 41 | if (!item.gtypeName && !item.constructorName && !item.cachedTitleRegex) return false 42 | return true 43 | } 44 | export const Default: ToggleOrderItem = { 45 | hide: false, 46 | titleRegex: "", 47 | constructorName: "", 48 | friendlyName: "", 49 | gtypeName: "", 50 | } 51 | export function create(friendlyName: string): ToggleOrderItem { 52 | return { 53 | ...Default, 54 | friendlyName, 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/prefPages/about.ts: -------------------------------------------------------------------------------- 1 | import Adw from "gi://Adw" 2 | import GObject from "gi://GObject" 3 | import Gio from "gi://Gio" 4 | import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js" 5 | import type QstExtensionPreferences from "../prefs.js" 6 | import Config from "../config.js" 7 | import { 8 | Group, 9 | Row, 10 | ContributorsRow, 11 | LicenseRow, 12 | LogoGroup, 13 | DialogRow, 14 | ChangelogDialog, 15 | fixPageScrollIssue, 16 | SwitchRow, 17 | DropdownRow, 18 | } from "../libs/prefs/components.js" 19 | 20 | export const AboutPage = GObject.registerClass({ 21 | GTypeName: Config.baseGTypeName+"AboutPage", 22 | }, class AboutPage extends Adw.PreferencesPage { 23 | constructor(settings: Gio.Settings, prefs: QstExtensionPreferences, window: Adw.PreferencesWindow) { 24 | super({ 25 | name: "about", 26 | title: _("About"), 27 | iconName: "dialog-information-symbolic" 28 | }) 29 | fixPageScrollIssue(this) 30 | 31 | // Logo 32 | LogoGroup({ 33 | parent: this, 34 | name: prefs.metadata.name, 35 | icon: "qst-project-icon", 36 | version: prefs.getVersionString(), 37 | versionAction: () => ChangelogDialog({ 38 | window, 39 | content: async () => prefs.getChangelog(), 40 | currentBuildNumber: Config.buildNumber, 41 | defaultPageBuildNumber: Config.buildNumber, 42 | }) 43 | }) 44 | 45 | // About 46 | Group({ 47 | parent: this, 48 | title: _("About"), 49 | description: _("Common extension informations"), 50 | },[ 51 | Row({ 52 | title: _("Changelogs"), 53 | subtitle: _("View the change history for this extension"), 54 | action: ()=>ChangelogDialog({ 55 | window, 56 | title: _("Changelogs"), 57 | subtitle: _("View the change history for this extension"), 58 | content: async () => prefs.getChangelog(), 59 | currentBuildNumber: Config.buildNumber, 60 | }), 61 | icon: "object-rotate-right-symbolic", 62 | }), 63 | DialogRow({ 64 | window, 65 | title: _("License"), 66 | subtitle: _("License of codes"), 67 | dialogTitle: _("License"), 68 | minHeight: 520, 69 | icon: "document-open-recent-symbolic", 70 | childrenRequest: _page=>[ 71 | Group({ 72 | title: _("License"), 73 | description: _("License of codes") 74 | }, prefs.getLicenses().map(LicenseRow)), 75 | ], 76 | }), 77 | DialogRow({ 78 | window, 79 | title: _("Contributors"), 80 | subtitle: _("The creators of this extension"), 81 | dialogTitle: _("Contributors"), 82 | icon: "starred-symbolic", 83 | childrenRequest: _page=>[ 84 | Group({ 85 | title: _("Contributors"), 86 | description: _("The creators of this extension"), 87 | }, [ 88 | ...prefs.getContributorRows().map(ContributorsRow), 89 | Row({ 90 | title: _("More contributors"), 91 | subtitle: _("See more contributors on github"), 92 | uri: "https://github.com/qwreey/quick-settings-tweaks/graphs/contributors" 93 | }), 94 | ]) 95 | ] 96 | }) 97 | ]) 98 | 99 | // Links 100 | Group({ 101 | parent: this, 102 | title: _("Link"), 103 | description: _("External links about this extension") 104 | },[ 105 | Row({ 106 | uri: "https://github.com/sponsors/qwreey", 107 | title: _("Donate via github sponsors"), 108 | subtitle: _("Support development!"), 109 | icon: "emblem-favorite-symbolic", 110 | }), 111 | Row({ 112 | uri: "https://extensions.gnome.org/extension/5446/quick-settings-tweaker/", 113 | title: "Gnome Extension", 114 | subtitle: _("Rate and comment the extension!"), 115 | icon: "qst-gnome-extension-logo-symbolic", 116 | }), 117 | Row({ 118 | uri: "https://github.com/qwreey75/quick-settings-tweaks", 119 | title: _("Github Repository"), 120 | subtitle: _("Add Star on Repository is helping me a lot!\nPlease, if you found bug from this extension, you can make issue to make me know that!\nOr, you can create PR with wonderful features!"), 121 | icon: "qst-github-logo-symbolic", 122 | }), 123 | Row({ 124 | uri: "https://weblate.paring.moe/projects/gs-quick-settings-tweaks/", 125 | title: "Webslate", 126 | subtitle: _("Add translation to this extension!"), 127 | icon: "qst-weblate-logo-symbolic", 128 | }), 129 | ]) 130 | 131 | Group({ 132 | parent: this, 133 | title: _("Debug"), 134 | description: _("Extension debugging options"), 135 | }, [ 136 | SwitchRow({ 137 | settings, 138 | title: _("Expose environment"), 139 | subtitle: _("Expose extension environment to globalThis.qst"), 140 | bind: "debug-expose" 141 | }), 142 | SwitchRow({ 143 | settings, 144 | title: _("Show layout border"), 145 | subtitle: _("Show layout borders on Quick Settings"), 146 | bind: "debug-show-layout-border" 147 | }), 148 | DropdownRow({ 149 | settings, 150 | title: _("Log level"), 151 | bind: "debug-log-level", 152 | items: [ 153 | { name: _("none"), value: -1 }, 154 | { name: _("error"), value: 0 }, 155 | { name: _("info"), value: 1 }, 156 | { name: _("debug"), value: 2 }, 157 | ], 158 | }), 159 | ]) 160 | } 161 | }) 162 | -------------------------------------------------------------------------------- /src/prefPages/menu.ts: -------------------------------------------------------------------------------- 1 | import Adw from "gi://Adw" 2 | import GObject from "gi://GObject" 3 | import Gio from "gi://Gio" 4 | import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js" 5 | import Config from "../config.js" 6 | import type QstExtensionPreferences from "../prefs.js" 7 | import { 8 | SwitchRow, 9 | AdjustmentRow, 10 | DropdownRow, 11 | Group, 12 | DialogRow, 13 | fixPageScrollIssue, 14 | } from "../libs/prefs/components.js" 15 | 16 | // #region AdvancedAnimationStyleGroup 17 | function AdvancedAnimationStyleGroup(settings: Gio.Settings): Adw.PreferencesGroup { 18 | return Group({ 19 | title: _("Advanced animation style"), 20 | },[ 21 | AdjustmentRow({ 22 | settings, 23 | title: _("Open Duration"), 24 | subtitle: _("Open animation duration in microseconds"), 25 | sensitiveBind: "menu-animation-enabled", 26 | bind: "menu-animation-open-duration", 27 | max: 4000, 28 | }), 29 | AdjustmentRow({ 30 | settings, 31 | title: _("Close Duration"), 32 | subtitle: _("Close animation duration in microseconds"), 33 | sensitiveBind: "menu-animation-enabled", 34 | bind: "menu-animation-close-duration", 35 | max: 4000, 36 | }), 37 | AdjustmentRow({ 38 | settings, 39 | title: _("Grid Content Opacity"), 40 | subtitle: _("Adjust grid content opacity.\nSet this to 255 to make opaque, and 0 to make transparent"), 41 | sensitiveBind: "menu-animation-enabled", 42 | bind: "menu-animation-grid-content-opacity", 43 | max: 255, 44 | }), 45 | AdjustmentRow({ 46 | settings, 47 | title: _("Background Blur Radius"), 48 | subtitle: _("Adjust background blur radius.\nSet this to 0 to disable blur effect"), 49 | sensitiveBind: "menu-animation-enabled", 50 | bind: "menu-animation-background-blur-radius", 51 | max: 32, 52 | }), 53 | AdjustmentRow({ 54 | settings, 55 | title: _("Background Brightness"), 56 | subtitle: _("Adjust background brightness.\nSet this to 1000 to disable brightness control effect.\nNot impacts on gnome-shell's default dim effect."), 57 | sensitiveBind: "menu-animation-enabled", 58 | bind: "menu-animation-background-brightness", 59 | max: 2000, 60 | }), 61 | AdjustmentRow({ 62 | settings, 63 | title: _("Background Opacity"), 64 | subtitle: _("Adjust background opacity.\nSet this to 255 to make opaque, and 0 to make transparent"), 65 | sensitiveBind: "menu-animation-enabled", 66 | bind: "menu-animation-background-opacity", 67 | max: 255, 68 | }), 69 | AdjustmentRow({ 70 | settings, 71 | title: _("Background X Scale"), 72 | subtitle: _("Adjust background x scale, 1000 means 1.0 scale"), 73 | sensitiveBind: "menu-animation-enabled", 74 | bind: "menu-animation-background-scale-x", 75 | max: 4000, 76 | }), 77 | AdjustmentRow({ 78 | settings, 79 | title: _("Background Y Scale"), 80 | subtitle: _("Adjust background y scale, 1000 means 1.0 scale"), 81 | sensitiveBind: "menu-animation-enabled", 82 | bind: "menu-animation-background-scale-y", 83 | max: 4000, 84 | }), 85 | ]) 86 | } 87 | // #endregion AdvancedAnimationStyleGroup 88 | 89 | export const MenuPage = GObject.registerClass({ 90 | GTypeName: Config.baseGTypeName+"MenuPage", 91 | }, class MenuPage extends Adw.PreferencesPage { 92 | constructor(settings: Gio.Settings, _prefs: QstExtensionPreferences, window: Adw.PreferencesWindow) { 93 | super({ 94 | name: "Menu", 95 | title: _("Menu"), 96 | iconName: "user-available-symbolic", 97 | }) 98 | fixPageScrollIssue(this) 99 | 100 | // Overlay 101 | Group({ 102 | parent: this, 103 | title: _("Overlay Mode"), 104 | description: _("Display toggle, power, and sound menus as overlay"), 105 | headerSuffix: SwitchRow({ 106 | settings, 107 | bind: "overlay-menu-enabled", 108 | }), 109 | experimental: true, 110 | },[ 111 | AdjustmentRow({ 112 | settings, 113 | title: _("Overlay Width"), 114 | subtitle: _("Adjust overlay menu width\nSet this to 0 to disable adjusting"), 115 | sensitiveBind: "overlay-menu-enabled", 116 | bind: "overlay-menu-width", 117 | max: 2048, 118 | }), 119 | AdjustmentRow({ 120 | settings, 121 | title: _("Overlay Animation Duration"), 122 | subtitle: _("Custom menu open animation duration in microseconds\nSet this to 0 to disable custom animation"), 123 | sensitiveBind: "overlay-menu-enabled", 124 | bind: "overlay-menu-animate-duration", 125 | max: 4000, 126 | }), 127 | DropdownRow({ 128 | settings, 129 | title: _("Overlay Animation Style"), 130 | subtitle: _("Custom menu open animation style"), 131 | items: [ 132 | { "name": _("Flyout"), "value": "flyout" }, 133 | { "name": _("Dialog"), "value": "dialog" }, 134 | ], 135 | bind: "overlay-menu-animate-style", 136 | sensitiveBind: "overlay-menu-enabled" 137 | }) 138 | ]) 139 | 140 | // Animation 141 | Group({ 142 | parent: this, 143 | title: _("Animation"), 144 | description: _("Add menu animation on toggle menu opening and closing\nTo get the best feel, turn on overlay mode"), 145 | headerSuffix: SwitchRow({ 146 | settings, 147 | bind: "menu-animation-enabled", 148 | }), 149 | experimental: true, 150 | },[ 151 | DialogRow({ 152 | window, 153 | settings, 154 | title: _("Advanced animation style"), 155 | subtitle: _("Adjust speed, blur, scale, and opacity"), 156 | dialogTitle: _("Animation"), 157 | sensitiveBind: "menu-animation-enabled", 158 | childrenRequest: ()=>[AdvancedAnimationStyleGroup(settings)], 159 | }), 160 | ]) 161 | } 162 | }) 163 | -------------------------------------------------------------------------------- /src/prefPages/toggles.ts: -------------------------------------------------------------------------------- 1 | import Adw from "gi://Adw" 2 | import GObject from "gi://GObject" 3 | import Gio from "gi://Gio" 4 | import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js" 5 | import Config from "../config.js" 6 | import type QstExtensionPreferences from "../prefs.js" 7 | import { 8 | SwitchRow, 9 | Group, 10 | DropdownRow, 11 | fixPageScrollIssue, 12 | } from "../libs/prefs/components.js" 13 | 14 | export const TogglesPage = GObject.registerClass({ 15 | GTypeName: Config.baseGTypeName+"TogglesPage", 16 | }, class TogglesPage extends Adw.PreferencesPage { 17 | constructor(settings: Gio.Settings, _prefs: QstExtensionPreferences, window: Adw.PreferencesWindow) { 18 | super({ 19 | name: "Toggles", 20 | title: _("Toggles"), 21 | iconName: "view-grid-symbolic", 22 | }) 23 | fixPageScrollIssue(this) 24 | 25 | // DND Quick Toggle 26 | Group({ 27 | parent: this, 28 | title: _("DND Quick Toggle"), 29 | description: _("Turn on to add the DND quick toggle on the Quick Settings panel"), 30 | headerSuffix: SwitchRow({ 31 | settings, 32 | bind: "dnd-quick-toggle-enabled", 33 | }), 34 | }, [ 35 | DropdownRow({ 36 | settings, 37 | title: _("DND indicator position"), 38 | subtitle: _("Set DND indicator position"), 39 | bind: "dnd-quick-toggle-indicator-position", 40 | sensitiveBind: "dnd-quick-toggle-enabled", 41 | items: [ 42 | { name: _("System Tray"), value: "system-tray" }, 43 | { name: _("Date Menu Button"), value: "date-menu" }, 44 | ], 45 | }) 46 | ]) 47 | 48 | // Unsafe Mode Toggle 49 | Group({ 50 | parent: this, 51 | title: _("Unsafe Mode Quick Toggle"), 52 | description: _("Turn on to add the unsafe quick toggle on the Quick Settings panel"), 53 | headerSuffix: SwitchRow({ 54 | settings, 55 | bind: "unsafe-quick-toggle-enabled", 56 | }), 57 | }, [ 58 | SwitchRow({ 59 | settings, 60 | title: _("Save last session state"), 61 | subtitle: _("Turn on to save last session unsafe state"), 62 | bind: "unsafe-quick-toggle-save-last-state", 63 | sensitiveBind: "unsafe-quick-toggle-enabled", 64 | }), 65 | ]) 66 | } 67 | }) 68 | -------------------------------------------------------------------------------- /src/prefs.ts: -------------------------------------------------------------------------------- 1 | import Gtk from "gi://Gtk" 2 | import Gdk from "gi://Gdk" 3 | import Gio from "gi://Gio" 4 | import Adw from "gi://Adw" 5 | import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js" 6 | import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js" 7 | import { WidgetsPage } from "./prefPages/widgets.js" 8 | import { TogglesPage } from "./prefPages/toggles.js" 9 | import { LayoutPage } from "./prefPages/layout.js" 10 | import { AboutPage } from "./prefPages/about.js" 11 | import { MenuPage } from "./prefPages/menu.js" 12 | import { ContributorsRow, LicenseRow } from "./libs/prefs/components.js" 13 | import Config from "./config.js" 14 | 15 | var pageList = [ 16 | WidgetsPage, 17 | TogglesPage, 18 | LayoutPage, 19 | MenuPage, 20 | AboutPage, 21 | ] 22 | 23 | export default class QstExtensionPreferences extends ExtensionPreferences { 24 | appendIconPath(path: string) { 25 | const iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) 26 | if (!iconTheme.get_search_path().includes(path)) 27 | iconTheme.add_search_path(path) 28 | } 29 | 30 | readExtensionFile(path: string) { 31 | const decoder = new TextDecoder() 32 | const file = Gio.File.new_for_path(`${this.path}/${path}`) 33 | const content = file.load_contents(null)[1] 34 | return decoder.decode(content) 35 | } 36 | 37 | getContributorRows(): ContributorsRow.Contributor[][] { 38 | const contributors = JSON.parse( 39 | this.readExtensionFile("media/contributors/data.json") 40 | ) as ContributorsRow.Contributor[] 41 | if (!contributors.length) return [] 42 | const rows: ContributorsRow.Contributor[][] = [[]] 43 | contributors.reduce((currentRow: ContributorsRow.Contributor[], obj: ContributorsRow.Contributor)=>{ 44 | if (currentRow.length >= 4) rows.push(currentRow = []) 45 | currentRow.push(obj) 46 | return currentRow 47 | }, rows[0]) 48 | return rows 49 | } 50 | 51 | getLicenses(): LicenseRow.License[] { 52 | const licenses = JSON.parse( 53 | this.readExtensionFile("media/licenses.json") 54 | ) as LicenseRow.License[] 55 | for (const item of licenses) { 56 | if (item.file) { 57 | item.content = async () => this.readExtensionFile(item.file) 58 | } 59 | } 60 | return licenses 61 | } 62 | 63 | getVersionString(): string { 64 | let version = Config.version.toUpperCase().replace(/-.*?$/, "") 65 | if (this.metadata.version) { 66 | version += "." + this.metadata.version 67 | } 68 | version += " — " 69 | if (Config.isReleaseBuild) { 70 | version += _("Stable") 71 | } else if (Config.isDevelopmentBuild) { 72 | version += _("Development") 73 | } else { 74 | version += _("Preview") 75 | } 76 | if (Config.isGithubBuild) { 77 | version += " " + _("(Github Release)") 78 | } else if (!this.metadata.version) { 79 | version += " " + _("(Built from source)") 80 | } 81 | return version 82 | } 83 | 84 | getChangelog(): string { 85 | return this.readExtensionFile("media/Changelog.md") 86 | } 87 | 88 | async fillPreferencesWindow(window: Adw.PreferencesWindow) { 89 | let settings = this.getSettings() 90 | 91 | // Window options 92 | window.set_search_enabled(true) 93 | window.set_default_size(690, 680) 94 | 95 | // Register icon path 96 | this.appendIconPath(this.path + "/media") 97 | this.appendIconPath(this.path + "/media/contributors") 98 | 99 | for (const page of pageList) { 100 | window.add(new page(settings, this, window)) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/styles/date-menu.scss: -------------------------------------------------------------------------------- 1 | @use "../global.scss" as *; 2 | 3 | .datemenu-popover { 4 | &#{$p}-hide-left-box { 5 | .datemenu-calendar-column { 6 | margin: 0px; 7 | } 8 | } 9 | &#{$p}-hide-right-box { 10 | .message-list { 11 | margin: 0px; 12 | border: none; 13 | min-height: 340px; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/debug.scss: -------------------------------------------------------------------------------- 1 | @use "../global.scss" as *; 2 | 3 | #{$p}-debug-show-layout * { 4 | outline: 1px solid red; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/media-widget.scss: -------------------------------------------------------------------------------- 1 | @use "../global.scss" as *; 2 | 3 | #{$p}-media { 4 | padding: 0px; 5 | margin: 0px 0px 0px 0px; 6 | 7 | // Message Card 8 | .message { 9 | margin: 0px; 10 | } 11 | 12 | // Message Content (Texts) 13 | .message-content { 14 | width: 0px; // Min size, texts will be expended 15 | text-overflow: ellipsis; 16 | } 17 | 18 | // Header 19 | #{$p}-header { 20 | margin-bottom: 6px; 21 | 22 | // Header Label 23 | #{$p}-header-label { 24 | font-weight: bold; 25 | font-size: 0.98em; 26 | margin-left: 4px; 27 | } 28 | 29 | .page-indicators { 30 | margin-right: 4px; 31 | spacing: 6px; 32 | .page-indicator { 33 | height: 8px; 34 | width: 8px; 35 | padding: 0px; 36 | } 37 | } 38 | } 39 | 40 | // Progress Control (Position/Length Text and Slider) 41 | #{$p}-progress-control { 42 | spacing: 8px; 43 | padding: 0px 6px; 44 | 45 | #{$p}-position-label, #{$p}-length-label { 46 | font-size: 0.92em; 47 | } 48 | } 49 | 50 | .message-list-section-list { 51 | margin: 0px; 52 | padding: 0px; 53 | spacing: 0px; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/styles/message-compact.scss: -------------------------------------------------------------------------------- 1 | @use "../global.scss" as *; 2 | 3 | #{$p}-message-compact { 4 | // Message Card 5 | .message { 6 | padding: 5px 6px; 7 | &.media-message { 8 | padding: 3px 6px 8px 6px !important; // idk required for some reason 9 | .message-media-control { 10 | /* control button sizing */ 11 | padding-left: 5px; 12 | padding-right: 5px; 13 | } 14 | } 15 | 16 | // Message Content Box (Outside) 17 | .message-box { 18 | padding-top: 1px; // Header & Content spacing 19 | 20 | // Message Content (Texts) 21 | .message-content { 22 | spacing: 3px; // Title & Content spacing 23 | font-size: 0.89em; 24 | } 25 | } 26 | 27 | // Message Header (App, Button) 28 | .message-header { 29 | spacing: 3px; // close / expand button spacing 30 | 31 | // Buttons 32 | .message-expand-button, 33 | .message-close-button { 34 | padding: 2px !important; 35 | } 36 | 37 | // Title 38 | .message-header-content { 39 | padding-left: 6px; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/styles/message-remove-shadow.scss: -------------------------------------------------------------------------------- 1 | @use "../global.scss" as *; 2 | 3 | #{$p}-message-remove-shadow .message { 4 | box-shadow: none !important; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/notification-widget.scss: -------------------------------------------------------------------------------- 1 | @use "../global.scss" as *; 2 | 3 | #{$p}-notifications { 4 | padding: 0px; 5 | margin: 0px 0px 0px 0px; 6 | 7 | // Message Card 8 | .message { 9 | margin: 0px; 10 | } 11 | 12 | // Message Content (Texts) 13 | .message-content { 14 | width: 0px; // Min size, texts will be expended 15 | text-overflow: ellipsis; 16 | } 17 | 18 | // Scrolling list margin/padding/spacing 19 | .message-list-section-list { 20 | margin: 0px; 21 | padding: 0px; 22 | spacing: 8px; // Message spacing 23 | } 24 | 25 | // Avoid Scrollbar 26 | .message-list-sections { 27 | margin: 0px; 28 | &#{$p}-has-scrollbar { 29 | padding: 0px 5px 0px 0px; 30 | } 31 | } 32 | 33 | // Header 34 | #{$p}-header { 35 | margin-bottom: 6px; 36 | 37 | // Header Label 38 | #{$p}-header-label { 39 | font-weight: bold; 40 | font-size: 0.98em; 41 | margin-left: 4px; 42 | } 43 | 44 | // Clear button 45 | #{$p}-clear-button { 46 | font-weight: bold; 47 | font-size: 0.98em; 48 | padding: 1px 4px; 49 | border-radius: 8px; 50 | margin-right: 4px; 51 | &:hover { 52 | background-color: rgba(127, 127, 127, .2); 53 | } 54 | #{$p}-icon { 55 | margin-right: 4px; 56 | } 57 | } 58 | } 59 | &#{$p}-use-native-controls #{$p}-header { 60 | margin-bottom: 8px; 61 | } 62 | 63 | // No notification placeholder 64 | #{$p}-placeholder { 65 | width: 100%; 66 | font-weight: bold; 67 | margin: 10px 0px; 68 | #{$p}-icon { 69 | margin-bottom: 12px; 70 | } 71 | } 72 | 73 | // Native controls 74 | #{$p}-native-controls { 75 | margin-top: 5px; 76 | padding: 0px 4px; 77 | #{$p}-native-dnd-switch { 78 | margin: 0px 0px 0px 6px; 79 | height: 24px; // 26px; 80 | width: 40px; 81 | .handle { 82 | margin: 2px; 83 | width: 20px; 84 | height: 20px; 85 | } 86 | } 87 | #{$p}-native-dnd-text { 88 | font-size: 0.92em; 89 | margin-right: 4px; 90 | margin-bottom: 0px; 91 | } 92 | #{$p}-native-clear-button { 93 | font-size: 0.92em; 94 | padding: 4px 4px; 95 | } 96 | #{$p}-native-control-box { 97 | padding: 8px 4px 0px 4px; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/styles/system-indicator.scss: -------------------------------------------------------------------------------- 1 | @use "../global.scss" as *; 2 | 3 | #panel { 4 | #{$p}-privacy-indicator-use-accent .privacy-indicator { 5 | color: -st-accent-color; 6 | }#{$p}-privacy-indicator-use-monochrome .privacy-indicator { 7 | color: -st-accent-fg-color; 8 | } 9 | #{$p}-screen-sharing-indicator-use-accent, 10 | #{$p}-screen-recording-indicator-use-accent { 11 | box-shadow: inset 0 0 0 100px -st-accent-color; 12 | &:hover, &:focus { 13 | box-shadow: inset 0 0 0 100px st-mix(-st-accent-color, -st-accent-fg-color, 75%); 14 | } 15 | &:active { 16 | box-shadow: inset 0 0 0 100px st-mix(-st-accent-color, -st-accent-fg-color, 60%); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/volume-mixer-widget.scss: -------------------------------------------------------------------------------- 1 | @use "../global.scss" as *; 2 | 3 | /* volume mixer label */ 4 | #{$p}-volume-mixer { 5 | #{$p}-item { 6 | #{$p}-label { 7 | padding-left: 6px; 8 | font-size: 0.92em; 9 | text-overflow: ellipsis; 10 | width: 0px; 11 | } 12 | spacing: 4px; 13 | } 14 | spacing: 6px; 15 | #{$p}-has-scrollbar { 16 | padding: 0px 5px 0px 0px; 17 | } 18 | } 19 | .quick-toggle-menu-container { 20 | #{$p}-volume-mixer { 21 | #{$p}-item { 22 | #{$p}-label { 23 | font-size: 1.02em; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/styles/weather-widget.scss: -------------------------------------------------------------------------------- 1 | @use "../global.scss" as *; 2 | 3 | #{$p}-weather { 4 | &#{$p}-weather-remove-shadow { 5 | .weather-button { 6 | box-shadow: none !important; 7 | } 8 | } 9 | &#{$p}-weather-compact { 10 | .weather-button { 11 | padding: 8px 0px 10px 0px; 12 | .weather-forecast-time { 13 | padding-top: 4px; 14 | padding-bottom: 2px; 15 | } 16 | .weather-forecast-icon { 17 | margin-bottom: 2px; 18 | } 19 | } 20 | } 21 | .weather-button { 22 | margin: 0px; 23 | #{$p}-status-label { 24 | margin: 2px 10px 2px 10px; 25 | } 26 | } 27 | 28 | // Header 29 | #{$p}-header { 30 | margin-bottom: 6px; 31 | 32 | // Header Label 33 | #{$p}-header-label { 34 | font-weight: bold; 35 | font-size: 0.98em; 36 | margin-left: 4px; 37 | } 38 | 39 | // Location Label 40 | #{$p}-location-label { 41 | font-weight: bold; 42 | font-size: 0.98em; 43 | margin-right: 6px; 44 | margin-top: 2px; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/stylesheet.scss: -------------------------------------------------------------------------------- 1 | @use "./global.scss" as *; 2 | 3 | @use "./styles/notification-widget.scss"; 4 | @use "./styles/media-widget.scss"; 5 | @use "./styles/weather-widget.scss"; 6 | @use "./styles/message-compact.scss"; 7 | @use "./styles/message-remove-shadow.scss"; 8 | @use "./styles/volume-mixer-widget.scss"; 9 | @use "./styles/date-menu.scss"; 10 | @use "./styles/system-indicator.scss"; 11 | @use "./styles/debug.scss"; 12 | 13 | /* input output labels */ 14 | #{$p}-input-output-label { 15 | width: auto; 16 | } 17 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import "@girs/gnome-shell/ambient" 2 | import "@girs/gnome-shell/extensions/global" 3 | import "@girs/gjs/dom" 4 | import "@girs/gjs" 5 | import "./ambient" 6 | 7 | // Shell environment 8 | // import Clutter from "@girs/clutter-15/clutter-15" 9 | // import GObject from "gi://GObject" 10 | // import Atk from "gi://Atk" 11 | declare module "@girs/clutter-15/clutter-15" { 12 | namespace Clutter { 13 | interface Actor { 14 | ease(params: EasingParamsWithProps): void 15 | ease_property(propName: string, target: any, params: EasingParams) 16 | } 17 | } 18 | } 19 | declare module "@girs/gobject-2.0/gobject-2.0" { 20 | import SignalTracker from "resource:///org/gnome/shell/misc/signals.js" 21 | namespace GObject { 22 | interface Object { 23 | connectObject: SignalTracker.EventEmitter["connectObject"] 24 | connect_object: SignalTracker.EventEmitter["connectObject"] 25 | disconnectObject: SignalTracker.EventEmitter["disconnectObject"] 26 | disconnect_object: SignalTracker.EventEmitter["disconnectObject"] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "Bundler", 4 | "outDir": "target/tsc", 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "removeComments": false, 8 | "skipLibCheck": true, 9 | "lib": ["ESNext"], 10 | "allowArbitraryExtensions": true, 11 | "incremental": true, 12 | "tsBuildInfoFile": "target/.tsbuildinfo", 13 | "useDefineForClassFields": false 14 | }, 15 | "include": ["./src/**/*.ts", "src/types.d.ts", "src/contributors.js"] 16 | } 17 | --------------------------------------------------------------------------------