summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Grothe <grothe.tr@gmail.com>2026-04-12 01:26:43 -0400
committerThomas Grothe <grothe.tr@gmail.com>2026-04-12 01:26:43 -0400
commitdf0f054366a81d02b28a5e2ae0d571cf5b153256 (patch)
tree47c674a6c5809a858fefa917c68b4b8822491944
parentd4f97aa956be051dd5b9a184557106dc7de112ac (diff)
updates, including removing axios, making a presence page, fixed 4chan search, vite asset bundling
-rw-r--r--app/Http/Controllers/FileController.php6
-rw-r--r--app/Http/Controllers/SiteController.php15
-rwxr-xr-xapp/Providers/AppServiceProvider.php11
-rwxr-xr-xpackage.json2
-rw-r--r--public/4chan_querybin0 -> 9247037 bytes
-rw-r--r--public/js/app.js7
-rw-r--r--public/js/writing_create.js70
-rw-r--r--resources/css/app.css (renamed from public/css/app.css)0
-rw-r--r--resources/css/home.css (renamed from public/css/home.css)0
-rw-r--r--resources/css/jstoys.css (renamed from public/css/jstoys.css)0
-rw-r--r--resources/css/normalize.css (renamed from public/css/normalize.css)0
-rw-r--r--resources/css/presence.css (renamed from public/css/presence.css)2
-rw-r--r--resources/css/reset.css (renamed from public/css/reset.css)0
-rw-r--r--resources/css/skeleton.css (renamed from public/css/skeleton.css)0
-rw-r--r--resources/css/style.css (renamed from public/css/style.css)0
-rw-r--r--resources/css/writings.css (renamed from public/css/writings.css)0
-rw-r--r--resources/css/youtube.css198
-rw-r--r--resources/js/app.js9
-rw-r--r--resources/js/blood.js (renamed from public/js/blood.js)0
-rw-r--r--resources/js/blood_gpu.js (renamed from public/js/blood_gpu.js)0
-rw-r--r--resources/js/bootstrap.js (renamed from public/js/bootstrap.js)0
-rw-r--r--resources/js/echo.js (renamed from public/js/echo.js)0
-rw-r--r--resources/js/hn.js (renamed from public/js/hn.js)0
-rw-r--r--resources/js/home.js (renamed from public/js/home.js)0
-rw-r--r--resources/js/lib.js (renamed from public/js/lib.js)0
-rw-r--r--resources/js/main.js (renamed from public/js/main.js)25
-rw-r--r--resources/js/marked.js (renamed from public/js/marked.js)0
-rw-r--r--resources/js/socket.io.esm.min.js (renamed from public/js/socket.io.esm.min.js)0
-rw-r--r--resources/js/webgpu.js (renamed from public/js/webgpu.js)0
-rw-r--r--resources/js/webgpu_gltf.js (renamed from public/js/webgpu_gltf.js)0
-rw-r--r--resources/js/writing_create.js17
-rw-r--r--resources/js/writing_index.js (renamed from public/js/writing_index.js)0
-rw-r--r--resources/js/writing_show.js (renamed from public/js/writing_show.js)0
-rw-r--r--resources/js/youtube.js276
-rw-r--r--resources/js/youtube.js.bak227
-rw-r--r--resources/views/.off/graphics.html (renamed from resources/views/graphics.html)0
-rw-r--r--resources/views/.off/marked.blade.php (renamed from resources/views/marked.blade.php)4
-rwxr-xr-xresources/views/4.blade.php5
-rwxr-xr-xresources/views/home.blade.php5
-rw-r--r--resources/views/presence/index.blade.php2
-rw-r--r--resources/views/presence/show.blade.php1
-rwxr-xr-xresources/views/template.blade.php3
-rw-r--r--resources/views/writings/create.blade.php6
-rw-r--r--resources/views/writings/edit.blade.php5
-rw-r--r--resources/views/writings/index.blade.php2
-rw-r--r--resources/views/writings/show.blade.php4
-rwxr-xr-xroutes/web.php8
-rwxr-xr-xvite.config.js13
48 files changed, 791 insertions, 132 deletions
diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php
index 70d07ef..362abf6 100644
--- a/app/Http/Controllers/FileController.php
+++ b/app/Http/Controllers/FileController.php
@@ -26,7 +26,11 @@ class FileController extends Controller
$currentDir = "${baseDir}/${dir}";
}
- // Get file listings
+ //check if dir exists
+ if (!is_dir($currentDir)) {
+ return 'Directory not found.';
+ }
+
$contents = scandir($currentDir);
$fileData = [];
diff --git a/app/Http/Controllers/SiteController.php b/app/Http/Controllers/SiteController.php
index 33d7dc3..301e101 100644
--- a/app/Http/Controllers/SiteController.php
+++ b/app/Http/Controllers/SiteController.php
@@ -105,13 +105,18 @@ class SiteController extends Controller
}
public function search4chan(Request $req){
- $query = $req->input('query');
Log::info('search4chan()');
-
- $cmd = 'python ./4chansearch.py ' . escapeshellarg($query);
- if ($req->input('board') != null) {
- $cmd .= ' -b ' . escapeshellarg($req->input('board'));
+ $validated = $req->validate([
+ 'query' => 'required|string',
+ 'board' => 'nullable|string'
+ ]);
+
+ $cmd = './4chan_query ';
+ Log::info($cmd);
+ if (isset($validated['board'])) {
+ $cmd .= ' -b ' . escapeshellarg($validated['board']);
}
+ $cmd .= ' ' . escapeshellarg($validated['query']);
exec($cmd, $res, $ret);
return $res;
}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 8eee884..9d78efd 100755
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -3,6 +3,7 @@
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
+use Illuminate\Support\Facades\Blade;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;
@@ -22,6 +23,16 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
+ Blade::directive('v', function ($expression) {
+ $expr = trim($expression);
+ if (str_starts_with($expr, '[')) {
+ // Array: prepend 'resources/' to each element
+ return "<?php echo app(\Illuminate\Foundation\Vite::class)(array_map(fn(\$p) => 'resources/' . \$p, {$expr})); ?>";
+ }
+ $path = trim($expr, "'\"");
+ return "<?php echo app(\Illuminate\Foundation\Vite::class)(\"resources/{$path}\"); ?>";
+ });
+
// Public routes: 60 requests per minute per IP
RateLimiter::for('public', function (Request $request) {
return Limit::perMinute(60)->by($request->ip());
diff --git a/package.json b/package.json
index e280a32..1132cc3 100755
--- a/package.json
+++ b/package.json
@@ -18,6 +18,8 @@
"vite": "^5.0"
},
"dependencies": {
+ "baseline-browser-mapping": "^2.10.18",
+ "caniuse-lite": "^1.0.30001787",
"htmx.org": "^2.0.1",
"jquery": "^3.7.1",
"marked": "^17.0.4"
diff --git a/public/4chan_query b/public/4chan_query
new file mode 100644
index 0000000..ecb9b62
--- /dev/null
+++ b/public/4chan_query
Binary files differ
diff --git a/public/js/app.js b/public/js/app.js
deleted file mode 100644
index a8093be..0000000
--- a/public/js/app.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import './bootstrap';
-
-import Alpine from 'alpinejs';
-
-window.Alpine = Alpine;
-
-Alpine.start();
diff --git a/public/js/writing_create.js b/public/js/writing_create.js
deleted file mode 100644
index dc737f8..0000000
--- a/public/js/writing_create.js
+++ /dev/null
@@ -1,70 +0,0 @@
-let dom = {};
-let editorHistory = [];
-
-document.addEventListener('DOMContentLoaded', () => {
- if (typeof marked === 'undefined') {
- console.error("marked lib not loaded");
- return;
- }
-
- initDOM();
-
- marked.use({ breaks: true });
-
- dom.inputText.addEventListener('input', () => {
- dom.contentPreview.innerHTML = marked.parse(dom.inputText.value);
- });
-
- // Restore autosaved draft
- const restoredContent = localStorage.getItem('writing_draft_content');
- if (restoredContent !== null) {
- const expiration = parseInt(localStorage.getItem('writing_draft_expiration') || '0');
- if (expiration < Date.now()) {
- localStorage.removeItem('writing_draft_content');
- localStorage.removeItem('writing_draft_expiration');
- } else {
- dom.inputText.value = restoredContent;
- dom.contentPreview.innerHTML = marked.parse(dom.inputText.value);
- }
- }
-
- // Periodically save contents to localStorage to prevent data loss
- setInterval(() => {
- localStorage.setItem('writing_draft_content', dom.inputText.value);
- localStorage.setItem('writing_draft_expiration', Date.now() + 1000 * 3600 * 96);
- }, 5000);
-
- // Clear draft on successful submit
- document.getElementById('writing-form')?.addEventListener('submit', () => {
- localStorage.removeItem('writing_draft_content');
- localStorage.removeItem('writing_draft_expiration');
- });
-});
-
-function initDOM() {
- dom.inputText = document.getElementById('input_content');
- dom.contentPreview = document.getElementById('content_preview');
- dom.findStr = document.getElementById('input_findstr');
- dom.replaceStr = document.getElementById('input_replacestr');
- dom.btnReplace = document.getElementById('button_replace');
- dom.btnUndo = document.getElementById('button_undo');
- dom.toggleMultiline = document.getElementById('input_multiline');
-
- dom.btnReplace.addEventListener('click', performReplace);
- dom.btnUndo.addEventListener('click', () => {
- if (editorHistory.length > 0) {
- dom.inputText.value = editorHistory.pop();
- dom.contentPreview.innerHTML = marked.parse(dom.inputText.value);
- }
- });
-}
-
-function performReplace() {
- // Save current state for undo
- editorHistory.push(dom.inputText.value);
-
- const flags = dom.toggleMultiline.checked ? 'gm' : 'g';
- const regex = new RegExp(dom.findStr.value, flags);
- dom.inputText.value = dom.inputText.value.replace(regex, dom.replaceStr.value);
- dom.contentPreview.innerHTML = marked.parse(dom.inputText.value);
-}; \ No newline at end of file
diff --git a/public/css/app.css b/resources/css/app.css
index b5c61c9..b5c61c9 100644
--- a/public/css/app.css
+++ b/resources/css/app.css
diff --git a/public/css/home.css b/resources/css/home.css
index ebbbb29..ebbbb29 100644
--- a/public/css/home.css
+++ b/resources/css/home.css
diff --git a/public/css/jstoys.css b/resources/css/jstoys.css
index add19e2..add19e2 100644
--- a/public/css/jstoys.css
+++ b/resources/css/jstoys.css
diff --git a/public/css/normalize.css b/resources/css/normalize.css
index 81c6f31..81c6f31 100644
--- a/public/css/normalize.css
+++ b/resources/css/normalize.css
diff --git a/public/css/presence.css b/resources/css/presence.css
index 40a2d8f..c8da547 100644
--- a/public/css/presence.css
+++ b/resources/css/presence.css
@@ -327,4 +327,4 @@
background: #2563eb;
color: #fff;
border-color: #2563eb;
-}
+} \ No newline at end of file
diff --git a/public/css/reset.css b/resources/css/reset.css
index 905b080..905b080 100644
--- a/public/css/reset.css
+++ b/resources/css/reset.css
diff --git a/public/css/skeleton.css b/resources/css/skeleton.css
index c3fda1c..c3fda1c 100644
--- a/public/css/skeleton.css
+++ b/resources/css/skeleton.css
diff --git a/public/css/style.css b/resources/css/style.css
index a5165fb..a5165fb 100644
--- a/public/css/style.css
+++ b/resources/css/style.css
diff --git a/public/css/writings.css b/resources/css/writings.css
index 73f89fa..73f89fa 100644
--- a/public/css/writings.css
+++ b/resources/css/writings.css
diff --git a/resources/css/youtube.css b/resources/css/youtube.css
new file mode 100644
index 0000000..9fed91e
--- /dev/null
+++ b/resources/css/youtube.css
@@ -0,0 +1,198 @@
+/* YouTube Player Styles */
+
+.input-section {
+ background: #272727;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+}
+
+.input-group {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 15px;
+}
+
+#videoUrl {
+ flex: 1;
+ padding: 12px;
+ background: #121212;
+ border: 1px solid #3f3f3f;
+ border-radius: 4px;
+ color: #f1f1f1;
+ font-size: 14px;
+}
+
+#videoUrl:focus {
+ outline: none;
+ border-color: #3ea6ff;
+}
+
+#loadBtn {
+ padding: 12px 24px;
+ background: #3ea6ff;
+ color: #0f0f0f;
+ border: none;
+ border-radius: 4px;
+ font-weight: 600;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background 0.2s;
+}
+
+#loadBtn:hover {
+ background: #5eb3ff;
+}
+
+#loadBtn:disabled {
+ background: #3f3f3f;
+ cursor: not-allowed;
+}
+
+.format-selector {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.format-btn {
+ padding: 8px 16px;
+ background: #3f3f3f;
+ color: #f1f1f1;
+ border: 2px solid transparent;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 13px;
+ transition: all 0.2s;
+}
+
+.format-btn:hover {
+ background: #4f4f4f;
+}
+
+.format-btn.active {
+ border-color: #3ea6ff;
+ background: #1f1f1f;
+}
+
+.video-section {
+ background: #000;
+ border-radius: 8px;
+ overflow: hidden;
+ margin-bottom: 20px;
+}
+
+video {
+ width: 100%;
+ max-height: 70vh;
+ display: block;
+}
+
+.video-info {
+ background: #272727;
+ padding: 15px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+}
+
+.video-info h5 {
+ font-size: 18px;
+ margin-bottom: 8px;
+ color: #f1f1f1;
+}
+
+.video-info p {
+ color: #aaa;
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+.stream-url-section {
+ margin-top: 15px;
+ padding-top: 15px;
+ border-top: 1px solid #3f3f3f;
+}
+
+.stream-url-section label {
+ display: block;
+ font-size: 12px;
+ color: #aaa;
+ margin-bottom: 5px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.url-display {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 5px;
+}
+
+.url-display input[type="text"] {
+ flex: 1;
+ padding: 8px;
+ background: #1a1a1a;
+ border: 1px solid #3f3f3f;
+ border-radius: 4px;
+ color: #f1f1f1;
+ font-size: 12px;
+ font-family: 'Courier New', monospace;
+}
+
+.url-display input[type="text"]:focus {
+ outline: none;
+ border-color: #3ea6ff;
+}
+
+.copy-btn {
+ padding: 8px 16px;
+ background: #3ea6ff;
+ color: #0f0f0f;
+ border: none;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+ white-space: nowrap;
+}
+
+.copy-btn:hover {
+ background: #5eb3ff;
+}
+
+.url-type {
+ display: block;
+ font-size: 11px;
+ color: #888;
+ margin-top: 5px;
+}
+
+.loading {
+ text-align: center;
+ padding: 40px;
+ color: #aaa;
+}
+
+.error {
+ background: #3f1616;
+ color: #ff6b6b;
+ padding: 15px;
+ border-radius: 4px;
+ margin-bottom: 20px;
+}
+
+.spinner {
+ border: 3px solid #3f3f3f;
+ border-top: 3px solid #3ea6ff;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ animation: spin 1s linear infinite;
+ margin: 20px auto;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
diff --git a/resources/js/app.js b/resources/js/app.js
index f0fa127..68f5beb 100644
--- a/resources/js/app.js
+++ b/resources/js/app.js
@@ -1,15 +1,10 @@
import htmx from 'htmx.org';
-import axios from 'axios';
import jQuery from 'jquery';
// Expose globally so inline scripts and Blade templates can use them
window.htmx = htmx;
-window.axios = axios;
window.$ = jQuery;
window.jQuery = jQuery;
-// Set axios CSRF header from meta tag
-const token = document.querySelector('meta[name="csrf-token"]');
-if (token) {
- axios.defaults.headers.common['X-CSRF-TOKEN'] = token.getAttribute('content');
-}
+const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
+window.csrfToken = csrfToken;
diff --git a/public/js/blood.js b/resources/js/blood.js
index c45663a..c45663a 100644
--- a/public/js/blood.js
+++ b/resources/js/blood.js
diff --git a/public/js/blood_gpu.js b/resources/js/blood_gpu.js
index 19d88c4..19d88c4 100644
--- a/public/js/blood_gpu.js
+++ b/resources/js/blood_gpu.js
diff --git a/public/js/bootstrap.js b/resources/js/bootstrap.js
index deb2e10..deb2e10 100644
--- a/public/js/bootstrap.js
+++ b/resources/js/bootstrap.js
diff --git a/public/js/echo.js b/resources/js/echo.js
index 9349afa..9349afa 100644
--- a/public/js/echo.js
+++ b/resources/js/echo.js
diff --git a/public/js/hn.js b/resources/js/hn.js
index d9201ec..d9201ec 100644
--- a/public/js/hn.js
+++ b/resources/js/hn.js
diff --git a/public/js/home.js b/resources/js/home.js
index e69de29..e69de29 100644
--- a/public/js/home.js
+++ b/resources/js/home.js
diff --git a/public/js/lib.js b/resources/js/lib.js
index 1067f7a..1067f7a 100644
--- a/public/js/lib.js
+++ b/resources/js/lib.js
diff --git a/public/js/main.js b/resources/js/main.js
index b7673cc..ed5956e 100644
--- a/public/js/main.js
+++ b/resources/js/main.js
@@ -95,12 +95,14 @@ $(document).ready(async function() {
});
async function getServerEnvVars(){
- await axios.get('/env').then((res)=>{
- env = res.data;
- log(env);
- }).catch((err)=>{
- log(err);
- });
+ await fetch('/env')
+ .then((res) => res.json())
+ .then((data) => {
+ env = data;
+ log(env);
+ }).catch((err) => {
+ log(err);
+ });
}
//initial setup such as hiding/showing certain things
@@ -153,17 +155,18 @@ async function uploadFile() {
formData.append('f[]', files[i], files[i].name);
}
- axios.post('/f', formData, {headers: {'Content-Type': 'multipart/form-data'}})
- .then((res)=>{
- console.log(res);
+ fetch('/f', {method: 'POST', headers: {'X-CSRF-TOKEN': window.csrfToken}, body: formData})
+ .then((res) => res.json())
+ .then((data) => {
+ console.log(data);
domElems.fileUploadResult.innerHTML = 'File uploaded successfully<br>';
- res.data.files.forEach((f)=>{
+ 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)=>{
+ .catch((err) => {
console.log(err);
domElems.fileUploadResult.innerHTML = 'Error uploading file';
domElems.buttonFileUpload.enabled = true;
diff --git a/public/js/marked.js b/resources/js/marked.js
index 2eb226a..2eb226a 100644
--- a/public/js/marked.js
+++ b/resources/js/marked.js
diff --git a/public/js/socket.io.esm.min.js b/resources/js/socket.io.esm.min.js
index 84cca0e..84cca0e 100644
--- a/public/js/socket.io.esm.min.js
+++ b/resources/js/socket.io.esm.min.js
diff --git a/public/js/webgpu.js b/resources/js/webgpu.js
index b66f9ab..b66f9ab 100644
--- a/public/js/webgpu.js
+++ b/resources/js/webgpu.js
diff --git a/public/js/webgpu_gltf.js b/resources/js/webgpu_gltf.js
index 28e9742..28e9742 100644
--- a/public/js/webgpu_gltf.js
+++ b/resources/js/webgpu_gltf.js
diff --git a/resources/js/writing_create.js b/resources/js/writing_create.js
index cd44a66..9a036b1 100644
--- a/resources/js/writing_create.js
+++ b/resources/js/writing_create.js
@@ -1,17 +1,17 @@
-import $ from 'jquery';
import { marked } from 'marked';
-marked.use({ breaks: true });
-
const dom = {};
const editorHistory = [];
-$(document).ready(() => {
+document.addEventListener('DOMContentLoaded', () => {
+ if (typeof marked === 'undefined') {
+ console.error("marked lib not loaded");
+ return;
+ }
+
initDOM();
- dom.inputText.addEventListener('input', () => {
- dom.contentPreview.innerHTML = marked.parse(dom.inputText.value);
- });
+ marked.use({ breaks: true });
// Restore autosaved draft
const restoredContent = localStorage.getItem('writing_draft_content');
@@ -55,6 +55,9 @@ function initDOM() {
dom.contentPreview.innerHTML = marked.parse(dom.inputText.value);
}
});
+ dom.inputText.addEventListener('input', () => {
+ dom.contentPreview.innerHTML = marked.parse(dom.inputText.value);
+ });
}
function performReplace() {
diff --git a/public/js/writing_index.js b/resources/js/writing_index.js
index de3159d..de3159d 100644
--- a/public/js/writing_index.js
+++ b/resources/js/writing_index.js
diff --git a/public/js/writing_show.js b/resources/js/writing_show.js
index 92487eb..92487eb 100644
--- a/public/js/writing_show.js
+++ b/resources/js/writing_show.js
diff --git a/resources/js/youtube.js b/resources/js/youtube.js
new file mode 100644
index 0000000..5160ff6
--- /dev/null
+++ b/resources/js/youtube.js
@@ -0,0 +1,276 @@
+// YouTube Player - Client-side JavaScript
+// Following standard site.js pattern
+
+let env = {}; // Set by blade template
+let cfg = {}; // User config from localStorage
+let dom = {
+ input: {},
+ button: {},
+ section: {},
+ video: {},
+ info: {}
+};
+
+let selectedFormat = 'best';
+
+// APP START HERE
+$(document).ready(async function() {
+ console.log('YouTube Player Init');
+
+ // Core loop of the client application
+ // 1. Setup relationship with DOM and grab references to its elements
+ log('init DOM');
+ await initDOM();
+
+ log('init cfg');
+ await initCfg();
+
+ log('init services');
+ await initServices();
+
+ log('YouTube Player Ready');
+});
+
+// Gets user config from local storage if there is any
+function initCfg() {
+ let localCfg = localStorage.getItem('youtube_cfg');
+ if (localCfg) {
+ try {
+ cfg = JSON.parse(localCfg);
+ // Restore last used format if available
+ if (cfg.lastFormat) {
+ selectedFormat = cfg.lastFormat;
+ updateFormatButtons();
+ }
+ } catch (e) {
+ cfg = {};
+ }
+ } else {
+ cfg = {};
+ }
+}
+
+function initServices() {
+ // Attach event listeners
+ dom.button.load.addEventListener('click', loadVideo);
+ dom.input.videoUrl.addEventListener('keypress', function(e) {
+ if (e.key === 'Enter') {
+ loadVideo();
+ }
+ });
+
+ // Format selector buttons
+ dom.button.formatBtns.forEach(btn => {
+ btn.addEventListener('click', function() {
+ selectFormat(this);
+ });
+ });
+
+ // Copy URL button
+ dom.button.copyUrl.addEventListener('click', copyStreamUrl);
+}
+
+function initDOM() {
+ dom.body = $('body')[0];
+
+ // Input elements
+ dom.input.videoUrl = $('#videoUrl')[0];
+
+ // Buttons
+ dom.button.load = $('#loadBtn')[0];
+ dom.button.formatBtns = $('.format-btn').toArray();
+
+ // Sections
+ dom.section.error = $('#errorMsg')[0];
+ dom.section.loading = $('#loadingMsg')[0];
+ dom.section.videoSection = $('#videoSection')[0];
+ dom.section.videoInfo = $('#videoInfo')[0];
+
+ // Video elements
+ dom.video.player = $('#videoPlayer')[0];
+
+ // Info elements
+ dom.info.title = $('#videoTitle')[0];
+ dom.info.description = $('#videoDescription')[0];
+ dom.info.streamUrl = $('#streamUrl')[0];
+ dom.info.urlType = $('#urlType')[0];
+
+ // Copy button
+ dom.button.copyUrl = $('#copyUrlBtn')[0];
+}
+
+function selectFormat(btn) {
+ // Remove active class from all buttons
+ dom.button.formatBtns.forEach(b => b.classList.remove('active'));
+
+ // Add active class to clicked button
+ btn.classList.add('active');
+
+ // Store selected format
+ selectedFormat = btn.dataset.format;
+
+ // Save to config
+ cfg.lastFormat = selectedFormat;
+ localStorage.setItem('youtube_cfg', JSON.stringify(cfg));
+}
+
+function updateFormatButtons() {
+ dom.button.formatBtns.forEach(btn => {
+ if (btn.dataset.format === selectedFormat) {
+ btn.classList.add('active');
+ } else {
+ btn.classList.remove('active');
+ }
+ });
+}
+
+async function loadVideo() {
+ const videoUrl = dom.input.videoUrl.value.trim();
+
+ if (!videoUrl) {
+ showError('Please enter a YouTube URL');
+ return;
+ }
+
+ // Validate YouTube URL
+ if (!isValidYouTubeUrl(videoUrl)) {
+ showError('Please enter a valid YouTube URL');
+ return;
+ }
+
+ hideError();
+ showLoading();
+ hideVideo();
+
+ try {
+ // Get video info and stream URL
+ const response = await axios.post(`/yt/info`, {
+ url: videoUrl,
+ format: selectedFormat
+ });
+
+ const data = response.data;
+
+ if (data.error) {
+ throw new Error(data.error);
+ }
+
+ // Display video
+ displayVideo(data);
+
+ // Save to recent videos in config
+ saveToRecent(videoUrl, data.title);
+
+ } catch (error) {
+ console.error('Error:', error);
+ showError(`Failed to load video: ${error.message}`);
+ } finally {
+ hideLoading();
+ }
+}
+
+function displayVideo(data) {
+ // Set video source
+ dom.video.player.src = data.stream_url;
+
+ // Display video info
+ dom.info.title.textContent = data.title || 'Untitled Video';
+ dom.info.description.textContent = data.description || 'No description available';
+
+ // Display stream URL
+ dom.info.streamUrl.value = data.stream_url;
+
+ // Detect and display URL type
+ const urlType = detectStreamType(data.stream_url);
+ dom.info.urlType.textContent = urlType;
+
+ // Show elements
+ dom.section.videoSection.hidden = false;
+ dom.section.videoInfo.hidden = false;
+
+ // Auto-play
+ dom.video.player.play().catch(e => {
+ log('Auto-play prevented: ' + e.message);
+ });
+}
+
+function detectStreamType(url) {
+ if (url.includes('.m3u8')) {
+ return '📡 HLS Stream (.m3u8) - Adaptive bitrate streaming';
+ } else if (url.includes('.mpd')) {
+ return '📡 DASH Stream (.mpd) - Adaptive bitrate streaming';
+ } else if (url.includes('mime=video%2Fmp4')) {
+ return '📹 Progressive MP4 - Direct download';
+ } else if (url.includes('googlevideo.com')) {
+ return '📹 Google Video CDN stream';
+ } else {
+ return '🎥 Video stream';
+ }
+}
+
+function copyStreamUrl() {
+ dom.info.streamUrl.select();
+ dom.info.streamUrl.setSelectionRange(0, 99999); // For mobile
+
+ navigator.clipboard.writeText(dom.info.streamUrl.value).then(() => {
+ // Visual feedback
+ const originalText = dom.button.copyUrl.textContent;
+ dom.button.copyUrl.textContent = '✓ Copied!';
+ dom.button.copyUrl.style.background = '#4caf50';
+
+ setTimeout(() => {
+ dom.button.copyUrl.textContent = originalText;
+ dom.button.copyUrl.style.background = '';
+ }, 2000);
+ }).catch(err => {
+ log('Failed to copy: ' + err);
+ alert('Failed to copy URL. Please copy manually.');
+ });
+}
+
+function isValidYouTubeUrl(url) {
+ const patterns = [
+ /^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)[\w-]+/,
+ /^(https?:\/\/)?(www\.)?youtube\.com\/embed\/[\w-]+/
+ ];
+ return patterns.some(pattern => pattern.test(url));
+}
+
+function saveToRecent(url, title) {
+ if (!cfg.recentVideos) {
+ cfg.recentVideos = [];
+ }
+
+ // Add to beginning of array, limit to 10 recent
+ cfg.recentVideos.unshift({ url, title, timestamp: Date.now() });
+ cfg.recentVideos = cfg.recentVideos.slice(0, 10);
+
+ localStorage.setItem('youtube_cfg', JSON.stringify(cfg));
+}
+
+// UI Helper functions
+function showError(message) {
+ dom.section.error.textContent = message;
+ dom.section.error.hidden = false;
+}
+
+function hideError() {
+ dom.section.error.hidden = true;
+}
+
+function showLoading() {
+ dom.section.loading.hidden = false;
+}
+
+function hideLoading() {
+ dom.section.loading.hidden = true;
+}
+
+function hideVideo() {
+ dom.section.videoSection.hidden = true;
+ dom.section.videoInfo.hidden = true;
+}
+
+function log(msg, lvl = 1) {
+ console.log(msg);
+}
diff --git a/resources/js/youtube.js.bak b/resources/js/youtube.js.bak
new file mode 100644
index 0000000..6aa2ca8
--- /dev/null
+++ b/resources/js/youtube.js.bak
@@ -0,0 +1,227 @@
+// YouTube Player - Client-side JavaScript
+// Following standard site.js pattern
+
+let env = {}; // Set by blade template
+let cfg = {}; // User config from localStorage
+let dom = {
+ input: {},
+ button: {},
+ section: {},
+ video: {},
+ info: {}
+};
+
+let selectedFormat = 'best';
+
+// APP START HERE
+$(document).ready(async function() {
+ console.log('YouTube Player Init');
+
+ // Core loop of the client application
+ // 1. Setup relationship with DOM and grab references to its elements
+ log('init DOM');
+ await initDOM();
+
+ log('init cfg');
+ await initCfg();
+
+ log('init services');
+ await initServices();
+
+ log('YouTube Player Ready');
+});
+
+// Gets user config from local storage if there is any
+function initCfg() {
+ let localCfg = localStorage.getItem('youtube_cfg');
+ if (localCfg) {
+ try {
+ cfg = JSON.parse(localCfg);
+ // Restore last used format if available
+ if (cfg.lastFormat) {
+ selectedFormat = cfg.lastFormat;
+ updateFormatButtons();
+ }
+ } catch (e) {
+ cfg = {};
+ }
+ } else {
+ cfg = {};
+ }
+}
+
+function initServices() {
+ // Attach event listeners
+ dom.button.load.addEventListener('click', loadVideo);
+ dom.input.videoUrl.addEventListener('keypress', function(e) {
+ if (e.key === 'Enter') {
+ loadVideo();
+ }
+ });
+
+ // Format selector buttons
+ dom.button.formatBtns.forEach(btn => {
+ btn.addEventListener('click', function() {
+ selectFormat(this);
+ });
+ });
+}
+
+function initDOM() {
+ dom.body = $('body')[0];
+
+ // Input elements
+ dom.input.videoUrl = $('#videoUrl')[0];
+
+ // Buttons
+ dom.button.load = $('#loadBtn')[0];
+ dom.button.formatBtns = $('.format-btn').toArray();
+
+ // Sections
+ dom.section.error = $('#errorMsg')[0];
+ dom.section.loading = $('#loadingMsg')[0];
+ dom.section.videoSection = $('#videoSection')[0];
+ dom.section.videoInfo = $('#videoInfo')[0];
+
+ // Video elements
+ dom.video.player = $('#videoPlayer')[0];
+
+ // Info elements
+ dom.info.title = $('#videoTitle')[0];
+ dom.info.description = $('#videoDescription')[0];
+}
+
+function selectFormat(btn) {
+ // Remove active class from all buttons
+ dom.button.formatBtns.forEach(b => b.classList.remove('active'));
+
+ // Add active class to clicked button
+ btn.classList.add('active');
+
+ // Store selected format
+ selectedFormat = btn.dataset.format;
+
+ // Save to config
+ cfg.lastFormat = selectedFormat;
+ localStorage.setItem('youtube_cfg', JSON.stringify(cfg));
+}
+
+function updateFormatButtons() {
+ dom.button.formatBtns.forEach(btn => {
+ if (btn.dataset.format === selectedFormat) {
+ btn.classList.add('active');
+ } else {
+ btn.classList.remove('active');
+ }
+ });
+}
+
+async function loadVideo() {
+ const videoUrl = dom.input.videoUrl.value.trim();
+
+ if (!videoUrl) {
+ showError('Please enter a YouTube URL');
+ return;
+ }
+
+ // Validate YouTube URL
+ if (!isValidYouTubeUrl(videoUrl)) {
+ showError('Please enter a valid YouTube URL');
+ return;
+ }
+
+ hideError();
+ showLoading();
+ hideVideo();
+
+ try {
+ // Get video info and stream URL
+ const response = await axios.post('/yt/info', {
+ url: videoUrl,
+ format: selectedFormat
+ });
+
+ const data = response.data;
+
+ if (data.error) {
+ throw new Error(data.error);
+ }
+
+ // Display video
+ displayVideo(data);
+
+ // Save to recent videos in config
+ saveToRecent(videoUrl, data.title);
+
+ } catch (error) {
+ console.error('Error:', error);
+ showError(`Failed to load video: ${error.message}`);
+ } finally {
+ hideLoading();
+ }
+}
+
+function displayVideo(data) {
+ // Set video source
+ dom.video.player.src = data.stream_url;
+
+ // Display video info
+ dom.info.title.textContent = data.title || 'Untitled Video';
+ dom.info.description.textContent = data.description || 'No description available';
+
+ // Show elements
+ dom.section.videoSection.hidden = false;
+ dom.section.videoInfo.hidden = false;
+
+ // Auto-play
+ dom.video.player.play().catch(e => {
+ log('Auto-play prevented: ' + e.message);
+ });
+}
+
+function isValidYouTubeUrl(url) {
+ const patterns = [
+ /^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)[\w-]+/,
+ /^(https?:\/\/)?(www\.)?youtube\.com\/embed\/[\w-]+/
+ ];
+ return patterns.some(pattern => pattern.test(url));
+}
+
+function saveToRecent(url, title) {
+ if (!cfg.recentVideos) {
+ cfg.recentVideos = [];
+ }
+
+ // Add to beginning of array, limit to 10 recent
+ cfg.recentVideos.unshift({ url, title, timestamp: Date.now() });
+ cfg.recentVideos = cfg.recentVideos.slice(0, 10);
+
+ localStorage.setItem('youtube_cfg', JSON.stringify(cfg));
+}
+
+// UI Helper functions
+function showError(message) {
+ dom.section.error.textContent = message;
+ dom.section.error.hidden = false;
+}
+
+function hideError() {
+ dom.section.error.hidden = true;
+}
+
+function showLoading() {
+ dom.section.loading.hidden = false;
+}
+
+function hideLoading() {
+ dom.section.loading.hidden = true;
+}
+
+function hideVideo() {
+ dom.section.videoSection.hidden = true;
+ dom.section.videoInfo.hidden = true;
+}
+
+function log(msg, lvl = 1) {
+ console.log(msg);
+}
diff --git a/resources/views/graphics.html b/resources/views/.off/graphics.html
index d6e8ae7..d6e8ae7 100644
--- a/resources/views/graphics.html
+++ b/resources/views/.off/graphics.html
diff --git a/resources/views/marked.blade.php b/resources/views/.off/marked.blade.php
index f1deefc..823900c 100644
--- a/resources/views/marked.blade.php
+++ b/resources/views/.off/marked.blade.php
@@ -7,9 +7,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--<link rel="stylesheet" type="text/css" href="static/reset.css">-->
<link rel="stylesheet" type="text/css" href="style.css">
- @vite('resources/js/app.js')
+ @v('js/app.js')
+ @v('js/marked.js')
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
- <script src="/js/marked.js"></script>
<!--<base href = "http://192.168.4.32:9002/">-->
<!--<base href = "http://localhost:9002/">-->
<title>markup playground</title>
diff --git a/resources/views/4.blade.php b/resources/views/4.blade.php
index f23d1e1..9e32cf5 100755
--- a/resources/views/4.blade.php
+++ b/resources/views/4.blade.php
@@ -3,8 +3,9 @@
<main>
<h4>Search Current 4chan Posts</h4>
<form action = "/4" method = "POST">
- query <input type = "text" id = "query" />
- board <input type = "text" id = "board" />
+ @csrf
+ query <input type = "text" name = "query" />
+ board <input type = "text" name = "board" />
<button type = "submit">Search</button>
</form>
</main>
diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php
index c993212..d6ac6ec 100755
--- a/resources/views/home.blade.php
+++ b/resources/views/home.blade.php
@@ -1,7 +1,6 @@
@extends('template')
@section('scripts')
-<script type = "module" src = "/js/main.js"></script>
-<script src = "js/hn.js" defer></script>
+@v(['js/main.js', 'js/hn.js'])
@endsection
@section('content')
@@ -70,7 +69,7 @@ UNDER CONSTRUCTION; APOLOGIES FOR LACKING FUNCTIONALITY.
</div>
</section>
<footer>
- <p>Last updated March 6th, 2026</p>
+ <p>Last updated April 12th, 2026</p>
</footer>
@endsection
</html>
diff --git a/resources/views/presence/index.blade.php b/resources/views/presence/index.blade.php
index b9765fa..bf05cc2 100644
--- a/resources/views/presence/index.blade.php
+++ b/resources/views/presence/index.blade.php
@@ -1,6 +1,6 @@
@extends('template')
@section('head')
-<link rel="stylesheet" href="/css/presence.css">
+@v(['css/presence.css'])
@endsection
@section('content')
diff --git a/resources/views/presence/show.blade.php b/resources/views/presence/show.blade.php
index 0b23bbe..a3f19b9 100644
--- a/resources/views/presence/show.blade.php
+++ b/resources/views/presence/show.blade.php
@@ -1,6 +1,5 @@
@extends('template')
@section('head')
-<link rel="stylesheet" href="/css/presence.css">
@endsection
@section('content')
diff --git a/resources/views/template.blade.php b/resources/views/template.blade.php
index 5d41e61..61a7a59 100755
--- a/resources/views/template.blade.php
+++ b/resources/views/template.blade.php
@@ -5,7 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TG</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
- <link rel="stylesheet" href="/css/home.css">
+ @v('css/home.css')
+ <!--<link rel="stylesheet" href="/css/home.css">-->
@yield('head')
</head>
<body>
diff --git a/resources/views/writings/create.blade.php b/resources/views/writings/create.blade.php
index c095b5a..5e7281d 100644
--- a/resources/views/writings/create.blade.php
+++ b/resources/views/writings/create.blade.php
@@ -2,9 +2,9 @@
@section('head')
<meta charset="UTF-8">
-@vite('resources/js/writing_create.js')
-<link rel="stylesheet" href="/css/home.css">
-<link rel="stylesheet" href="/css/writings.css">
+@v('js/writing_create.js')
+@v('css/home.css')
+@v('css/writings.css')
@endsection
@section('content')
diff --git a/resources/views/writings/edit.blade.php b/resources/views/writings/edit.blade.php
index e442b5d..10a9e92 100644
--- a/resources/views/writings/edit.blade.php
+++ b/resources/views/writings/edit.blade.php
@@ -1,9 +1,10 @@
@extends('template')
@section('head')
-<link rel="stylesheet" href="/css/writings.css">
+@v('css/writings.css')
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
-<script type = "module" src = "/js/writing_show.js"></script>
+@v('js/writing_show.js')
@endsection
@section('content')
diff --git a/resources/views/writings/index.blade.php b/resources/views/writings/index.blade.php
index 3f5d0db..7e12333 100644
--- a/resources/views/writings/index.blade.php
+++ b/resources/views/writings/index.blade.php
@@ -1,7 +1,7 @@
@extends('template')
@section('head')
+@v('js/writing_index.js')
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
-<script type = "module" src = "/js/writing_index.js"></script>
@endsection
@section('content')
<main>
diff --git a/resources/views/writings/show.blade.php b/resources/views/writings/show.blade.php
index 4b191ad..e55fb4b 100644
--- a/resources/views/writings/show.blade.php
+++ b/resources/views/writings/show.blade.php
@@ -1,9 +1,9 @@
@extends('template')
@section('head')
-<link rel="stylesheet" href="/css/writings.css">
+@v('css/writings.css')
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
-<script type = "module" src = "/js/writing_show.js"></script>
+@v('js/writing_show.js')
@endsection
@section('nav')
diff --git a/routes/web.php b/routes/web.php
index e2cf097..df3f526 100755
--- a/routes/web.php
+++ b/routes/web.php
@@ -80,8 +80,8 @@ Route::middleware(['auth', 'throttle:admin', \App\Http\Middleware\Admin::class])
Route::get('/admin', [AdminController::class, 'index'])->name('admin');
Route::post('/l/import', [LinkController::class, 'import'])->name('l.import');
Route::get('/env', [SiteController::class, 'env']);
- Route::get('/4', [SiteController::class, 'search4chan']);
});
+Route::post('/4', [SiteController::class, 'search4chan']);
// Public read-only routes
Route::resource('w', WritingController::class)->only(['index', 'show']);
@@ -102,7 +102,7 @@ Route::post('/update-session', function(Request $req){
require __DIR__.'/auth.php';
-Route::get('/toy/{v?}', function($v = null){
+/*Route::get('/toy/{v?}', function($v = null){
if (is_null($v)){
return view('toys.index');
}
@@ -114,7 +114,7 @@ Route::get('/toy/{v?}', function($v = null){
abort(404);
}
return view('toys.'.$v);
-});
+});*/
// Audio stream - requires auth to prevent abuse
Route::get('/stream/audio', function() {
@@ -146,7 +146,7 @@ Route::get('/newtab', function() {
// Catch-all: only allow specific whitelisted views
Route::get('/{v}', function($v){
- $allowed = ['notes', 'kyanite', 'marked', 'v'];
+ $allowed = ['notes', 'kyanite', 'marked', 'v', '4'];
if (!in_array($v, $allowed)) {
abort(404);
}
diff --git a/vite.config.js b/vite.config.js
index 9f67760..35a4549 100755
--- a/vite.config.js
+++ b/vite.config.js
@@ -6,13 +6,24 @@ export default defineConfig({
laravel({
input: [
'resources/js/app.js',
+ 'resources/js/main.js',
+ 'resources/js/hn.js',
'resources/js/writing_create.js',
+ 'resources/css/writings.css',
+ 'resources/css/app.css',
+ 'resources/css/home.css',
+ 'resources/css/presence.css',
],
refresh: true,
}),
],
server: {
- host: '192.168.1.171',
+ host: '127.0.0.1',
cors: true,
+ },
+ resolve: {
+ alias: {
+ '@': 'resources'
+ }
}
});