finish plugin function
This commit is contained in:
+40
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||
|
||||
<changeSet id="20260702143000-1" author="system">
|
||||
<createSequence sequenceName="sis_media_seq" startValue="1" incrementBy="1"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="20260702143000-2" author="system">
|
||||
<createTable tableName="sis_media">
|
||||
<column name="id" type="bigint">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="original_filename" type="varchar(500)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="stored_filename" type="varchar(500)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="file_url" type="varchar(1000)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="mime_type" type="varchar(100)"/>
|
||||
<column name="file_size" type="bigint"/>
|
||||
<column name="alt_text" type="varchar(500)"/>
|
||||
<column name="media_type" type="varchar(20)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_by" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_date" type="timestamp"/>
|
||||
<column name="last_modified_by" type="varchar(50)"/>
|
||||
<column name="last_modified_date" type="timestamp"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<!--
|
||||
Added the entity Plugin.
|
||||
-->
|
||||
<changeSet id="20260702144700-1" author="antigravity">
|
||||
<createTable tableName="sis_plugin">
|
||||
<column name="id" type="bigint">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="plugin_key" type="varchar(100)">
|
||||
<constraints nullable="false" unique="true" uniqueConstraintName="ux_sis_plugin__plugin_key"/>
|
||||
</column>
|
||||
<column name="name" type="varchar(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="description" type="varchar(1000)">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
<column name="version" type="varchar(50)">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
<column name="author" type="varchar(255)">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
<column name="status" type="varchar(20)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Audit fields -->
|
||||
<column name="created_by" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_date" type="timestamp"/>
|
||||
<column name="last_modified_by" type="varchar(50)"/>
|
||||
<column name="last_modified_date" type="timestamp"/>
|
||||
</createTable>
|
||||
|
||||
<createSequence sequenceName="sis_plugin_seq" startValue="1" incrementBy="1"/>
|
||||
|
||||
<!-- Insert sample plugin -->
|
||||
<insert tableName="sis_plugin">
|
||||
<column name="id" valueSequenceNext="sis_plugin_seq"/>
|
||||
<column name="plugin_key" value="sampleplugin"/>
|
||||
<column name="name" value="Sample Plugin"/>
|
||||
<column name="description" value="A sample plugin to demonstrate the HookManager."/>
|
||||
<column name="version" value="1.0.0"/>
|
||||
<column name="author" value="Admin"/>
|
||||
<column name="status" value="ACTIVE"/>
|
||||
<column name="created_by" value="system"/>
|
||||
<column name="created_date" valueDate="CURRENT_TIMESTAMP"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -26,6 +26,8 @@
|
||||
<include file="config/liquibase/changelog/20260629084709_add_page_layout.xml" relativeToChangelogFile="false"/>
|
||||
<include file="config/liquibase/changelog/20260629163500_add_wordpress_roles.xml" relativeToChangelogFile="false"/>
|
||||
<include file="config/liquibase/changelog/20260629165300_add_totp_to_user.xml" relativeToChangelogFile="false"/>
|
||||
<include file="config/liquibase/changelog/20260702143000_add_media_entity.xml" relativeToChangelogFile="false"/>
|
||||
<include file="config/liquibase/changelog/20260702144700_add_plugin_entity.xml" relativeToChangelogFile="false"/>
|
||||
<!-- jhipster-needle-liquibase-add-changelog - JHipster will add liquibase changelogs here -->
|
||||
<!-- jhipster-needle-liquibase-add-constraints-changelog - JHipster will add liquibase constraints changelogs here -->
|
||||
<!-- jhipster-needle-liquibase-add-incremental-changelog - JHipster will add incremental liquibase changelogs here -->
|
||||
|
||||
@@ -73,6 +73,22 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Nav Item - Media Collapse Menu -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseMedia"
|
||||
aria-expanded="true" aria-controls="collapseMedia">
|
||||
<i class="fas fa-fw fa-photo-video"></i>
|
||||
<span>Media</span>
|
||||
</a>
|
||||
<div id="collapseMedia" class="collapse" aria-labelledby="headingMedia" data-parent="#accordionSidebar">
|
||||
<div class="bg-white py-2 collapse-inner rounded">
|
||||
<h6 class="collapse-header">Media Management:</h6>
|
||||
<a class="collapse-item" th:href="@{/manage/media}">Library</a>
|
||||
<a class="collapse-item" th:href="@{/manage/media/new}">Add New</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Nav Item - Pages Collapse Menu -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapsePages"
|
||||
@@ -97,6 +113,23 @@
|
||||
System
|
||||
</div>
|
||||
|
||||
<!-- Nav Item - Plugins Collapse Menu -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapsePlugins"
|
||||
aria-expanded="true" aria-controls="collapsePlugins">
|
||||
<i class="fas fa-fw fa-plug"></i>
|
||||
<span>Plugins</span>
|
||||
</a>
|
||||
<div id="collapsePlugins" class="collapse" aria-labelledby="headingPlugins" data-parent="#accordionSidebar">
|
||||
<div class="bg-white py-2 collapse-inner rounded">
|
||||
<h6 class="collapse-header">Plugin Management:</h6>
|
||||
<a class="collapse-item" th:href="@{/manage/plugins}">Installed Plugins</a>
|
||||
<a class="collapse-item" th:href="@{/manage/plugins/new}">Add New</a>
|
||||
<a class="collapse-item" th:href="@{/manage/plugins/editor}">Plugin File Editor</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Nav Item - Users Collapse Menu -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseUsers"
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
|
||||
|
||||
<head>
|
||||
<title>Media Detail</title>
|
||||
<style>
|
||||
.media-preview {
|
||||
background: #f8f9fc;
|
||||
border: 1px solid #e3e6f0;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.media-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 500px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.media-preview video {
|
||||
max-width: 100%;
|
||||
max-height: 500px;
|
||||
}
|
||||
.media-preview audio {
|
||||
width: 100%;
|
||||
margin: 2rem;
|
||||
}
|
||||
.media-preview .file-icon-lg {
|
||||
font-size: 6rem;
|
||||
color: #b7b9cc;
|
||||
}
|
||||
.meta-table th {
|
||||
width: 140px;
|
||||
font-weight: 600;
|
||||
color: #5a5c69;
|
||||
background: #f8f9fc;
|
||||
}
|
||||
.url-copy-group {
|
||||
position: relative;
|
||||
}
|
||||
.url-copy-group .btn-copy {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
.copy-toast {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background: #1cc88a;
|
||||
color: #fff;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.15rem 1.75rem rgba(0,0,0,0.2);
|
||||
z-index: 9999;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div layout:fragment="content">
|
||||
<!-- Page Heading -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Media Detail</h1>
|
||||
<a th:href="@{/manage/media}" class="d-none d-sm-inline-block btn btn-sm btn-secondary shadow-sm">
|
||||
<i class="fas fa-arrow-left fa-sm text-white-50"></i> Back to Library
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<span th:text="${successMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Preview Column -->
|
||||
<div class="col-lg-7 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
<i class="fas fa-eye"></i> Preview
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="media-preview">
|
||||
<!-- Image -->
|
||||
<img th:if="${media.mediaType.name() == 'IMAGE'}"
|
||||
th:src="${media.fileUrl}"
|
||||
th:alt="${media.altText ?: media.originalFilename}">
|
||||
<!-- Video -->
|
||||
<video th:if="${media.mediaType.name() == 'VIDEO'}" controls>
|
||||
<source th:src="${media.fileUrl}" th:type="${media.mimeType}">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<!-- Audio -->
|
||||
<audio th:if="${media.mediaType.name() == 'AUDIO'}" controls>
|
||||
<source th:src="${media.fileUrl}" th:type="${media.mimeType}">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<!-- Document / Other -->
|
||||
<div th:if="${media.mediaType.name() == 'DOCUMENT'}" class="text-center p-4">
|
||||
<i class="fas fa-file-alt file-icon-lg" style="color:#4e73df;"></i>
|
||||
<p class="mt-3 text-muted" th:text="${media.originalFilename}"></p>
|
||||
<a th:href="${media.fileUrl}" target="_blank" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-external-link-alt"></i> Open File
|
||||
</a>
|
||||
</div>
|
||||
<div th:if="${media.mediaType.name() == 'OTHER'}" class="text-center p-4">
|
||||
<i class="fas fa-file file-icon-lg"></i>
|
||||
<p class="mt-3 text-muted" th:text="${media.originalFilename}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details Column -->
|
||||
<div class="col-lg-5 mb-4">
|
||||
<!-- Metadata Card -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
<i class="fas fa-info-circle"></i> File Information
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm meta-table mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<td th:text="${media.originalFilename}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<td>
|
||||
<span class="badge badge-pill"
|
||||
th:classappend="${media.mediaType.name() == 'IMAGE'} ? 'badge-success' : (${media.mediaType.name() == 'DOCUMENT'} ? 'badge-primary' : (${media.mediaType.name() == 'VIDEO'} ? 'badge-danger' : (${media.mediaType.name() == 'AUDIO'} ? 'badge-warning' : 'badge-secondary')))"
|
||||
th:text="${media.mediaType}"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>MIME Type</th>
|
||||
<td th:text="${media.mimeType}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>File Size</th>
|
||||
<td th:text="${media.formattedFileSize}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Uploaded By</th>
|
||||
<td th:text="${media.createdBy}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Upload Date</th>
|
||||
<td th:if="${media.createdDateAsDate != null}"
|
||||
th:text="${#dates.format(media.createdDateAsDate, 'yyyy-MM-dd HH:mm')}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>URL</th>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control form-control-sm" id="mediaUrl"
|
||||
th:value="${fullUrl}" readonly>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-primary" type="button" id="copyUrlBtn"
|
||||
title="Copy URL">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Alt Text Card -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
<i class="fas fa-edit"></i> Edit Details
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form th:action="@{/manage/media/{id}(id=${media.id})}" method="post">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<div class="form-group">
|
||||
<label for="altText" class="font-weight-bold">Alt Text</label>
|
||||
<textarea class="form-control" id="altText" name="altText" rows="3"
|
||||
placeholder="Describe this media for accessibility..."
|
||||
th:text="${media.altText}"></textarea>
|
||||
<small class="form-text text-muted">
|
||||
Used for image alt attributes and screen readers.
|
||||
</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Card -->
|
||||
<div class="card shadow border-left-danger">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h6 class="font-weight-bold text-danger mb-1">Delete this file</h6>
|
||||
<small class="text-muted">This action cannot be undone.</small>
|
||||
</div>
|
||||
<form th:action="@{/manage/media/{id}/delete(id=${media.id})}" method="post"
|
||||
onsubmit="return confirm('Are you sure you want to permanently delete this file?');">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Copy Toast -->
|
||||
<div class="copy-toast" id="copyToast">
|
||||
<i class="fas fa-check-circle"></i> URL copied to clipboard!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section layout:fragment="scripts">
|
||||
<script>
|
||||
document.getElementById('copyUrlBtn').addEventListener('click', function() {
|
||||
var urlInput = document.getElementById('mediaUrl');
|
||||
urlInput.select();
|
||||
urlInput.setSelectionRange(0, 99999);
|
||||
navigator.clipboard.writeText(urlInput.value).then(function() {
|
||||
var toast = document.getElementById('copyToast');
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 2000);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,205 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
|
||||
|
||||
<head>
|
||||
<title>Media Library</title>
|
||||
<style>
|
||||
.media-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
.media-card {
|
||||
border: 1px solid #e3e6f0;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
background: #fff;
|
||||
}
|
||||
.media-card:hover {
|
||||
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.25);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.media-card .media-thumb {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8f9fc;
|
||||
overflow: hidden;
|
||||
}
|
||||
.media-card .media-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.media-card .media-thumb .file-icon {
|
||||
font-size: 3.5rem;
|
||||
color: #b7b9cc;
|
||||
}
|
||||
.media-card .media-thumb .file-icon.doc { color: #4e73df; }
|
||||
.media-card .media-thumb .file-icon.video { color: #e74a3b; }
|
||||
.media-card .media-thumb .file-icon.audio { color: #f6c23e; }
|
||||
.media-card .media-info {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
.media-card .media-info .filename {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #3a3b45;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.media-card .media-info .meta {
|
||||
font-size: 0.7rem;
|
||||
color: #858796;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.media-card .media-actions {
|
||||
padding: 0 0.75rem 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.empty-library {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: #858796;
|
||||
}
|
||||
.empty-library i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
display: block;
|
||||
color: #d1d3e2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div layout:fragment="content">
|
||||
<!-- Page Heading -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Media Library</h1>
|
||||
<a th:href="@{/manage/media/new}" class="d-none d-sm-inline-block btn btn-sm btn-primary shadow-sm">
|
||||
<i class="fas fa-plus fa-sm text-white-50"></i> Add New
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<span th:text="${successMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<span th:text="${errorMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
<i class="fas fa-filter"></i> Filter Media
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form th:action="@{/manage/media}" method="get" class="form-inline">
|
||||
<div class="form-group mr-3 mb-2">
|
||||
<label for="filterType" class="mr-2 font-weight-bold">Type:</label>
|
||||
<select class="form-control form-control-sm" id="filterType" name="type">
|
||||
<option value="">All Types</option>
|
||||
<option th:each="mt : ${mediaTypes}" th:value="${mt}" th:text="${mt}"
|
||||
th:selected="${selectedType != null && selectedType == mt.name()}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group mr-3 mb-2">
|
||||
<label for="filterKeyword" class="mr-2 font-weight-bold">Search:</label>
|
||||
<input type="text" class="form-control form-control-sm" id="filterKeyword" name="keyword"
|
||||
placeholder="Filename..." th:value="${keyword}">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-primary mb-2 mr-2">
|
||||
<i class="fas fa-search"></i> Filter
|
||||
</button>
|
||||
<a th:href="@{/manage/media}" class="btn btn-sm btn-outline-secondary mb-2">
|
||||
<i class="fas fa-times"></i> Clear
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Media Grid -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
All Media
|
||||
<span class="badge badge-light ml-1" th:text="${#lists.size(mediaList)}"></span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Empty state -->
|
||||
<div th:if="${#lists.isEmpty(mediaList)}" class="empty-library">
|
||||
<i class="fas fa-photo-video"></i>
|
||||
<h5>No media files found</h5>
|
||||
<p>Click "Add New" to upload your first file.</p>
|
||||
</div>
|
||||
|
||||
<!-- Grid -->
|
||||
<div th:unless="${#lists.isEmpty(mediaList)}" class="media-grid">
|
||||
<div th:each="media : ${mediaList}" class="media-card">
|
||||
<a th:href="@{/manage/media/{id}(id=${media.id})}" style="text-decoration:none; color:inherit;">
|
||||
<div class="media-thumb">
|
||||
<!-- Image thumbnail -->
|
||||
<img th:if="${media.mediaType.name() == 'IMAGE'}"
|
||||
th:src="${media.fileUrl}"
|
||||
th:alt="${media.altText ?: media.originalFilename}">
|
||||
<!-- Document icon -->
|
||||
<i th:if="${media.mediaType.name() == 'DOCUMENT'}" class="fas fa-file-alt file-icon doc"></i>
|
||||
<!-- Video icon -->
|
||||
<i th:if="${media.mediaType.name() == 'VIDEO'}" class="fas fa-file-video file-icon video"></i>
|
||||
<!-- Audio icon -->
|
||||
<i th:if="${media.mediaType.name() == 'AUDIO'}" class="fas fa-file-audio file-icon audio"></i>
|
||||
<!-- Other icon -->
|
||||
<i th:if="${media.mediaType.name() == 'OTHER'}" class="fas fa-file file-icon"></i>
|
||||
</div>
|
||||
</a>
|
||||
<div class="media-info">
|
||||
<div class="filename" th:text="${media.originalFilename}" th:title="${media.originalFilename}"></div>
|
||||
<div class="meta">
|
||||
<span class="badge badge-pill"
|
||||
th:classappend="${media.mediaType.name() == 'IMAGE'} ? 'badge-success' : (${media.mediaType.name() == 'DOCUMENT'} ? 'badge-primary' : (${media.mediaType.name() == 'VIDEO'} ? 'badge-danger' : (${media.mediaType.name() == 'AUDIO'} ? 'badge-warning' : 'badge-secondary')))"
|
||||
th:text="${media.mediaType}"></span>
|
||||
<span th:text="${media.formattedFileSize}"></span>
|
||||
</div>
|
||||
<div class="meta" th:if="${media.createdDateAsDate != null}">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span th:text="${#dates.format(media.createdDateAsDate, 'yyyy-MM-dd HH:mm')}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-actions">
|
||||
<a th:href="@{/manage/media/{id}(id=${media.id})}" class="btn btn-sm btn-outline-info flex-fill" title="View">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<form th:action="@{/manage/media/{id}/delete(id=${media.id})}" method="post" class="flex-fill"
|
||||
onsubmit="return confirm('Are you sure you want to delete this file?');">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger w-100" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,225 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
|
||||
|
||||
<head>
|
||||
<title>Add New Media</title>
|
||||
<style>
|
||||
.upload-zone {
|
||||
border: 3px dashed #d1d3e2;
|
||||
border-radius: 0.75rem;
|
||||
padding: 3rem 2rem;
|
||||
text-align: center;
|
||||
background: #f8f9fc;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.upload-zone:hover,
|
||||
.upload-zone.dragover {
|
||||
border-color: #4e73df;
|
||||
background: #eaecf4;
|
||||
}
|
||||
.upload-zone .upload-icon {
|
||||
font-size: 4rem;
|
||||
color: #b7b9cc;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.upload-zone.dragover .upload-icon {
|
||||
color: #4e73df;
|
||||
}
|
||||
.upload-zone h4 {
|
||||
color: #5a5c69;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.upload-zone p {
|
||||
color: #858796;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.file-list {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.file-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #fff;
|
||||
border: 1px solid #e3e6f0;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.file-list-item .file-name {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
color: #3a3b45;
|
||||
margin-left: 0.75rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.file-list-item .file-size {
|
||||
color: #858796;
|
||||
font-size: 0.8rem;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
.file-list-item .remove-file {
|
||||
color: #e74a3b;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.progress-bar-container {
|
||||
display: none;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div layout:fragment="content">
|
||||
<!-- Page Heading -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Upload New Media</h1>
|
||||
<a th:href="@{/manage/media}" class="d-none d-sm-inline-block btn btn-sm btn-secondary shadow-sm">
|
||||
<i class="fas fa-arrow-left fa-sm text-white-50"></i> Back to Library
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Upload Card -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
<i class="fas fa-cloud-upload-alt"></i> Upload Files
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="uploadForm" th:action="@{/manage/media/upload}" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
|
||||
<!-- Drop Zone -->
|
||||
<div class="upload-zone" id="dropZone" onclick="document.getElementById('fileInput').click();">
|
||||
<div class="upload-icon">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
</div>
|
||||
<h4>Drop files here or click to browse</h4>
|
||||
<p>Supports images (JPG, PNG, GIF, WebP), documents (PDF, DOCX), video, and audio files.</p>
|
||||
<button type="button" class="btn btn-primary">
|
||||
<i class="fas fa-folder-open"></i> Select Files
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input type="file" id="fileInput" name="files" multiple
|
||||
accept="image/*,application/pdf,.doc,.docx,video/*,audio/*"
|
||||
style="display: none;">
|
||||
|
||||
<!-- Selected Files List -->
|
||||
<div class="file-list" id="fileList"></div>
|
||||
|
||||
<!-- Upload Button -->
|
||||
<div class="mt-3" id="uploadButtonContainer" style="display: none;">
|
||||
<button type="submit" class="btn btn-success btn-lg" id="uploadBtn">
|
||||
<i class="fas fa-upload"></i> Upload All Files
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section layout:fragment="scripts">
|
||||
<script>
|
||||
(function() {
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const fileList = document.getElementById('fileList');
|
||||
const uploadBtn = document.getElementById('uploadButtonContainer');
|
||||
const dataTransfer = new DataTransfer();
|
||||
|
||||
// Drag and drop events
|
||||
['dragenter', 'dragover'].forEach(evtName => {
|
||||
dropZone.addEventListener(evtName, function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dropZone.classList.add('dragover');
|
||||
});
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(evtName => {
|
||||
dropZone.addEventListener(evtName, function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dropZone.classList.remove('dragover');
|
||||
});
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function(e) {
|
||||
const files = e.dataTransfer.files;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
dataTransfer.items.add(files[i]);
|
||||
}
|
||||
fileInput.files = dataTransfer.files;
|
||||
renderFileList();
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', function() {
|
||||
// Add newly selected files to our accumulator
|
||||
for (let i = 0; i < fileInput.files.length; i++) {
|
||||
dataTransfer.items.add(fileInput.files[i]);
|
||||
}
|
||||
fileInput.files = dataTransfer.files;
|
||||
renderFileList();
|
||||
});
|
||||
|
||||
function renderFileList() {
|
||||
fileList.innerHTML = '';
|
||||
const files = dataTransfer.files;
|
||||
|
||||
if (files.length === 0) {
|
||||
uploadBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
uploadBtn.style.display = 'block';
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const item = document.createElement('div');
|
||||
item.className = 'file-list-item';
|
||||
|
||||
let icon = 'fa-file';
|
||||
if (file.type.startsWith('image/')) icon = 'fa-file-image';
|
||||
else if (file.type.startsWith('video/')) icon = 'fa-file-video';
|
||||
else if (file.type.startsWith('audio/')) icon = 'fa-file-audio';
|
||||
else if (file.type.includes('pdf') || file.type.includes('word') || file.type.includes('document'))
|
||||
icon = 'fa-file-alt';
|
||||
|
||||
const sizeStr = file.size < 1024 ? file.size + ' B'
|
||||
: file.size < 1024*1024 ? (file.size/1024).toFixed(1) + ' KB'
|
||||
: (file.size/(1024*1024)).toFixed(1) + ' MB';
|
||||
|
||||
item.innerHTML =
|
||||
'<i class="fas ' + icon + ' text-primary"></i>' +
|
||||
'<span class="file-name">' + file.name + '</span>' +
|
||||
'<span class="file-size">' + sizeStr + '</span>' +
|
||||
'<button type="button" class="remove-file" data-index="' + i + '" title="Remove">' +
|
||||
'<i class="fas fa-times-circle"></i></button>';
|
||||
|
||||
fileList.appendChild(item);
|
||||
}
|
||||
|
||||
// Remove file buttons
|
||||
document.querySelectorAll('.remove-file').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const idx = parseInt(this.getAttribute('data-index'));
|
||||
dataTransfer.items.remove(idx);
|
||||
fileInput.files = dataTransfer.files;
|
||||
renderFileList();
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
|
||||
|
||||
<head>
|
||||
<title>Add New Plugin</title>
|
||||
<style>
|
||||
.upload-zone {
|
||||
border: 3px dashed #d1d3e2;
|
||||
border-radius: 0.75rem;
|
||||
padding: 3rem 2rem;
|
||||
text-align: center;
|
||||
background: #f8f9fc;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.upload-zone:hover,
|
||||
.upload-zone.dragover {
|
||||
border-color: #4e73df;
|
||||
background: #eaecf4;
|
||||
}
|
||||
.upload-zone .upload-icon {
|
||||
font-size: 4rem;
|
||||
color: #b7b9cc;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.upload-zone h4 {
|
||||
color: #5a5c69;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.upload-zone p {
|
||||
color: #858796;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div layout:fragment="content">
|
||||
<!-- Page Heading -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Add New Plugin</h1>
|
||||
<a th:href="@{/manage/plugins}" class="d-none d-sm-inline-block btn btn-sm btn-secondary shadow-sm">
|
||||
<i class="fas fa-arrow-left fa-sm text-white-50"></i> Back to Plugins
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<span th:text="${errorMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Upload Card -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
<i class="fas fa-file-archive"></i> Upload Plugin .zip
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="uploadForm" th:action="@{/manage/plugins/upload}" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
|
||||
<!-- Drop Zone -->
|
||||
<div class="upload-zone" id="dropZone" onclick="document.getElementById('zipFile').click();">
|
||||
<div class="upload-icon">
|
||||
<i class="fas fa-file-archive"></i>
|
||||
</div>
|
||||
<h4 id="fileText">Select a .zip file</h4>
|
||||
<p>Upload a plugin archive to install it on the system.</p>
|
||||
<button type="button" class="btn btn-primary">
|
||||
<i class="fas fa-folder-open"></i> Browse
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input type="file" id="zipFile" name="zipFile" accept=".zip" style="display: none;">
|
||||
|
||||
<!-- Upload Button -->
|
||||
<div class="mt-4 text-center" id="uploadButtonContainer" style="display: none;">
|
||||
<button type="submit" class="btn btn-success btn-lg">
|
||||
<i class="fas fa-upload"></i> Install Plugin
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section layout:fragment="scripts">
|
||||
<script>
|
||||
const fileInput = document.getElementById('zipFile');
|
||||
const fileText = document.getElementById('fileText');
|
||||
const uploadBtn = document.getElementById('uploadButtonContainer');
|
||||
|
||||
fileInput.addEventListener('change', function() {
|
||||
if (fileInput.files.length > 0) {
|
||||
const file = fileInput.files[0];
|
||||
if (file.name.endsWith('.zip')) {
|
||||
fileText.textContent = file.name;
|
||||
fileText.classList.add('text-primary');
|
||||
uploadBtn.style.display = 'block';
|
||||
} else {
|
||||
fileText.textContent = "Please select a valid .zip file";
|
||||
fileText.classList.remove('text-primary');
|
||||
fileText.classList.add('text-danger');
|
||||
uploadBtn.style.display = 'none';
|
||||
fileInput.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
|
||||
|
||||
<head>
|
||||
<title>Plugin File Editor</title>
|
||||
<style>
|
||||
.editor-container {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
background-color: #f8f9fc;
|
||||
border: 1px solid #d1d3e2;
|
||||
border-radius: 0.35rem;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
padding: 1rem;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
}
|
||||
.file-list-group .list-group-item {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.file-list-group .list-group-item.active {
|
||||
border-left-color: #4e73df;
|
||||
background-color: #eaecf4;
|
||||
color: #4e73df;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div layout:fragment="content">
|
||||
<!-- Page Heading -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Plugin File Editor</h1>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<span th:text="${successMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<span th:text="${errorMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Plugin Selector -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body py-3 d-flex align-items-center justify-content-end">
|
||||
<span class="mr-3 font-weight-bold text-gray-800">Select plugin to edit:</span>
|
||||
<form id="pluginSelectForm" th:action="@{/manage/plugins/editor}" method="get" class="form-inline">
|
||||
<select class="form-control" name="pluginKey" onchange="document.getElementById('pluginSelectBtn').click();">
|
||||
<option value="" disabled th:selected="${selectedPlugin == null}">-- Select a Plugin --</option>
|
||||
<option th:each="p : ${plugins}"
|
||||
th:value="${p.pluginKey}"
|
||||
th:text="${p.name}"
|
||||
th:selected="${selectedPlugin != null && selectedPlugin.pluginKey == p.pluginKey}"></option>
|
||||
</select>
|
||||
<button type="submit" id="pluginSelectBtn" style="display:none;">Select</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:if="${selectedPlugin != null}" class="row">
|
||||
|
||||
<!-- Sidebar: File Tree -->
|
||||
<div class="col-lg-3 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Plugin Files</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="list-group list-group-flush file-list-group">
|
||||
<a th:each="f : ${pluginFiles}"
|
||||
th:href="@{/manage/plugins/editor/{key}(key=${selectedPlugin.pluginKey}, file=${f})}"
|
||||
class="list-group-item list-group-item-action"
|
||||
th:classappend="${f == selectedFile} ? 'active' : ''">
|
||||
<i class="fas fa-file-code fa-sm mr-2 text-gray-400"></i>
|
||||
<span th:text="${f}"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor Panel -->
|
||||
<div class="col-lg-9 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
Editing: <span class="text-dark" th:text="${selectedPlugin.name}"></span>
|
||||
<span class="text-muted font-weight-normal ml-2" th:text="${selectedFile}"></span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form th:action="@{/manage/plugins/editor/{key}/save(key=${selectedPlugin.pluginKey})}" method="post">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<input type="hidden" name="file" th:value="${selectedFile}" />
|
||||
|
||||
<textarea class="editor-container mb-3" name="content" spellcheck="false" th:text="${fileContent}"></textarea>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Save File
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,111 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
|
||||
|
||||
<head>
|
||||
<title>Installed Plugins</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div layout:fragment="content">
|
||||
<!-- Page Heading -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Installed Plugins</h1>
|
||||
<a th:href="@{/manage/plugins/new}" class="d-none d-sm-inline-block btn btn-sm btn-primary shadow-sm">
|
||||
<i class="fas fa-plus fa-sm text-white-50"></i> Add New Plugin
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<span th:text="${successMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<span th:text="${errorMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Plugins List Card -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">All Plugins</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25%">Plugin</th>
|
||||
<th style="width: 45%">Description</th>
|
||||
<th style="width: 15%">Status</th>
|
||||
<th style="width: 15%">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:if="${plugins.empty}">
|
||||
<td colspan="4" class="text-center text-muted py-4">
|
||||
No plugins installed.
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:each="plugin : ${plugins}" th:classappend="${plugin.status.name() == 'ACTIVE'} ? 'table-primary-light' : ''">
|
||||
<td>
|
||||
<strong class="text-dark" th:text="${plugin.name}"></strong><br>
|
||||
<small class="text-muted">
|
||||
Version <span th:text="${plugin.version}"></span> |
|
||||
By <span th:text="${plugin.author}"></span><br>
|
||||
<code th:text="${plugin.pluginKey}"></code>
|
||||
</small>
|
||||
</td>
|
||||
<td th:text="${plugin.description}"></td>
|
||||
<td>
|
||||
<span class="badge"
|
||||
th:classappend="${plugin.status.name() == 'ACTIVE'} ? 'badge-success' : 'badge-secondary'"
|
||||
th:text="${plugin.status}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- Activate -->
|
||||
<form th:if="${plugin.status.name() == 'INACTIVE'}" th:action="@{/manage/plugins/{id}/activate(id=${plugin.id})}" method="post" class="mr-2 mb-0">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-success border-0" title="Activate">
|
||||
<i class="fas fa-play"></i> Activate
|
||||
</button>
|
||||
</form>
|
||||
<!-- Deactivate -->
|
||||
<form th:if="${plugin.status.name() == 'ACTIVE'}" th:action="@{/manage/plugins/{id}/deactivate(id=${plugin.id})}" method="post" class="mr-2 mb-0">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning border-0" title="Deactivate">
|
||||
<i class="fas fa-pause"></i> Deactivate
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Edit Files -->
|
||||
<a th:href="@{/manage/plugins/editor/{key}(key=${plugin.pluginKey})}" class="btn btn-sm btn-outline-primary border-0 mr-2" title="Edit Files">
|
||||
<i class="fas fa-code"></i> Edit
|
||||
</a>
|
||||
|
||||
<!-- Delete -->
|
||||
<form th:if="${plugin.status.name() == 'INACTIVE'}" th:action="@{/manage/plugins/{id}/delete(id=${plugin.id})}" method="post" class="mb-0" onsubmit="return confirm('Are you sure you want to permanently delete this plugin?');">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger border-0" title="Delete">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user