Preferences: Improve the appearances tab with live preview, color scheme and accent color settings

This commit is contained in:
Romain Vigier
2026-08-02 00:25:06 +02:00
parent 955fdef40d
commit 39faff700a
19 changed files with 878 additions and 135 deletions
+74 -1
View File
@@ -10,11 +10,14 @@
"@girs/adw-1": "^4.1.0",
"@girs/gdesktopenums-3.0": "^4.1.0",
"@girs/gdk-4.0": "^4.1.0",
"@girs/gdkpixbuf-2.0": "^4.1.0",
"@girs/geoclue-2.0": "^4.1.0",
"@girs/gio-2.0": "^4.1.0",
"@girs/glib-2.0": "^4.1.0",
"@girs/gly-2": "^4.1.0",
"@girs/glygtk4-2": "^4.1.0",
"@girs/gobject-2.0": "^4.1.0",
"@girs/graphene-1.0": "^4.1.0",
"@girs/gsk-4.0": "^4.1.0",
"@girs/gtk-4.0": "^4.1.0",
"@girs/meta-18": "^4.1.0",
"@girs/shell-18": "^4.1.0",
@@ -444,6 +447,42 @@
"@girs/gobject-2.0": "^4.1.0"
}
},
"node_modules/@girs/gly-2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@girs/gly-2/-/gly-2-4.1.0.tgz",
"integrity": "sha512-blF/PuZpQp5NhYYkIJOu9Xq+vEX3vxq2bzbjrNS3XodSh8gjX6wxX8Gxp9808YK7b2/6+MYVt3fVeZQ34ybHuw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@girs/gio-2.0": "^4.1.0",
"@girs/gjs": "^4.1.0",
"@girs/glib-2.0": "^4.1.0",
"@girs/gmodule-2.0": "^4.1.0",
"@girs/gobject-2.0": "^4.1.0"
}
},
"node_modules/@girs/glygtk4-2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@girs/glygtk4-2/-/glygtk4-2-4.1.0.tgz",
"integrity": "sha512-gD1kjxo1cnkq/rXKDVOY1gJfaxvgqtgl2jn5q56asN+9yk0Cp3jZyDPlPcqrC1Gdv8deD8j81P7nybo4jdJZwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@girs/cairo-1.0": "^4.1.0",
"@girs/freetype2-2.0": "^4.1.0",
"@girs/gdk-4.0": "^4.1.0",
"@girs/gdkpixbuf-2.0": "^4.1.0",
"@girs/gio-2.0": "^4.1.0",
"@girs/gjs": "^4.1.0",
"@girs/glib-2.0": "^4.1.0",
"@girs/gly-2": "^4.1.0",
"@girs/gmodule-2.0": "^4.1.0",
"@girs/gobject-2.0": "^4.1.0",
"@girs/harfbuzz-0.0": "^4.1.0",
"@girs/pango-1.0": "^4.1.0",
"@girs/pangocairo-1.0": "^4.1.0"
}
},
"node_modules/@girs/gmodule-2.0": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@girs/gmodule-2.0/-/gmodule-2.0-4.1.0.tgz",
@@ -2116,6 +2155,40 @@
"@girs/gobject-2.0": "^4.1.0"
}
},
"@girs/gly-2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@girs/gly-2/-/gly-2-4.1.0.tgz",
"integrity": "sha512-blF/PuZpQp5NhYYkIJOu9Xq+vEX3vxq2bzbjrNS3XodSh8gjX6wxX8Gxp9808YK7b2/6+MYVt3fVeZQ34ybHuw==",
"dev": true,
"requires": {
"@girs/gio-2.0": "^4.1.0",
"@girs/gjs": "^4.1.0",
"@girs/glib-2.0": "^4.1.0",
"@girs/gmodule-2.0": "^4.1.0",
"@girs/gobject-2.0": "^4.1.0"
}
},
"@girs/glygtk4-2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@girs/glygtk4-2/-/glygtk4-2-4.1.0.tgz",
"integrity": "sha512-gD1kjxo1cnkq/rXKDVOY1gJfaxvgqtgl2jn5q56asN+9yk0Cp3jZyDPlPcqrC1Gdv8deD8j81P7nybo4jdJZwg==",
"dev": true,
"requires": {
"@girs/cairo-1.0": "^4.1.0",
"@girs/freetype2-2.0": "^4.1.0",
"@girs/gdk-4.0": "^4.1.0",
"@girs/gdkpixbuf-2.0": "^4.1.0",
"@girs/gio-2.0": "^4.1.0",
"@girs/gjs": "^4.1.0",
"@girs/glib-2.0": "^4.1.0",
"@girs/gly-2": "^4.1.0",
"@girs/gmodule-2.0": "^4.1.0",
"@girs/gobject-2.0": "^4.1.0",
"@girs/harfbuzz-0.0": "^4.1.0",
"@girs/pango-1.0": "^4.1.0",
"@girs/pangocairo-1.0": "^4.1.0"
}
},
"@girs/gmodule-2.0": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@girs/gmodule-2.0/-/gmodule-2.0-4.1.0.tgz",
+4 -1
View File
@@ -14,11 +14,14 @@
"@girs/adw-1": "^4.1.0",
"@girs/gdesktopenums-3.0": "^4.1.0",
"@girs/gdk-4.0": "^4.1.0",
"@girs/gdkpixbuf-2.0": "^4.1.0",
"@girs/geoclue-2.0": "^4.1.0",
"@girs/gio-2.0": "^4.1.0",
"@girs/glib-2.0": "^4.1.0",
"@girs/gly-2": "^4.1.0",
"@girs/glygtk4-2": "^4.1.0",
"@girs/gobject-2.0": "^4.1.0",
"@girs/graphene-1.0": "^4.1.0",
"@girs/gsk-4.0": "^4.1.0",
"@girs/gtk-4.0": "^4.1.0",
"@girs/meta-18": "^4.1.0",
"@girs/shell-18": "^4.1.0",
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SPDX-FileCopyrightText: Night Theme Switcher Contributors
SPDX-License-Identifier: GPL-3.0-or-later
-->
<svg width="160" height="120" version="1.1" viewBox="0 0 160 120" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="filter12" x="-.0586" y="-.082" width="1.12" height="1.18" color-interpolation-filters="sRGB">
<feFlood flood-color="rgb(0,0,0)" flood-opacity=".2" in="SourceGraphic" result="flood"/>
<feGaussianBlur in="SourceGraphic" result="blur" stdDeviation="3.000000"/>
<feOffset dx="0.000000" dy="2.000000" in="blur" result="offset"/>
<feComposite in="flood" in2="offset" operator="in" result="comp1"/>
<feComposite in="SourceGraphic" in2="comp1" result="comp2"/>
</filter>
</defs>
<rect x="10" y="10" width="140" height="100" rx="6" ry="6" fill="#222226" filter="url(#filter12)" stroke="#000006" stroke-opacity=".12" stroke-width="2" style="paint-order:markers stroke fill"/>
<rect x="10" y="10" width="140" height="20" rx="6" ry="6" fill="#2e2e32"/>
<rect x="10" y="20" width="140" height="10" fill="#2e2e32"/>
<rect x="10" y="30" width="140" height="1" fill="#000006" fill-opacity=".12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SPDX-FileCopyrightText: Night Theme Switcher Contributors
SPDX-License-Identifier: GPL-3.0-or-later
-->
<svg width="160" height="120" version="1.1" viewBox="0 0 160 120" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="filter12" x="-.0586" y="-.082" width="1.12" height="1.18" color-interpolation-filters="sRGB">
<feFlood flood-color="rgb(0,0,0)" flood-opacity=".2" in="SourceGraphic" result="flood"/>
<feGaussianBlur in="SourceGraphic" result="blur" stdDeviation="3.000000"/>
<feOffset dx="0.000000" dy="2.000000" in="blur" result="offset"/>
<feComposite in="flood" in2="offset" operator="in" result="comp1"/>
<feComposite in="SourceGraphic" in2="comp1" result="comp2"/>
</filter>
</defs>
<rect x="10" y="10" width="140" height="100" rx="6" ry="6" fill="#fafafb" filter="url(#filter12)" stroke="#000006" stroke-opacity=".12" stroke-width="2" style="paint-order:markers stroke fill"/>
<rect x="10" y="10" width="140" height="20" rx="6" ry="6" fill="#fff"/>
<rect x="10" y="20" width="140" height="10" fill="#fff"/>
<rect x="10" y="30" width="140" height="1" fill="#000006" fill-opacity=".12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -4,6 +4,18 @@ SPDX-FileCopyrightText: Night Theme Switcher Contributors
SPDX-License-Identifier: GPL-3.0-or-later
-->
<schemalist gettext-domain="nightthemeswitcher@romainvigier.fr">
<enum id="org.gnome.shell.extensions.nightthemeswitcher.accent-color-enum">
<value nick="unset" value="0"/>
<value nick="blue" value="1"/>
<value nick="green" value="2"/>
<value nick="orange" value="3"/>
<value nick="red" value="4"/>
<value nick="pink" value="5"/>
<value nick="purple" value="6"/>
<value nick="slate" value="7"/>
<value nick="teal" value="8"/>
<value nick="yellow" value="9"/>
</enum>
<enum id="org.gnome.shell.extensions.nightthemeswitcher.color-scheme-enum">
<value nick="default" value="0"/>
<value nick="prefer-dark" value="1"/>
@@ -59,4 +71,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
<default>"prefer-dark"</default>
</key>
</schema>
<schema id="org.gnome.shell.extensions.nightthemeswitcher.accent-color" path="/org/gnome/shell/extensions/nightthemeswitcher/accent-color/">
<key name="day" enum="org.gnome.shell.extensions.nightthemeswitcher.accent-color-enum">
<default>"unset"</default>
</key>
<key name="night" enum="org.gnome.shell.extensions.nightthemeswitcher.accent-color-enum">
<default>"unset"</default>
</key>
</schema>
</schemalist>
+4
View File
@@ -5,6 +5,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
-->
<gresources>
<gresource prefix="/org/gnome/Shell/Extensions/nightthemeswitcher/preferences">
<file compressed="true" preprocess="xml-stripblanks">assets/preview-window-dark.svg</file>
<file compressed="true" preprocess="xml-stripblanks">assets/preview-window-light.svg</file>
<file compressed="true" preprocess="xml-stripblanks" alias="icons/scalable/actions/nightthemeswitcher-code-symbolic.svg">icons/code-symbolic.svg</file>
<file compressed="true" preprocess="xml-stripblanks" alias="icons/scalable/actions/nightthemeswitcher-translate-symbolic.svg">icons/translate-symbolic.svg</file>
<file compressed="true" preprocess="xml-stripblanks" alias="icons/scalable/apps/nightthemeswitcher-symbolic.svg">icons/nightthemeswitcher-symbolic.svg</file>
@@ -12,6 +14,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<file compressed="true" preprocess="xml-stripblanks" alias="icons/scalable/categories/nightthemeswitcher-commands-symbolic.svg">icons/commands-symbolic.svg</file>
<file compressed="true" preprocess="xml-stripblanks" alias="icons/scalable/categories/nightthemeswitcher-contribute-symbolic.svg">icons/contribute-symbolic.svg</file>
<file compressed="true" preprocess="xml-stripblanks" alias="icons/scalable/categories/nightthemeswitcher-schedule-symbolic.svg">icons/schedule-symbolic.svg</file>
<file compressed="true" preprocess="xml-stripblanks">ui/AppearanceChooser.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/AppearancePage.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/BackgroundButton.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/CommandsPage.ui</file>
@@ -19,5 +22,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<file compressed="true" preprocess="xml-stripblanks">ui/SchedulePage.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/ShortcutButton.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/TimeChooser.ui</file>
<file compressed="true">style.css</file>
</gresource>
</gresources>
+59
View File
@@ -0,0 +1,59 @@
/*
SPDX-FileCopyrightText: Night Theme Switcher Contributors
SPDX-License-Identifier: GPL-3.0-or-later
*/
color-chooser button {
min-width: 22px;
min-height: 22px;
margin: 3px;
padding: 0;
border-radius: 50%;
background-color: var(--accent-color);
outline: 3px solid transparent;
outline-offset: 3px;
}
color-chooser button:focus {
outline-color: alpha(var(--accent-color), 0.5);
}
color-chooser button:checked {
outline-color: var(--accent-color);
}
color-chooser button.blue {
--accent-color: var(--accent-blue);
}
color-chooser button.green {
--accent-color: var(--accent-green);
}
color-chooser button.orange {
--accent-color: var(--accent-orange);
}
color-chooser button.red {
--accent-color: var(--accent-red);
}
color-chooser button.pink {
--accent-color: var(--accent-pink);
}
color-chooser button.purple {
--accent-color: var(--accent-purple);
}
color-chooser button.slate {
--accent-color: var(--accent-slate);
}
color-chooser button.teal {
--accent-color: var(--accent-teal);
}
color-chooser button.yellow {
--accent-color: var(--accent-yellow);
}
+91
View File
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SPDX-FileCopyrightText: Night Theme Switcher Contributors
SPDX-License-Identifier: GPL-3.0-or-later
-->
<interface domain="nightthemeswitcher@romainvigier.fr">
<template class="AppearanceChooser" parent="GtkWidget">
<property name="layout-manager">
<object class="GtkBoxLayout">
<property name="orientation">vertical</property>
<property name="spacing">24</property>
</object>
</property>
<child>
<object class="AppearancePreview">
<binding name="accent-color">
<lookup name="accent-color" type="AppearanceChooser"/>
</binding>
<binding name="background-uri">
<lookup name="background-uri" type="AppearanceChooser"/>
</binding>
<binding name="color-scheme">
<lookup name="color-scheme" type="AppearanceChooser"/>
</binding>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Style</property>
<child>
<object class="AdwToggleGroup">
<style>
<class name="flat"/>
<class name="round"/>
</style>
<property name="valign">center</property>
<property name="active-name" bind-source="AppearanceChooser" bind-property="color-scheme" bind-flags="bidirectional"/>
<child>
<object class="AdwToggle">
<property name="name">prefer-light</property>
<property name="label" translatable="yes">Light</property>
</object>
</child>
<child>
<object class="AdwToggle">
<property name="name">default</property>
<property name="label" translatable="yes">Default</property>
</object>
</child>
<child>
<object class="AdwToggle">
<property name="name">prefer-dark</property>
<property name="label" translatable="yes">Dark</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Accent color</property>
<child>
<object class="ColorChooser">
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<property name="active-color" bind-source="AppearanceChooser" bind-property="accent-color" bind-flags="bidirectional"/>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Background</property>
<property name="activatable-widget">background_button</property>
<child>
<object class="BackgroundButton" id="background_button">
<property name="valign">center</property>
<property name="uri" bind-source="AppearanceChooser" bind-property="background-uri" bind-flags="bidirectional"/>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>
+33 -18
View File
@@ -10,29 +10,44 @@ SPDX-License-Identifier: GPL-3.0-or-later
<property name="icon-name">nightthemeswitcher-appearance-symbolic</property>
<child>
<object class="AdwPreferencesGroup">
<property name="description" translatable="yes">Background switching is handled by the Shell, these settings allow you to change the images it uses.</property>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Day background</property>
<property name="activatable-widget">day_button</property>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">24</property>
<child>
<object class="BackgroundButton" id="day_button">
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="valign">center</property>
<object class="AdwInlineViewSwitcher">
<property name="stack">stack</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Night background</property>
<property name="activatable-widget">night_button</property>
<child>
<object class="BackgroundButton" id="night_button">
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="valign">center</property>
<object class="AdwViewStack" id="stack">
<property name="enable-transitions">True</property>
<child>
<object class="AdwViewStackPage">
<property name="name">day</property>
<property name="title" translatable="yes">Day</property>
<property name="child">
<object class="AppearanceChooser" id="day_appearance_chooser"/>
</property>
</object>
</child>
<child>
<object class="AdwViewStackPage">
<property name="name">night</property>
<property name="title" translatable="yes">Night</property>
<property name="child">
<object class="AppearanceChooser" id="night_appearance_chooser"/>
</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel">
<style>
<class name="dimmed"/>
</style>
<property name="label" translatable="yes">Settings are applied when the extension is enabled.</property>
</object>
</child>
</object>
+11 -7
View File
@@ -5,18 +5,22 @@ SPDX-License-Identifier: GPL-3.0-or-later
-->
<interface domain="nightthemeswitcher@romainvigier.fr">
<template class="BackgroundButton" parent="GtkButton">
<property name="tooltip-text" translatable="yes">Change background</property>
<signal name="clicked" handler="onClicked" swapped="no"/>
<style>
<class name="image-button"/>
</style>
<child>
<object class="GtkPicture" id="thumbnail">
<property name="can-shrink">False</property>
<property name="width-request" bind-source="BackgroundButton" bind-property="thumb-width" bind-flags="sync-create"/>
<property name="height-request" bind-source="BackgroundButton" bind-property="thumb-height" bind-flags="sync-create"/>
<property name="tooltip-text" translatable="yes">Change background</property>
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">document-open-symbolic</property>
<property name="can-shrink">True</property>
<binding name="label">
<closure type="gchararray" function="_getBackgroundFileName">
<lookup name="uri" type="BackgroundButton"/>
</closure>
</binding>
</object>
</child>
</property>
</template>
<object class="GtkFileDialog" id="file_dialog">
<property name="modal">True</property>
+19
View File
@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: Night Theme Switcher Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
/**
* Accent colors.
* @readonly
* @enum {string}
*/
export const AccentColor = {
BLUE: "blue",
GREEN: "green",
ORANGE: "orange",
RED: "red",
PINK: "pink",
PURPLE: "purple",
SLATE: "slate",
TEAL: "teal",
YELLOW: "yellow",
};
+1 -1
View File
@@ -9,5 +9,5 @@
export const ColorScheme = {
DEFAULT: "default",
PREFER_DARK: "prefer-dark",
PREFER_LIGHT: "prefer-night",
PREFER_LIGHT: "prefer-light",
};
+4
View File
@@ -19,6 +19,7 @@ main = [
'prefs.js',
]
enums = [
'enums/AccentColor.js',
'enums/ColorScheme.js',
'enums/Time.js',
]
@@ -33,8 +34,11 @@ modules = [
'modules/Timer.js',
]
preferences = [
'preferences/AppearanceChooser.js',
'preferences/AppearancePage.js',
'preferences/AppearancePreview.js',
'preferences/BackgroundButton.js',
'preferences/ColorChooser.js',
'preferences/CommandsPage.js',
'preferences/ContributePage.js',
'preferences/SchedulePage.js',
+40
View File
@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: Night Theme Switcher Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
import GObject from "gi://GObject";
import Gtk from "gi://Gtk";
export class AppearanceChooser extends Gtk.Widget {
static {
GObject.registerClass(
{
GTypeName: "AppearanceChooser",
Template: "resource:///org/gnome/Shell/Extensions/nightthemeswitcher/preferences/ui/AppearanceChooser.ui",
Properties: {
"accent-color": GObject.ParamSpec.string(
"accent-color",
"Accent color",
"Accent color",
GObject.ParamFlags.READWRITE,
null,
),
"background-uri": GObject.ParamSpec.string(
"background-uri",
"Background URI",
"URI to the background file",
GObject.ParamFlags.READWRITE,
null,
),
"color-scheme": GObject.ParamSpec.string(
"color-scheme",
"Color scheme",
"Color scheme",
GObject.ParamFlags.READWRITE,
null,
),
},
},
this,
);
}
}
+27 -5
View File
@@ -11,17 +11,39 @@ export class AppearancePage extends Adw.PreferencesPage {
{
GTypeName: "AppearancePage",
Template: "resource:///org/gnome/Shell/Extensions/nightthemeswitcher/preferences/ui/AppearancePage.ui",
InternalChildren: ["day_button", "night_button"],
InternalChildren: ["day_appearance_chooser", "night_appearance_chooser"],
},
this,
);
}
constructor({ ...params } = {}) {
constructor({ accentColorSettings, colorSchemeSettings, ...params } = {}) {
super(params);
const settings = new Gio.Settings({ schema: "org.gnome.desktop.background" });
const backgroundSettings = new Gio.Settings({ schema: "org.gnome.desktop.background" });
const interfaceSettings = new Gio.Settings({ schema: "org.gnome.desktop.interface" });
settings.bind("picture-uri", this._day_button, "uri", Gio.SettingsBindFlags.DEFAULT);
settings.bind("picture-uri-dark", this._night_button, "uri", Gio.SettingsBindFlags.DEFAULT);
if (accentColorSettings.get_string("day") === "unset")
accentColorSettings.set_string("day", interfaceSettings.get_string("accent-color"));
if (accentColorSettings.get_string("night") === "unset")
accentColorSettings.set_string("night", interfaceSettings.get_string("accent-color"));
accentColorSettings.bind("day", this._day_appearance_chooser, "accent-color", Gio.SettingsBindFlags.DEFAULT);
accentColorSettings.bind("night", this._night_appearance_chooser, "accent-color", Gio.SettingsBindFlags.DEFAULT);
colorSchemeSettings.bind("day", this._day_appearance_chooser, "color-scheme", Gio.SettingsBindFlags.DEFAULT);
colorSchemeSettings.bind("night", this._night_appearance_chooser, "color-scheme", Gio.SettingsBindFlags.DEFAULT);
backgroundSettings.bind(
"picture-uri",
this._day_appearance_chooser,
"background-uri",
Gio.SettingsBindFlags.DEFAULT,
);
backgroundSettings.bind(
"picture-uri-dark",
this._night_appearance_chooser,
"background-uri",
Gio.SettingsBindFlags.DEFAULT,
);
}
}
+333
View File
@@ -0,0 +1,333 @@
// SPDX-FileCopyrightText: Night Theme Switcher Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
import Gdk from "gi://Gdk";
import Gio from "gi://Gio";
import Gly from "gi://Gly";
import GlyGtk4 from "gi://GlyGtk4";
import GObject from "gi://GObject";
import Graphene from "gi://Graphene";
import Gsk from "gi://Gsk";
import Gtk from "gi://Gtk";
import { AccentColor } from "../enums/AccentColor.js";
import { ColorScheme } from "../enums/ColorScheme.js";
Gio._promisify(Gly.Loader.prototype, "load_async", "load_finish");
const PREVIEW_HEIGHT = 200;
const PANEL_HEIGHT = 15;
const WINDOW_WIDTH = 160;
const WINDOW_HEIGHT = 120;
const BUTTON_WIDTH = 60;
const BUTTON_HEIGHT = 20;
export class AppearancePreview extends Gtk.Widget {
/** @type {?Gdk.Texture} */
#backgroundTexture = null;
/** @type {Gdk.Texture} */
#windowDarkTexture;
/** @type {Gdk.Texture} */
#windowLightTexture;
/** @type {?AccentColor} */
#accentColor = null;
/** @type {?string} */
#backgroundUri = null;
/** @type {?ColorScheme} */
#colorScheme = null;
static {
GObject.registerClass(
{
GTypeName: "AppearancePreview",
CssName: "appearance-preview",
Properties: {
"accent-color": GObject.ParamSpec.string(
"accent-color",
"Accent color",
"Accent color",
GObject.ParamFlags.READWRITE,
null,
),
"background-uri": GObject.ParamSpec.string(
"background-uri",
"Background URI",
"URI to the background file",
GObject.ParamFlags.READWRITE,
null,
),
"color-scheme": GObject.ParamSpec.string(
"color-scheme",
"Color scheme",
"Color scheme",
GObject.ParamFlags.READWRITE,
null,
),
},
},
this,
);
}
constructor({ ...params } = {}) {
super(params);
this.#windowDarkTexture = Gdk.Texture.new_from_resource(
"/org/gnome/Shell/Extensions/nightthemeswitcher/preferences/assets/preview-window-dark.svg",
);
this.#windowLightTexture = Gdk.Texture.new_from_resource(
"/org/gnome/Shell/Extensions/nightthemeswitcher/preferences/assets/preview-window-light.svg",
);
}
get accentColor() {
return this.#accentColor;
}
set accentColor(color) {
if (!color || color === this.#accentColor || !Object.values(AccentColor).includes(color)) return;
this.#accentColor = color;
this.notify("accent-color");
this.queue_draw();
}
get backgroundUri() {
return this.#backgroundUri;
}
set backgroundUri(uri) {
if (!uri || uri === this.#backgroundUri) return;
this.#backgroundUri = uri;
this.notify("background-uri");
this.#updateBackgroundTexture();
}
get colorScheme() {
return this.#colorScheme;
}
set colorScheme(scheme) {
if (!scheme || scheme === this.#colorScheme || !Object.values(ColorScheme).includes(scheme)) return;
this.#colorScheme = scheme;
this.notify("color-scheme");
this.queue_draw();
}
/**
* @param {Gtk.Orientation} _orientation The orientation to measure.
* @param {number} _for_size Size for the opposite of orientation.
* @returns {[number, number, number, number]} Minimum size, natural size, minimum baseline, natural baseline.
*/
vfunc_measure(_orientation, _for_size) {
return [PREVIEW_HEIGHT, PREVIEW_HEIGHT, -1, -1];
}
/**
* @param {Gtk.Snapshot} snapshot The snapshot to populate.
*/
vfunc_snapshot(snapshot) {
const previewWidth = this.get_width();
const previewHeight = this.get_height();
snapshot.push_fill(buildGskRoundedRectanglePath(previewWidth, previewHeight, 12), Gsk.FillRule.WINDING);
this.#snapshotBackground(snapshot, previewWidth, previewHeight);
this.#snapshotWindows(snapshot, previewWidth, previewHeight, this.colorScheme, this.accentColor);
this.#snapshotPanel(snapshot, previewWidth, this.colorScheme);
snapshot.pop();
}
async #updateBackgroundTexture() {
try {
const loader = new Gly.Loader({ file: Gio.File.new_for_uri(this.backgroundUri) });
const image = await loader.load_async(null);
this.#backgroundTexture = GlyGtk4.frame_get_texture(image.next_frame());
} catch (e) {
console.error(e);
}
this.queue_draw();
}
/**
* @param {Gtk.Snapshot} snapshot The snapshot to populate.
* @param {number} width The width to populate.
* @param {number} height The height to populate.
*/
#snapshotBackground(snapshot, width, height) {
const ratio = width / height;
if (!this.#backgroundTexture) {
snapshot.append_color(
new Gdk.RGBA({ red: 0.08, green: 0.235, blue: 0.533, alpha: 1 }),
new Graphene.Rect({ origin: new Graphene.Point({ x: 0, y: 0 }), size: { width, height } }),
);
return;
}
let backgroundSnapshotWidth, backgroundSnapshotHeight;
const backgroundRatio = this.#backgroundTexture.get_intrinsic_aspect_ratio();
if (backgroundRatio > ratio) {
backgroundSnapshotWidth = height;
backgroundSnapshotHeight = height / backgroundRatio;
} else {
backgroundSnapshotWidth = width * backgroundRatio;
backgroundSnapshotHeight = width;
}
const x = (width - backgroundSnapshotWidth) / 2;
const y = (height - backgroundSnapshotHeight) / 2;
snapshot.save();
snapshot.translate(new Graphene.Point({ x, y }));
this.#backgroundTexture.snapshot(snapshot, backgroundSnapshotWidth, backgroundSnapshotHeight);
snapshot.restore();
}
/**
* @param {Gtk.Snapshot} snapshot The snapshot to populate.
* @param {number} width The width to populate.
* @param {number} height The height to populate.
* @param {ColorScheme} colorScheme The color scheme to render.
* @param {AccentColor} accentColor The accent color to render.
*/
#snapshotWindows(snapshot, width, height, colorScheme, accentColor) {
const usableHeight = height - PANEL_HEIGHT;
const backgroundWindowX = Math.ceil(width / 2 - WINDOW_WIDTH + WINDOW_WIDTH / 2 - width / 16);
const backgroundWindowY = Math.ceil(
usableHeight / 2 - WINDOW_HEIGHT + WINDOW_HEIGHT / 2 - usableHeight / 16 + PANEL_HEIGHT,
);
const foregroundWindowX = Math.ceil(width / 2 - WINDOW_WIDTH / 2 + width / 16);
const foregroundWindowY = Math.ceil(usableHeight / 2 - WINDOW_HEIGHT / 2 + usableHeight / 16 + PANEL_HEIGHT);
switch (colorScheme) {
case ColorScheme.DEFAULT:
this.#snapshotWindow(snapshot, backgroundWindowX, backgroundWindowY, ColorScheme.PREFER_DARK);
this.#snapshotWindow(snapshot, foregroundWindowX, foregroundWindowY, ColorScheme.PREFER_LIGHT);
break;
case ColorScheme.PREFER_DARK:
this.#snapshotWindow(snapshot, backgroundWindowX, backgroundWindowY, ColorScheme.PREFER_DARK);
this.#snapshotWindow(snapshot, foregroundWindowX, foregroundWindowY, ColorScheme.PREFER_DARK);
break;
case ColorScheme.PREFER_LIGHT:
this.#snapshotWindow(snapshot, backgroundWindowX, backgroundWindowY, ColorScheme.PREFER_LIGHT);
this.#snapshotWindow(snapshot, foregroundWindowX, foregroundWindowY, ColorScheme.PREFER_LIGHT);
break;
}
const buttonX = foregroundWindowX + WINDOW_WIDTH - BUTTON_WIDTH - 20;
const buttonY = foregroundWindowY + WINDOW_HEIGHT - BUTTON_HEIGHT - 20;
this.#snapshotButton(snapshot, buttonX, buttonY, accentColor);
}
/**
* @param {Gtk.Snapshot} snapshot The snapshot to populate.
* @param {number} x The X position at which to populate.
* @param {number} y The Y position at which to populate.
* @param {ColorScheme} colorScheme The color scheme to render.
*/
#snapshotWindow(snapshot, x, y, colorScheme) {
const texture = colorScheme === ColorScheme.PREFER_DARK ? this.#windowDarkTexture : this.#windowLightTexture;
snapshot.save();
snapshot.translate(new Graphene.Point({ x, y }));
texture.snapshot(snapshot, WINDOW_WIDTH, WINDOW_HEIGHT);
snapshot.restore();
}
/**
* @param {Gtk.Snapshot} snapshot The snapshot to populate.
* @param {number} x The X position at which to populate.
* @param {number} y The Y position at which to populate.
* @param {AccentColor} accentColor The accent color to render.
*/
#snapshotButton(snapshot, x, y, accentColor) {
let color;
switch (accentColor) {
case AccentColor.BLUE:
color = new Gdk.RGBA({ red: 0.208, green: 0.518, blue: 0.894, alpha: 1 });
break;
case AccentColor.GREEN:
color = new Gdk.RGBA({ red: 0.227, green: 0.58, blue: 0.29, alpha: 1 });
break;
case AccentColor.ORANGE:
color = new Gdk.RGBA({ red: 0.929, green: 0.357, blue: 0, alpha: 1 });
break;
case AccentColor.PINK:
color = new Gdk.RGBA({ red: 0.835, green: 0.38, blue: 0.6, alpha: 1 });
break;
case AccentColor.PURPLE:
color = new Gdk.RGBA({ red: 0.569, green: 0.255, blue: 0.675, alpha: 1 });
break;
case AccentColor.RED:
color = new Gdk.RGBA({ red: 0.902, green: 0.176, blue: 0.259, alpha: 1 });
break;
case AccentColor.SLATE:
color = new Gdk.RGBA({ red: 0.435, green: 0.514, blue: 0.588, alpha: 1 });
break;
case AccentColor.TEAL:
color = new Gdk.RGBA({ red: 0.129, green: 0.565, blue: 0.643, alpha: 1 });
break;
case AccentColor.YELLOW:
color = new Gdk.RGBA({ red: 0.784, green: 0.533, blue: 0, alpha: 1 });
break;
}
if (!color) return;
snapshot.save();
snapshot.translate(new Graphene.Point({ x, y }));
snapshot.append_fill(buildGskRoundedRectanglePath(BUTTON_WIDTH, BUTTON_HEIGHT, 3), Gsk.FillRule.WINDING, color);
snapshot.restore();
}
/**
* @param {Gtk.Snapshot} snapshot The snapshot to populate.
* @param {number} width The width to populate.
* @param {ColorScheme} colorScheme The color scheme to render.
*/
#snapshotPanel(snapshot, width, colorScheme) {
const backgroundColor =
colorScheme === ColorScheme.PREFER_LIGHT
? new Gdk.RGBA({ red: 0.98, green: 0.98, blue: 0.984, alpha: 1 })
: new Gdk.RGBA({ red: 0, green: 0, blue: 0, alpha: 1 });
snapshot.append_color(
backgroundColor,
new Graphene.Rect({
origin: new Graphene.Point({ x: 0, y: 0 }),
size: new Graphene.Size({ width, height: PANEL_HEIGHT }),
}),
);
}
}
/**
* Build a rounded rectangle path until GJS supports `Gsk.RoundedRect`.
* @param {number} width The width of the rectangle.
* @param {number} height The height of the rectangle.
* @param {number} radius The radius of the corners.
* @returns {Gsk.Path} The path.
*/
function buildGskRoundedRectanglePath(width, height, radius) {
const builder = new Gsk.PathBuilder();
builder.add_circle(new Graphene.Point({ x: radius, y: radius }), radius);
builder.add_circle(new Graphene.Point({ x: width - radius, y: radius }), radius);
builder.add_circle(new Graphene.Point({ x: radius, y: height - radius }), radius);
builder.add_circle(new Graphene.Point({ x: width - radius, y: height - radius }), radius);
builder.add_rect(
new Graphene.Rect({
origin: new Graphene.Point({ x: radius, y: 0 }),
size: new Graphene.Size({ width: width - 2 * radius, height }),
}),
);
builder.add_rect(
new Graphene.Rect({
origin: new Graphene.Point({ x: 0, y: radius }),
size: new Graphene.Size({ width, height: height - 2 * radius }),
}),
);
return builder.to_path();
}
+6 -99
View File
@@ -3,9 +3,9 @@
import Adw from "gi://Adw";
import Gdk from "gi://Gdk";
import GdkPixbuf from "gi://GdkPixbuf";
import Gio from "gi://Gio";
import GLib from "gi://GLib";
import Gly from "gi://Gly";
import GObject from "gi://GObject";
import Gtk from "gi://Gtk";
@@ -14,34 +14,14 @@ import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensio
Gio._promisify(Gtk.FileDialog.prototype, "open", "open_finish");
export class BackgroundButton extends Gtk.Button {
#uri;
static {
GObject.registerClass(
{
GTypeName: "BackgroundButton",
Template: "resource:///org/gnome/Shell/Extensions/nightthemeswitcher/preferences/ui/BackgroundButton.ui",
InternalChildren: ["file_dialog", "thumbnail"],
InternalChildren: ["file_dialog"],
Properties: {
uri: GObject.ParamSpec.string("uri", "URI", "URI to the background file", GObject.ParamFlags.READWRITE, null),
thumbWidth: GObject.ParamSpec.int(
"thumb-width",
"Thumbnail width",
"Width of the displayed thumbnail",
GObject.ParamFlags.READWRITE,
0,
600,
180,
),
thumbHeight: GObject.ParamSpec.int(
"thumb-height",
"Thumbnail height",
"Height of the displayed thumbnail",
GObject.ParamFlags.READWRITE,
0,
600,
180,
),
},
},
this,
@@ -50,33 +30,10 @@ export class BackgroundButton extends Gtk.Button {
constructor({ ...params } = {}) {
super(params);
this.#setupSize();
this.#setupDropTarget();
this.#setupFileDialog();
}
get uri() {
return this.#uri || null;
}
set uri(uri) {
if (uri === this.#uri) return;
this.#uri = uri;
this.notify("uri");
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
this.#updateThumbnail();
return GLib.SOURCE_REMOVE;
});
}
#setupSize() {
const display = Gdk.Display.get_default();
const monitor = display.get_monitors().get_item(0);
if (monitor.width_mm === 0 || monitor.height_mm === 0) return;
if (monitor.width_mm > monitor.height_mm) this.thumbHeight *= monitor.height_mm / monitor.width_mm;
else this.thumbWidth *= monitor.width_mm / monitor.height_mm;
}
#setupDropTarget() {
const dropTarget = Gtk.DropTarget.new(Gio.File.$gtype, Gdk.DragAction.COPY);
dropTarget.connect("drop", (_target, file, _x, _y) => {
@@ -108,9 +65,7 @@ export class BackgroundButton extends Gtk.Button {
}
#getSupportedContentTypes() {
return GdkPixbuf.Pixbuf.get_formats()
.flatMap((format) => format.get_mime_types())
.concat("application/xml");
return Gly.Loader.get_mime_types().concat("application/xml");
}
#isContentTypeSupported(contentType) {
@@ -130,61 +85,13 @@ export class BackgroundButton extends Gtk.Button {
async #setURIFromFileDialog() {
try {
this._file_dialog.initial_folder = Gio.File.new_for_uri(this.uri).get_parent();
const file = await this._file_dialog.open(this.get_root(), null);
this.uri = file.get_uri();
} catch {}
}
#updateThumbnail() {
this._thumbnail.paintable = null;
if (!this.uri) return;
const file = Gio.File.new_for_uri(this.uri);
const contentType = Gio.content_type_guess(file.get_basename(), null)[0];
if (!this.#isContentTypeSupported(contentType)) return;
let path;
if (Gio.content_type_equals(contentType, "application/xml")) {
const decoder = new TextDecoder("utf-8");
const contents = decoder.decode(file.load_contents(null)[1]);
try {
path = contents.match(/<file>(.+)<\/file>/m)[1];
if (!this.#isContentTypeSupported(Gio.content_type_guess(path, null)[0])) throw new Error();
} catch (e) {
console.error(`No suitable background file found in ${file.get_path()}.\n${e}`);
return;
}
} else {
path = file.get_path();
}
const pixbuf = GdkPixbuf.Pixbuf.new_from_file(path);
const scale =
pixbuf.width / pixbuf.height > this.thumbWidth / this.thumbHeight
? this.thumbHeight / pixbuf.height
: this.thumbWidth / pixbuf.width;
const thumbPixbuf = GdkPixbuf.Pixbuf.new(
pixbuf.colorspace,
pixbuf.has_alpha,
pixbuf.bits_per_sample,
this.thumbWidth,
this.thumbHeight,
);
pixbuf.scale(
thumbPixbuf,
0,
0,
this.thumbWidth,
this.thumbHeight,
-(pixbuf.width * scale - this.thumbWidth) / 2,
-(pixbuf.height * scale - this.thumbHeight) / 2,
scale,
scale,
GdkPixbuf.InterpType.TILES,
);
this._thumbnail.paintable = Gdk.Texture.new_for_pixbuf(thumbPixbuf);
_getBackgroundFileName(_widget, uri) {
return uri ? GLib.filename_display_basename(uri) : "";
}
}
+90
View File
@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: Night Theme Switcher Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
import GObject from "gi://GObject";
import Gtk from "gi://Gtk";
import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
import { AccentColor } from "../enums/AccentColor.js";
export class ColorChooser extends Gtk.Widget {
/** @type {Gtk.ToggleButton[]} */
#buttons = [];
/** @type {AccentColor} */
#activeColor;
static {
GObject.registerClass(
{
GTypeName: "ColorChooser",
CssName: "color-chooser",
Properties: {
color: GObject.ParamSpec.string(
"active-color",
"Active color",
"Active color",
GObject.ParamFlags.READWRITE,
null,
),
},
},
this,
);
}
constructor({ ...params } = {}) {
super({ layoutManager: new Gtk.BoxLayout({ spacing: 6 }), ...params });
const colors = [
{ accentColor: AccentColor.BLUE, name: _("Blue") },
{ accentColor: AccentColor.TEAL, name: _("Teal") },
{ accentColor: AccentColor.GREEN, name: _("Green") },
{ accentColor: AccentColor.YELLOW, name: _("Yellow") },
{ accentColor: AccentColor.ORANGE, name: _("Orange") },
{ accentColor: AccentColor.RED, name: _("Red") },
{ accentColor: AccentColor.PINK, name: _("Pink") },
{ accentColor: AccentColor.PURPLE, name: _("Purple") },
{ accentColor: AccentColor.SLATE, name: _("Slate") },
];
let toggleGroup;
for (const color of colors) {
const button = new Gtk.ToggleButton({
tooltipText: color.name,
cssClasses: ["circular", color.accentColor],
active: this.activeColor === color.accentColor,
});
if (!toggleGroup) toggleGroup = button;
else button.group = toggleGroup;
button.connect("toggled", () => {
if (!button.active) return;
this.activeColor = color.accentColor;
});
this.#buttons.push(button);
button.set_parent(this);
}
}
get activeColor() {
return this.#activeColor;
}
set activeColor(color) {
if (!color || color === this.#activeColor || !Object.values(AccentColor).includes(color)) return;
this.#activeColor = color;
this.notify("active-color");
this.#updateButtons();
}
#updateButtons() {
this.#buttons.forEach((button) => {
button.active = button.cssClasses.includes(this.activeColor);
});
}
}
+22 -3
View File
@@ -21,17 +21,30 @@ export default class NightThemeSwitcherPreferences extends ExtensionPreferences
const resource = Gio.Resource.load(GLib.build_filenamev([this.path, "resources", "preferences.gresource"]));
Gio.resources_register(resource);
// Load stylesheet
const provider = new Gtk.CssProvider();
provider.load_from_resource("/org/gnome/Shell/Extensions/nightthemeswitcher/preferences/style.css");
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(),
provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
);
// Load icons
const iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default());
iconTheme.add_resource_path("/org/gnome/Shell/Extensions/nightthemeswitcher/preferences/icons");
// Set window properties
window.search_enabled = true;
window.set_size_request(420, 320);
window.set_default_size(500, 630);
// Dynamically import all classes
const { BackgroundButton } = await import("./preferences/BackgroundButton.js");
const { AppearanceChooser } = await import("./preferences/AppearanceChooser.js");
const { AppearancePage } = await import("./preferences/AppearancePage.js");
const { AppearancePreview } = await import("./preferences/AppearancePreview.js");
const { BackgroundButton } = await import("./preferences/BackgroundButton.js");
const { ColorChooser } = await import("./preferences/ColorChooser.js");
const { CommandsPage } = await import("./preferences/CommandsPage.js");
const { ContributePage } = await import("./preferences/ContributePage.js");
const { SchedulePage } = await import("./preferences/SchedulePage.js");
@@ -39,8 +52,11 @@ export default class NightThemeSwitcherPreferences extends ExtensionPreferences
const { TimeChooser } = await import("./preferences/TimeChooser.js");
// Make sure all GObjects are registered
GObject.type_ensure(BackgroundButton);
GObject.type_ensure(AppearanceChooser);
GObject.type_ensure(AppearancePage);
GObject.type_ensure(AppearancePreview);
GObject.type_ensure(BackgroundButton);
GObject.type_ensure(ColorChooser);
GObject.type_ensure(CommandsPage);
GObject.type_ensure(ContributePage);
GObject.type_ensure(SchedulePage);
@@ -50,7 +66,10 @@ export default class NightThemeSwitcherPreferences extends ExtensionPreferences
// Add all pages
[
new SchedulePage({ settings: this.getSettings(`${this.metadata["settings-schema"]}.time`) }),
new AppearancePage(),
new AppearancePage({
accentColorSettings: this.getSettings(`${this.metadata["settings-schema"]}.accent-color`),
colorSchemeSettings: this.getSettings(`${this.metadata["settings-schema"]}.color-scheme`),
}),
new CommandsPage({ settings: this.getSettings(`${this.metadata["settings-schema"]}.commands`) }),
new ContributePage(),
].forEach((page) => window.add(page));