embedded.html 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Productivity</title>
  7. <script src="../script/jquery.min.js"></script>
  8. <script src="../script/ao_module.js"></script>
  9. <style>
  10. *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  11. html, body {
  12. width: 100%; height: 100%;
  13. overflow: hidden;
  14. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  15. background: #F2F2F7;
  16. color: #1C1C1E;
  17. }
  18. /* ── TOOLBAR ─────────────────────────────────────────── */
  19. #toolbar {
  20. position: fixed;
  21. top: 0; left: 0; right: 0;
  22. height: 44px;
  23. display: flex;
  24. align-items: center;
  25. padding: 0 8px;
  26. gap: 4px;
  27. background: rgba(242,242,247,0.94);
  28. backdrop-filter: blur(20px) saturate(1.8);
  29. -webkit-backdrop-filter: blur(20px) saturate(1.8);
  30. border-bottom: 1px solid rgba(0,0,0,0.1);
  31. z-index: 500;
  32. user-select: none;
  33. -webkit-user-select: none;
  34. }
  35. .tb-group { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
  36. .tb-btn {
  37. width: 30px; height: 30px;
  38. border: none; background: none;
  39. border-radius: 7px;
  40. cursor: pointer;
  41. display: flex; align-items: center; justify-content: center;
  42. color: #3C3C43;
  43. transition: background 0.1s;
  44. flex-shrink: 0;
  45. }
  46. .tb-btn:hover { background: rgba(0,0,0,0.07); }
  47. .tb-btn:active { background: rgba(0,0,0,0.13); }
  48. .tb-btn.active { background: rgba(0,122,255,0.12); color: #007AFF; }
  49. .tb-btn:disabled { opacity: 0.3; cursor: default; }
  50. .tb-btn:disabled:hover { background: none; }
  51. .tb-sep {
  52. width: 1px; height: 18px;
  53. background: rgba(0,0,0,0.14);
  54. flex-shrink: 0; margin: 0 5px;
  55. }
  56. #tbCenter {
  57. flex: 1;
  58. display: flex; align-items: center; justify-content: center;
  59. gap: 7px; min-width: 0;
  60. }
  61. #tbFileIcon { flex-shrink: 0; }
  62. #tbFileName {
  63. font-size: 13px; font-weight: 590;
  64. color: #1C1C1E;
  65. overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  66. }
  67. #tbZoomLabel {
  68. font-size: 11.5px; font-weight: 500;
  69. color: #6C6C70;
  70. min-width: 38px; text-align: center;
  71. font-variant-numeric: tabular-nums;
  72. }
  73. /* ── CONTENT ─────────────────────────────────────────── */
  74. #contentWrap {
  75. position: fixed;
  76. top: 44px; left: 0; right: 0; bottom: 0;
  77. }
  78. /* Empty state */
  79. #emptyState {
  80. width: 100%; height: 100%;
  81. display: flex; flex-direction: column;
  82. align-items: center; justify-content: center;
  83. gap: 12px; color: #8E8E93;
  84. }
  85. #emptyState svg { opacity: 0.45; }
  86. #emptyState p { font-size: 14px; font-weight: 500; }
  87. /* ── iframe viewer (HTML + PDF) ──────────────────────── */
  88. #iframeViewer {
  89. display: none;
  90. width: 100%; height: 100%;
  91. border: none;
  92. transform-origin: top left;
  93. }
  94. /* ── Image viewer ────────────────────────────────────── */
  95. #imageViewer {
  96. display: none;
  97. width: 100%; height: 100%;
  98. overflow: hidden;
  99. background: #1C1C1E;
  100. position: relative;
  101. }
  102. #imgStage {
  103. position: absolute;
  104. top: 0; left: 0;
  105. width: 100%; height: 100%;
  106. display: flex;
  107. align-items: center;
  108. justify-content: center;
  109. cursor: grab;
  110. }
  111. #imgStage.grabbing { cursor: grabbing; }
  112. #imgEl {
  113. display: block;
  114. max-width: 100%; max-height: 100%;
  115. object-fit: contain;
  116. transform-origin: center center;
  117. pointer-events: none;
  118. user-select: none;
  119. -webkit-user-drag: none;
  120. box-shadow: 0 12px 40px rgba(0,0,0,0.55);
  121. }
  122. #imgHint {
  123. position: absolute;
  124. bottom: 14px; left: 50%;
  125. transform: translateX(-50%);
  126. background: rgba(0,0,0,0.6);
  127. color: white; font-size: 12px;
  128. padding: 5px 14px; border-radius: 20px;
  129. backdrop-filter: blur(8px);
  130. pointer-events: none;
  131. opacity: 0;
  132. transition: opacity 0.35s;
  133. white-space: nowrap;
  134. }
  135. /* ── Code/text viewer ────────────────────────────────── */
  136. #codeViewer {
  137. display: none;
  138. width: 100%; height: 100%;
  139. overflow: auto;
  140. background: #FAFAFA;
  141. }
  142. #codeTable {
  143. border-collapse: collapse;
  144. min-width: 100%;
  145. font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, 'Courier New', monospace;
  146. font-size: 12.5px;
  147. line-height: 1.65;
  148. }
  149. .cl-num {
  150. padding: 0 12px 0 16px;
  151. text-align: right;
  152. color: #B0B0B0;
  153. border-right: 1px solid #E5E5EA;
  154. user-select: none;
  155. white-space: nowrap;
  156. vertical-align: top;
  157. background: #F5F5F7;
  158. min-width: 52px;
  159. }
  160. .cl-code {
  161. padding: 0 20px 0 14px;
  162. white-space: pre;
  163. vertical-align: top;
  164. color: #1C1C1E;
  165. }
  166. #codeTable tr:first-child .cl-num,
  167. #codeTable tr:first-child .cl-code { padding-top: 16px; }
  168. #codeTable tr:last-child .cl-num,
  169. #codeTable tr:last-child .cl-code { padding-bottom: 16px; }
  170. /* ── Markdown viewer ─────────────────────────────────── */
  171. #mdViewer {
  172. display: none;
  173. width: 100%; height: 100%;
  174. overflow: auto;
  175. background: white;
  176. }
  177. #mdBody {
  178. max-width: 820px;
  179. margin: 0 auto;
  180. padding: 44px 36px 72px;
  181. font-size: 15px;
  182. line-height: 1.78;
  183. color: #24292E;
  184. }
  185. #mdBody h1,#mdBody h2,#mdBody h3,#mdBody h4,#mdBody h5,#mdBody h6 {
  186. margin-top: 1.6em; margin-bottom: 0.55em;
  187. line-height: 1.3; font-weight: 600;
  188. }
  189. #mdBody h1 { font-size: 2em; padding-bottom: 0.3em; border-bottom: 1.5px solid #E5E7EB; }
  190. #mdBody h2 { font-size: 1.5em; padding-bottom: 0.2em; border-bottom: 1px solid #E5E7EB; }
  191. #mdBody h3 { font-size: 1.25em; }
  192. #mdBody h4 { font-size: 1em; }
  193. #mdBody p { margin: 0.75em 0; }
  194. #mdBody a { color: #0366D6; text-decoration: none; }
  195. #mdBody a:hover { text-decoration: underline; }
  196. #mdBody strong { font-weight: 600; }
  197. #mdBody em { font-style: italic; }
  198. #mdBody del { color: #6A737D; }
  199. #mdBody code {
  200. font-family: 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace;
  201. font-size: 0.875em;
  202. background: #F6F8FA; border: 1px solid #E1E4E8;
  203. border-radius: 4px; padding: 0.1em 0.4em;
  204. }
  205. #mdBody pre {
  206. background: #F6F8FA; border: 1px solid #E1E4E8;
  207. border-radius: 8px; padding: 16px;
  208. overflow-x: auto; margin: 1.1em 0;
  209. }
  210. #mdBody pre code { background: none; border: none; padding: 0; font-size: 0.875em; }
  211. #mdBody blockquote {
  212. border-left: 4px solid #DFE2E5;
  213. padding: 0.35em 0 0.35em 1em;
  214. color: #6A737D; margin: 1em 0;
  215. }
  216. #mdBody ul, #mdBody ol { padding-left: 2em; margin: 0.55em 0; }
  217. #mdBody li { margin: 0.25em 0; }
  218. #mdBody li > ul, #mdBody li > ol { margin: 0.15em 0; }
  219. #mdBody hr { border: none; border-top: 1.5px solid #E5E7EB; margin: 1.6em 0; }
  220. #mdBody img { max-width: 100%; border-radius: 6px; margin: 0.5em 0; }
  221. #mdBody table { border-collapse: collapse; width: 100%; margin: 1.1em 0; }
  222. #mdBody th, #mdBody td { border: 1px solid #DFE2E5; padding: 7px 14px; text-align: left; }
  223. #mdBody th { background: #F6F8FA; font-weight: 600; }
  224. #mdBody tr:nth-child(even) td { background: #FAFBFC; }
  225. /* ── Info sidebar ────────────────────────────────────── */
  226. #infoSidebar {
  227. position: fixed;
  228. top: 44px; right: 0;
  229. width: 260px; height: calc(100% - 44px);
  230. background: rgba(242,242,247,0.97);
  231. backdrop-filter: blur(20px);
  232. -webkit-backdrop-filter: blur(20px);
  233. border-left: 1px solid rgba(0,0,0,0.1);
  234. transform: translateX(100%);
  235. transition: transform 0.22s cubic-bezier(0.25,0.1,0.25,1);
  236. overflow-y: auto; padding: 16px; z-index: 400;
  237. }
  238. #infoSidebar.open { transform: translateX(0); }
  239. .info-hdr {
  240. display: flex; align-items: center;
  241. justify-content: space-between;
  242. margin-bottom: 18px;
  243. }
  244. .info-hdr strong { font-size: 14px; font-weight: 600; }
  245. .info-group { margin-bottom: 18px; }
  246. .info-group-title {
  247. font-size: 10.5px; font-weight: 700;
  248. letter-spacing: 0.7px; text-transform: uppercase;
  249. color: #8E8E93; margin-bottom: 8px;
  250. }
  251. .info-row { margin-bottom: 9px; }
  252. .info-key { font-size: 11px; color: #8E8E93; margin-bottom: 2px; }
  253. .info-val { font-size: 12.5px; color: #1C1C1E; word-break: break-all; line-height: 1.45; }
  254. /* ── Dark mode ───────────────────────────────────────── */
  255. @media (prefers-color-scheme: dark) {
  256. html, body { background: #1C1C1E; color: #F2F2F7; }
  257. #toolbar { background: rgba(28,28,30,0.94); border-bottom-color: rgba(255,255,255,0.1); }
  258. #tbFileName { color: #F2F2F7; }
  259. .tb-btn { color: #EBEBF5; }
  260. .tb-btn:hover { background: rgba(255,255,255,0.1); }
  261. .tb-btn:active { background: rgba(255,255,255,0.17); }
  262. .tb-btn.active { background: rgba(10,132,255,0.2); color: #0A84FF; }
  263. .tb-sep { background: rgba(255,255,255,0.14); }
  264. #tbZoomLabel { color: #8E8E93; }
  265. #emptyState { color: #48484A; }
  266. #codeViewer { background: #1C1C1E; }
  267. .cl-num { background: #2C2C2E; border-right-color: #3A3A3C; color: #48484A; }
  268. .cl-code { color: #E5E5EA; }
  269. #mdViewer { background: #1C1C1E; }
  270. #mdBody { color: #E5E5EA; }
  271. #mdBody h1,#mdBody h2 { border-bottom-color: #38383A; }
  272. #mdBody a { color: #4EA1F3; }
  273. #mdBody code { background: #2C2C2E; border-color: #38383A; }
  274. #mdBody pre { background: #2C2C2E; border-color: #38383A; }
  275. #mdBody blockquote { border-left-color: #38383A; color: #8E8E93; }
  276. #mdBody th, #mdBody td { border-color: #38383A; }
  277. #mdBody th { background: #2C2C2E; }
  278. #mdBody tr:nth-child(even) td { background: #252527; }
  279. #mdBody hr { border-top-color: #38383A; }
  280. #infoSidebar { background: rgba(28,28,30,0.97); border-left-color: rgba(255,255,255,0.1); }
  281. .info-val { color: #E5E5EA; }
  282. }
  283. /* ArozOS theme override — applied by ao_module_onThemeChanged via data-theme attribute.
  284. These rules win over the media query because the attribute selector adds specificity. */
  285. html[data-theme="dark"] body { background: #1C1C1E; color: #F2F2F7; }
  286. html[data-theme="dark"] #toolbar { background: rgba(28,28,30,0.94); border-bottom-color: rgba(255,255,255,0.1); }
  287. html[data-theme="dark"] #tbFileName { color: #F2F2F7; }
  288. html[data-theme="dark"] .tb-btn { color: #EBEBF5; }
  289. html[data-theme="dark"] .tb-btn:hover { background: rgba(255,255,255,0.1); }
  290. html[data-theme="dark"] .tb-btn:active { background: rgba(255,255,255,0.17); }
  291. html[data-theme="dark"] .tb-btn.active { background: rgba(10,132,255,0.2); color: #0A84FF; }
  292. html[data-theme="dark"] .tb-sep { background: rgba(255,255,255,0.14); }
  293. html[data-theme="dark"] #tbZoomLabel { color: #8E8E93; }
  294. html[data-theme="dark"] #emptyState { color: #48484A; }
  295. html[data-theme="dark"] #codeViewer { background: #1C1C1E; }
  296. html[data-theme="dark"] .cl-num { background: #2C2C2E; border-right-color: #3A3A3C; color: #48484A; }
  297. html[data-theme="dark"] .cl-code { color: #E5E5EA; }
  298. html[data-theme="dark"] #mdViewer { background: #1C1C1E; }
  299. html[data-theme="dark"] #mdBody { color: #E5E5EA; }
  300. html[data-theme="dark"] #mdBody h1, html[data-theme="dark"] #mdBody h2 { border-bottom-color: #38383A; }
  301. html[data-theme="dark"] #mdBody a { color: #4EA1F3; }
  302. html[data-theme="dark"] #mdBody code { background: #2C2C2E; border-color: #38383A; }
  303. html[data-theme="dark"] #mdBody pre { background: #2C2C2E; border-color: #38383A; }
  304. html[data-theme="dark"] #mdBody blockquote { border-left-color: #38383A; color: #8E8E93; }
  305. html[data-theme="dark"] #mdBody th, html[data-theme="dark"] #mdBody td { border-color: #38383A; }
  306. html[data-theme="dark"] #mdBody th { background: #2C2C2E; }
  307. html[data-theme="dark"] #mdBody tr:nth-child(even) td { background: #252527; }
  308. html[data-theme="dark"] #mdBody hr { border-top-color: #38383A; }
  309. html[data-theme="dark"] #infoSidebar { background: rgba(28,28,30,0.97); border-left-color: rgba(255,255,255,0.1); }
  310. html[data-theme="dark"] .info-val { color: #E5E5EA; }
  311. /* Force light mode even when the OS prefers dark */
  312. html[data-theme="light"] body { background: #F2F2F7; color: #1C1C1E; }
  313. html[data-theme="light"] #toolbar { background: rgba(242,242,247,0.94); border-bottom-color: rgba(0,0,0,0.1); }
  314. html[data-theme="light"] #tbFileName { color: #1C1C1E; }
  315. html[data-theme="light"] .tb-btn { color: #3C3C43; }
  316. html[data-theme="light"] .tb-btn:hover { background: rgba(0,0,0,0.07); }
  317. html[data-theme="light"] .tb-btn:active { background: rgba(0,0,0,0.13); }
  318. html[data-theme="light"] .tb-btn.active { background: rgba(0,122,255,0.12); color: #007AFF; }
  319. html[data-theme="light"] .tb-sep { background: rgba(0,0,0,0.14); }
  320. html[data-theme="light"] #tbZoomLabel { color: #6C6C70; }
  321. html[data-theme="light"] #emptyState { color: #8E8E93; }
  322. html[data-theme="light"] #codeViewer { background: #FFFFFF; }
  323. html[data-theme="light"] .cl-num { background: #F2F2F7; border-right-color: #D1D1D6; color: #8E8E93; }
  324. html[data-theme="light"] .cl-code { color: #1C1C1E; }
  325. html[data-theme="light"] #mdViewer { background: #FFFFFF; }
  326. html[data-theme="light"] #mdBody { color: #1C1C1E; }
  327. html[data-theme="light"] #infoSidebar { background: rgba(242,242,247,0.97); border-left-color: rgba(0,0,0,0.1); }
  328. html[data-theme="light"] .info-val { color: #1C1C1E; }
  329. </style>
  330. </head>
  331. <body>
  332. <!-- ═══════════════════════════════════════════════════════════
  333. TOOLBAR
  334. ══════════════════════════════════════════════════════════════ -->
  335. <div id="toolbar">
  336. <!-- Left: file type badge -->
  337. <div class="tb-group">
  338. <div id="tbFileIcon" style="width:28px;height:28px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;letter-spacing:0.3px;color:white;background:#8E8E93;flex-shrink:0;">
  339. FILE
  340. </div>
  341. </div>
  342. <!-- Center: file name -->
  343. <div id="tbCenter">
  344. <span id="tbFileName">Preview</span>
  345. </div>
  346. <!-- Right: zoom + info -->
  347. <div class="tb-group">
  348. <div class="tb-sep"></div>
  349. <!-- Zoom out -->
  350. <button class="tb-btn" id="btnZoomOut" onclick="adjustZoom(-0.15)" title="Zoom Out (Ctrl –)" disabled>
  351. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
  352. <circle cx="6.5" cy="6.5" r="4.5"/>
  353. <line x1="4" y1="6.5" x2="9" y2="6.5"/>
  354. <line x1="10.2" y1="10.2" x2="14" y2="14"/>
  355. </svg>
  356. </button>
  357. <span id="tbZoomLabel">—</span>
  358. <!-- Zoom in -->
  359. <button class="tb-btn" id="btnZoomIn" onclick="adjustZoom(0.15)" title="Zoom In (Ctrl +)" disabled>
  360. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
  361. <circle cx="6.5" cy="6.5" r="4.5"/>
  362. <line x1="4" y1="6.5" x2="9" y2="6.5"/>
  363. <line x1="6.5" y1="4" x2="6.5" y2="9"/>
  364. <line x1="10.2" y1="10.2" x2="14" y2="14"/>
  365. </svg>
  366. </button>
  367. <!-- Reset zoom -->
  368. <button class="tb-btn" id="btnZoomReset" onclick="resetZoom()" title="Reset Zoom (Ctrl 0)" disabled>
  369. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
  370. <path d="M2.5 8 A5.5 5.5 0 1 1 8 13.5"/>
  371. <polyline points="2.5,5.5 2.5,8 5,8"/>
  372. </svg>
  373. </button>
  374. <!-- Open in Web Builder (HTML files only) -->
  375. <button class="tb-btn" id="btnWebBuilder" onclick="openInWebBuilder()" title="Open in Web Builder" style="display:none">
  376. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
  377. <rect x="1" y="2" width="14" height="10" rx="1.3"/>
  378. <line x1="1" y1="5.5" x2="15" y2="5.5"/>
  379. <circle cx="3" cy="3.8" r="0.6" fill="currentColor" stroke="none"/>
  380. <circle cx="5" cy="3.8" r="0.6" fill="currentColor" stroke="none"/>
  381. <path d="M5.5 8.5 L4 10 L5.5 11.5"/>
  382. <path d="M9.5 8.5 L11 10 L9.5 11.5"/>
  383. </svg>
  384. </button>
  385. <div class="tb-sep" id="sepWebBuilder" style="display:none"></div>
  386. <div class="tb-sep"></div>
  387. <!-- Info toggle -->
  388. <button class="tb-btn" id="btnInfo" onclick="toggleInfo()" title="File Info (Ctrl I)">
  389. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
  390. <circle cx="8" cy="8" r="6.5"/>
  391. <line x1="8" y1="7.5" x2="8" y2="11.5"/>
  392. <circle cx="8" cy="5" r="0.8" fill="currentColor" stroke="none"/>
  393. </svg>
  394. </button>
  395. </div>
  396. </div>
  397. <!-- ═══════════════════════════════════════════════════════════
  398. CONTENT
  399. ══════════════════════════════════════════════════════════════ -->
  400. <div id="contentWrap">
  401. <!-- Empty / error state -->
  402. <div id="emptyState">
  403. <svg width="64" height="64" viewBox="0 0 64 64" fill="none">
  404. <rect x="10" y="5" width="36" height="46" rx="5" fill="currentColor" opacity="0.2"/>
  405. <path d="M36 5 L46 15 L36 15 Z" fill="currentColor" opacity="0.25"/>
  406. <rect x="15" y="24" width="26" height="3" rx="1.5" fill="currentColor" opacity="0.4"/>
  407. <rect x="15" y="30" width="20" height="3" rx="1.5" fill="currentColor" opacity="0.3"/>
  408. <rect x="15" y="36" width="23" height="3" rx="1.5" fill="currentColor" opacity="0.3"/>
  409. <circle cx="44" cy="44" r="14" stroke="currentColor" stroke-width="3" opacity="0.35"/>
  410. <line x1="53.5" y1="53.5" x2="61" y2="61" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
  411. </svg>
  412. <p id="emptyMsg">No file to preview</p>
  413. </div>
  414. <!-- iframe viewer: HTML and PDF -->
  415. <iframe id="iframeViewer" title="File Preview" allowfullscreen></iframe>
  416. <!-- Image viewer -->
  417. <div id="imageViewer">
  418. <div id="imgStage">
  419. <img id="imgEl" alt="Preview image">
  420. </div>
  421. <div id="imgHint"></div>
  422. </div>
  423. <!-- Code / text viewer -->
  424. <div id="codeViewer">
  425. <table id="codeTable"><tbody id="codeTbody"></tbody></table>
  426. </div>
  427. <!-- Markdown viewer -->
  428. <div id="mdViewer">
  429. <div id="mdBody"></div>
  430. </div>
  431. </div>
  432. <!-- ═══════════════════════════════════════════════════════════
  433. INFO SIDEBAR
  434. ══════════════════════════════════════════════════════════════ -->
  435. <div id="infoSidebar">
  436. <div class="info-hdr">
  437. <strong>File Info</strong>
  438. <button class="tb-btn" onclick="toggleInfo()" style="color:#8E8E93">
  439. <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
  440. <line x1="1" y1="1" x2="13" y2="13"/>
  441. <line x1="13" y1="1" x2="1" y2="13"/>
  442. </svg>
  443. </button>
  444. </div>
  445. <div class="info-group">
  446. <div class="info-group-title">General</div>
  447. <div class="info-row"><div class="info-key">Name</div><div class="info-val" id="infoName">—</div></div>
  448. <div class="info-row"><div class="info-key">Type</div><div class="info-val" id="infoType">—</div></div>
  449. <div class="info-row"><div class="info-key">Extension</div><div class="info-val" id="infoExt">—</div></div>
  450. </div>
  451. <div class="info-group">
  452. <div class="info-group-title">Location</div>
  453. <div class="info-row"><div class="info-key">Path</div><div class="info-val" id="infoPath">—</div></div>
  454. </div>
  455. </div>
  456. <script>
  457. /* ═══════════════════════════════════════════════════════════════
  458. FILE TYPE REGISTRY
  459. ═══════════════════════════════════════════════════════════════ */
  460. var TYPE_MAP = {
  461. html: ['html','htm'],
  462. pdf: ['pdf'],
  463. image:['jpg','jpeg','png','gif','webp','svg','bmp','ico'],
  464. markdown: ['md','markdown'],
  465. code: [
  466. 'js','mjs','cjs','jsx','ts','tsx',
  467. 'py','rb','pl','r','go','rs','swift','kt','scala',
  468. 'java','c','h','cpp','hpp','cs','php',
  469. 'sh','bash','zsh','bat','cmd','ps1',
  470. 'json','yaml','yml','xml','css','scss','less',
  471. 'sql','lua','agi','dockerfile','makefile',
  472. 'gitignore','editorconfig','env',
  473. 'toml','ini','conf','cfg','log','txt'
  474. ]
  475. };
  476. var TYPE_LABELS = {
  477. html:'HTML', pdf:'PDF', image:'IMG',
  478. markdown:'MD', code:'CODE'
  479. };
  480. var TYPE_COLORS = {
  481. html: '#E44D26',
  482. pdf: '#FF3B30',
  483. image:'#30B06E',
  484. markdown: '#8B5CF6',
  485. code: '#007AFF'
  486. };
  487. var TYPE_NAMES = {
  488. html:'HTML Document', pdf:'PDF Document', image:'Image',
  489. markdown:'Markdown Document', code:'Source File'
  490. };
  491. function getFileType(ext) {
  492. ext = ext.toLowerCase().replace(/^\./, '');
  493. for (var t in TYPE_MAP) {
  494. if (TYPE_MAP[t].indexOf(ext) !== -1) return t;
  495. }
  496. return 'code';
  497. }
  498. /* ═══════════════════════════════════════════════════════════════
  499. STATE
  500. ═══════════════════════════════════════════════════════════════ */
  501. var currentFile = null;
  502. var currentType = null;
  503. var zoom = 1.0;
  504. // Image pan state
  505. var imgPan = { dragging: false, startX: 0, startY: 0, tx: 0, ty: 0, baseTx: 0, baseTy: 0 };
  506. /* ═══════════════════════════════════════════════════════════════
  507. HELPERS
  508. ═══════════════════════════════════════════════════════════════ */
  509. function escHtml(s) {
  510. return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
  511. }
  512. function showHint(msg) {
  513. var h = document.getElementById('imgHint');
  514. h.textContent = msg;
  515. h.style.opacity = '1';
  516. clearTimeout(h._tid);
  517. h._tid = setTimeout(function(){ h.style.opacity = '0'; }, 1600);
  518. }
  519. function setZoomEnabled(on) {
  520. document.getElementById('btnZoomOut').disabled = !on;
  521. document.getElementById('btnZoomIn').disabled = !on;
  522. document.getElementById('btnZoomReset').disabled = !on;
  523. }
  524. /* ═══════════════════════════════════════════════════════════════
  525. VIEWER SWITCHING
  526. ═══════════════════════════════════════════════════════════════ */
  527. function showViewer(type) {
  528. document.getElementById('emptyState').style.display = 'none';
  529. document.getElementById('iframeViewer').style.display = 'none';
  530. document.getElementById('imageViewer').style.display = 'none';
  531. document.getElementById('codeViewer').style.display = 'none';
  532. document.getElementById('mdViewer').style.display = 'none';
  533. if (type === 'html' || type === 'pdf') document.getElementById('iframeViewer').style.display = 'block';
  534. else if (type === 'image') document.getElementById('imageViewer').style.display = 'block';
  535. else if (type === 'code') document.getElementById('codeViewer').style.display = 'block';
  536. else if (type === 'markdown') document.getElementById('mdViewer').style.display = 'block';
  537. }
  538. /* ═══════════════════════════════════════════════════════════════
  539. ZOOM
  540. ═══════════════════════════════════════════════════════════════ */
  541. function adjustZoom(delta) {
  542. if (!currentType) return;
  543. zoom = Math.round(Math.max(0.1, Math.min(8.0, zoom + delta)) * 100) / 100;
  544. applyZoom();
  545. updateZoomLabel();
  546. }
  547. function resetZoom() {
  548. zoom = 1.0;
  549. if (currentType === 'image') {
  550. imgPan.tx = 0; imgPan.ty = 0;
  551. imgPan.baseTx = 0; imgPan.baseTy = 0;
  552. }
  553. applyZoom();
  554. updateZoomLabel();
  555. }
  556. function applyZoom() {
  557. if (currentType === 'image') {
  558. document.getElementById('imgEl').style.transform =
  559. 'translate(' + imgPan.tx + 'px,' + imgPan.ty + 'px) scale(' + zoom + ')';
  560. } else if (currentType === 'code') {
  561. document.getElementById('codeTable').style.fontSize = (12.5 * zoom) + 'px';
  562. } else if (currentType === 'markdown') {
  563. document.getElementById('mdBody').style.fontSize = (15 * zoom) + 'px';
  564. } else if (currentType === 'html') {
  565. /* CSS zoom works in Chromium/WebKit; harmless elsewhere */
  566. document.getElementById('iframeViewer').style.zoom = zoom;
  567. }
  568. }
  569. function updateZoomLabel() {
  570. document.getElementById('tbZoomLabel').textContent = Math.round(zoom * 100) + '%';
  571. }
  572. /* ═══════════════════════════════════════════════════════════════
  573. IMAGE PAN
  574. ═══════════════════════════════════════════════════════════════ */
  575. function initImagePan() {
  576. var stage = document.getElementById('imgStage');
  577. stage.addEventListener('mousedown', function(e) {
  578. if (zoom <= 1.0) return;
  579. e.preventDefault();
  580. imgPan.dragging = true;
  581. imgPan.startX = e.clientX;
  582. imgPan.startY = e.clientY;
  583. imgPan.baseTx = imgPan.tx;
  584. imgPan.baseTy = imgPan.ty;
  585. stage.classList.add('grabbing');
  586. });
  587. window.addEventListener('mousemove', function(e) {
  588. if (!imgPan.dragging) return;
  589. imgPan.tx = imgPan.baseTx + (e.clientX - imgPan.startX);
  590. imgPan.ty = imgPan.baseTy + (e.clientY - imgPan.startY);
  591. applyZoom();
  592. });
  593. window.addEventListener('mouseup', function() {
  594. if (!imgPan.dragging) return;
  595. imgPan.dragging = false;
  596. stage.classList.remove('grabbing');
  597. });
  598. /* touch pan */
  599. stage.addEventListener('touchstart', function(e) {
  600. if (e.touches.length === 1 && zoom > 1.0) {
  601. imgPan.dragging = true;
  602. imgPan.startX = e.touches[0].clientX;
  603. imgPan.startY = e.touches[0].clientY;
  604. imgPan.baseTx = imgPan.tx;
  605. imgPan.baseTy = imgPan.ty;
  606. }
  607. }, {passive: true});
  608. stage.addEventListener('touchmove', function(e) {
  609. if (!imgPan.dragging || e.touches.length !== 1) return;
  610. imgPan.tx = imgPan.baseTx + (e.touches[0].clientX - imgPan.startX);
  611. imgPan.ty = imgPan.baseTy + (e.touches[0].clientY - imgPan.startY);
  612. applyZoom();
  613. }, {passive: true});
  614. stage.addEventListener('touchend', function() { imgPan.dragging = false; });
  615. }
  616. /* ═══════════════════════════════════════════════════════════════
  617. MARKDOWN RENDERER
  618. ═══════════════════════════════════════════════════════════════ */
  619. function renderMarkdown(raw) {
  620. var codeBlocks = [];
  621. var inlineCodes = [];
  622. /* 1. Extract fenced code blocks */
  623. raw = raw.replace(/```([^\n]*)\n([\s\S]*?)```/gm, function(_, lang, code) {
  624. var i = codeBlocks.length;
  625. codeBlocks.push('<pre><code>' + escHtml(code.replace(/\n$/, '')) + '</code></pre>');
  626. return '\x01CB' + i + '\x01';
  627. });
  628. /* 2. Extract inline code */
  629. raw = raw.replace(/`([^`]+)`/g, function(_, code) {
  630. var i = inlineCodes.length;
  631. inlineCodes.push('<code>' + escHtml(code) + '</code>');
  632. return '\x01IC' + i + '\x01';
  633. });
  634. /* 3. Escape remaining HTML */
  635. raw = raw.replace(/&(?![a-zA-Z#]\w*;)/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  636. /* 4. Block-level elements */
  637. var lines = raw.split('\n');
  638. var out = [];
  639. var i = 0;
  640. while (i < lines.length) {
  641. var line = lines[i];
  642. /* Headings */
  643. var hm = line.match(/^(#{1,6})\s+(.*)/);
  644. if (hm) {
  645. var lvl = hm[1].length;
  646. out.push('<h' + lvl + '>' + inlineMarkdown(hm[2]) + '</h' + lvl + '>');
  647. i++; continue;
  648. }
  649. /* Horizontal rule */
  650. if (/^(\*{3,}|-{3,}|_{3,})\s*$/.test(line)) {
  651. out.push('<hr>'); i++; continue;
  652. }
  653. /* Blockquote */
  654. if (line.startsWith('&gt;')) {
  655. var qlines = [];
  656. while (i < lines.length && lines[i].startsWith('&gt;')) {
  657. qlines.push(lines[i].replace(/^&gt;\s?/, ''));
  658. i++;
  659. }
  660. out.push('<blockquote>' + inlineMarkdown(qlines.join('\n')) + '</blockquote>');
  661. continue;
  662. }
  663. /* Unordered list */
  664. if (/^[\*\-]\s/.test(line)) {
  665. var items = [];
  666. while (i < lines.length && /^[\*\-]\s/.test(lines[i])) {
  667. items.push('<li>' + inlineMarkdown(lines[i].replace(/^[\*\-]\s/, '')) + '</li>');
  668. i++;
  669. }
  670. out.push('<ul>' + items.join('') + '</ul>');
  671. continue;
  672. }
  673. /* Ordered list */
  674. if (/^\d+\.\s/.test(line)) {
  675. var oitems = [];
  676. while (i < lines.length && /^\d+\.\s/.test(lines[i])) {
  677. oitems.push('<li>' + inlineMarkdown(lines[i].replace(/^\d+\.\s/, '')) + '</li>');
  678. i++;
  679. }
  680. out.push('<ol>' + oitems.join('') + '</ol>');
  681. continue;
  682. }
  683. /* Table (pipe-separated) */
  684. if (line.includes('|') && i + 1 < lines.length && /^\|?[\s\-:|]+\|/.test(lines[i+1])) {
  685. var trows = [];
  686. var headers = line.split('|').filter(function(c,idx,arr){ return idx > 0 || c.trim(); }).map(function(c){ return c.trim(); }).filter(Boolean);
  687. trows.push('<thead><tr>' + headers.map(function(h){ return '<th>' + inlineMarkdown(h) + '</th>'; }).join('') + '</tr></thead>');
  688. i += 2; /* skip separator */
  689. var tbody = [];
  690. while (i < lines.length && lines[i].includes('|')) {
  691. var cells = lines[i].split('|').filter(function(c,idx,arr){ return idx > 0 || c.trim(); }).map(function(c){ return c.trim(); }).filter(Boolean);
  692. tbody.push('<tr>' + cells.map(function(c){ return '<td>' + inlineMarkdown(c) + '</td>'; }).join('') + '</tr>');
  693. i++;
  694. }
  695. out.push('<table><' + trows[0] + '<tbody>' + tbody.join('') + '</tbody></table>');
  696. continue;
  697. }
  698. /* Blank line */
  699. if (line.trim() === '') { out.push(''); i++; continue; }
  700. /* Paragraph / inline text */
  701. var para = [];
  702. while (i < lines.length && lines[i].trim() !== '' &&
  703. !/^(#{1,6}\s|[\*\-]\s|\d+\.\s|&gt;|(\*{3,}|-{3,}|_{3,})\s*$)/.test(lines[i]) &&
  704. !lines[i].includes('\x01CB')) {
  705. para.push(lines[i]);
  706. i++;
  707. }
  708. if (para.length) out.push('<p>' + inlineMarkdown(para.join(' ')) + '</p>');
  709. }
  710. var html = out.join('\n');
  711. /* Restore inline codes and code blocks */
  712. inlineCodes.forEach(function(c, idx) { html = html.split('\x01IC' + idx + '\x01').join(c); });
  713. codeBlocks.forEach(function(b, idx) { html = html.split('\x01CB' + idx + '\x01').join(b); });
  714. return html;
  715. }
  716. function inlineMarkdown(s) {
  717. /* images before links */
  718. s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img alt="$1" src="$2">');
  719. s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
  720. s = s.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
  721. s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
  722. s = s.replace(/__(.+?)__/g, '<strong>$1</strong>');
  723. s = s.replace(/\*(.+?)\*/g, '<em>$1</em>');
  724. s = s.replace(/_(.+?)_/g, '<em>$1</em>');
  725. s = s.replace(/~~(.+?)~~/g, '<del>$1</del>');
  726. /* restore inline code placeholders pass-through */
  727. return s;
  728. }
  729. /* ═══════════════════════════════════════════════════════════════
  730. LOAD FILE
  731. ═══════════════════════════════════════════════════════════════ */
  732. function loadFile(file) {
  733. currentFile = file;
  734. var ext = file.filename.split('.').pop().toLowerCase();
  735. currentType = getFileType(ext);
  736. /* Toolbar badge */
  737. var badge = document.getElementById('tbFileIcon');
  738. badge.textContent = TYPE_LABELS[currentType] || 'FILE';
  739. badge.style.background = TYPE_COLORS[currentType] || '#8E8E93';
  740. badge.style.fontSize = currentType === 'markdown' ? '10px' : '11px';
  741. document.getElementById('tbFileName').textContent = file.filename;
  742. ao_module_setWindowTitle('Preview — ' + file.filename);
  743. /* Info sidebar */
  744. document.getElementById('infoName').textContent = file.filename;
  745. document.getElementById('infoPath').textContent = file.filepath;
  746. document.getElementById('infoExt').textContent = '.' + ext.toUpperCase();
  747. document.getElementById('infoType').textContent = TYPE_NAMES[currentType] || 'File';
  748. /* Reset zoom */
  749. zoom = 1.0;
  750. imgPan.tx = 0; imgPan.ty = 0; imgPan.baseTx = 0; imgPan.baseTy = 0;
  751. updateZoomLabel();
  752. showViewer(currentType);
  753. /* Web Builder button — only available for HTML files */
  754. var _wbVisible = currentType === 'html';
  755. document.getElementById('btnWebBuilder').style.display = _wbVisible ? '' : 'none';
  756. document.getElementById('sepWebBuilder').style.display = _wbVisible ? '' : 'none';
  757. /* Support data URLs for files dragged in from the OS desktop */
  758. var fileUrl = file.dataUrl
  759. ? file.dataUrl
  760. : '../media?file=' + encodeURIComponent(file.filepath);
  761. if (currentType === 'html') {
  762. setZoomEnabled(true);
  763. var fr = document.getElementById('iframeViewer');
  764. fr.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups');
  765. if (file.dataUrl) {
  766. /* Use srcdoc so the sandboxed iframe renders the HTML directly */
  767. fetch(file.dataUrl).then(function(r){ return r.text(); }).then(function(html){
  768. fr.removeAttribute('src');
  769. fr.srcdoc = html;
  770. });
  771. } else {
  772. fr.removeAttribute('srcdoc');
  773. fr.src = fileUrl;
  774. }
  775. } else if (currentType === 'pdf') {
  776. setZoomEnabled(false);
  777. var fr = document.getElementById('iframeViewer');
  778. fr.removeAttribute('sandbox');
  779. fr.removeAttribute('srcdoc');
  780. fr.src = fileUrl;
  781. } else if (currentType === 'image') {
  782. setZoomEnabled(true);
  783. document.getElementById('imgEl').src = fileUrl;
  784. } else if (currentType === 'code') {
  785. setZoomEnabled(true);
  786. fetchAndRenderCode(fileUrl);
  787. } else if (currentType === 'markdown') {
  788. setZoomEnabled(true);
  789. fetchAndRenderMarkdown(fileUrl);
  790. }
  791. }
  792. function fetchAndRenderCode(url) {
  793. var tbody = document.getElementById('codeTbody');
  794. tbody.innerHTML = '<tr><td class="cl-num">…</td><td class="cl-code" style="color:#8E8E93">Loading…</td></tr>';
  795. fetch(url)
  796. .then(function(r) {
  797. if (!r.ok) throw new Error('HTTP ' + r.status);
  798. return r.text();
  799. })
  800. .then(function(text) {
  801. var lines = text.split('\n');
  802. /* Remove trailing empty line that split() adds */
  803. if (lines.length > 1 && lines[lines.length - 1] === '') lines.pop();
  804. var rows = '';
  805. for (var i = 0; i < lines.length; i++) {
  806. rows += '<tr><td class="cl-num">' + (i + 1) + '</td>'
  807. + '<td class="cl-code">' + escHtml(lines[i]) + '</td></tr>';
  808. }
  809. tbody.innerHTML = rows;
  810. })
  811. .catch(function(e) {
  812. tbody.innerHTML = '<tr><td class="cl-num">!</td><td class="cl-code" style="color:#FF3B30">Failed to load file: ' + escHtml(e.message) + '</td></tr>';
  813. });
  814. }
  815. function fetchAndRenderMarkdown(url) {
  816. var body = document.getElementById('mdBody');
  817. body.innerHTML = '<p style="color:#8E8E93">Loading…</p>';
  818. fetch(url)
  819. .then(function(r) {
  820. if (!r.ok) throw new Error('HTTP ' + r.status);
  821. return r.text();
  822. })
  823. .then(function(text) {
  824. body.innerHTML = renderMarkdown(text);
  825. })
  826. .catch(function(e) {
  827. body.innerHTML = '<p style="color:#FF3B30">Failed to load file: ' + escHtml(e.message) + '</p>';
  828. });
  829. }
  830. /* ═══════════════════════════════════════════════════════════════
  831. INFO SIDEBAR
  832. ═══════════════════════════════════════════════════════════════ */
  833. function toggleInfo() {
  834. var sb = document.getElementById('infoSidebar');
  835. sb.classList.toggle('open');
  836. document.getElementById('btnInfo').classList.toggle('active', sb.classList.contains('open'));
  837. }
  838. function openInWebBuilder() {
  839. if (!currentFile) return;
  840. var hashData = {
  841. tool: 'webbuilder',
  842. file: { filename: currentFile.filename, filepath: currentFile.filepath }
  843. };
  844. ao_module_newfw({
  845. url: 'Productivity/index.html#' + encodeURIComponent(JSON.stringify(hashData)),
  846. width: 1080,
  847. height: 580,
  848. title: currentFile.filename + ' - Web Builder',
  849. appicon: 'Productivity/img/module_icon.svg'
  850. });
  851. }
  852. /* ═══════════════════════════════════════════════════════════════
  853. EVENT WIRING
  854. ═══════════════════════════════════════════════════════════════ */
  855. /* Ctrl/Cmd +/- zoom */
  856. document.addEventListener('keydown', function(e) {
  857. var ctrl = e.ctrlKey || e.metaKey;
  858. if (!ctrl) return;
  859. if (e.key === '=' || e.key === '+') { e.preventDefault(); adjustZoom(0.15); }
  860. else if (e.key === '-') { e.preventDefault(); adjustZoom(-0.15); }
  861. else if (e.key === '0') { e.preventDefault(); resetZoom(); }
  862. else if (e.key === 'i' || e.key === 'I') { e.preventDefault(); toggleInfo(); }
  863. });
  864. /* Scroll-wheel zoom on image viewer */
  865. document.getElementById('imageViewer').addEventListener('wheel', function(e) {
  866. e.preventDefault();
  867. var delta = e.deltaY < 0 ? 0.12 : -0.12;
  868. adjustZoom(delta);
  869. showHint(Math.round(zoom * 100) + '%');
  870. }, { passive: false });
  871. /* Ctrl+scroll zoom on code viewer */
  872. document.getElementById('codeViewer').addEventListener('wheel', function(e) {
  873. if (!(e.ctrlKey || e.metaKey)) return;
  874. e.preventDefault();
  875. adjustZoom(e.deltaY < 0 ? 0.12 : -0.12);
  876. }, { passive: false });
  877. /* Ctrl+scroll zoom on markdown viewer */
  878. document.getElementById('mdViewer').addEventListener('wheel', function(e) {
  879. if (!(e.ctrlKey || e.metaKey)) return;
  880. e.preventDefault();
  881. adjustZoom(e.deltaY < 0 ? 0.12 : -0.12);
  882. }, { passive: false });
  883. /* ═══════════════════════════════════════════════════════════════
  884. INIT
  885. ═══════════════════════════════════════════════════════════════ */
  886. initImagePan();
  887. var inputFiles = ao_module_loadInputFiles();
  888. if (inputFiles && inputFiles.length > 0) {
  889. loadFile(inputFiles[0]);
  890. } else {
  891. document.getElementById('emptyMsg').textContent = 'No file was passed to Preview';
  892. document.getElementById('emptyState').style.display = 'flex';
  893. }
  894. /* ── ArozOS system theme binding ── */
  895. function applyAozTheme(theme) {
  896. document.documentElement.setAttribute('data-theme', theme === 'dark' ? 'dark' : 'light');
  897. }
  898. ao_module_onThemeChanged(applyAozTheme);
  899. $.get(ao_root + 'system/file_system/preference?key=file_explorer/theme', function(data) {
  900. applyAozTheme(data === 'darkTheme' ? 'dark' : 'light');
  901. });
  902. </script>
  903. </body>
  904. </html>