├── .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 [
](https://extensions.gnome.org/extension/5446/quick-settings-tweaker/)
2 |
3 |
4 |
5 | ### Let's tweak Gnome Quick Settings!
6 |
7 | [

](https://extensions.gnome.org/extension/5446/quick-settings-tweaker/)
8 |
9 |
10 |
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.
|
|
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.
|
|
26 | | **Add the Notifications Widget**
You can check what has been sent to your mailbox or messenger, without missing!
|
|
27 | | **Layout customize**
Hide, re-order, re-color your panel and Quick Settings layout
Make it simple and keep organized!
|
|
28 | | **Overlay menu layout**
Your Quick Settings panel is too big?
Try overlay layout! you can customize background and animation style too.
|
|
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 | [](https://github.com/sponsors/qwreey)
37 |
38 | ## Stars
39 |
40 | [](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 | 
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 |
5 |
--------------------------------------------------------------------------------
/media/hicolor/scalable/actions/qst-gnome-extension-logo-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/media/hicolor/scalable/actions/qst-weblate-logo-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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 |
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 |
--------------------------------------------------------------------------------