App.svelte 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. <script>
  2. import { onMount, tick } from 'svelte'
  3. import { writable } from 'svelte/store'
  4. import { invoke } from '@tauri-apps/api/core'
  5. import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
  6. import { setTheme } from '@tauri-apps/api/app'
  7. import Welcome from './views/Welcome.svelte'
  8. import Communication from './views/Communication.svelte'
  9. import Window from './views/Window.svelte'
  10. import WebRTC from './views/WebRTC.svelte'
  11. import App from './views/App.svelte'
  12. import Menu from './views/Menu.svelte'
  13. import Tray from './views/Tray.svelte'
  14. document.addEventListener('keydown', (event) => {
  15. if (event.ctrlKey && event.key === 'b') {
  16. invoke('plugin:app-menu|toggle')
  17. }
  18. })
  19. const appWindow = getCurrentWebviewWindow()
  20. appWindow.onDragDropEvent((event) => {
  21. onMessage(event.payload)
  22. })
  23. const userAgent = navigator.userAgent.toLowerCase()
  24. const isMobile = userAgent.includes('android') || userAgent.includes('iphone')
  25. const views = [
  26. {
  27. label: 'Welcome',
  28. component: Welcome,
  29. icon: 'i-ph-hand-waving'
  30. },
  31. {
  32. label: 'Communication',
  33. component: Communication,
  34. icon: 'i-codicon-radio-tower'
  35. },
  36. !isMobile && {
  37. label: 'App',
  38. component: App,
  39. icon: 'i-codicon-hubot'
  40. },
  41. !isMobile && {
  42. label: 'Window',
  43. component: Window,
  44. icon: 'i-codicon-window'
  45. },
  46. !isMobile && {
  47. label: 'Menu',
  48. component: Menu,
  49. icon: 'i-ph-list'
  50. },
  51. !isMobile && {
  52. label: 'Tray',
  53. component: Tray,
  54. icon: 'i-ph-tray'
  55. },
  56. {
  57. label: 'WebRTC',
  58. component: WebRTC,
  59. icon: 'i-ph-broadcast'
  60. }
  61. ]
  62. let selected = views[0]
  63. function select(view) {
  64. selected = view
  65. }
  66. // dark/light
  67. let isDark
  68. onMount(() => {
  69. isDark = localStorage && localStorage.getItem('theme') == 'dark'
  70. applyTheme(isDark)
  71. })
  72. function applyTheme(isDark) {
  73. const html = document.querySelector('html')
  74. isDark ? html.classList.add('dark') : html.classList.remove('dark')
  75. localStorage && localStorage.setItem('theme', isDark ? 'dark' : '')
  76. }
  77. function toggleDark() {
  78. isDark = !isDark
  79. applyTheme(isDark)
  80. setTheme(isDark ? 'dark' : 'light')
  81. }
  82. // Console
  83. let messages = writable([])
  84. let consoleTextEl
  85. async function onMessage(value) {
  86. const valueStr =
  87. typeof value === 'string'
  88. ? value
  89. : JSON.stringify(
  90. value instanceof ArrayBuffer
  91. ? Array.from(new Uint8Array(value))
  92. : value,
  93. null,
  94. 1
  95. )
  96. messages.update((r) => [
  97. ...r,
  98. {
  99. html:
  100. `<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
  101. valueStr +
  102. '</pre>'
  103. }
  104. ])
  105. await tick()
  106. if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight
  107. }
  108. // this function is renders HTML without sanitizing it so it's insecure
  109. // we only use it with our own input data
  110. async function insecureRenderHtml(html) {
  111. messages.update((r) => [
  112. ...r,
  113. {
  114. html:
  115. `<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
  116. html +
  117. '</pre>'
  118. }
  119. ])
  120. await tick()
  121. if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight
  122. }
  123. function clear() {
  124. messages.update(() => [])
  125. }
  126. let consoleEl, consoleH, cStartY
  127. let minConsoleHeight = 50
  128. function startResizingConsole(e) {
  129. cStartY = e.clientY
  130. const styles = window.getComputedStyle(consoleEl)
  131. consoleH = parseInt(styles.height, 10)
  132. const moveHandler = (e) => {
  133. const dy = e.clientY - cStartY
  134. const newH = consoleH - dy
  135. consoleEl.style.height = `${
  136. newH < minConsoleHeight ? minConsoleHeight : newH
  137. }px`
  138. }
  139. const upHandler = () => {
  140. document.removeEventListener('mouseup', upHandler)
  141. document.removeEventListener('mousemove', moveHandler)
  142. }
  143. document.addEventListener('mouseup', upHandler)
  144. document.addEventListener('mousemove', moveHandler)
  145. }
  146. // mobile
  147. let isSideBarOpen = false
  148. let sidebar
  149. let sidebarToggle
  150. let isDraggingSideBar = false
  151. let draggingStartPosX = 0
  152. let draggingEndPosX = 0
  153. const clamp = (min, num, max) => Math.min(Math.max(num, min), max)
  154. function toggleSidebar(sidebar, isSideBarOpen) {
  155. sidebar.style.setProperty(
  156. '--translate-x',
  157. `${isSideBarOpen ? '0' : '-18.75'}rem`
  158. )
  159. }
  160. onMount(() => {
  161. sidebar = document.querySelector('#sidebar')
  162. sidebarToggle = document.querySelector('#sidebarToggle')
  163. document.addEventListener('click', (e) => {
  164. if (sidebarToggle.contains(e.target)) {
  165. isSideBarOpen = !isSideBarOpen
  166. } else if (isSideBarOpen && !sidebar.contains(e.target)) {
  167. isSideBarOpen = false
  168. }
  169. })
  170. document.addEventListener('touchstart', (e) => {
  171. if (sidebarToggle.contains(e.target)) return
  172. const x = e.touches[0].clientX
  173. if ((0 < x && x < 20 && !isSideBarOpen) || isSideBarOpen) {
  174. isDraggingSideBar = true
  175. draggingStartPosX = x
  176. }
  177. })
  178. document.addEventListener('touchmove', (e) => {
  179. if (isDraggingSideBar) {
  180. const x = e.touches[0].clientX
  181. draggingEndPosX = x
  182. const delta = (x - draggingStartPosX) / 10
  183. sidebar.style.setProperty(
  184. '--translate-x',
  185. `-${clamp(0, isSideBarOpen ? 0 - delta : 18.75 - delta, 18.75)}rem`
  186. )
  187. }
  188. })
  189. document.addEventListener('touchend', () => {
  190. if (isDraggingSideBar) {
  191. const delta = (draggingEndPosX - draggingStartPosX) / 10
  192. isSideBarOpen = isSideBarOpen ? delta > -(18.75 / 2) : delta > 18.75 / 2
  193. }
  194. isDraggingSideBar = false
  195. })
  196. })
  197. $: {
  198. const sidebar = document.querySelector('#sidebar')
  199. if (sidebar) {
  200. toggleSidebar(sidebar, isSideBarOpen)
  201. }
  202. }
  203. </script>
  204. <!-- Sidebar toggle, only visible on small screens -->
  205. <div
  206. id="sidebarToggle"
  207. class="z-2000 hidden lt-sm:flex justify-center items-center absolute top-2 left-2 w-8 h-8 rd-8
  208. bg-accent dark:bg-darkAccent active:bg-accentDark dark:active:bg-darkAccentDark"
  209. >
  210. {#if isSideBarOpen}
  211. <span class="i-codicon-close animate-duration-300ms animate-fade-in" />
  212. {:else}
  213. <span class="i-codicon-menu animate-duration-300ms animate-fade-in" />
  214. {/if}
  215. </div>
  216. <div
  217. class="flex h-screen w-screen overflow-hidden children-pt4 children-pb-2 text-primaryText dark:text-darkPrimaryText"
  218. >
  219. <aside
  220. id="sidebar"
  221. class="lt-sm:h-screen lt-sm:shadow-lg lt-sm:shadow lt-sm:transition-transform lt-sm:absolute lt-sm:z-1999
  222. bg-darkPrimaryLighter transition-colors-250 overflow-hidden grid grid-rows-[min-content_auto] select-none px-2"
  223. >
  224. <img
  225. class="self-center p-7 cursor-pointer"
  226. src="tauri_logo.png"
  227. alt="Tauri logo"
  228. />
  229. <a href="##" class="nv justify-between" on:click={toggleDark}>
  230. {#if isDark}
  231. Switch to Light mode
  232. <div class="i-ph-sun" />
  233. {:else}
  234. Switch to Dark mode
  235. <div class="i-ph-moon" />
  236. {/if}
  237. </a>
  238. <br />
  239. <div class="bg-white/5 h-2px" />
  240. <br />
  241. <a
  242. class="nv justify-between"
  243. target="_blank"
  244. href="https://v2.tauri.app/start/"
  245. >
  246. Documentation
  247. <span class="i-codicon-link-external" />
  248. </a>
  249. <a
  250. class="nv justify-between"
  251. target="_blank"
  252. href="https://github.com/tauri-apps/tauri"
  253. >
  254. GitHub
  255. <span class="i-codicon-link-external" />
  256. </a>
  257. <a
  258. class="nv justify-between"
  259. target="_blank"
  260. href="https://github.com/tauri-apps/tauri/tree/dev/examples/api"
  261. >
  262. Source
  263. <span class="i-codicon-link-external" />
  264. </a>
  265. <br />
  266. <div class="bg-white/5 h-2px" />
  267. <br />
  268. <div class="flex flex-col overflow-y-auto children-flex-none gap-1">
  269. {#each views as view}
  270. {#if view}
  271. <a
  272. href="##"
  273. class="nv {selected === view ? 'nv_selected' : ''}"
  274. on:click={() => {
  275. select(view)
  276. isSideBarOpen = false
  277. }}
  278. >
  279. <div class="{view.icon} mr-2" />
  280. <p>{view.label}</p></a
  281. >
  282. {/if}
  283. {/each}
  284. </div>
  285. </aside>
  286. <main
  287. class="flex-1 bg-primary dark:bg-darkPrimary transition-transform transition-colors-250 grid grid-rows-[2fr_auto]"
  288. >
  289. <div class="px-5 overflow-hidden grid grid-rows-[auto_1fr]">
  290. <h1>{selected.label}</h1>
  291. <div class="overflow-y-auto">
  292. <div class="mr-2">
  293. <svelte:component
  294. this={selected.component}
  295. {onMessage}
  296. {insecureRenderHtml}
  297. />
  298. </div>
  299. </div>
  300. </div>
  301. <div
  302. bind:this={consoleEl}
  303. id="console"
  304. class="select-none h-15rem grid grid-rows-[2px_2rem_1fr] gap-1 overflow-hidden"
  305. >
  306. <div
  307. role="button"
  308. tabindex="0"
  309. on:mousedown={startResizingConsole}
  310. class="bg-black/20 h-4px cursor-ns-resize"
  311. />
  312. <div class="flex justify-between items-center px-2">
  313. <p class="font-semibold">Console</p>
  314. <div
  315. role="button"
  316. tabindex="0"
  317. class="cursor-pointer h-85% rd-1 p-1 flex justify-center items-center
  318. hover:bg-hoverOverlay dark:hover:bg-darkHoverOverlay
  319. active:bg-hoverOverlay/25 dark:active:bg-darkHoverOverlay/25
  320. "
  321. on:keypress={(e) => (e.key === 'Enter' ? clear() : {})}
  322. on:click={clear}
  323. >
  324. <div class="i-codicon-clear-all" />
  325. </div>
  326. </div>
  327. <div
  328. bind:this={consoleTextEl}
  329. class="px-2 overflow-y-auto all:font-mono code-block all:text-xs select-text mr-2"
  330. >
  331. {#each $messages as r}
  332. {@html r.html}
  333. {/each}
  334. </div>
  335. </div>
  336. </main>
  337. </div>