feat(admin): rebuild release upload form with Livewire and CDN validation
Replace the server-rendered release upload form with a Livewire component, introducing an interactive admin release form that supports dynamic episode management, real‑time CDN path validation, and direct download file checks. Key changes: - New `AdminReleaseForm` Livewire component with dynamic episode fields, Tagify integration, and upload handling via Livewire's file uploads. - Added `CdnPathValidator` service to verify download and stream paths across all configured mirrors, caching results and capturing file sizes. - Extended `EpisodeService` and `GalleryService` to support array‑based data and temporary uploaded files from Livewire. - Persist validated download sizes and store the validation timestamp on the `downloads` table via a new migration; also add a unique constraint on `hentais.slug` to prevent duplicates. - Remove the old POST `/admin/release/upload` route and controller logic, along with the legacy `upload.js` script. - Add comprehensive feature and unit tests covering the new component and the CDN validation service.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import Tagify from '@yaireo/tagify';
|
||||
import '@yaireo/tagify/dist/tagify.css';
|
||||
|
||||
document.addEventListener('livewire:init', () => {
|
||||
const Alpine = window.Alpine;
|
||||
|
||||
const dropdownOptions = {
|
||||
classname: "color-blue",
|
||||
enabled: 0,
|
||||
maxItems: 10,
|
||||
position: "text",
|
||||
closeOnSelect: false,
|
||||
highlightFirst: true
|
||||
};
|
||||
|
||||
Alpine.data('adminReleaseTags', () => ({
|
||||
tagify: null,
|
||||
|
||||
init() {
|
||||
const el = this.$refs.tags;
|
||||
|
||||
window.axios.get('/admin/tags').then((response) => {
|
||||
if (response.status != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tagify = new Tagify(el, {
|
||||
whitelist: response.data.tags,
|
||||
dropdown: dropdownOptions
|
||||
});
|
||||
|
||||
this.tagify.on('change', () => {
|
||||
this.$wire.set('tags', this.tagify.value.map((t) => t.value));
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
Alpine.data('adminReleaseStudio', () => ({
|
||||
tagify: null,
|
||||
|
||||
init() {
|
||||
const el = this.$refs.studio;
|
||||
|
||||
window.axios.get('/admin/studios').then((response) => {
|
||||
if (response.status != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tagify = new Tagify(el, {
|
||||
whitelist: response.data.studios,
|
||||
dropdown: dropdownOptions
|
||||
});
|
||||
|
||||
this.tagify.on('change', () => {
|
||||
const values = this.tagify.value.map((t) => t.value);
|
||||
this.$wire.set('studio', values.length ? values[values.length - 1] : '');
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}));
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
import Tagify from '@yaireo/tagify';
|
||||
import '@yaireo/tagify/dist/tagify.css';
|
||||
|
||||
const taginput = document.querySelector("#tags");
|
||||
const studioinput = document.querySelector("#studio");
|
||||
|
||||
// Get Tags from API
|
||||
window.axios.get('/admin/tags').then(function (response) {
|
||||
if (response.status != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
new Tagify(taginput, {
|
||||
whitelist: response.data.tags,
|
||||
dropdown: {
|
||||
classname: "color-blue",
|
||||
enabled: 0, // show the dropdown immediately on focus
|
||||
maxItems: 10,
|
||||
position: "text", // place the dropdown near the typed text
|
||||
closeOnSelect: false, // keep the dropdown open after selecting a suggestion
|
||||
highlightFirst: true
|
||||
}
|
||||
});
|
||||
}).catch(function (error) {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
// Get Studios from API
|
||||
window.axios.get('/admin/studios').then(function (response) {
|
||||
if (response.status != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
new Tagify(studioinput, {
|
||||
whitelist: response.data.studios,
|
||||
dropdown: {
|
||||
classname: "color-blue",
|
||||
enabled: 0, // show the dropdown immediately on focus
|
||||
maxItems: 10,
|
||||
position: "text", // place the dropdown near the typed text
|
||||
closeOnSelect: false, // keep the dropdown open after selecting a suggestion
|
||||
highlightFirst: true
|
||||
}
|
||||
});
|
||||
}).catch(function (error) {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
let eps = 1;
|
||||
|
||||
function dynEpisode() {
|
||||
let amount = this.value;
|
||||
|
||||
if (amount > eps) {
|
||||
eps += 1;
|
||||
var episodeUploads = `
|
||||
<div class="grid grid-cols-2" id="dynU` + eps + `">
|
||||
<div class="p-4">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodecover` + eps + `">Cover ` + eps + `:</label>
|
||||
<input class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900" type="file" name="episodecover` + eps + `" id="episodecover` + eps + `" required>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodegallery` + eps + `">Gallery ` + eps + `:</label>
|
||||
<input class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900" type="file" name="episodegallery` + eps + `[]" id="episodegallery` + eps + `" multiple="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 pt-0" id="dynB` + eps + `">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="description` + eps + `">Description ` + eps + `:</label>
|
||||
<textarea rows="4" cols="50" id="description` + eps + `" name="description` + eps + `" class="mt-1 block w-full border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm" required>
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="p-4 pt-0" id="dynD` + eps + `">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodedlurl` + eps + `">Download 1080p ` + eps + `:</label>
|
||||
<input class="border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm block w-full" id="episodedlurl` + eps + `" type="text" name="episodedlurl` + eps + `" required="required">
|
||||
</div>
|
||||
|
||||
<div class="p-4 pt-0" id="dynD48fps` + eps + `">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodedlurlinterpolated` + eps + `">Download 1080p48fps ` + eps + `:</label>
|
||||
<input class="border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm block w-full" id="episodedlurlinterpolated` + eps + `" type="text" name="episodedlurlinterpolated` + eps + `">
|
||||
</div>
|
||||
|
||||
<div class="p-4 pt-0" id="dynD4k` + eps + `">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodedlurl4k` + eps + `">Download 4k ` + eps + `:</label>
|
||||
<input class="border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm block w-full" id="episodedlurl4k` + eps + `" type="text" name="episodedlurl4k` + eps + `" required="required">
|
||||
</div>
|
||||
|
||||
<div class="p-4 pt-0" id="dynDUHD48fps` + eps + `">
|
||||
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="downloadUHDi` + eps + `">Download 4k 48fps ` + eps + `:</label>
|
||||
<input class="border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm block w-full" id="downloadUHDi` + eps + `" type="text" name="downloadUHDi` + eps + `">
|
||||
</div>
|
||||
`;
|
||||
|
||||
var element = document.getElementById('moreEpisodes');
|
||||
element.innerHTML = element.innerHTML + episodeUploads;
|
||||
} else if (amount < eps) {
|
||||
if (amount == 0) {
|
||||
this.value = 1;
|
||||
return;
|
||||
}
|
||||
document.getElementById("dynU" + eps).remove();
|
||||
document.getElementById("dynD" + eps).remove();
|
||||
document.getElementById("dynD4k" + eps).remove();
|
||||
document.getElementById("dynD48fps" + eps).remove();
|
||||
document.getElementById("dynDUHD48fps" + eps).remove();
|
||||
document.getElementById("dynB" + eps).remove();
|
||||
eps -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document.getElementById("episodes").addEventListener('change', dynEpisode);
|
||||
Reference in New Issue
Block a user