@decentnetwork/lan 0.1.136 → 0.1.138

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.
@@ -285,7 +285,7 @@ function useDaemonData() {
285
285
  status: m.file.status,
286
286
  pct: m.file.status === "sending" && m.file.size ? Math.min(100, Math.floor((m.file.sent || 0) / m.file.size * 100)) : void 0
287
287
  } : void 0,
288
- status: m.dir === "out" ? "read" : void 0
288
+ status: m.dir === "out" ? m.status === "queued" ? "queued" : "read" : void 0
289
289
  }));
290
290
  const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
291
291
  setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
@@ -1032,38 +1032,39 @@ const inputStyle = {
1032
1032
  };
1033
1033
  function Msg({ m, peer, T }) {
1034
1034
  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 ? !mine && m.file.status !== "sending" && m.file.status !== "failed" && (m.file.media === "image" || m.file.media === "video" || m.file.media === "audio") ? (
1036
- // Received media inline preview / player, with name, size, download.
1037
- /* @__PURE__ */ React.createElement("div", { style: {
1038
- display: "flex",
1039
- flexDirection: "column",
1040
- gap: 6,
1041
- maxWidth: 300,
1042
- background: "var(--bub-them)",
1043
- border: "1px solid var(--line)",
1044
- borderRadius: 12,
1045
- padding: 6
1046
- } }, 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(
1047
- "img",
1048
- {
1049
- src: dkFileUrl(m.file.name),
1050
- alt: m.file.name,
1051
- style: { display: "block", maxWidth: "100%", maxHeight: 280, borderRadius: 8, objectFit: "cover" }
1052
- }
1053
- )), m.file.media === "video" && /* @__PURE__ */ React.createElement(
1054
- "video",
1055
- {
1056
- src: dkFileUrl(m.file.name),
1057
- controls: true,
1058
- preload: "metadata",
1059
- style: { maxWidth: "100%", maxHeight: 320, borderRadius: 8, background: "#000" }
1060
- }
1061
- ), 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)" }))))
1062
- ) : React.createElement(
1063
- mine ? "div" : "a",
1064
- {
1065
- ...mine ? {} : { href: dkFileDownloadUrl(m.file.name), download: m.file.name, title: "download" },
1066
- style: {
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 ? (
1036
+ // Inline preview/player. Received media: as soon as it's saved.
1037
+ // Sent media: once it's fully delivered ('sent') the daemon keeps a
1038
+ // local copy so it plays on the sender's side too.
1039
+ (m.file.media === "image" || m.file.media === "video" || m.file.media === "audio") && (mine ? m.file.status === "sent" : m.file.status !== "sending" && m.file.status !== "failed" && m.file.status !== "queued") ? (
1040
+ // Received media → inline preview / player, with name, size, download.
1041
+ /* @__PURE__ */ React.createElement("div", { style: {
1042
+ display: "flex",
1043
+ flexDirection: "column",
1044
+ gap: 6,
1045
+ maxWidth: 300,
1046
+ background: "var(--bub-them)",
1047
+ border: "1px solid var(--line)",
1048
+ borderRadius: 12,
1049
+ 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(
1051
+ "img",
1052
+ {
1053
+ src: dkFileUrl(m.file.name),
1054
+ alt: m.file.name,
1055
+ style: { display: "block", maxWidth: "100%", maxHeight: 280, borderRadius: 8, objectFit: "cover" }
1056
+ }
1057
+ )), m.file.media === "video" && /* @__PURE__ */ React.createElement(
1058
+ "video",
1059
+ {
1060
+ src: dkFileUrl(m.file.name),
1061
+ controls: true,
1062
+ preload: "metadata",
1063
+ style: { maxWidth: "100%", maxHeight: 320, borderRadius: 8, background: "#000" }
1064
+ }
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)" }))))
1066
+ ) : (() => {
1067
+ const cardStyle = {
1067
1068
  display: "flex",
1068
1069
  alignItems: "center",
1069
1070
  gap: 10,
@@ -1074,11 +1075,34 @@ function Msg({ m, peer, T }) {
1074
1075
  minWidth: 200,
1075
1076
  textDecoration: "none",
1076
1077
  cursor: mine ? "default" : "pointer"
1078
+ };
1079
+ const icon = /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 7, flexShrink: 0, background: mine ? "rgba(255,255,255,0.16)" : "var(--chip)", display: "flex", alignItems: "center", justifyContent: "center", color: mine ? "#fff" : "var(--accent)" } }, /* @__PURE__ */ React.createElement(Icon, { name: m.file.media === "image" ? "image" : m.file.media === "video" ? "video" : m.file.media === "audio" ? "play" : "file", size: 18, stroke: 1.9 }));
1080
+ const meta = /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: mine ? "#fff" : "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: mine ? "rgba(255,255,255,0.7)" : "var(--faint)", marginTop: 1 } }, m.file.status === "queued" ? `${m.file.size} \xB7 ${T.queued || "queued"}` : m.file.status === "sending" ? `${m.file.size} \xB7 sending ${m.file.pct != null ? m.file.pct + "%" : "\u2026"}` : m.file.status === "failed" ? `${m.file.size} \xB7 failed` : mine ? `${m.file.size} \xB7 sent` : `${m.file.size} \xB7 ${T.open || "open"}`));
1081
+ if (mine) {
1082
+ return /* @__PURE__ */ React.createElement("div", { style: cardStyle }, icon, meta, m.file.status === "queued" ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : m.file.status === "sending" ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "rgba(255,255,255,0.9)" } }, m.file.pct != null ? m.file.pct + "%" : "\u2026") : m.file.status === "failed" ? /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 16, stroke: 2.4, color: "rgba(255,200,190,0.95)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }));
1077
1083
  }
1078
- },
1079
- /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 7, flexShrink: 0, background: mine ? "rgba(255,255,255,0.16)" : "var(--chip)", display: "flex", alignItems: "center", justifyContent: "center", color: mine ? "#fff" : "var(--accent)" } }, /* @__PURE__ */ React.createElement(Icon, { name: m.file.media === "image" ? "image" : m.file.media === "video" ? "video" : m.file.media === "audio" ? "play" : "file", size: 18, stroke: 1.9 })),
1080
- /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: mine ? "#fff" : "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: mine ? "rgba(255,255,255,0.7)" : "var(--faint)", marginTop: 1 } }, m.file.status === "sending" ? `${m.file.size} \xB7 sending ${m.file.pct != null ? m.file.pct + "%" : "\u2026"}` : m.file.status === "failed" ? `${m.file.size} \xB7 failed` : mine ? `${m.file.size} \xB7 sent` : `${m.file.size} \xB7 download`)),
1081
- mine ? m.file.status === "sending" ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "rgba(255,255,255,0.9)" } }, m.file.pct != null ? m.file.pct + "%" : "\u2026") : m.file.status === "failed" ? /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 16, stroke: 2.4, color: "rgba(255,200,190,0.95)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 16, stroke: 2, color: "var(--accent)" })
1084
+ return /* @__PURE__ */ React.createElement(
1085
+ "div",
1086
+ {
1087
+ style: cardStyle,
1088
+ title: "open",
1089
+ onClick: () => window.open(dkFileUrl(m.file.name), "_blank", "noopener")
1090
+ },
1091
+ icon,
1092
+ meta,
1093
+ /* @__PURE__ */ React.createElement(
1094
+ "a",
1095
+ {
1096
+ href: dkFileDownloadUrl(m.file.name),
1097
+ download: m.file.name,
1098
+ title: "download",
1099
+ onClick: (e) => e.stopPropagation(),
1100
+ style: { display: "inline-flex", flexShrink: 0 }
1101
+ },
1102
+ /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 16, stroke: 2, color: "var(--accent)" })
1103
+ )
1104
+ );
1105
+ })()
1082
1106
  ) : /* @__PURE__ */ React.createElement("div", { style: {
1083
1107
  padding: "8px 12px",
1084
1108
  borderRadius: 12,
@@ -1092,7 +1116,7 @@ function Msg({ m, peer, T }) {
1092
1116
  lineHeight: 1.4,
1093
1117
  letterSpacing: -0.1,
1094
1118
  wordBreak: "break-word"
1095
- } }, m.text), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" }))));
1119
+ } }, m.text), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), mine && m.status === "queued" ? /* @__PURE__ */ React.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 2 }, title: "waiting for peer \u2014 will send when they're online" }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--faint)" }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, T.queued || "queued")) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" }))));
1096
1120
  }
1097
1121
  function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onAlias, onRemove, onOpenNet }) {
1098
1122
  const scrollRef = React.useRef(null);
@@ -1362,6 +1386,8 @@ const STR = {
1362
1386
  message: "Message",
1363
1387
  send: "Send",
1364
1388
  pickPeer: "Select a peer to open the conversation",
1389
+ queued: "queued",
1390
+ open: "open",
1365
1391
  myIp: "my ip",
1366
1392
  copyAddr: "copy address",
1367
1393
  peersOnline: "peers online",
@@ -1437,6 +1463,8 @@ const STR = {
1437
1463
  message: "\u6D88\u606F",
1438
1464
  send: "\u53D1\u9001",
1439
1465
  pickPeer: "\u9009\u62E9\u4E00\u4F4D\u597D\u53CB\u5F00\u59CB\u4F1A\u8BDD",
1466
+ queued: "\u5F85\u53D1\u9001",
1467
+ open: "\u6253\u5F00",
1440
1468
  myIp: "\u6211\u7684 IP",
1441
1469
  copyAddr: "\u590D\u5236\u5730\u5740",
1442
1470
  peersOnline: "\u5728\u7EBF\u597D\u53CB",
@@ -1579,7 +1607,7 @@ function DkApp() {
1579
1607
  { id: "network", icon: "network", label: T.network },
1580
1608
  { id: "profile", icon: "userRound", label: T.profile }
1581
1609
  ];
1582
- return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "decentlan"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
1610
+ return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
1583
1611
  TweakRadio,
1584
1612
  {
1585
1613
  label: t.lang === "zh" ? "\u4E3B\u9898" : "Theme",
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>decentlan — Beagle Chat</title>
6
+ <title>Beagle</title>
7
7
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
8
8
  <style>
9
9
  * { box-sizing: border-box; }
package/dist/ui/server.js CHANGED
@@ -319,10 +319,28 @@ export function startFriendUi(opts) {
319
319
  mp3: "audio/mpeg", m4a: "audio/mp4", aac: "audio/aac", wav: "audio/wav",
320
320
  ogg: "audio/ogg", flac: "audio/flac", opus: "audio/opus",
321
321
  pdf: "application/pdf",
322
+ // Text / markup / code / config → served as UTF-8 text so a plain
323
+ // click opens (previews) them in the browser instead of forcing a
324
+ // download. `.md` and friends are the common case here.
325
+ md: "text/markdown", markdown: "text/markdown",
326
+ txt: "text/plain", text: "text/plain", log: "text/plain",
327
+ json: "application/json", csv: "text/csv", tsv: "text/tab-separated-values",
328
+ xml: "text/xml", yaml: "text/plain", yml: "text/plain",
329
+ ini: "text/plain", conf: "text/plain", cfg: "text/plain", toml: "text/plain",
330
+ html: "text/html", htm: "text/html", css: "text/css",
331
+ js: "text/plain", ts: "text/plain", jsx: "text/plain", tsx: "text/plain",
332
+ py: "text/plain", sh: "text/plain", srt: "text/plain", vtt: "text/vtt",
322
333
  };
323
334
  const ctype = MIME[ext] || "application/octet-stream";
324
335
  const isMedia = ctype.startsWith("image/") || ctype.startsWith("video/") || ctype.startsWith("audio/");
325
- const wantDownload = q.get("dl") === "1" || !isMedia;
336
+ const isText = ctype.startsWith("text/") || ctype === "application/json" || ctype === "application/pdf";
337
+ // Anything we can render inline (media, text, pdf) opens on a plain GET;
338
+ // `?dl=1` forces a save. Binary stays attachment-only.
339
+ const inlineViewable = isMedia || isText;
340
+ const wantDownload = q.get("dl") === "1" || !inlineViewable;
341
+ // Text needs an explicit charset so the browser renders UTF-8 (Chinese
342
+ // markdown etc.) correctly rather than as mojibake.
343
+ const ctypeHeader = ctype.startsWith("text/") ? `${ctype}; charset=utf-8` : ctype;
326
344
  // Content-Disposition must be ASCII — Node throws ERR_INVALID_CHAR on a
327
345
  // non-ASCII header value (filenames from macOS contain U+202F before
328
346
  // "PM", and others may be Chinese, etc.). Provide an ASCII-only
@@ -343,7 +361,7 @@ export function startFriendUi(opts) {
343
361
  return;
344
362
  }
345
363
  res.writeHead(206, {
346
- "content-type": ctype,
364
+ "content-type": ctypeHeader,
347
365
  "content-disposition": disposition,
348
366
  "accept-ranges": "bytes",
349
367
  "content-range": `bytes ${start}-${end}/${size}`,
@@ -353,7 +371,7 @@ export function startFriendUi(opts) {
353
371
  return;
354
372
  }
355
373
  res.writeHead(200, {
356
- "content-type": ctype,
374
+ "content-type": ctypeHeader,
357
375
  "content-disposition": disposition,
358
376
  "accept-ranges": "bytes",
359
377
  "content-length": String(size),
@@ -633,7 +651,7 @@ export function startFriendUi(opts) {
633
651
  const PAGE = `<!doctype html>
634
652
  <html lang="en"><head><meta charset="utf-8"/>
635
653
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
636
- <title>decentlan — friends</title>
654
+ <title>Beagle — friends</title>
637
655
  <style>
638
656
  :root { color-scheme: light dark; }
639
657
  body { font: 15px/1.5 -apple-system, system-ui, sans-serif; max-width: 760px; margin: 2rem auto; padding: 0 1rem; }
package/docs/INSTALL.md CHANGED
@@ -13,13 +13,27 @@ transparently between any two daemons that are Carrier friends.
13
13
  | | minimum |
14
14
  |---|---|
15
15
  | Node.js | **20+** |
16
- | OS | Linux (`x86_64`, `aarch64`) or macOS (`aarch64`, `x86_64`) |
16
+ | OS | Linux (`x86_64`, `aarch64`), macOS (`aarch64`, `x86_64`), or **Windows 10 / 11 via WSL2** ([Windows guide](INSTALL-WINDOWS.md)) |
17
17
  | Root / sudo | Yes — needed to create the TUN device. The daemon drops back to user-mode for everything else. |
18
18
  | Outbound network | TCP and UDP to public Carrier bootstrap nodes (ports 33445 + 443). No inbound port needed; no public IP needed. |
19
19
 
20
- Windows isn't supported yet; WSL2 with Linux mode works (UDP through
21
- WSL's NAT is unreliable, so the SDK falls back to TCP relay — works,
22
- just slower).
20
+ ### Windows
21
+
22
+ Windows **is** supported — but the daemon runs inside **WSL2** (Windows
23
+ Subsystem for Linux), a real Linux environment Windows installs for you, not
24
+ on Windows directly. Requirements:
25
+
26
+ - **Windows 11**, or **Windows 10 version 2004 or later** (build 19041+) —
27
+ this is what the one-command `wsl --install` needs.
28
+ - **Fast (direct UDP) path needs Windows 11, version 22H2 or later.** There you
29
+ turn on WSL **mirrored networking mode**, which lets the direct UDP
30
+ hole-punch succeed (~200 ms — smooth for live video). On **Windows 10**
31
+ mirrored mode doesn't exist, so WSL's NAT blocks inbound UDP and the SDK
32
+ falls back to the TCP relay: it still works, just slower (fine for SSH, chat,
33
+ and most browsing; live sports may stutter).
34
+
35
+ 👉 **Non-technical step-by-step for Windows users: [INSTALL-WINDOWS.md](INSTALL-WINDOWS.md)**
36
+ (install WSL → install Node.js → install decentlan, three steps).
23
37
 
24
38
  ## Install
25
39
 
@@ -52,7 +66,7 @@ Wait ~60–90s, then verify:
52
66
 
53
67
  ```bash
54
68
  agentnet ipam list # should show 6+ peers
55
- agentnet diag | grep tun # your allocated 10.86.1.x
69
+ agentnet diag | grep tun # your allocated 10.86.x.x
56
70
  ```
57
71
 
58
72
  > ### If `ipam list` shows only yourself after 2 minutes
@@ -63,7 +77,7 @@ agentnet diag | grep tun # your allocated 10.86.1.x
63
77
  >
64
78
  > 1. Re-send the friend-request directly through your live daemon:
65
79
  > ```bash
66
- > agentnet friend-request Jt7w1pKkyLT5GVue9h6ZPkjg1EeuuTbD6JVSLycXLsdm6nvBGSUd
80
+ > agentnet friend-request NsuN81dZdEoyvwEFgWaHkT8SPJB6UWeRmdYcCGFV5CdbbPXoK2RM
67
81
  > ```
68
82
  > Then wait 60s and re-check `agentnet ipam list`.
69
83
  >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.136",
3
+ "version": "0.1.138",
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.70",
82
+ "@decentnetwork/peer": "^0.1.71",
83
83
  "ink": "^5.2.1",
84
84
  "js-yaml": "^4.1.0",
85
85
  "react": "^18.3.1",