Initial commit
This commit is contained in:
37
resources/js/SubmissionsClass/Credits.js
Normal file
37
resources/js/SubmissionsClass/Credits.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/** @typedef { import('types/CreditsObject.js').CreditsObject} CreditsObject */
|
||||
|
||||
export function Credits(){
|
||||
return {
|
||||
|
||||
/**
|
||||
* Credits. Preloaded at startup.
|
||||
* @type {CreditsObject[]}
|
||||
*/
|
||||
credits: [],
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string|null} jsonCredits
|
||||
*/
|
||||
init( jsonCredits= null ){
|
||||
if( jsonCredits !== null ){
|
||||
this.credits = JSON.parse(jsonCredits);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add an empty credit.
|
||||
*/
|
||||
addEmptyCredits() {
|
||||
this.credits.push({name: '', description: ''});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a specific credits.
|
||||
* @param {number} index index of credit.
|
||||
*/
|
||||
removeCredits( index ){
|
||||
this.credits.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
resources/js/SubmissionsClass/FSFileData.js
Normal file
154
resources/js/SubmissionsClass/FSFileData.js
Normal file
@@ -0,0 +1,154 @@
|
||||
/** @typedef { import('types/UploadchunkResponse.js').UploadchunkResponse} UploadchunkResponse */
|
||||
|
||||
export const CHUNK_SIZE = 8192;
|
||||
|
||||
/**
|
||||
* An uploaded file instance.
|
||||
* Create a new file data.
|
||||
*
|
||||
* @param {string} name Filename
|
||||
* @param {number} totalChunks Total number of chunks of the file.
|
||||
* @param rawFile The JS file element relation.
|
||||
*/
|
||||
export function FSFileData(name, totalChunks, rawFile ) {
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* Filename.
|
||||
* @type {string}
|
||||
*/
|
||||
name,
|
||||
|
||||
/**
|
||||
* Number of total chunks based on CHUNK_SIZE.
|
||||
* @type {number}
|
||||
*/
|
||||
totalChunks,
|
||||
|
||||
/**
|
||||
* The JS file element relation.
|
||||
*/
|
||||
rawFile,
|
||||
|
||||
/**
|
||||
* Upload progression value.
|
||||
* @type {number}
|
||||
*/
|
||||
progressValue: 0,
|
||||
|
||||
/**
|
||||
* Current chunk uploaded.
|
||||
* @type {number}
|
||||
*/
|
||||
currentChunk: 0,
|
||||
|
||||
/**
|
||||
* If the upload of the file is finished.
|
||||
* @type {boolean}
|
||||
*/
|
||||
done: false,
|
||||
|
||||
/**
|
||||
* If there is an error during file uploading.
|
||||
* @type {any|null}
|
||||
*/
|
||||
error: null,
|
||||
|
||||
/**
|
||||
* UUID v4 for the file.
|
||||
* @type {`${string}-${string}-${string}-${string}-${string}`}
|
||||
*/
|
||||
uuid: crypto.randomUUID(),
|
||||
|
||||
/**
|
||||
* Look if this file is currently uploading.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isUploading()
|
||||
{
|
||||
return !this.done && !this.error;
|
||||
},
|
||||
|
||||
/**
|
||||
* Build API url.
|
||||
* @param {string} section
|
||||
* @returns {string} The API url.
|
||||
*/
|
||||
buildUrl(section)
|
||||
{
|
||||
return `/api/fs/upload-chunk/${section}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload the file.
|
||||
* @param {string} section section of the file.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async upload(section)
|
||||
{
|
||||
|
||||
if (!this.rawFile)
|
||||
return; // Can't upload in that case.
|
||||
|
||||
/**
|
||||
* Get CSRF token for uploading request.
|
||||
* @type {string}
|
||||
*/
|
||||
const CSRF = document.querySelector('meta[name=csrf-token]')?.content ?? '';
|
||||
|
||||
for (let i = 0; i < this.totalChunks; i++) {
|
||||
|
||||
if (this.error)
|
||||
return; // Abort the process.
|
||||
|
||||
const start = i * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, this.rawFile.size);
|
||||
const chunk = this.rawFile.slice(start, end);
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('file', chunk);
|
||||
formData.append('file_uuid', this.uuid);
|
||||
formData.append('current_chunk', i);
|
||||
formData.append('total_chunks', this.totalChunks);
|
||||
formData.append('filename', this.rawFile.name);
|
||||
formData.append('_token', CSRF);
|
||||
|
||||
// -----
|
||||
// UPLOAD TIME !
|
||||
// -----
|
||||
|
||||
try {
|
||||
const RESPONSE = await fetch(this.buildUrl(section), {method: 'POST', body: formData});
|
||||
|
||||
if (!RESPONSE.ok) // Problem with the request.
|
||||
throw new Error(`${RESPONSE.status} ${RESPONSE.statusText}`);
|
||||
|
||||
/** @type {UploadchunkResponse} */
|
||||
const DATA = await RESPONSE.json();
|
||||
|
||||
if (DATA.success !== true || DATA.uploaded !== true)
|
||||
// The request reached the file server but could not be sent.
|
||||
throw new Error(`${DATA.error}`);
|
||||
|
||||
this.currentChunk = i + 1;
|
||||
this.progressValue = Math.round(((i + 1) / this.totalChunks) * 100);
|
||||
|
||||
if (DATA.finished === true) {
|
||||
this.done = true;
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
this.error = 'Error on chunk ' + (i + 1) + '. ' + err.message;
|
||||
this.progressValue = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
133
resources/js/SubmissionsClass/FSUploader.js
Normal file
133
resources/js/SubmissionsClass/FSUploader.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import { FSFileData, CHUNK_SIZE } from "./FSFileData.js";
|
||||
|
||||
/**
|
||||
* File uploader on FileServer.
|
||||
* @returns {{files: Array<FSFileData>, section: string, init(): void, readonly numberOfFiles: number, readonly isUploading: boolean, readonly hasErrors: boolean, readonly allFilesUploaded: boolean, totalChunksNumber(*): number, handleSubmitFile(Event): Promise<void>, handleRetryFile(number): Promise<void>, removeFile(number): void}|boolean|this is FSFileData[]|number|boolean}
|
||||
* @constructor
|
||||
*/
|
||||
export function FSUploader(){
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* Array of uploaded files.
|
||||
* @type {Array<FSFileData>}
|
||||
*/
|
||||
files: [],
|
||||
|
||||
/**
|
||||
* Section that files must be uploaded.
|
||||
* @type {string}
|
||||
*/
|
||||
section: document.querySelector("meta[name='fs-section']")?.content ?? '',
|
||||
|
||||
/**
|
||||
* Triggered in fs-upload.blade.php
|
||||
* Refresh icons.
|
||||
*
|
||||
* @param {Array<FSFileData>} oldFilesArray
|
||||
*/
|
||||
init( oldFilesArray ){
|
||||
this.$watch('files', () =>{
|
||||
this.$nextTick(() => window.refreshIcons(this.$el) )
|
||||
})
|
||||
|
||||
if( oldFilesArray !== undefined && oldFilesArray.length > 0)
|
||||
this.files = oldFilesArray;
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortcut to files.length.
|
||||
* @returns {number}
|
||||
*/
|
||||
get numberOfFiles(){
|
||||
return this.files.length
|
||||
},
|
||||
|
||||
/**
|
||||
* Look if some files are currently in upload.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isUploading(){
|
||||
return this.files.some(
|
||||
file => file.isUploading
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if some files have an error or not.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get hasErrors(){
|
||||
return this.files.some(
|
||||
file => file.error
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if all files are uploaded.
|
||||
* True if all files are uploaded or no one.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get allFilesUploaded(){
|
||||
return ( this.numberOfFiles === 0 || this.files.every(file => file.done) );
|
||||
},
|
||||
|
||||
/**
|
||||
* Get total chunks size of a raw file.
|
||||
* @param rawFile
|
||||
* @returns {number}
|
||||
*/
|
||||
totalChunksNumber( rawFile ){
|
||||
return Math.ceil( rawFile.size / CHUNK_SIZE );
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle file submission.
|
||||
* You can submit multiple files at once.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleSubmitFile( e ){
|
||||
|
||||
const RAW_FILES_FROM_EVENT = Array.from( e.target.files );
|
||||
e.target.value = ''; // Default.
|
||||
|
||||
for( const RAW_FILE of RAW_FILES_FROM_EVENT ){
|
||||
const TOTAL_CHUNKS = this.totalChunksNumber(RAW_FILE);
|
||||
let fsData = FSFileData( RAW_FILE.name, TOTAL_CHUNKS, RAW_FILE );
|
||||
|
||||
this.files.push( fsData );
|
||||
await this.files[this.files.length - 1].upload(this.section);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Retry file uploading.
|
||||
*
|
||||
* @param {number} index FSFileData index in this.files.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
handleRetryFile( index ){
|
||||
|
||||
const OLDFSFILE = this.files[index];
|
||||
let fsData = FSFileData(OLDFSFILE.name, this.totalChunksNumber(OLDFSFILE.rawFile), OLDFSFILE.rawFile );
|
||||
this.files[index] = fsData;
|
||||
|
||||
this.files[index].upload(this.section);
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a file.
|
||||
* @param {number} index FSFileData index in this.files.
|
||||
*/
|
||||
handleRemoveFile( index ){
|
||||
this.files.splice(index, 1);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
127
resources/js/SubmissionsClass/GalleryManager.js
Normal file
127
resources/js/SubmissionsClass/GalleryManager.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import { MainImageManager as GalleryImage } from "./MainImageManager.js";
|
||||
|
||||
const MAX_GALLERY = 20;
|
||||
|
||||
export function GalleryManager() {
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* All images uploaded.
|
||||
* @type {Array<GalleryImage>}
|
||||
*/
|
||||
images: [],
|
||||
|
||||
/**
|
||||
* Forward to this.images.length
|
||||
* @returns {number}
|
||||
*/
|
||||
get number(){
|
||||
return this.images.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if all images has been uploaded or not.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get allUploaded(){
|
||||
for(const IMG of this.images){
|
||||
if( IMG.serverFilePath == null ){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return if there is an error on an image or not.
|
||||
*
|
||||
* @returns {null|Array<string>}
|
||||
*/
|
||||
get error(){
|
||||
const RESPONSE = [];
|
||||
for( const IMG of this.images ){
|
||||
if( IMG.error !== null )
|
||||
RESPONSE.push( IMG.error );
|
||||
}
|
||||
if( RESPONSE.length === 0 )
|
||||
return null;
|
||||
return RESPONSE;
|
||||
},
|
||||
|
||||
/**
|
||||
* Superior or equal than 20.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isFull(){
|
||||
return this.images.length >= MAX_GALLERY;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reload image if there is an error or edition.
|
||||
*
|
||||
* @param {Array<string>} oldPaths
|
||||
*/
|
||||
init( oldPaths = null ){
|
||||
if( oldPaths === null || oldPaths.length <= 0 )
|
||||
return;
|
||||
|
||||
for( const PATH of oldPaths ){
|
||||
|
||||
if( this.isFull )
|
||||
break;
|
||||
|
||||
const IMG = GalleryImage();
|
||||
IMG.getOldImage( PATH );
|
||||
this.images.push(IMG);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload images, refresh the preview and store images if there is a problem.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleSubmitFiles(e){
|
||||
const FILES = e.target.files;
|
||||
if( !FILES || FILES.length <= 0 )
|
||||
return; // No file uploaded.
|
||||
|
||||
for( const FILE of FILES ){
|
||||
|
||||
if( this.isFull )
|
||||
break;
|
||||
|
||||
const IMG = GalleryImage();
|
||||
IMG.name = FILE.name;
|
||||
IMG.type = FILE.type;
|
||||
this.images.push(IMG);
|
||||
|
||||
const IMG_FROM_LIST = this.images[this.images.length - 1];
|
||||
|
||||
await IMG_FROM_LIST.uploadImageToTemporary( FILE );
|
||||
|
||||
await new Promise( resolve => {
|
||||
const READER = new FileReader();
|
||||
READER.onload = (e2) => {
|
||||
IMG_FROM_LIST.preview = e2.target.result;
|
||||
resolve();
|
||||
}
|
||||
READER.onerror = () => resolve();
|
||||
READER.readAsDataURL(FILE);
|
||||
});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove specific file.
|
||||
* @param {number} index
|
||||
*/
|
||||
handleRemoveFile(index){
|
||||
this.images[index].handleRemoveFile(null);
|
||||
this.images.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
resources/js/SubmissionsClass/GameSelector.js
Normal file
38
resources/js/SubmissionsClass/GameSelector.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Handle new game already selected.
|
||||
*
|
||||
* @param {Object} initialContent With gameName, gamePlatformId and gameGenreId field.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function GameSelector(){
|
||||
return {
|
||||
|
||||
/**
|
||||
* Game Name
|
||||
* @type {string|null}
|
||||
*/
|
||||
name: null,
|
||||
|
||||
/**
|
||||
* Game Platform Id.
|
||||
* @type {number|null}
|
||||
*/
|
||||
platformId: null,
|
||||
|
||||
/**
|
||||
* Game genre Id.
|
||||
* @type {number|null}
|
||||
*/
|
||||
genreId: null,
|
||||
|
||||
/**
|
||||
* Initialize game selector.
|
||||
* @param initialContent
|
||||
*/
|
||||
init( initialContent = {} ){
|
||||
this.name = initialContent.name ?? null;
|
||||
this.platformId = Number(initialContent.platformId) ?? null;
|
||||
this.genreId = Number(initialContent.genreId) ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
resources/js/SubmissionsClass/HashesManager.js
Normal file
60
resources/js/SubmissionsClass/HashesManager.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { calculate as calculateHashes } from "../hashes.js";
|
||||
|
||||
export function HashesManager( wire ) {
|
||||
return {
|
||||
|
||||
/**
|
||||
* Wire variable instance.
|
||||
*/
|
||||
$wire: wire,
|
||||
|
||||
/**
|
||||
* If a file hash is currently calculated or not.
|
||||
* @type {boolean}
|
||||
*/
|
||||
isCalculating: false,
|
||||
|
||||
/**
|
||||
* An error on hash calculation.
|
||||
* @type {any|null}
|
||||
*/
|
||||
error: null,
|
||||
|
||||
async handleSubmitFile(){
|
||||
|
||||
if( this.isCalculating === true ) // Calculation already done for another file.
|
||||
return;
|
||||
|
||||
this.error = null; // Reset.
|
||||
const FILE = await this.openFileInput();
|
||||
|
||||
if( !FILE )
|
||||
return; // No file sent.
|
||||
|
||||
this.isCalculating = true;
|
||||
|
||||
try {
|
||||
const RESULT = await calculateHashes(FILE);
|
||||
await this.$wire.addHash(RESULT.filename, RESULT.crc32, RESULT.sha1); // Send a signal to livewire.
|
||||
} catch(err) {
|
||||
this.error = err.message;
|
||||
} finally {
|
||||
this.isCalculating = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Open a specific file.
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
async openFileInput(){
|
||||
return new Promise(resolve => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.onchange = () => resolve(input.files[0] ?? null );
|
||||
input.oncancel = () => resolve(null);
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
151
resources/js/SubmissionsClass/MainImageManager.js
Normal file
151
resources/js/SubmissionsClass/MainImageManager.js
Normal file
@@ -0,0 +1,151 @@
|
||||
export function MainImageManager() {
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* If an image has been uploaded or not.
|
||||
* @type {boolean}
|
||||
*/
|
||||
uploaded: false,
|
||||
|
||||
/**
|
||||
* Actual image path on the server.
|
||||
* @type {string|null}
|
||||
*/
|
||||
serverFilePath: null,
|
||||
|
||||
/**
|
||||
* Image filename.
|
||||
* @type {string|null}
|
||||
*/
|
||||
name: null,
|
||||
|
||||
/**
|
||||
* Image filetype.
|
||||
* @type {string|null}
|
||||
*/
|
||||
type: null,
|
||||
|
||||
/**
|
||||
* Handle preview.
|
||||
* @type {unknown}
|
||||
*/
|
||||
preview: null,
|
||||
|
||||
/**
|
||||
* Current error message.
|
||||
* @type {string}
|
||||
*/
|
||||
error: null,
|
||||
|
||||
/**
|
||||
* Reload image if there is an error.
|
||||
*
|
||||
* @param {string|null} oldPath If there is already a path.
|
||||
*/
|
||||
init( oldPath = null ){
|
||||
if(oldPath === "" || oldPath === null)
|
||||
return;
|
||||
|
||||
this.getOldImage(oldPath)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get old image from old path and refresh preview.
|
||||
* @param {string} oldPath
|
||||
* @param {function|null} callback Used in the gallery to push the image at the right time.
|
||||
*/
|
||||
getOldImage( oldPath, callback = null ) {
|
||||
this.readOldImage( '/storage/' + oldPath ).then( blob => {
|
||||
|
||||
this.uploaded = true;
|
||||
this.serverFilePath = oldPath;
|
||||
|
||||
const READER = new FileReader();
|
||||
READER.onload = () => {
|
||||
this.preview = READER.result;
|
||||
}
|
||||
READER.readAsDataURL(blob);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get old image data.
|
||||
* @param {string} url
|
||||
* @returns {Promise<Blob>}
|
||||
*/
|
||||
async readOldImage(url){
|
||||
const RESPONSE = await fetch(url);
|
||||
return await RESPONSE.blob();
|
||||
},
|
||||
|
||||
async uploadImageToTemporary( file ){
|
||||
|
||||
const CSRF = document.querySelector('meta[name=csrf-token]')?.content ?? '';
|
||||
const URL = `/api/tempfile/upload`;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('file', file);
|
||||
formData.append('_token', CSRF );
|
||||
|
||||
try {
|
||||
const RESPONSE = await fetch( URL, { method: 'POST', body: formData } );
|
||||
|
||||
if( !RESPONSE.ok ) // Problem with the request.
|
||||
throw new Error(`${RESPONSE.status} ${RESPONSE.statusText}`);
|
||||
|
||||
const DATA = await RESPONSE.json();
|
||||
|
||||
if( DATA.path === null ){
|
||||
throw new Error(`${RESPONSE.status} ${RESPONSE.statusText}`);
|
||||
}
|
||||
|
||||
this.serverFilePath = DATA.path;
|
||||
this.uploaded = true;
|
||||
|
||||
} catch (err){
|
||||
this.error = 'Error on main image uploading ' + ( i + 1 ) + '. ' + err.message;
|
||||
console.error( this.error );
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload image, refresh the preview and store image if there is a problem.
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
async handleSubmitFile(e) {
|
||||
const FILE = e.target.files[0];
|
||||
if (!FILE)
|
||||
return; // No file uploaded.
|
||||
|
||||
this.handleRemoveFile(null);
|
||||
await this.uploadImageToTemporary(FILE);
|
||||
|
||||
this.name = FILE.name;
|
||||
this.type = FILE.type;
|
||||
|
||||
const READER = new FileReader();
|
||||
READER.onload = (e2) => {
|
||||
this.preview = e2.target.result;
|
||||
}
|
||||
READER.readAsDataURL(FILE);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove main image.
|
||||
* @param {Event} e
|
||||
*/
|
||||
handleRemoveFile(e){
|
||||
this.uploaded = false;
|
||||
this.serverFilePath = null;
|
||||
this.preview = null;
|
||||
this.name = null;
|
||||
this.type = null;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
18
resources/js/SubmissionsClass/types/CreditsObject.js
Normal file
18
resources/js/SubmissionsClass/types/CreditsObject.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @typedef {Object} CreditsObject
|
||||
*
|
||||
* @property {string} name Credits name.
|
||||
* @property {string} description Credits description.
|
||||
*/
|
||||
|
||||
/**
|
||||
* If this object is a credit object or not.
|
||||
*
|
||||
* @param {object} object
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isACreditsObject( object ){
|
||||
return typeof object === 'object' && object.name !== undefined && object.description !== undefined;
|
||||
}
|
||||
|
||||
export {}
|
||||
13
resources/js/SubmissionsClass/types/UploadchunkResponse.js
Normal file
13
resources/js/SubmissionsClass/types/UploadchunkResponse.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @typedef {Object} UploadchunkResponse
|
||||
*
|
||||
* @see app/Http/FileServerController.php
|
||||
* @external RHPZFS::src/Endpoints/Uploadchunk
|
||||
*
|
||||
* @property {number} chunk The current chunk that has been uploaded.
|
||||
* @property {number} total_chunks Number of total chunks.
|
||||
* @property {boolean} uploaded If the chunk has been correctly uploaded.
|
||||
* @property {Object|boolean} file If the file has been entirely uploaded or not.
|
||||
* @property {boolean} finished Added by main server. Indicates if the file upload is finished or not.
|
||||
*/
|
||||
export {}
|
||||
23
resources/js/app.js
Normal file
23
resources/js/app.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createIcons, icons } from "lucide";
|
||||
import EasyMDE from "easymde";
|
||||
import "easymde/dist/easymde.min.css";
|
||||
|
||||
import { calculate as calculateHashes } from "./hashes.js";
|
||||
|
||||
|
||||
// Lucide icons.
|
||||
createIcons({ icons });
|
||||
|
||||
window.refreshIcons = (container = document) => {
|
||||
const pending = container.querySelectorAll('[data-lucide]');
|
||||
if (pending.length === 0) return;
|
||||
createIcons({ icons });
|
||||
};
|
||||
|
||||
// EasyMDE.
|
||||
window.EasyMDE = EasyMDE;
|
||||
|
||||
// Hashes.
|
||||
window.calculateHashes = calculateHashes;
|
||||
|
||||
|
||||
56
resources/js/hashes.js
Normal file
56
resources/js/hashes.js
Normal file
@@ -0,0 +1,56 @@
|
||||
function createCrcTable() {
|
||||
const table = [];
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) {
|
||||
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
||||
}
|
||||
table[n] = c;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
async function crc32(buffer) {
|
||||
let crc = 0 ^ (-1);
|
||||
const bytes = new Uint8Array(buffer);
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
crc = (crc >>> 8) ^ crcTable[(crc ^ bytes[i]) & 0xFF];
|
||||
}
|
||||
return ((crc ^ (-1)) >>> 0).toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
async function sha1(buffer) {
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-1', buffer);
|
||||
return Array.from(new Uint8Array(hashBuffer))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
const crcTable = createCrcTable();
|
||||
|
||||
export async function calculate( file ){
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const freader = new FileReader();
|
||||
|
||||
freader.onload = async e => {
|
||||
try {
|
||||
const buffer = e.target.result;
|
||||
const crc32V = await crc32(buffer);
|
||||
const sha1V = await sha1(buffer);
|
||||
|
||||
resolve({
|
||||
filename: file.name,
|
||||
crc32: crc32V,
|
||||
sha1: sha1V
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
freader.onerror = () => {
|
||||
reject( new Error(`Could not parse file: ${file.name}`) );
|
||||
}
|
||||
freader.readAsArrayBuffer(file);
|
||||
})
|
||||
}
|
||||
387
resources/js/submissions.js
Normal file
387
resources/js/submissions.js
Normal file
@@ -0,0 +1,387 @@
|
||||
import { FSUploader } from "./SubmissionsClass/FSUploader.js";
|
||||
import { HashesManager } from "./SubmissionsClass/HashesManager.js";
|
||||
import { GameSelector } from "./SubmissionsClass/GameSelector.js";
|
||||
import { MainImageManager } from "./SubmissionsClass/MainImageManager.js";
|
||||
import { GalleryManager } from "./SubmissionsClass/GalleryManager.js";
|
||||
import { Credits } from "./SubmissionsClass/Credits.js";
|
||||
|
||||
/**
|
||||
* If there is some server side errors.
|
||||
* We may need reload some things.
|
||||
* @type {boolean}
|
||||
*/
|
||||
const SERVER_SIDE_ERRORS = document.querySelector('meta[name="submission-has-errors"]')?.content === '1';
|
||||
|
||||
/**
|
||||
* Object map of errors messages
|
||||
* @type {Object<string,string>}
|
||||
*/
|
||||
const ERROR_TABLE = {
|
||||
isUploading: "A file is uploading. Please wait.",
|
||||
noFiles: "Please select a file to upload",
|
||||
uploadError: "One or more files failed to upload.",
|
||||
notAllFilesDone: "Not all the files have finished uploading yet.",
|
||||
noModifications: "Please select at least a type of hack.",
|
||||
noDescription: "Please provide a description.",
|
||||
noGame: "Please provide a game or create a new one and fill all the required fields.",
|
||||
noLanguages: "Please select at least a language.",
|
||||
noAuthors: "Please provide at least an author or create a new one and fill all the required fields.",
|
||||
noMainImage: "Please select a main image.",
|
||||
noGalleryImages: "Please select at least a gallery image.",
|
||||
isSubmitting: "The entry is already during submission."
|
||||
}
|
||||
|
||||
/**
|
||||
* Current section.
|
||||
* @returns {string}
|
||||
* @constructor
|
||||
*/
|
||||
const SECTION = () => document.querySelector("meta[name='fs-section']")?.content ?? '';
|
||||
|
||||
window.FSUploader = FSUploader;
|
||||
window.HashesManager = HashesManager;
|
||||
window.GameSelector = GameSelector;
|
||||
window.MainImageManager = MainImageManager;
|
||||
window.GalleryManager = GalleryManager;
|
||||
window.Credits = Credits;
|
||||
|
||||
/**
|
||||
* Verify if at least one checkbox is checked in this element.
|
||||
* @param {HTMLElement} element
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function verifyCheckboxes( element ){
|
||||
if( !parent ) return false;
|
||||
return Array.from(element.querySelectorAll('input[type="checkbox"]')).some(el => el.checked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if an EasyMDE field is filled.
|
||||
*
|
||||
* @param {string} fieldName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function verifyMDE( fieldName ){
|
||||
const textarea = document.querySelector('#field_' + fieldName);
|
||||
if( textarea && textarea.value.trim().length > 0 ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const field = window['mde_' + fieldName] || null;
|
||||
return field && typeof field.value === 'function' && field.value().trim().length > 0;
|
||||
}
|
||||
|
||||
window.SubmissionVerifications = {
|
||||
|
||||
/**
|
||||
* Verify if we are in an upload.
|
||||
* @param {FSUploader} Uploader
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step1_DuringFSUpload: function( Uploader ){
|
||||
return !Uploader.isUploading;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if at least one file is uploaded.
|
||||
* @param {FSUploader} Uploader
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step2_NoFilesFSUpload: function( Uploader ){
|
||||
return Uploader.numberOfFiles > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if any files haven't error.
|
||||
* @param {FSUploader} Uploader
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step3_ErrorsFSUpload: function( Uploader ){
|
||||
return !Uploader.hasErrors;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if all files are uploaded.
|
||||
* @param {FSUploader} Uploader
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step4_AllFilesUploadedFSUpload: function( Uploader ){
|
||||
return Uploader.allFilesUploaded;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if at least one checkbox of romhacks modifications is checked.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step5_RomhacksModificationsCheckboxes: function(){
|
||||
return verifyCheckboxes( document.querySelector( '#modifications-group' ) );
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if the description field has at least one character.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step6_VerifyDescription: function(){
|
||||
return verifyMDE('description');
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if a game is provided.
|
||||
* @param element this.$el
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step7_VerifyGame: function( element ){
|
||||
|
||||
// Check if we have an already existent selected game.
|
||||
const GAME_ID_INPUT = document.querySelector('input[name="game_id"]');
|
||||
if( GAME_ID_INPUT ){
|
||||
if( GAME_ID_INPUT.value !== '' && Number(GAME_ID_INPUT.value) > 0){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have a new game.
|
||||
let gameSelector = element.querySelector('[x-data="GameSelector()"]');
|
||||
gameSelector = gameSelector ? Alpine.$data(gameSelector) : null;
|
||||
if( gameSelector !== null ){
|
||||
|
||||
if( !gameSelector.name || !gameSelector.name.toString().trim().length )
|
||||
return false;
|
||||
if( !gameSelector.platformId || gameSelector.platformId === '' || gameSelector.platformId === 0 )
|
||||
return false;
|
||||
if( !gameSelector.genreId || gameSelector.genreId === '' || gameSelector.genreId === 0 )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if at least one checkbox of languages is checked.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step8_LanguagesCheckboxes: function(){
|
||||
return verifyCheckboxes( document.querySelector( '#languages-group' ) );
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if at least one (new) author has been filled.
|
||||
* @return {boolean}
|
||||
*/
|
||||
step9_verifyAuthors: function(){
|
||||
const authorField = document.querySelectorAll('input[name="authors[]"]');
|
||||
const newAuthorField = document.querySelectorAll('input[name="new-authors[]"]');
|
||||
|
||||
return ( authorField.length > 0 || newAuthorField.length > 0 );
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if a main image has been uploaded.
|
||||
* @param element this.$el
|
||||
* @return {boolean}
|
||||
*/
|
||||
step10_verifyMainImage: function( element ){
|
||||
let MainImageData = element.querySelector('[x-data="MainImageManager()"]');
|
||||
MainImageData = MainImageData ? Alpine.$data(MainImageData) : null;
|
||||
|
||||
if( ! MainImageData ){
|
||||
return false;
|
||||
}
|
||||
|
||||
return MainImageData.uploaded;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if at least one image is uploaded in the gallery.
|
||||
* @param element this.$el
|
||||
* @return {boolean}
|
||||
*/
|
||||
step11_verifyGallery: function( element){
|
||||
let GalleryData = element.querySelector('[x-data="GalleryManager()"]');
|
||||
GalleryData = GalleryData ? Alpine.$data(GalleryData) : null;
|
||||
|
||||
if( ! GalleryData ){
|
||||
return false;
|
||||
}
|
||||
|
||||
return GalleryData.number > 0 && GalleryData.allUploaded;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle entire submission process.
|
||||
*/
|
||||
window.Submission = function(){
|
||||
return {
|
||||
|
||||
/**
|
||||
* If the script is during a try of submission process.
|
||||
* @type {boolean}
|
||||
*/
|
||||
duringSubmissionProcess: false,
|
||||
|
||||
/**
|
||||
* Error checked.
|
||||
* @type {string|null}
|
||||
*/
|
||||
errorKey: null,
|
||||
|
||||
/**
|
||||
* Return error message.
|
||||
* @return {string}
|
||||
*/
|
||||
get errorMessage(){
|
||||
return ERROR_TABLE[this.errorKey] ?? "Unknown error";
|
||||
},
|
||||
|
||||
init(){
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current FSUploader if initialized.
|
||||
*
|
||||
* @returns {FSUploader|null}
|
||||
* @constructor
|
||||
*/
|
||||
get Uploader(){
|
||||
const el = this.$el.querySelector('[x-data="FSUploader()"]');
|
||||
return el ? Alpine.$data(el) : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Do each form verifications.
|
||||
* Update also this.errorKey.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
verifyForm(){
|
||||
|
||||
console.log( "Step 1" );
|
||||
if( !SubmissionVerifications.step1_DuringFSUpload( this.Uploader ) ){
|
||||
this.errorKey = "isUploading";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 2" );
|
||||
if( !SubmissionVerifications.step2_NoFilesFSUpload( this.Uploader ) ){
|
||||
this.errorKey = "noFiles";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 3" );
|
||||
if( !SubmissionVerifications.step3_ErrorsFSUpload( this.Uploader ) ){
|
||||
this.errorKey = "uploadError";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 4" );
|
||||
if( !SubmissionVerifications.step4_AllFilesUploadedFSUpload( this.Uploader ) ){
|
||||
this.errorKey = "notAllFilesDone";
|
||||
return false;
|
||||
}
|
||||
|
||||
if( SECTION() === "romhacks" ){
|
||||
console.log( "Step 5" );
|
||||
if( !SubmissionVerifications.step5_RomhacksModificationsCheckboxes()){
|
||||
this.errorKey = "noModifications";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log( "Step 6" );
|
||||
if( !SubmissionVerifications.step6_VerifyDescription() ){
|
||||
this.errorKey = "noDescription";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 7" );
|
||||
if( !SubmissionVerifications.step7_VerifyGame( this.$el ) ){
|
||||
this.errorKey = "noGame";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 8" );
|
||||
if( !SubmissionVerifications.step8_LanguagesCheckboxes()){
|
||||
this.errorKey = "noLanguages";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 9" );
|
||||
if( !SubmissionVerifications.step9_verifyAuthors()){
|
||||
this.errorKey = "noAuthors";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 10" );
|
||||
if( !SubmissionVerifications.step10_verifyMainImage( this.$el )){
|
||||
this.errorKey = "noMainImage";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 11" );
|
||||
if( !SubmissionVerifications.step11_verifyGallery( this.$el )){
|
||||
this.errorKey = "noGalleryImages";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Scroll to the specific error field.
|
||||
*/
|
||||
scrollToError(){
|
||||
const refMap = {
|
||||
noFiles: 'uploadTarget',
|
||||
isUploading: 'uploadTarget',
|
||||
notAllFilesDone: 'uploadTarget',
|
||||
uploadError: 'uploadTarget',
|
||||
noModifications: 'modificationsGroup',
|
||||
noDescription: 'descriptionField',
|
||||
noGame: 'gameSelector',
|
||||
noLanguages: 'languagesGroup',
|
||||
noAuthors: 'authorsSelector',
|
||||
noMainImage: 'main-image-field',
|
||||
noGalleryImages: 'gallery-field',
|
||||
isSubmitting: 'submitButton'
|
||||
};
|
||||
|
||||
const target = this.$refs[refMap[this.errorKey]]
|
||||
|| this.$el.querySelector('.upload-list')
|
||||
|| this.$el.querySelector('.form-upload');
|
||||
|
||||
if (target) {
|
||||
target.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* If you want to submit the form.
|
||||
* @param {Event} e
|
||||
*/
|
||||
submitForm( e ){
|
||||
|
||||
if( this.duringSubmissionProcess )
|
||||
return; // Don't submit two times.
|
||||
|
||||
this.errorKey = null; // Reset.
|
||||
this.duringSubmissionProcess = true;
|
||||
|
||||
if( !this.verifyForm() ){
|
||||
|
||||
this.scrollToError();
|
||||
|
||||
this.duringSubmissionProcess = false;
|
||||
return;
|
||||
}
|
||||
|
||||
e.target.submit();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
104
resources/js/uploader.js
Normal file
104
resources/js/uploader.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import {createIcons, icons} from "lucide";
|
||||
export const CHUNK_SIZE = 8192;
|
||||
export function FSUploader(){
|
||||
return {
|
||||
files: [],
|
||||
rawFiles: [],
|
||||
section: document.querySelector("meta[name='fs-section']")?.content ?? '',
|
||||
init( oldFiles ){
|
||||
this.$watch('files', () =>{
|
||||
this.$nextTick(() => window.refreshIcons(this.$el) )
|
||||
})
|
||||
},
|
||||
get isUploading(){
|
||||
return this.files.some(f => !f.done && !f.error);
|
||||
},
|
||||
get hasErrors(){
|
||||
return this.files.some(f => f.error);
|
||||
},
|
||||
get allUploaded(){
|
||||
return this.files.length === 0 || this.files.every(f => f.done);
|
||||
},
|
||||
async submitFile(e){
|
||||
const selected = Array.from(e.target.files);
|
||||
e.target.value = '';
|
||||
|
||||
for( const raw of selected ){
|
||||
const totalChunks = Math.ceil(raw.size / CHUNK_SIZE );
|
||||
const index = this.files.length;
|
||||
this.files.push({
|
||||
name: raw.name,
|
||||
progress: 0,
|
||||
currentChunk: 0,
|
||||
totalChunks: totalChunks,
|
||||
done: false,
|
||||
error: null,
|
||||
uuid: crypto.randomUUID()
|
||||
});
|
||||
this.rawFiles.push(raw);
|
||||
this.uploadChunks(raw,index);
|
||||
}
|
||||
},
|
||||
async uploadChunks(rawFile, index){
|
||||
const file = this.files[index];
|
||||
const url = `/api/fs/upload-chunk/${this.section}`;
|
||||
const csrf = document.querySelector('meta[name=csrf-token]')?.content ?? '';
|
||||
|
||||
for( let i = 0; i < file.totalChunks; i++ ){
|
||||
if( file.error )
|
||||
return;
|
||||
|
||||
const start = i * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, rawFile.size);
|
||||
const chunk = rawFile.slice(start, end);
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('file', chunk);
|
||||
formData.append('file_uuid', file.uuid);
|
||||
formData.append('current_chunk', i);
|
||||
formData.append('total_chunks', file.totalChunks);
|
||||
formData.append( 'filename', rawFile.name );
|
||||
formData.append('_token', csrf );
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { method: 'POST', body: formData });
|
||||
if(!response.ok)
|
||||
throw new Error(`${response.status} ${response.statusText}`);
|
||||
|
||||
const data = await response.json();
|
||||
if( data.success !== true || data.uploaded !== true )
|
||||
throw new Error(`${data.error}`);
|
||||
|
||||
file.currentChunk = i + 1;
|
||||
file.progress = Math.round((( i + 1) / file.totalChunks ) * 100);
|
||||
|
||||
if( data.finished === true ){
|
||||
file.done = true;
|
||||
}
|
||||
} catch(err){
|
||||
file.error = 'Error on chunk ' + ( i + 1 ) + '. ' + err.message;
|
||||
file.progress = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
retry(index){
|
||||
const rawFile = this.rawFiles[index];
|
||||
const totalChunks = Math.ceil(rawFile.size / CHUNK_SIZE );
|
||||
this.files[index] = {
|
||||
name: rawFile.name,
|
||||
progress: 0,
|
||||
currentChunk: 0,
|
||||
totalChunks: totalChunks,
|
||||
done: false,
|
||||
error: null,
|
||||
uuid: crypto.randomUUID()
|
||||
};
|
||||
this.uploadChunks(raw,index);
|
||||
},
|
||||
remove(index){
|
||||
this.files.splice(index, 1);
|
||||
this.rawFiles.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user