chore(example): clean up and migrate to ts partially (#3492)

* chore(example): clean up and migrate to ts

* Add missing permissions

* Mirror line-height: 1.5 and remove h-*
This commit is contained in:
Tony
2026-07-16 19:47:52 +08:00
committed by GitHub
parent 40ae0a7fa0
commit d6e0b6bbb1
7 changed files with 224 additions and 188 deletions
@@ -15,6 +15,7 @@
]
},
"core:default",
"core:app:allow-set-app-theme",
"fs:default",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
+118 -97
View File
@@ -1,8 +1,16 @@
<script>
<script module lang="ts">
export type ViewProps = {
onMessage: (value: unknown) => void
}
</script>
<script lang="ts">
import { onMount, tick } from 'svelte'
import { writable } from 'svelte/store'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { MediaQuery } from 'svelte/reactivity'
import { getCurrentWindow, type Theme } from '@tauri-apps/api/window'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { setTheme } from '@tauri-apps/api/app'
import * as os from '@tauri-apps/plugin-os'
import Welcome from './views/Welcome.svelte'
@@ -148,79 +156,94 @@
component: Haptics,
icon: 'i-ph-vibrate'
}
]
].filter(Boolean) as {
label: string
component: typeof Haptics
icon: string
}[]
let selected = $state.raw(views[0])
let selected = views[0]
function select(view) {
selected = view
// dark/light themes
const preferDark = new MediaQuery('prefers-color-scheme: dark')
let theme = $state<Theme | 'auto'>(
(localStorage.getItem('theme') as Theme | null) || 'auto'
)
async function switchTheme() {
switch (theme) {
case 'dark':
theme = 'light'
break
case 'light':
theme = 'auto'
break
case 'auto':
theme = 'dark'
break
}
applyTheme()
}
// dark/light
let isDark = localStorage.getItem('theme') == 'dark'
onMount(() => {
applyTheme(isDark)
function applyTheme() {
const isDark = theme === 'auto' ? preferDark.current : theme === 'dark'
isDark
? document.documentElement.classList.add('dark')
: document.documentElement.classList.remove('dark')
setTheme(theme === 'auto' ? null : theme)
localStorage.setItem('theme', theme)
}
$effect(() => {
applyTheme()
})
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([])
let consoleTextEl
async function onMessage(value) {
const messages = writable<string[]>([])
let consoleTextEl: HTMLDivElement
// this function is renders HTML without sanitizing it so it's insecure
// we only use it with our own input data
async function insecureRenderHtml(html: string) {
messages.update((r) => [
...r,
{
html:
`<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> `
+ (typeof value === 'string' ? value : JSON.stringify(value, null, 1))
+ '</pre>'
}
`<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ${html}</pre>`
])
await tick()
if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight
consoleTextEl.scrollTop = consoleTextEl.scrollHeight
}
// this function renders HTML without sanitizing it so it's insecure
// we only use it with our own input data
async function insecureRenderHtml(html) {
messages.update((r) => [
...r,
{
html:
`<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> `
+ html
+ '</pre>'
}
])
await tick()
if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight
async function onMessage(value: unknown) {
const valueStr =
typeof value === 'string'
? value
: JSON.stringify(
value instanceof ArrayBuffer
? Array.from(new Uint8Array(value))
: value,
null,
1
)
insecureRenderHtml(valueStr)
}
function clear() {
messages.update(() => [])
}
let consoleEl, consoleH, cStartY
let minConsoleHeight = 50
function startResizingConsole(e) {
let consoleEl: HTMLDivElement
let consoleH = 0
let cStartY = 0
const minConsoleHeight = 50
function startResizingConsole(e: MouseEvent) {
cStartY = e.clientY
const styles = window.getComputedStyle(consoleEl)
consoleH = parseInt(styles.height, 10)
const moveHandler = (e) => {
const moveHandler = (e: MouseEvent) => {
const dy = e.clientY - cStartY
const newH = consoleH - dy
consoleEl.style.height = `${
newH < minConsoleHeight ? minConsoleHeight : newH
}px`
consoleEl.style.height = `${newH < minConsoleHeight ? minConsoleHeight : newH}px`
}
const upHandler = () => {
document.removeEventListener('mouseup', upHandler)
@@ -233,15 +256,16 @@
let isWindows = os.platform() === 'windows'
// mobile
let isSideBarOpen = false
let sidebar
let sidebarToggle
let isSideBarOpen = $state(false)
let sidebar: HTMLElement
let sidebarToggle: HTMLElement
let isDraggingSideBar = false
let draggingStartPosX = 0
let draggingEndPosX = 0
const clamp = (min, num, max) => Math.min(Math.max(num, min), max)
const clamp = (min: number, num: number, max: number) =>
Math.min(Math.max(num, min), max)
function toggleSidebar(sidebar, isSideBarOpen) {
function toggleSidebar() {
sidebar.style.setProperty(
'--translate-x',
`${isSideBarOpen ? '0' : '-18.75'}rem`
@@ -249,10 +273,9 @@
}
onMount(() => {
sidebar = document.querySelector('#sidebar')
sidebarToggle = document.querySelector('#sidebarToggle')
document.addEventListener('click', (e) => {
if (!(e.target instanceof Node)) return
if (sidebarToggle.contains(e.target)) {
isSideBarOpen = !isSideBarOpen
} else if (isSideBarOpen && !sidebar.contains(e.target)) {
@@ -261,6 +284,7 @@
})
document.addEventListener('touchstart', (e) => {
if (!(e.target instanceof Node)) return
if (sidebarToggle.contains(e.target)) return
const x = e.touches[0].clientX
@@ -292,22 +316,20 @@
})
})
$: {
const sidebar = document.querySelector('#sidebar')
if (sidebar) {
toggleSidebar(sidebar, isSideBarOpen)
}
}
$effect(() => {
toggleSidebar()
})
</script>
<!-- custom titlebar for Windows -->
{#if isWindows}
<TitleBar {appWindow} {isDark} {toggleDark} />
<TitleBar {appWindow} {theme} {switchTheme} />
{/if}
<!-- Sidebar toggle, only visible on small screens -->
<div
id="sidebarToggle"
bind:this={sidebarToggle}
class="z-2000 hidden lt-sm:flex justify-center absolute items-center w-8 h-8 rd-8
bg-accent dark:bg-darkAccent active:bg-accentDark dark:active:bg-darkAccentDark"
>
@@ -323,6 +345,7 @@
>
<aside
id="sidebar"
bind:this={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"
>
@@ -330,13 +353,16 @@
<img class="p-7" src="tauri_logo.png" alt="Tauri logo" />
</a>
{#if !isWindows}
<a href="##" class="nv justify-between h-8" on:click={toggleDark}>
{#if isDark}
Switch to Light mode
<div class="i-ph-sun"></div>
{:else}
<a href="##" class="nv justify-between" onclick={switchTheme}>
{#if theme === 'auto'}
Switch to Dark mode
<div class="i-ph-circle-half-fill"></div>
{:else if theme === 'dark'}
Switch to Light mode
<div class="i-ph-moon"></div>
{:else if theme === 'light'}
Switch to Auto mode
<div class="i-ph-sun"></div>
{/if}
</a>
<br />
@@ -345,7 +371,7 @@
{/if}
<a
class="nv justify-between h-8"
class="nv justify-between"
target="_blank"
href="https://tauri.app/v1/guides/"
>
@@ -353,7 +379,7 @@
<span class="i-codicon-link-external"></span>
</a>
<a
class="nv justify-between h-8"
class="nv justify-between"
target="_blank"
href="https://github.com/tauri-apps/tauri"
>
@@ -361,7 +387,7 @@
<span class="i-codicon-link-external"></span>
</a>
<a
class="nv justify-between h-8"
class="nv justify-between"
target="_blank"
href="https://github.com/tauri-apps/tauri/tree/dev/examples/api"
>
@@ -372,22 +398,20 @@
<div class="bg-white/5 h-2px"></div>
<br />
<div
class="flex flex-col overflow-y-auto children-h-10 children-flex-none gap-1"
class="flex flex-col overflow-y-auto 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"></div>
<p>{view.label}</p></a
>
{/if}
<a
href="##"
class="nv {selected === view ? 'nv_selected' : ''}"
onclick={() => {
selected = view
isSideBarOpen = false
}}
>
<div class="{view.icon} mr-2"></div>
<p>{view.label}</p></a
>
{/each}
</div>
</aside>
@@ -399,11 +423,7 @@
<h1>{selected.label}</h1>
<div class="overflow-y-auto">
<div class="mr-2">
<svelte:component
this={selected.component}
{onMessage}
{insecureRenderHtml}
/>
<selected.component {onMessage} />
</div>
</div>
</div>
@@ -413,9 +433,10 @@
id="console"
class="select-none h-15rem grid grid-rows-[2px_2rem_1fr] gap-1 overflow-hidden"
>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
on:mousedown={startResizingConsole}
role="button"
tabindex="0"
onmousedown={startResizingConsole}
class="bg-black/20 h-2px cursor-ns-resize"
></div>
<div class="flex justify-between items-center px-2">
@@ -426,7 +447,7 @@
hover:bg-hoverOverlay dark:hover:bg-darkHoverOverlay
active:bg-hoverOverlay/25 dark:active:bg-darkHoverOverlay/25
"
on:click={clear}
onclick={clear}
>
<div class="i-codicon-clear-all"></div>
</button>
@@ -435,8 +456,8 @@
bind:this={consoleTextEl}
class="px-2 overflow-y-auto all:font-mono code-block all:text-xs select-text mr-2"
>
{#each $messages as r}
{@html r.html}
{#each $messages as messageHtml}
{@html messageHtml}
{/each}
</div>
</div>
+4
View File
@@ -1,3 +1,7 @@
:root {
line-height: 1.5;
}
*:not(h1, h2, h3, h4, h5, h6) {
margin: 0;
padding: 0;
+13 -10
View File
@@ -1,13 +1,14 @@
<script lang="ts">
import type { Window } from '@tauri-apps/api/window'
import type { Theme, Window } from '@tauri-apps/api/window'
import { ask } from '@tauri-apps/plugin-dialog'
import { onMount } from 'svelte'
let {
appWindow,
toggleDark,
isDark
}: { appWindow: Window; toggleDark: () => void; isDark: boolean } = $props()
switchTheme,
theme
}: { appWindow: Window; switchTheme: () => void; theme: Theme | 'auto' } =
$props()
// Window controls
let isWindowMaximized = $state(false)
@@ -58,14 +59,16 @@
>
<button
aria-label="Toggle dark mode"
title={isDark ? 'Switch to Light mode' : 'Switch to Dark mode'}
title={theme ? 'Switch to Light mode' : 'Switch to Dark mode'}
class="border-none hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
onclick={toggleDark}
onclick={switchTheme}
>
{#if isDark}
<div class="i-ph-sun"></div>
{:else}
<div class="i-ph-moon"></div>
{#if theme === 'auto'}
<div title="Switch to Dark mode" class="i-ph-circle-half-fill"></div>
{:else if theme === 'dark'}
<div title="Switch to Light mode" class="i-ph-moon"></div>
{:else if theme === 'light'}
<div title="Switch to Auto mode" class="i-ph-sun"></div>
{/if}
</button>
<button
+1 -2
View File
@@ -5,7 +5,6 @@
import { readFile } from '@tauri-apps/plugin-fs'
export let onMessage
export let insecureRenderHtml
let text = 'clipboard message'
function writeText() {
@@ -41,7 +40,7 @@
const image = await clipboard.readImage()
arrayBufferToBase64(await image.rgba(), function (base64) {
const src = 'data:image/png;base64,' + base64
insecureRenderHtml('<img src="' + src + '"></img>')
onMessage('<img src="' + src + '"></img>')
})
return
} catch (_) {}
+84 -76
View File
@@ -1,131 +1,138 @@
<script>
import { open, save, confirm, message } from "@tauri-apps/plugin-dialog";
import { readFile } from "@tauri-apps/plugin-fs";
<script lang="ts">
import {
open,
save,
confirm,
message,
type PickerMode,
type FileAccessMode
} from '@tauri-apps/plugin-dialog'
import { readFile } from '@tauri-apps/plugin-fs'
import type { ViewProps } from '../App.svelte'
export let onMessage;
export let insecureRenderHtml;
let defaultPath = null;
let filter = null;
let multiple = false;
let directory = false;
let pickerMode = "document";
let fileAccessMode = "scoped";
let { onMessage }: ViewProps = $props()
function arrayBufferToBase64(buffer, callback) {
let defaultPath: string | undefined = $state()
let filter = $state('')
let multiple = $state(false)
let directory = $state(false)
let pickerMode: PickerMode = $state('document')
let fileAccessMode: FileAccessMode = $state('scoped')
function arrayBufferToBase64(
buffer: ArrayBuffer,
callback: (base64: string) => void
) {
var blob = new Blob([buffer], {
type: "application/octet-binary",
});
var reader = new FileReader();
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);
var dataurl = evt.target!.result as string
callback(dataurl.split(',')[1])
}
reader.readAsDataURL(blob)
}
async function prompt() {
confirm("Do you want to do something?")
.then((res) => onMessage(res ? "Yes" : "No"))
.catch(onMessage);
confirm('Do you want to do something?')
.then((res) => onMessage(res ? 'Yes' : 'No'))
.catch(onMessage)
}
async function promptCustom() {
confirm("Is Tauri awesome?", {
okLabel: "Absolutely",
cancelLabel: "Totally",
confirm('Is Tauri awesome?', {
okLabel: 'Absolutely',
cancelLabel: 'Totally'
})
.then((res) =>
onMessage(
res ? "Tauri is absolutely awesome" : "Tauri is totally awesome"
res ? 'Tauri is absolutely awesome' : 'Tauri is totally awesome'
)
)
.catch(onMessage);
.catch(onMessage)
}
async function msg() {
await message("Tauri is awesome!").then((res) => onMessage(res));
await message('Tauri is awesome!').then((res) => onMessage(res))
}
async function msgCustom(result) {
const buttons = { yes: "awesome", no: "amazing", cancel: "stunning" };
async function msgCustom() {
const buttons = { yes: 'awesome', no: 'amazing', cancel: 'stunning' }
await message(`Tauri is: `, { buttons })
.then((res) => onMessage(`Tauri is ${res}`))
.catch(onMessage);
.catch(onMessage)
}
async function openDialog() {
try {
const result = await open({
title: "My wonderful open dialog",
title: 'My wonderful open dialog',
defaultPath,
filters: filter
? [
{
name: "Tauri Example",
extensions: filter.split(",").map((f) => f.trim()),
},
name: 'Tauri Example',
extensions: filter.split(',').map((f) => f.trim())
}
]
: [],
multiple,
directory,
pickerMode,
fileAccessMode,
fileAccessMode
})
if (Array.isArray(result)) {
onMessage(result);
onMessage(result)
} else if (result === null) {
onMessage('user cancelled the selection')
} else {
const pathToRead = result;
const isFile = pathToRead.match(/\S+\.\S+$/g);
const pathToRead = result
const isFile = pathToRead.match(/\S+\.\S+$/g)
await readFile(pathToRead)
.then(function (res) {
if (isFile) {
if (
pathToRead.includes(".png") ||
pathToRead.includes(".jpg") ||
pathToRead.includes(".jpeg")
) {
arrayBufferToBase64(
new Uint8Array(res),
function (base64) {
const src = "data:image/png;base64," + base64;
insecureRenderHtml('<img src="' + src + '"></img>');
}
);
} else {
// Convert byte array to UTF-8 string
const decoder = new TextDecoder('utf-8');
const text = decoder.decode(new Uint8Array(res));
onMessage(text);
}
await readFile(pathToRead).then(function (res) {
if (isFile) {
if (
pathToRead.includes('.png')
|| pathToRead.includes('.jpg')
|| pathToRead.includes('.jpeg')
) {
arrayBufferToBase64(res.buffer, function (base64) {
const src = 'data:image/png;base64,' + base64
onMessage('<img src="' + src + '"></img>')
})
} else {
onMessage(res);
// Convert byte array to UTF-8 string
const decoder = new TextDecoder('utf-8')
const text = decoder.decode(res)
onMessage(text)
}
} else {
onMessage(res)
}
})
}
} catch(exception) {
} catch (exception) {
onMessage(exception)
}
}
function saveDialog() {
save({
title: "My wonderful save dialog",
title: 'My wonderful save dialog',
defaultPath,
filters: filter
? [
{
name: "Tauri Example",
extensions: filter.split(",").map((f) => f.trim()),
},
name: 'Tauri Example',
extensions: filter.split(',').map((f) => f.trim())
}
]
: [],
})
: []
})
.then(onMessage)
.catch(onMessage);
.catch(onMessage)
}
</script>
@@ -173,15 +180,16 @@
<br />
<div class="flex flex-wrap flex-col md:flex-row gap-2 children:flex-shrink-0">
<button class="btn" id="open-dialog" on:click={openDialog}>Open dialog</button>
<button class="btn" id="save-dialog" on:click={saveDialog}
<button class="btn" id="open-dialog" onclick={openDialog}>Open dialog</button>
<button class="btn" id="save-dialog" onclick={saveDialog}
>Open save dialog</button
>
<button class="btn" id="prompt-dialog" on:click={prompt}>Prompt</button>
<button class="btn" id="custom-prompt-dialog" on:click={promptCustom}
<button class="btn" id="prompt-dialog" onclick={prompt}>Prompt</button>
<button class="btn" id="custom-prompt-dialog" onclick={promptCustom}
>Prompt (custom)</button
>
<button class="btn" id="message-dialog" on:click={msg}>Message</button>
<button class="btn" id="message-dialog" on:click={msgCustom}>Message (custom)</button>
<button class="btn" id="message-dialog" onclick={msg}>Message</button>
<button class="btn" id="message-dialog" onclick={msgCustom}
>Message (custom)</button
>
</div>
+3 -3
View File
@@ -5,7 +5,7 @@
import { arrayBufferToBase64 } from '../lib/utils'
import { onDestroy, onMount } from 'svelte'
const { onMessage, insecureRenderHtml } = $props()
const { onMessage } = $props()
let path = $state('')
let img
@@ -118,12 +118,12 @@
new Uint8Array(response),
function (base64) {
const src = 'data:image/png;base64,' + base64
insecureRenderHtml('<img src="' + src + '"></img>')
onMessage('<img src="' + src + '"></img>')
}
)
} else {
const value = String.fromCharCode.apply(null, response)
insecureRenderHtml(
onMessage(
'<textarea id="file-response"></textarea><button id="file-save">Save</button>'
)
setTimeout(() => {