App.svelte 13 KB

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