'/system-information',
[GaletteController::class, 'systemInformation']
)->setName('sysinfos')->add($authenticate);
+
+$app->post(
+ '/write-dark-css',
+ function ($request, $response) {
+ $post = $request->getParsedBody();
+ file_put_contents(GALETTE_CACHE_DIR . '/dark.css', $post);
+ return $response->withStatus(200);
+ }
+)->setName('writeDarkCSS');
+
+$app->get(
+ '/get-dark-css',
+ function ($request, $response) {
+ $cssfile = GALETTE_CACHE_DIR . '/dark.css';
+ if (file_exists($cssfile)) {
+ $response = $response->withHeader('Content-type', 'text/css');
+ $body = $response->getBody();
+ $body->write(file_get_contents($cssfile));
+ }
+ return $response;
+ }
+)->setName('getDarkCSS');
protected $managed_groups = [];
protected $cron = false;
protected $compact_menu = false;
+ protected $dark_mode = false;
/**
* Logs in user.
*/
public function getCompactMenu(): bool
{
- return ($this->logged && isset($_COOKIE['galette_compact_menu']) && $_COOKIE['galette_compact_menu']) ? true : false;
+ return $this->logged && isset($_COOKIE['galette_compact_menu']) && $_COOKIE['galette_compact_menu'];
+ }
+
+ /**
+ * Is dark mode enabled?
+ *
+ * @return bool
+ */
+ public function isDarkModeEnabled(): bool
+ {
+ return isset($_COOKIE['galette_dark_mode']) && $_COOKIE['galette_dark_mode'];
}
/**
* Is user currently up to date?
- * An up to date member is active and either due free, or with up to date
+ * An up-to-date member is active and either due free, or with up-to-date
* subscription
*
* @return bool
{% endif %}
</nav>
{% if login.isLogged() and cur_route == 'dashboard' %}
- {# Comply with the Twemoji project's requirements about attribution #}
+ {# Comply with the Twemoji project's requirements about attribution #}
<!--
Emojis used by Galette's core and its official plugins on the dashboard
are licensed under CC-BY 4.0 <https://creativecommons.org/licenses/by/4.0/>
{% include header with {'module_id': mid} %}
{% endfor %}
{% endif %}
+
+{% set darkcssfile = constant('GALETTE_CACHE_DIR') ~ "dark.css" %}
+{% if login.isDarkModeEnabled() and file_exists(darkcssfile) %}
+ <link rel="stylesheet" type="text/css" href="{{ url_for('getDarkCSS') }}">
+{% endif %}
--- /dev/null
+{% if ui is defined %}
+ {% if ui == 'item' %}
+ {% set component_classes = "item" %}
+ {% elseif ui == 'menu' %}
+ {% set component_classes = "ui text compact small fluid menu" %}
+ {% endif %}
+{% endif %}
+{% if login.isLogged() %}
+ {% if ui == 'item' %}
+ <div class="{{ component_classes }}">
+ <div class="ui basic center aligned fitted segment">
+ <span class="ui tiny header">{{ login.loggedInAs()|raw }}</span>
+ </div>
+ <a
+ href="#"
+ class="ui fluid darkmode{% if login.isDarkModeEnabled() %} black{% endif %} basic button"
+ >
+ <i class="icon adjust"></i>
+ {% if login.isDarkModeEnabled() %}{{ _T("Disable dark mode") }}{% else %}{{ _T("Enable dark mode") }}{% endif %}
+ </a>
+ <a
+ class="ui fluid {% if login.isImpersonated() %}purple{% else %}red{% endif %} basic button"
+ href="{% if login.isImpersonated() %}{{ url_for("unimpersonate") }}{% else %}{{ url_for("logout") }}{% endif %}"
+ >
+ <i class="icon {% if login.isImpersonated() %}user secret{% else %}sign out alt{% endif %}"></i>
+ {% if login.isImpersonated() %}{{ _T("Unimpersonate") }}{% else %}{{ _T("Log off") }}{% endif %}
+ </a>
+ </div>
+ {% else %}
+ {% if not login.getCompactMenu() %}
+ <div class="{{ component_classes }}">
+ <div class="ui item">
+ <i class="user circle big icon"></i>
+ {{ login.loggedInAs()|raw }}
+ </div>
+ <div class="right menu">
+ <div class="item">
+ <div class="ui icon buttons">
+ <a
+ href="#"
+ class="ui darkmode{% if login.isDarkModeEnabled() %} black{% endif %} icon button"
+ title="{% if login.isDarkModeEnabled() %}{{ _T("Disable dark mode") }}{% else %}{{ _T("Enable dark mode") }}{% endif %}"
+ >
+ <i class="icon adjust"></i>
+ <span class="displaynone">{% if login.isDarkModeEnabled() %}{{ _T("Disable dark mode") }}{% else %}{{ _T("Enable dark mode") }}{% endif %}</span>
+ </a>
+ <a
+ class="ui {% if login.isImpersonated() %}purple{% else %}red{% endif %} icon button"
+ href="{% if login.isImpersonated() %}{{ url_for("unimpersonate") }}{% else %}{{ url_for("logout") }}{% endif %}"
+ title="{% if login.isImpersonated() %}{{ _T("Unimpersonate") }}{% else %}{{ _T("Log off") }}{% endif %}"
+ data-position="bottom right"
+ >
+ <i class="icon {% if login.isImpersonated() %}user secret{% else %}sign out alt{% endif %}"></i>
+ <span class="displaynone">{% if login.isImpersonated() %}{{ _T("Unimpersonate") }}{% else %}{{ _T("Log off") }}{% endif %}</span>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ {% include "elements/modes.html.twig" %}
+ {% else %}
+ {% set component_classes = "ui vertical centered tiny fluid icon menu" %}
+ <div id="logoutmenu" class="{{ component_classes }}">
+ <div class="ui dropdown item no-touch tooltip" data-html="{{ login.loggedInAs()|raw }}" data-position="right center">
+ <i class="user circle icon"></i>
+ <span class="text displaynone">{{ login.loggedInAs()|raw }}</span>
+ <div class="menu">
+ <div class="item">
+ <div class="ui basic center aligned fitted segment">
+ <img src="{{ url_for('logo') }}" width="{{ logo.getOptimalWidth() }}" height="{{ logo.getOptimalHeight() }}" alt="{{ preferences.pref_nom }}" class="icon"/>
+ <div class="ui block huge brand header">
+ {{ preferences.pref_nom }}
+ {% if preferences.pref_slogan %}<div class="sub tiny header">{{ __(preferences.pref_slogan) }}</div>{% endif %}
+ </div>
+ </div>
+ {{ login.loggedInAs()|raw }}
+ <div class="ui basic fitted segment">
+ {% include "elements/modes.html.twig" %}
+ </div>
+ <a
+ href="#"
+ class="ui darkmode{% if login.isDarkModeEnabled() %} black{% endif %} fluid icon button"
+ title="{% if login.isDarkModeEnabled() %}{{ _T("Disable dark mode") }}{% else %}{{ _T("Enable dark mode") }}{% endif %}"
+ >
+ <i class="icon adjust"></i>
+ {% if login.isDarkModeEnabled() %}{{ _T("Disable dark mode") }}{% else %}{{ _T("Enable dark mode") }}{% endif %}
+ </a>
+ <a
+ class="ui {% if login.isImpersonated() %}purple{% else %}red{% endif %} fluid icon button"
+ href="{% if login.isImpersonated() %}{{ url_for("unimpersonate") }}{% else %}{{ url_for("logout") }}{% endif %}"
+ >
+ <i class="icon {% if login.isImpersonated() %}user secret{% else %}sign out alt{% endif %}"></i>
+ {% if login.isImpersonated() %}{{ _T("Unimpersonate") }}{% else %}{{ _T("Log off") }}{% endif %}
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ {% endif %}
+ {% endif %}
+{% endif %}
+++ /dev/null
-{% if ui is defined %}
- {% if ui == 'item' %}
- {% set component_classes = "item" %}
- {% elseif ui == 'menu' %}
- {% set component_classes = "ui text compact small fluid menu" %}
- {% endif %}
-{% endif %}
-{% if login.isLogged() %}
- {% if ui == 'item' %}
- <div class="{{ component_classes }}">
- <div class="ui basic center aligned fitted segment">
- <span class="ui tiny header">{{ login.loggedInAs()|raw }}</span>
- </div>
- <a
- class="ui fluid {% if login.isImpersonated() %}purple{% else %}red{% endif %} basic button"
- href="{% if login.isImpersonated() %}{{ url_for("unimpersonate") }}{% else %}{{ url_for("logout") }}{% endif %}"
- >
- <i class="icon {% if login.isImpersonated() %}user secret{% else %}sign out alt{% endif %}"></i>
- {% if login.isImpersonated() %}{{ _T("Unimpersonate") }}{% else %}{{ _T("Log off") }}{% endif %}
- </a>
- </div>
- {% else %}
- {% if not login.getCompactMenu() %}
- <div class="{{ component_classes }}">
- <div class="ui item">
- <i class="user circle big icon"></i>
- {{ login.loggedInAs()|raw }}
- </div>
- <div class="right menu">
- <div class="item">
- <a
- class="ui {% if login.isImpersonated() %}purple{% else %}red{% endif %} icon button"
- href="{% if login.isImpersonated() %}{{ url_for("unimpersonate") }}{% else %}{{ url_for("logout") }}{% endif %}"
- title="{% if login.isImpersonated() %}{{ _T("Unimpersonate") }}{% else %}{{ _T("Log off") }}{% endif %}"
- data-position="bottom right"
- >
- <i class="icon {% if login.isImpersonated() %}user secret{% else %}sign out alt{% endif %}"></i>
- <span class="displaynone">{% if login.isImpersonated() %}{{ _T("Unimpersonate") }}{% else %}{{ _T("Log off") }}{% endif %}</span>
- </a>
- </div>
- </div>
- </div>
- {% include "elements/modes.html.twig" %}
- {% else %}
- {% set component_classes = "ui vertical centered tiny fluid icon menu" %}
- <div id="logoutmenu" class="{{ component_classes }}">
- <div class="ui dropdown item no-touch tooltip" data-html="{{ login.loggedInAs()|raw }}" data-position="right center">
- <i class="user circle icon"></i>
- <span class="text displaynone">{{ login.loggedInAs()|raw }}</span>
- <div class="menu">
- <div class="item">
- <div class="ui basic center aligned fitted segment">
- <img src="{{ url_for('logo') }}" width="{{ logo.getOptimalWidth() }}" height="{{ logo.getOptimalHeight() }}" alt="{{ preferences.pref_nom }}" class="icon"/>
- <div class="ui block huge brand header">
- {{ preferences.pref_nom }}
- {% if preferences.pref_slogan %}<div class="sub tiny header">{{ __(preferences.pref_slogan) }}</div>{% endif %}
- </div>
- </div>
- {{ login.loggedInAs()|raw }}
- <div class="ui basic fitted segment">
- {% include "elements/modes.html.twig" %}
- </div>
- <a
- class="ui {% if login.isImpersonated() %}purple{% else %}red{% endif %} icon button"
- href="{% if login.isImpersonated() %}{{ url_for("unimpersonate") }}{% else %}{{ url_for("logout") }}{% endif %}"
- >
- <i class="icon {% if login.isImpersonated() %}user secret{% else %}sign out alt{% endif %}"></i>
- {% if login.isImpersonated() %}{{ _T("Unimpersonate") }}{% else %}{{ _T("Log off") }}{% endif %}
- </a>
- </div>
- </div>
- </div>
- </div>
- {% endif %}
- {% endif %}
-{% endif %}
<aside id="sidemenu" class="ui computer only toc{% if login.getCompactMenu() %} compact_menu{% endif %}">
- {% include "elements/logout.html.twig" with {
+ {% include "elements/logged_user.html.twig" with {
ui: "menu"
} %}
ui: "item"
} %}
{% if login.isLogged() %}
- {% include "elements/logout.html.twig" with {
+ {% include "elements/logged_user.html.twig" with {
ui: "item"
} %}
{% endif %}
{% for public_item in public_items %}
{{ menus_macros.renderMenuItem(public_item.label, public_item.title, public_item.route, public_item.icon, public_item.class, tips_position) }}
{% endfor %}
+ <a
+ href="#"
+ class="ui darkmode{% if login.isDarkModeEnabled() %} black{% endif %} icon button"
+ data-position="{{ tips_position }}"
+ title="{% if login.isDarkModeEnabled() %}{{ _T("Disable dark mode") }}{% else %}{{ _T("Enable dark mode") }}{% endif %}"
+ >
+ <i class="icon adjust"></i>
+ <span>{% if login.isDarkModeEnabled() %}{{ _T("Disable dark mode") }}{% else %}{{ _T("Enable dark mode") }}{% endif %}</span>
+ </a>
</div>
{% endif %}
<script type="text/javascript" src="{{ base_path() }}/{{ constant('GALETTE_THEME') }}ui/semantic.min.js"></script>
<script type="text/javascript" src="{{ base_path() }}/assets/js/galette-main.bundle.min.js"></script>
+ {% if login.isDarkModeEnabled() %}
+ <script type="text/javascript" src="{{ base_path() }}/assets/js/darkreader.min.js"></script>
+ {% endif %}
<script type="text/javascript">
function csrfSafeMethod(method) {
}
$(function(){
+ function _darkMode() {
+ var _dark_enabled = Cookies.get('galette_dark_mode');
+ var _cookie_value = 1;
+ if (_dark_enabled && _dark_enabled == 1) {
+ var _cookie_value = 0;
+ {% set darkcssfile = constant('GALETTE_CACHE_DIR') ~ "dark.css" %}
+ {% if not file_exists(darkcssfile) %}
+ function writeDarkTheme() {
+ DarkReader.enable({
+ brightness: 100,
+ contrast: 90,
+ sepia: 10
+ });
+ return DarkReader.exportGeneratedCSS();
+ }
+ writeDarkTheme().then(function(cssdata) {
+ $.ajax({
+ url: '{{ url_for("writeDarkCSS") }}',
+ method: 'post',
+ data: cssdata.replaceAll('themes/galette/assets', 'themes/default/ui/themes/galette/assets'),
+ success: function(res) {
+ console.log('Dark theme CSS stored');
+ },
+ error: function() {
+ console.log('Error storing dark theme CSS');
+ }
+ });
+ });
+ {% endif %}
+ }
+ $('.darkmode').on('click', function(e) {
+ e.preventDefault();
+ Cookies.set(
+ 'galette_dark_mode',
+ _cookie_value,
+ {
+ expires: 365,
+ path: '/'
+ }
+ );
+ window.location.reload();
+ });
+ if (window.matchMedia) {
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
+ if (event.matches) {
+ _cookie_value = 1;
+ }
+ Cookies.set(
+ 'galette_dark_mode',
+ _cookie_value,
+ {
+ expires: 365,
+ path: '/'
+ }
+ );
+ window.location.reload();
+ });
+ }
+ }
+ _darkMode();
+
$.ajaxPrefilter(function(options, originalOptions, jqXHR){
if (options.type.toLowerCase() === "post") {
// initialize `data` to empty string if it does not exist
],
codemirrorformatting: [
'https://cdnjs.cloudflare.com/ajax/libs/codemirror/2.36.0/formatting.js'
+ ],
+ darkreader: [
+ './node_modules/darkreader/darkreader.js'
]
},
extras: [
.pipe(gulp.dest(paths.assets.js))
.pipe(browserSync.stream());
- return merge(main, masschanges, chartjs, sortablejs, summernote, codemirror, codemirrorxml, codemirrorformatting);
+ darkreader = gulp.src(paths.scripts.darkreader)
+ .pipe(concat('darkreader.min.js'))
+ .pipe(uglify({
+ output: {
+ comments: /^!/
+ }
+ }))
+ .pipe(gulp.dest(paths.assets.js))
+ .pipe(browserSync.stream());
+
+ return merge(main, masschanges, chartjs, sortablejs, summernote, codemirror, codemirrorxml, codemirrorformatting, darkreader);
}
function movefiles() {
"chart.js": "^4.4.0",
"chartjs-plugin-autocolors": "^0.2.2",
"chartjs-plugin-datalabels": "^2.2.0",
+ "darkreader": "^4.9.67",
"fomantic-ui": "^2.9.3",
"jquery": "^3.7.1",
"js-cookie": "^3.0.5",
"type": "^1.0.1"
}
},
+ "node_modules/darkreader": {
+ "version": "4.9.67",
+ "resolved": "https://registry.npmjs.org/darkreader/-/darkreader-4.9.67.tgz",
+ "integrity": "sha512-gFEz+GZGCWVtnRbTb/7oa3QdvPqe21Cyjj/hxg3sMP8RK7zUYptjnDFxe77xvVcfhkf+bz6CfweqrixVoHxaPg==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/darkreader/donate"
+ }
+ },
"node_modules/dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"type": "^1.0.1"
}
},
+ "darkreader": {
+ "version": "4.9.67",
+ "resolved": "https://registry.npmjs.org/darkreader/-/darkreader-4.9.67.tgz",
+ "integrity": "sha512-gFEz+GZGCWVtnRbTb/7oa3QdvPqe21Cyjj/hxg3sMP8RK7zUYptjnDFxe77xvVcfhkf+bz6CfweqrixVoHxaPg=="
+ },
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"chart.js": "^4.4.0",
"chartjs-plugin-autocolors": "^0.2.2",
"chartjs-plugin-datalabels": "^2.2.0",
+ "darkreader": "^4.9.67",
"fomantic-ui": "^2.9.3",
"jquery": "^3.7.1",
"js-cookie": "^3.0.5",
padding-top: 0;
}
+ #top-navbar div.item a.button.darkmode span {
+ display: none;
+ }
+
aside.computer.toc {
background: @galetteNavBackground;
}
}
}
+@media only screen and (max-width: 1199px) {
+ aside.computer.toc {
+ .ui.text.compact.small.fluid.menu {
+ font-size: .8em;
+
+ .ui.buttons .button {
+ padding: .5em;
+ }
+ }
+ }
+}
+
@media only screen and (min-width: 1200px) {
aside.toc {
width: 350px;