diff options
| author | grothedev <grothedev@gmail.com> | 2024-12-20 22:43:07 -0600 |
|---|---|---|
| committer | grothedev <grothedev@gmail.com> | 2024-12-20 22:43:07 -0600 |
| commit | c24efb154121f2f98c1d9d2473e96e9e1ec7379d (patch) | |
| tree | 4e621965b508f744d12bc4d720e4628bf7544896 | |
| parent | b554f0614dd02924a536ce34f09fb1fc3d17bc3e (diff) | |
working on stuff
24 files changed, 1728 insertions, 5 deletions
@@ -1,5 +1,7 @@ + + + https://laravel.com/api/11.x/ https://laravel.com/docs/11.x/blade - diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php new file mode 100644 index 0000000..c56b6f2 --- /dev/null +++ b/app/Http/Controllers/FileController.php @@ -0,0 +1,65 @@ +<?php + +namespace App\Http\Controllers; + +use App\Models\File; +use Illuminate\Http\Request; + +class FileController extends Controller +{ + /** + * Display a listing of the resource. + */ + public function index() + { + // + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + // + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + // + } + + /** + * Display the specified resource. + */ + public function show(File $file) + { + // + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(File $file) + { + // + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, File $file) + { + // + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(File $file) + { + // + } +} diff --git a/app/Http/Controllers/LinkController.php b/app/Http/Controllers/LinkController.php new file mode 100644 index 0000000..a175d2f --- /dev/null +++ b/app/Http/Controllers/LinkController.php @@ -0,0 +1,66 @@ +<?php + +namespace App\Http\Controllers; + +use App\Http\Requests\StoreLinkRequest; +use App\Http\Requests\UpdateLinkRequest; +use App\Models\Link; + +class LinkController extends Controller +{ + /** + * Display a listing of the resource. + */ + public function index() + { + // + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + // + } + + /** + * Store a newly created resource in storage. + */ + public function store(StoreLinkRequest $request) + { + // + } + + /** + * Display the specified resource. + */ + public function show(Link $link) + { + // + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(Link $link) + { + // + } + + /** + * Update the specified resource in storage. + */ + public function update(UpdateLinkRequest $request, Link $link) + { + // + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Link $link) + { + // + } +} diff --git a/app/Http/Controllers/SiteController.php b/app/Http/Controllers/SiteController.php index 9607aea..a3f3fec 100644 --- a/app/Http/Controllers/SiteController.php +++ b/app/Http/Controllers/SiteController.php @@ -72,7 +72,7 @@ class SiteController extends Controller return $res; } else { //return "File uploaded: ${filename} "; - return redirect("f/${filename}"); + return redirect("f/${filename}"); //TODO homepage with data flashing (->with()) } } diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php new file mode 100644 index 0000000..754c75a --- /dev/null +++ b/app/Http/Controllers/TagController.php @@ -0,0 +1,65 @@ +<?php + +namespace App\Http\Controllers; + +use App\Models\Tag; +use Illuminate\Http\Request; + +class TagController extends Controller +{ + /** + * Display a listing of the resource. + */ + public function index() + { + // + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + // + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + // + } + + /** + * Display the specified resource. + */ + public function show(Tag $tag) + { + // + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(Tag $tag) + { + // + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Tag $tag) + { + // + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Tag $tag) + { + // + } +} diff --git a/app/Http/Requests/StoreLinkRequest.php b/app/Http/Requests/StoreLinkRequest.php new file mode 100644 index 0000000..b3e52e1 --- /dev/null +++ b/app/Http/Requests/StoreLinkRequest.php @@ -0,0 +1,28 @@ +<?php + +namespace App\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +class StoreLinkRequest extends FormRequest +{ + /** + * Determine if the user is authorized to make this request. + */ + public function authorize(): bool + { + return false; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/UpdateLinkRequest.php b/app/Http/Requests/UpdateLinkRequest.php new file mode 100644 index 0000000..1d70c9a --- /dev/null +++ b/app/Http/Requests/UpdateLinkRequest.php @@ -0,0 +1,28 @@ +<?php + +namespace App\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +class UpdateLinkRequest extends FormRequest +{ + /** + * Determine if the user is authorized to make this request. + */ + public function authorize(): bool + { + return false; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/app/Models/File.php b/app/Models/File.php new file mode 100644 index 0000000..f66e6f9 --- /dev/null +++ b/app/Models/File.php @@ -0,0 +1,11 @@ +<?php + +namespace App\Models; + +use Illuminate\Database\Eloquent\Model; +use App\Models\Traits\AutoFillable; + +class File extends Model +{ + use AutoFillable; +} diff --git a/app/Models/Link.php b/app/Models/Link.php new file mode 100644 index 0000000..285c611 --- /dev/null +++ b/app/Models/Link.php @@ -0,0 +1,12 @@ +<?php + +namespace App\Models; + +use Illuminate\Database\Eloquent\Model; +use App\Models\Traits\AutoFillable; + +class Link extends Model +{ + use AutoFillable; + //protected $fillable = []; +} diff --git a/app/Models/Tag.php b/app/Models/Tag.php new file mode 100644 index 0000000..5a3da52 --- /dev/null +++ b/app/Models/Tag.php @@ -0,0 +1,11 @@ +<?php + +namespace App\Models; + +use Illuminate\Database\Eloquent\Model; +use App\Models\Traits\AutoFillable; + +class Tag extends Model +{ + use Fillable; +} diff --git a/app/Models/Traits/AutoFillable.php b/app/Models/Traits/AutoFillable.php new file mode 100644 index 0000000..044d5b8 --- /dev/null +++ b/app/Models/Traits/AutoFillable.php @@ -0,0 +1,25 @@ +<?php + +namespace App\Models\Traits; + +use Illuminate\Support\Facades\Schema; + +trait AutoFillable +{ + public function initializeAutoFillable() + { + $table = $this->getTable(); + $columns = Schema::getColumnListing($table); + + $protected = ['id', 'created_at', 'updated_at', 'deleted_at']; + $this->fillable = array_diff($columns, $protected); + } +} + +// Then in your model: +use App\Models\Traits\AutoFillable; + +class User extends Model +{ + use AutoFillable; +}
\ No newline at end of file diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 05fb5d9..36a83ff 100755 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -13,10 +13,11 @@ return new class extends Migration { Schema::create('users', function (Blueprint $table) { $table->id(); - $table->string('name'); - $table->string('email')->unique(); + $table->string('name')->required(); + $table->string('email')->nullable()->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); + $table->string('description'); $table->rememberToken(); $table->timestamps(); }); diff --git a/database/migrations/2024_12_21_021128_files.php b/database/migrations/2024_12_21_021128_files.php new file mode 100644 index 0000000..62d1b2a --- /dev/null +++ b/database/migrations/2024_12_21_021128_files.php @@ -0,0 +1,32 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('files', function (Blueprint $table) { + $table->id(); + $table->timestamps(); + $table->string('filename')->required(); + $table->string('path')->unique(); + $table->string('source'); + $table->string('description'); + $table->string('md5')->required(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('files'); + } +}; diff --git a/database/migrations/2024_12_21_021157_tags.php b/database/migrations/2024_12_21_021157_tags.php new file mode 100644 index 0000000..ac79a2c --- /dev/null +++ b/database/migrations/2024_12_21_021157_tags.php @@ -0,0 +1,29 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('tags', function (Blueprint $table) { + $table->id(); + $table->timestamps(); + $table->string('label')->required()->unique(); + $table->integer('refs')->default(0); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tags'); + } +}; diff --git a/database/migrations/2024_12_21_021206_quests.php b/database/migrations/2024_12_21_021206_quests.php new file mode 100644 index 0000000..6cfb763 --- /dev/null +++ b/database/migrations/2024_12_21_021206_quests.php @@ -0,0 +1,27 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('quests', function (Blueprint $table) { + $table->id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('quests'); + } +}; diff --git a/database/migrations/2024_12_21_021215_links.php b/database/migrations/2024_12_21_021215_links.php new file mode 100644 index 0000000..51b2cbe --- /dev/null +++ b/database/migrations/2024_12_21_021215_links.php @@ -0,0 +1,27 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('links', function (Blueprint $table) { + $table->id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('links'); + } +}; diff --git a/database/migrations/2024_12_21_032114_pivots.php b/database/migrations/2024_12_21_032114_pivots.php new file mode 100644 index 0000000..df65191 --- /dev/null +++ b/database/migrations/2024_12_21_032114_pivots.php @@ -0,0 +1,44 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +//the pivot tables for many-to-many +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('link_tag', function (Blueprint $table) { + $table->integer('link_id')->unsigned()->index(); + $table->foreign('link_id')->references('id')->on('links')->onDelete('cascade'); + $table->integer('tag_id')->unsigned()->index(); + $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); + }); + Schema::create('file_tag', function (Blueprint $table) { + $table->integer('file_id')->unsigned()->index(); + $table->foreign('file_id')->references('id')->on('files')->onDelete('cascade'); + $table->integer('tag_id')->unsigned()->index(); + $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); + }); + Schema::create('quest_tag', function (Blueprint $table) { + $table->integer('quest_id')->unsigned()->index(); + $table->foreign('quest_id')->references('id')->on('quests')->onDelete('cascade'); + $table->integer('tag_id')->unsigned()->index(); + $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('link_tag'); + Schema::dropIfExists('file_tag'); + Schema::dropIfExists('quest_tag'); + } +}; diff --git a/resources/css/reset.css b/resources/css/reset.css new file mode 100644 index 0000000..905b080 --- /dev/null +++ b/resources/css/reset.css @@ -0,0 +1,40 @@ +/* Reset CSS */ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +}
\ No newline at end of file diff --git a/resources/css/style.css b/resources/css/style.css new file mode 100644 index 0000000..f92d792 --- /dev/null +++ b/resources/css/style.css @@ -0,0 +1,104 @@ +#bg { + position: fixed; + top: 0; + left: 0; + z-index: -1; +} + +body * { + font-family: 'Roboto', sans-serif; + color: #1b1b1b; +} + +main { + position: relative; + z-index: 1; + width: 100%; + height: 100%; + overflow-y: auto; + margin: 0px auto; + padding: 80px 12px; + display: block; + max-width: 800px; + justify-content: left; +} + +section { + padding: 1.5rem; + justify-content: left; + margin: 36px 24px; + box-shadow: 0 0 10px rgba(0,0,0,0.1); + min-width: 200px; +} + +section * { + padding: 2px; + margin: 2px; +} + +h4 { + padding: 2px; +} + +header { + padding: 8px 12px 12px; + background-color: #cdcdcd33; + + box-shadow: 0 0 10px rgba(0,0,0,0.1); + min-width: 200px; +} + +li a { + color: #2e3f25; + text-decoration: none; + word-break: break-all; +} + +#links .section { + margin: 1rem 0; + padding: 1rem; +} + +ul { + list-style: none; + padding: 0; + margin: 0; +} + +#links li { + margin-bottom: .5rem; + padding-bottom: .5rem; + border-bottom: 1px solid #3e3e3e; +} + +/* Remove border from last item */ +#links li:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +/* Link titles */ +#links li strong { + display: block; + margin-bottom: 0.5rem; + color: #2f2f2f; +} + +/* Actual links */ +#links li a { + color: #2e3f25; + text-decoration: none; + word-break: break-all; +} + +#links li a:hover { + text-decoration: underline; +} + +/* Description text */ +#links li p { + margin: 0.5rem 0 0 .1f; + color: #444; + font-size: 0.9em; +}
\ No newline at end of file diff --git a/resources/js/main.js b/resources/js/main.js new file mode 100644 index 0000000..14a64dc --- /dev/null +++ b/resources/js/main.js @@ -0,0 +1,302 @@ +import { io } from './socket.io.esm.min.js'; +import { drawScene } from './drawer.js'; + +const CHUNK_SIZE = 4 * 1024 * 1024; // 4MB chunks +const FILESIZE_THRESHOLD = 16 * 1024 * 1024; // 16MB // how big before use chunked upload +const MAXFILESIZE = 2048 * 1024 * 1024; // 2GB +const API_URL = 'http://192.168.4.32:9002'; +//const WS_URL = 'ws://localhost:80'; //10.50.231.35:80'; // belthelziquor.com:443'; //TODO get from cfg or env var +const WS_URL = 'wss://belthelziquor.com:443'; +//const API_URL = 'http://localhost:9002'; +const color_bg = "#dedede"; +//TODO change color based on time + +var ctx = null; +var cnv = null; +var W = 800; +var H = 600; +var socket = null; +var myID; + +var elemNumConnected; + +//each connected client. same format as server's model +var friends = { + +}; + +var me = [0,0]; //my cursor position +var myNickname = ''; + +var domElems = {}; + +function prepareFileInput(){ + var file_input = domElems.inputFile; + var files = file_input.files; + for (f in files) { + console.log(f); + } + //TODO validation + domElems.buttonFileUpload.enabled = true; +} + +//APP START HERE +$(document).ready(function() { + cnv = $('#bg')[0]; + if (cnv != null) { + getServerEnvVars(); + initDOM(); + + ctx = cnv.getContext("2d"); + W = window.outerWidth; + H = window.outerHeight; + cnv.width = W; + cnv.height = H; + ctx.fillStyle = color_bg; + ctx.fillRect(0,0,W,H); + + if (connectWebSocket()){ + setInterval(() => { + socket.emit('update_pos', {pos: me, nick: myNickname}); + }, 100); + } else { + console.error('Failed to connect web sockets'); + domElems.debugInfo.innerHTML = 'Unable to connect to web socket server'; + } + document.onmousemove = (e) => handleMouseMove(e); + setInterval(() => { + draw(); + }, 20); + window.addEventListener('resize', resizeCanvas); + } else { + console.error('Canvas element not found'); + } + +}); +/* +var canvas = document.getElementById('canvas');// $('#canvas')[0]; +console.log(canvas); +var context = canvas.getContext("2d"); +*/ + + + +function getServerEnvVars(){ + axios.get('/env').then((res)=>{ + console.log(res);//TODO set api url from this + }); +} + +//initial setup such as hiding/showing certain things +function initDOM(){ + $('#fileupload')[0].hidden = false; + domElems.numConnected = $('#numFriendsConnected')[0]; + domElems.inputFile = $('#f')[0]; + domElems.debugInfo = $('#debugInfo')[0]; + domElems.fileUploadResult = $('#fileupload_result')[0]; + domElems.inputFile.onchange = prepareFileInput; + domElems.buttonFileUpload = $('#button_fileupload')[0]; + //domElems.buttonFileUpload.enabled = false; + domElems.buttonFileUpload.onclick = uploadFile; + domElems.inputWho = $('#input_who')[0]; + domElems.inputWho.onchange = ()=> { myNickname = domElems.inputWho.value; }; +} + +function resizeCanvas() { + W = window.outerWidth; + H = window.outerHeight; + cnv.width = W; + cnv.height = H; +} + +//todo move the old code from file stuff etc to other file so that this file can be just for the canvas+graphics+networkedgame part of the client app +async function uploadFile() { + var files = domElems.inputFile.files; + domElems.buttonFileUpload.enabled = false; + let formData = new FormData(); + console.log(files); + for (let i = 0; i < files.length; i++) { + formData.append('f[]', files[i], files[i].name); + } + + axios.post('/f', formData, {headers: {'Content-Type': 'multipart/form-data'}}) + .then((res)=>{ + console.log(res); + domElems.fileUploadResult.innerHTML = 'File uploaded successfully<br>'; + res.data.files.forEach((f)=>{ + domElems.fileUploadResult.innerHTML += `<a href = "f/${f.filename}">${f.filename}</a><br>`; + }); + domElems.inputFile.value = null; + domElems.buttonFileUpload.enabled = true; + }) + .catch((err)=>{ + console.log(err); + domElems.fileUploadResult.innerHTML = 'Error uploading file'; + domElems.buttonFileUpload.enabled = true; + }); +} + +async function uploadFileChunked(f){ + const totalChunks = Math.ceil(file.size / CHUNK_SIZE); + const filename = file.name; + + for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) { + const chunk = file.slice(chunkNumber * CHUNK_SIZE, (chunkNumber + 1) * CHUNK_SIZE); + const formData = new FormData(); + formData.append('file', chunk); + formData.append('filename', filename); + formData.append('chunkNumber', chunkNumber); + formData.append('totalChunks', totalChunks); + + try { + const response = await fetch(`${API_URL}/uploadfiles`, { + method: 'POST', + body: formData + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + updateProgress(chunkNumber + 1, totalChunks); + + } catch (error) { + console.error('Error uploading chunk:', error); + alert('Error uploading file. Please try again.'); + return; + } + } +} + +async function uploadFileSingle(f){ + const formData = new FormData(); + formData.append('f', f); + const res = await fetch(`${API_URL}/uploadfiles`, { + method: 'POST', + body: formData, + credentials: 'include' + }); + console.log(res); +} + +function updateProgress(currentChunk, totalChunks) { + const progressPercentage = Math.round((currentChunk / totalChunks) * 100); + document.getElementById('progress').textContent = `${progressPercentage}% uploaded`; +} + + +function connectWebSocket(){ + try { + socket = io(WS_URL, { + secure: true, + rejectUnauthorized: false + }); + socket.on('connect_error', (err) => { + console.log('connection error'); + console.log(err); + //TODO $('').textContent = err; + }); + socket.on('connect_failed', (err) => { + console.log('conn failed'); + console.log(err); + //TODO $('').textContent = err; + }); + socket.on('disconnect', (err) => { + console.log('disconnected'); + console.log(err); + //TODO $('').textContent = err; + }); + socket.on('connect', (err) => { + console.log('connected'); + if (err) console.log(err); + //TODO $('').textContent = 'Connected'; + socket.pingTimeout = 1000; + socket.pingInterval = 500; + }); + socket.on('syncCanvas', (data) => { + //TODO appShapes = data.shapes; + //TODO ClearBake(); + //TODO DrawCanvas(); + //TODO $('').textContent = 'Synced'; + }); + socket.on('init', (id) => { + myID = id; + }); + + //sync data from server. this is the data from server that should be replicated on each client + socket.on('sync_data', (data) => { + //data is a map of client id to client payload data (currently just the screen position) + friends = {}; + for (const cid in data){ + if (cid == myID){ + continue; + } + friends[cid] = data[cid]; + } + domElems.numConnected.innerHTML = Object.keys(friends).length + 1; + }); + return true; + } catch (err) { + console.error(err); + return false; + } +} + + +function draw() { + //draw background + ctx.fillStyle = color_bg; + ctx.fillRect(0,0,W,H); + + //draw me + ctx.fillStyle = "#000000"; + ctx.fillRect(me[0], me[1], 4, 4); + if (myNickname != ''){ + ctx.fillText(myNickname, me[0], me[1]); + } + + //draw friends + Object.keys(friends).forEach((fid)=>{ + ctx.fillStyle = "#050505"; + let x = friends[fid].pos[0]; + let y =friends[fid].pos[1]; + ctx.fillRect(x, y, 6, 6); + if (friends[fid].nick != ''){ + ctx.fillText(friends[fid].nick, x, y); + } + }); + + drawScene(ctx); + +} + +function getMousePos(canvas, evt) { + var rect = canvas.getBoundingClientRect(); + /*return { + x: evt.clientX - rect.left, + y: evt.clientY - rect.top + };*/ + return [evt.clientX - rect.left, evt.clientY - rect.top]; +} + + +function handleMouseMove(e) { + var eDoc, doc, body; + e = e || window.event; // IE-ism + // If pageX/Y aren't available and clientX/Y are, + // calculate pageX/Y - logic taken from jQuery. + // (This is to support old IE) + if (e.pageX == null && e.clientX != null) { + eventDoc = (e.target && e.target.ownerDocument) || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + e.pageX = e.clientX + + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - + (doc && doc.clientLeft || body && body.clientLeft || 0); + e.pageY = e.clientY + + (doc && doc.scrollTop || body && body.scrollTop || 0) - + (doc && doc.clientTop || body && body.clientTop || 0 ); + } + me = getMousePos(cnv, e); +}
\ No newline at end of file diff --git a/resources/js/marked.js b/resources/js/marked.js new file mode 100644 index 0000000..f193c5a --- /dev/null +++ b/resources/js/marked.js @@ -0,0 +1,38 @@ +let mainContent; +let inputText; +let htmlResult; //the resulting html to display the inputted markup text +let buttonSave; + +$(function() { + inputText = $('#input_text')[0]; + mainContent = $('main')[0]; + buttonSave = $('#button_save')[0]; + + inputText.oninput = ()=>{ + htmlResult = marked.parse(inputText.value); + mainContent.innerHTML = htmlResult; + }; + + buttonSave.onclick = async ()=>{ + //save to local file + console.log(htmlResult); + + downloadFile(htmlResult, 'markdown-doc.html', 'text/plain'); + + }; + + +}); + +function downloadFile(content, fileName, mimeType) { + const blob = new Blob([content], {type: mimeType}); + + const url = window.URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + a.click(); + + window.URL.revokeObjectURL(url); +}
\ No newline at end of file diff --git a/resources/js/webgpu.js b/resources/js/webgpu.js new file mode 100644 index 0000000..b66f9ab --- /dev/null +++ b/resources/js/webgpu.js @@ -0,0 +1,253 @@ +//import { mat4, vec3 } from './gl-matrix/dist/gl-matrix.js'; + +//import { formToJSON } from "axios"; + + +var ctx = null; +var cnv = null; +var wgc = null; +var W = 800; +var H = 600; + +const vertexShaderWGSL = ` +struct Uniforms { + modelViewProjectionMatrix : mat4x4<f32>, +} +@binding(0) @group(0) var<uniform> uniforms : Uniforms; + +struct VertexOutput { + @builtin(position) Position : vec4<f32>, + @location(0) color : vec4<f32>, +} + +@vertex +fn main( + @location(0) position : vec4<f32>, + @location(1) color : vec4<f32> +) -> VertexOutput { + var output : VertexOutput; + output.Position = uniforms.modelViewProjectionMatrix * position; + output.color = color; + return output; +} +`; + +const fragmentShaderWGSL = ` +@fragment +fn main(@location(0) color : vec4<f32>) -> @location(0) vec4<f32> { + return color; +} +`; + +$(document).ready(function() { + cnv = $('#c')[0]; + if (cnv != null) { + //initDOM(); + //ctx = cnv.getContext("2d"); + W = window.outerWidth; + H = window.outerHeight; + cnv.width = W; + cnv.height = H; + //ctx.fillRect(0,0,W,H); + + window.addEventListener('resize', resizeCanvas); + doWebGPUStuff(); + } else { + console.error('Canvas element not found'); + } +}); + +function resizeCanvas() { + W = window.outerWidth; + H = window.outerHeight; + cnv.width = W; + cnv.height = H; +} + +async function doWebGPUStuff(){ + console.log(navigator); + if (!navigator.gpu) { + console.error("WebGPU not supported on this browser."); + return; + } + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) { + console.error("No appropriate GPUAdapter found."); + return; + } + console.log(adapter); + + const device = await adapter.requestDevice(); + console.log(device); + + wgc = cnv.getContext("webgpu"); + console.log(wgc); + const cnvFormat = navigator.gpu.getPreferredCanvasFormat(); + wgc.configure({ + device: device, + format: cnvFormat + }); + + const vertices = new Float32Array([ + // Front face + -0.5, -0.5, 0.5, 1.0, 0.0, 0.0, 1.0, + 0.5, -0.5, 0.5, 1.0, 0.0, 0.0, 1.0, + 0.5, 0.5, 0.5, 1.0, 0.0, 0.0, 1.0, + -0.5, 0.5, 0.5, 1.0, 0.0, 0.0, 1.0, + // Back face + -0.5, -0.5, -0.5, 0.0, 1.0, 0.0, 1.0, + 0.5, -0.5, -0.5, 0.0, 1.0, 0.0, 1.0, + 0.5, 0.5, -0.5, 0.0, 1.0, 0.0, 1.0, + -0.5, 0.5, -0.5, 0.0, 1.0, 0.0, 1.0, + ]); + + const indices = new Uint16Array([ + 0, 1, 2, 0, 2, 3, + 1, 5, 6, 1, 6, 2, + 5, 4, 7, 5, 7, 6, + 4, 0, 3, 4, 3, 7, + 3, 2, 6, 3, 6, 7, + 4, 5, 1, 4, 1, 0 + ]); + + const vertexBuffer = device.createBuffer({ + size: vertices.byteLength, + usage: GPUBufferUsage.VERTEX, + mappedAtCreation: true, + }); + new Float32Array(vertexBuffer.getMappedRange()).set(vertices); + vertexBuffer.unmap(); + + const indexBuffer = device.createBuffer({ + size: indices.byteLength, + usage: GPUBufferUsage.INDEX, + mappedAtCreation: true, + }); + new Uint16Array(indexBuffer.getMappedRange()).set(indices); + indexBuffer.unmap(); + + const uniformBuffer = device.createBuffer({ + size: 16 * 4, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: device.createShaderModule({ + code: vertexShaderWGSL, + }), + entryPoint: 'main', + buffers: [ + { + arrayStride: 7 * 4, + attributes: [ + { shaderLocation: 0, offset: 0, format: 'float32x3' }, + { shaderLocation: 1, offset: 3 * 4, format: 'float32x4' }, + ], + }, + ], + }, + fragment: { + module: device.createShaderModule({ + code: fragmentShaderWGSL, + }), + entryPoint: 'main', + targets: [ + { + format: cnvFormat, + }, + ], + }, + primitive: { + topology: 'triangle-list', + }, + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + format: 'depth24plus', + }, + }); + + const depthTexture = device.createTexture({ + size: [cnv.width, cnv.height], + format: 'depth24plus', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const uniformBindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { + buffer: uniformBuffer, + }, + }, + ], + }); + + render(device); +} + +function render (device) { + const aspect = cnv.width / cnv.height; + const projectionMatrix = mat4.create(); + mat4.perspective(projectionMatrix, (2 * Math.PI) / 5, aspect, 1, 100.0); + + const viewMatrix = mat4.create(); + const eye = vec3.fromValues(0, 0, 4); + const center = vec3.fromValues(0, 0, 0); + const up = vec3.fromValues(0, 1, 0); + mat4.lookAt(viewMatrix, eye, center, up); + + const modelViewProjectionMatrix = mat4.create(); + const modelMatrix = mat4.create(); + mat4.translate(modelMatrix, modelMatrix, [ + (200 /*cursorPosition.x*/ / cnv.width) * 2 - 1, + -(( 200 /*cursorPosition.y*/ / cnv.height) * 2 - 1), + 0 + ]); + mat4.rotate(modelMatrix, modelMatrix, Date.now() * 0.001, vec3.fromValues(0, 1, 0)); + + mat4.multiply(modelViewProjectionMatrix, viewMatrix, modelMatrix); + mat4.multiply(modelViewProjectionMatrix, projectionMatrix, modelViewProjectionMatrix); + + device.queue.writeBuffer( + uniformBuffer, + 0, + modelViewProjectionMatrix.buffer, + modelViewProjectionMatrix.byteOffset, + modelViewProjectionMatrix.byteLength + ); + + const commandEncoder = device.createCommandEncoder(); + const textureView = wgc.getCurrentTexture().createView(); + + const renderPassDescriptor = { + colorAttachments: [ + { + view: textureView, + clearValue: { r: 0.1, g: 0.2, b: 0.3, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: depthTexture.createView(), + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }, + }; + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(pipeline); + passEncoder.setBindGroup(0, uniformBindGroup); + passEncoder.setVertexBuffer(0, vertexBuffer); + passEncoder.setIndexBuffer(indexBuffer, 'uint16'); + passEncoder.drawIndexed(36); + passEncoder.end(); + + device.queue.submit([commandEncoder.finish()]); + requestAnimationFrame(render); +} diff --git a/resources/js/webgpu_gltf.js b/resources/js/webgpu_gltf.js new file mode 100644 index 0000000..28e9742 --- /dev/null +++ b/resources/js/webgpu_gltf.js @@ -0,0 +1,514 @@ +//import { mat4, vec3 } from './gl-matrix/dist/gl-matrix.js'; + +//import { formToJSON } from "axios"; + + +var ctx = null; +var cnv = null; +var wgc = null; +var W = 800; +var H = 600; + +const shaderCode = ` +struct Camera { + viewProjectionMatrix: mat4x4f, + cameraPosition: vec4f, +} + +struct Model { + modelMatrix: mat4x4f, +} + +@group(0) @binding(0) var<uniform> camera: Camera; +@group(0) @binding(1) var<uniform> model: Model; + +struct VertexInput { + @location(0) position: vec3f, + @location(1) normal: vec3f, + @location(2) uv: vec2f, +} + +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) worldPos: vec3f, + @location(1) normal: vec3f, + @location(2) uv: vec2f, +} + +@vertex +fn vertexMain(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + let worldPosition = model.modelMatrix * vec4f(input.position, 1.0); + output.position = camera.viewProjectionMatrix * worldPosition; + output.worldPos = worldPosition.xyz; + output.normal = normalize((model.modelMatrix * vec4f(input.normal, 0.0)).xyz); + output.uv = input.uv; + return output; +} + +@fragment +fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { + let lightDir = normalize(vec3f(1.0, 1.0, 1.0)); + let normal = normalize(input.normal); + let diffuse = max(dot(normal, lightDir), 0.0); + let ambient = 0.1; + let color = vec3f(0.7, 0.7, 0.7); + return vec4f(color * (diffuse + ambient), 1.0); +}`; + +class GLTFRenderer { + constructor(canvas) { + this.canvas = canvas; + this.rotation = 0; + } + + async initialize() { + if (!navigator.gpu) { + throw new Error('WebGPU not supported'); + } + + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) { + throw new Error('No GPU adapter found'); + } + + this.device = await adapter.requestDevice(); + this.context = this.canvas.getContext('webgpu'); + + const format = navigator.gpu.getPreferredCanvasFormat(); + this.context.configure({ + device: this.device, + format: format, + alphaMode: 'premultiplied', + }); + + // Create pipeline + this.pipeline = this.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: this.device.createShaderModule({ + code: shaderCode + }), + entryPoint: 'vertexMain', + buffers: [ + { + // position + arrayStride: 12, + attributes: [{ + shaderLocation: 0, + offset: 0, + format: 'float32x3' + }] + }, + { + // normal + arrayStride: 12, + attributes: [{ + shaderLocation: 1, + offset: 0, + format: 'float32x3' + }] + }, + { + // uv + arrayStride: 8, + attributes: [{ + shaderLocation: 2, + offset: 0, + format: 'float32x2' + }] + } + ] + }, + fragment: { + module: this.device.createShaderModule({ + code: shaderCode + }), + entryPoint: 'fragmentMain', + targets: [{ + format: format + }] + }, + primitive: { + topology: 'triangle-list', + cullMode: 'back' + }, + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + format: 'depth24plus' + } + }); + + // Create depth texture + const depthTexture = this.device.createTexture({ + size: [this.canvas.width, this.canvas.height], + format: 'depth24plus', + usage: GPUTextureUsage.RENDER_ATTACHMENT + }); + + this.depthView = depthTexture.createView(); + } + + async loadGLTF(url) { + const response = await fetch(url); + const gltfData = await response.json(); + + // For this example, we'll assume the first mesh, first primitive + const primitive = gltfData.meshes[0].primitives[0]; + + // Load buffers + const bufferResponse = await fetch(url.replace('gltf', 'bin')); + const bufferData = await bufferResponse.arrayBuffer(); + + // Process vertices + const positionAccessor = gltfData.accessors[primitive.attributes.POSITION]; + const positionView = gltfData.bufferViews[positionAccessor.bufferView]; + const positionData = new Float32Array(bufferData, positionView.byteOffset, positionAccessor.count * 3); + + // Create vertex buffer + this.vertexBuffer = this.device.createBuffer({ + size: positionData.byteLength, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, + }); + this.device.queue.writeBuffer(this.vertexBuffer, 0, positionData); + + // Process indices + const indexAccessor = gltfData.accessors[primitive.indices]; + const indexView = gltfData.bufferViews[indexAccessor.bufferView]; + const indexData = new Uint16Array(bufferData, indexView.byteOffset, indexAccessor.count); + + // Create index buffer + this.indexBuffer = this.device.createBuffer({ + size: indexData.byteLength, + usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST, + }); + this.device.queue.writeBuffer(this.indexBuffer, 0, indexData); + + this.indexCount = indexAccessor.count; + } + + createMatrix4(values) { + return new Float32Array(values); + } + + async render() { + // Update rotation + this.rotation += 0.01; + + // Create uniform buffers for camera and model + const cameraUniformBuffer = this.device.createBuffer({ + size: 64 + 16, // mat4 + vec4 + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + const modelUniformBuffer = this.device.createBuffer({ + size: 64, // mat4 + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + // Create view-projection matrix + const aspect = this.canvas.width / this.canvas.height; + const projectionMatrix = this.createMatrix4([ + 1 / (aspect * Math.tan(Math.PI / 4)), 0, 0, 0, + 0, 1 / Math.tan(Math.PI / 4), 0, 0, + 0, 0, -1, -1, + 0, 0, -0.1, 0 + ]); + + const viewMatrix = this.createMatrix4([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, -5, 1 + ]); + + // Create model matrix with rotation + const modelMatrix = this.createMatrix4([ + Math.cos(this.rotation), 0, Math.sin(this.rotation), 0, + 0, 1, 0, 0, + -Math.sin(this.rotation), 0, Math.cos(this.rotation), 0, + 0, 0, 0, 1 + ]); + + // Write uniform data + this.device.queue.writeBuffer(cameraUniformBuffer, 0, projectionMatrix); + this.device.queue.writeBuffer(cameraUniformBuffer, 64, new Float32Array([0, 0, -5, 1])); + this.device.queue.writeBuffer(modelUniformBuffer, 0, modelMatrix); + + // Create bind group + const bindGroup = this.device.createBindGroup({ + layout: this.pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { buffer: cameraUniformBuffer } + }, + { + binding: 1, + resource: { buffer: modelUniformBuffer } + } + ] + }); + + // Create command encoder + const commandEncoder = this.device.createCommandEncoder(); + const renderPass = commandEncoder.beginRenderPass({ + colorAttachments: [{ + view: this.context.getCurrentTexture().createView(), + clearValue: { r: 0.1, g: 0.1, b: 0.1, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store' + }], + depthStencilAttachment: { + view: this.depthView, + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + } + }); + + renderPass.setPipeline(this.pipeline); + renderPass.setBindGroup(0, bindGroup); + renderPass.setVertexBuffer(0, this.vertexBuffer); + renderPass.setIndexBuffer(this.indexBuffer, 'uint16'); + renderPass.drawIndexed(this.indexCount); + renderPass.end(); + + // Submit commands + this.device.queue.submit([commandEncoder.finish()]); + } + + async start() { + const render = () => { + this.render(); + requestAnimationFrame(render); + }; + render(); + } +} + + +$(document).ready(async function() { + cnv = $('#c')[0]; + if (cnv != null) { + //initDOM(); + //ctx = cnv.getContext("2d"); + W = window.outerWidth; + H = window.outerHeight; + cnv.width = W; + cnv.height = H; + //ctx.fillRect(0,0,W,H); + + window.addEventListener('resize', resizeCanvas); + try { + const renderer = new GLTFRenderer(cnv); + await renderer.initialize(); + await renderer.loadGLTF('cube.gltf'); // Make sure to have your GLTF file available + renderer.start(); + } catch (error) { + const errorDiv = document.getElementById('error'); + errorDiv.style.display = 'block'; + errorDiv.textContent = 'Error: ' + error.message; + console.error(error); + } + doWebGPUStuff(); + } else { + console.error('Canvas element not found'); + } +}); + +function resizeCanvas() { + W = window.outerWidth; + H = window.outerHeight; + cnv.width = W; + cnv.height = H; +} + +async function doWebGPUStuff(){ + console.log(navigator); + if (!navigator.gpu) { + console.error("WebGPU not supported on this browser."); + return; + } + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) { + console.error("No appropriate GPUAdapter found."); + return; + } + console.log(adapter); + + const device = await adapter.requestDevice(); + console.log(device); + + wgc = cnv.getContext("webgpu"); + console.log(wgc); + const cnvFormat = navigator.gpu.getPreferredCanvasFormat(); + wgc.configure({ + device: device, + format: cnvFormat + }); + + const vertices = new Float32Array([ + // Front face + -0.5, -0.5, 0.5, 1.0, 0.0, 0.0, 1.0, + 0.5, -0.5, 0.5, 1.0, 0.0, 0.0, 1.0, + 0.5, 0.5, 0.5, 1.0, 0.0, 0.0, 1.0, + -0.5, 0.5, 0.5, 1.0, 0.0, 0.0, 1.0, + // Back face + -0.5, -0.5, -0.5, 0.0, 1.0, 0.0, 1.0, + 0.5, -0.5, -0.5, 0.0, 1.0, 0.0, 1.0, + 0.5, 0.5, -0.5, 0.0, 1.0, 0.0, 1.0, + -0.5, 0.5, -0.5, 0.0, 1.0, 0.0, 1.0, + ]); + + const indices = new Uint16Array([ + 0, 1, 2, 0, 2, 3, + 1, 5, 6, 1, 6, 2, + 5, 4, 7, 5, 7, 6, + 4, 0, 3, 4, 3, 7, + 3, 2, 6, 3, 6, 7, + 4, 5, 1, 4, 1, 0 + ]); + + const vertexBuffer = device.createBuffer({ + size: vertices.byteLength, + usage: GPUBufferUsage.VERTEX, + mappedAtCreation: true, + }); + new Float32Array(vertexBuffer.getMappedRange()).set(vertices); + vertexBuffer.unmap(); + + const indexBuffer = device.createBuffer({ + size: indices.byteLength, + usage: GPUBufferUsage.INDEX, + mappedAtCreation: true, + }); + new Uint16Array(indexBuffer.getMappedRange()).set(indices); + indexBuffer.unmap(); + + const uniformBuffer = device.createBuffer({ + size: 16 * 4, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: device.createShaderModule({ + code: vertexShaderWGSL, + }), + entryPoint: 'main', + buffers: [ + { + arrayStride: 7 * 4, + attributes: [ + { shaderLocation: 0, offset: 0, format: 'float32x3' }, + { shaderLocation: 1, offset: 3 * 4, format: 'float32x4' }, + ], + }, + ], + }, + fragment: { + module: device.createShaderModule({ + code: fragmentShaderWGSL, + }), + entryPoint: 'main', + targets: [ + { + format: cnvFormat, + }, + ], + }, + primitive: { + topology: 'triangle-list', + }, + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + format: 'depth24plus', + }, + }); + + const depthTexture = device.createTexture({ + size: [cnv.width, cnv.height], + format: 'depth24plus', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const uniformBindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { + buffer: uniformBuffer, + }, + }, + ], + }); + + render(device); +} + +function render (device) { + const aspect = cnv.width / cnv.height; + const projectionMatrix = mat4.create(); + mat4.perspective(projectionMatrix, (2 * Math.PI) / 5, aspect, 1, 100.0); + + const viewMatrix = mat4.create(); + const eye = vec3.fromValues(0, 0, 4); + const center = vec3.fromValues(0, 0, 0); + const up = vec3.fromValues(0, 1, 0); + mat4.lookAt(viewMatrix, eye, center, up); + + const modelViewProjectionMatrix = mat4.create(); + const modelMatrix = mat4.create(); + mat4.translate(modelMatrix, modelMatrix, [ + (200 /*cursorPosition.x*/ / cnv.width) * 2 - 1, + -(( 200 /*cursorPosition.y*/ / cnv.height) * 2 - 1), + 0 + ]); + mat4.rotate(modelMatrix, modelMatrix, Date.now() * 0.001, vec3.fromValues(0, 1, 0)); + + mat4.multiply(modelViewProjectionMatrix, viewMatrix, modelMatrix); + mat4.multiply(modelViewProjectionMatrix, projectionMatrix, modelViewProjectionMatrix); + + device.queue.writeBuffer( + uniformBuffer, + 0, + modelViewProjectionMatrix.buffer, + modelViewProjectionMatrix.byteOffset, + modelViewProjectionMatrix.byteLength + ); + + const commandEncoder = device.createCommandEncoder(); + const textureView = wgc.getCurrentTexture().createView(); + + const renderPassDescriptor = { + colorAttachments: [ + { + view: textureView, + clearValue: { r: 0.1, g: 0.2, b: 0.3, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: depthTexture.createView(), + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }, + }; + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(pipeline); + passEncoder.setBindGroup(0, uniformBindGroup); + passEncoder.setVertexBuffer(0, vertexBuffer); + passEncoder.setIndexBuffer(indexBuffer, 'uint16'); + passEncoder.drawIndexed(36); + passEncoder.end(); + + device.queue.submit([commandEncoder.finish()]); + requestAnimationFrame(render); +} diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 0a5d12f..c7d6102 100755 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -38,7 +38,6 @@ <input hidden name = "response_format" value = "html" /> <button type = "submit">Upload</button> </form> - <p>For now, you will get a json respone upon upload. Just navigate to /f to access. I might update this to return html if useragent is a browser</p> </section> <!--</noscript>--> <section id = "fileupload" hidden="true"> |
