@decentnetwork/lan 0.1.141 → 0.1.143

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.
@@ -52,6 +52,8 @@ const ICON_PATHS = {
52
52
  trash: '<path d="M3 6h18"/><path d="M8 6V4.5A1.5 1.5 0 0 1 9.5 3h5A1.5 1.5 0 0 1 16 4.5V6"/><path d="M18.5 6l-1 13.5a2 2 0 0 1-2 1.5h-7a2 2 0 0 1-2-1.5L5.5 6"/><path d="M10 10.5v6M14 10.5v6"/>',
53
53
  arrowDown: '<path d="M12 4v15M6 13l6 6 6-6"/>',
54
54
  shrink: '<path d="M4 14h6v6"/><path d="M20 10h-6V4"/><path d="m14 10 7-7"/><path d="m3 21 7-7"/>',
55
+ maximize: '<path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/>',
56
+ theater: '<rect x="2.5" y="6.5" width="19" height="11" rx="2"/>',
55
57
  speaker: '<path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="M16 9a3 3 0 0 1 0 6"/><path d="M19 7a6 6 0 0 1 0 10"/>',
56
58
  micFill: '<rect x="9" y="2.5" width="6" height="11.5" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0"/><path d="M12 17.5v3.5"/>'
57
59
  };
@@ -1030,9 +1032,21 @@ const inputStyle = {
1030
1032
  outline: "none",
1031
1033
  minWidth: 0
1032
1034
  };
1033
- function Msg({ m, peer, T }) {
1035
+ const mediaBtnStyle = {
1036
+ width: 28,
1037
+ height: 28,
1038
+ borderRadius: 8,
1039
+ border: "none",
1040
+ background: "rgba(0,0,0,0.5)",
1041
+ cursor: "pointer",
1042
+ display: "flex",
1043
+ alignItems: "center",
1044
+ justifyContent: "center",
1045
+ lineHeight: 0
1046
+ };
1047
+ function Msg({ m, peer, T, onTheater }) {
1034
1048
  const mine = m.from === "me";
1035
- return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: mine ? "flex-end" : "flex-start", alignItems: "flex-end", gap: 8, margin: "3px 0" } }, !mine && /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } }, m.file ? (
1049
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: mine ? "flex-end" : "flex-start", alignItems: "flex-end", gap: 8, margin: "3px 0" } }, !mine && /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: m.file && (m.file.media === "image" || m.file.media === "video") ? "min(680px, 88%)" : "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } }, m.file ? (
1036
1050
  // Inline preview/player. Received media: as soon as it's saved.
1037
1051
  // Sent media: once it's fully delivered ('sent') — the daemon keeps a
1038
1052
  // local copy so it plays on the sender's side too.
@@ -1042,27 +1056,48 @@ function Msg({ m, peer, T }) {
1042
1056
  display: "flex",
1043
1057
  flexDirection: "column",
1044
1058
  gap: 6,
1045
- maxWidth: 300,
1059
+ maxWidth: m.file.media === "image" || m.file.media === "video" ? "100%" : 300,
1060
+ width: m.file.media === "image" || m.file.media === "video" ? "100%" : void 0,
1046
1061
  background: "var(--bub-them)",
1047
1062
  border: "1px solid var(--line)",
1048
1063
  borderRadius: 12,
1049
1064
  padding: 6
1050
- } }, m.file.media === "image" && /* @__PURE__ */ React.createElement("a", { href: dkFileUrl(m.file.name), target: "_blank", rel: "noreferrer", style: { display: "block", lineHeight: 0 } }, /* @__PURE__ */ React.createElement(
1065
+ } }, (m.file.media === "image" || m.file.media === "video") && /* @__PURE__ */ React.createElement("div", { "data-media": true, style: { position: "relative", lineHeight: 0 } }, m.file.media === "image" ? /* @__PURE__ */ React.createElement(
1051
1066
  "img",
1052
1067
  {
1053
1068
  src: dkFileUrl(m.file.name),
1054
1069
  alt: m.file.name,
1055
- style: { display: "block", maxWidth: "100%", maxHeight: 280, borderRadius: 8, objectFit: "cover" }
1070
+ style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain" }
1056
1071
  }
1057
- )), m.file.media === "video" && /* @__PURE__ */ React.createElement(
1072
+ ) : /* @__PURE__ */ React.createElement(
1058
1073
  "video",
1059
1074
  {
1060
1075
  src: dkFileUrl(m.file.name),
1061
1076
  controls: true,
1062
1077
  preload: "metadata",
1063
- style: { maxWidth: "100%", maxHeight: 320, borderRadius: 8, background: "#000" }
1078
+ style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain", background: "#000" }
1064
1079
  }
1065
- ), m.file.media === "audio" && /* @__PURE__ */ React.createElement("audio", { src: dkFileUrl(m.file.name), controls: true, style: { width: "100%" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px" } }, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", flexShrink: 0 } }, m.file.size), /* @__PURE__ */ React.createElement("a", { href: dkFileDownloadUrl(m.file.name), download: m.file.name, title: "download", style: { display: "inline-flex", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 15, stroke: 2, color: "var(--accent)" }))))
1080
+ ), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", top: 8, right: 8, display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(
1081
+ "button",
1082
+ {
1083
+ onClick: () => onTheater({ kind: m.file.media, url: dkFileUrl(m.file.name), name: m.file.name }),
1084
+ title: "theater / \u7F51\u9875\u5168\u5C4F",
1085
+ style: mediaBtnStyle
1086
+ },
1087
+ /* @__PURE__ */ React.createElement(Icon, { name: "theater", size: 16, stroke: 2, color: "#fff" })
1088
+ ), /* @__PURE__ */ React.createElement(
1089
+ "button",
1090
+ {
1091
+ onClick: (e) => {
1092
+ const w = e.currentTarget.closest("[data-media]");
1093
+ const el = w && w.querySelector("video,img");
1094
+ if (el && el.requestFullscreen) el.requestFullscreen();
1095
+ },
1096
+ title: "fullscreen / \u5168\u5C4F",
1097
+ style: mediaBtnStyle
1098
+ },
1099
+ /* @__PURE__ */ React.createElement(Icon, { name: "maximize", size: 16, stroke: 2, color: "#fff" })
1100
+ ))), m.file.media === "audio" && /* @__PURE__ */ React.createElement("audio", { src: dkFileUrl(m.file.name), controls: true, style: { width: "100%" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px" } }, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", flexShrink: 0 } }, m.file.size), /* @__PURE__ */ React.createElement("a", { href: dkFileDownloadUrl(m.file.name), download: m.file.name, title: "download", style: { display: "inline-flex", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 15, stroke: 2, color: "var(--accent)" }))))
1066
1101
  ) : (() => {
1067
1102
  const cardStyle = {
1068
1103
  display: "flex",
@@ -1129,9 +1164,21 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
1129
1164
  const [draft, setDraft] = React.useState("");
1130
1165
  const [dragOver, setDragOver] = React.useState(false);
1131
1166
  const [sending, setSending] = React.useState(null);
1167
+ const [theater, setTheater] = React.useState(null);
1132
1168
  React.useEffect(() => {
1133
1169
  setDraft("");
1134
1170
  }, [peer.id]);
1171
+ React.useEffect(() => {
1172
+ setTheater(null);
1173
+ }, [peer.id]);
1174
+ React.useEffect(() => {
1175
+ if (!theater) return;
1176
+ const onKey = (e) => {
1177
+ if (e.key === "Escape") setTheater(null);
1178
+ };
1179
+ window.addEventListener("keydown", onKey);
1180
+ return () => window.removeEventListener("keydown", onKey);
1181
+ }, [theater]);
1135
1182
  const sendDraft = () => {
1136
1183
  if (draft.trim()) {
1137
1184
  onSend(draft);
@@ -1172,7 +1219,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
1172
1219
  setMenu(false);
1173
1220
  onRemove(peer);
1174
1221
  } }))))),
1175
- /* @__PURE__ */ React.createElement("div", { ref: scrollRef, style: { flex: 1, overflow: "auto", padding: "18px 22px" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto" } }, thread.map((m, i) => m.day ? /* @__PURE__ */ React.createElement("div", { key: i, style: { display: "flex", justifyContent: "center", margin: "14px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 600, color: "var(--faint)", background: "var(--chip)", padding: "3px 10px", borderRadius: 999 } }, m.day)) : /* @__PURE__ */ React.createElement(Msg, { key: i, m, peer, T })))),
1222
+ /* @__PURE__ */ React.createElement("div", { ref: scrollRef, style: { flex: 1, overflow: "auto", padding: "18px 22px" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto" } }, thread.map((m, i) => m.day ? /* @__PURE__ */ React.createElement("div", { key: i, style: { display: "flex", justifyContent: "center", margin: "14px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 600, color: "var(--faint)", background: "var(--chip)", padding: "3px 10px", borderRadius: 999 } }, m.day)) : /* @__PURE__ */ React.createElement(Msg, { key: i, m, peer, T, onTheater: setTheater })))),
1176
1223
  /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: "12px 16px", background: "var(--panel)" } }, sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto 8px", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "paperclip", size: 13, stroke: 2, color: "var(--accent)" }), (lang === "zh" ? "\u53D1\u9001\u4E2D: " : "Sending: ") + sending.name), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
1177
1224
  "input",
1178
1225
  {
@@ -1198,7 +1245,53 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
1198
1245
  placeholder: `${T.message} ${peer.alias || shortKey(peer.userId, 6, 4)}\u2026`,
1199
1246
  style: { flex: 1, height: 38, borderRadius: 9, border: "1px solid var(--line)", background: "var(--panel-2)", color: "var(--text)", fontFamily: "var(--ui)", fontSize: 13.5, padding: "0 14px", outline: "none", minWidth: 0 }
1200
1247
  }
1201
- ), /* @__PURE__ */ React.createElement(Btn, { tone: "solid", icon: "arrowUp", title: T.send, onClick: sendDraft })))
1248
+ ), /* @__PURE__ */ React.createElement(Btn, { tone: "solid", icon: "arrowUp", title: T.send, onClick: sendDraft }))),
1249
+ theater && /* @__PURE__ */ React.createElement("div", { onClick: () => setTheater(null), style: {
1250
+ position: "absolute",
1251
+ inset: 0,
1252
+ zIndex: 60,
1253
+ background: "rgba(0,0,0,0.86)",
1254
+ display: "flex",
1255
+ flexDirection: "column"
1256
+ } }, /* @__PURE__ */ React.createElement("div", { onClick: (e) => e.stopPropagation(), style: { flexShrink: 0, height: 46, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, padding: "0 14px" } }, /* @__PURE__ */ React.createElement("span", { style: { minWidth: 0, fontFamily: "var(--mono)", fontSize: 12, color: "rgba(255,255,255,0.72)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, theater.name), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, flexShrink: 0 } }, /* @__PURE__ */ React.createElement(
1257
+ "button",
1258
+ {
1259
+ title: lang === "zh" ? "\u5168\u5C4F" : "fullscreen",
1260
+ onClick: () => {
1261
+ const el = document.getElementById("dk-theater-media");
1262
+ if (el && el.requestFullscreen) el.requestFullscreen();
1263
+ },
1264
+ style: { width: 34, height: 34, borderRadius: 8, border: "none", background: "rgba(255,255,255,0.14)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }
1265
+ },
1266
+ /* @__PURE__ */ React.createElement(Icon, { name: "maximize", size: 18, stroke: 2, color: "#fff" })
1267
+ ), /* @__PURE__ */ React.createElement(
1268
+ "button",
1269
+ {
1270
+ title: (lang === "zh" ? "\u5173\u95ED" : "close") + " \xB7 Esc",
1271
+ onClick: () => setTheater(null),
1272
+ style: { width: 34, height: 34, borderRadius: 8, border: "none", background: "rgba(255,255,255,0.14)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }
1273
+ },
1274
+ /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 20, stroke: 2.2, color: "#fff" })
1275
+ ))), /* @__PURE__ */ React.createElement("div", { onClick: () => setTheater(null), style: { flex: 1, minHeight: 0, display: "flex", alignItems: "center", justifyContent: "center", padding: "0 16px 16px" } }, theater.kind === "video" ? /* @__PURE__ */ React.createElement(
1276
+ "video",
1277
+ {
1278
+ id: "dk-theater-media",
1279
+ src: theater.url,
1280
+ controls: true,
1281
+ autoPlay: true,
1282
+ onClick: (e) => e.stopPropagation(),
1283
+ style: { maxWidth: "100%", maxHeight: "100%", objectFit: "contain", borderRadius: 6, background: "#000" }
1284
+ }
1285
+ ) : /* @__PURE__ */ React.createElement(
1286
+ "img",
1287
+ {
1288
+ id: "dk-theater-media",
1289
+ src: theater.url,
1290
+ alt: theater.name,
1291
+ onClick: (e) => e.stopPropagation(),
1292
+ style: { maxWidth: "100%", maxHeight: "100%", objectFit: "contain", borderRadius: 6 }
1293
+ }
1294
+ )))
1202
1295
  );
1203
1296
  }
1204
1297
  function MenuItem({ icon, label, onClick, danger }) {
package/dist/ui/server.js CHANGED
@@ -176,7 +176,10 @@ export function startFriendUi(opts) {
176
176
  const desktopIndex = join(DESKTOP_DIR, "index.html");
177
177
  if (req.method === "GET" && (url === "/" || url === "/index.html")) {
178
178
  if (existsSync(desktopIndex)) {
179
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
179
+ // no-store: the UI bundle is rebuilt in place on every UI change, so the
180
+ // browser must never serve a stale cached index/app.js (that's why UI
181
+ // updates appeared to "not take" until a hard reload).
182
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
180
183
  res.end(readFileSync(desktopIndex));
181
184
  return;
182
185
  }
@@ -195,7 +198,12 @@ export function startFriendUi(opts) {
195
198
  const file = join(DESKTOP_DIR, rel);
196
199
  // Guard against path traversal: resolved file must stay under DESKTOP_DIR.
197
200
  if (existsSync(file) && file.startsWith(DESKTOP_DIR)) {
198
- res.writeHead(200, { "content-type": "application/javascript; charset=utf-8" });
201
+ // app.js changes on every UI rebuild → never cache it. Vendored React
202
+ // UMD is stable, so let it cache normally.
203
+ const headers = { "content-type": "application/javascript; charset=utf-8" };
204
+ if (rel === "app.js")
205
+ headers["cache-control"] = "no-store";
206
+ res.writeHead(200, headers);
199
207
  res.end(readFileSync(file));
200
208
  return;
201
209
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.141",
3
+ "version": "0.1.143",
4
4
  "description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -79,7 +79,7 @@
79
79
  },
80
80
  "dependencies": {
81
81
  "@decentnetwork/dora": "^0.1.11",
82
- "@decentnetwork/peer": "^0.1.74",
82
+ "@decentnetwork/peer": "^0.1.76",
83
83
  "ink": "^5.2.1",
84
84
  "js-yaml": "^4.1.0",
85
85
  "node-forge": "^1.4.0",