mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-06-23 14:01:11 +02:00
- Calculate total installed plugins count - Calculate total active/enabled plugins count - Display statistics in page header with icons - Shows 'Installed: X' and 'Active: Y' counts - Statistics only shown when plugins are installed - Improves visibility of plugin status at a glance
2206 lines
79 KiB
HTML
2206 lines
79 KiB
HTML
{% extends "baseTemplate/index.html" %}
|
|
{% load i18n %}
|
|
{% block title %}{% trans "Installed Plugins - CyberPanel" %}{% endblock %}
|
|
|
|
{% block header_scripts %}
|
|
<style>
|
|
/* Installed Plugins Specific Styles */
|
|
.plugins-wrapper {
|
|
background: transparent;
|
|
padding: 20px;
|
|
}
|
|
|
|
.plugins-container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Page Header */
|
|
.page-header {
|
|
background: var(--bg-primary, white);
|
|
border-radius: 12px;
|
|
padding: 25px;
|
|
margin-bottom: 25px;
|
|
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
|
|
border: 1px solid var(--border-primary, #e8e9ff);
|
|
}
|
|
|
|
.page-header h1 {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
color: var(--text-primary, #2f3640);
|
|
margin: 0 0 10px 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.page-header .icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
background: #5856d6;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-size: 24px;
|
|
box-shadow: 0 4px 12px rgba(88,86,214,0.3);
|
|
}
|
|
|
|
.page-header p {
|
|
font-size: 15px;
|
|
color: var(--text-secondary, #64748b);
|
|
margin: 0;
|
|
}
|
|
|
|
/* Content Section */
|
|
.content-section {
|
|
background: var(--bg-primary, white);
|
|
border-radius: 12px;
|
|
padding: 25px;
|
|
margin-bottom: 25px;
|
|
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
|
|
border: 1px solid var(--border-primary, #e8e9ff);
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--text-primary, #2f3640);
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.section-title::before {
|
|
content: '';
|
|
width: 4px;
|
|
height: 24px;
|
|
background: #5856d6;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* Plugin Cards */
|
|
.plugins-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.plugin-card {
|
|
background: var(--bg-primary, white);
|
|
border: 1px solid var(--border-primary, #e8e9ff);
|
|
border-radius: 12px;
|
|
padding: 25px;
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.plugin-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: var(--shadow-lg, 0 8px 24px rgba(0,0,0,0.1));
|
|
border-color: #5856d6;
|
|
}
|
|
|
|
.plugin-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 4px;
|
|
background: linear-gradient(90deg, #5856d6, #4a90e2);
|
|
transform: translateX(-100%);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.plugin-card:hover::before {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.plugin-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.plugin-icon {
|
|
width: 56px;
|
|
height: 56px;
|
|
background: var(--bg-secondary, #f8f9ff);
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
font-size: 24px;
|
|
color: #5856d6;
|
|
}
|
|
|
|
.plugin-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.plugin-name {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--text-primary, #2f3640);
|
|
margin-bottom: 5px;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.plugin-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.plugin-type {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
background: var(--purple-light, #e8e6ff);
|
|
color: #5856d6;
|
|
border-radius: 6px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.plugin-version-number {
|
|
background: var(--bg-secondary, #f8f9ff);
|
|
padding: 3px 8px;
|
|
border-radius: 4px;
|
|
font-weight: 600;
|
|
font-size: 11px;
|
|
color: var(--text-secondary, #64748b);
|
|
}
|
|
|
|
.plugin-pricing-badge {
|
|
display: inline-block;
|
|
padding: 3px 8px;
|
|
border-radius: 4px;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-left: 6px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.plugin-pricing-badge.free {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
border: 1px solid #c3e6cb;
|
|
}
|
|
|
|
.plugin-pricing-badge.paid {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
border: 1px solid #ffeaa7;
|
|
}
|
|
|
|
/* NEW and Stale badges */
|
|
.plugin-status-badge {
|
|
display: inline-block;
|
|
padding: 3px 8px;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
margin-left: 8px;
|
|
vertical-align: middle;
|
|
cursor: help;
|
|
position: relative;
|
|
}
|
|
|
|
.plugin-status-badge.new {
|
|
background: #ffc107;
|
|
color: #000;
|
|
box-shadow: 0 0 0 2px rgba(255, 193, 7, 0.3);
|
|
}
|
|
|
|
.plugin-status-badge.stale {
|
|
background: #dc3545;
|
|
color: white;
|
|
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.3);
|
|
}
|
|
|
|
.paid-badge {
|
|
display: inline-block;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-left: 8px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.paid-badge i {
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.subscription-warning {
|
|
background: #fff3cd;
|
|
border: 1px solid #ffc107;
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
margin-top: 10px;
|
|
font-size: 12px;
|
|
color: #856404;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
}
|
|
|
|
.subscription-warning-content {
|
|
display: flex;
|
|
align-items: center;
|
|
flex: 1;
|
|
}
|
|
|
|
.subscription-warning i {
|
|
margin-right: 6px;
|
|
color: #ffc107;
|
|
}
|
|
|
|
.subscription-warning-button {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 8px 16px;
|
|
background: linear-gradient(135deg, #f96854 0%, #f96854 100%);
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
transition: all 0.3s ease;
|
|
white-space: nowrap;
|
|
box-shadow: 0 2px 4px rgba(249, 104, 84, 0.3);
|
|
}
|
|
|
|
.subscription-warning-button:hover {
|
|
background: linear-gradient(135deg, #e55a47 0%, #e55a47 100%);
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(249, 104, 84, 0.4);
|
|
color: white;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.subscription-warning-button i {
|
|
margin-right: 0;
|
|
color: white;
|
|
}
|
|
|
|
.plugin-description {
|
|
font-size: 13px;
|
|
color: var(--text-secondary, #64748b);
|
|
line-height: 1.6;
|
|
margin-bottom: 15px;
|
|
min-height: 40px;
|
|
}
|
|
|
|
.plugin-status-section {
|
|
padding: 12px;
|
|
background: var(--bg-secondary, #f8f9ff);
|
|
border-radius: 8px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.plugin-status-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.plugin-status-row:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.plugin-status-row .label {
|
|
color: var(--text-secondary, #64748b);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.status-installed-small {
|
|
display: inline-block;
|
|
padding: 3px 8px;
|
|
background: #d4edda;
|
|
color: #155724;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.status-not-installed-small {
|
|
display: inline-block;
|
|
padding: 3px 8px;
|
|
background: #f8f9fa;
|
|
color: #6c757d;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.active-status {
|
|
font-size: 14px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.active-status.active-yes {
|
|
color: #28a745;
|
|
}
|
|
|
|
.active-status.active-no {
|
|
color: #dc3545;
|
|
}
|
|
|
|
.active-status.active-na {
|
|
color: #6c757d;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.plugin-footer {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
padding-top: 15px;
|
|
border-top: 1px solid var(--border-primary, #e8e9ff);
|
|
}
|
|
|
|
.plugin-links {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn-small {
|
|
padding: 6px 12px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.btn-link-small {
|
|
padding: 4px 10px;
|
|
font-size: 11px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Table View (Alternative) */
|
|
.plugins-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.plugins-table th {
|
|
background: var(--bg-secondary, #f8f9ff);
|
|
padding: 15px;
|
|
text-align: left;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary, #64748b);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
border-bottom: 2px solid var(--border-primary, #e8e9ff);
|
|
}
|
|
|
|
.plugins-table td {
|
|
padding: 20px 15px;
|
|
border-bottom: 1px solid var(--border-primary, #e8e9ff);
|
|
font-size: 14px;
|
|
color: var(--text-primary, #2f3640);
|
|
}
|
|
|
|
.plugins-table tr:hover {
|
|
background: var(--bg-hover, #f8f9ff);
|
|
}
|
|
|
|
.plugins-table tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
/* Empty State */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
}
|
|
|
|
.empty-icon {
|
|
width: 80px;
|
|
height: 80px;
|
|
background: var(--bg-secondary, #f8f9ff);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0 auto 20px;
|
|
font-size: 36px;
|
|
color: var(--text-muted, #94a3b8);
|
|
}
|
|
|
|
.empty-title {
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
color: var(--text-primary, #2f3640);
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.empty-description {
|
|
font-size: 15px;
|
|
color: var(--text-secondary, #64748b);
|
|
max-width: 400px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* View Toggle */
|
|
.view-toggle {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.view-btn {
|
|
padding: 8px 16px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
border: 1px solid var(--border-primary, #e8e9ff);
|
|
background: var(--bg-primary, white);
|
|
color: var(--text-secondary, #64748b);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.view-btn.active {
|
|
background: #5856d6;
|
|
color: white;
|
|
border-color: #5856d6;
|
|
}
|
|
|
|
.view-btn:hover:not(.active) {
|
|
background: var(--bg-hover, #f8f9ff);
|
|
color: #5856d6;
|
|
border-color: #5856d6;
|
|
}
|
|
|
|
/* Style links that use view-btn class */
|
|
a.view-btn {
|
|
color: var(--text-secondary, #64748b);
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
a.view-btn:hover {
|
|
background: var(--bg-hover, #f8f9ff);
|
|
color: #5856d6;
|
|
border-color: #5856d6;
|
|
}
|
|
|
|
/* Alert Messages */
|
|
.alert {
|
|
padding: 15px 20px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.alert-icon {
|
|
font-size: 20px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.alert-danger {
|
|
background: var(--alert-danger-bg, #fee2e2);
|
|
color: var(--alert-danger-text, #991b1b);
|
|
border: 1px solid var(--alert-danger-border, #fecaca);
|
|
}
|
|
|
|
.alert-danger .alert-icon {
|
|
color: var(--alert-danger-icon, #ef4444);
|
|
}
|
|
|
|
/* Plugin Store Styles */
|
|
.store-notice {
|
|
background: #fff3cd;
|
|
border: 1px solid #ffc107;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-bottom: 25px;
|
|
position: relative;
|
|
}
|
|
|
|
.notice-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
font-weight: 700;
|
|
color: #856404;
|
|
}
|
|
|
|
.notice-close {
|
|
background: none;
|
|
border: none;
|
|
font-size: 18px;
|
|
color: #856404;
|
|
cursor: pointer;
|
|
padding: 5px;
|
|
border-radius: 4px;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.notice-close:hover {
|
|
background: rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.notice-content p {
|
|
margin: 10px 0;
|
|
color: #856404;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.warning-text {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: #ff6b00 !important;
|
|
font-weight: 600;
|
|
margin-top: 15px !important;
|
|
}
|
|
|
|
.warning-text i {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.store-loading {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 4px solid #f3f3f3;
|
|
border-top: 4px solid #5856d6;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 20px;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.alphabet-filter {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-bottom: 20px;
|
|
padding: 15px;
|
|
background: var(--bg-secondary, #f8f9ff);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.alpha-btn {
|
|
padding: 6px 12px;
|
|
border: 1px solid var(--border-primary, #e8e9ff);
|
|
background: var(--bg-primary, white);
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
color: var(--text-secondary, #64748b);
|
|
min-width: 36px;
|
|
text-align: center;
|
|
}
|
|
|
|
.alpha-btn:hover {
|
|
background: var(--bg-hover, #f0f1ff);
|
|
border-color: #5856d6;
|
|
color: #5856d6;
|
|
}
|
|
|
|
.alpha-btn.active {
|
|
background: #5856d6;
|
|
color: white;
|
|
border-color: #5856d6;
|
|
}
|
|
|
|
/* Store Search Styles */
|
|
.store-search-container {
|
|
padding: 15px;
|
|
background: var(--bg-secondary, #f8f9ff);
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.store-search-wrapper {
|
|
position: relative;
|
|
}
|
|
|
|
.store-search-input {
|
|
width: 100%;
|
|
padding: 12px 15px 12px 45px;
|
|
border: 2px solid #e8e9ff;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
transition: all 0.3s ease;
|
|
background: white;
|
|
color: var(--text-primary, #2f3640);
|
|
}
|
|
|
|
.store-search-input:focus {
|
|
outline: none;
|
|
border-color: #5856d6;
|
|
box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1);
|
|
}
|
|
|
|
.store-search-input::placeholder {
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.clear-search-btn {
|
|
display: none;
|
|
position: absolute;
|
|
right: 10px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
background: none;
|
|
border: none;
|
|
color: #64748b;
|
|
cursor: pointer;
|
|
padding: 5px;
|
|
font-size: 14px;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.clear-search-btn:hover {
|
|
color: #dc3545;
|
|
}
|
|
|
|
.clear-search-btn.visible {
|
|
display: block;
|
|
}
|
|
|
|
.store-table-wrapper {
|
|
overflow-x: auto;
|
|
background: var(--bg-primary, white);
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border-primary, #e8e9ff);
|
|
}
|
|
|
|
.store-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.store-table th {
|
|
background: #2f3640;
|
|
color: white;
|
|
padding: 15px;
|
|
text-align: left;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
border-bottom: 2px solid #1a1d2e;
|
|
}
|
|
|
|
.store-table td {
|
|
padding: 18px 15px;
|
|
border-bottom: 1px solid var(--border-primary, #e8e9ff);
|
|
font-size: 14px;
|
|
color: var(--text-primary, #2f3640);
|
|
}
|
|
|
|
.store-table tr:hover {
|
|
background: var(--bg-hover, #f8f9ff);
|
|
}
|
|
|
|
.store-table tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.status-action {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.status-installed {
|
|
display: inline-block;
|
|
padding: 6px 12px;
|
|
background: #d4edda;
|
|
color: #155724;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.btn-install {
|
|
padding: 8px 16px;
|
|
background: #5856d6;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.btn-install:hover:not(:disabled) {
|
|
background: #4a48c4;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(88,86,214,0.3);
|
|
}
|
|
|
|
.btn-install:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-link {
|
|
padding: 6px 12px;
|
|
background: var(--bg-secondary, #f8f9ff);
|
|
color: var(--text-secondary, #64748b);
|
|
text-decoration: none;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
transition: all 0.2s;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.btn-link:hover {
|
|
background: var(--bg-hover, #f0f1ff);
|
|
color: #5856d6;
|
|
}
|
|
|
|
/* Status and Action Columns */
|
|
.status-installed {
|
|
display: inline-block;
|
|
padding: 4px 10px;
|
|
background: #d4edda;
|
|
color: #155724;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.status-not-installed {
|
|
display: inline-block;
|
|
padding: 4px 10px;
|
|
background: #f8f9fa;
|
|
color: #6c757d;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.btn-action {
|
|
padding: 6px 12px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.btn-install {
|
|
background: #5856d6;
|
|
color: white;
|
|
}
|
|
|
|
.btn-install:hover:not(:disabled) {
|
|
background: #4a48c4;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(88,86,214,0.3);
|
|
}
|
|
|
|
.btn-uninstall {
|
|
background: #dc3545;
|
|
color: white;
|
|
}
|
|
|
|
.btn-uninstall:hover:not(:disabled) {
|
|
background: #c82333;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(220,53,69,0.3);
|
|
}
|
|
|
|
.btn-activate {
|
|
background: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.btn-activate:hover:not(:disabled) {
|
|
background: #218838;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(40,167,69,0.3);
|
|
}
|
|
|
|
.btn-deactivate {
|
|
background: #ffc107;
|
|
color: #212529;
|
|
}
|
|
|
|
.btn-deactivate:hover:not(:disabled) {
|
|
background: #e0a800;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(255,193,7,0.3);
|
|
}
|
|
|
|
.btn-settings {
|
|
background: #5856d6;
|
|
color: white;
|
|
}
|
|
|
|
.btn-settings:hover {
|
|
background: #4a48c4;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(88,86,214,0.3);
|
|
}
|
|
|
|
.btn-action:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.plugin-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* Active Column */
|
|
.active-column {
|
|
text-align: center;
|
|
}
|
|
|
|
.active-status {
|
|
font-size: 18px;
|
|
display: inline-block;
|
|
}
|
|
|
|
.active-status.active-yes {
|
|
color: #28a745;
|
|
}
|
|
|
|
.active-status.active-no {
|
|
color: #dc3545;
|
|
}
|
|
|
|
.active-status.active-na {
|
|
color: #6c757d;
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.plugins-wrapper {
|
|
padding: 15px;
|
|
}
|
|
|
|
.page-header h1 {
|
|
font-size: 24px;
|
|
flex-direction: column;
|
|
text-align: center;
|
|
}
|
|
|
|
.content-section {
|
|
padding: 20px;
|
|
}
|
|
|
|
.plugins-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.plugins-table-wrapper {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.view-toggle {
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.alphabet-filter {
|
|
padding: 10px;
|
|
}
|
|
|
|
.alpha-btn {
|
|
padding: 5px 10px;
|
|
font-size: 12px;
|
|
min-width: 32px;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{% load static %}
|
|
{% get_current_language as LANGUAGE_CODE %}
|
|
|
|
<div class="plugins-wrapper">
|
|
<div class="plugins-container">
|
|
<!-- Page Header -->
|
|
<div class="page-header">
|
|
<h1>
|
|
<div class="icon">
|
|
<i class="fas fa-plug"></i>
|
|
</div>
|
|
{% trans "Installed Plugins" %}
|
|
</h1>
|
|
<p>{% trans "List of installed plugins on your CyberPanel" %}</p>
|
|
{% if plugins %}
|
|
<div style="margin-top: 15px; display: flex; gap: 20px; flex-wrap: wrap;">
|
|
<div style="display: flex; align-items: center; gap: 8px; color: var(--text-secondary, #64748b); font-size: 14px;">
|
|
<i class="fas fa-check-circle" style="color: #10b981;"></i>
|
|
<strong>{% trans "Installed:" %}</strong>
|
|
<span style="color: var(--text-primary, #2f3640); font-weight: 600;">{{ total_installed }}</span>
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 8px; color: var(--text-secondary, #64748b); font-size: 14px;">
|
|
<i class="fas fa-power-off" style="color: #3b82f6;"></i>
|
|
<strong>{% trans "Active:" %}</strong>
|
|
<span style="color: var(--text-primary, #2f3640); font-weight: 600;">{{ total_active }}</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Plugins Section -->
|
|
<div class="content-section">
|
|
<h2 class="section-title">{% trans "Plugins" %}</h2>
|
|
|
|
<!-- View Toggle (always shown) -->
|
|
<div class="view-toggle">
|
|
{% if plugins %}
|
|
<button class="view-btn active" onclick="toggleView('grid')">
|
|
<i class="fas fa-th-large"></i>
|
|
{% trans "Grid View" %}
|
|
</button>
|
|
<button class="view-btn" onclick="toggleView('table')">
|
|
<i class="fas fa-list"></i>
|
|
{% trans "Table View" %}
|
|
</button>
|
|
<button class="view-btn" onclick="toggleView('store')">
|
|
<i class="fas fa-store"></i>
|
|
{% trans "CyberPanel Plugin Store" %}
|
|
</button>
|
|
{% else %}
|
|
<!-- Hide Grid/Table views when no plugins installed - only show Store -->
|
|
<button class="view-btn" onclick="toggleView('grid')" style="display: none;">
|
|
<i class="fas fa-th-large"></i>
|
|
{% trans "Grid View" %}
|
|
</button>
|
|
<button class="view-btn" onclick="toggleView('table')" style="display: none;">
|
|
<i class="fas fa-list"></i>
|
|
{% trans "Table View" %}
|
|
</button>
|
|
<button class="view-btn active" onclick="toggleView('store')">
|
|
<i class="fas fa-store"></i>
|
|
{% trans "CyberPanel Plugin Store" %}
|
|
</button>
|
|
{% endif %}
|
|
<a href="/plugins/help/" class="view-btn" style="text-decoration: none;">
|
|
<i class="fas fa-book"></i>
|
|
{% trans "Plugin Development Guide" %}
|
|
</a>
|
|
</div>
|
|
|
|
{% if plugins %}
|
|
|
|
<!-- Grid View -->
|
|
<div id="gridView" class="plugins-grid">
|
|
{% for plugin in plugins %}
|
|
<div class="plugin-card">
|
|
<div class="plugin-header">
|
|
<div class="plugin-icon">
|
|
{% if plugin.type == "Security" %}
|
|
<i class="fas fa-shield-alt"></i>
|
|
{% elif plugin.type == "Performance" %}
|
|
<i class="fas fa-rocket"></i>
|
|
{% elif plugin.type == "Utility" %}
|
|
<i class="fas fa-tools"></i>
|
|
{% elif plugin.type == "Backup" %}
|
|
<i class="fas fa-save"></i>
|
|
{% else %}
|
|
<i class="fas fa-puzzle-piece"></i>
|
|
{% endif %}
|
|
</div>
|
|
<div class="plugin-info">
|
|
<h3 class="plugin-name">
|
|
{{ plugin.name }}
|
|
{% if plugin.is_paid|default:False|default_if_none:False %} <span class="paid-badge" title="{% trans 'Paid Plugin - Patreon Subscription Required' %}"><i class="fas fa-crown"></i> {% trans "Premium" %}</span>{% endif %}
|
|
{% if plugin.is_new|default:False %} <span class="plugin-status-badge new" title="{% trans 'This plugin was released/updated within the last 3 months' %}">NEW</span>{% endif %}
|
|
{% if plugin.is_stale|default:False %} <span class="plugin-status-badge stale" title="{% trans 'This plugin is marked \'Stale\' (Last release over two years ago). This means it may work fine, but it has not had any recent development. Use your own discretion when using this plugin!' %}">STALE</span>{% endif %}
|
|
</h3>
|
|
<div class="plugin-meta">
|
|
<span class="plugin-type">{{ plugin.type }}</span>
|
|
<span class="plugin-version-number">v{{ plugin.version }}</span>
|
|
{% if plugin.is_paid|default:False|default_if_none:False %}
|
|
<span class="plugin-pricing-badge paid">{% trans "Paid" %}</span>
|
|
{% else %}
|
|
<span class="plugin-pricing-badge free">{% trans "Free" %}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="plugin-description">
|
|
{{ plugin.desc }}
|
|
{% if plugin.is_paid|default:False|default_if_none:False %}
|
|
<div class="subscription-warning">
|
|
<div class="subscription-warning-content">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
{% if plugin.payment_type == 'paypal' %}
|
|
<strong>{% trans "Paid Plugin:" %}</strong> {% trans "Requires PayPal payment to access." %}
|
|
{% else %}
|
|
<strong>{% trans "Paid Plugin:" %}</strong> {% trans "Requires Patreon subscription to" %} "{{ plugin.patreon_tier|default:'CyberPanel Paid Plugin' }}"
|
|
{% endif %}
|
|
</div>
|
|
{% if plugin.payment_type == 'paypal' %}
|
|
{% if plugin.paypal_me_url %}
|
|
<a href="{{ plugin.paypal_me_url }}" target="_blank" rel="noopener noreferrer" class="subscription-warning-button" style="background: linear-gradient(135deg, #0070ba 0%, #003087 100%);">
|
|
<i class="fab fa-paypal"></i>
|
|
{% trans "Pay with PayPal.me" %}
|
|
</a>
|
|
{% endif %}
|
|
{% if plugin.paypal_payment_link %}
|
|
<a href="{{ plugin.paypal_payment_link }}" target="_blank" rel="noopener noreferrer" class="subscription-warning-button" style="background: linear-gradient(135deg, #009cde 0%, #0070ba 100%); margin-left: 10px;">
|
|
<i class="fab fa-paypal"></i>
|
|
{% trans "Pay with Payment Link" %}
|
|
</a>
|
|
{% endif %}
|
|
{% else %}
|
|
{% if plugin.patreon_url %}
|
|
<a href="{{ plugin.patreon_url }}" target="_blank" rel="noopener noreferrer" class="subscription-warning-button">
|
|
<i class="fab fa-patreon"></i>
|
|
{% trans "Subscribe on Patreon" %}
|
|
</a>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="plugin-status-section">
|
|
<div class="plugin-status-row">
|
|
<span class="label">{% trans "Status:" %}</span>
|
|
{% if plugin.installed %}
|
|
<span class="status-installed-small">{% trans "Installed" %}</span>
|
|
{% else %}
|
|
<span class="status-not-installed-small">{% trans "Not Installed" %}</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="plugin-status-row">
|
|
<span class="label">{% trans "Active:" %}</span>
|
|
{% if plugin.installed %}
|
|
{% if plugin.enabled %}
|
|
<span class="active-status active-yes"><i class="fas fa-check-circle"></i> {% trans "Yes" %}</span>
|
|
{% else %}
|
|
<span class="active-status active-no"><i class="fas fa-times-circle"></i> {% trans "No" %}</span>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="active-status active-na">-</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="plugin-footer">
|
|
<div class="plugin-actions">
|
|
{% if plugin.installed %}
|
|
{% if plugin.manage_url %}
|
|
<a href="{{ plugin.manage_url }}" class="btn-action btn-settings btn-small" title="{% trans 'Plugin Settings' %}">
|
|
<i class="fas fa-cog"></i> {% trans "Settings" %}
|
|
</a>
|
|
{% endif %}
|
|
{% if plugin.enabled %}
|
|
<button class="btn-action btn-deactivate btn-small" onclick="deactivatePlugin('{{ plugin.plugin_dir }}')">
|
|
<i class="fas fa-toggle-on"></i> {% trans "Deactivate" %}
|
|
</button>
|
|
{% else %}
|
|
<button class="btn-action btn-activate btn-small" onclick="activatePlugin('{{ plugin.plugin_dir }}')">
|
|
<i class="fas fa-toggle-off"></i> {% trans "Activate" %}
|
|
</button>
|
|
{% endif %}
|
|
<button class="btn-action btn-uninstall btn-small" onclick="uninstallPlugin('{{ plugin.plugin_dir }}')">
|
|
<i class="fas fa-trash"></i> {% trans "Uninstall" %}
|
|
</button>
|
|
{% else %}
|
|
<button class="btn-action btn-install btn-small" onclick="installPlugin('{{ plugin.plugin_dir }}')">
|
|
<i class="fas fa-download"></i> {% trans "Install" %}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
<div class="plugin-links">
|
|
<a href="/plugins/{{ plugin.plugin_dir }}/help/" class="btn-link btn-link-small" title="{% trans 'Plugin Help' %}">
|
|
<i class="fas fa-question-circle"></i> {% trans "Help" %}
|
|
</a>
|
|
<a href="https://github.com/master3395/cyberpanel-plugins/tree/main/{{ plugin.plugin_dir }}" target="_blank" class="btn-link btn-link-small" title="{% trans 'About Plugin' %}">
|
|
<i class="fas fa-info-circle"></i> {% trans "About" %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Table View -->
|
|
<div id="tableView" style="display: none;">
|
|
<div class="plugins-table-wrapper">
|
|
<table class="plugins-table">
|
|
<thead>
|
|
<tr>
|
|
<th>{% trans "Icon" %}</th>
|
|
<th>{% trans "Plugin Name" %}</th>
|
|
<th>{% trans "Version" %}</th>
|
|
<th>{% trans "Pricing" %}</th>
|
|
<th>{% trans "Modify Date" %}</th>
|
|
<th>{% trans "Status" %}</th>
|
|
<th>{% trans "Action" %}</th>
|
|
<th>{% trans "Active" %}</th>
|
|
<th>{% trans "Help" %}</th>
|
|
<th>{% trans "About" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for plugin in plugins %}
|
|
<tr>
|
|
<td style="text-align: center; vertical-align: middle;">
|
|
<div style="width: 40px; height: 40px; background: #f8f9ff; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; font-size: 18px; color: #5856d6; margin: 0 auto;">
|
|
{% if plugin.type == "Security" %}
|
|
<i class="fas fa-shield-alt"></i>
|
|
{% elif plugin.type == "Performance" %}
|
|
<i class="fas fa-rocket"></i>
|
|
{% elif plugin.type == "Utility" %}
|
|
<i class="fas fa-tools"></i>
|
|
{% elif plugin.type == "Backup" %}
|
|
<i class="fas fa-save"></i>
|
|
{% else %}
|
|
<i class="fas fa-puzzle-piece"></i>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<strong>{{ plugin.name }}</strong>
|
|
{% if plugin.is_new|default:False %} <span class="plugin-status-badge new" title="{% trans 'This plugin was released/updated within the last 3 months' %}">NEW</span>{% endif %}
|
|
{% if plugin.is_stale|default:False %} <span class="plugin-status-badge stale" title="{% trans 'This plugin is marked \'Stale\' (Last release over two years ago). This means it may work fine, but it has not had any recent development. Use your own discretion when using this plugin!' %}">STALE</span>{% endif %}
|
|
</td>
|
|
<td>
|
|
<span class="plugin-version-number">{{ plugin.version }}</span>
|
|
</td>
|
|
<td>
|
|
{% if plugin.is_paid|default:False|default_if_none:False %}
|
|
<span class="plugin-pricing-badge paid">{% trans "Paid" %}</span>
|
|
{% else %}
|
|
<span class="plugin-pricing-badge free">{% trans "Free" %}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<small style="color: var(--text-secondary, #64748b);">
|
|
{{ plugin.modify_date|default:"N/A" }}
|
|
</small>
|
|
</td>
|
|
<td>
|
|
{% if plugin.installed %}
|
|
<span class="status-installed">{% trans "Installed" %}</span>
|
|
{% else %}
|
|
<span class="status-not-installed">{% trans "Not Installed" %}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="plugin-actions">
|
|
{% if plugin.installed %}
|
|
{% if plugin.manage_url %}
|
|
<a href="{{ plugin.manage_url }}" class="btn-action btn-settings" title="{% trans 'Plugin Settings' %}">
|
|
<i class="fas fa-cog"></i> {% trans "Settings" %}
|
|
</a>
|
|
{% endif %}
|
|
{% if plugin.enabled %}
|
|
<button class="btn-action btn-deactivate" onclick="deactivatePlugin('{{ plugin.plugin_dir }}')">
|
|
<i class="fas fa-toggle-on"></i> {% trans "Deactivate" %}
|
|
</button>
|
|
{% else %}
|
|
<button class="btn-action btn-activate" onclick="activatePlugin('{{ plugin.plugin_dir }}')">
|
|
<i class="fas fa-toggle-off"></i> {% trans "Activate" %}
|
|
</button>
|
|
{% endif %}
|
|
<button class="btn-action btn-uninstall" onclick="uninstallPlugin('{{ plugin.plugin_dir }}')">
|
|
<i class="fas fa-trash"></i> {% trans "Uninstall" %}
|
|
</button>
|
|
{% else %}
|
|
<button class="btn-action btn-install" onclick="installPlugin('{{ plugin.plugin_dir }}')">
|
|
<i class="fas fa-download"></i> {% trans "Install" %}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td class="active-column">
|
|
{% if plugin.installed %}
|
|
{% if plugin.enabled %}
|
|
<span class="active-status active-yes"><i class="fas fa-check-circle"></i></span>
|
|
{% else %}
|
|
<span class="active-status active-no"><i class="fas fa-times-circle"></i></span>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="active-status active-na">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<a href="/plugins/{{ plugin.plugin_dir }}/help/" class="btn-link" title="{% trans 'Plugin Help' %}">
|
|
<i class="fas fa-question-circle"></i> {% trans "Help" %}
|
|
</a>
|
|
</td>
|
|
<td>
|
|
<a href="https://github.com/master3395/cyberpanel-plugins/tree/main/{{ plugin.plugin_dir }}" target="_blank" class="btn-link" title="{% trans 'About Plugin' %}">
|
|
<i class="fas fa-info-circle"></i> {% trans "About" %}
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{% else %}
|
|
<!-- Empty State -->
|
|
<div class="empty-state">
|
|
<div class="empty-icon">
|
|
<i class="fas fa-plug"></i>
|
|
</div>
|
|
<h3 class="empty-title">{% trans "No Plugins Installed" %}</h3>
|
|
<p class="empty-description">{% trans "You haven't installed any plugins yet. Plugins extend CyberPanel's functionality with additional features." %}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- CyberPanel Plugin Store (always available) -->
|
|
<div id="storeView" style="display: {% if not plugins %}block{% else %}none{% endif %};">
|
|
<!-- Notice Section (similar to CMSMS Module Manager) -->
|
|
<div class="store-notice" id="pluginStoreNotice">
|
|
<div class="notice-header">
|
|
<strong>{% trans "Notice" %}</strong>
|
|
<button class="notice-close" onclick="dismissNotice()" aria-label="{% trans 'Close notice' %}">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="notice-content">
|
|
<p>{% trans "The versions displayed here represent the latest plugins from the CyberPanel Plugin Store repository. They may or may not represent the latest available versions. Additionally, the plugin repository may only contain plugins released within the last few months." %}</p>
|
|
<p style="color: var(--text-secondary, #64748b); font-size: 13px; margin-top: 10px; padding: 10px; background: var(--bg-secondary, #f8f9ff); border-radius: 6px; border-left: 3px solid #5856d6;">
|
|
<i class="fas fa-info-circle" style="color: #5856d6;"></i>
|
|
<strong>{% trans "Cache Information:" %}</strong>
|
|
{% trans "Plugin store data is cached for 1 hour to improve performance and reduce GitHub API rate limits. New plugins may take up to 1 hour to appear after being published." %}
|
|
</p>
|
|
<p class="warning-text">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<strong>{% trans "Use at Your Own Risk" %}</strong>
|
|
</p>
|
|
<p>{% trans "The plugins displayed here are contributed by both the CyberPanel Developers and independent third parties. We make no guarantees that the plugins available here are functional, tested, or compatible with your system. You are encouraged to read the information found in the help and about links for each plugin before attempting the installation." %}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Store Loading Indicator -->
|
|
<div id="storeLoading" class="store-loading" style="display: none;">
|
|
<div class="loading-spinner"></div>
|
|
<p>{% trans "Loading plugins from store..." %}</p>
|
|
</div>
|
|
|
|
<!-- Store Error -->
|
|
<div id="storeError" class="alert alert-danger" style="display: none;">
|
|
<i class="fas fa-exclamation-circle alert-icon"></i>
|
|
<span id="storeErrorText"></span>
|
|
</div>
|
|
|
|
<!-- Search Bar - Always visible when store view is active -->
|
|
<div id="storeSearchContainer" class="store-search-container" style="margin-bottom: 20px; display: none;">
|
|
<div class="store-search-wrapper" style="position: relative; max-width: 500px; margin: 0 auto;">
|
|
<i class="fas fa-search" style="position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: #64748b; z-index: 1;"></i>
|
|
<input type="text" id="storeSearchInput" class="store-search-input" placeholder="{% trans 'Search plugins by name or description...' %}" style="width: 100%; padding: 12px 15px 12px 45px; border: 2px solid #e8e9ff; border-radius: 8px; font-size: 14px; transition: all 0.3s ease; background: white;">
|
|
<button id="clearSearchBtn" class="clear-search-btn" style="display: none; position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #64748b; cursor: pointer; padding: 5px; font-size: 14px;" onclick="clearStoreSearch()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Alphabetical Filter - Always visible when store view is active -->
|
|
<div id="storeAlphabetFilter" class="alphabet-filter" style="display: none;">
|
|
{% for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" %}
|
|
<button class="alpha-btn" onclick="filterByLetter('{{ letter }}')">{{ letter }}</button>
|
|
{% endfor %}
|
|
<button class="alpha-btn active" onclick="filterByLetter('all')">{% trans "All" %}</button>
|
|
</div>
|
|
|
|
<!-- Store Content -->
|
|
<div id="storeContent" style="display: none;">
|
|
<!-- Store Table -->
|
|
<div class="store-table-wrapper">
|
|
<table class="store-table">
|
|
<thead>
|
|
<tr>
|
|
<th>{% trans "Icon" %}</th>
|
|
<th>{% trans "Plugin Name" %}</th>
|
|
<th>{% trans "Version" %}</th>
|
|
<th>{% trans "Pricing" %}</th>
|
|
<th>{% trans "Modify Date" %}</th>
|
|
<th>{% trans "Action" %}</th>
|
|
<th>{% trans "Help" %}</th>
|
|
<th>{% trans "About" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="storeTableBody">
|
|
<!-- Plugin rows will be populated by JavaScript -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div ng-controller="listWebsites" id="listFail" class="alert alert-danger" style="display: none;">
|
|
<i class="fas fa-exclamation-circle alert-icon"></i>
|
|
<span>{% trans "Cannot list plugins. Error message:" %} {$ errorMessage $}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Cache-busting version: 2026-01-25-v8 - Added search functionality to Plugin Store
|
|
// Force browser to reload this script by changing version number
|
|
let storePlugins = [];
|
|
let currentFilter = 'all';
|
|
let currentSearchQuery = '';
|
|
|
|
// Get CSRF cookie helper function
|
|
function getCookie(name) {
|
|
let cookieValue = null;
|
|
if (document.cookie && document.cookie !== '') {
|
|
const cookies = document.cookie.split(';');
|
|
for (let i = 0; i < cookies.length; i++) {
|
|
const cookie = cookies[i].trim();
|
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return cookieValue;
|
|
}
|
|
|
|
function toggleView(view) {
|
|
const gridView = document.getElementById('gridView');
|
|
const tableView = document.getElementById('tableView');
|
|
const storeView = document.getElementById('storeView');
|
|
const viewBtns = document.querySelectorAll('.view-btn');
|
|
|
|
viewBtns.forEach(btn => btn.classList.remove('active'));
|
|
|
|
if (view === 'grid') {
|
|
// Grid view requires gridView to exist
|
|
if (!gridView) {
|
|
console.warn('Grid view not available (no plugins installed)');
|
|
return;
|
|
}
|
|
gridView.style.display = 'grid';
|
|
if (tableView) tableView.style.display = 'none';
|
|
if (storeView) storeView.style.display = 'none';
|
|
if (viewBtns[0]) viewBtns[0].classList.add('active');
|
|
} else if (view === 'table') {
|
|
// Table view requires tableView to exist
|
|
if (!tableView) {
|
|
console.warn('Table view not available (no plugins installed)');
|
|
return;
|
|
}
|
|
if (gridView) gridView.style.display = 'none';
|
|
tableView.style.display = 'block';
|
|
if (storeView) storeView.style.display = 'none';
|
|
if (viewBtns[1]) viewBtns[1].classList.add('active');
|
|
} else if (view === 'store') {
|
|
// Store view requires storeView to exist
|
|
if (!storeView) {
|
|
console.error('Store view element not found');
|
|
return;
|
|
}
|
|
if (gridView) gridView.style.display = 'none';
|
|
if (tableView) tableView.style.display = 'none';
|
|
storeView.style.display = 'block';
|
|
|
|
// Find and activate the store button (it might be at different index)
|
|
viewBtns.forEach((btn, index) => {
|
|
if (btn.textContent.includes('Store') || btn.textContent.includes('Plugin Store')) {
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
|
|
// Show search bar and alphabet filter immediately
|
|
const searchContainer = document.getElementById('storeSearchContainer');
|
|
const alphabetFilter = document.getElementById('storeAlphabetFilter');
|
|
if (searchContainer) searchContainer.style.display = 'block';
|
|
if (alphabetFilter) alphabetFilter.style.display = 'flex';
|
|
|
|
// Setup search functionality
|
|
if (typeof setupStoreSearch === 'function') {
|
|
setupStoreSearch();
|
|
}
|
|
|
|
// Load plugins from store if not already loaded
|
|
if (typeof storePlugins !== 'undefined' && storePlugins.length === 0) {
|
|
if (typeof loadPluginStore === 'function') {
|
|
loadPluginStore();
|
|
}
|
|
} else if (typeof displayStorePlugins === 'function') {
|
|
displayStorePlugins();
|
|
}
|
|
}
|
|
}
|
|
|
|
function loadPluginStore() {
|
|
const storeLoading = document.getElementById('storeLoading');
|
|
const storeError = document.getElementById('storeError');
|
|
const storeErrorText = document.getElementById('storeErrorText');
|
|
const storeContent = document.getElementById('storeContent');
|
|
|
|
storeLoading.style.display = 'block';
|
|
storeError.style.display = 'none';
|
|
storeContent.style.display = 'none';
|
|
|
|
// Add cache-busting timestamp to prevent browser caching
|
|
const cacheBuster = '?t=' + Date.now();
|
|
|
|
fetch('/plugins/api/store/plugins/' + cacheBuster, {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-CSRFToken': getCookie('csrftoken'),
|
|
'Cache-Control': 'no-cache',
|
|
'Pragma': 'no-cache'
|
|
},
|
|
cache: 'no-store'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
storeLoading.style.display = 'none';
|
|
|
|
if (data.success && data.plugins) {
|
|
// Sort plugins deterministically by name to prevent order changes
|
|
storePlugins = data.plugins.sort((a, b) => {
|
|
const nameA = (a.name || '').toLowerCase();
|
|
const nameB = (b.name || '').toLowerCase();
|
|
return nameA.localeCompare(nameB);
|
|
});
|
|
|
|
// Normalize is_paid to boolean for all plugins
|
|
storePlugins = storePlugins.map(plugin => {
|
|
if (plugin.is_paid !== undefined && plugin.is_paid !== null) {
|
|
const isPaidValue = plugin.is_paid;
|
|
if (isPaidValue === true || isPaidValue === 'true' || isPaidValue === 'True' || isPaidValue === 1 || isPaidValue === '1' || String(isPaidValue).toLowerCase() === 'true') {
|
|
plugin.is_paid = true;
|
|
} else {
|
|
plugin.is_paid = false;
|
|
}
|
|
} else {
|
|
plugin.is_paid = false;
|
|
}
|
|
// Force boolean type
|
|
plugin.is_paid = Boolean(plugin.is_paid);
|
|
return plugin;
|
|
});
|
|
|
|
displayStorePlugins();
|
|
storeContent.style.display = 'block';
|
|
// Ensure search and filter are visible
|
|
const searchContainer = document.getElementById('storeSearchContainer');
|
|
const alphabetFilter = document.getElementById('storeAlphabetFilter');
|
|
if (searchContainer) searchContainer.style.display = 'block';
|
|
if (alphabetFilter) alphabetFilter.style.display = 'flex';
|
|
} else {
|
|
storeErrorText.textContent = data.error || 'Failed to load plugins from store';
|
|
storeError.style.display = 'block';
|
|
// Keep search and filter visible even on error
|
|
const searchContainer = document.getElementById('storeSearchContainer');
|
|
const alphabetFilter = document.getElementById('storeAlphabetFilter');
|
|
if (searchContainer) searchContainer.style.display = 'block';
|
|
if (alphabetFilter) alphabetFilter.style.display = 'flex';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
storeLoading.style.display = 'none';
|
|
storeErrorText.textContent = 'Error loading plugin store: ' + error.message;
|
|
storeError.style.display = 'block';
|
|
console.error('Plugin store load error:', error);
|
|
});
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function displayStorePlugins() {
|
|
// Version: 2026-01-25-v8 - Store view: 8 columns with search functionality
|
|
// CRITICAL: NO Author, NO Status, NO Active columns - these are for Grid/Table views only
|
|
// Store view shows: Icon | Plugin Name (with NEW/Stale badges) | Version | Pricing | Modify Date | Action (Installed/Install) | Help | About
|
|
const tbody = document.getElementById('storeTableBody');
|
|
if (!tbody) {
|
|
console.error('storeTableBody not found!');
|
|
return;
|
|
}
|
|
tbody.innerHTML = '';
|
|
|
|
if (!storePlugins || storePlugins.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">No plugins available in store</td></tr>';
|
|
return;
|
|
}
|
|
|
|
// CRITICAL: Sort plugins deterministically by name to prevent order changes
|
|
// Create a copy to avoid mutating the original array
|
|
let sortedPlugins = [...storePlugins].sort((a, b) => {
|
|
const nameA = (a.name || '').toLowerCase();
|
|
const nameB = (b.name || '').toLowerCase();
|
|
return nameA.localeCompare(nameB);
|
|
});
|
|
|
|
// Initialize filteredPlugins FIRST
|
|
let filteredPlugins = sortedPlugins;
|
|
|
|
// Apply alphabetical filter
|
|
if (currentFilter !== 'all') {
|
|
filteredPlugins = filteredPlugins.filter(plugin =>
|
|
plugin.name && plugin.name.charAt(0).toUpperCase() === currentFilter
|
|
);
|
|
}
|
|
|
|
// Apply search filter
|
|
if (currentSearchQuery && currentSearchQuery.trim() !== '') {
|
|
const searchLower = currentSearchQuery.toLowerCase().trim();
|
|
filteredPlugins = filteredPlugins.filter(plugin => {
|
|
const nameMatch = plugin.name && plugin.name.toLowerCase().includes(searchLower);
|
|
const descMatch = plugin.description && plugin.description.toLowerCase().includes(searchLower);
|
|
const authorMatch = plugin.author && plugin.author.toLowerCase().includes(searchLower);
|
|
return nameMatch || descMatch || authorMatch;
|
|
});
|
|
}
|
|
|
|
// Show message if search/filter returns no results (AFTER filtering)
|
|
if (filteredPlugins.length === 0) {
|
|
const searchMsg = currentSearchQuery
|
|
? `No plugins found matching "${currentSearchQuery}"`
|
|
: 'No plugins available';
|
|
tbody.innerHTML = `<tr><td colspan="8" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">${escapeHtml(searchMsg)}</td></tr>`;
|
|
return;
|
|
}
|
|
|
|
filteredPlugins.forEach(plugin => {
|
|
const row = document.createElement('tr');
|
|
|
|
// Plugin icon - based on plugin type (same logic as Grid/Table views)
|
|
const pluginType = (plugin.type || 'Plugin').toLowerCase();
|
|
let iconClass = 'fas fa-puzzle-piece'; // Default icon
|
|
if (pluginType.includes('security')) {
|
|
iconClass = 'fas fa-shield-alt';
|
|
} else if (pluginType.includes('performance')) {
|
|
iconClass = 'fas fa-rocket';
|
|
} else if (pluginType.includes('utility')) {
|
|
iconClass = 'fas fa-tools';
|
|
} else if (pluginType.includes('backup')) {
|
|
iconClass = 'fas fa-save';
|
|
}
|
|
const iconHtml = `<div class="plugin-icon" style="width: 40px; height: 40px; background: var(--bg-secondary, #f8f9ff); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 18px; color: #5856d6; margin: 0 auto;">
|
|
<i class="${iconClass}"></i>
|
|
</div>`;
|
|
|
|
// Action column - Store view only shows Install/Installed (no Deactivate/Uninstall)
|
|
// NOTE: Store view should NOT show Deactivate/Uninstall buttons - users manage from Grid/Table views
|
|
let actionHtml = '';
|
|
if (plugin.installed) {
|
|
// Show "Installed" text
|
|
actionHtml = '<span class="status-installed">Installed</span>';
|
|
} else {
|
|
// Show Install button
|
|
actionHtml = `<button class="btn-action btn-install" onclick="installFromStore('${plugin.plugin_dir}')">
|
|
<i class="fas fa-download"></i> Install
|
|
</button>`;
|
|
}
|
|
|
|
// Help column - always use consistent help URL
|
|
const helpUrl = `/plugins/${plugin.plugin_dir}/help/`;
|
|
const helpHtml = `<a href="${helpUrl}" class="btn-link" title="Plugin Help">
|
|
<i class="fas fa-question-circle"></i> Help
|
|
</a>`;
|
|
|
|
// About column
|
|
const aboutUrl = plugin.about_url || plugin.github_url || '#';
|
|
const aboutHtml = `<a href="${aboutUrl}" target="_blank" class="btn-link" title="About Plugin">
|
|
<i class="fas fa-info-circle"></i> About
|
|
</a>`;
|
|
|
|
// Modify Date column - show N/A for store plugins (they're from GitHub, not local)
|
|
const modifyDateHtml = plugin.modify_date ? `<small style="color: var(--text-secondary, #64748b);">${escapeHtml(plugin.modify_date)}</small>` : '<small style="color: var(--text-secondary, #64748b);">N/A</small>';
|
|
|
|
// Pricing badge - ALWAYS show a badge (default to Free if is_paid is missing/undefined/null)
|
|
// Version: 2026-01-25-v5 - Normalize is_paid to handle all possible values, force boolean
|
|
let isPaid = false;
|
|
if (plugin.is_paid !== undefined && plugin.is_paid !== null) {
|
|
const isPaidValue = plugin.is_paid;
|
|
// Handle all possible true values (boolean, string, number)
|
|
if (isPaidValue === true || isPaidValue === 'true' || isPaidValue === 'True' || isPaidValue === 1 || isPaidValue === '1' || String(isPaidValue).toLowerCase() === 'true') {
|
|
isPaid = true;
|
|
} else {
|
|
isPaid = false; // Explicitly set to false for any other value
|
|
}
|
|
}
|
|
// Force boolean type
|
|
isPaid = Boolean(isPaid);
|
|
const pricingBadge = isPaid
|
|
? '<span class="plugin-pricing-badge paid">Paid</span>'
|
|
: '<span class="plugin-pricing-badge free">Free</span>';
|
|
|
|
// NEW and Stale badges
|
|
let statusBadges = '';
|
|
if (plugin.is_new === true) {
|
|
statusBadges += '<span class="plugin-status-badge new" title="This plugin was released/updated within the last 3 months">NEW</span>';
|
|
}
|
|
if (plugin.is_stale === true) {
|
|
statusBadges += '<span class="plugin-status-badge stale" title="This plugin is marked \'Stale\' (Last release over two years ago). This means it may work fine, but it has not had any recent development. Use your own discretion when using this plugin!">STALE</span>';
|
|
}
|
|
|
|
// Version: 2026-01-25-v7 - Store view: 8 columns only (Icon, Plugin Name, Version, Pricing, Modify Date, Action, Help, About)
|
|
// NO Author, NO Status, NO Active columns - these are removed from Store view
|
|
// Plugin Name includes NEW/Stale badges
|
|
row.innerHTML = `
|
|
<td style="text-align: center;">${iconHtml}</td>
|
|
<td>
|
|
<strong>${escapeHtml(plugin.name)}</strong>${statusBadges}
|
|
</td>
|
|
<td><span class="plugin-version-number">${escapeHtml(plugin.version)}</span></td>
|
|
<td>${pricingBadge}</td>
|
|
<td>${modifyDateHtml}</td>
|
|
<td>${actionHtml}</td>
|
|
<td>${helpHtml}</td>
|
|
<td>${aboutHtml}</td>
|
|
`;
|
|
|
|
// Ensure row has exactly 8 cells (no more, no less)
|
|
if (row.cells.length !== 8) {
|
|
console.warn(`Plugin ${plugin.name} row has ${row.cells.length} cells, expected 8. Rebuilding...`);
|
|
const newRow = document.createElement('tr');
|
|
newRow.innerHTML = row.innerHTML;
|
|
row.parentNode.replaceChild(newRow, row);
|
|
}
|
|
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
function filterByLetter(letter) {
|
|
currentFilter = letter;
|
|
const alphaBtns = document.querySelectorAll('.alpha-btn');
|
|
alphaBtns.forEach(btn => btn.classList.remove('active'));
|
|
if (event && event.target) {
|
|
event.target.classList.add('active');
|
|
} else {
|
|
// Find and activate the clicked button
|
|
alphaBtns.forEach(btn => {
|
|
if (btn.textContent.trim() === letter || (letter === 'all' && btn.textContent.trim() === 'All')) {
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
displayStorePlugins();
|
|
}
|
|
|
|
// Search functionality
|
|
function setupStoreSearch() {
|
|
const searchInput = document.getElementById('storeSearchInput');
|
|
const clearBtn = document.getElementById('clearSearchBtn');
|
|
|
|
if (!searchInput) return;
|
|
|
|
// Search on input
|
|
searchInput.addEventListener('input', function(e) {
|
|
currentSearchQuery = e.target.value;
|
|
|
|
// Show/hide clear button
|
|
if (currentSearchQuery && currentSearchQuery.trim() !== '') {
|
|
clearBtn.style.display = 'block';
|
|
} else {
|
|
clearBtn.style.display = 'none';
|
|
}
|
|
|
|
// Filter plugins
|
|
displayStorePlugins();
|
|
});
|
|
|
|
// Search on Enter key
|
|
searchInput.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
displayStorePlugins();
|
|
}
|
|
});
|
|
}
|
|
|
|
function clearStoreSearch() {
|
|
const searchInput = document.getElementById('storeSearchInput');
|
|
const clearBtn = document.getElementById('clearSearchBtn');
|
|
|
|
if (searchInput) {
|
|
searchInput.value = '';
|
|
currentSearchQuery = '';
|
|
clearBtn.style.display = 'none';
|
|
displayStorePlugins();
|
|
searchInput.focus();
|
|
}
|
|
}
|
|
|
|
function installFromStore(pluginName) {
|
|
if (!confirm(`Install ${pluginName} from the CyberPanel Plugin Store?`)) {
|
|
return;
|
|
}
|
|
|
|
const btn = event.target.closest('.btn-install') || event.target;
|
|
const originalText = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installing...';
|
|
|
|
fetch(`/plugins/api/store/install/${pluginName}/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': getCookie('csrftoken'),
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Show success notification
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Success!',
|
|
text: data.message || `Plugin ${pluginName} installed successfully`,
|
|
type: 'success'
|
|
});
|
|
} else {
|
|
alert('Success: ' + (data.message || `Plugin ${pluginName} installed successfully`));
|
|
}
|
|
|
|
// Reload plugins
|
|
loadPluginStore();
|
|
} else {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Installation Failed!',
|
|
text: data.error || 'Failed to install plugin',
|
|
type: 'error'
|
|
});
|
|
} else {
|
|
alert('Error: ' + (data.error || 'Failed to install plugin'));
|
|
}
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Error!',
|
|
text: 'Failed to install plugin: ' + error.message,
|
|
type: 'error'
|
|
});
|
|
} else {
|
|
alert('Error: Failed to install plugin - ' + error.message);
|
|
}
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
});
|
|
}
|
|
|
|
function uninstallPluginFromStore(pluginName) {
|
|
if (!confirm(`Are you sure you want to uninstall ${pluginName}? All data from this plugin will be deleted.`)) {
|
|
return;
|
|
}
|
|
|
|
const btn = event.target.closest('.btn-uninstall') || event.target;
|
|
const originalText = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Uninstalling...';
|
|
|
|
fetch(`/plugins/api/uninstall/${pluginName}/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': getCookie('csrftoken'),
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Success!',
|
|
text: data.message || `Plugin ${pluginName} uninstalled successfully`,
|
|
type: 'success'
|
|
});
|
|
} else {
|
|
alert('Success: ' + (data.message || `Plugin ${pluginName} uninstalled successfully`));
|
|
}
|
|
|
|
// Reload plugins
|
|
loadPluginStore();
|
|
} else {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Uninstall Failed!',
|
|
text: data.error || 'Failed to uninstall plugin',
|
|
type: 'error'
|
|
});
|
|
} else {
|
|
alert('Error: ' + (data.error || 'Failed to uninstall plugin'));
|
|
}
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Error!',
|
|
text: 'Failed to uninstall plugin: ' + error.message,
|
|
type: 'error'
|
|
});
|
|
} else {
|
|
alert('Error: Failed to uninstall plugin - ' + error.message);
|
|
}
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
});
|
|
}
|
|
|
|
function installPlugin(pluginName) {
|
|
if (!confirm(`Install ${pluginName}?`)) {
|
|
return;
|
|
}
|
|
|
|
const btn = event.target.closest('.btn-install') || event.target;
|
|
const originalText = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installing...';
|
|
|
|
fetch(`/plugins/api/install/${pluginName}/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': getCookie('csrftoken'),
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Installation Failed!',
|
|
text: data.error || 'Failed to install plugin',
|
|
type: 'error'
|
|
});
|
|
} else {
|
|
alert('Error: ' + (data.error || 'Failed to install plugin'));
|
|
}
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Error!',
|
|
text: 'Failed to install plugin: ' + error.message,
|
|
type: 'error'
|
|
});
|
|
} else {
|
|
alert('Error: Failed to install plugin - ' + error.message);
|
|
}
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
});
|
|
}
|
|
|
|
function uninstallPlugin(pluginName) {
|
|
if (!confirm(`Are you sure you want to uninstall ${pluginName}? All data from this plugin will be deleted.`)) {
|
|
return;
|
|
}
|
|
|
|
const btn = event.target.closest('.btn-uninstall') || event.target;
|
|
const originalText = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Uninstalling...';
|
|
|
|
fetch(`/plugins/api/uninstall/${pluginName}/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': getCookie('csrftoken'),
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Uninstall Failed!',
|
|
text: data.error || 'Failed to uninstall plugin',
|
|
type: 'error'
|
|
});
|
|
} else {
|
|
alert('Error: ' + (data.error || 'Failed to uninstall plugin'));
|
|
}
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Error!',
|
|
text: 'Failed to uninstall plugin: ' + error.message,
|
|
type: 'error'
|
|
});
|
|
} else {
|
|
alert('Error: Failed to uninstall plugin - ' + error.message);
|
|
}
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
});
|
|
}
|
|
|
|
function activatePlugin(pluginName) {
|
|
const btn = event.target.closest('.btn-activate') || event.target;
|
|
const originalText = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Activating...';
|
|
|
|
fetch(`/plugins/api/enable/${pluginName}/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': getCookie('csrftoken'),
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Success!',
|
|
text: data.message || `Plugin ${pluginName} activated successfully`,
|
|
type: 'success'
|
|
});
|
|
} else {
|
|
alert('Success: ' + (data.message || `Plugin ${pluginName} activated successfully`));
|
|
}
|
|
// Reload page to update UI
|
|
setTimeout(() => location.reload(), 1000);
|
|
} else {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Activation Failed!',
|
|
text: data.error || 'Failed to activate plugin',
|
|
type: 'error'
|
|
});
|
|
} else {
|
|
alert('Error: ' + (data.error || 'Failed to activate plugin'));
|
|
}
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Error!',
|
|
text: 'Failed to activate plugin: ' + error.message,
|
|
type: 'error'
|
|
});
|
|
} else {
|
|
alert('Error: Failed to activate plugin - ' + error.message);
|
|
}
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
});
|
|
}
|
|
|
|
function deactivatePlugin(pluginName) {
|
|
if (!confirm(`Deactivate ${pluginName}? The plugin will be disabled but remain installed.`)) {
|
|
return;
|
|
}
|
|
|
|
const btn = event.target.closest('.btn-deactivate') || event.target;
|
|
const originalText = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deactivating...';
|
|
|
|
fetch(`/plugins/api/disable/${pluginName}/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': getCookie('csrftoken'),
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Success!',
|
|
text: data.message || `Plugin ${pluginName} deactivated successfully`,
|
|
type: 'success'
|
|
});
|
|
} else {
|
|
alert('Success: ' + (data.message || `Plugin ${pluginName} deactivated successfully`));
|
|
}
|
|
// Reload page to update UI
|
|
setTimeout(() => location.reload(), 1000);
|
|
} else {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Deactivation Failed!',
|
|
text: data.error || 'Failed to deactivate plugin',
|
|
type: 'error'
|
|
});
|
|
} else {
|
|
alert('Error: ' + (data.error || 'Failed to deactivate plugin'));
|
|
}
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
if (typeof PNotify !== 'undefined') {
|
|
new PNotify({
|
|
title: 'Error!',
|
|
text: 'Failed to deactivate plugin: ' + error.message,
|
|
type: 'error'
|
|
});
|
|
} else {
|
|
alert('Error: Failed to deactivate plugin - ' + error.message);
|
|
}
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
});
|
|
}
|
|
|
|
function dismissNotice() {
|
|
const notice = document.getElementById('pluginStoreNotice');
|
|
if (notice) {
|
|
notice.style.display = 'none';
|
|
localStorage.setItem('pluginStoreNoticeDismissed', 'true');
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Check if notice was previously dismissed
|
|
if (localStorage.getItem('pluginStoreNoticeDismissed') === 'true') {
|
|
const notice = document.getElementById('pluginStoreNotice');
|
|
if (notice) {
|
|
notice.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Initialize view on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const gridView = document.getElementById('gridView');
|
|
const tableView = document.getElementById('tableView');
|
|
const storeView = document.getElementById('storeView');
|
|
|
|
// Determine which view to show based on what's available
|
|
if (gridView && gridView.children.length > 0) {
|
|
// Plugins are installed, show grid view
|
|
toggleView('grid');
|
|
} else if (storeView) {
|
|
// No plugins installed, show store by default
|
|
// Don't call toggleView here to avoid recursion, just show it directly
|
|
storeView.style.display = 'block';
|
|
if (gridView) gridView.style.display = 'none';
|
|
if (tableView) tableView.style.display = 'none';
|
|
|
|
// Activate the store button
|
|
const viewBtns = document.querySelectorAll('.view-btn');
|
|
viewBtns.forEach(btn => {
|
|
btn.classList.remove('active');
|
|
if (btn.textContent.includes('Store') || btn.textContent.includes('Plugin Store')) {
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
|
|
// Show search bar and alphabet filter
|
|
const searchContainer = document.getElementById('storeSearchContainer');
|
|
const alphabetFilter = document.getElementById('storeAlphabetFilter');
|
|
if (searchContainer) searchContainer.style.display = 'block';
|
|
if (alphabetFilter) alphabetFilter.style.display = 'flex';
|
|
|
|
// Setup search functionality
|
|
if (typeof setupStoreSearch === 'function') {
|
|
setupStoreSearch();
|
|
}
|
|
|
|
// Load plugins from store
|
|
if (typeof loadPluginStore === 'function') {
|
|
loadPluginStore();
|
|
}
|
|
} else {
|
|
console.error('No view elements found on page load');
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %} |