From ef86b0264ab7c649fd919c02d52424c9c69d9d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=87=E6=B4=BE=E5=A4=87=E6=A1=88?= <130886204+modiqi@users.noreply.github.com> Date: Tue, 20 May 2025 00:38:10 +0800 Subject: [PATCH] dev --- assets/css/admin.css | 1003 ++++++++++++++++++++++++++++++++++++ assets/js/admin.js | 1031 +++++++++++++++++++++++++++++++++++++ bulk-installer-server.php | 585 +++++++++++++++++++++ templates/admin-page.php | 514 ++++++++++++++++++ 4 files changed, 3133 insertions(+) create mode 100644 assets/css/admin.css create mode 100644 assets/js/admin.js create mode 100644 bulk-installer-server.php create mode 100644 templates/admin-page.php diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 0000000..d8ba8f0 --- /dev/null +++ b/assets/css/admin.css @@ -0,0 +1,1003 @@ +/** + * Admin styles for Collection Manager + */ + +/* General Layout */ +.bis-wrap { + margin: 20px 0; +} + +.bis-container { + display: flex; + gap: 24px; + flex-wrap: wrap; + margin-top: 24px; +} + +.bis-sidebar { + flex: 0 0 320px; +} + +.bis-content { + flex: 1; + min-width: 500px; +} + +@media (max-width: 1100px) { + .bis-container { + flex-direction: column; + } + + .bis-sidebar, + .bis-content { + flex: 1 1 100%; + width: 100%; + min-width: 0; + } +} + +.bis-card { + background: #fff; + border: 1px solid #ccd0d4; + border-radius: 4px; + padding: 24px; + margin-bottom: 24px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); +} + +.bis-version-tag { + font-size: 13px; + padding-left: 10px; + font-weight: normal; + opacity: 0.7; +} + +/* Form Elements */ +.bis-form-section { + margin-bottom: 30px; + padding-bottom: 24px; + border-bottom: 1px solid #f0f0f1; +} + +.bis-section-title { + margin-top: 0; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid #f0f0f1; + font-size: 16px; + color: #1d2327; +} + +.bis-section-description { + margin-top: -8px; + margin-bottom: 16px; + color: #646970; +} + +.bis-form-row { + margin-bottom: 18px; +} + +.bis-form-row label { + display: block; + margin-bottom: 6px; + font-weight: 600; + color: #1d2327; +} + +.bis-form-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 16px; + margin-bottom: 18px; +} + +.bis-form-group { + margin-bottom: 4px; +} + +.bis-required { + color: #d63638; +} + +.bis-form-actions { + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid #f0f0f1; + display: flex; + justify-content: space-between; +} + +.bis-checkbox-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.bis-select { + width: 100%; + max-width: 100%; +} + +/* Icon Select */ +.bis-icon-select-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.bis-selected-icon { + margin-left: 10px; + font-size: 18px; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + background: #f0f6fc; + border-radius: 50%; + color: #2271b1; +} + +/* Screenshot Upload */ +.bis-screenshot-field { + display: flex; + align-items: center; + gap: 10px; +} + +.bis-screenshot-preview { + margin-top: 10px; +} + +.bis-screenshot-preview img { + max-width: 100%; + height: auto; + max-height: 200px; + border: 1px solid #ddd; + border-radius: 4px; +} + +/* Collections List */ +.bis-collections-list { + margin: 15px 0; +} + +.bis-collections-list ul { + margin: 0; + padding: 0; + list-style: none; +} + +.bis-collection-item { + display: flex; + align-items: center; + padding: 12px; + margin-bottom: 10px; + border: 1px solid #e5e5e5; + border-radius: 4px; + background: #f9f9f9; + transition: all 0.2s ease; +} + +.bis-collection-item:hover { + background: #f0f0f1; + border-color: #c3c4c7; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.bis-collection-icon { + flex: 0 0 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: #e9f0f5; + border-radius: 20px; + margin-right: 12px; +} + +.bis-collection-icon .dashicons { + font-size: 20px; + width: 20px; + height: 20px; + color: #2271b1; +} + +.bis-collection-details { + flex: 1; +} + +.bis-collection-details h4 { + margin: 0 0 6px 0; + font-size: 14px; + color: #1d2327; +} + +.bis-collection-meta { + font-size: 12px; + color: #646970; + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; +} + +.bis-item-count { + display: flex; + align-items: center; + gap: 4px; +} + +.bis-item-count .dashicons { + font-size: 14px; + width: 14px; + height: 14px; +} + +.bis-meta-tag { + display: inline-block; + padding: 2px 6px; + font-size: 11px; + border-radius: 10px; + background: #f0f0f1; +} + +.bis-category-tag { + background: #e9f0f5; + color: #2271b1; +} + +.bis-level-tag { + background: #f0f6e5; + color: #3c6e21; +} + +.bis-collection-actions { + display: flex; + gap: 6px; +} + +.bis-collection-actions .button { + display: flex; + align-items: center; + gap: 4px; +} + +.bis-collection-actions .dashicons { + font-size: 14px; + width: 14px; + height: 14px; +} + +.bis-empty-message { + padding: 16px; + background: #f0f0f1; + border-radius: 4px; + text-align: center; + color: #646970; +} + +.bis-actions { + display: flex; + justify-content: space-between; + margin-top: 20px; +} + +.bis-actions .button { + display: flex; + align-items: center; + gap: 6px; +} + +.bis-actions .dashicons { + font-size: 16px; + width: 16px; + height: 16px; +} + +/* API Info */ +.bis-api-info { + margin: 16px 0; + font-size: 13px; +} + +.bis-api-endpoint { + margin-bottom: 16px; +} + +.bis-api-endpoint label, +.bis-collection-endpoints label { + display: block; + font-weight: 600; + margin-bottom: 6px; +} + +.bis-endpoint-url { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.bis-endpoint-url code { + flex: 1; + padding: 6px 8px; + background: #f0f0f1; + border-radius: 3px; + word-break: break-all; + font-size: 12px; +} + +.bis-copy-url { + margin-left: 5px; +} + +.bis-client-instructions { + margin-top: 20px; + padding-top: 15px; + border-top: 1px solid #f0f0f1; +} + +.bis-client-instructions h4 { + margin-top: 0; +} + +.bis-client-instructions ol { + margin-left: 20px; +} + +/* Export Options */ +.bis-export-options { + margin: 15px 0; +} + +.bis-export-label { + display: block; + margin-bottom: 6px; + font-weight: 600; +} + +.bis-export-collections { + display: flex; + align-items: center; + gap: 6px; + margin-top: 12px; +} + +.bis-export-collections .dashicons { + font-size: 16px; + width: 16px; + height: 16px; +} + +/* Tabs */ +.bis-tabs { + margin-top: 20px; +} + +.bis-tabs-nav { + display: flex; + border-bottom: 1px solid #c3c4c7; + margin-bottom: 20px; + gap: 4px; +} + +.bis-tab { + padding: 10px 16px; + background: none; + border: none; + cursor: pointer; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + font-weight: 500; + display: flex; + align-items: center; + gap: 6px; +} + +.bis-tab .dashicons { + font-size: 16px; + width: 16px; + height: 16px; +} + +.bis-tab.active { + border-color: #2271b1; + color: #2271b1; + font-weight: 600; +} + +.bis-tab:hover:not(.active) { + color: #135e96; + border-color: #dcdcde; +} + +.bis-tab-content { + display: none; +} + +.bis-tab-content.active { + display: block; +} + +/* Source Sections */ +.bis-source-sections { + display: flex; + flex-direction: column; + gap: 24px; +} + +.bis-source-section { + border: 1px solid #e5e5e5; + border-radius: 6px; + padding: 16px; + background: #fbfbfb; +} + +.bis-source-section h4 { + margin-top: 0; + padding-bottom: 10px; + border-bottom: 1px solid #eee; + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; +} + +.bis-source-section h4 .dashicons { + color: #2271b1; +} + +.bis-source-description { + margin-top: -6px; + margin-bottom: 16px; + color: #646970; + font-size: 13px; +} + +/* Items */ +.bis-items-container { + min-height: 50px; + margin: 15px 0; + transition: all 0.2s ease; +} + +.bis-item { + background: #fff; + border: 1px solid #e5e5e5; + border-radius: 4px; + margin-bottom: 10px; + transition: all 0.2s ease; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); +} + +.bis-item:hover { + border-color: #c3c4c7; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.bis-item-header { + padding: 12px 15px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: move; + background: #f9f9f9; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom: 1px solid #f0f0f1; +} + +.bis-item-info { + flex: 1; +} + +.bis-item-title { + margin: 0; + font-size: 14px; + color: #1d2327; +} + +.bis-item-slug { + color: #646970; + font-size: 12px; + margin-top: 2px; +} + +.bis-item-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.bis-item-description { + padding: 12px 15px; + color: #646970; + font-size: 13px; +} + +.bis-item-placeholder { + border: 2px dashed #c3c4c7; + height: 40px; + border-radius: 4px; + margin-bottom: 10px; + background: #f0f0f1; +} + +.bis-required-toggle { + display: flex; + align-items: center; + font-size: 12px; + color: #555; +} + +.bis-required-toggle input { + margin-right: 5px; +} + +.bis-add-item { + display: flex; + align-items: center; + gap: 6px; +} + +.bis-add-item .dashicons { + font-size: 14px; + width: 14px; + height: 14px; +} + +/* Modal */ +.bis-modal { + display: none; + position: fixed; + z-index: 100000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.bis-modal-content { + position: relative; + background-color: #fff; + margin: 10% auto; + padding: 0; + border-radius: 6px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); + width: 80%; + max-width: 600px; + animation: fadeIn 0.3s; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.bis-modal-header { + padding: 16px 20px; + border-bottom: 1px solid #eee; + display: flex; + justify-content: space-between; + align-items: center; +} + +.bis-modal-header h3 { + margin: 0; + font-size: 16px; +} + +.bis-modal-close { + color: #aaa; + font-size: 24px; + font-weight: bold; + cursor: pointer; + background: none; + border: none; + padding: 0; + line-height: 20px; +} + +.bis-modal-close:hover { + color: #555; +} + +.bis-modal-body { + padding: 20px; +} + +.bis-modal-footer { + padding: 16px 20px; + border-top: 1px solid #eee; + display: flex; + justify-content: flex-end; + gap: 10px; +} + +body.bis-modal-open { + overflow: hidden; +} + +/* Welcome Card */ +.bis-welcome-intro { + font-size: 15px; + color: #646970; + margin-bottom: 24px; +} + +.bis-welcome-content { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); + gap: 24px; + margin: 28px 0; +} + +.bis-welcome-step { + display: flex; + gap: 16px; + align-items: flex-start; +} + +.bis-step-icon { + width: 48px; + height: 48px; + background: #e9f0f5; + border-radius: 24px; + display: flex; + align-items: center; + justify-content: center; +} + +.bis-step-icon .dashicons { + font-size: 24px; + width: 24px; + height: 24px; + color: #2271b1; +} + +.bis-step-content { + flex: 1; +} + +.bis-step-content h3 { + margin: 0 0 10px 0; + font-size: 16px; + color: #1d2327; +} + +.bis-step-content p { + margin: 0; + color: #646970; + font-size: 14px; +} + +.bis-welcome-actions { + text-align: center; + margin-top: 32px; +} + +.bis-welcome-actions .button { + padding: 8px 20px; + height: auto; + font-size: 15px; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.bis-welcome-actions .dashicons { + font-size: 18px; + width: 18px; + height: 18px; +} + +/* Utility Classes */ +.bis-form-actions .button { + display: flex; + align-items: center; + gap: 6px; +} + +.bis-form-actions .dashicons { + font-size: 16px; + width: 16px; + height: 16px; +} + +.description { + font-size: 12px; + color: #646970; + margin-top: 4px; + margin-bottom: 0; +} + +/* Add some animations for better feedback */ +@keyframes highlight { + 0% { background-color: #f0f6fc; } + 100% { background-color: transparent; } +} + +.bis-highlight { + animation: highlight 1.5s ease-out; +} + +.bis-save-success { + color: #00a32a; +} + +.bis-save-error { + color: #d63638; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + body.is-dark-theme .bis-card, + body.is-dark-theme .bis-item, + body.is-dark-theme .bis-modal-content, + body.is-dark-theme .bis-source-section { + background-color: #1d2327; + border-color: #2c3338; + } + + body.is-dark-theme .bis-item-header { + background-color: #2c3338; + border-color: #3c434a; + } + + body.is-dark-theme .bis-section-title, + body.is-dark-theme .bis-form-section, + body.is-dark-theme .bis-form-actions, + body.is-dark-theme .bis-modal-header, + body.is-dark-theme .bis-modal-footer { + border-color: #3c434a; + } + + body.is-dark-theme .bis-collection-item { + background-color: #2c3338; + border-color: #3c434a; + } + + body.is-dark-theme .bis-collection-item:hover { + background-color: #32373c; + border-color: #50575e; + } + + body.is-dark-theme .bis-endpoint-url code { + background-color: #2c3338; + } + + body.is-dark-theme .bis-empty-message { + background-color: #2c3338; + } + + body.is-dark-theme .bis-tabs-nav { + border-color: #3c434a; + } +} +/** + * 需要添加到 admin.css 文件中的通知样式 + */ + +/* 通知消息样式 */ +.bis-notification { + position: fixed; + bottom: 20px; + right: 20px; + padding: 12px 16px; + background: #fff; + border-left: 4px solid #72aee6; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + gap: 10px; + z-index: 999999; + transform: translateY(100px); + opacity: 0; + transition: transform 0.3s ease, opacity 0.3s ease; + max-width: 400px; +} + +.bis-notification-show { + transform: translateY(0); + opacity: 1; +} + +.bis-notification-success { + border-left-color: #00a32a; +} + +.bis-notification-error { + border-left-color: #d63638; +} + +.bis-notification-warning { + border-left-color: #dba617; +} + +.bis-notification .dashicons { + font-size: 18px; + width: 18px; + height: 18px; + color: #72aee6; +} + +.bis-notification-success .dashicons { + color: #00a32a; +} + +.bis-notification-error .dashicons { + color: #d63638; +} + +.bis-notification-warning .dashicons { + color: #dba617; +} + +.bis-notification-message { + flex: 1; + font-size: 14px; +} + +.bis-notification-close { + background: none; + border: none; + cursor: pointer; + padding: 0; + color: #646970; +} + +.bis-notification-close:hover { + color: #1d2327; +} + +.bis-notification-close .dashicons { + font-size: 16px; + width: 16px; + height: 16px; + color: inherit; +} + +/* 加载蒙层 */ +.bis-loading-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 100000; + color: #fff; +} + +.bis-loading-overlay .spinner { + float: none; + margin: 0 auto 10px; + background-color: #fff; +} + +/* 暗色模式支持 */ +body.is-dark-theme .bis-notification { + background-color: #2c3338; + color: #f0f0f1; +} + +body.is-dark-theme .bis-notification-close { + color: #a7aaad; +} + +body.is-dark-theme .bis-notification-close:hover { + color: #f0f0f1; +} +/** + * 为slug显示和工具提示添加的额外CSS样式 + * + */ + +/* Slug 展示样式 */ +.bis-slug-display { + margin-bottom: 18px; + padding: 10px; + background-color: #f8f8f8; + border-radius: 4px; + border-left: 3px solid #2271b1; +} + +.bis-slug-info-wrapper { + display: flex; + align-items: center; + gap: 10px; +} + +.bis-slug-info { + padding: 5px 8px; + background: #f0f0f1; + border-radius: 3px; + font-size: 13px; + color: #3c434a; + flex-grow: 1; +} + +.bis-regenerate-slug { + display: flex; + align-items: center; + gap: 4px; +} + +.bis-regenerate-slug .dashicons { + font-size: 14px; + width: 14px; + height: 14px; +} + +/* 工具提示样式 */ +.bis-tooltip { + position: relative; + cursor: help; + color: #646970; +} + +.bis-tooltip .dashicons { + width: 16px; + height: 16px; + font-size: 16px; + vertical-align: middle; +} + +.bis-tooltip-content { + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + background-color: #333; + color: #fff; + padding: 6px 10px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + z-index: 100; + margin-top: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + max-width: 250px; + width: max-content; +} + +.bis-tooltip-content::before { + content: ''; + position: absolute; + bottom: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent #333 transparent; +} + +/* 暗色模式支持 */ +body.is-dark-theme .bis-slug-display { + background-color: #2c3338; + border-left-color: #72aee6; +} + +body.is-dark-theme .bis-slug-info { + background-color: #1d2327; + color: #f0f0f1; +} + +body.is-dark-theme .bis-tooltip-content { + background-color: #1d2327; + color: #f0f0f1; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); +} + +body.is-dark-theme .bis-tooltip-content::before { + border-color: transparent transparent #1d2327 transparent; +} diff --git a/assets/js/admin.js b/assets/js/admin.js new file mode 100644 index 0000000..06b494e --- /dev/null +++ b/assets/js/admin.js @@ -0,0 +1,1031 @@ +/** + * Admin JavaScript for Collection Manager + */ +jQuery(document).ready(function($) { + // Collection data model + let currentCollection = { + name: '', + description: '', + icon: 'dashicons-admin-plugins', + category: 'business', + level: 'beginner', + author: '', + screenshot: '', + plugins: { + repository: [], + wenpai: [], + url: [] + }, + themes: { + repository: [], + wenpai: [], + url: [] + } + }; + + let editingItemId = null; + let nextItemId = 1; + + // Initialize UI + initTabs(); + initItemSortable(); + initEvents(); + initIconSelection(); + initTooltips(); + + // Tab navigation + function initTabs() { + $('.bis-tab').on('click', function() { + const tab = $(this).data('tab'); + $('.bis-tab').removeClass('active'); + $(this).addClass('active'); + $('.bis-tab-content').removeClass('active').hide(); + $('#bis-tab-' + tab).addClass('active').show(); + }); + } + + // Initialize tooltips + function initTooltips() { + $('.bis-tooltip').hover(function() { + const tooltip = $(this).attr('data-tooltip'); + if (tooltip) { + const $tooltipElement = $('
'); + $tooltipElement.text(tooltip); + $(this).append($tooltipElement); + } + }, function() { + $(this).find('.bis-tooltip-content').remove(); + }); + } + + // Icon selection functionality + function initIconSelection() { + const selectedIcon = $('#bis-collection-icon').val(); + $('.bis-selected-icon').attr('class', 'bis-selected-icon dashicons ' + selectedIcon); + + $('#bis-collection-icon').on('change', function() { + const selectedIcon = $(this).val(); + $('.bis-selected-icon').attr('class', 'bis-selected-icon dashicons ' + selectedIcon); + }); + } + + // Make items sortable + function initItemSortable() { + $('.bis-items-container').sortable({ + placeholder: 'bis-item-placeholder', + handle: '.bis-item-header', + update: function() { + updateCollectionFromUI(); + } + }); + } + + // Initialize all event handlers + function initEvents() { + // Create new collection + $('.bis-new-collection').on('click', function() { + resetForm(); + $('#bis-welcome-card').hide(); + $('#bis-editor-title').text(bisAjax.i18n.add_collection || '创建新集合'); + $('#bis-editor-card').show(); + }); + + // Edit collection + $(document).on('click', '.bis-edit-collection', function() { + const collectionId = $(this).data('id'); + loadCollection(collectionId); + }); + + // Regenerate slug + $(document).on('click', '.bis-regenerate-slug', function(e) { + e.preventDefault(); + if (confirm(bisAjax.i18n.confirm_regenerate_slug || '这将为这个集合生成一个新的 slug。API URL 和这个集合的书签可能会失效。继续吗?')) { + $(this).data('force-new-slug', true); + saveCollection(true); + } + }); + + // Delete collection + $(document).on('click', '.bis-delete-collection', function() { + const collectionId = $(this).data('id'); + const collectionName = $(this).closest('.bis-collection-item').find('h4').text(); + if (confirm(bisAjax.i18n.confirm_delete || `确定要删除集合 "${collectionName}" 吗?`)) { + deleteCollection(collectionId); + } + }); + + // Cancel edit + $('.bis-cancel-edit').on('click', function() { + $('#bis-editor-card').hide(); + $('#bis-welcome-card').show(); + }); + + // Save collection + $('#bis-collection-form').on('submit', function(e) { + e.preventDefault(); + saveCollection(); + return false; + }); + + // Add item button + $('.bis-add-item').on('click', function() { + const itemType = $(this).data('type'); + const itemSource = $(this).data('source'); + + // Show modal with empty form + editingItemId = null; + $('#bis-item-type').val(itemType); + $('#bis-item-source').val(itemSource); + $('#bis-item-id').val(''); + $('#bis-item-slug').val(''); + $('#bis-item-name').val(''); + $('#bis-item-description').val(''); + $('#bis-item-required').prop('checked', false); + + // Show URL field if needed + $('#bis-url-field').toggle(itemSource === 'url'); + if (itemSource === 'url') { + $('#bis-item-url').val(''); + } + + $('#bis-modal-title').text( + itemType === 'plugin' ? + bisAjax.i18n.add_plugin || '添加插件' : + bisAjax.i18n.add_theme || '添加主题' + ); + + showModal('#bis-item-modal'); + }); + + // Modal close + $('.bis-modal-close, .bis-modal-cancel').on('click', function() { + closeModals(); + }); + + // Click outside modal to close + $(document).on('click', '.bis-modal', function(e) { + if ($(e.target).hasClass('bis-modal')) { + closeModals(); + } + }); + + // Save item + $('.bis-save-item').on('click', function() { + saveItem(); + }); + + // Edit item (delegated) + $(document).on('click', '.bis-edit-item', function() { + const $item = $(this).closest('.bis-item'); + const itemId = parseInt($item.data('id')); + const itemType = $item.closest('.bis-tab-content').attr('id') === 'bis-tab-plugins' ? 'plugin' : 'theme'; + const itemSource = $item.closest('.bis-source-section').data('source'); + + // Find the item in the collection data + const items = itemType === 'plugin' ? currentCollection.plugins[itemSource] : currentCollection.themes[itemSource]; + const item = items.find(i => i.id === itemId); + + if (item) { + editingItemId = itemId; + $('#bis-item-type').val(itemType); + $('#bis-item-source').val(itemSource); + $('#bis-item-id').val(itemId); + $('#bis-item-slug').val(item.slug); + $('#bis-item-name').val(item.name); + $('#bis-item-description').val(item.description || ''); + $('#bis-item-required').prop('checked', item.required || false); + + // Show URL field if needed + $('#bis-url-field').toggle(itemSource === 'url'); + if (itemSource === 'url') { + $('#bis-item-url').val(item.url || ''); + } + + $('#bis-modal-title').text('编辑' + (itemType === 'plugin' ? '插件' : '主题')); + showModal('#bis-item-modal'); + } + }); + + // Remove item (delegated) + $(document).on('click', '.bis-remove-item', function() { + const $item = $(this).closest('.bis-item'); + $item.fadeOut(300, function() { + $item.remove(); + updateCollectionFromUI(); + }); + }); + + // Toggle item required + $(document).on('change', '.bis-item-required', function() { + updateCollectionFromUI(); + }); + + // Upload screenshot + $('.bis-upload-screenshot').on('click', function(e) { + e.preventDefault(); + + // If the media frame already exists, reopen it + if (mediaUploader) { + mediaUploader.open(); + return; + } + + // Create the media frame + mediaUploader = wp.media({ + title: '选择或上传截图', + button: { + text: '使用此图片' + }, + multiple: false + }); + + // When an image is selected, run a callback + mediaUploader.on('select', function() { + const attachment = mediaUploader.state().get('selection').first().toJSON(); + $('#bis-collection-screenshot').val(attachment.url); + updateScreenshotPreview(attachment.url); + }); + + // Open the media uploader + mediaUploader.open(); + }); + + // Update screenshot preview when URL changes + $('#bis-collection-screenshot').on('change', function() { + updateScreenshotPreview($(this).val()); + }); + + // Export format change + $('#bis-export-format').on('change', function() { + $('#bis-export-collection-select').toggle($(this).val() === 'selected'); + }); + + // Export collections + $('.bis-export-collections').on('click', function() { + exportCollections(); + }); + + // Copy URL buttons + $('.bis-copy-url').on('click', function() { + const url = $(this).data('url'); + copyToClipboard(url); + + // Show success message + const $button = $(this); + $button.html(''); + setTimeout(function() { + $button.html(''); + }, 2000); + }); + + // Import collection + $('.bis-import-collection').on('click', function() { + showModal('#bis-import-modal'); + }); + + // Import submit + $('.bis-import-submit').on('click', function() { + importCollections(); + }); + + // Form key event handling + $('#bis-item-form').on('keypress', function(e) { + if (e.which === 13) { + e.preventDefault(); + $('.bis-save-item').click(); + } + }); + + // Keyboard shortcut (Escape to close modals) + $(document).on('keydown', function(e) { + if (e.key === 'Escape') { + closeModals(); + } + }); + + // Prevent modals from closing when clicking inside the modal content + $('.bis-modal-content').on('click', function(e) { + e.stopPropagation(); + }); + + // Check if URL parameters contain action=edit and id=xyz + const urlParams = new URLSearchParams(window.location.search); + const action = urlParams.get('action'); + const id = urlParams.get('id'); + + if (action === 'edit' && id) { + // Auto-load collection for editing + loadCollection(id); + } + } + + // Media uploader instance + let mediaUploader = null; + + // Show a modal + function showModal(selector) { + $(selector).fadeIn(300); + $('body').addClass('bis-modal-open'); + } + + // Close all modals + function closeModals() { + $('.bis-modal').fadeOut(200); + $('body').removeClass('bis-modal-open'); + } + + // Update screenshot preview + function updateScreenshotPreview(url) { + const $preview = $('#bis-screenshot-preview'); + if (url) { + $preview.html(`Screenshot preview`); + } else { + $preview.empty(); + } + } + + // Reset form to create a new collection + function resetForm() { + $('#bis-collection-id').val(''); + $('#bis-collection-name').val(''); + $('#bis-collection-description').val(''); + $('#bis-collection-icon').val('dashicons-admin-plugins'); + $('#bis-collection-category').val('business'); + $('#bis-collection-level').val('beginner'); + $('#bis-collection-author').val(document.title.split(' ‹ ')[1] || ''); + $('#bis-collection-screenshot').val(''); + $('#bis-screenshot-preview').empty(); + + // 隐藏 slug 信息 (只在编辑现有集合时显示) + $('.bis-slug-display').hide(); + $('.bis-slug-info').text(''); + $('.bis-regenerate-slug').data('force-new-slug', false); + + // Clear all items + $('.bis-items-container').empty(); + + // Reset collection data + currentCollection = { + name: '', + description: '', + icon: 'dashicons-admin-plugins', + category: 'business', + level: 'beginner', + author: document.title.split(' ‹ ')[1] || '', + screenshot: '', + plugins: { + repository: [], + wenpai: [], + url: [] + }, + themes: { + repository: [], + wenpai: [], + url: [] + } + }; + + // Reset item ID counter + nextItemId = 1; + + // Update icon preview + initIconSelection(); + } + + // Load collection data into form + function loadCollection(collectionId) { + // Add loading state to the editor card + const $editorCard = $('#bis-editor-card'); + const $loadingOverlay = $('

加载中...

'); + $editorCard.append($loadingOverlay); + + $.ajax({ + url: bisAjax.ajaxurl, + type: 'GET', + data: { + action: 'bis_get_collection', + nonce: bisAjax.nonce, + collection_id: collectionId + }, + success: function(response) { + $loadingOverlay.remove(); + + if (response.success) { + const collection = response.data.collection; + currentCollection = collection; + + // Update form fields + $('#bis-collection-id').val(collectionId); + $('#bis-collection-name').val(collection.name); + $('#bis-collection-description').val(collection.description); + $('#bis-collection-icon').val(collection.icon); + $('#bis-collection-category').val(collection.category); + $('#bis-collection-level').val(collection.level); + $('#bis-collection-author').val(collection.author); + $('#bis-collection-screenshot').val(collection.screenshot); + updateScreenshotPreview(collection.screenshot); + + // 显示 slug 信息 + $('.bis-slug-display').show(); + $('.bis-slug-info').text(collectionId); + + // 重置强制生成新 slug 标记 + $('.bis-regenerate-slug').data('force-new-slug', false); + + // Update icon preview + initIconSelection(); + + // Clear all items first + $('.bis-items-container').empty(); + + // Add plugins and themes to UI + renderItems('plugin', 'repository', collection.plugins.repository || []); + renderItems('plugin', 'wenpai', collection.plugins.wenpai || []); + renderItems('plugin', 'url', collection.plugins.url || []); + renderItems('theme', 'repository', collection.themes.repository || []); + renderItems('theme', 'wenpai', collection.themes.wenpai || []); + renderItems('theme', 'url', collection.themes.url || []); + + // Find the highest item ID to continue from + nextItemId = 1; + const updateNextId = (items) => { + if (!items) return; + items.forEach(item => { + if (item.id && item.id >= nextItemId) { + nextItemId = item.id + 1; + } + }); + }; + + updateNextId(collection.plugins.repository); + updateNextId(collection.plugins.wenpai); + updateNextId(collection.plugins.url); + updateNextId(collection.themes.repository); + updateNextId(collection.themes.wenpai); + updateNextId(collection.themes.url); + + // Show editor + $('#bis-welcome-card').hide(); + $('#bis-editor-title').text((bisAjax.i18n.edit_collection || '编辑集合') + ': ' + collection.name); + $('#bis-editor-card').show(); + } else { + console.error('加载集合失败:', response.data); + showNotification(response.data.message || '加载集合时出错', 'error'); + } + }, + error: function(xhr, status, error) { + $loadingOverlay.remove(); + console.error('AJAX错误:', xhr.responseText); + showNotification('加载集合时出错: ' + error, 'error'); + } + }); + } + + // Render items to the UI + function renderItems(type, source, items) { + if (!items || !Array.isArray(items)) return; + + const $container = $(`#bis-${type}s-${source}`); + + items.forEach(item => { + // Ensure item has an ID + if (!item.id) { + item.id = nextItemId++; + } + + const itemHtml = generateItemHtml(item); + $container.append(itemHtml); + }); + } + + // Generate HTML for an item + function generateItemHtml(item) { + let template = $('#bis-item-template').html(); + + template = template.replace(/{{id}}/g, item.id); + template = template.replace(/{{name}}/g, escapeHtml(item.name)); + template = template.replace(/{{slug}}/g, escapeHtml(item.slug)); + template = template.replace(/{{description}}/g, escapeHtml(item.description || '')); + template = template.replace(/{{required}}/g, item.required ? 'checked' : ''); + + return template; + } + + // Escape HTML to prevent XSS + function escapeHtml(text) { + if (!text) return ''; + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + // Highlight newly added/edited elements + function highlightElement($element) { + $element.addClass('bis-highlight'); + setTimeout(function() { + $element.removeClass('bis-highlight'); + }, 1500); + } + + // Save an item from the modal + function saveItem() { + const itemType = $('#bis-item-type').val(); + const itemSource = $('#bis-item-source').val(); + const itemId = editingItemId || nextItemId++; + const itemSlug = $('#bis-item-slug').val(); + const itemName = $('#bis-item-name').val(); + const itemDescription = $('#bis-item-description').val(); + const itemRequired = $('#bis-item-required').is(':checked'); + + if (!itemSlug || !itemName) { + if (!itemSlug) { + $('#bis-item-slug').focus(); + showNotification('Slug 是必填项', 'warning'); + } else { + $('#bis-item-name').focus(); + showNotification('名称是必填项', 'warning'); + } + return; + } + + // Create item object + const item = { + id: itemId, + slug: itemSlug, + name: itemName, + description: itemDescription, + required: itemRequired + }; + + // Add URL if it's a URL source + if (itemSource === 'url') { + item.url = $('#bis-item-url').val(); + + if (!item.url) { + $('#bis-item-url').focus(); + showNotification('URL 是必填项', 'warning'); + return; + } + } + + // Add to collection data + const items = itemType === 'plugin' ? currentCollection.plugins[itemSource] : currentCollection.themes[itemSource]; + + if (editingItemId) { + // Update existing item + const index = items.findIndex(i => i.id === editingItemId); + if (index !== -1) { + items[index] = item; + } + } else { + // Add new item + items.push(item); + } + + // Update UI + const $container = $(`#bis-${itemType}s-${itemSource}`); + + if (editingItemId) { + // Update existing item in UI + const $item = $container.find(`.bis-item[data-id="${editingItemId}"]`); + const $newItem = $(generateItemHtml(item)); + $item.replaceWith($newItem); + highlightElement($newItem); + } else { + // Add new item to UI + const $newItem = $(generateItemHtml(item)); + $container.append($newItem); + highlightElement($newItem); + } + + // Close modal + closeModals(); + + // Show success notification + showNotification(editingItemId ? '项目已更新' : '项目已添加', 'success'); + } + + // Update collection data from UI + function updateCollectionFromUI() { + // Basic info + currentCollection.name = $('#bis-collection-name').val(); + currentCollection.description = $('#bis-collection-description').val(); + currentCollection.icon = $('#bis-collection-icon').val(); + currentCollection.category = $('#bis-collection-category').val(); + currentCollection.level = $('#bis-collection-level').val(); + currentCollection.author = $('#bis-collection-author').val(); + currentCollection.screenshot = $('#bis-collection-screenshot').val(); + + // Get the order and required status of plugins and themes + updateItemsFromUI('plugin', 'repository'); + updateItemsFromUI('plugin', 'wenpai'); + updateItemsFromUI('plugin', 'url'); + updateItemsFromUI('theme', 'repository'); + updateItemsFromUI('theme', 'wenpai'); + updateItemsFromUI('theme', 'url'); + } + + // Update items from UI + function updateItemsFromUI(type, source) { + const $container = $(`#bis-${type}s-${source}`); + const items = type === 'plugin' ? currentCollection.plugins[source] : currentCollection.themes[source]; + + // Get the current order of items + const itemIds = $container.find('.bis-item').map(function() { + return parseInt($(this).data('id')); + }).get(); + + // Get required status for each item + $container.find('.bis-item').each(function() { + const id = parseInt($(this).data('id')); + const required = $(this).find('.bis-item-required').is(':checked'); + + // Update required status in collection data + const item = items.find(i => i.id === id); + if (item) { + item.required = required; + } + }); + + // Reorder items based on UI + const reorderedItems = []; + itemIds.forEach(id => { + const item = items.find(i => i.id === id); + if (item) { + reorderedItems.push(item); + } + }); + + // Update collection with reordered items + if (type === 'plugin') { + currentCollection.plugins[source] = reorderedItems; + } else { + currentCollection.themes[source] = reorderedItems; + } + } + + // Add debug logging function + function logDebug(label, data) { + console.group('调试: ' + label); + console.log(data); + console.groupEnd(); + } + + // Save collection + function saveCollection(forceNewSlug = false) { + updateCollectionFromUI(); + + // Validate required fields + if (!currentCollection.name) { + $('#bis-collection-name').focus(); + showNotification(bisAjax.i18n.name_required || '集合名称是必填项', 'warning'); + return; + } + + // Check if we have at least one plugin or theme + let hasItems = false; + let totalItemCount = 0; + + for (const source in currentCollection.plugins) { + totalItemCount += currentCollection.plugins[source].length; + if (currentCollection.plugins[source].length > 0) { + hasItems = true; + } + } + + if (!hasItems) { + for (const source in currentCollection.themes) { + totalItemCount += currentCollection.themes[source].length; + if (currentCollection.themes[source].length > 0) { + hasItems = true; + } + } + } + + if (!hasItems) { + showNotification(bisAjax.i18n.item_required || '请至少添加一个插件或主题到集合中', 'warning'); + // Switch to plugins tab + $('.bis-tab[data-tab="plugins"]').click(); + return; + } + + // Prepare data for saving + const slug = $('#bis-collection-id').val(); + const $saveButton = $('.bis-save-collection'); + + // Add loading state to button + const originalButtonText = $saveButton.html(); + $saveButton.prop('disabled', true) + .html('' + + (bisAjax.i18n.saving || '保存中...')); + + // 判断是否需要强制生成新的 slug + if (forceNewSlug || $('.bis-regenerate-slug').data('force-new-slug')) { + forceNewSlug = true; + } + + // 添加日志记录 + logDebug('发送数据', { + collection: currentCollection, + collection_id: slug, + force_new_slug: forceNewSlug + }); + + $.ajax({ + url: bisAjax.ajaxurl, + type: 'POST', + data: { + action: 'bis_save_collection', + nonce: bisAjax.nonce, + collection: JSON.stringify(currentCollection), + collection_id: slug, + force_new_slug: forceNewSlug + }, + success: function(response) { + logDebug('保存响应', response); + + if (response.success) { + const newSlug = response.data.collection_id; + + // 如果 slug 发生了变化,更新表单 + if (slug !== newSlug) { + $('#bis-collection-id').val(newSlug); + $('.bis-slug-info').text(newSlug); + + // 显示 slug 信息区域 + $('.bis-slug-display').show(); + + if (forceNewSlug) { + showNotification('已成功重新生成 Slug', 'success'); + } else { + showNotification(bisAjax.i18n.slug_generated || '已为此集合自动生成 slug', 'info', 5000); + } + + // 重置强制生成新 slug 标记 + $('.bis-regenerate-slug').data('force-new-slug', false); + } else { + // 显示成功消息 + showNotification('集合保存成功!', 'success'); + } + + // 重新加载页面以显示更新后的集合 + setTimeout(function() { + window.location.reload(); + }, 1500); + } else { + showNotification(response.data.message || bisAjax.i18n.save_error || '保存集合时出错', 'error'); + $saveButton.prop('disabled', false).html(originalButtonText); + } + }, + error: function(xhr, status, error) { + console.error('保存错误:', xhr.responseText); + showNotification(bisAjax.i18n.save_error || '保存集合时出错', 'error'); + $saveButton.prop('disabled', false).html(originalButtonText); + } + }); + } + + // Show notification messages + function showNotification(message, type = 'info', duration = 3000) { + // Remove any existing notifications + $('.bis-notification').remove(); + + // Set notification class based on type + let notificationClass = 'bis-notification-info'; + let iconClass = 'dashicons-info'; + + if (type === 'success') { + notificationClass = 'bis-notification-success'; + iconClass = 'dashicons-yes'; + } else if (type === 'error') { + notificationClass = 'bis-notification-error'; + iconClass = 'dashicons-no'; + } else if (type === 'warning') { + notificationClass = 'bis-notification-warning'; + iconClass = 'dashicons-warning'; + } + + // Create notification element + const $notification = $( + `
+ + ${message} + +
` + ); + + // Add to page + $('body').append($notification); + + // Show with animation + setTimeout(function() { + $notification.addClass('bis-notification-show'); + }, 10); + + // Close button functionality + $notification.find('.bis-notification-close').on('click', function() { + $notification.removeClass('bis-notification-show'); + setTimeout(function() { + $notification.remove(); + }, 300); + }); + + // Auto close after duration + if (duration > 0) { + setTimeout(function() { + $notification.removeClass('bis-notification-show'); + setTimeout(function() { + $notification.remove(); + }, 300); + }, duration); + } + } + + // Delete collection + function deleteCollection(slug) { + // Add loading overlay + const $loadingOverlay = $('

删除中...

'); + $('body').append($loadingOverlay); + + $.ajax({ + url: bisAjax.ajaxurl, + type: 'POST', + data: { + action: 'bis_delete_collection', + nonce: bisAjax.nonce, + collection_id: slug + }, + success: function(response) { + $loadingOverlay.remove(); + + if (response.success) { + // Show success message + showNotification('集合已成功删除', 'success'); + // Reload page to reflect changes + setTimeout(function() { + window.location.reload(); + }, 1000); + } else { + showNotification(response.data.message || '删除集合时出错', 'error'); + } + }, + error: function(xhr, status, error) { + $loadingOverlay.remove(); + console.error('删除错误:', xhr.responseText); + showNotification('删除集合时出错: ' + error, 'error'); + } + }); + } + + // Export collections + function exportCollections() { + const format = $('#bis-export-format').val(); + + if (format === 'all') { + // Export all collections + $.ajax({ + url: bisAjax.rest_url, + type: 'GET', + success: function(response) { + downloadJson(response, 'collections.json'); + showNotification('所有集合导出成功', 'success'); + }, + error: function(xhr, status, error) { + console.error('导出错误:', xhr.responseText); + showNotification('导出集合时出错: ' + error, 'error'); + } + }); + } else { + // Export selected collection + const slug = $('#bis-export-collection').val(); + + if (!slug) { + showNotification('请选择要导出的集合', 'warning'); + return; + } + + // 使用 REST API 路径获取集合 + // 注意:我们使用正确的 URL 路径,"collection" 而不是 "collections" + const collectionUrl = bisAjax.rest_url.replace('/collections', '/collection/' + slug); + + $.ajax({ + url: collectionUrl, + type: 'GET', + success: function(response) { + // For single collection, we can optionally strip the wrapper and just export the collection + const collection = response.collections[slug]; + downloadJson(collection, slug + '.json'); + showNotification('集合导出成功', 'success'); + }, + error: function(xhr, status, error) { + console.error('导出失败:', xhr.responseJSON); + showNotification('导出集合时出错: ' + error, 'error'); + } + }); + } + } + + // Import collections + function importCollections() { + const importData = $('#bis-import-data').val(); + + if (!importData) { + showNotification('请粘贴集合 JSON 数据', 'warning'); + return; + } + + try { + // Validate JSON + JSON.parse(importData); + + // Add loading state + const $importButton = $('.bis-import-submit'); + const originalButtonText = $importButton.text(); + $importButton.prop('disabled', true) + .html('导入中...'); + + // Submit import + $.ajax({ + url: bisAjax.ajaxurl, + type: 'POST', + data: { + action: 'bis_import_collection', + nonce: bisAjax.nonce, + import_data: importData + }, + success: function(response) { + if (response.success) { + // Close modal + closeModals(); + // Show success message + showNotification(response.data.message || '集合导入成功', 'success'); + // Reload page to reflect changes + setTimeout(function() { + window.location.reload(); + }, 1000); + } else { + $importButton.prop('disabled', false).text(originalButtonText); + showNotification(response.data.message || '导入集合时出错', 'error'); + } + }, + error: function(xhr, status, error) { + $importButton.prop('disabled', false).text(originalButtonText); + console.error('导入错误:', xhr.responseText); + showNotification('导入集合时出错: ' + error, 'error'); + } + }); + } catch (e) { + showNotification(bisAjax.i18n.invalid_json || 'JSON 格式无效', 'error'); + } + } + + // Utility functions + function downloadJson(data, filename) { + const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2)); + const downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute("href", dataStr); + downloadAnchorNode.setAttribute("download", filename); + document.body.appendChild(downloadAnchorNode); + downloadAnchorNode.click(); + downloadAnchorNode.remove(); + } + + function copyToClipboard(text) { + // 使用现代 Clipboard API (如果可用) + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text).then(() => { + showNotification('已复制到剪贴板', 'success', 2000); + }).catch(() => { + // 如果 Clipboard API 失败,回退到传统方法 + fallbackCopyToClipboard(text); + }); + } else { + // 对于不支持 Clipboard API 的浏览器使用传统方法 + fallbackCopyToClipboard(text); + } + } + + function fallbackCopyToClipboard(text) { + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; // 防止滚动到视图底部 + document.body.appendChild(textarea); + textarea.select(); + + try { + const successful = document.execCommand('copy'); + if (successful) { + showNotification('已复制到剪贴板', 'success', 2000); + } else { + showNotification('复制失败,请手动复制', 'warning'); + } + } catch (err) { + showNotification('复制失败: ' + err, 'error'); + } + + document.body.removeChild(textarea); + } +}); diff --git a/bulk-installer-server.php b/bulk-installer-server.php new file mode 100644 index 0000000..c2aa63a --- /dev/null +++ b/bulk-installer-server.php @@ -0,0 +1,585 @@ + 'GET', + 'callback' => 'bis_rest_get_collections', + 'permission_callback' => '__return_true' + ]); + + // 按 slug 获取特定合集 - 使用 WordPress 标准的 slug 格式(字母、数字、连字符) + register_rest_route('bulk-installer-server/v1', '/collection/(?P[\w-]+)', [ + 'methods' => 'GET', + 'callback' => 'bis_rest_get_collection', + 'permission_callback' => '__return_true' + ]); +} + +/** + * REST API handler for getting all collections + */ +function bis_rest_get_collections() { + $collections = bis_get_collections(); + + return new WP_REST_Response([ + 'version' => BIS_VERSION, + 'last_updated' => gmdate('Y-m-d'), + 'collections' => $collections + ], 200); +} + +/** + * REST API handler for getting a specific collection + */ +function bis_rest_get_collection($request) { + $slug = $request['slug']; + $collections = bis_get_collections(); + + if (!isset($collections[$slug])) { + return new WP_REST_Response([ + 'error' => 'Collection not found', + 'requested_slug' => $slug, + 'available_slugs' => array_keys($collections) + ], 404); + } + + return new WP_REST_Response([ + 'version' => BIS_VERSION, + 'last_updated' => gmdate('Y-m-d'), + 'collections' => [ + $slug => $collections[$slug] + ] + ], 200); +} + +/** + * Add admin menu page + */ +function bis_add_menu_page() { + add_menu_page( + __('Collection Manager', 'bulk-installer-server'), + __('Collection Manager', 'bulk-installer-server'), + 'manage_options', + 'bulk-installer-server', + 'bis_render_admin_page', + 'dashicons-layout', + 65 + ); +} + +/** + * Enqueue admin scripts and styles + */ +function bis_admin_scripts($hook) { + if ($hook !== 'toplevel_page_bulk-installer-server') { + return; + } + + wp_enqueue_style('bis-admin-style', BIS_URL . 'assets/css/admin.css', [], BIS_VERSION); + wp_enqueue_script('bis-admin', BIS_URL . 'assets/js/admin.js', ['jquery', 'jquery-ui-sortable'], BIS_VERSION, true); + + wp_localize_script('bis-admin', 'bisAjax', [ + 'nonce' => wp_create_nonce('bis_nonce'), + 'ajaxurl' => admin_url('admin-ajax.php'), + 'rest_url' => rest_url('bulk-installer-server/v1/collections'), + 'siteurl' => site_url(), + 'i18n' => [ + 'confirm_delete' => __('Are you sure you want to delete this collection?', 'bulk-installer-server'), + 'saving' => __('Saving...', 'bulk-installer-server'), + 'save_error' => __('Error saving collection', 'bulk-installer-server'), + 'add_plugin' => __('Add Plugin', 'bulk-installer-server'), + 'add_theme' => __('Add Theme', 'bulk-installer-server'), + 'invalid_json' => __('Invalid JSON format', 'bulk-installer-server'), + 'add_collection' => __('Create New Collection', 'bulk-installer-server'), + 'edit_collection' => __('Edit Collection', 'bulk-installer-server'), + 'slug_generated' => __('A slug has been automatically generated for this collection', 'bulk-installer-server'), + 'name_required' => __('Collection name is required', 'bulk-installer-server'), + 'item_required' => __('Please add at least one plugin or theme', 'bulk-installer-server'), + 'confirm_regenerate_slug' => __('This will generate a new slug for this collection. API URLs and bookmarks to this collection may break. Continue?', 'bulk-installer-server') + ] + ]); + + // Enqueue WordPress media scripts + wp_enqueue_media(); +} + +/** + * Render the admin page + */ +function bis_render_admin_page() { + if (!current_user_can('manage_options')) { + wp_die(__('You do not have sufficient permissions to access this page.', 'bulk-installer-server')); + } + + $collections = bis_get_collections(); + include BIS_PATH . 'templates/admin-page.php'; +} + +/** + * Get all collections + * + * @return array Collections data + */ +function bis_get_collections() { + $collections = get_option('bis_collections', []); + return $collections; +} + +/** + * Generate a WordPress-compatible slug + * + * @param string $name The collection name + * @param array $existing_collections Existing collections + * @return string The generated slug + */ +function bis_generate_slug($name, $existing_collections = []) { + // 使用 WordPress 原生函数生成 slug + $slug = sanitize_title($name); + + // 如果 slug 为空(可能由于只包含特殊字符),则生成一个默认 slug + if (empty($slug)) { + $slug = 'collection-' . substr(md5($name), 0, 8); + } + + $original_slug = $slug; + $counter = 1; + + // 确保 slug 的唯一性 + while (isset($existing_collections[$slug])) { + $slug = $original_slug . '-' . $counter; + $counter++; + } + + return $slug; +} + +/** + * Save a collection + * + * @param array $collection Collection data + * @param string $slug Collection slug + * @param bool $force_new_slug Whether to force generating a new slug + * @return bool|string True if successful, error message if failed + */ +function bis_save_collection($collection, $slug = '', $force_new_slug = false) { + if (!current_user_can('manage_options')) { + return __('Insufficient permissions', 'bulk-installer-server'); + } + + $collections = bis_get_collections(); + + // 获取集合名称 + $name = isset($collection['name']) ? trim($collection['name']) : ''; + + if (empty($name)) { + return __('Collection name is required', 'bulk-installer-server'); + } + + // 处理 slug 生成 + if (empty($slug) || $force_new_slug) { + // 生成新的 slug + $slug = bis_generate_slug($name, $collections); + } else if (!isset($collections[$slug])) { + // 如果提供了 slug 但不存在,检查它是否是有效的 + if (!preg_match('/^[\w-]+$/', $slug)) { + // 无效的 slug,生成新的 + $slug = bis_generate_slug($name, $collections); + } + } + + // Sanitize collection data + $collection['name'] = sanitize_text_field($name); + $collection['slug'] = $slug; // 存储生成的 slug + $collection['description'] = isset($collection['description']) ? sanitize_textarea_field($collection['description']) : ''; + $collection['icon'] = isset($collection['icon']) ? sanitize_text_field($collection['icon']) : 'dashicons-admin-plugins'; + $collection['category'] = isset($collection['category']) ? sanitize_text_field($collection['category']) : 'other'; + $collection['level'] = isset($collection['level']) ? sanitize_text_field($collection['level']) : 'beginner'; + $collection['author'] = isset($collection['author']) ? sanitize_text_field($collection['author']) : get_bloginfo('name'); + + if (!empty($collection['screenshot'])) { + $collection['screenshot'] = esc_url_raw($collection['screenshot']); + } + + // Sanitize plugins/themes + $collection['plugins'] = isset($collection['plugins']) ? bis_sanitize_items($collection['plugins']) : ['repository' => [], 'wenpai' => [], 'url' => []]; + $collection['themes'] = isset($collection['themes']) ? bis_sanitize_items($collection['themes']) : ['repository' => [], 'wenpai' => [], 'url' => []]; + + // Store in the collections array + $collections[$slug] = $collection; + + // Save to the database + $updated = update_option('bis_collections', $collections); + + if (!$updated) { + return __('Failed to save collection', 'bulk-installer-server'); + } + + return true; +} + +/** + * Sanitize plugin/theme items + * + * @param array $items Items to sanitize + * @return array Sanitized items + */ +function bis_sanitize_items($items) { + $sanitized = [ + 'repository' => [], + 'wenpai' => [], + 'url' => [] + ]; + + if (!is_array($items)) { + return $sanitized; + } + + foreach (['repository', 'wenpai', 'url'] as $source) { + if (!isset($items[$source]) || !is_array($items[$source])) { + continue; + } + + foreach ($items[$source] as $item) { + if (is_array($item)) { + $sanitized_item = [ + 'id' => isset($item['id']) ? absint($item['id']) : 0, + 'slug' => sanitize_text_field($item['slug'] ?? ''), + 'name' => sanitize_text_field($item['name'] ?? ''), + 'description' => sanitize_textarea_field($item['description'] ?? ''), + 'required' => !empty($item['required']) + ]; + + // Add URL for URL source items + if ($source === 'url' && !empty($item['url'])) { + $sanitized_item['url'] = esc_url_raw($item['url']); + } + + $sanitized[$source][] = $sanitized_item; + } else if (is_string($item)) { + $sanitized[$source][] = sanitize_text_field($item); + } + } + } + + return $sanitized; +} + +/** + * Ajax handler for getting a collection + */ +function bis_ajax_get_collection() { + check_ajax_referer('bis_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error([ + 'message' => __('Insufficient permissions', 'bulk-installer-server') + ]); + } + + $slug = isset($_GET['collection_id']) ? sanitize_text_field(wp_unslash($_GET['collection_id'])) : ''; + + if (empty($slug)) { + wp_send_json_error([ + 'message' => __('No collection ID provided', 'bulk-installer-server') + ]); + } + + $collections = bis_get_collections(); + + if (!isset($collections[$slug])) { + wp_send_json_error([ + 'message' => __('Collection not found', 'bulk-installer-server'), + 'requested_slug' => $slug, + 'available_slugs' => array_keys($collections) + ]); + } + + wp_send_json_success([ + 'collection' => $collections[$slug] + ]); +} + +/** + * Ajax handler for saving collections + */ +function bis_ajax_save_collection() { + check_ajax_referer('bis_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error([ + 'message' => __('Insufficient permissions', 'bulk-installer-server') + ]); + } + + $collection_data = isset($_POST['collection']) ? json_decode(stripslashes($_POST['collection']), true) : []; + $slug = isset($_POST['collection_id']) ? sanitize_text_field(wp_unslash($_POST['collection_id'])) : ''; + $force_new_slug = isset($_POST['force_new_slug']) && $_POST['force_new_slug'] === 'true'; + + if (empty($collection_data) || !is_array($collection_data)) { + wp_send_json_error([ + 'message' => __('Invalid collection data', 'bulk-installer-server') + ]); + } + + $result = bis_save_collection($collection_data, $slug, $force_new_slug); + + if ($result === true) { + // 如果是新集合,我们需要找到新的 slug + if (empty($slug) || $force_new_slug) { + $slug = bis_generate_slug($collection_data['name'], bis_get_collections()); + + // 再次检查是否匹配,因为可能在保存过程中其他集合也创建了相同的 slug + $collections = bis_get_collections(); + foreach ($collections as $collection_slug => $collection) { + if ($collection['name'] === $collection_data['name'] && $collection_slug !== $slug) { + $slug = $collection_slug; + break; + } + } + } + + wp_send_json_success([ + 'message' => __('Collection saved successfully', 'bulk-installer-server'), + 'collection_id' => $slug, + 'collections' => bis_get_collections() + ]); + } else { + wp_send_json_error([ + 'message' => $result + ]); + } +} + +/** + * Ajax handler for deleting collections + */ +function bis_ajax_delete_collection() { + check_ajax_referer('bis_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error([ + 'message' => __('Insufficient permissions', 'bulk-installer-server') + ]); + } + + $slug = isset($_POST['collection_id']) ? sanitize_text_field(wp_unslash($_POST['collection_id'])) : ''; + + if (empty($slug)) { + wp_send_json_error([ + 'message' => __('No collection ID provided', 'bulk-installer-server') + ]); + } + + $collections = bis_get_collections(); + + if (!isset($collections[$slug])) { + wp_send_json_error([ + 'message' => __('Collection not found', 'bulk-installer-server') + ]); + } + + unset($collections[$slug]); + + $updated = update_option('bis_collections', $collections); + + if (!$updated) { + wp_send_json_error([ + 'message' => __('Failed to delete collection', 'bulk-installer-server') + ]); + } + + wp_send_json_success([ + 'message' => __('Collection deleted successfully', 'bulk-installer-server'), + 'collections' => $collections + ]); +} + +/** + * Ajax handler for importing collections + */ +function bis_ajax_import_collection() { + check_ajax_referer('bis_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error([ + 'message' => __('Insufficient permissions', 'bulk-installer-server') + ]); + } + + $json_data = isset($_POST['import_data']) ? json_decode(stripslashes($_POST['import_data']), true) : []; + + if (empty($json_data) || !is_array($json_data)) { + wp_send_json_error([ + 'message' => __('Invalid JSON data', 'bulk-installer-server') + ]); + } + + $imported = 0; + $existing_collections = bis_get_collections(); + + // Check if we have a "collections" key (full format) or direct collection data + if (isset($json_data['collections']) && is_array($json_data['collections'])) { + foreach ($json_data['collections'] as $imported_slug => $collection) { + if (isset($existing_collections[$imported_slug])) { + // 已存在同名集合,生成新 slug + $result = bis_save_collection($collection); + } else { + // 使用导入文件中的 slug + $result = bis_save_collection($collection, $imported_slug); + } + + if ($result === true) { + $imported++; + } + } + } else { + // Assume it's a single collection + $result = bis_save_collection($json_data); + + if ($result === true) { + $imported++; + } + } + + if ($imported > 0) { + wp_send_json_success([ + 'message' => sprintf(_n('%d collection imported successfully', '%d collections imported successfully', $imported, 'bulk-installer-server'), $imported), + 'collections' => bis_get_collections() + ]); + } else { + wp_send_json_error([ + 'message' => __('No collections were imported', 'bulk-installer-server') + ]); + } +} + +/** + * Plugin activation hook + */ +register_activation_hook(__FILE__, 'bis_activate'); +function bis_activate() { + if (version_compare(PHP_VERSION, '7.4', '<')) { + deactivate_plugins(plugin_basename(__FILE__)); + wp_die(__('This plugin requires PHP 7.4 or higher.', 'bulk-installer-server')); + } + + // Create default collection if none exist + $collections = bis_get_collections(); + + if (empty($collections)) { + $default_collection = [ + 'name' => __('Business Website', 'bulk-installer-server'), + 'description' => __('Essential plugins for a professional business website.', 'bulk-installer-server'), + 'icon' => 'dashicons-building', + 'category' => 'business', + 'level' => 'beginner', + 'author' => get_bloginfo('name'), + 'plugins' => [ + 'repository' => [ + [ + 'id' => 1, + 'slug' => 'wordpress-seo', + 'name' => 'Yoast SEO', + 'description' => __('The leading SEO plugin for WordPress', 'bulk-installer-server'), + 'required' => true + ], + [ + 'id' => 2, + 'slug' => 'contact-form-7', + 'name' => 'Contact Form 7', + 'description' => __('Simple but flexible contact form plugin', 'bulk-installer-server'), + 'required' => true + ] + ], + 'wenpai' => [], + 'url' => [] + ], + 'themes' => [ + 'repository' => [ + [ + 'id' => 3, + 'slug' => 'astra', + 'name' => 'Astra', + 'description' => __('Fast, lightweight theme for business websites', 'bulk-installer-server'), + 'required' => false + ] + ], + 'wenpai' => [], + 'url' => [] + ] + ]; + + bis_save_collection($default_collection, 'business'); + } + + // Create required directories + $upload_dir = wp_upload_dir(); + $export_dir = $upload_dir['basedir'] . '/bis-exports'; + + if (!file_exists($export_dir)) { + wp_mkdir_p($export_dir); + } + + // Create .htaccess file to protect directory + $htaccess_file = $export_dir . '/.htaccess'; + if (!file_exists($htaccess_file)) { + $htaccess_content = "Options -Indexes\n"; + $htaccess_content .= "\n"; + $htaccess_content .= "Header set Access-Control-Allow-Origin \"*\"\n"; + $htaccess_content .= "Header set Content-Type \"application/json\"\n"; + $htaccess_content .= "\n"; + + file_put_contents($htaccess_file, $htaccess_content); + } +} + +/** + * Get collection JSON URL + * + * @param string $slug Collection slug + * @return string Collection JSON URL + */ +function bis_get_collection_json_url($slug) { + return rest_url("bulk-installer-server/v1/collection/" . urlencode($slug)); +} diff --git a/templates/admin-page.php b/templates/admin-page.php new file mode 100644 index 0000000..096ed5f --- /dev/null +++ b/templates/admin-page.php @@ -0,0 +1,514 @@ + +
+

+ +

+ +
+
+
+

+

+ +
+ +

+ +
    + $collection) : ?> +
  • +
    + +
    +
    +

    +
    + $plugins) { + $plugin_count += count($plugins); + } + } + + if (!empty($collection['themes'])) { + foreach ($collection['themes'] as $source => $themes) { + $theme_count += count($themes); + } + } + + $category = !empty($collection['category']) ? $collection['category'] : ''; + $level = !empty($collection['level']) ? $collection['level'] : ''; + ?> + + + + + + + + + + + + + + +
    +
    +
    + + +
    +
  • + +
+ +
+ +
+ + +
+
+ +
+

+

+ +
+
+ + +
+ + + + +
+
+ +
+

+

+ +
+
+ +
+ + +
+
+ + +
+ + $collection) : ?> +
+ : + + +
+ +
+ +
+ +
+

+
    +
  1. +
  2. Settings', 'bulk-installer-server'); ?>
  3. +
  4. +
  5. +
+
+
+
+ +
+ + +
+

+

+ +
+
+
+ +
+
+

+

+
+
+ +
+
+ +
+
+

+

+
+
+ +
+
+ +
+
+

+

+
+
+ +
+
+ +
+
+

+

+
+
+
+ +
+ +
+
+
+
+ + + + + + + + + +