{{ plugin.name }}
+{{ plugin.description || 'No description provided.' }}
+ +diff --git a/.gitignore b/.gitignore index 2975e2a..9b2b663 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ app/node_modules/ app/dist/ +dist/ *.log .DS_Store Thumbs.db diff --git a/admin/assets/native-dark.css b/admin/assets/native-dark.css index 4606f52..791bf5e 100644 --- a/admin/assets/native-dark.css +++ b/admin/assets/native-dark.css @@ -163,9 +163,24 @@ html.aurora-dark.aurora-ready #wpbody-content .button-primary:hover { /* Nav tabs -------------------------------------------------------------- */ html.aurora-dark.aurora-ready #wpbody-content .nav-tab-wrapper, html.aurora-dark.aurora-ready #wpbody-content .wp-filter { + background: var(--aurora-card-bg); border-color: var(--aurora-border); } +html.aurora-dark.aurora-ready #wpbody-content .wp-filter .filter-links a { + color: var(--aurora-accent); +} + +html.aurora-dark.aurora-ready #wpbody-content .wp-filter .filter-links .current { + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .wp-filter .search-form input[type='search'] { + background: var(--aurora-bg-subtle); + border-color: var(--aurora-border); + color: var(--aurora-text); +} + html.aurora-dark.aurora-ready #wpbody-content .nav-tab { background: var(--aurora-bg-subtle); border-color: var(--aurora-border); @@ -251,14 +266,14 @@ html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .wrap > p { html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .aurora-native-card { background: var(--aurora-card-bg); border: 1px solid var(--aurora-card-border); - border-radius: 16px; - padding: 20px 24px; + border-radius: 20px; + padding: 20px; margin-bottom: 16px; } html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .aurora-native-card h2 { - font-size: 1.05rem; + font-size: 1.0625rem; font-weight: 600; - margin: 4px 0 8px; + margin: 0 0 18px; color: var(--aurora-text); border: 0; padding-top: 0; @@ -280,3 +295,85 @@ html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .aurora-nativ html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .aurora-native-card .button:hover { background: var(--aurora-accent-soft); } + +/* --------------------------------------------------------------------------- + * Modals & popups (media library, plugin/theme install details) — these are + * Backbone/thickbox views appended to
, outside #wpbody-content, so + * every rule above misses them entirely. Scoped to html.aurora-dark only + * (no #wpbody-content ancestor requirement) since that's the only anchor + * available at body level. + * ------------------------------------------------------------------------ */ +html.aurora-dark.aurora-ready .media-modal, +html.aurora-dark.aurora-ready .media-frame, +html.aurora-dark.aurora-ready .media-frame-title, +html.aurora-dark.aurora-ready .media-frame-router, +html.aurora-dark.aurora-ready .media-frame-content, +html.aurora-dark.aurora-ready .media-frame-toolbar, +html.aurora-dark.aurora-ready .media-sidebar, +html.aurora-dark.aurora-ready #TB_window, +html.aurora-dark.aurora-ready #TB_ajaxContent, +html.aurora-dark.aurora-ready .plugin-details-modal, +html.aurora-dark.aurora-ready .plugin-install-php .plugin-card { + background: var(--aurora-card-bg); + color: var(--aurora-text); + border-color: var(--aurora-card-border); +} + +html.aurora-dark.aurora-ready .media-modal h1, +html.aurora-dark.aurora-ready .media-modal h2, +html.aurora-dark.aurora-ready .media-modal h3, +html.aurora-dark.aurora-ready .media-modal label, +html.aurora-dark.aurora-ready .media-frame-title h1, +html.aurora-dark.aurora-ready #TB_ajaxContent, +html.aurora-dark.aurora-ready #TB_ajaxContent p, +html.aurora-dark.aurora-ready #TB_ajaxContent h1, +html.aurora-dark.aurora-ready .plugin-card-top, +html.aurora-dark.aurora-ready .plugin-card .name, +html.aurora-dark.aurora-ready .plugin-card .desc { + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready .media-frame-menu, +html.aurora-dark.aurora-ready .media-menu, +html.aurora-dark.aurora-ready .attachments-browser .media-toolbar, +html.aurora-dark.aurora-ready .media-sidebar, +html.aurora-dark.aurora-ready .plugin-card-bottom { + background: var(--aurora-bg-subtle); + border-color: var(--aurora-border); +} + +html.aurora-dark.aurora-ready .attachments-browser .attachments, +html.aurora-dark.aurora-ready .media-frame-content { + background: var(--aurora-app-bg-solid); +} + +html.aurora-dark.aurora-ready .media-modal input, +html.aurora-dark.aurora-ready .media-modal textarea, +html.aurora-dark.aurora-ready .media-modal select, +html.aurora-dark.aurora-ready #TB_ajaxContent input, +html.aurora-dark.aurora-ready #TB_ajaxContent textarea, +html.aurora-dark.aurora-ready #TB_ajaxContent select { + background: var(--aurora-bg-subtle); + border-color: var(--aurora-border); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready .media-modal-backdrop, +html.aurora-dark.aurora-ready #TB_overlay { + background: rgba(0, 0, 0, 0.7); +} + +/* Plugin/theme card grids on plugins.php / plugin-install.php / themes.php */ +html.aurora-dark.aurora-ready #wpbody-content .theme-browser .theme, +html.aurora-dark.aurora-ready #wpbody-content .plugin-card { + background: var(--aurora-card-bg); + border-color: var(--aurora-card-border); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .theme-browser .theme .theme-name, +html.aurora-dark.aurora-ready #wpbody-content .theme-browser .theme .theme-actions { + background: var(--aurora-card-bg); + color: var(--aurora-text); + border-color: var(--aurora-card-border); +} diff --git a/admin/src/App/AuroraAdmin.php b/admin/src/App/AuroraAdmin.php index 8c396f3..11b18ce 100644 --- a/admin/src/App/AuroraAdmin.php +++ b/admin/src/App/AuroraAdmin.php @@ -8,6 +8,7 @@ use AuroraAdmin\Pages\SettingsPage; use AuroraAdmin\Rest\DashboardData; use AuroraAdmin\Analytics\Analytics; use AuroraAdmin\Rest\AnalyticsData; +use AuroraAdmin\Rest\PluginsData; use AuroraAdmin\Posts\ScheduledDayFilter; use AuroraAdmin\WhiteLabel\WhiteLabel; use AuroraAdmin\Pages\SubApps; @@ -39,6 +40,7 @@ class AuroraAdmin new DashboardData(); new Analytics(); new AnalyticsData(); + new PluginsData(); new ScheduledDayFilter(); new WhiteLabel(); new SubApps(); diff --git a/admin/src/Options/Settings.php b/admin/src/Options/Settings.php index ff66d28..756609a 100644 --- a/admin/src/Options/Settings.php +++ b/admin/src/Options/Settings.php @@ -35,7 +35,12 @@ class Settings "schema" => [ "type" => "object", "properties" => [ - "dark_mode" => ["type" => "string"], + // Two producers write this key with different types: the + // toolbar's tri-state switcher sends 'light'/'dark'/'system' + // (string), the settings page's Dark Mode toggle sends a plain + // boolean. Leaving it untyped (falls through to + // additionalProperties) lets both save instead of one + // rejecting the other with a 400. "dashboard_card_order" => ["type" => "object"], ], "additionalProperties" => true, diff --git a/admin/src/Pages/SubApps.php b/admin/src/Pages/SubApps.php index 65de9bd..d5e6b7b 100644 --- a/admin/src/Pages/SubApps.php +++ b/admin/src/Pages/SubApps.php @@ -24,6 +24,7 @@ class SubApps "aurora-admin-roles" => ["title" => "Role Editor", "entry" => "role-editor"], "aurora-admin-activity-log" => ["title" => "Activity Log", "entry" => "activity-log"], "aurora-admin-database" => ["title" => "Database Explorer", "entry" => "database-explorer"], + "aurora-admin-plugins" => ["title" => "Installed Plugins", "entry" => "plugins"], ]; private static $hook_suffixes = []; @@ -38,11 +39,12 @@ class SubApps public static function register_menus() { foreach (self::PAGES as $slug => $page) { + $parent = $slug === "aurora-admin-plugins" ? "plugins.php" : "aurora-admin"; $hook = add_submenu_page( - "aurora-admin", + $parent, __($page["title"], "aurora-admin"), __($page["title"], "aurora-admin"), - "manage_options", + $slug === "aurora-admin-plugins" ? "activate_plugins" : "manage_options", $slug, [self::class, "render"] ); @@ -53,7 +55,7 @@ class SubApps public static function hide_from_native_submenu() { foreach (array_keys(self::PAGES) as $slug) { - remove_submenu_page("aurora-admin", $slug); + remove_submenu_page($slug === "aurora-admin-plugins" ? "plugins.php" : "aurora-admin", $slug); } } diff --git a/admin/src/Rest/PluginsData.php b/admin/src/Rest/PluginsData.php new file mode 100644 index 0000000..af8afc3 --- /dev/null +++ b/admin/src/Rest/PluginsData.php @@ -0,0 +1,248 @@ + "GET", + "callback" => [self::class, "list_plugins"], + "permission_callback" => function () { + return current_user_can("activate_plugins"); + }, + ]); + + register_rest_route("aurora-admin/v1", "/plugins/activate", [ + "methods" => "POST", + "callback" => [self::class, "activate_plugin"], + "permission_callback" => function () { + return current_user_can("activate_plugins"); + }, + "args" => [ + "file" => ["type" => "string", "required" => true], + ], + ]); + + register_rest_route("aurora-admin/v1", "/plugins/deactivate", [ + "methods" => "POST", + "callback" => [self::class, "deactivate_plugin"], + "permission_callback" => function () { + return current_user_can("activate_plugins"); + }, + "args" => [ + "file" => ["type" => "string", "required" => true], + ], + ]); + + register_rest_route("aurora-admin/v1", "/plugins/delete", [ + "methods" => "DELETE", + "callback" => [self::class, "delete_plugin"], + "permission_callback" => function () { + return current_user_can("delete_plugins"); + }, + "args" => [ + "file" => ["type" => "string", "required" => true], + ], + ]); + + register_rest_route("aurora-admin/v1", "/plugins/update", [ + "methods" => "POST", + "callback" => [self::class, "update_plugin"], + "permission_callback" => function () { + return current_user_can("update_plugins"); + }, + "args" => [ + "file" => ["type" => "string", "required" => true], + ], + ]); + } + + public static function list_plugins() + { + self::load_plugin_admin_functions(); + self::load_update_functions(); + + $updates = get_site_transient("update_plugins"); + if (!is_object($updates) && function_exists("wp_update_plugins")) { + wp_update_plugins(); + $updates = get_site_transient("update_plugins"); + } + + $update_response = is_object($updates) && isset($updates->response) && is_array($updates->response) + ? $updates->response + : []; + + $items = []; + foreach (get_plugins() as $file => $plugin) { + $update = isset($update_response[$file]) ? $update_response[$file] : null; + $items[] = [ + "file" => $file, + "slug" => self::plugin_slug($file), + "name" => isset($plugin["Name"]) ? $plugin["Name"] : self::plugin_slug($file), + "description" => isset($plugin["Description"]) ? wp_strip_all_tags($plugin["Description"]) : "", + "version" => isset($plugin["Version"]) ? $plugin["Version"] : "", + "author" => isset($plugin["Author"]) ? wp_strip_all_tags($plugin["Author"]) : "", + "pluginUri" => isset($plugin["PluginURI"]) ? esc_url_raw($plugin["PluginURI"]) : "", + "authorUri" => isset($plugin["AuthorURI"]) ? esc_url_raw($plugin["AuthorURI"]) : "", + "active" => is_plugin_active($file), + "networkActive" => is_multisite() && is_plugin_active_for_network($file), + "updateAvailable" => $update !== null, + "newVersion" => $update && isset($update->new_version) ? $update->new_version : "", + "canActivate" => current_user_can("activate_plugin", $file), + "canDelete" => current_user_can("delete_plugins") && !is_plugin_active($file), + "canUpdate" => current_user_can("update_plugins") && $update !== null, + ]; + } + + usort($items, function ($a, $b) { + if ($a["active"] !== $b["active"]) { + return $a["active"] ? -1 : 1; + } + return strcasecmp($a["name"], $b["name"]); + }); + + return new \WP_REST_Response([ + "items" => $items, + "stats" => self::stats($items), + "addUrl" => admin_url("plugin-install.php"), + ], 200); + } + + public static function activate_plugin($request) + { + self::load_plugin_admin_functions(); + $file = self::sanitize_file($request->get_param("file")); + $result = activate_plugin($file); + + if (is_wp_error($result)) { + return new \WP_Error("aurora_plugin_activate_failed", $result->get_error_message(), ["status" => 500]); + } + + self::clean_plugin_cache(); + return new \WP_REST_Response(["success" => true, "message" => __("Plugin activated.", "aurora-admin")], 200); + } + + public static function deactivate_plugin($request) + { + self::load_plugin_admin_functions(); + $file = self::sanitize_file($request->get_param("file")); + deactivate_plugins($file); + + self::clean_plugin_cache(); + return new \WP_REST_Response([ + "success" => true, + "message" => __("Plugin deactivated.", "aurora-admin"), + "redirect" => $file === "aurora-admin/aurora-admin.php" ? admin_url("plugins.php") : "", + ], 200); + } + + public static function delete_plugin($request) + { + self::load_plugin_admin_functions(); + if (!function_exists("delete_plugins")) { + require_once ABSPATH . "wp-admin/includes/plugin.php"; + } + + $file = self::sanitize_file($request->get_param("file")); + if (is_plugin_active($file)) { + return new \WP_Error("aurora_plugin_active_delete", __("Deactivate the plugin before deleting it.", "aurora-admin"), ["status" => 400]); + } + + $result = delete_plugins([$file]); + if (is_wp_error($result)) { + return new \WP_Error("aurora_plugin_delete_failed", $result->get_error_message(), ["status" => 500]); + } + + self::clean_plugin_cache(); + return new \WP_REST_Response(["success" => true, "message" => __("Plugin deleted.", "aurora-admin")], 200); + } + + public static function update_plugin($request) + { + self::load_plugin_admin_functions(); + self::load_update_functions(); + + require_once ABSPATH . "wp-admin/includes/class-wp-upgrader.php"; + require_once ABSPATH . "wp-admin/includes/file.php"; + require_once ABSPATH . "wp-admin/includes/misc.php"; + + $file = self::sanitize_file($request->get_param("file")); + $skin = new \Automatic_Upgrader_Skin(); + $upgrader = new \Plugin_Upgrader($skin); + $result = $upgrader->upgrade($file); + + if (is_wp_error($result)) { + return new \WP_Error("aurora_plugin_update_failed", $result->get_error_message(), ["status" => 500]); + } + if (!$result) { + $error = $skin->get_errors(); + $message = is_wp_error($error) && $error->has_errors() + ? $error->get_error_message() + : __("Plugin update failed.", "aurora-admin"); + return new \WP_Error("aurora_plugin_update_failed", $message, ["status" => 500]); + } + + self::clean_plugin_cache(); + return new \WP_REST_Response(["success" => true, "message" => __("Plugin updated.", "aurora-admin")], 200); + } + + private static function load_plugin_admin_functions() + { + if (!function_exists("get_plugins") || !function_exists("is_plugin_active")) { + require_once ABSPATH . "wp-admin/includes/plugin.php"; + } + } + + private static function load_update_functions() + { + if (!function_exists("wp_update_plugins")) { + require_once ABSPATH . "wp-admin/includes/update.php"; + } + } + + private static function sanitize_file($file) + { + return plugin_basename(sanitize_text_field((string) $file)); + } + + private static function plugin_slug($file) + { + $parts = explode("/", $file); + return $parts[0]; + } + + private static function clean_plugin_cache() + { + if (function_exists("wp_clean_plugins_cache")) { + wp_clean_plugins_cache(true); + } + wp_cache_delete("plugins", "plugins"); + } + + private static function stats($items) + { + $stats = ["total" => count($items), "active" => 0, "inactive" => 0, "updates" => 0]; + foreach ($items as $item) { + if ($item["active"]) { + $stats["active"]++; + } else { + $stats["inactive"]++; + } + if ($item["updateAvailable"]) { + $stats["updates"]++; + } + } + return $stats; + } +} diff --git a/admin/src/Shell/MenuSerializer.php b/admin/src/Shell/MenuSerializer.php index 5d4afc9..58f91f9 100644 --- a/admin/src/Shell/MenuSerializer.php +++ b/admin/src/Shell/MenuSerializer.php @@ -132,6 +132,10 @@ class MenuSerializer return ""; } + if ($slug === "plugins.php") { + return admin_url("plugins.php?page=aurora-admin-plugins"); + } + // Already an absolute URL. if (preg_match('#^https?://#', $slug)) { return $slug; diff --git a/app/src/entries/plugins.js b/app/src/entries/plugins.js new file mode 100644 index 0000000..02d68d2 --- /dev/null +++ b/app/src/entries/plugins.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Plugins from '@/pages/plugins/Plugins.vue'; + +const mountEl = document.getElementById('aurora-admin-plugins-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Plugins, { serverData: data }).mount(mountEl); +} diff --git a/app/src/pages/_shared/AppListShell.vue b/app/src/pages/_shared/AppListShell.vue index 1200a26..097be39 100644 --- a/app/src/pages/_shared/AppListShell.vue +++ b/app/src/pages/_shared/AppListShell.vue @@ -76,15 +76,15 @@ const search = defineModel('search', { type: String, default: '' }); margin: -32px; padding: 22px; box-sizing: border-box; - background: var(--aurora-app-bg); + /* No background — html/body already paint the page gradient; this was + a redundant duplicate layer on top of it. */ } -/* Left — list box */ +/* Left — no background/border of its own; the page gradient shows + straight through so it reads as one surface instead of a boxed panel. */ .app-list-shell__nav { width: 340px; flex-shrink: 0; - background: var(--aurora-frame-bg); - border: 1px solid var(--aurora-frame-border); border-radius: 16px; padding: 24px 20px; box-sizing: border-box; @@ -127,7 +127,9 @@ const search = defineModel('search', { type: String, default: '' }); background: var(--aurora-bg-subtle); color: var(--aurora-text-muted); } .app-list-shell__filters :deep(button.is-active) { - background: var(--aurora-text); color: var(--aurora-app-bg); + /* --aurora-app-bg can be a gradient (invalid for `color`), so this one + inverted-contrast badge uses the flat fallback token instead. */ + background: var(--aurora-text); color: var(--aurora-app-bg-solid); } .app-list-shell__meta { @@ -145,11 +147,9 @@ const search = defineModel('search', { type: String, default: '' }); .app-list-shell__list { display: flex; flex-direction: column; gap: 2px; } -/* Right — content box */ +/* Right — no background/border of its own, same reasoning. */ .app-list-shell__content { flex: 1; min-width: 0; - background: var(--aurora-frame-bg); - border: 1px solid var(--aurora-frame-border); border-radius: 16px; padding: 28px 32px; min-height: 480px; diff --git a/app/src/pages/dashboard/components/AuroraCard.vue b/app/src/pages/dashboard/components/AuroraCard.vue index e6b16e3..7c470b0 100644 --- a/app/src/pages/dashboard/components/AuroraCard.vue +++ b/app/src/pages/dashboard/components/AuroraCard.vue @@ -30,7 +30,11 @@ defineProps({ display: flex; flex-direction: column; height: 100%; - background: var(--aurora-card-bg); + /* Deeper than the generic --aurora-card-bg — the Dashboard is now + wrapped in its own frame (windowChrome.js's .aurora-dashboard-frame, + using --aurora-frame-bg), and that's close in tone to --aurora-card-bg + for most presets, so cards would nearly disappear into it otherwise. */ + background: var(--aurora-card-bg-deep, var(--aurora-card-bg)); border: 1px solid var(--aurora-card-border); border-radius: 20px; padding: 20px; diff --git a/app/src/pages/plugins/Plugins.vue b/app/src/pages/plugins/Plugins.vue new file mode 100644 index 0000000..555278b --- /dev/null +++ b/app/src/pages/plugins/Plugins.vue @@ -0,0 +1,690 @@ + + + +Aurora Admin
+{{ plugin.description || 'No description provided.' }}
+ +No fonts match.
+