App.svelte 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. <script>
  2. import { writable } from 'svelte/store'
  3. import { open } from '@tauri-apps/api/shell'
  4. import { appWindow, getCurrent } from '@tauri-apps/api/window'
  5. import * as os from '@tauri-apps/api/os'
  6. import Welcome from './views/Welcome.svelte'
  7. import Cli from './views/Cli.svelte'
  8. import Communication from './views/Communication.svelte'
  9. import Http from './views/Http.svelte'
  10. import Notifications from './views/Notifications.svelte'
  11. import Window from './views/Window.svelte'
  12. import Shortcuts from './views/Shortcuts.svelte'
  13. import Shell from './views/Shell.svelte'
  14. import Updater from './views/Updater.svelte'
  15. import WebRTC from './views/WebRTC.svelte'
  16. import App from './views/App.svelte'
  17. import { onMount } from 'svelte'
  18. import { listen } from '@tauri-apps/api/event'
  19. appWindow.onFileDropEvent((event) => {
  20. onMessage(`File drop: ${JSON.stringify(event.payload)}`)
  21. })
  22. const userAgent = navigator.userAgent.toLowerCase()
  23. const isMobile = userAgent.includes('android') || userAgent.includes('iphone')
  24. const views = [
  25. {
  26. label: 'Welcome',
  27. component: Welcome,
  28. icon: 'i-ph-hand-waving'
  29. },
  30. {
  31. label: 'Communication',
  32. component: Communication,
  33. icon: 'i-codicon-radio-tower'
  34. },
  35. !isMobile && {
  36. label: 'CLI',
  37. component: Cli,
  38. icon: 'i-codicon-terminal'
  39. },
  40. {
  41. label: 'HTTP',
  42. component: Http,
  43. icon: 'i-ph-globe-hemisphere-west'
  44. },
  45. !isMobile && {
  46. label: 'Notifications',
  47. component: Notifications,
  48. icon: 'i-codicon-bell-dot'
  49. },
  50. !isMobile && {
  51. label: 'App',
  52. component: App,
  53. icon: 'i-codicon-hubot'
  54. },
  55. !isMobile && {
  56. label: 'Window',
  57. component: Window,
  58. icon: 'i-codicon-window'
  59. },
  60. !isMobile && {
  61. label: 'Shortcuts',
  62. component: Shortcuts,
  63. icon: 'i-codicon-record-keys'
  64. },
  65. {
  66. label: 'Shell',
  67. component: Shell,
  68. icon: 'i-codicon-terminal-bash'
  69. },
  70. !isMobile && {
  71. label: 'Updater',
  72. component: Updater,
  73. icon: 'i-codicon-cloud-download'
  74. },
  75. {
  76. label: 'WebRTC',
  77. component: WebRTC,
  78. icon: 'i-ph-broadcast'
  79. }
  80. ]
  81. let selected = views[0]
  82. function select(view) {
  83. selected = view
  84. }
  85. // Window controls
  86. let isWindowMaximized
  87. onMount(async () => {
  88. const window = getCurrent()
  89. isWindowMaximized = await window.isMaximized()
  90. listen('tauri://resize', async () => {
  91. isWindowMaximized = await window.isMaximized()
  92. })
  93. })
  94. function minimize() {
  95. getCurrent().minimize()
  96. }
  97. async function toggleMaximize() {
  98. const window = getCurrent()
  99. ;(await window.isMaximized()) ? window.unmaximize() : window.maximize()
  100. }
  101. // dark/light
  102. let isDark
  103. onMount(() => {
  104. isDark = localStorage && localStorage.getItem('theme') == 'dark'
  105. applyTheme(isDark)
  106. })
  107. function applyTheme(isDark) {
  108. const html = document.querySelector('html')
  109. isDark ? html.classList.add('dark') : html.classList.remove('dark')
  110. localStorage && localStorage.setItem('theme', isDark ? 'dark' : '')
  111. }
  112. function toggleDark() {
  113. isDark = !isDark
  114. applyTheme(isDark)
  115. }
  116. // Console
  117. let messages = writable([])
  118. function onMessage(value) {
  119. messages.update((r) => [
  120. {
  121. html:
  122. `<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
  123. (typeof value === 'string' ? value : JSON.stringify(value, null, 1)) +
  124. '</pre>'
  125. },
  126. ...r
  127. ])
  128. }
  129. // this function is renders HTML without sanitizing it so it's insecure
  130. // we only use it with our own input data
  131. function insecureRenderHtml(html) {
  132. messages.update((r) => [
  133. {
  134. html:
  135. `<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
  136. html +
  137. '</pre>'
  138. },
  139. ...r
  140. ])
  141. }
  142. function clear() {
  143. messages.update(() => [])
  144. }
  145. let consoleEl, consoleH, cStartY
  146. let minConsoleHeight = 50
  147. function startResizingConsole(e) {
  148. cStartY = e.clientY
  149. const styles = window.getComputedStyle(consoleEl)
  150. consoleH = parseInt(styles.height, 10)
  151. const moveHandler = (e) => {
  152. const dy = e.clientY - cStartY
  153. const newH = consoleH - dy
  154. consoleEl.style.height = `${
  155. newH < minConsoleHeight ? minConsoleHeight : newH
  156. }px`
  157. }
  158. const upHandler = () => {
  159. document.removeEventListener('mouseup', upHandler)
  160. document.removeEventListener('mousemove', moveHandler)
  161. }
  162. document.addEventListener('mouseup', upHandler)
  163. document.addEventListener('mousemove', moveHandler)
  164. }
  165. let isWindows
  166. onMount(async () => {
  167. isWindows = (await os.platform()) === 'win32'
  168. })
  169. // mobile
  170. let isSideBarOpen = false
  171. let sidebar
  172. let sidebarToggle
  173. let isDraggingSideBar = false
  174. let draggingStartPosX = 0
  175. let draggingEndPosX = 0
  176. const clamp = (min, num, max) => Math.min(Math.max(num, min), max)
  177. function toggleSidebar(sidebar, isSideBarOpen) {
  178. sidebar.style.setProperty(
  179. '--translate-x',
  180. `${isSideBarOpen ? '0' : '-18.75'}rem`
  181. )
  182. }
  183. onMount(() => {
  184. sidebar = document.querySelector('#sidebar')
  185. sidebarToggle = document.querySelector('#sidebarToggle')
  186. document.addEventListener('click', (e) => {
  187. if (sidebarToggle.contains(e.target)) {
  188. isSideBarOpen = !isSideBarOpen
  189. } else if (isSideBarOpen && !sidebar.contains(e.target)) {
  190. isSideBarOpen = false
  191. }
  192. })
  193. document.addEventListener('touchstart', (e) => {
  194. if (sidebarToggle.contains(e.target)) return
  195. const x = e.touches[0].clientX
  196. if ((0 < x && x < 20 && !isSideBarOpen) || isSideBarOpen) {
  197. isDraggingSideBar = true
  198. draggingStartPosX = x
  199. }
  200. })
  201. document.addEventListener('touchmove', (e) => {
  202. if (isDraggingSideBar) {
  203. const x = e.touches[0].clientX
  204. draggingEndPosX = x
  205. const delta = (x - draggingStartPosX) / 10
  206. sidebar.style.setProperty(
  207. '--translate-x',
  208. `-${clamp(0, isSideBarOpen ? 0 - delta : 18.75 - delta, 18.75)}rem`
  209. )
  210. }
  211. })
  212. document.addEventListener('touchend', () => {
  213. if (isDraggingSideBar) {
  214. const delta = (draggingEndPosX - draggingStartPosX) / 10
  215. isSideBarOpen = isSideBarOpen ? delta > -(18.75 / 2) : delta > 18.75 / 2
  216. }
  217. isDraggingSideBar = false
  218. })
  219. })
  220. $: {
  221. const sidebar = document.querySelector('#sidebar')
  222. if (sidebar) {
  223. toggleSidebar(sidebar, isSideBarOpen)
  224. }
  225. }
  226. </script>
  227. <!-- custom titlebar for Windows -->
  228. {#if isWindows}
  229. <div
  230. class="w-screen select-none h-8 pl-2 flex justify-between items-center absolute text-primaryText dark:text-darkPrimaryText"
  231. data-tauri-drag-region
  232. >
  233. <span class="lt-sm:pl-10 text-darkPrimaryText">Tauri API Validation</span>
  234. <span
  235. class="
  236. h-100%
  237. children:h-100% children:w-12 children:inline-flex
  238. children:items-center children:justify-center"
  239. >
  240. <span
  241. title={isDark ? 'Switch to Light mode' : 'Switch to Dark mode'}
  242. class="hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
  243. on:click={toggleDark}
  244. >
  245. {#if isDark}
  246. <div class="i-ph-sun" />
  247. {:else}
  248. <div class="i-ph-moon" />
  249. {/if}
  250. </span>
  251. <span
  252. title="Minimize"
  253. class="hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
  254. on:click={minimize}
  255. >
  256. <div class="i-codicon-chrome-minimize" />
  257. </span>
  258. <span
  259. title={isWindowMaximized ? 'Restore' : 'Maximize'}
  260. class="hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
  261. on:click={toggleMaximize}
  262. >
  263. {#if isWindowMaximized}
  264. <div class="i-codicon-chrome-restore" />
  265. {:else}
  266. <div class="i-codicon-chrome-maximize" />
  267. {/if}
  268. </span>
  269. </span>
  270. </div>
  271. {/if}
  272. <!-- Sidebar toggle, only visible on small screens -->
  273. <div
  274. id="sidebarToggle"
  275. class="z-2000 display-none lt-sm:flex justify-center items-center absolute top-2 left-2 w-8 h-8 rd-8
  276. bg-accent dark:bg-darkAccent active:bg-accentDark dark:active:bg-darkAccentDark"
  277. >
  278. {#if isSideBarOpen}
  279. <span class="i-codicon-close animate-duration-300ms animate-fade-in" />
  280. {:else}
  281. <span class="i-codicon-menu animate-duration-300ms animate-fade-in" />
  282. {/if}
  283. </div>
  284. <div
  285. class="flex h-screen w-screen overflow-hidden children-pt8 children-pb-2 text-primaryText dark:text-darkPrimaryText"
  286. >
  287. <aside
  288. id="sidebar"
  289. class="lt-sm:h-screen lt-sm:shadow-lg lt-sm:shadow lt-sm:transition-transform lt-sm:absolute lt-sm:z-1999
  290. bg-darkPrimaryLighter transition-colors-250 overflow-hidden grid select-none px-2"
  291. >
  292. <img
  293. on:click={() => open('https://tauri.app/')}
  294. class="self-center p-7 cursor-pointer"
  295. src="tauri_logo.png"
  296. alt="Tauri logo"
  297. />
  298. {#if !isWindows}
  299. <a href="##" class="nv justify-between h-8" on:click={toggleDark}>
  300. {#if isDark}
  301. Switch to Light mode
  302. <div class="i-ph-sun" />
  303. {:else}
  304. Switch to Dark mode
  305. <div class="i-ph-moon" />
  306. {/if}
  307. </a>
  308. <br />
  309. <div class="bg-white/5 h-2px" />
  310. <br />
  311. {/if}
  312. <a
  313. class="nv justify-between h-8"
  314. target="_blank"
  315. href="https://tauri.app/v1/guides/"
  316. >
  317. Documentation
  318. <span class="i-codicon-link-external" />
  319. </a>
  320. <a
  321. class="nv justify-between h-8"
  322. target="_blank"
  323. href="https://github.com/tauri-apps/tauri"
  324. >
  325. GitHub
  326. <span class="i-codicon-link-external" />
  327. </a>
  328. <a
  329. class="nv justify-between h-8"
  330. target="_blank"
  331. href="https://github.com/tauri-apps/tauri/tree/dev/examples/api"
  332. >
  333. Source
  334. <span class="i-codicon-link-external" />
  335. </a>
  336. <br />
  337. <div class="bg-white/5 h-2px" />
  338. <br />
  339. <div
  340. class="flex flex-col overflow-y-auto children-h-10 children-flex-none gap-1"
  341. >
  342. {#each views as view}
  343. {#if view}
  344. <a
  345. href="##"
  346. class="nv {selected === view ? 'nv_selected' : ''}"
  347. on:click={() => {
  348. select(view)
  349. isSideBarOpen = false
  350. }}
  351. >
  352. <div class="{view.icon} mr-2" />
  353. <p>{view.label}</p></a
  354. >
  355. {/if}
  356. {/each}
  357. </div>
  358. </aside>
  359. <main
  360. class="flex-1 bg-primary dark:bg-darkPrimary transition-transform transition-colors-250 grid grid-rows-[2fr_auto]"
  361. >
  362. <div class="px-5 overflow-hidden grid grid-rows-[auto_1fr]">
  363. <h1>{selected.label}</h1>
  364. <div class="overflow-y-auto">
  365. <div class="mr-2">
  366. <svelte:component
  367. this={selected.component}
  368. {onMessage}
  369. {insecureRenderHtml}
  370. />
  371. </div>
  372. </div>
  373. </div>
  374. <div
  375. bind:this={consoleEl}
  376. id="console"
  377. class="select-none h-15rem grid grid-rows-[2px_2rem_1fr] gap-1 overflow-hidden"
  378. >
  379. <div
  380. on:mousedown={startResizingConsole}
  381. class="bg-black/20 h-2px cursor-ns-resize"
  382. />
  383. <div class="flex justify-between items-center px-2">
  384. <p class="font-semibold">Console</p>
  385. <div
  386. class="cursor-pointer h-85% rd-1 p-1 flex justify-center items-center
  387. hover:bg-hoverOverlay dark:hover:bg-darkHoverOverlay
  388. active:bg-hoverOverlay/25 dark:active:bg-darkHoverOverlay/25
  389. "
  390. on:click={clear}
  391. >
  392. <div class="i-codicon-clear-all" />
  393. </div>
  394. </div>
  395. <div class="px-2 overflow-y-auto all:font-mono code-block all:text-xs">
  396. {#each $messages as r}
  397. {@html r.html}
  398. {/each}
  399. </div>
  400. </div>
  401. </main>
  402. </div>