PluginProbe
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) / 2.1.3
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) v2.1.3
2.1.3 2.1.2 2.1.1 2.1.0 2.0.4 2.0.3 2.0.2 2.0.1 2.0.0 1.5.5 1.5.4 1.5.3 1.5.2 1.5.1 1.5.0 trunk 1.0.1 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 All 57 releases
← All changes | src/Admin/Admin.php +470 -197 1.5.42.1.3 View file →
@@ -11,15 +11,16 @@
11 11 * @author ThemeAtelier<themeatelierbd@gmail.com>
12 12 */
13 13
14 14 namespace ThemeAtelier\Darkify\Admin;
15 -use ThemeAtelier\Darkify\Admin\TADiscountPage\TADiscountPage;
15 +
16 16 use ThemeAtelier\Darkify\Admin\ReviewNotice\ReviewNotice;
17 +use ThemeAtelier\Darkify\Admin\ReviewNotice\ThemeAtelier_Offer_Banner;
18 +use ThemeAtelier\Darkify\Admin\Schema\SchemaDefaults;
17 19 use ThemeAtelier\Darkify\Admin\Views\DarkifyOptions;
18 20 use ThemeAtelier\Darkify\Includes\DarkifyExternalSupport;
19 21 use ThemeAtelier\Darkify\Includes\DarkifyUtils;
20 22 use ThemeAtelier\Darkify\Admin\DBUpdates;
21 -use ThemeAtelier\Darkify\Admin\ReviewNotice\ThemeAtelier_Offer_Banner;
22 23
23 24 /**
24 25 * The admin class
25 26 */
@@ -26,17 +27,8 @@
26 27 class Admin
27 28 {
28 29
29 30 /**
30 - * The slug of this plugin.
31 - *
32 - * @since 1.0.0
33 - * @access private
34 - * @var string $plugin_slug The slug of this plugin.
35 - */
36 - private $plugin_slug;
37 -
38 - /**
39 31 * The min of this plugin.
40 32 *
41 33 * @since 1.0.0
42 34 * @access private
@@ -42,93 +34,197 @@
42 34 * @access private
43 35 * @var string $min The slug of this plugin.
44 36 */
45 37 private $min;
46 -
47 - /**
48 - * The version of this plugin.
49 - *
50 - * @since 1.0.0
51 - * @access private
52 - * @var string $version The current version of this plugin.
53 - */
54 - private $version;
55 38 public $utils;
56 39 public $settings;
57 40 public $external_support;
41 + private $plugin_name;
42 + private $version;
58 43 public $unique_id;
59 - private $plugin_name;
60 44
45 + /**
46 + * Whether enqueue_scripts() actually put the admin engine in the queue on
47 + * this request. Read by darkify_restore_no_conflict_scripts(), which must
48 + * only re-add handles Darkify itself asked for.
49 + *
50 + * @var bool
51 + */
52 + private $engine_enqueued = false;
61 53
62 54 /**
63 55 * The class constructor.
64 56 *
65 - * @param string $plugin_slug The slug of the plugin.
57 + * @param string $plugin_name The slug of the plugin.
66 58 * @param string $version Current version of the plugin.
67 59 */
68 - function __construct($plugin_slug, $version)
60 + public function __construct($plugin_name, $version)
69 61 {
62 + $this->plugin_name = $plugin_name;
63 + $this->version = $version;
70 64 $this->utils = new DarkifyUtils($this);
71 65 $this->external_support = new DarkifyExternalSupport($this);
66 + $this->min = (apply_filters('darkify_dev_mode', false) || WP_DEBUG) ? '' : '.min';
72 67 if (function_exists('wp_rand')) {
73 68 $this->unique_id = wp_rand();
74 69 }
75 70
76 - $options = get_option('darkify');
71 + add_action('current_screen', function ($screen) {
72 + $options = get_option('darkify');
73 + $block_editor_dark_mode = isset($options['block_editor_dark_mode']) ? $options['block_editor_dark_mode'] : false;
74 + $enable_admin_panel_dark_mode = isset($options['enable_admin_panel_dark_mode']) ? $options['enable_admin_panel_dark_mode'] : false;
75 + $is_block_editor = method_exists($screen, 'is_block_editor') && $screen->is_block_editor();
77 76
78 - $enable_admin_panel_dark_mode = isset($options['enable_admin_panel_dark_mode']) ? $options['enable_admin_panel_dark_mode'] : false;
79 - $this->plugin_name = $plugin_slug;
80 - $this->min = (apply_filters('darkify_dev_mode', false) || WP_DEBUG) ? '' : '.min';
81 -
82 - if ($enable_admin_panel_dark_mode) {
83 - add_action('admin_bar_menu', array($this, 'darkify_admin_bar_switch'), 9999);
84 - add_action('admin_print_scripts', array($this, 'darkify_admin_header_script'), 1);
85 - add_action('admin_footer', array($this, 'darkify_admin_footer_script'));
77 + // Block Editor Dark Mode is INDEPENDENT of Admin Panel Dark Mode:
78 + // the block editor darkens whenever Block Editor Dark Mode is on,
79 + // even if the rest of wp-admin stays light. Every other admin screen
80 + // still keys on Admin Panel Dark Mode.
81 + $needs_engine = $is_block_editor
82 + ? (bool) $block_editor_dark_mode
83 + : (bool) $enable_admin_panel_dark_mode;
84 +
85 + // Darkify's own settings/help screens additionally load it even while
86 + // the option is OFF, so flipping "Admin Panel Dark Mode" in the React
87 + // admin can show the admin-bar icon (and have it actually work)
88 + // immediately instead of only after a reload. Loading it here cannot
89 + // darken anything on its own: both the FOUC snippet in
90 + // header_script.php and darkify_check_preloading() in client_main.js
91 + // are gated on `darkify_admin_panel_dark_enabled`.
92 + if ($needs_engine || $this->is_darkify_spa_page()) {
93 + add_action('admin_bar_menu', array($this, 'darkify_admin_bar_switch'), 9999);
94 + add_action('admin_print_scripts', array($this, 'darkify_admin_header_script'), 1);
95 + add_action('admin_footer', array($this, 'darkify_admin_footer_script'));
96 + }
97 + });
98 +
99 + new ReviewNotice();
100 + new DBUpdates();
101 + if (! defined('THEMEATELIER_OFFER_BANNER_LOADED')) {
102 + define('THEMEATELIER_OFFER_BANNER_LOADED', true);
103 + ThemeAtelier_Offer_Banner::instance();
86 104 }
87 105
88 - add_action( 'admin_menu', array( $this, 'admin_menu' ), 10 );
106 + // Building the schema is what evaluates every `__()` in
107 + // src/Admin/Views/*.php, so the text domain has to be registered first —
108 + // Darkify::load_textdomain() runs on this same hook at priority 1.
89 109 add_action('after_setup_theme', array($this, 'init_components'));
90 110 add_filter('autoptimize_filter_js_exclude', array($this, 'darkify_exclude_js_from_cache_plugins'));
111 +
112 + /*
113 + * "No-conflict mode" compatibility.
114 + *
115 + * Fluent Forms, FluentCRM, Fluent Booking, FluentSMTP, WP Social Ninja
116 + * and friends dequeue *every* script served from wp-content/plugins on
117 + * their own admin screens (Fluent Forms: app/Hooks/actions.php, on
118 + * `wp_print_scripts` priority 1). That runs after `admin_enqueue_scripts`,
119 + * so it strips Darkify's engine while the admin-bar node — printed by
120 + * `admin_bar_menu` and untouched by the dequeue — keeps its inline
121 + * onclick, producing "darkify_switch_trigger is not defined" and a dead
122 + * toggle on e.g. admin.php?page=fluent_forms.
123 + *
124 + * Two layers: the published allow-list filters where a plugin offers one,
125 + * and a generic re-enqueue after their sweep for the ones that don't.
126 + */
127 + add_filter('fluentform/exclude_js_slugs_from_dequeue', array($this, 'darkify_no_conflict_allow_slugs'));
128 + add_filter('fluent_crm_asset_listed_slugs', array($this, 'darkify_no_conflict_allow_slugs'));
129 + add_filter('fluent_booking/asset_listed_slugs', array($this, 'darkify_no_conflict_allow_slugs'));
130 + add_action('wp_print_scripts', array($this, 'darkify_restore_no_conflict_scripts'), 100);
131 + add_action('wp_print_styles', array($this, 'darkify_restore_no_conflict_styles'), 100);
132 +
91 133 add_action('admin_init', [$this, 'load_classic_editor_scripts']);
92 - new ReviewNotice();
93 - new DBUpdates();
94 - if ( ! defined( 'THEMEATELIER_OFFER_BANNER_LOADED' ) ) {
95 - define( 'THEMEATELIER_OFFER_BANNER_LOADED', true );
96 - ThemeAtelier_Offer_Banner::instance();
97 - }
98 -
99 - $active_plugins = get_option('active_plugins');
134 + add_filter('admin_footer_text', array($this, 'darkify_admin_footer'), 1, 2);
135 + $active_plugins = get_option('active_plugins');
100 136 foreach ($active_plugins as $active_plugin) {
101 137 $_temp = strpos($active_plugin, 'darkify.php');
102 138 if (false != $_temp) {
103 139 add_filter('plugin_action_links_' . DARKIFY_BASENAME, array($this, 'add_plugin_action_links'), 10, 2);
104 - add_filter( 'plugin_row_meta', array( $this, 'after_darkify_row_meta' ), 10, 4 );
140 + add_filter('plugin_row_meta', array($this, 'after_darkify_row_meta'), 10, 4);
105 141 }
106 142 }
107 - add_filter( 'admin_footer_text', array( $this, 'darkify_admin_footer' ), 1, 2 );
108 143 }
109 144
110 - public function init_components() {
145 + public function init_components()
146 + {
111 147 DarkifyOptions::options('darkify');
148 + $this->seed_default_options();
112 149 }
113 150
151 + /**
152 + * Write the schema's defaults into the `darkify` option on a fresh install.
153 + *
154 + * Nothing used to create that option until the first save in the admin. Up to
155 + * that point the two halves of the plugin disagreed: the React admin resolved
156 + * each field from the schema (so the Switch Toggler showed Orbit, the declared
157 + * default), while the frontend read the raw option — found nothing — and fell
158 + * back to the literal in its own template, rendering the Classic switcher. Same
159 + * class of mismatch for every other field whose template fallback differs from
160 + * its declared default.
161 + *
162 + * Seeding makes the stored values the single source both sides read, so a site
163 + * ships with exactly what the admin displays before anyone touches Settings.
164 + *
165 + * Runs on `init` (every request, admin and frontend), right after
166 + * the config classes have registered their sections, and is a no-op once the
167 + * option exists — so it never overwrites a user's saved settings, and an
168 + * upgrade from an older version is left alone.
169 + */
170 + private function seed_default_options()
171 + {
172 + $existing = get_option('darkify');
173 + if (is_array($existing) && ! empty($existing)) {
174 + return;
175 + }
176 +
177 + $defaults = SchemaDefaults::for_option('darkify');
178 + if (empty($defaults)) {
179 + return;
180 + }
181 +
182 + // `update_option` rather than `add_option`: an install that already holds
183 + // an empty/corrupt value (a stray '' or false) gets seeded too.
184 + update_option('darkify', $defaults);
185 + }
186 +
187 + /**
188 + * Whether the current screen is one of Darkify's own React SPA screens
189 + * (Settings / Get Help) — the only place the Admin Panel Dark Mode option can
190 + * be toggled, and therefore the only place that needs the engine loaded up
191 + * front so the admin-bar icon can react instantly.
192 + */
193 + public function is_darkify_spa_page(): bool
194 + {
195 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen check.
196 + $page = isset($_GET['page']) ? \sanitize_key(\wp_unslash($_GET['page'])) : '';
197 + return \in_array($page, Assets::SPA_SLUGS, true);
198 + }
199 +
114 200 public function darkify_exclude_js_from_cache_plugins($excludes)
115 201 {
116 - $excludes .= ',client_main.js,client_main.min.js';
202 + // Dark Reader proxies document.styleSheets and injects its own <style>
203 + // elements; an optimizer that concatenates or defers it changes when it
204 + // sees the page's sheets, which shows up as a half-darkened admin.
205 + $excludes .= ',client_main.js,client_main.min.js'
206 + . ',darkreader.js,darkreader.min.js'
207 + . ',admin_darkreader.js,admin_darkreader.min.js';
117 208 return $excludes;
118 209 }
119 210
120 - public function enqueue_styles($hook)
211 + public function enqueue_styles()
121 212 {
122 - if($hook === 'toplevel_page_darkify') {
123 - wp_enqueue_style('darkify-help-page', DARKIFY_DIR_URL . 'src/Admin/HelpPage/assets/css/help-page' . $this->min . '.css', array(), DARKIFY_VERSION, 'all');
124 - }
125 -
126 213 $options = get_option('darkify');
127 214 $enable_admin_panel_dark_mode = isset($options["enable_admin_panel_dark_mode"]) ? $options["enable_admin_panel_dark_mode"] : false;
128 - if ($enable_admin_panel_dark_mode) {
215 + // Block Editor Dark Mode darkens the editor independently of Admin Panel
216 + // Dark Mode, so the stylesheet must load there too.
217 + $block_editor_dark_mode = isset($options['block_editor_dark_mode']) ? $options['block_editor_dark_mode'] : false;
218 + $screen = \function_exists('get_current_screen') ? \get_current_screen() : null;
219 + $is_block_editor = $screen && \method_exists($screen, 'is_block_editor') && $screen->is_block_editor();
220 + $allow_admin_dark = $enable_admin_panel_dark_mode || ($is_block_editor && $block_editor_dark_mode);
221 + // Darkify's own screens always get the stylesheet so the admin-bar icon
222 + // can be revealed the moment the option is switched on. It only styles
223 + // `.darkify_*` classes, so it is inert until the engine adds them.
224 + if (($allow_admin_dark || $this->is_darkify_spa_page()) && $this->darkify_is_dark_mode_allowed()) {
129 225 if (!wp_style_is('darkify-admin-switch', 'enqueued')) {
130 - wp_enqueue_style('darkify-admin-switch', DARKIFY_DIR_URL . 'src/assets/css/client_main' . $this->min . '.css', array(), DARKIFY_VERSION, 'all');
226 + wp_enqueue_style('darkify-admin-switch', DARKIFY_DIR_URL . 'src/assets/css/client_main' . $this->min . '.css', array(), $this->darkify_asset_version('src/assets/css/client_main' . $this->min . '.css'), 'all');
131 227 }
132 228 }
133 229 // Review notice CSS
134 230 wp_enqueue_style('darkify-review-notice', DARKIFY_DIR_URL . 'src/Admin/ReviewNotice/assets/css/review-notice' . $this->min . '.css', array(), DARKIFY_VERSION, 'all');
@@ -137,101 +233,252 @@
137 233 public function enqueue_scripts($hook)
138 234 {
139 235 $options = get_option('darkify');
140 236 $enable_admin_panel_dark_mode = isset($options["enable_admin_panel_dark_mode"]) ? $options["enable_admin_panel_dark_mode"] : false;
141 - if ($enable_admin_panel_dark_mode) {
142 - wp_enqueue_script('darkify-admin-client-main', DARKIFY_DIR_URL . 'src/assets/js/client_main' . $this->min . '.js', array(), DARKIFY_VERSION);
237 + $is_spa = $this->is_darkify_spa_page();
238 + if (! $this->darkify_is_dark_mode_allowed()) {
239 + return;
143 240 }
144 241
145 - if($hook === 'toplevel_page_darkify') {
146 - wp_enqueue_script('darkify-help-page', DARKIFY_DIR_URL . 'src/Admin/HelpPage/assets/js/help-page' . $this->min . '.js', array('darkify-plugins'), DARKIFY_VERSION, true);
242 + // Mirror the `current_screen` engine-load rule exactly, so the engine is
243 + // only enqueued where its inline config (header_script.php) is ALSO
244 + // printed. Block Editor Dark Mode is independent of Admin Panel Dark
245 + // Mode: the block editor loads the engine when Block Editor Dark Mode is
246 + // on (regardless of Admin Panel Dark Mode); every other admin screen
247 + // keys on Admin Panel Dark Mode. (A SPA screen always gets the config.)
248 + if (! $is_spa) {
249 + $screen = \function_exists('get_current_screen') ? \get_current_screen() : null;
250 + $is_block_editor = $screen && \method_exists($screen, 'is_block_editor') && $screen->is_block_editor();
251 + $block_editor_dark_mode = isset($options['block_editor_dark_mode']) ? $options['block_editor_dark_mode'] : false;
252 + $allow = $is_block_editor ? (bool) $block_editor_dark_mode : (bool) $enable_admin_panel_dark_mode;
253 + if (! $allow) {
254 + return;
255 + }
147 256 }
257 +
258 + /*
259 + * wp-admin runs the Dark Reader engine, NOT the per-element classifier in
260 + * client_main.js. That engine walks every element and reads
261 + * getComputedStyle on each, then re-walks on DOM mutation; wp-admin (the
262 + * block editor especially) mutates continuously, so it never settles and
263 + * the editor locks up. Dark Reader works per stylesheet instead, so the
264 + * cost does not scale with element churn at all.
265 + *
266 + * The frontend is untouched and still runs client_main.js.
267 + * See src/assets/js/admin_darkreader.js for the full rationale.
268 + */
269 + $darkify_lib_rel = 'src/assets/js/vendor/darkreader.min.js';
270 + $darkify_engine_rel = 'src/assets/js/admin_darkreader' . $this->min . '.js';
271 + $darkify_lib_url = DARKIFY_DIR_URL . $darkify_lib_rel;
272 +
273 + /*
274 + * Both in <head> (in_footer = false), deliberately. Dark Reader does not
275 + * need a parsed body to start, and enabling it before first paint is what
276 + * keeps the admin from flashing light and then darkening.
277 + */
278 + wp_enqueue_script(
279 + 'darkify-darkreader',
280 + $darkify_lib_url,
281 + array(),
282 + $this->darkify_asset_version($darkify_lib_rel),
283 + false
284 + );
285 +
286 + /*
287 + * The block editor canvas and the classic editor's TinyMCE body are
288 + * separate documents, and Dark Reader binds the realm it was loaded into.
289 + * The engine injects its own copy into each same-origin frame, so it
290 + * needs a URL it can point a <script> at from inside that frame.
291 + *
292 + * Darkify's own React settings screens are excluded from Dark Reader:
293 + * they ship a full dark theme of their own (`.dark` in
294 + * darkify-react/src/index.css) which also covers the surrounding wp-admin
295 + * chrome, so darkening them again produces a double-dark UI — and because
296 + * Dark Reader 4.9 cannot parse the `oklch()` values those tokens use, the
297 + * unresolved properties settle on `--destructive` and paint card borders
298 + * and toggles red. The engine still loads there: it owns the toggle the
299 + * admin-bar icon calls, and the SPA mirrors the class onto `.dark` itself.
300 + */
301 + wp_add_inline_script(
302 + 'darkify-darkreader',
303 + 'window.darkifyDarkReaderSrc = ' . wp_json_encode($darkify_lib_url) . ';'
304 + . 'window.darkifyAdminSelfThemed = ' . ($is_spa ? 'true' : 'false') . ';',
305 + 'before'
306 + );
307 +
308 + wp_enqueue_script(
309 + 'darkify-admin-engine',
310 + DARKIFY_DIR_URL . $darkify_engine_rel,
311 + array('darkify-darkreader'),
312 + $this->darkify_asset_version($darkify_engine_rel),
313 + false
314 + );
315 +
316 + $this->engine_enqueued = true;
148 317 }
149 318
150 - /*
319 + /**
320 + * Add Darkify to a no-conflict allow-list.
321 + *
322 + * Both filter shapes this is attached to pass an array of substrings matched
323 + * against the script `src`, so the plugin folder name is the right token.
324 + *
325 + * @param array $slugs Allowed slugs.
326 + *
327 + * @return array
328 + */
329 + public function darkify_no_conflict_allow_slugs($slugs)
330 + {
331 + if (! is_array($slugs)) {
332 + return $slugs;
333 + }
334 +
335 + $slugs[] = 'darkify';
336 +
337 + return array_unique($slugs);
338 + }
339 +
340 + /**
341 + * Put the admin engine back after a no-conflict sweep dropped it.
342 + *
343 + * Runs on `wp_print_scripts` at 100 — after the sweeps, which all hook it at
344 + * 1 — and before `do_items()`, so a re-enqueue here still prints. Gated on
345 + * `$engine_enqueued`, so it never resurrects a script Darkify itself chose
346 + * not to load (or dequeued in admin_dequeue_for_specefic_pages).
347 + */
348 + public function darkify_restore_no_conflict_scripts()
349 + {
350 + if (! $this->engine_enqueued || ! is_admin()) {
351 + return;
352 + }
353 +
354 + foreach (array('darkify-darkreader', 'darkify-admin-engine') as $handle) {
355 + if (wp_script_is($handle, 'registered') && ! wp_script_is($handle, 'enqueued')) {
356 + wp_enqueue_script($handle);
357 + }
358 + }
359 + }
360 +
361 + /**
362 + * Same sweep, styles side: the engine adds `.darkify_dark_mode_enabled` and
363 + * the switch classes, and client_main.css is what they mean. Losing only the
364 + * stylesheet leaves a half-styled admin bar icon.
365 + */
366 + public function darkify_restore_no_conflict_styles()
367 + {
368 + if (! $this->engine_enqueued || ! is_admin()) {
369 + return;
370 + }
371 +
372 + if (wp_style_is('darkify-admin-switch', 'registered') && ! wp_style_is('darkify-admin-switch', 'enqueued')) {
373 + wp_enqueue_style('darkify-admin-switch');
374 + }
375 + }
376 +
377 + /**
378 + * Cache-busting version for a bundled asset: the plugin version plus the
379 + * file's mtime, so rebuilding client_main.js/.css (which does NOT bump the
380 + * plugin version) still changes the URL and browsers fetch the new file
381 + * instead of serving a stale one. Mirrors Admin\Assets::asset_version().
382 + */
383 + public function darkify_asset_version(string $relative_path): string
384 + {
385 + $abs = DARKIFY_PATH . $relative_path;
386 + $mtime = \is_readable($abs) ? \filemtime($abs) : false;
387 + return $mtime ? DARKIFY_VERSION . '.' . $mtime : DARKIFY_VERSION;
388 + }
389 +
390 + /*
151 391 Dequeue Darkify scripts and styles on specific admin pages (e.g., MainWP pages)
152 392 */
153 -
393 +
154 394 public function admin_dequeue_for_specefic_pages()
155 395 {
156 - $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
157 - $dequeue_pages = array(
158 - 'mainwp_tab',
159 - 'managesites',
160 - 'ManageClients',
161 - 'CostSummary',
162 - 'InsightsOverview',
163 - 'Extensions',
164 - 'RESTAPI',
165 - 'mainwp-setup',
166 - 'ThemesManage',
167 - 'PluginsManage',
168 - 'InsightsManage',
169 - 'ManageGroups',
170 - 'UpdatesManage',
171 - 'PluginsInstall',
172 - 'PluginsAutoUpdate',
173 - 'PluginsIgnore',
174 - 'PluginsAbandoned',
175 - 'PluginsIgnoredAbandoned',
176 - 'ThemesInstall',
177 - 'ThemesAutoUpdate',
178 - 'ThemesIgnore',
179 - 'ThemesAbandoned',
180 - 'ThemesIgnoredAbandoned',
181 - 'UserBulkManage',
182 - 'UserBulkAdd',
183 - 'BulkImportUsers',
184 - 'UpdateAdminPasswords',
185 - 'PostBulkManage',
186 - 'Extensions-Mainwp-Backups',
187 - 'Extensions-Mainwp-Security',
188 - 'Extensions-Mainwp-Analytics',
189 - 'Mainwp-Monitoring',
190 - 'Extensions-Mainwp-Agency',
191 - 'Extensions-Mainwp-Administrative',
192 - 'Extensions-Mainwp-Development',
193 - 'Extensions-Mainwp-Performance',
194 - 'ClientAddNew',
195 - 'ClientImport',
196 - 'ManageCostTracker',
197 - 'CostTrackerAdd',
198 - 'AddApiKeys',
199 - );
396 + $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';
397 + $dequeue_pages = array(
398 + 'mainwp_tab',
399 + 'managesites',
400 + 'ManageClients',
401 + 'CostSummary',
402 + 'InsightsOverview',
403 + 'Extensions',
404 + 'RESTAPI',
405 + 'mainwp-setup',
406 + 'ThemesManage',
407 + 'PluginsManage',
408 + 'InsightsManage',
409 + 'ManageGroups',
410 + 'UpdatesManage',
411 + 'PluginsInstall',
412 + 'PluginsAutoUpdate',
413 + 'PluginsIgnore',
414 + 'PluginsAbandoned',
415 + 'PluginsIgnoredAbandoned',
416 + 'ThemesInstall',
417 + 'ThemesAutoUpdate',
418 + 'ThemesIgnore',
419 + 'ThemesAbandoned',
420 + 'ThemesIgnoredAbandoned',
421 + 'UserBulkManage',
422 + 'UserBulkAdd',
423 + 'BulkImportUsers',
424 + 'UpdateAdminPasswords',
425 + 'PostBulkManage',
426 + 'Extensions-Mainwp-Backups',
427 + 'Extensions-Mainwp-Security',
428 + 'Extensions-Mainwp-Analytics',
429 + 'Mainwp-Monitoring',
430 + 'Extensions-Mainwp-Agency',
431 + 'Extensions-Mainwp-Administrative',
432 + 'Extensions-Mainwp-Development',
433 + 'Extensions-Mainwp-Performance',
434 + 'ClientAddNew',
435 + 'ClientImport',
436 + 'ManageCostTracker',
437 + 'CostTrackerAdd',
438 + 'AddApiKeys',
439 + );
200 440
201 441
202 - if ( in_array( $page, $dequeue_pages, true ) ) {
203 - wp_deregister_style( 'darkify-admin-switch' );
204 - wp_dequeue_script( 'darkify-admin-client-main' );
442 + if (in_array($page, $dequeue_pages, true)) {
443 + wp_deregister_style('darkify-admin-switch');
444 + wp_dequeue_script('darkify-admin-client-main');
445 + wp_dequeue_script('darkify-admin-engine');
446 + wp_dequeue_script('darkify-darkreader');
447 + // Keep darkify_restore_no_conflict_scripts() from undoing this.
448 + $this->engine_enqueued = false;
449 + }
205 450 }
206 -
207 - }
208 451
209 - /**
210 - * Review Text.
211 - *
212 - * @param string $text text.
213 - *
214 - * @return string
215 - */
216 - public function darkify_admin_footer( $text ) {
452 + /**
453 + * Replace wp-admin's footer credit with Darkify's review request, on
454 + * Darkify's own screens only.
455 + *
456 + * Screens are matched by page slug (`is_darkify_spa_page()`) rather than by
457 + * a hardcoded screen id, so both the Settings and Get Help SPA screens match
458 + * and the check keeps working if the menu is ever retitled. The slug list is
459 + * the same source of truth the assets already load from.
460 + *
461 + * @param string $text Existing footer text.
462 + *
463 + * @return string
464 + */
465 + public function darkify_admin_footer($text)
466 + {
467 + if (! $this->is_darkify_spa_page()) {
468 + return $text;
469 + }
217 470
218 - $screen = get_current_screen();
219 -
220 - if ( 'toplevel_page_darkify' === $screen->id ) {
221 - $text = sprintf(
222 - /* translators: 1: start strong tag, 2: close strong tag. 3: start link 4: close link */
223 - __( '<i>Enjoying %1$sDarkify?%2$s Please rate us %3$sWordPress.org%4$s. Your positive feedback will help us grow more. Thank you! 😊</i>', 'darkify' ),
224 - '<strong>',
225 - '</strong>',
226 - '<span class="greet-footer-text-star">★★★★★</span> <a href="https://wordpress.org/support/plugin/darkify/reviews/#new-post" target="_blank">',
227 - '</a>'
228 - );
229 - }
471 + return sprintf(
472 + /* translators: 1: start strong tag, 2: close strong tag. 3: start link 4: close link */
473 + __('<i>Enjoying %1$sDarkify?%2$s Please rate us %3$sWordPress.org%4$s. Your positive feedback will help us grow more. Thank you! 😊</i>', 'darkify'),
474 + '<strong>',
475 + '</strong>',
476 + '<span class="greet-footer-text-star">★★★★★</span> <a href="https://wordpress.org/support/plugin/darkify/reviews/#new-post" target="_blank" rel="noopener noreferrer">',
477 + '</a>'
478 + );
479 + }
230 480
231 - return $text;
232 - }
233 -
234 481 // Plugin settings in plugin list
235 482 public function add_plugin_action_links(array $links)
236 483 {
237 484 $new_links = array(
@@ -236,99 +483,122 @@
236 483 {
237 484 $new_links = array(
238 485 sprintf('<a href="' . esc_url(admin_url('admin.php?page=darkify')) . '">' . esc_html__('Settings', 'darkify') . '</a>'),
239 486 sprintf('<a target="_blank" href="https://wordpress.org/support/plugin/darkify/">' . esc_html__('Support', 'darkify') . '</a>'),
240 - // sprintf('<a style="font-weight: bold;color:#171717" target="_blank" href="https://darkifywp.com/pricing/?ref=1">' . esc_html__('Go Pro', 'darkify') . '</a>'),
241 487 );
242 488
243 - $links[] = sprintf( '<a style="font-weight: bold;color:#35b747" target="_blank" href="https://darkifywp.com/pricing/?utm_source=darkify_plugin&utm_medium=action_link&utm_campaign=regular">%s</a>', esc_html__( 'Go Pro!', 'darkify' ));
244 -
489 + $links[] = sprintf('<a style="font-weight: bold;color:#35b747" target="_blank" href="https://darkifywp.com/pricing/?utm_source=darkify_plugin&utm_medium=action_link&utm_campaign=regular">%s</a>', esc_html__('Go Pro!', 'darkify'));
490 +
245 491 return array_merge($new_links, $links);
246 492 }
247 493
248 494 /**
249 - * Add plugin row meta link.
250 - *
251 - * @since 2.0
252 - *
253 - * @param array $plugin_meta .
254 - * @param string $file .
255 - *
256 - * @return array
257 - */
258 - public function after_darkify_row_meta( $plugin_meta, $file ) {
495 + * Add plugin row meta link.
496 + *
497 + * @since 2.0
498 + *
499 + * @param array $plugin_meta .
500 + * @param string $file .
501 + *
502 + * @return array
503 + */
504 + public function after_darkify_row_meta($plugin_meta, $file)
505 + {
259 506
260 - if ( DARKIFY_BASENAME === $file ) {
261 - $plugin_meta[] = '<a href="'.DARKIFY_DEMO_URL.'" target="_blank">' . __( 'Live Demo', 'darkify' ) . '</a>';
262 - }
507 + if (DARKIFY_BASENAME === $file) {
508 + $plugin_meta[] = '<a href="' . DARKIFY_DEMO_URL . '" target="_blank">' . __('Live Demo', 'darkify') . '</a>';
509 + }
263 510
264 - return $plugin_meta;
265 - }
511 + return $plugin_meta;
512 + }
266 513
267 -
268 514 public function darkify_admin_bar_switch($wp_admin_bar)
269 515 {
270 - $args = array(
516 + if (! $this->darkify_is_dark_mode_allowed()) {
517 + return;
518 + }
519 +
520 + /*
521 + * The block editor already carries its own toggle in the top toolbar
522 + * (see Frontend/Templates/footer_script.php), firing the same
523 + * darkify_switch_trigger() this node would — two controls for one state,
524 + * side by side.
525 + *
526 + * The toolbar one is the keeper, not this one: Gutenberg hides the WP
527 + * admin bar in fullscreen mode, which is the default, so this node is
528 + * simply absent for most people editing a post. Dropping the toolbar
529 + * button instead would leave them no toggle at all.
530 + *
531 + * The classic editor is deliberately not included here. Its TinyMCE moon
532 + * toggles only the content frame, while this switch toggles the whole
533 + * admin — two different controls, so both belong on screen.
534 + */
535 + $screen = \function_exists('get_current_screen') ? \get_current_screen() : null;
536 + if ($screen && \method_exists($screen, 'is_block_editor') && $screen->is_block_editor()) {
537 + return;
538 + }
539 +
540 + $options = get_option('darkify');
541 + $enable_admin_panel_dark_mode = isset($options['enable_admin_panel_dark_mode']) ? $options['enable_admin_panel_dark_mode'] : false;
542 +
543 + // Everywhere but Darkify's own screens the node is only registered when
544 + // the option is on, so it is always visible. On the SPA screens it is
545 + // registered regardless and starts hidden while the option is off — the
546 + // React admin then just flips this class (see WpAdminBarBridge), which is
547 + // what makes the icon appear/disappear without a reload.
548 + $classes = 'darkify_admin_bar_switch_container';
549 + if (! $enable_admin_panel_dark_mode) {
550 + $classes .= ' darkify_admin_bar_hidden';
551 + }
552 +
553 + $wp_admin_bar->add_node(array(
271 554 'parent' => 'top-secondary',
272 555 'id' => 'darkify_admin_bar_switch_container',
273 556 'meta' => array(
274 - 'class' => 'darkify_admin_bar_switch_container',
275 - 'onclick' => 'darkify_switch_trigger()',
276 - )
277 - );
278 - $wp_admin_bar->add_node($args);
557 + 'class' => $classes,
558 + // Guarded call: a plugin's "no-conflict mode" can still strip the
559 + // engine on its own screens (see the compatibility hooks in the
560 + // constructor). A missing function should be an inert click, not
561 + // an uncaught ReferenceError in every visitor's console.
562 + // No quotes in the expression: the admin bar escapes `meta`
563 + // attributes, and the escaped quotes come back as literal
564 + // entities inside the handler ("Invalid or unexpected token").
565 + 'onclick' => 'window.darkify_switch_trigger&&window.darkify_switch_trigger()',
566 + ),
567 + ));
279 568 }
280 569
281 - public function darkify_admin_header_script()
570 + public function darkify_is_dark_mode_allowed()
282 571 {
283 - include_once DarkifyUtils::darkify_locate_template('header_script.php');
572 +
573 + $options = get_option('darkify');
574 + $options = is_array($options) ? $options : array();
575 + // Guarded read: `disallowed_admin_pages` has no schema default, so the
576 + // React settings save doesn't persist it until the field is used. Reading
577 + // it unguarded would emit an "Undefined array key" warning on every
578 + // dark-mode hook (see the Pro plugin's Admin.php for the full story).
579 + $disallowed_admin_pages = isset($options['disallowed_admin_pages']) ? $options['disallowed_admin_pages'] : '';
580 + /* Disable on Disallowed Admin Plugins */
581 + if ($this->utils->isRestrictedByDisallowedAdminPages($disallowed_admin_pages)) {
582 + return False;
583 + }
584 + return True;
284 585 }
285 586
286 - public function darkify_admin_footer_script()
587 + public function darkify_admin_header_script()
287 588 {
288 - include_once DarkifyUtils::darkify_locate_template('footer_script.php');
589 + if ($this->darkify_is_dark_mode_allowed()) {
590 + include_once DarkifyUtils::darkify_locate_template('header_script.php');
591 + }
289 592 }
290 593
291 - public function admin_menu()
594 + public function darkify_admin_footer_script()
292 595 {
293 - // global $menu, $admin_page_hooks;
294 - $capability = apply_filters( 'darkify_ui_permission', 'manage_options' );
295 - $menu_icon = 'data:image/svg+xml;base64,' . base64_encode('<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
296 - <path d="M23.5 1.5C23.5 0.671573 22.8284 0 22 0C21.1716 0 20.5 0.671573 20.5 1.5V4.5C20.5 5.32843 21.1716 6 22 6C22.8284 6 23.5 5.32843 23.5 4.5V1.5Z" fill="white" />
297 - <path d="M34 22C34 28.6274 28.6274 34 22 34C15.3726 34 10 28.6274 10 22C10 15.3726 15.3726 10 22 10C28.6274 10 34 15.3726 34 22Z" fill="white" />
298 - <path d="M22 38C22.8284 38 23.5 38.6716 23.5 39.5V42.5C23.5 43.3284 22.8284 44 22 44C21.1716 44 20.5 43.3284 20.5 42.5V39.5C20.5 38.6716 21.1716 38 22 38Z" fill="white" />
299 - <path d="M10.6863 10.6863C10.1005 11.2721 9.15074 11.2721 8.56495 10.6863L6.44363 8.56498C5.85784 7.97919 5.85784 7.02945 6.44363 6.44366C7.02942 5.85787 7.97916 5.85787 8.56495 6.44366L10.6863 8.56498C11.2721 9.15077 11.2721 10.1005 10.6863 10.6863Z" fill="white" />
300 - <path d="M35.435 37.5564C36.0208 38.1421 36.9705 38.1421 37.5563 37.5564C38.1421 36.9706 38.1421 36.0208 37.5563 35.435L35.435 33.3137C34.8492 32.7279 33.8995 32.7279 33.3137 33.3137C32.7279 33.8995 32.7279 34.8493 33.3137 35.435L35.435 37.5564Z" fill="white" />
301 - <path d="M10.6863 33.3137C11.2721 33.8995 11.2721 34.8492 10.6863 35.435L8.56495 37.5563C7.97916 38.1421 7.02942 38.1421 6.44363 37.5563C5.85784 36.9706 5.85784 36.0208 6.44363 35.435L8.56495 33.3137C9.15074 32.7279 10.1005 32.7279 10.6863 33.3137Z" fill="white" />
302 - <path d="M37.5563 8.56496C38.1421 7.97918 38.1421 7.02943 37.5563 6.44364C36.9705 5.85786 36.0208 5.85786 35.435 6.44364L33.3137 8.56496C32.7279 9.15075 32.7279 10.1005 33.3137 10.6863C33.8995 11.2721 34.8492 11.2721 35.435 10.6863L37.5563 8.56496Z" fill="white" />
303 - <path d="M6 22C6 22.8284 5.32843 23.5 4.5 23.5H1.5C0.671573 23.5 0 22.8284 0 22C0 21.1716 0.671573 20.5 1.5 20.5H4.5C5.32843 20.5 6 21.1716 6 22Z" fill="white" />
304 - <path d="M42.5 23.5C43.3284 23.5 44 22.8284 44 22C44 21.1716 43.3284 20.5 42.5 20.5H39.5C38.6716 20.5 38 21.1716 38 22C38 22.8284 38.6716 23.5 39.5 23.5H42.5Z" fill="white" />
305 - </svg>');
306 - // This page will be under "Settings"
307 - add_menu_page(
308 - esc_html__('Darkify', 'darkify'),
309 - esc_html__('Darkify', 'darkify'),
310 - $capability,
311 - 'darkify',
312 - [$this, 'darkify_settings'],
313 - $menu_icon,
314 - 60
315 - );
316 - add_submenu_page(
317 - 'darkify',
318 - __(
319 - 'Upgrade To Pro! 👑',
320 - 'darkify'
321 - ),
322 - sprintf('<span style="color: #35b747;font-weight:600;" class="darkify-get-pro-text">%s</span>', __('Upgrade To Pro! 👑', 'darkify')),
323 - 'manage_options',
324 - 'https://darkifywp.com/pricing/?utm_source=darkify_plugin&utm_medium=submenu_page&utm_campaign=regular'
325 - );
326 -
596 + if ($this->darkify_is_dark_mode_allowed()) {
597 + include_once DarkifyUtils::darkify_locate_template('footer_script.php');
598 + }
327 599 }
328 600
329 - public function darkify_settings() {}
330 -
331 601 /**
332 602 * Load classic editor scripts.
333 603 *
334 604 * @since 5.0.0
@@ -349,9 +619,12 @@
349 619 return;
350 620 }
351 621
352 622 add_filter('mce_external_plugins', function ($plugins) {
353 - $plugins['darkify_button'] = plugins_url('src/assets/js/admin-classic-editor.js', DARKIFY_FILE);
623 + // Honour the same debug switch as every other bundled asset; gulp
624 + // builds the .min beside it and this was the one enqueue still
625 + // pointing at the readable copy in production.
626 + $plugins['darkify_button'] = plugins_url('src/assets/js/admin-classic-editor' . $this->min . '.js', DARKIFY_FILE);
354 627 return $plugins;
355 628 });
356 629
357 630 add_filter('mce_buttons', function ($buttons) {