feat: add API example (#317)

This commit is contained in:
Lucas Fernandes Nogueira
2023-05-05 05:41:17 -07:00
committed by GitHub
parent be1c775b8d
commit 5015132ece
98 changed files with 10334 additions and 156 deletions
+485
View File
@@ -0,0 +1,485 @@
<script>
import { writable } from 'svelte/store'
import { open } from 'tauri-plugin-shell-api'
import { appWindow, getCurrent } from '@tauri-apps/api/window'
import * as os from '@tauri-apps/api/os'
import Welcome from './views/Welcome.svelte'
import Cli from './views/Cli.svelte'
import Communication from './views/Communication.svelte'
import Dialog from './views/Dialog.svelte'
import FileSystem from './views/FileSystem.svelte'
import Http from './views/Http.svelte'
import Notifications from './views/Notifications.svelte'
import Window from './views/Window.svelte'
import Shortcuts from './views/Shortcuts.svelte'
import Shell from './views/Shell.svelte'
import Updater from './views/Updater.svelte'
import Clipboard from './views/Clipboard.svelte'
import WebRTC from './views/WebRTC.svelte'
import App from './views/App.svelte'
import { onMount } from 'svelte'
import { listen } from '@tauri-apps/api/event'
import { ask } from 'tauri-plugin-dialog-api'
if (appWindow.label !== 'main') {
appWindow.onCloseRequested(async (event) => {
const confirmed = await confirm('Are you sure?')
if (!confirmed) {
// user did not confirm closing the window; let's prevent it
event.preventDefault()
}
})
}
appWindow.onFileDropEvent((event) => {
onMessage(`File drop: ${JSON.stringify(event.payload)}`)
})
const userAgent = navigator.userAgent.toLowerCase()
const isMobile = userAgent.includes('android') || userAgent.includes('iphone')
const views = [
{
label: 'Welcome',
component: Welcome,
icon: 'i-ph-hand-waving'
},
{
label: 'Communication',
component: Communication,
icon: 'i-codicon-radio-tower'
},
!isMobile && {
label: 'CLI',
component: Cli,
icon: 'i-codicon-terminal'
},
!isMobile && {
label: 'Dialog',
component: Dialog,
icon: 'i-codicon-multiple-windows'
},
{
label: 'File system',
component: FileSystem,
icon: 'i-codicon-files'
},
{
label: 'HTTP',
component: Http,
icon: 'i-ph-globe-hemisphere-west'
},
!isMobile && {
label: 'Notifications',
component: Notifications,
icon: 'i-codicon-bell-dot'
},
!isMobile && {
label: 'App',
component: App,
icon: 'i-codicon-hubot'
},
!isMobile && {
label: 'Window',
component: Window,
icon: 'i-codicon-window'
},
!isMobile && {
label: 'Shortcuts',
component: Shortcuts,
icon: 'i-codicon-record-keys'
},
{
label: 'Shell',
component: Shell,
icon: 'i-codicon-terminal-bash'
},
!isMobile && {
label: 'Updater',
component: Updater,
icon: 'i-codicon-cloud-download'
},
!isMobile && {
label: 'Clipboard',
component: Clipboard,
icon: 'i-codicon-clippy'
},
{
label: 'WebRTC',
component: WebRTC,
icon: 'i-ph-broadcast'
}
]
let selected = views[0]
function select(view) {
selected = view
}
// Window controls
let isWindowMaximized
onMount(async () => {
const window = getCurrent()
isWindowMaximized = await window.isMaximized()
listen('tauri://resize', async () => {
isWindowMaximized = await window.isMaximized()
})
})
function minimize() {
getCurrent().minimize()
}
async function toggleMaximize() {
const window = getCurrent()
;(await window.isMaximized()) ? window.unmaximize() : window.maximize()
}
let confirmed_close = false
async function close() {
if (!confirmed_close) {
confirmed_close = await ask(
'Are you sure that you want to close this window?',
{
title: 'Tauri API'
}
)
if (confirmed_close) {
getCurrent().close()
}
}
}
// dark/light
let isDark
onMount(() => {
isDark = localStorage && localStorage.getItem('theme') == 'dark'
applyTheme(isDark)
})
function applyTheme(isDark) {
const html = document.querySelector('html')
isDark ? html.classList.add('dark') : html.classList.remove('dark')
localStorage && localStorage.setItem('theme', isDark ? 'dark' : '')
}
function toggleDark() {
isDark = !isDark
applyTheme(isDark)
}
// Console
let messages = writable([])
function onMessage(value) {
messages.update((r) => [
{
html:
`<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
(typeof value === 'string' ? value : JSON.stringify(value, null, 1)) +
'</pre>'
},
...r
])
}
// this function is renders HTML without sanitizing it so it's insecure
// we only use it with our own input data
function insecureRenderHtml(html) {
messages.update((r) => [
{
html:
`<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
html +
'</pre>'
},
...r
])
}
function clear() {
messages.update(() => [])
}
let consoleEl, consoleH, cStartY
let minConsoleHeight = 50
function startResizingConsole(e) {
cStartY = e.clientY
const styles = window.getComputedStyle(consoleEl)
consoleH = parseInt(styles.height, 10)
const moveHandler = (e) => {
const dy = e.clientY - cStartY
const newH = consoleH - dy
consoleEl.style.height = `${
newH < minConsoleHeight ? minConsoleHeight : newH
}px`
}
const upHandler = () => {
document.removeEventListener('mouseup', upHandler)
document.removeEventListener('mousemove', moveHandler)
}
document.addEventListener('mouseup', upHandler)
document.addEventListener('mousemove', moveHandler)
}
let isWindows
onMount(async () => {
isWindows = (await os.platform()) === 'win32'
})
// mobile
let isSideBarOpen = false
let sidebar
let sidebarToggle
let isDraggingSideBar = false
let draggingStartPosX = 0
let draggingEndPosX = 0
const clamp = (min, num, max) => Math.min(Math.max(num, min), max)
function toggleSidebar(sidebar, isSideBarOpen) {
sidebar.style.setProperty(
'--translate-x',
`${isSideBarOpen ? '0' : '-18.75'}rem`
)
}
onMount(() => {
sidebar = document.querySelector('#sidebar')
sidebarToggle = document.querySelector('#sidebarToggle')
document.addEventListener('click', (e) => {
if (sidebarToggle.contains(e.target)) {
isSideBarOpen = !isSideBarOpen
} else if (isSideBarOpen && !sidebar.contains(e.target)) {
isSideBarOpen = false
}
})
document.addEventListener('touchstart', (e) => {
if (sidebarToggle.contains(e.target)) return
const x = e.touches[0].clientX
if ((0 < x && x < 20 && !isSideBarOpen) || isSideBarOpen) {
isDraggingSideBar = true
draggingStartPosX = x
}
})
document.addEventListener('touchmove', (e) => {
if (isDraggingSideBar) {
const x = e.touches[0].clientX
draggingEndPosX = x
const delta = (x - draggingStartPosX) / 10
sidebar.style.setProperty(
'--translate-x',
`-${clamp(0, isSideBarOpen ? 0 - delta : 18.75 - delta, 18.75)}rem`
)
}
})
document.addEventListener('touchend', () => {
if (isDraggingSideBar) {
const delta = (draggingEndPosX - draggingStartPosX) / 10
isSideBarOpen = isSideBarOpen ? delta > -(18.75 / 2) : delta > 18.75 / 2
}
isDraggingSideBar = false
})
})
$: {
const sidebar = document.querySelector('#sidebar')
if (sidebar) {
toggleSidebar(sidebar, isSideBarOpen)
}
}
</script>
<!-- custom titlebar for Windows -->
{#if isWindows}
<div
class="w-screen select-none h-8 pl-2 flex justify-between items-center absolute text-primaryText dark:text-darkPrimaryText"
data-tauri-drag-region
>
<span class="lt-sm:pl-10 text-darkPrimaryText">Tauri API Validation</span>
<span
class="
h-100%
children:h-100% children:w-12 children:inline-flex
children:items-center children:justify-center"
>
<span
title={isDark ? 'Switch to Light mode' : 'Switch to Dark mode'}
class="hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
on:click={toggleDark}
>
{#if isDark}
<div class="i-ph-sun" />
{:else}
<div class="i-ph-moon" />
{/if}
</span>
<span
title="Minimize"
class="hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
on:click={minimize}
>
<div class="i-codicon-chrome-minimize" />
</span>
<span
title={isWindowMaximized ? 'Restore' : 'Maximize'}
class="hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
on:click={toggleMaximize}
>
{#if isWindowMaximized}
<div class="i-codicon-chrome-restore" />
{:else}
<div class="i-codicon-chrome-maximize" />
{/if}
</span>
<span
title="Close"
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 "
on:click={close}
>
<div class="i-codicon-chrome-close" />
</span>
</span>
</div>
{/if}
<!-- Sidebar toggle, only visible on small screens -->
<div
id="sidebarToggle"
class="z-2000 display-none lt-sm:flex justify-center items-center absolute top-2 left-2 w-8 h-8 rd-8
bg-accent dark:bg-darkAccent active:bg-accentDark dark:active:bg-darkAccentDark"
>
{#if isSideBarOpen}
<span class="i-codicon-close animate-duration-300ms animate-fade-in" />
{:else}
<span class="i-codicon-menu animate-duration-300ms animate-fade-in" />
{/if}
</div>
<div
class="flex h-screen w-screen overflow-hidden children-pt8 children-pb-2 text-primaryText dark:text-darkPrimaryText"
>
<aside
id="sidebar"
class="lt-sm:h-screen lt-sm:shadow-lg lt-sm:shadow lt-sm:transition-transform lt-sm:absolute lt-sm:z-1999
bg-darkPrimaryLighter transition-colors-250 overflow-hidden grid select-none px-2"
>
<img
on:click={() => open('https://tauri.app/')}
class="self-center p-7 cursor-pointer"
src="tauri_logo.png"
alt="Tauri logo"
/>
{#if !isWindows}
<a href="##" class="nv justify-between h-8" on:click={toggleDark}>
{#if isDark}
Switch to Light mode
<div class="i-ph-sun" />
{:else}
Switch to Dark mode
<div class="i-ph-moon" />
{/if}
</a>
<br />
<div class="bg-white/5 h-2px" />
<br />
{/if}
<a
class="nv justify-between h-8"
target="_blank"
href="https://tauri.app/v1/guides/"
>
Documentation
<span class="i-codicon-link-external" />
</a>
<a
class="nv justify-between h-8"
target="_blank"
href="https://github.com/tauri-apps/tauri"
>
GitHub
<span class="i-codicon-link-external" />
</a>
<a
class="nv justify-between h-8"
target="_blank"
href="https://github.com/tauri-apps/tauri/tree/dev/examples/api"
>
Source
<span class="i-codicon-link-external" />
</a>
<br />
<div class="bg-white/5 h-2px" />
<br />
<div
class="flex flex-col overflow-y-auto children-h-10 children-flex-none gap-1"
>
{#each views as view}
{#if view}
<a
href="##"
class="nv {selected === view ? 'nv_selected' : ''}"
on:click={() => {
select(view)
isSideBarOpen = false
}}
>
<div class="{view.icon} mr-2" />
<p>{view.label}</p></a
>
{/if}
{/each}
</div>
</aside>
<main
class="flex-1 bg-primary dark:bg-darkPrimary transition-transform transition-colors-250 grid grid-rows-[2fr_auto]"
>
<div class="px-5 overflow-hidden grid grid-rows-[auto_1fr]">
<h1>{selected.label}</h1>
<div class="overflow-y-auto">
<div class="mr-2">
<svelte:component
this={selected.component}
{onMessage}
{insecureRenderHtml}
/>
</div>
</div>
</div>
<div
bind:this={consoleEl}
id="console"
class="select-none h-15rem grid grid-rows-[2px_2rem_1fr] gap-1 overflow-hidden"
>
<div
on:mousedown={startResizingConsole}
class="bg-black/20 h-2px cursor-ns-resize"
/>
<div class="flex justify-between items-center px-2">
<p class="font-semibold">Console</p>
<div
class="cursor-pointer h-85% rd-1 p-1 flex justify-center items-center
hover:bg-hoverOverlay dark:hover:bg-darkHoverOverlay
active:bg-hoverOverlay/25 dark:active:bg-darkHoverOverlay/25
"
on:click={clear}
>
<div class="i-codicon-clear-all" />
</div>
</div>
<div class="px-2 overflow-y-auto all:font-mono code-block all:text-xs">
{#each $messages as r}
{@html r.html}
{/each}
</div>
</div>
</main>
</div>
+41
View File
@@ -0,0 +1,41 @@
*:not(h1, h2, h3, h4, h5, h6) {
margin: 0;
padding: 0;
}
* {
box-sizing: border-box;
font-family: "Rubik", sans-serif;
}
::-webkit-scrollbar {
width: 0.25rem;
height: 3px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
border-radius: 0.25rem;
}
code {
padding: 0.05rem 0.25rem;
}
code.code-block {
padding: 0.5rem;
}
#sidebar {
width: 18.75rem;
}
@media screen and (max-width: 640px) {
#sidebar {
--translate-x: -18.75rem;
transform: translateX(var(--translate-x));
}
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import "uno.css";
import "./app.css";
import App from "./App.svelte";
const app = new App({
target: document.querySelector("#app"),
});
export default app;
+33
View File
@@ -0,0 +1,33 @@
<script>
import { show, hide } from '@tauri-apps/api/app'
export let onMessage
function showApp() {
hideApp()
.then(() => {
setTimeout(() => {
show()
.then(() => onMessage('Shown app'))
.catch(onMessage)
}, 2000)
})
.catch(onMessage)
}
function hideApp() {
return hide()
.then(() => onMessage('Hide app'))
.catch(onMessage)
}
</script>
<div>
<button
class="btn"
id="show"
title="Hides and shows the app after 2 seconds"
on:click={showApp}>Show</button
>
<button class="btn" id="hide" on:click={hideApp}>Hide</button>
</div>
+29
View File
@@ -0,0 +1,29 @@
<script>
import { getMatches } from "tauri-plugin-cli-api";
export let onMessage;
function cliMatches() {
getMatches().then(onMessage).catch(onMessage);
}
</script>
<p>
This binary can be run from the terminal and takes the following arguments:
<code class="code-block flex flex-wrap my-2">
<pre>
--config &lt;PATH&gt;
--theme &lt;light|dark|system&gt;
--verbose</pre>
</code>
Additionally, it has a <code>update --background</code> subcommand.
</p>
<br />
<div class="note">
Note that the arguments are only parsed, not implemented.
</div>
<br />
<br />
<button class="btn" id="cli-matches" on:click={cliMatches}>
Get matches
</button>
+32
View File
@@ -0,0 +1,32 @@
<script>
import { writeText, readText } from 'tauri-plugin-clipboard-api'
export let onMessage
let text = 'clipboard message'
function write() {
writeText(text)
.then(() => {
onMessage('Wrote to the clipboard')
})
.catch(onMessage)
}
function read() {
readText()
.then((contents) => {
onMessage(`Clipboard contents: ${contents}`)
})
.catch(onMessage)
}
</script>
<div class="flex gap-1">
<input
class="grow input"
placeholder="Text to write to the clipboard"
bind:value={text}
/>
<button class="btn" type="button" on:click={write}>Write</button>
<button class="btn" type="button" on:click={read}>Read</button>
</div>
@@ -0,0 +1,50 @@
<script>
import { listen, emit } from '@tauri-apps/api/event'
import { invoke } from '@tauri-apps/api/tauri'
import { onMount, onDestroy } from 'svelte'
export let onMessage
let unlisten
onMount(async () => {
unlisten = await listen('rust-event', onMessage)
})
onDestroy(() => {
if (unlisten) {
unlisten()
}
})
function log() {
invoke('log_operation', {
event: 'tauri-click',
payload: 'this payload is optional because we used Option in Rust'
})
}
function performRequest() {
invoke('perform_request', {
endpoint: 'dummy endpoint arg',
body: {
id: 5,
name: 'test'
}
})
.then(onMessage)
.catch(onMessage)
}
function emitEvent() {
emit('js-event', 'this is the payload string')
}
</script>
<div>
<button class="btn" id="log" on:click={log}>Call Log API</button>
<button class="btn" id="request" on:click={performRequest}>
Call Request (async) API
</button>
<button class="btn" id="event" on:click={emitEvent}>
Send event to Rust
</button>
</div>
+118
View File
@@ -0,0 +1,118 @@
<script>
import { open, save } from 'tauri-plugin-dialog-api'
import { readBinaryFile } from 'tauri-plugin-fs-api'
export let onMessage
export let insecureRenderHtml
let defaultPath = null
let filter = null
let multiple = false
let directory = false
function arrayBufferToBase64(buffer, callback) {
var blob = new Blob([buffer], {
type: 'application/octet-binary'
})
var reader = new FileReader()
reader.onload = function (evt) {
var dataurl = evt.target.result
callback(dataurl.substr(dataurl.indexOf(',') + 1))
}
reader.readAsDataURL(blob)
}
function openDialog() {
open({
title: 'My wonderful open dialog',
defaultPath,
filters: filter
? [
{
name: 'Tauri Example',
extensions: filter.split(',').map((f) => f.trim())
}
]
: [],
multiple,
directory
})
.then(function (res) {
if (Array.isArray(res)) {
onMessage(res)
} else {
var pathToRead = typeof res === 'string' ? res : res.path
var isFile = pathToRead.match(/\S+\.\S+$/g)
readBinaryFile(pathToRead)
.then(function (response) {
if (isFile) {
if (
pathToRead.includes('.png') ||
pathToRead.includes('.jpg')
) {
arrayBufferToBase64(
new Uint8Array(response),
function (base64) {
var src = 'data:image/png;base64,' + base64
insecureRenderHtml('<img src="' + src + '"></img>')
}
)
} else {
onMessage(res)
}
} else {
onMessage(res)
}
})
.catch(onMessage(res))
}
})
.catch(onMessage)
}
function saveDialog() {
save({
title: 'My wonderful save dialog',
defaultPath,
filters: filter
? [
{
name: 'Tauri Example',
extensions: filter.split(',').map((f) => f.trim())
}
]
: []
})
.then(onMessage)
.catch(onMessage)
}
</script>
<div class="flex gap-2 children:grow">
<input
class="input"
id="dialog-default-path"
placeholder="Default path"
bind:value={defaultPath}
/>
<input
class="input"
id="dialog-filter"
placeholder="Extensions filter, comma-separated"
bind:value={filter}
/>
</div>
<br />
<div>
<input type="checkbox" id="dialog-multiple" bind:checked={multiple} />
<label for="dialog-multiple">Multiple</label>
</div>
<div>
<input type="checkbox" id="dialog-directory" bind:checked={directory} />
<label for="dialog-directory">Directory</label>
</div>
<br />
<button class="btn" id="open-dialog" on:click={openDialog}>Open dialog</button>
<button class="btn" id="save-dialog" on:click={saveDialog}
>Open save dialog</button
>
+106
View File
@@ -0,0 +1,106 @@
<script>
import {
readBinaryFile,
writeTextFile,
readDir,
Dir
} from 'tauri-plugin-fs-api'
import { convertFileSrc } from '@tauri-apps/api/tauri'
export let onMessage
export let insecureRenderHtml
let pathToRead = ''
let img
function getDir() {
const dirSelect = document.getElementById('dir')
return dirSelect.value ? parseInt(dir.value) : null
}
function arrayBufferToBase64(buffer, callback) {
const blob = new Blob([buffer], {
type: 'application/octet-binary'
})
const reader = new FileReader()
reader.onload = function (evt) {
const dataurl = evt.target.result
callback(dataurl.substr(dataurl.indexOf(',') + 1))
}
reader.readAsDataURL(blob)
}
const DirOptions = Object.keys(Dir)
.filter((key) => isNaN(parseInt(key)))
.map((dir) => [dir, Dir[dir]])
function read() {
const isFile = pathToRead.match(/\S+\.\S+$/g)
const opts = {
dir: getDir()
}
const promise = isFile
? readBinaryFile(pathToRead, opts)
: readDir(pathToRead, opts)
promise
.then(function (response) {
if (isFile) {
if (pathToRead.includes('.png') || pathToRead.includes('.jpg')) {
arrayBufferToBase64(new Uint8Array(response), function (base64) {
const src = 'data:image/png;base64,' + base64
insecureRenderHtml('<img src="' + src + '"></img>')
})
} else {
const value = String.fromCharCode.apply(null, response)
insecureRenderHtml(
'<textarea id="file-response"></textarea><button id="file-save">Save</button>'
)
setTimeout(() => {
const fileInput = document.getElementById('file-response')
fileInput.value = value
document
.getElementById('file-save')
.addEventListener('click', function () {
writeTextFile(pathToRead, fileInput.value, {
dir: getDir()
}).catch(onMessage)
})
})
}
} else {
onMessage(response)
}
})
.catch(onMessage)
}
function setSrc() {
img.src = convertFileSrc(pathToRead)
}
</script>
<form class="flex flex-col" on:submit|preventDefault={read}>
<div class="flex gap-1">
<select class="input" id="dir">
<option value="">None</option>
{#each DirOptions as dir}
<option value={dir[1]}>{dir[0]}</option>
{/each}
</select>
<input
class="input grow"
id="path-to-read"
placeholder="Type the path to read..."
bind:value={pathToRead}
/>
</div>
<br />
<div>
<button class="btn" id="read">Read</button>
<button class="btn" type="button" on:click={setSrc}>Use as img src</button>
</div>
</form>
<br />
<img alt="" bind:this={img} />
+99
View File
@@ -0,0 +1,99 @@
<script>
import { getClient, Body, ResponseType } from 'tauri-plugin-http-api'
import { JsonView } from '@zerodevx/svelte-json-view'
let httpMethod = 'GET'
let httpBody = ''
export let onMessage
async function makeHttpRequest() {
const client = await getClient().catch((e) => {
onMessage(e)
throw e
})
let method = httpMethod || 'GET'
const options = {
url: 'http://localhost:3003',
method: method || 'GET'
}
if (
(httpBody.startsWith('{') && httpBody.endsWith('}')) ||
(httpBody.startsWith('[') && httpBody.endsWith(']'))
) {
options.body = Body.json(JSON.parse(httpBody))
} else if (httpBody !== '') {
options.body = Body.text(httpBody)
}
client.request(options).then(onMessage).catch(onMessage)
}
/// http form
let foo = 'baz'
let bar = 'qux'
let result = null
let multipart = true
async function doPost() {
const client = await getClient().catch((e) => {
onMessage(e)
throw e
})
result = await client.request({
url: 'http://localhost:3003',
method: 'POST',
body: Body.form({
foo,
bar
}),
headers: multipart
? { 'Content-Type': 'multipart/form-data' }
: undefined,
responseType: ResponseType.Text
})
}
</script>
<form on:submit|preventDefault={makeHttpRequest}>
<select class="input" id="request-method" bind:value={httpMethod}>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
<br />
<textarea
class="input h-auto w-100%"
id="request-body"
placeholder="Request body"
rows="5"
bind:value={httpBody}
/>
<br />
<button class="btn" id="make-request"> Make request </button>
</form>
<br />
<h3>HTTP Form</h3>
<div class="flex gap-2 children:grow">
<input class="input" bind:value={foo} />
<input class="input" bind:value={bar} />
</div>
<br />
<label>
<input type="checkbox" bind:checked={multipart} />
Multipart
</label>
<br />
<br />
<button class="btn" type="button" on:click={doPost}> Post it</button>
<br />
<br />
<JsonView json={result} />
@@ -0,0 +1,34 @@
<script>
export let onMessage
// send the notification directly
// the backend is responsible for checking the permission
function _sendNotification() {
new Notification('Notification title', {
body: 'This is the notification body'
})
}
// alternatively, check the permission ourselves
function sendNotification() {
if (Notification.permission === 'default') {
Notification.requestPermission()
.then(function (response) {
if (response === 'granted') {
_sendNotification()
} else {
onMessage('Permission is ' + response)
}
})
.catch(onMessage)
} else if (Notification.permission === 'granted') {
_sendNotification()
} else {
onMessage('Permission is denied')
}
}
</script>
<button class="btn" id="notification" on:click={_sendNotification}>
Send test notification
</button>
+100
View File
@@ -0,0 +1,100 @@
<script>
import { Command } from 'tauri-plugin-shell-api'
const windows = navigator.userAgent.includes('Windows')
let cmd = windows ? 'cmd' : 'sh'
let args = windows ? ['/C'] : ['-c']
export let onMessage
let script = 'echo "hello world"'
let cwd = null
let env = 'SOMETHING=value ANOTHER=2'
let encoding = ''
let stdin = ''
let child
function _getEnv() {
return env.split(' ').reduce((env, clause) => {
let [key, value] = clause.split('=')
return {
...env,
[key]: value
}
}, {})
}
function spawn() {
child = null
const command = Command.create(cmd, [...args, script], {
cwd: cwd || null,
env: _getEnv(),
encoding: encoding || undefined,
})
command.on('close', (data) => {
onMessage(
`command finished with code ${data.code} and signal ${data.signal}`
)
child = null
})
command.on('error', (error) => onMessage(`command error: "${error}"`))
command.stdout.on('data', (line) => onMessage(`command stdout: "${line}"`))
command.stderr.on('data', (line) => onMessage(`command stderr: "${line}"`))
command
.spawn()
.then((c) => {
child = c
})
.catch(onMessage)
}
function kill() {
child
.kill()
.then(() => onMessage('killed child process'))
.catch(onMessage)
}
function writeToStdin() {
child.write(stdin).catch(onMessage)
}
</script>
<div class="flex flex-col childre:grow gap-1">
<div class="flex items-center gap-1">
Script:
<input class="grow input" bind:value={script} />
</div>
<div class="flex items-center gap-1">
Encoding:
<input class="grow input" bind:value={encoding} />
</div>
<div class="flex items-center gap-1">
Working directory:
<input
class="grow input"
bind:value={cwd}
placeholder="Working directory"
/>
</div>
<div class="flex items-center gap-1">
Arguments:
<input
class="grow input"
bind:value={env}
placeholder="Environment variables"
/>
</div>
<div class="flex children:grow gap-1">
<button class="btn" on:click={spawn}>Run</button>
<button class="btn" on:click={kill}>Kill</button>
</div>
{#if child}
<br />
<input class="input" placeholder="write to stdin" bind:value={stdin} />
<button class="btn" on:click={writeToStdin}>Write</button>
{/if}
</div>
+73
View File
@@ -0,0 +1,73 @@
<script>
import { writable } from 'svelte/store'
import {
register as registerShortcut,
unregister as unregisterShortcut,
unregisterAll as unregisterAllShortcuts
} from 'tauri-plugin-global-shortcut-api'
export let onMessage
const shortcuts = writable([])
let shortcut = 'CmdOrControl+X'
function register() {
const shortcut_ = shortcut
registerShortcut(shortcut_, () => {
onMessage(`Shortcut ${shortcut_} triggered`)
})
.then(() => {
shortcuts.update((shortcuts_) => [...shortcuts_, shortcut_])
onMessage(`Shortcut ${shortcut_} registered successfully`)
})
.catch(onMessage)
}
function unregister(shortcut) {
const shortcut_ = shortcut
unregisterShortcut(shortcut_)
.then(() => {
shortcuts.update((shortcuts_) =>
shortcuts_.filter((s) => s !== shortcut_)
)
onMessage(`Shortcut ${shortcut_} unregistered`)
})
.catch(onMessage)
}
function unregisterAll() {
unregisterAllShortcuts()
.then(() => {
shortcuts.update(() => [])
onMessage(`Unregistered all shortcuts`)
})
.catch(onMessage)
}
</script>
<div class="flex gap-1">
<input
class="input grow"
placeholder="Type a shortcut with '+' as separator..."
bind:value={shortcut}
/>
<button class="btn" type="button" on:click={register}>Register</button>
</div>
<br />
<div class="flex flex-col gap-1">
{#each $shortcuts as savedShortcut}
<div class="flex justify-between">
{savedShortcut}
<button
class="btn"
type="button"
on:click={() => unregister(savedShortcut)}>Unregister</button
>
</div>
{/each}
{#if $shortcuts.length > 1}
<br />
<button class="btn" type="button" on:click={unregisterAll}
>Unregister all</button
>
{/if}
</div>
+76
View File
@@ -0,0 +1,76 @@
<script>
import { onMount, onDestroy } from 'svelte'
// This example show how updater events work when dialog is disabled.
// This allow you to use custom dialog for the updater.
// This is your responsibility to restart the application after you receive the STATUS: DONE.
import { checkUpdate, installUpdate } from '@tauri-apps/api/updater'
import { listen } from '@tauri-apps/api/event'
import { relaunch } from '@tauri-apps/api/process'
export let onMessage
let unlisten
onMount(async () => {
unlisten = await listen('tauri://update-status', onMessage)
})
onDestroy(() => {
if (unlisten) {
unlisten()
}
})
let isChecking, isInstalling, newUpdate
async function check() {
isChecking = true
try {
const { shouldUpdate, manifest } = await checkUpdate()
onMessage(`Should update: ${shouldUpdate}`)
onMessage(manifest)
newUpdate = shouldUpdate
} catch (e) {
onMessage(e)
} finally {
isChecking = false
}
}
async function install() {
isInstalling = true
try {
await installUpdate()
onMessage('Installation complete, restart required.')
await relaunch()
} catch (e) {
onMessage(e)
} finally {
isInstalling = false
}
}
</script>
<div class="flex children:grow children:h10">
{#if !isChecking && !newUpdate}
<button class="btn" on:click={check}>Check update</button>
{:else if !isInstalling && newUpdate}
<button class="btn" on:click={install}>Install update</button>
{:else}
<button
class="btn text-accentText dark:text-darkAccentText flex items-center justify-center"
><div class="spinner animate-spin" /></button
>
{/if}
</div>
<style>
.spinner {
height: 1.2rem;
width: 1.2rem;
border-radius: 50rem;
color: currentColor;
border: 2px dashed currentColor;
}
</style>
+56
View File
@@ -0,0 +1,56 @@
<script>
import { onMount, onDestroy } from 'svelte'
export let onMessage
const constraints = (window.constraints = {
audio: true,
video: true
})
function handleSuccess(stream) {
const video = document.querySelector('video')
const videoTracks = stream.getVideoTracks()
onMessage('Got stream with constraints:', constraints)
onMessage(`Using video device: ${videoTracks[0].label}`)
window.stream = stream // make variable available to browser console
video.srcObject = stream
}
function handleError(error) {
if (error.name === 'ConstraintNotSatisfiedError') {
const v = constraints.video
onMessage(
`The resolution ${v.width.exact}x${v.height.exact} px is not supported by your device.`
)
} else if (error.name === 'PermissionDeniedError') {
onMessage(
'Permissions have not been granted to use your camera and ' +
'microphone, you need to allow the page access to your devices in ' +
'order for the demo to work.'
)
}
onMessage(`getUserMedia error: ${error.name}`, error)
}
onMount(async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints)
handleSuccess(stream)
} catch (e) {
handleError(e)
}
})
onDestroy(() => {
window.stream.getTracks().forEach(function (track) {
track.stop()
})
})
</script>
<div class="flex flex-col gap-2">
<div class="note-red grow">Not available for Linux</div>
<video id="localVideo" autoplay playsinline>
<track kind="captions" />
</video>
</div>
+47
View File
@@ -0,0 +1,47 @@
<script>
import { getName, getVersion, getTauriVersion } from '@tauri-apps/api/app'
import { relaunch, exit } from '@tauri-apps/api/process'
let version = '0.0.0'
let tauriVersion = '0.0.0'
let appName = 'Unknown'
getName().then((n) => {
appName = n
})
getVersion().then((v) => {
version = v
})
getTauriVersion().then((v) => {
tauriVersion = v
})
async function closeApp() {
await exit()
}
async function relaunchApp() {
await relaunch()
}
</script>
<p>
This is a demo of Tauri's API capabilities using the <code
>@tauri-apps/api</code
> package. It's used as the main validation app, serving as the test bed of our
development process. In the future, this app will be used on Tauri's integration
tests.
</p>
<br />
<br />
<pre>
App name: <code>{appName}</code>
App version: <code>{version}</code>
Tauri version: <code>{tauriVersion}</code>
</pre>
<br />
<div class="flex flex-wrap gap-1 shadow-">
<button class="btn" on:click={closeApp}>Close application</button>
<button class="btn" on:click={relaunchApp}>Relaunch application</button>
</div>
+459
View File
@@ -0,0 +1,459 @@
<script>
import {
appWindow,
WebviewWindow,
LogicalSize,
UserAttentionType,
PhysicalSize,
PhysicalPosition
} from '@tauri-apps/api/window'
import { open as openDialog } from 'tauri-plugin-dialog-api'
import { open } from 'tauri-plugin-shell-api'
let selectedWindow = appWindow.label
const windowMap = {
[appWindow.label]: appWindow
}
const cursorIconOptions = [
'default',
'crosshair',
'hand',
'arrow',
'move',
'text',
'wait',
'help',
'progress',
// something cannot be done
'notAllowed',
'contextMenu',
'cell',
'verticalText',
'alias',
'copy',
'noDrop',
// something can be grabbed
'grab',
/// something is grabbed
'grabbing',
'allScroll',
'zoomIn',
'zoomOut',
// edge is to be moved
'eResize',
'nResize',
'neResize',
'nwResize',
'sResize',
'seResize',
'swResize',
'wResize',
'ewResize',
'nsResize',
'neswResize',
'nwseResize',
'colResize',
'rowResize'
]
export let onMessage
let newWindowLabel
let urlValue = 'https://tauri.app'
let resizable = true
let maximized = false
let decorations = true
let alwaysOnTop = false
let contentProtected = true
let fullscreen = false
let width = null
let height = null
let minWidth = null
let minHeight = null
let maxWidth = null
let maxHeight = null
let x = null
let y = null
let scaleFactor = 1
let innerPosition = new PhysicalPosition(x, y)
let outerPosition = new PhysicalPosition(x, y)
let innerSize = new PhysicalSize(width, height)
let outerSize = new PhysicalSize(width, height)
let resizeEventUnlisten
let moveEventUnlisten
let cursorGrab = false
let cursorVisible = true
let cursorX = null
let cursorY = null
let cursorIcon = 'default'
let cursorIgnoreEvents = false
let windowTitle = 'Awesome Tauri Example!'
function openUrl() {
open(urlValue)
}
function setTitle_() {
windowMap[selectedWindow].setTitle(windowTitle)
}
function hide_() {
windowMap[selectedWindow].hide()
setTimeout(windowMap[selectedWindow].show, 2000)
}
function minimize_() {
windowMap[selectedWindow].minimize()
setTimeout(windowMap[selectedWindow].unminimize, 2000)
}
function getIcon() {
openDialog({
multiple: false
}).then((path) => {
if (typeof path === 'string') {
windowMap[selectedWindow].setIcon(path)
}
})
}
function createWindow() {
if (!newWindowLabel) return
const webview = new WebviewWindow(newWindowLabel)
windowMap[newWindowLabel] = webview
webview.once('tauri://error', function () {
onMessage('Error creating new webview')
})
}
function loadWindowSize() {
windowMap[selectedWindow].innerSize().then((response) => {
innerSize = response
width = innerSize.width
height = innerSize.height
})
windowMap[selectedWindow].outerSize().then((response) => {
outerSize = response
})
}
function loadWindowPosition() {
windowMap[selectedWindow].innerPosition().then((response) => {
innerPosition = response
})
windowMap[selectedWindow].outerPosition().then((response) => {
outerPosition = response
x = outerPosition.x
y = outerPosition.y
})
}
async function addWindowEventListeners(window) {
if (!window) return
if (resizeEventUnlisten) {
resizeEventUnlisten()
}
if (moveEventUnlisten) {
moveEventUnlisten()
}
moveEventUnlisten = await window.listen('tauri://move', loadWindowPosition)
resizeEventUnlisten = await window.listen('tauri://resize', loadWindowSize)
}
async function requestUserAttention_() {
await windowMap[selectedWindow].minimize()
await windowMap[selectedWindow].requestUserAttention(
UserAttentionType.Critical
)
await new Promise((resolve) => setTimeout(resolve, 3000))
await windowMap[selectedWindow].requestUserAttention(null)
}
$: {
windowMap[selectedWindow]
loadWindowPosition()
loadWindowSize()
}
$: windowMap[selectedWindow]?.setResizable(resizable)
$: maximized
? windowMap[selectedWindow]?.maximize()
: windowMap[selectedWindow]?.unmaximize()
$: windowMap[selectedWindow]?.setDecorations(decorations)
$: windowMap[selectedWindow]?.setAlwaysOnTop(alwaysOnTop)
$: windowMap[selectedWindow]?.setContentProtected(contentProtected)
$: windowMap[selectedWindow]?.setFullscreen(fullscreen)
$: width &&
height &&
windowMap[selectedWindow]?.setSize(new PhysicalSize(width, height))
$: minWidth && minHeight
? windowMap[selectedWindow]?.setMinSize(
new LogicalSize(minWidth, minHeight)
)
: windowMap[selectedWindow]?.setMinSize(null)
$: maxWidth > 800 && maxHeight > 400
? windowMap[selectedWindow]?.setMaxSize(
new LogicalSize(maxWidth, maxHeight)
)
: windowMap[selectedWindow]?.setMaxSize(null)
$: x !== null &&
y !== null &&
windowMap[selectedWindow]?.setPosition(new PhysicalPosition(x, y))
$: windowMap[selectedWindow]
?.scaleFactor()
.then((factor) => (scaleFactor = factor))
$: addWindowEventListeners(windowMap[selectedWindow])
$: windowMap[selectedWindow]?.setCursorGrab(cursorGrab)
$: windowMap[selectedWindow]?.setCursorVisible(cursorVisible)
$: windowMap[selectedWindow]?.setCursorIcon(cursorIcon)
$: cursorX !== null &&
cursorY !== null &&
windowMap[selectedWindow]?.setCursorPosition(
new PhysicalPosition(cursorX, cursorY)
)
$: windowMap[selectedWindow]?.setIgnoreCursorEvents(cursorIgnoreEvents)
</script>
<div class="flex flex-col children:grow gap-2">
<div class="flex gap-1">
<input
class="input grow"
type="text"
placeholder="New Window label.."
bind:value={newWindowLabel}
/>
<button class="btn" on:click={createWindow}>New window</button>
</div>
<br />
{#if Object.keys(windowMap).length >= 1}
<span class="font-700 text-sm">Selected window:</span>
<select class="input" bind:value={selectedWindow}>
<option value="" disabled selected>Choose a window...</option>
{#each Object.keys(windowMap) as label}
<option value={label}>{label}</option>
{/each}
</select>
{/if}
{#if windowMap[selectedWindow]}
<br />
<div class="flex flex-wrap gap-2">
<button
class="btn"
title="Unminimizes after 2 seconds"
on:click={() => windowMap[selectedWindow].center()}
>
Center
</button>
<button
class="btn"
title="Unminimizes after 2 seconds"
on:click={minimize_}
>
Minimize
</button>
<button
class="btn"
title="Visible again after 2 seconds"
on:click={hide_}
>
Hide
</button>
<button class="btn" on:click={getIcon}> Change icon </button>
<button
class="btn"
on:click={requestUserAttention_}
title="Minimizes the window, requests attention for 3s and then resets it"
>Request attention</button
>
</div>
<br />
<div class="flex flex-wrap gap-2">
<label>
Maximized
<input type="checkbox" bind:checked={maximized} />
</label>
<label>
Resizable
<input type="checkbox" bind:checked={resizable} />
</label>
<label>
Has decorations
<input type="checkbox" bind:checked={decorations} />
</label>
<label>
Always on top
<input type="checkbox" bind:checked={alwaysOnTop} />
</label>
<label>
Content protected
<input type="checkbox" bind:checked={contentProtected} />
</label>
<label>
Fullscreen
<input type="checkbox" bind:checked={fullscreen} />
</label>
</div>
<br />
<div class="flex flex-row gap-2 flex-wrap">
<div class="flex children:grow flex-col">
<div>
X
<input class="input" type="number" bind:value={x} min="0" />
</div>
<div>
Y
<input class="input" type="number" bind:value={y} min="0" />
</div>
</div>
<div class="flex children:grow flex-col">
<div>
Width
<input class="input" type="number" bind:value={width} min="400" />
</div>
<div>
Height
<input class="input" type="number" bind:value={height} min="400" />
</div>
</div>
<div class="flex children:grow flex-col">
<div>
Min width
<input class="input" type="number" bind:value={minWidth} />
</div>
<div>
Min height
<input class="input" type="number" bind:value={minHeight} />
</div>
</div>
<div class="flex children:grow flex-col">
<div>
Max width
<input class="input" type="number" bind:value={maxWidth} min="800" />
</div>
<div>
Max height
<input class="input" type="number" bind:value={maxHeight} min="400" />
</div>
</div>
</div>
<br />
<div>
<div class="flex">
<div class="grow">
<div class="text-accent dark:text-darkAccent font-700">
Inner Size
</div>
<span>Width: {innerSize.width}</span>
<span>Height: {innerSize.height}</span>
</div>
<div class="grow">
<div class="text-accent dark:text-darkAccent font-700">
Outer Size
</div>
<span>Width: {outerSize.width}</span>
<span>Height: {outerSize.height}</span>
</div>
</div>
<div class="flex">
<div class="grow">
<div class="text-accent dark:text-darkAccent font-700">
Inner Logical Size
</div>
<span>Width: {innerSize.toLogical(scaleFactor).width}</span>
<span>Height: {innerSize.toLogical(scaleFactor).height}</span>
</div>
<div class="grow">
<div class="text-accent dark:text-darkAccent font-700">
Outer Logical Size
</div>
<span>Width: {outerSize.toLogical(scaleFactor).width}</span>
<span>Height: {outerSize.toLogical(scaleFactor).height}</span>
</div>
</div>
<div class="flex">
<div class="grow">
<div class="text-accent dark:text-darkAccent font-700">
Inner Position
</div>
<span>x: {innerPosition.x}</span>
<span>y: {innerPosition.y}</span>
</div>
<div class="grow">
<div class="text-accent dark:text-darkAccent font-700">
Outer Position
</div>
<span>x: {outerPosition.x}</span>
<span>y: {outerPosition.y}</span>
</div>
</div>
<div class="flex">
<div class="grow">
<div class="text-accent dark:text-darkAccent font-700">
Inner Logical Position
</div>
<span>x: {innerPosition.toLogical(scaleFactor).x}</span>
<span>y: {innerPosition.toLogical(scaleFactor).y}</span>
</div>
<div class="grow">
<div class="text-accent dark:text-darkAccent font-700">
Outer Logical Position
</div>
<span>x: {outerPosition.toLogical(scaleFactor).x}</span>
<span>y: {outerPosition.toLogical(scaleFactor).y}</span>
</div>
</div>
</div>
<br />
<h4 class="mb-2">Cursor</h4>
<div class="flex gap-2">
<label>
<input type="checkbox" bind:checked={cursorGrab} />
Grab
</label>
<label>
<input type="checkbox" bind:checked={cursorVisible} />
Visible
</label>
<label>
<input type="checkbox" bind:checked={cursorIgnoreEvents} />
Ignore events
</label>
</div>
<div class="flex gap-2">
<label>
Icon
<select class="input" bind:value={cursorIcon}>
{#each cursorIconOptions as kind}
<option value={kind}>{kind}</option>
{/each}
</select>
</label>
<label>
X position
<input class="input" type="number" bind:value={cursorX} />
</label>
<label>
Y position
<input class="input" type="number" bind:value={cursorY} />
</label>
</div>
<br />
<div class="flex flex-col gap-1">
<form class="flex gap-1" on:submit|preventDefault={setTitle_}>
<input class="input grow" id="title" bind:value={windowTitle} />
<button class="btn" type="submit">Set title</button>
</form>
<form class="flex gap-1" on:submit|preventDefault={openUrl}>
<input class="input grow" id="url" bind:value={urlValue} />
<button class="btn" id="open-url"> Open URL </button>
</form>
</div>
{/if}
</div>