instbyte 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/index.html CHANGED
@@ -25,10 +25,11 @@
25
25
  </div>
26
26
  </div>
27
27
  <div class="header-right">
28
- <span id="who"></span>
29
- <button onclick="changeName()" class="link-btn">change</button>
30
- <button id="themeToggle" onclick="cycleTheme()">🌙</button>
31
- <button id="logoutBtn" onclick="logout()" class="link-btn">logout</button>
28
+ <span id="who" class="username-display" onclick="changeName()" title="Click to change name"></span>
29
+ <span id="onlineCount" class="online-count"></span>
30
+ <button id="themeToggle" onclick="cycleTheme()" class="link-btn theme-toggle" title="Toggle theme">🌙</button>
31
+ <button id="logoutBtn" onclick="logout()" class="logout-btn">logout</button>
32
+ </div>
32
33
  </div>
33
34
  </header>
34
35
 
@@ -54,6 +55,9 @@
54
55
  </div>
55
56
  </div>
56
57
  <div id="items"></div>
58
+ <div id="loadMoreWrapper" style="display:none; text-align:center; margin:16px 0;">
59
+ <button id="loadMoreBtn" onclick="loadMore()" class="load-more-btn">Load more</button>
60
+ </div>
57
61
  </div>
58
62
 
59
63
  <div id="dragOverlay"
package/client/js/app.js CHANGED
@@ -1,5 +1,8 @@
1
1
  const socket = io();
2
2
 
3
+ let currentPage = 1;
4
+ let hasMoreItems = false;
5
+
3
6
  // ========================
4
7
  // THEME MANAGEMENT (FIXED)
5
8
  // ========================
@@ -94,6 +97,37 @@ function formatSize(bytes) {
94
97
  return bytes + " B";
95
98
  }
96
99
 
100
+ function playChime() {
101
+ try {
102
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
103
+
104
+ const gain = ctx.createGain();
105
+ gain.connect(ctx.destination);
106
+ gain.gain.setValueAtTime(0, ctx.currentTime);
107
+ gain.gain.linearRampToValueAtTime(0.15, ctx.currentTime + 0.01);
108
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.6);
109
+
110
+ const osc1 = ctx.createOscillator();
111
+ osc1.type = "sine";
112
+ osc1.frequency.setValueAtTime(880, ctx.currentTime);
113
+ osc1.frequency.exponentialRampToValueAtTime(1100, ctx.currentTime + 0.1);
114
+ osc1.connect(gain);
115
+ osc1.start(ctx.currentTime);
116
+ osc1.stop(ctx.currentTime + 0.6);
117
+
118
+ const osc2 = ctx.createOscillator();
119
+ osc2.type = "sine";
120
+ osc2.frequency.setValueAtTime(1320, ctx.currentTime + 0.08);
121
+ osc2.frequency.exponentialRampToValueAtTime(1760, ctx.currentTime + 0.2);
122
+ osc2.connect(gain);
123
+ osc2.start(ctx.currentTime + 0.08);
124
+ osc2.stop(ctx.currentTime + 0.6);
125
+
126
+ } catch (e) {
127
+ // audio not supported or blocked — fail silently
128
+ }
129
+ }
130
+
97
131
  function getSizeTag(bytes) {
98
132
  if (!bytes) return "";
99
133
  const mb = bytes / (1024 * 1024);
@@ -363,6 +397,7 @@ document.addEventListener("click", () => {
363
397
 
364
398
  let channel = null;
365
399
  let channels = [];
400
+ let unreadChannels = new Set();
366
401
 
367
402
  let uploader = localStorage.getItem("name") || "";
368
403
 
@@ -379,7 +414,7 @@ async function initName() {
379
414
  uploader = prompt("Your name?", suggested) || suggested;
380
415
  localStorage.setItem("name", uploader);
381
416
  }
382
- document.getElementById("who").innerText = "You: " + uploader;
417
+ document.getElementById("who").innerText = uploader;
383
418
  socket.emit("join", uploader);
384
419
  }
385
420
 
@@ -400,16 +435,36 @@ function setChannel(c) {
400
435
  pendingDeleteEl = null;
401
436
  hideUndoToast();
402
437
  }
438
+ unreadChannels.delete(c); // clear unread when switching to channel
403
439
  channel = c;
404
440
  renderChannels();
405
441
  highlight();
406
442
  load();
407
443
  }
408
444
 
409
- async function load() {
410
- const res = await fetch("/items/" + channel);
445
+ async function load(resetPage = true) {
446
+ if (resetPage) currentPage = 1;
447
+
448
+ const res = await fetch(`/items/${channel}?page=${currentPage}`);
411
449
  const data = await res.json();
412
- render(data);
450
+
451
+ console.log("load called, page:", currentPage, "data:", data);
452
+
453
+ hasMoreItems = data.hasMore;
454
+
455
+ if (currentPage === 1) {
456
+ render(data.items);
457
+ } else {
458
+ renderMore(data.items);
459
+ }
460
+
461
+ const wrapper = document.getElementById("loadMoreWrapper");
462
+ wrapper.style.display = hasMoreItems ? "block" : "none";
463
+ }
464
+
465
+ async function loadMore() {
466
+ currentPage++;
467
+ await load(false);
413
468
  }
414
469
 
415
470
  function render(data) {
@@ -496,6 +551,74 @@ data-value="${i.type === 'file'
496
551
  });
497
552
  }
498
553
 
554
+ function renderMore(data) {
555
+ const el = document.getElementById("items");
556
+ data.forEach(i => {
557
+ const div = document.createElement("div");
558
+ div.className = "item";
559
+ div.dataset.itemId = i.id;
560
+
561
+ let content = "";
562
+
563
+ if (i.type === "file") {
564
+ const isImg = i.filename.match(/\.(jpg|png|jpeg|gif)$/i);
565
+ const sizeLabel = formatSize(i.size);
566
+ const sizeClass = getSizeTag(i.size);
567
+ const sizeTag = sizeClass
568
+ ? `<span class="size-tag ${sizeClass}">${sizeLabel}</span>`
569
+ : sizeLabel
570
+ ? `<span style="font-size:11px;color:#9ca3af;margin-left:6px;">${sizeLabel}</span>`
571
+ : "";
572
+
573
+ if (isImg) {
574
+ content = `<img src="/uploads/${i.filename}" style="max-width:200px;border-radius:6px"><br>
575
+ <a href="/uploads/${i.filename}" target="_blank">${i.filename}</a>${sizeTag}`;
576
+ } else {
577
+ content = `<a href="/uploads/${i.filename}" target="_blank">${i.filename}</a>${sizeTag}`;
578
+ }
579
+ } else {
580
+ const isLink = i.content && i.content.startsWith("http");
581
+ if (isLink) {
582
+ content = `<a href="${i.content}" target="_blank">${i.content}</a>`;
583
+ } else {
584
+ content = renderText(i.content);
585
+ }
586
+ }
587
+
588
+ div.innerHTML = `
589
+ <div class="item-top">
590
+ <div class="left"
591
+ data-tooltip="${i.type === 'file' ? 'Click to download' : 'Click to copy'}"
592
+ data-type="${i.type === 'file' ? 'file' : 'text'}"
593
+ data-value="${i.type === 'file'
594
+ ? `/uploads/${i.filename}`
595
+ : (i.content || '').replace(/"/g, '&quot;')}">
596
+ ${content}
597
+ <div class="meta">${i.uploader}</div>
598
+ </div>
599
+ <div class="item-actions">
600
+ ${getPreviewType(i.filename) !== "none" && getPreviewType(i.filename) !== "image"
601
+ ? `<button class="icon-btn" id="prevbtn-${i.id}"
602
+ onclick="togglePreview(${i.id}, '${i.filename}')"
603
+ title="Preview">👁</button>`
604
+ : ""}
605
+ <button class="icon-btn" onclick="pin(${i.id})" title="${i.pinned ? 'Unpin' : 'Pin'}">
606
+ ${i.pinned ? "📍" : "📌"}
607
+ </button>
608
+ <div class="move-wrapper">
609
+ <button class="icon-btn" title="Move to channel"
610
+ onclick="toggleMoveDropdown(event, ${i.id}, '${i.channel}')">⇄</button>
611
+ <div class="move-dropdown"></div>
612
+ </div>
613
+ <button class="icon-btn delete" onclick="del(${i.id}, ${i.pinned})" title="Delete">🗑</button>
614
+ </div>
615
+ </div>
616
+ <div class="preview-panel" id="preview-${i.id}"></div>`;
617
+
618
+ el.appendChild(div);
619
+ });
620
+ }
621
+
499
622
  function renderGrouped(data) {
500
623
  const el = document.getElementById("items");
501
624
  el.innerHTML = "";
@@ -625,7 +748,7 @@ function changeName() {
625
748
  if (!n) return;
626
749
  uploader = n;
627
750
  localStorage.setItem("name", n);
628
- document.getElementById("who").innerText = "You: " + uploader;
751
+ document.getElementById("who").innerText = uploader;
629
752
  }
630
753
 
631
754
  let qrLoaded = false;
@@ -719,6 +842,18 @@ function hideUndoToast() {
719
842
  progress.classList.remove("running");
720
843
  }
721
844
 
845
+
846
+ // prime AudioContext on first interaction so chime works immediately after
847
+ let audioContextPrimed = false;
848
+ document.addEventListener("click", () => {
849
+ if (audioContextPrimed) return;
850
+ try {
851
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
852
+ ctx.resume().then(() => ctx.close());
853
+ audioContextPrimed = true;
854
+ } catch (e) { }
855
+ }, { once: false });
856
+
722
857
  async function pin(id) {
723
858
  await fetch("/pin/" + id, { method: "POST" });
724
859
  load();
@@ -731,7 +866,14 @@ async function logout() {
731
866
  }
732
867
 
733
868
  socket.on("new-item", item => {
734
- if (item.channel === channel) load();
869
+ if (item.channel === channel) {
870
+ load();
871
+ if (item.uploader !== uploader) playChime();
872
+ } else if (item.uploader !== uploader) {
873
+ // item arrived in a different channel — mark it unread
874
+ unreadChannels.add(item.channel);
875
+ renderChannels();
876
+ }
735
877
  });
736
878
 
737
879
  socket.on("delete-item", id => {
@@ -747,6 +889,11 @@ socket.on("delete-item", id => {
747
889
  }
748
890
  });
749
891
 
892
+ socket.on("user-count", count => {
893
+ const el = document.getElementById("onlineCount");
894
+ if (el) el.textContent = `● ${count} online`;
895
+ });
896
+
750
897
  socket.on("item-moved", ({ id, channel: toChannel }) => {
751
898
  if (toChannel !== channel) {
752
899
  // item left this channel — remove it from view
@@ -793,15 +940,17 @@ document.getElementById("search").addEventListener("input", async e => {
793
940
  const q = e.target.value.trim();
794
941
 
795
942
  if (!q) {
943
+ document.getElementById("loadMoreWrapper").style.display = "none";
796
944
  highlight();
797
945
  load();
798
946
  return;
799
947
  }
800
948
 
801
- // remove active tab highlight while searching
802
949
  document.querySelectorAll(".channels button")
803
950
  .forEach(b => b.classList.remove("active"));
804
951
 
952
+ document.getElementById("loadMoreWrapper").style.display = "none";
953
+
805
954
  const res = await fetch(`/search/${q}`);
806
955
  const data = await res.json();
807
956
  renderGrouped(data);
@@ -976,6 +1125,12 @@ function renderChannels() {
976
1125
  btn.appendChild(dot);
977
1126
  }
978
1127
 
1128
+ if (unreadChannels.has(ch.name)) {
1129
+ const unread = document.createElement("span");
1130
+ unread.className = "ch-unread-dot";
1131
+ btn.appendChild(unread);
1132
+ }
1133
+
979
1134
  const more = document.createElement("span");
980
1135
  more.className = "ch-more";
981
1136
  more.innerText = "⋯";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instbyte",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "A self-hosted LAN sharing utility for fast, frictionless file, link, and snippet exchange across devices — no cloud required.",
5
5
  "main": "server/server.js",
6
6
  "bin": {
package/server/server.js CHANGED
@@ -366,16 +366,47 @@ app.post("/pin/:id", (req, res) => {
366
366
  /* GET ITEMS */
367
367
  app.get("/items/:channel", (req, res) => {
368
368
  const channel = req.params.channel;
369
+ const page = parseInt(req.query.page) || 1;
370
+ const limit = 10;
371
+ const offset = (page - 1) * limit;
369
372
 
370
- db.all(
371
- `SELECT * FROM items WHERE channel=? ORDER BY pinned DESC, created_at DESC`,
373
+ db.get(
374
+ `SELECT COUNT(*) as count FROM items WHERE channel=? AND pinned=0`,
372
375
  [channel],
373
- (err, rows) => {
374
- res.json(rows);
376
+ (err, row) => {
377
+ if (err) return res.status(500).json({ error: "DB error" });
378
+
379
+ const totalUnpinned = row.count;
380
+ const hasMore = offset + limit < totalUnpinned;
381
+
382
+ db.all(
383
+ `SELECT * FROM items WHERE channel=? AND pinned=0
384
+ ORDER BY created_at DESC LIMIT ? OFFSET ?`,
385
+ [channel, limit, offset],
386
+ (err, unpinned) => {
387
+ if (err) return res.status(500).json({ error: "DB error" });
388
+
389
+ if (page === 1) {
390
+ // only fetch pinned on first page
391
+ db.all(
392
+ `SELECT * FROM items WHERE channel=? AND pinned=1
393
+ ORDER BY created_at DESC`,
394
+ [channel],
395
+ (err, pinned) => {
396
+ if (err) return res.status(500).json({ error: "DB error" });
397
+ res.json({ items: [...pinned, ...unpinned], hasMore, page });
398
+ }
399
+ );
400
+ } else {
401
+ res.json({ items: unpinned, hasMore, page });
402
+ }
403
+ }
404
+ );
375
405
  }
376
406
  );
377
407
  });
378
408
 
409
+
379
410
  /* SEARCH */
380
411
  app.get("/search/:channel/:q", (req, res) => {
381
412
  const { channel, q } = req.params;
@@ -624,6 +655,7 @@ let connectedUsers = 0;
624
655
 
625
656
  io.on("connection", (socket) => {
626
657
  connectedUsers++;
658
+ io.emit("user-count", connectedUsers);
627
659
 
628
660
  let username = "Unknown";
629
661
 
@@ -635,6 +667,7 @@ io.on("connection", (socket) => {
635
667
  socket.on("disconnect", () => {
636
668
  connectedUsers--;
637
669
  console.log(username + " disconnected | total:", connectedUsers);
670
+ io.emit("user-count", connectedUsers);
638
671
  });
639
672
  });
640
673
 
@@ -696,3 +729,33 @@ findFreePort(PREFERRED).then(p => {
696
729
  console.log("");
697
730
  });
698
731
  });
732
+
733
+
734
+ // ========================
735
+ // GRACEFUL SHUTDOWN
736
+ // ========================
737
+ function shutdown(signal) {
738
+ console.log(`\n${signal} received — shutting down gracefully...`);
739
+
740
+ // stop accepting new connections
741
+ server.close(() => {
742
+ console.log("HTTP server closed");
743
+
744
+ // close database connection
745
+ db.close((err) => {
746
+ if (err) console.error("Error closing database:", err);
747
+ else console.log("Database connection closed");
748
+ console.log("Shutdown complete");
749
+ process.exit(0);
750
+ });
751
+ });
752
+
753
+ // force exit after 10 seconds if something hangs
754
+ setTimeout(() => {
755
+ console.error("Forced shutdown after timeout");
756
+ process.exit(1);
757
+ }, 10000);
758
+ }
759
+
760
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
761
+ process.on("SIGINT", () => shutdown("SIGINT"));